feat(slot-definitions): add create/edit form with validation
continuous-integration/drone/push Build is passing Details

Implement full CRUD form modal for slot definitions with:
- All fields: court, day, time, duration, capacity, date range, description
- Field-level validation with inline error messages
- API contract alignment (HH:MM:SS, 0=Monday format, optional valid_to)
- Overlap detection (409 handling)
- No end date checkbox
- Edit mode support
- Professional slate theme
- Loading states
- Delete functionality integrated

Form validates:
- Required fields
- Duration must be positive
- Capacity must be positive
- End date must be after start date
- Slot cannot extend past midnight
- Backend validation errors shown inline

Fully functional create/edit/delete flow ready for backend integration.
master
Guillermo Pages 1 month ago
parent a8ad9fed51
commit 478d43b44a

@ -0,0 +1,840 @@
{
"Menu": "Menu",
"Navigate your game": "Navigate your game",
"Login": "Login",
"Continue with SwissOID": "Continue with SwissOID",
"Authentication Required": "Authentication Required",
"Please sign in to continue": "Please sign in to continue",
"You'll be redirected to SwissOID to sign in securely": "You'll be redirected to SwissOID to sign in securely",
"Secure OAuth 2.0 / OpenID Connect via SwissOID": "Secure OAuth 2.0 / OpenID Connect via SwissOID",
"Secure OAuth 2.0 / OpenID Connect": "Secure OAuth 2.0 / OpenID Connect",
"Secure OAuth 2.0 / OpenID Connect • Join thousands of players": "Secure OAuth 2.0 / OpenID Connect • Join thousands of players",
"We don't see or store your password": "We don't see or store your password",
"By continuing, you agree to our": "By continuing, you agree to our",
"Terms": "Terms",
"and": "and",
"Privacy Policy": "Privacy Policy",
"Register": "Register",
"Dashboard": "Dashboard",
"Booking": "Booking",
"Settings": "Settings",
"Find Partners": "Find Partners",
"Player Lookup": "Player Lookup",
"Language": "Language",
"Search for players": "Search for players",
"Click name to view profile": "Click name to view profile",
"Click to view profile": "Click to view profile",
"Loading profile...": "Loading profile...",
"View Full Profile": "View Full Profile",
"Profile data not available": "Profile data not available",
"No recent matches": "No recent matches",
"Recent Form": "Recent Form",
"WhatsApp Share": "WhatsApp Share",
"Logout": "Logout",
"Powered by passion": "Powered by passion",
"© 2024 Playchoo. Because booking is just the start.": "© 2024 Playchoo. Because booking is just the start.",
"Made with ❤️ for players": "Made with ❤️ for players",
"Welcome to {brand}": "Welcome to {brand}",
"Because booking is just the start": "Because booking is just the start",
"The gorgeous booking system that makes players' lives sweet. Find partners instantly, share matches with a simple WhatsApp message, choose your team and position, and track your progress like never before.": "The gorgeous booking system that makes players' lives sweet. Find partners instantly, share matches with a simple WhatsApp message, choose your team and position, and track your progress like never before.",
"WhatsApp Integration": "WhatsApp Integration",
"Smart Partner Matching": "Smart Partner Matching",
"Level Tracking": "Level Tracking",
"Login with SwissOID": "Login with SwissOID",
"Create Account": "Create Account",
"Secure authentication powered by SwissOID • Join thousands of players": "Secure authentication powered by SwissOID • Join thousands of players",
"Secure authentication powered by SwissOID • Streamline your club management": "Secure authentication powered by SwissOID • Streamline your club management",
"Go to Dashboard": "Go to Dashboard",
"Welcome back! Ready to find your next match?": "Welcome back! Ready to find your next match?",
"Why Players Choose {brand}": "Why Players Choose {brand}",
"Every feature designed to make your game better, your connections stronger, and your experience unforgettable": "Every feature designed to make your game better, your connections stronger, and your experience unforgettable",
"Secure & Trusted": "Secure & Trusted",
"Protected by Swiss ID authentication. Your data is safe, your matches are verified, your peace of mind is guaranteed.": "Protected by Swiss ID authentication. Your data is safe, your matches are verified, your peace of mind is guaranteed.",
"WhatsApp Magic": "WhatsApp Magic",
"Share matches instantly with a simple, beautiful message. No apps to download, no complicated links - just pure simplicity.": "Share matches instantly with a simple, beautiful message. No apps to download, no complicated links - just pure simplicity.",
"Smart Matching": "Smart Matching",
"Find partners who match your skill level, schedule, and playing style. Quality connections, every time.": "Find partners who match your skill level, schedule, and playing style. Quality connections, every time.",
"Effortless Booking": "Effortless Booking",
"Book courts, organize teams, manage positions - all in one beautiful interface that just works.": "Book courts, organize teams, manage positions - all in one beautiful interface that just works.",
"Watch your skills grow with intelligent tracking. See your progress, celebrate improvements, reach new heights.": "Watch your skills grow with intelligent tracking. See your progress, celebrate improvements, reach new heights.",
"Made with Love": "Made with Love",
"Built by players, for players. Every pixel crafted with passion, every feature born from real needs.": "Built by players, for players. Every pixel crafted with passion, every feature born from real needs.",
"Happy Players": "Happy Players",
"Matches Organized": "Matches Organized",
"Player Rating": "Player Rating",
"Skill Assessment": "Skill Assessment",
"Answer questions to determine your skill level": "Answer questions to determine your skill level",
"Assessment not found or has been removed": "Assessment not found or has been removed",
"This assessment is private and cannot be viewed": "This assessment is private and cannot be viewed",
"Failed to load assessment result": "Failed to load assessment result",
"An error occurred while loading the assessment": "An error occurred while loading the assessment",
"My Badminton Assessment Result": "My Badminton Assessment Result",
"Check out my badminton skill assessment - Level {{level}}!": "Check out my badminton skill assessment - Level {{level}}!",
"Unable to Load Assessment": "Unable to Load Assessment",
"Take Your Own Assessment": "Take Your Own Assessment",
"No assessment data available": "No assessment data available",
"Assessment by": "Assessment by",
"Completed on": "Completed on",
"Want to test your own skills?": "Want to test your own skills?",
"Take the assessment and see how you compare!": "Take the assessment and see how you compare!",
"Start Your Assessment": "Start Your Assessment",
"Share Your Results": "Share Your Results",
"This assessment is locked and can be shared": "This assessment is locked and can be shared",
"Anonymous": "Anonymous",
"Share on WhatsApp": "Share on WhatsApp",
"Check out my badminton skill assessment": "Check out my badminton skill assessment",
"Unlock": "Unlock",
"Select Country Mode": "Select Country Mode",
"Skill Rating": "Skill Rating",
"What's my level?": "What's my level?",
"Start Assessment": "Start Assessment",
"Previous": "Previous",
"Next": "Next",
"Yes": "Yes",
"No": "No",
"Swipe or Tap": "Swipe or Tap",
"Submit": "Submit",
"Submitting...": "Submitting...",
"Saving...": "Saving...",
"Starting...": "Starting...",
"Loading": "Loading",
"Loading assessment...": "Loading assessment...",
"Question": "Question",
"Level": "Level",
"Points": "Points",
"Confidence": "Confidence",
"Answered": "Answered",
"Select your answer below": "Select your answer below",
"Assessment Complete!": "Assessment Complete!",
"Your Level": "Your Level",
"Total Points Earned": "Total Points Earned",
"Assessment Confidence": "Assessment Confidence",
"Performance Breakdown": "Performance Breakdown",
"Save": "Save",
"Save as My Skill Level": "Save as My Skill Level",
"Save your assessment result?": "Save your assessment result?",
"Your skill level will be updated to Level {{level}} on your profile": "Your skill level will be updated to Level {{level}} on your profile",
"Not now": "Not now",
"Retake Assessment": "Retake Assessment",
"Saved Successfully": "Saved Successfully",
"Assessment Progress": "Assessment Progress",
"By Category": "By Category",
"Quick Navigation": "Quick Navigation",
"Technical": "Technical",
"Tactical": "Tactical",
"Competition": "Competition",
"Technical Skills": "Technical Skills",
"Tactical Knowledge": "Tactical Knowledge",
"Competition Experience": "Competition Experience",
"Beginner": "Beginner",
"Novice": "Novice",
"Intermediate": "Intermediate",
"Competent": "Competent",
"Proficient": "Proficient",
"Advanced": "Advanced",
"Expert": "Expert",
"Elite": "Elite",
"Master": "Master",
"Never": "Never",
"Rarely": "Rarely",
"Sometimes": "Sometimes",
"Often": "Often",
"Always": "Always",
"Frequently": "Frequently",
"Technical and tactical questions only": "Technical and tactical questions only",
"Includes competition questions": "Includes competition questions",
"You have an assessment in progress": "You have an assessment in progress",
"Progress": "Progress",
"Switzerland": "Switzerland",
"France": "France",
"Ready to Transform Your Game?": "Ready to Transform Your Game?",
"Start Playing Today": "Start Playing Today",
"Username": "Username",
"Book Your Next Match": "Book Your Next Match",
"Password": "Password",
"Update Club Account": "Update Club Account",
"Connect Club Account": "Connect Club Account",
"Failed to save credentials": "Failed to save credentials",
"These credentials have already been claimed. You should only claim your own credentials. This is important to get an accurate level measurement.": "These credentials have already been claimed. You should only claim your own credentials. This is important to get an accurate level measurement.",
"Network error occurred": "Network error occurred",
"Refreshing...": "Refreshing...",
"Refreshing session...": "Refreshing session...",
"Cancel": "Cancel",
"Maybe later": "Maybe later",
"Confirm": "Confirm",
"Confirm Change": "Confirm Change",
"Try again": "Try again",
"Confirm Booking": "Confirm Booking",
"Who do you want to play with?": "Who do you want to play with?",
"Click below to start adding partners.": "Click below to start adding partners.",
"Add Partner": "Add Partner",
"Ready to confirm?": "Ready to confirm?",
"Click below to finalize your reservation.": "Click below to finalize your reservation.",
"Booking...": "Booking...",
"Almost done!": "Almost done!",
"Proceed to payment to complete your booking.": "Proceed to payment to complete your booking.",
"Go to Payment": "Go to Payment",
"In progress": "In progress",
"Players joined": "Players joined",
"No players yet": "No players yet",
"Ends in {time}": "Ends in {time}",
"Book": "Book",
"View": "View",
"Join request sent": "Join request sent",
"Failed to send request": "Failed to send request",
"Network error": "Network error",
"Request to join booking": "Request to join booking",
"Request to join this booking?": "Request to join this booking?",
"Available Courts": "Available Courts",
"Select a date to view available time slots": "Select a date to view available time slots",
"Live updates • Past slots hidden automatically": "Live updates • Past slots hidden automatically",
"Date:": "Date:",
"Timeline": "Timeline",
"Courts": "Courts",
"No courts available": "No courts available",
"Try selecting a different date or check back later.": "Try selecting a different date or check back later.",
"No slots available": "No slots available",
"Try selecting a different filter.": "Try selecting a different filter.",
"Report Score": "Report Score",
"Won": "Won",
"Lost": "Lost",
"Hide detailed score": "Hide detailed score",
"Add detailed score": "Add detailed score",
"Add set": "Add set",
"Success": "Success",
"Provide your club login so we can connect your account": "Provide your club login so we can connect your account",
"Loading...": "Loading...",
"Error Loading Dashboard": "Error Loading Dashboard",
"Your Dashboard": "Your Dashboard",
"Track your game, celebrate victories, and never miss a match": "Track your game, celebrate victories, and never miss a match",
"Matches Won": "Matches Won",
"Upcoming Games": "Upcoming Games",
"Win Streak": "Win Streak",
"Upcoming Matches": "Upcoming Matches",
"Get ready for your next games": "Get ready for your next games",
"Pending Scores": "Pending Scores",
"Report your match results": "Report your match results",
"Score Reporting": "Score Reporting",
"Track your past performances": "Track your past performances",
"Achievement Unlocked!": "Achievement Unlocked!",
"Legendary Champion": "Legendary Champion",
"Unstoppable Force": "Unstoppable Force",
"Win Streak Master": "Win Streak Master",
"Rising Star": "Rising Star",
"You've won {count} matches in a row.": "You've won {count} matches in a row.",
"You are absolutely unstoppable!": "You are absolutely unstoppable!",
"Your dominance is legendary!": "Your dominance is legendary!",
"Keep the momentum going!": "Keep the momentum going!",
"Great start to your winning streak!": "Great start to your winning streak!",
"Account Search": "Account Search",
"Find and manage accounts": "Find and manage accounts",
"Accounts": "Accounts",
"Sign out": "Sign out",
"Signing out...": "Signing out...",
"Search Accounts": "Search Accounts",
"Enter a name or account number to find accounts": "Enter a name or account number to find accounts",
"Search by name or account number...": "Search by name or account number...",
"Search": "Search",
"Searching...": "Searching...",
"Searching accounts...": "Searching accounts...",
"Found {count} accounts": "Found {count} accounts",
"Search completed": "Search completed",
"No accounts found matching your search.": "No accounts found matching your search.",
"Search failed. Please try again.": "Search failed. Please try again.",
"Network error. Please check your connection.": "Network error. Please check your connection.",
"No Results Found": "No Results Found",
"Try adjusting your search terms or check the spelling.": "Try adjusting your search terms or check the spelling.",
"Search Results": "Search Results",
"{count} accounts found": "{count} accounts found",
"Account ID": "Account ID",
"Welcome to Account Search": "Welcome to Account Search",
"Start typing in the search box above to find accounts by name or account number.": "Start typing in the search box above to find accounts by name or account number.",
"Results will appear instantly as you type.": "Results will appear instantly as you type.",
"Real-time search": "Real-time search",
"Instant results": "Instant results",
"Smart matching": "Smart matching",
"Failed to load bookings": "Failed to load bookings",
"Network error while loading bookings": "Network error while loading bookings",
"Today": "Today",
"Tomorrow": "Tomorrow",
"Yesterday": "Yesterday",
"Starting now": "Starting now",
"Next Match": "Next Match",
"Next {count} Matches": "Next {count} Matches",
"Ready to play? Here's what's coming up": "Ready to play? Here's what's coming up",
"Matches": "Matches",
"{count}/4 players": "{count}/4 players",
"View Match": "View Match",
"Want to play more?": "Want to play more?",
"Book Another Match": "Book Another Match",
"Court {court} • {duration} minutes": "Court {court} • {duration} minutes",
"Court {court}": "Court {court}",
"No match history yet": "No match history yet",
"Play some matches to see your history and track your progress!": "Play some matches to see your history and track your progress!",
"Book Your First Match": "Book Your First Match",
"View Details": "View Details",
"Showing recent matches": "Showing recent matches",
"View Full History": "View Full History",
"Confirmed": "Confirmed",
"Trusted": "Trusted",
"Waiting": "Waiting",
"Loading pending scores...": "Loading pending scores...",
"All": "All",
"All caught up!": "All caught up!",
"No pending scores to report right now.": "No pending scores to report right now.",
"Failed to submit": "Failed to submit",
"Failed": "Failed",
"Search and add players to your game": "Search and add players to your game",
"Searching players...": "Searching players...",
"Start typing to search": "Start typing to search",
"Enter at least 2 characters to find partners": "Enter at least 2 characters to find partners",
"No players found": "No players found",
"Try a different search term": "Try a different search term",
"Click to add to your game": "Click to add to your game",
"Find the perfect partners for your game": "Find the perfect partners for your game",
"No upcoming matches": "No upcoming matches",
"Your calendar is clear! Time to book your next exciting match.": "Your calendar is clear! Time to book your next exciting match.",
"Book a Match": "Book a Match",
"Close": "Close",
"Loading slot details...": "Loading slot details...",
"Failed to load slot": "Failed to load slot",
"Network error while loading slot": "Network error while loading slot",
"Book Your Court": "Book Your Court",
"No slot found": "No slot found",
"Available": "Available",
"Players": "Players",
"Go Back": "Go Back",
"Login Required": "Login Required",
"Please login to continue.": "Please login to continue.",
"Return to Home": "Return to Home",
"Login Failed": "Login Failed",
"No Token Provided": "No Token Provided",
"We couldn't find an authentication token. Please try logging in again.": "We couldn't find an authentication token. Please try logging in again.",
"Logging you in...": "Logging you in...",
"Please wait while we authenticate your session": "Please wait while we authenticate your session",
"Secure Authentication": "Secure Authentication",
"Redirecting to dashboard": "Redirecting to dashboard",
"Return to Login": "Return to Login",
"Need help? Contact support for assistance.": "Need help? Contact support for assistance.",
"Please wait": "Please wait",
"Secured by Playchoo Authentication System": "Secured by Playchoo Authentication System",
"Encrypted": "Encrypted",
"Secure": "Secure",
"Protected": "Protected",
"Signing you out...": "Signing you out...",
"Securing your session": "Securing your session",
"Successfully signed out!": "Successfully signed out!",
"Your session has been securely ended": "Your session has been securely ended",
"Redirecting...": "Redirecting...",
"Taking you back to the homepage": "Taking you back to the homepage",
"Thanks for using Playchoo": "Thanks for using Playchoo",
"See you on the court! 🎾": "See you on the court! 🎾",
"Click to select": "Click to select",
"No clubs available": "No clubs available",
"Please contact support if you believe this is an error.": "Please contact support if you believe this is an error.",
"No sports available": "No sports available",
"This club doesn't have any sports configured yet.": "This club doesn't have any sports configured yet.",
"Loading clubs...": "Loading clubs...",
"Choose Your Sport": "Choose Your Sport",
"Select Your Club": "Select Your Club",
"What sport do you play at {club}?": "What sport do you play at {club}?",
"Choose the club where you play": "Choose the club where you play",
"Failed to load clubs": "Failed to load clubs",
"Failed to save preferences": "Failed to save preferences",
"Select to continue": "Select to continue",
"Back to Clubs": "Back to Clubs",
"Select Club": "Select Club",
"Default Club & Sport": "Default Club & Sport",
"Your preferred club and sport for seamless bookings": "Your preferred club and sport for seamless bookings",
"Edit": "Edit",
"Club Connection": "Club Connection",
"Your connection status with the club management system": "Your connection status with the club management system",
"Connection Status": "Connection Status",
"Not claimed": "Not claimed",
"Contact your club to link your account for seamless booking": "Contact your club to link your account for seamless booking",
"Other Linked Accounts": "Other Linked Accounts",
"Choose your preferred club and sport": "Choose your preferred club and sport",
"Update Default Club & Sport": "Update Default Club & Sport",
"Customize your experience and manage your account preferences with ease": "Customize your experience and manage your account preferences with ease",
"Reservation created! Proceed to payment.": "Reservation created! Proceed to payment.",
"Booking confirmed!": "Booking confirmed!",
"Failed to book slot": "Failed to book slot",
"Booking failed": "Booking failed",
"Processing your booking...": "Processing your booking...",
"Reconnecting to booking service...": "Reconnecting to booking service...",
"Booking Details": "Booking Details",
"This slot is already booked": "This slot is already booked",
"Redirecting you to view the booking details...": "Redirecting you to view the booking details...",
"Failed to load booking": "Failed to load booking",
"Network error while loading booking": "Network error while loading booking",
"Invalid booking data received from server": "Invalid booking data received from server",
"Invalid booking date format": "Invalid booking date format",
"No booking found": "No booking found",
"Pending Position Requests": "Pending Position Requests",
"Players requesting to swap positions with you": "Players requesting to swap positions with you",
"Cancelling...": "Cancelling...",
"Approving...": "Approving...",
"Rejecting...": "Rejecting...",
"Approve": "Approve",
"Reject": "Reject",
"Request cancelled": "Request cancelled",
"Failed to cancel request": "Failed to cancel request",
"Request rejected": "Request rejected",
"Failed to reject request": "Failed to reject request",
"Request approved and positions swapped!": "Request approved and positions swapped!",
"Failed to approve request": "Failed to approve request",
"Pending Join Requests": "Pending Join Requests",
"Players requesting to join your booking": "Players requesting to join your booking",
"Waiting for approval": "Waiting for approval",
"Seat requested": "Seat requested",
"You already asked for that seat.": "You already asked for that seat.",
"Slot full": "Slot full",
"Position taken": "Position taken",
"Request expired": "Request expired",
"Accepted slot updated.": "Accepted slot updated.",
"Red Team": "Red Team",
"Blue Team": "Blue Team",
"You": "You",
"Sport": "Sport",
"Club information not available": "Club information not available",
"Failed to change position": "Failed to change position",
"Side": "Side",
"Left": "Left",
"Right": "Right",
"Full": "Full",
"Booked": "Booked",
"Drag your avatar to change positions. Other players will need to approve swaps.": "Drag your avatar to change positions. Other players will need to approve swaps.",
"and": "and",
"to {request}": "to {request}",
"move to the {side} side": "move to the {side} side",
"switch to the {team} team": "switch to the {team} team",
"move you to the {side} side": "move you to the {side} side",
"switch you to the {team} team": "switch you to the {team} team",
"{requesterName} wants{requestLabel} with you": "{requesterName} wants{requestLabel} with you",
"{requesterName} wants{requestLabel} with {approverName}": "{requesterName} wants{requestLabel} with {approverName}",
"You would {effect}": "You would {effect}",
"This would {effect}": "This would {effect}",
"{requesterName} would {effect}": "{requesterName} would {effect}",
"Match closed header": "🎾 Match closed 🎾",
"Match open header": "🎾 Match open 🎾",
"Urgent": "URGENT",
"{duration} min": "{duration} min",
"{count} courts • {available} available": "{count} courts • {available} available",
"{count} slots": "{count} slots",
"{count} available": "{count} available",
"Failed to load slots": "Failed to load slots",
"Network error while loading slots": "Network error while loading slots",
"Error": "Error",
"Visible Slots": "Visible Slots",
"Total Slots": "Total Slots",
"Pending": "Pending",
"{count} past slots hidden automatically": "{count} past slots hidden automatically",
"Authenticating": "Authenticating",
"Authentication Failed": "Authentication Failed",
"Authentication successful. Redirecting to your dashboard...": "Authentication successful. Redirecting to your dashboard...",
"Invalid Token": "Invalid Token",
"The authentication token is missing or invalid.": "The authentication token is missing or invalid.",
"Verifying your access token...": "Verifying your access token...",
"Login failed": "Login failed",
"No players assigned": "No players assigned",
"Welcome Back!": "Welcome Back!",
"Welcome!": "Welcome!",
"Club": "Club",
"Select Account": "Select Account",
"vs": "vs",
"Disputed": "Disputed",
"Needs Review": "Needs Review",
"Review": "Review",
"Resolve Dispute": "Resolve Dispute",
"Accept": "Accept",
"Detailed Score": "Detailed Score",
"Dispute Resolution": "Dispute Resolution",
"Match Score": "Match Score",
"Match Results - Dispute Resolution": "Match Results - Dispute Resolution",
"Match Results": "Match Results",
"Confirm Score": "Confirm Score",
"Accept Score": "Accept Score",
"Reported by": "Reported by",
"Accept This Score": "Accept This Score",
"No sets were played": "No sets were played",
"You dominated from start to finish": "You dominated from start to finish",
"The start was rough but you managed to overtake at the end": "The start was rough but you managed to overtake at the end",
"You got ahead early and held your nerve to seal the victory": "You got ahead early and held your nerve to seal the victory",
"They dominated from start to finish": "They dominated from start to finish",
"Great start, but things slipped away in the end": "Great start, but things slipped away in the end",
"It was a tight battle, but they pulled through when it mattered": "It was a tight battle, but they pulled through when it mattered",
"All Matches": "All Matches",
"Needs Score": "Needs Score",
"Completed": "Completed",
"No matches pending score submission": "No matches pending score submission",
"No completed matches": "No completed matches",
"No matches found": "No matches found",
"Set {number}": "Set {number}",
"Report Match Score": "Report Match Score",
"Tell us how the match went": "Tell us how the match went",
"Your Team": "Your Team",
"(You)": "(You)",
"Opponents": "Opponents",
"Match Result": "Match Result",
"Quick Report": "Quick Report",
"Detailed Report": "Detailed Report",
"or": "or",
"We Won!": "We Won!",
"We Lost": "We Lost",
"Hide set scores": "Hide set scores",
"Add set scores": "Add set scores",
"Us": "Us",
"Them": "Them",
"Add another set": "Add another set",
"Please add at least 2 sets for detailed score": "Please add at least 2 sets for detailed score",
"Submit Report": "Submit Report",
"(RED)": "(RED)",
"(BLUE)": "(BLUE)",
"RED TEAM (You)": "RED TEAM (You)",
"RED": "Red",
"BLUE": "Blue",
"Sets show loss": "Sets show loss",
"Sets show win": "Sets show win",
"Force swap": "Force swap",
"Use if you know the other person approves": "Use if you know the other person approves",
"Failed to submit score": "Failed to submit score",
"available": "available",
"pending": "pending",
"booked": "booked",
"coach": "coach",
"club": "club",
"Share on WhatsApp": "Share on WhatsApp",
"{duration}m": "{duration}m",
"{min}m left": "{min}m left",
"Compact": "Compact",
"Player Profile": "Player Profile",
"Player Information": "Player Information",
"Sport Information": "Sport Information",
"Reliability": "Reliability",
"Match Statistics": "Match Statistics",
"Recent Matches": "Recent Matches",
"Member Since": "Member Since",
"Skill Level": "Skill Level",
"Total Gradings": "Total Gradings",
"Reliability Score": "Reliability Score",
"Reporting Rate": "Reporting Rate",
"Consensus Rate": "Consensus Rate",
"Matches Played": "Matches Played",
"Matches Reported": "Matches Reported",
"In Consensus": "In Consensus",
"Score Not Reported": "Score Not Reported",
"Please report the match score": "Please report the match score",
"Player Score Reports": "Player Score Reports",
"Unknown Player": "Unknown Player",
"Reported": "Reported",
"Multiple score reports detected. The match result will be confirmed once all players agree.": "Multiple score reports detected. The match result will be confirmed once all players agree.",
"Total Matches": "Total Matches",
"Wins": "Wins",
"Losses": "Losses",
"Win Rate": "Win Rate",
"Longest Win Streak": "Longest Win Streak",
"matches in last 30 days": "matches in last 30 days",
"matches in last 90 days": "matches in last 90 days",
"Favorite Venue": "Favorite Venue",
"No data available": "No data available",
"Failed to load profile": "Failed to load profile",
"Slot information not loaded yet": "Slot information not loaded yet",
"Inactive": "Inactive",
"Self-Assessment": "Self-Assessment",
"Player's self-reported skill level (1-7)": "Player's self-reported skill level (1-7)",
"Competitive Rating": "Competitive Rating",
"Glicko-2 rating based on match results": "Glicko-2 rating based on match results",
"Rating": "Rating",
"Ranked Matches": "Ranked Matches",
"Competitive games": "Competitive games",
"Rating Confidence": "Rating Confidence",
"High": "High",
"Medium": "Medium",
"Low": "Low",
"No rating data available yet": "No rating data available yet",
"Play ranked matches to build your rating": "Play ranked matches to build your rating",
"Loss Streak": "Loss Streak",
"Elite Winner": "Elite Winner",
"Streak Master": "Streak Master",
"Centurion": "Centurion",
"Experienced": "Experienced",
"Create Your Profile": "Create Your Profile",
"Challenge Player": "Challenge Player",
"Frequent Players": "Frequent Players",
"No playing history yet": "No playing history yet",
"Play more matches to build your network": "Play more matches to build your network",
"Excellent": "Excellent",
"Good": "Good",
"Fair": "Fair",
"Needs Improvement": "Needs Improvement",
"Various venues": "Various venues",
"Last 30 days": "Last 30 days",
"Last 90 days": "Last 90 days",
"/week avg": "/week avg",
"Home Court": "Home Court",
"On-time arrival": "On-time arrival",
"Total bookings": "Total bookings",
"No-shows": "No-shows",
"Unique Players": "Unique Players",
"Most Played With": "Most Played With",
"Best Streak": "Best Streak",
"consecutive wins": "consecutive wins",
"Current Streak": "Current Streak",
"wins in a row": "wins in a row",
"losses in a row": "losses in a row",
"no active streak": "no active streak",
"matches": "matches",
"match": "match",
"W": "W",
"L": "L",
"Trusted Player": "Trusted Player",
"Avg response": "Avg response",
"Last {count} matches": "Last {count} matches",
"No recent matches to display": "No recent matches to display",
"View all {count} matches →": "View all {count} matches →",
"Join thousands of players who've already discovered the joy of effortless booking and perfect matches.": "Join thousands of players who've already discovered the joy of effortless booking and perfect matches.",
"Profile": "Profile",
"Ready to play? Here's what's coming up.": "Ready to play? Here's what's coming up.",
"This club doesn't support this sport": "This club doesn't support this sport",
"We couldn't find that booking": "We couldn't find that booking",
"Your request{requestLabel} is awaiting {approverName}'s approval": "Your request{requestLabel} is awaiting {approverName}'s approval",
"position": "position",
"Join Match": "Join Match",
"Do you want to request to join this match?": "Do you want to request to join this match?",
"Court": "Court",
"Time": "Time",
"Team": "Team",
"Position": "Position",
"Red": "Red",
"Blue": "Blue",
"Match Tiebreak": "Match Tiebreak",
"Match TB": "Match TB",
"Tiebreak Points": "Tiebreak Points",
"Tiebreak (7-6 or 6-7)": "Tiebreak (7-6 or 6-7)",
"Add Set": "Add Set",
"Active": "Active",
"Assessment Locked": "Assessment Locked",
"Back to Home": "Back to Home",
"Booker": "Booker",
"Check out my padel skill assessment results!": "Check out my padel skill assessment results!",
"Collapse": "Collapse",
"Continue Assessment": "Continue Assessment",
"Copied!": "Copied!",
"Copy": "Copy",
"Done": "Done",
"Easy Scheduling": "Easy Scheduling",
"My Padel Assessment Results": "My Padel Assessment Results",
"No level data": "No level data",
"No match history available": "No match history available",
"Share": "Share",
"Share Link": "Share Link",
"Share your results": "Share your results",
"View Profile": "View Profile",
"View Results": "View Results",
"Your assessment is now locked and can be shared. The results cannot be modified.": "Your assessment is now locked and can be shared. The results cannot be modified.",
"Review Answers": "Review Answers",
"Session Locked": "Session Locked",
"This assessment is locked and read-only": "This assessment is locked and read-only",
"Unlock to Edit": "Unlock to Edit",
"Unlocking...": "Unlocking...",
"Play With the Right People.": "Play With the Right People.",
"Every Time.": "Every Time.",
"No more wrong-level matches. Book smarter, play more, and see your progress at a glance.": "No more wrong-level matches. Book smarter, play more, and see your progress at a glance.",
"Always matched at your level": "Always matched at your level",
"Book in seconds": "Book in seconds",
"Track your progress": "Track your progress",
"One-tap invites": "One-tap invites",
"See How It Works": "Sign In to See More",
"Find Your Perfect Match": "Sign Up Now",
"The wrong match kills the fun.": "The wrong match kills the fun.",
"Playing above your level": "Playing above your level",
"= frustration": "= frustration",
"Playing below your level": "Playing below your level",
"= boredom": "= boredom",
"Organizing games": "Organizing games",
"= endless chat chaos": "= endless chat chaos",
"Playchoo fixes this:": "Playchoo fixes this:",
"Always matched with players at your level": "Always matched with players at your level",
"Book in seconds with live availability": "Book in seconds with live availability",
"Track your progress at a glance": "Track your progress at a glance",
"Everything You Need to Play Better": "Everything You Need to Play Better",
"Smart features that work together to transform your game": "Smart features that work together to transform your game",
"Perfect Matchmaking": "Perfect Matchmaking",
"Smart level system: start with your own estimate, then Playchoo adjusts automatically as you play.": "Smart level system: start with your own estimate, then Playchoo adjusts automatically as you play.",
"Always fair, always fun": "Always fair, always fun",
"Save time with partner suggestions": "Save time with partner suggestions",
"See real-time court availability and book instantly.": "See real-time court availability and book instantly.",
"Drag-and-drop players onto the court": "Drag-and-drop players onto the court",
"Timeline and calendar views": "Timeline and calendar views",
"Works for padel, tennis, and more": "Works for padel, tennis, and more",
"Progress That Motivates": "Progress That Motivates",
"See your progress at a glance with stats, streaks, and head-to-heads.": "See your progress at a glance with stats, streaks, and head-to-heads.",
"Earn achievements as you play": "Earn achievements as you play",
"Personal player card brings your game to life": "Personal player card brings your game to life",
"Easy Sharing": "Easy Sharing",
"One-tap WhatsApp invites with all details included.": "One-tap WhatsApp invites with all details included.",
"No app required for your friends": "No app required for your friends",
"Beautiful formatted messages": "Beautiful formatted messages",
"Real-Time Updates": "Real-Time Updates",
"Live court availability, instant confirmations, and automatic conflict detection. Everything updates in real-time.": "Live court availability, instant confirmations, and automatic conflict detection. Everything updates in real-time.",
"Club Ready": "Club Ready",
"Multi-club support, timezone handling, and professional management tools. Perfect for players and clubs alike.": "Multi-club support, timezone handling, and professional management tools. Perfect for players and clubs alike.",
"Simplify Club Management": "Simplify Club Management",
"Professional tools that save hours every week": "Professional tools that save hours every week",
"Multi-Club Support": "Multi-Club Support",
"Manage multiple clubs and locations from one account": "Manage multiple clubs and locations from one account",
"Automatic Scheduling": "Automatic Scheduling",
"Smart timezone handling and conflict detection": "Smart timezone handling and conflict detection",
"Member Management": "Member Management",
"Remote access and credentials for all members": "Remote access and credentials for all members",
"Bring Playchoo to Your Club": "Sign Up Your Club",
"Players and clubs are loving it": "Players and clubs are loving it",
"Finally, I always play at my level — and every match is fun again.": "Finally, I always play at my level — and every match is fun again.",
"The app shows me how I've improved without me even thinking about it.": "The app shows me how I've improved without me even thinking about it.",
"Sarah M., Padel Player": "Sarah M., Padel Player",
"Mike R., Tennis Player": "Mike R., Tennis Player",
"Never play the wrong match again.": "Never play the wrong match again.",
"Book smarter, play at your level, and see your progress at a glance.": "Book smarter, play at your level, and see your progress at a glance.",
"Get Early Access Free": "Sign Up Free",
"Know Your Level": "Know Your Level",
"Curious about your skill level?": "Curious about your skill level?",
"Take the quick test!": "Take the quick test!",
"Get matched with players at your exact level": "Get matched with players at your exact level",
"Share your verified level with your community": "Share your verified level with your community",
"Track your improvement over time": "Track your improvement over time",
"Discover Your Level": "Discover Your Level",
"Takes only 2 minutes": "Takes only 2 minutes",
"Perfect matches guaranteed": "Perfect matches guaranteed",
"Join 1000+ rated players": "Join 1000+ rated players",
"Results saved to your profile • Retake anytime to track progress": "Results saved to your profile • Retake anytime to track progress",
"Theme": "Theme",
"Color Scheme": "Color Scheme",
"Choose your preferred color theme": "Choose your preferred color theme",
"Personalize the appearance of your app": "Personalize the appearance of your app",
"Match Type": "Match Type",
"Ranked": "Ranked",
"Friendly": "Friendly",
"Training": "Training",
"Score required": "Score required",
"Change": "Change",
"Change Match Type": "Change Match Type",
"Current Match Type": "Current Match Type",
"Pending Request": "Pending Request",
"Your Request": "Your Request",
"Change to": "Change to",
"Force Change": "Force Change",
"Pending Requests": "Pending Requests",
"Change To": "Change To",
"Force change without approval": "Force change without approval",
"Skip team approval and apply change immediately": "Skip team approval and apply change immediately",
"Changing...": "Changing...",
"Request Change": "Request Change",
"Match type changed successfully": "Match type changed successfully",
"Match type change request sent": "Match type change request sent",
"Failed to change match type": "Failed to change match type",
"Network error while changing match type": "Network error while changing match type",
"{approved} approved, {pending} pending": "{approved} approved, {pending} pending",
"Approve": "Approve",
"Reject": "Reject",
"Rejected": "Rejected",
"You Approved": "You Approved",
"You Rejected": "You Rejected",
"Cancel Request": "Cancel Request",
"Force Override": "Force Override",
"Force": "Force",
"Request": "Request",
"Forced": "Forced",
"All approvals completed": "All approvals completed",
"Player": "Player",
"Match type request approved": "Match type request approved",
"Failed to approve request": "Failed to approve request",
"Network error while approving request": "Network error while approving request",
"Match type request rejected": "Match type request rejected",
"Failed to reject request": "Failed to reject request",
"Network error while rejecting request": "Network error while rejecting request",
"Unknown Match Type": "Unknown Match Type",
"Cannot change match type for past bookings": "Cannot change match type for past bookings",
"Only players in this booking can change the match type": "Only players in this booking can change the match type",
"Cannot change match type after scores have been submitted": "Cannot change match type after scores have been submitted",
"Match type cannot be changed": "Match type cannot be changed",
"Waiting for approval from": "Waiting for approval from",
"Waiting for approval from these players": "Waiting for approval from these players",
"Approval pending from": "Approval pending from",
"Approval pending from these players": "Approval pending from these players",
"Punctuality Report": "Punctuality Report",
"Punctuality Reports": "Punctuality Reports",
"Reports updated successfully": "Reports updated successfully",
"Reports submitted successfully": "Reports submitted successfully",
"Failed to submit reports": "Failed to submit reports",
"Network error while submitting reports": "Network error while submitting reports",
"You have reached the maximum number of updates for this match": "You have reached the maximum number of updates for this match",
"Update {count}/5": "Update {count}/5",
"You can update your punctuality reports up to 5 times": "You can update your punctuality reports up to 5 times",
"Report how punctual each player was for this match (0 = on time)": "Report how punctual each player was for this match (0 = on time)",
"Consensus: {minutes} min late": "Consensus: {minutes} min late",
"Disputed": "Disputed",
"Pending reports": "Pending reports",
"min": "min",
"Late": "Late",
"Update Reports": "Update Reports",
"Submit Reports": "Submit Reports",
"Submitting...": "Submitting...",
"Anyone late to the match?": "Anyone late to the match?",
"Update punctuality report": "Update punctuality report",
"Update Punctuality Report": "Update Punctuality Report",
"You reported late arrivals": "You reported late arrivals",
"You reported everyone on time": "You reported everyone on time",
"{count} updates remaining": "{count} updates remaining",
"Select any players who were late to the match": "Select any players who were late to the match",
"Set how many minutes late each player was": "Set how many minutes late each player was",
"No players selected - everyone was on time!": "No players selected - everyone was on time!",
"Update Report": "Update Report",
"Submit Report": "Submit Report",
"1 minute late": "1 minute late",
"{minutes} minutes late": "{minutes} minutes late",
"Streak": "Streak",
"Session Expiring": "Session Expiring",
"Your session will expire in {minutes} minutes.": "Your session will expire in {minutes} minutes.",
"Your session will expire in less than a minute.": "Your session will expire in less than a minute.",
"Stay logged in": "Stay logged in",
"Refreshing...": "Refreshing...",
"Pending Position Swaps": "Pending Position Swaps",
"Multi-player position changes requiring approval": "Multi-player position changes requiring approval",
"Your swap request": "Your swap request",
"{initiator} initiated a swap": "{initiator} initiated a swap",
"{pending} of {total} approvals pending": "{pending} of {total} approvals pending",
"Cancel Swap": "Cancel Swap",
"Approved": "Approved",
"Auto-approved": "Auto-approved",
"Position 1 (Blue, Left)": "Position 1 (Blue, Left)",
"Position 2 (Blue, Right)": "Position 2 (Blue, Right)",
"Position 3 (Red, Left)": "Position 3 (Red, Left)",
"Position 4 (Red, Right)": "Position 4 (Red, Right)",
"Your approval is required for this swap to execute": "Your approval is required for this swap to execute",
"Waiting for all players to approve": "Waiting for all players to approve",
"All players must approve before positions change": "All players must approve before positions change",
"Swap approved successfully": "Swap approved successfully",
"Failed to approve swap": "Failed to approve swap",
"Network error while approving swap": "Network error while approving swap",
"Swap cancelled successfully": "Swap cancelled successfully",
"Failed to cancel swap": "Failed to cancel swap",
"Network error while cancelling swap": "Network error while cancelling swap",
"You already have a pending request for this match type. Please wait for approval or cancel your current request.": "You already have a pending request for this match type. Please wait for approval or cancel your current request.",
"Network error while cancelling request": "Network error while cancelling request",
"Request approved": "Request approved",
"Failed to force change": "Failed to force change",
"Network error while forcing change": "Network error while forcing change",
"Error": "Error",
"Dismiss": "Dismiss",
"Your Approval Needed": "Your Approval Needed",
"Position swap requires your approval": "Position swap requires your approval",
"of": "of",
"pending": "pending",
"Your position change": "Your position change",
"Show details": "Show details",
"Hide details": "Hide details",
"swap request": "swap request",
"Waiting for": "Waiting for",
"approval(s)": "approval(s)",
"All approved": "All approved"
}

@ -0,0 +1,376 @@
'use client';
import { useState, useEffect } from 'react';
import { X, AlertCircle, Loader2 } from 'lucide-react';
import type { SlotDefinition, SlotDefinitionRequest, DayOfWeek, ValidationError } from '@/src/types/slot-definitions';
import { DAY_NAMES, formatTimeForAPI } from '@/src/types/slot-definitions';
import { createSlotDefinition, updateSlotDefinition } from '@/src/lib/api/slot-definitions';
interface SlotDefinitionFormProps {
clubId: number;
courts: { court_id: number; name: string }[];
definition?: SlotDefinition;
onClose: () => void;
onSuccess: () => void;
}
export default function SlotDefinitionForm({
clubId,
courts,
definition,
onClose,
onSuccess,
}: SlotDefinitionFormProps) {
const isEditing = !!definition;
// Form state
const [courtId, setCourtId] = useState(definition?.court_id || (courts[0]?.court_id || 0));
const [dow, setDow] = useState<DayOfWeek>(definition?.dow || 0);
const [startsAt, setStartsAt] = useState(definition ? definition.starts_at.substring(0, 5) : '09:00');
const [durationMinutes, setDurationMinutes] = useState(definition?.duration_minutes || 90);
const [capacity, setCapacity] = useState(definition?.capacity || 4);
const [validFrom, setValidFrom] = useState(definition?.valid_from || getTodayString());
const [validTo, setValidTo] = useState(definition?.valid_to || '');
const [noEndDate, setNoEndDate] = useState(!definition?.valid_to);
const [ruleDescription, setRuleDescription] = useState(definition?.rule?.description || '');
// UI state
const [loading, setLoading] = useState(false);
const [errors, setErrors] = useState<Record<string, string>>({});
const [generalError, setGeneralError] = useState('');
useEffect(() => {
if (noEndDate) {
setValidTo('');
}
}, [noEndDate]);
function getTodayString(): string {
const today = new Date();
return today.toISOString().split('T')[0];
}
function validateForm(): boolean {
const newErrors: Record<string, string> = {};
if (!courtId) {
newErrors.court_id = 'Please select a court';
}
if (!startsAt) {
newErrors.starts_at = 'Start time is required';
}
if (durationMinutes <= 0) {
newErrors.duration_minutes = 'Duration must be greater than 0';
}
if (capacity <= 0) {
newErrors.capacity = 'Capacity must be greater than 0';
}
if (!validFrom) {
newErrors.valid_from = 'Start date is required';
}
if (validTo && validFrom && validTo < validFrom) {
newErrors.valid_to = 'End date must be after start date';
}
// Check if end time exceeds 24:00
const [hours, minutes] = startsAt.split(':').map(Number);
const totalMinutes = hours * 60 + minutes + durationMinutes;
if (totalMinutes >= 24 * 60) {
newErrors.duration_minutes = 'Slot cannot extend past midnight';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
}
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!validateForm()) {
return;
}
setLoading(true);
setGeneralError('');
const request: SlotDefinitionRequest = {
court_id: courtId,
dow,
starts_at: formatTimeForAPI(startsAt),
duration_minutes: durationMinutes,
capacity,
valid_from: validFrom,
valid_to: noEndDate ? undefined : validTo,
rule: {
weekly: true,
description: ruleDescription || undefined,
},
};
const result = isEditing
? await updateSlotDefinition(clubId, definition.slot_definition_id, request)
: await createSlotDefinition(clubId, request);
if (result.success) {
onSuccess();
onClose();
} else {
// Handle validation errors
if (result.error.code === 'validation_error' && result.error.errors) {
const fieldErrors: Record<string, string> = {};
result.error.errors.forEach((err: ValidationError) => {
fieldErrors[err.field] = err.message;
});
setErrors(fieldErrors);
} else if (result.error.code === 'slot_definition_overlap') {
setGeneralError('A slot definition already exists for this court, day, and time. Please choose a different time or court.');
} else {
setGeneralError(result.error.detail || 'An error occurred. Please try again.');
}
}
setLoading(false);
}
return (
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4 overflow-y-auto">
<div className="bg-white rounded-2xl p-8 max-w-2xl w-full my-8">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-bold text-slate-900">
{isEditing ? 'Edit Slot Definition' : 'Create Slot Definition'}
</h2>
<button
onClick={onClose}
className="p-2 hover:bg-slate-100 rounded-lg transition-colors"
disabled={loading}
>
<X className="w-5 h-5 text-slate-600" />
</button>
</div>
{/* General Error */}
{generalError && (
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg flex items-start space-x-3">
<AlertCircle className="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5" />
<p className="text-sm text-red-700">{generalError}</p>
</div>
)}
{/* Form */}
<form onSubmit={handleSubmit} className="space-y-6">
{/* Court Selection */}
<div>
<label className="block text-sm font-semibold text-slate-900 mb-2">
Court <span className="text-red-600">*</span>
</label>
<select
value={courtId}
onChange={(e) => setCourtId(parseInt(e.target.value))}
className={`w-full px-4 py-3 border-2 rounded-lg font-medium transition-colors ${
errors.court_id
? 'border-red-300 focus:border-red-500'
: 'border-slate-200 focus:border-slate-900'
} focus:outline-none`}
disabled={loading}
>
{courts.map((court) => (
<option key={court.court_id} value={court.court_id}>
{court.name}
</option>
))}
</select>
{errors.court_id && (
<p className="mt-1 text-sm text-red-600">{errors.court_id}</p>
)}
</div>
{/* Day of Week */}
<div>
<label className="block text-sm font-semibold text-slate-900 mb-2">
Day of Week <span className="text-red-600">*</span>
</label>
<select
value={dow}
onChange={(e) => setDow(parseInt(e.target.value) as DayOfWeek)}
className="w-full px-4 py-3 border-2 border-slate-200 rounded-lg font-medium focus:border-slate-900 focus:outline-none transition-colors"
disabled={loading}
>
{(Object.keys(DAY_NAMES) as unknown as DayOfWeek[]).map((d) => (
<option key={d} value={d}>
{DAY_NAMES[d]}
</option>
))}
</select>
</div>
{/* Time and Duration */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-semibold text-slate-900 mb-2">
Start Time <span className="text-red-600">*</span>
</label>
<input
type="time"
value={startsAt}
onChange={(e) => setStartsAt(e.target.value)}
className={`w-full px-4 py-3 border-2 rounded-lg font-medium transition-colors ${
errors.starts_at
? 'border-red-300 focus:border-red-500'
: 'border-slate-200 focus:border-slate-900'
} focus:outline-none`}
disabled={loading}
/>
{errors.starts_at && (
<p className="mt-1 text-sm text-red-600">{errors.starts_at}</p>
)}
</div>
<div>
<label className="block text-sm font-semibold text-slate-900 mb-2">
Duration (minutes) <span className="text-red-600">*</span>
</label>
<input
type="number"
value={durationMinutes}
onChange={(e) => setDurationMinutes(parseInt(e.target.value) || 0)}
min="1"
step="15"
className={`w-full px-4 py-3 border-2 rounded-lg font-medium transition-colors ${
errors.duration_minutes
? 'border-red-300 focus:border-red-500'
: 'border-slate-200 focus:border-slate-900'
} focus:outline-none`}
disabled={loading}
/>
{errors.duration_minutes && (
<p className="mt-1 text-sm text-red-600">{errors.duration_minutes}</p>
)}
</div>
</div>
{/* Capacity */}
<div>
<label className="block text-sm font-semibold text-slate-900 mb-2">
Capacity (players) <span className="text-red-600">*</span>
</label>
<input
type="number"
value={capacity}
onChange={(e) => setCapacity(parseInt(e.target.value) || 0)}
min="1"
className={`w-full px-4 py-3 border-2 rounded-lg font-medium transition-colors ${
errors.capacity
? 'border-red-300 focus:border-red-500'
: 'border-slate-200 focus:border-slate-900'
} focus:outline-none`}
disabled={loading}
/>
{errors.capacity && (
<p className="mt-1 text-sm text-red-600">{errors.capacity}</p>
)}
</div>
{/* Valid Date Range */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-semibold text-slate-900 mb-2">
Valid From <span className="text-red-600">*</span>
</label>
<input
type="date"
value={validFrom}
onChange={(e) => setValidFrom(e.target.value)}
min={getTodayString()}
className={`w-full px-4 py-3 border-2 rounded-lg font-medium transition-colors ${
errors.valid_from
? 'border-red-300 focus:border-red-500'
: 'border-slate-200 focus:border-slate-900'
} focus:outline-none`}
disabled={loading}
/>
{errors.valid_from && (
<p className="mt-1 text-sm text-red-600">{errors.valid_from}</p>
)}
</div>
<div>
<label className="block text-sm font-semibold text-slate-900 mb-2">
Valid To
</label>
<input
type="date"
value={validTo}
onChange={(e) => setValidTo(e.target.value)}
min={validFrom}
disabled={noEndDate || loading}
className={`w-full px-4 py-3 border-2 rounded-lg font-medium transition-colors ${
errors.valid_to
? 'border-red-300 focus:border-red-500'
: 'border-slate-200 focus:border-slate-900'
} focus:outline-none disabled:bg-slate-50 disabled:text-slate-400`}
/>
{errors.valid_to && (
<p className="mt-1 text-sm text-red-600">{errors.valid_to}</p>
)}
<label className="mt-2 flex items-center">
<input
type="checkbox"
checked={noEndDate}
onChange={(e) => setNoEndDate(e.target.checked)}
className="w-4 h-4 rounded border-slate-300 text-slate-900 focus:ring-slate-900"
disabled={loading}
/>
<span className="ml-2 text-sm text-slate-600">No end date</span>
</label>
</div>
</div>
{/* Description */}
<div>
<label className="block text-sm font-semibold text-slate-900 mb-2">
Internal Note (optional)
</label>
<input
type="text"
value={ruleDescription}
onChange={(e) => setRuleDescription(e.target.value)}
placeholder="e.g., Monday morning regulars"
maxLength={200}
className="w-full px-4 py-3 border-2 border-slate-200 rounded-lg font-medium focus:border-slate-900 focus:outline-none transition-colors"
disabled={loading}
/>
<p className="mt-1 text-xs text-slate-500">
Helps you remember context for this slot definition
</p>
</div>
{/* Actions */}
<div className="flex items-center justify-end space-x-3 pt-4 border-t-2 border-slate-100">
<button
type="button"
onClick={onClose}
className="px-6 py-3 text-slate-700 font-semibold rounded-lg hover:bg-slate-100 transition-colors"
disabled={loading}
>
Cancel
</button>
<button
type="submit"
className="inline-flex items-center px-6 py-3 bg-slate-900 text-white font-semibold rounded-lg hover:bg-slate-800 transition-colors disabled:opacity-50"
disabled={loading}
>
{loading && <Loader2 className="w-4 h-4 mr-2 animate-spin" />}
{isEditing ? 'Update' : 'Create'} Definition
</button>
</div>
</form>
</div>
</div>
);
}

@ -4,20 +4,29 @@ import { useState, useEffect } from 'react';
import { Calendar, Plus, Loader2, AlertCircle, Edit, Trash2, ArrowLeft } from 'lucide-react';
import Link from 'next/link';
import useTranslation from '@/src/hooks/useTranslation';
import { getSlotDefinitions, getMockSlotDefinitions } from '@/src/lib/api/slot-definitions';
import { getSlotDefinitions, getMockSlotDefinitions, deleteSlotDefinition } from '@/src/lib/api/slot-definitions';
import type { SlotDefinition, SlotDefinitionError } from '@/src/types/slot-definitions';
import { DAY_NAMES, formatTime, calculateEndTime } from '@/src/types/slot-definitions';
import SlotDefinitionForm from './SlotDefinitionForm';
interface SlotDefinitionsComponentProps {
clubId: number;
}
// Mock courts data (will come from parent/API later)
const MOCK_COURTS = [
{ court_id: 101, name: 'Court 1' },
{ court_id: 102, name: 'Court 2' },
{ court_id: 103, name: 'Court 3' },
];
export default function SlotDefinitionsComponent({ clubId }: SlotDefinitionsComponentProps) {
const { t, locale } = useTranslation();
const [definitions, setDefinitions] = useState<SlotDefinition[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<SlotDefinitionError | null>(null);
const [showCreateModal, setShowCreateModal] = useState(false);
const [editingDefinition, setEditingDefinition] = useState<SlotDefinition | undefined>(undefined);
useEffect(() => {
loadDefinitions();
@ -50,20 +59,35 @@ export default function SlotDefinitionsComponent({ clubId }: SlotDefinitionsComp
}
function handleCreate() {
setEditingDefinition(undefined);
setShowCreateModal(true);
}
function handleEdit(definition: SlotDefinition) {
// TODO: Open edit modal
console.log('Edit', definition);
setEditingDefinition(definition);
setShowCreateModal(true);
}
async function handleDelete(definition: SlotDefinition) {
if (!confirm(`Delete slot definition for ${DAY_NAMES[definition.dow]} at ${formatTime(definition.starts_at)}?`)) {
return;
}
// TODO: Call delete API
console.log('Delete', definition);
const result = await deleteSlotDefinition(clubId, definition.slot_definition_id);
if (result.success) {
loadDefinitions();
} else {
alert(`Failed to delete: ${result.error.detail}`);
}
}
function handleFormClose() {
setShowCreateModal(false);
setEditingDefinition(undefined);
}
function handleFormSuccess() {
loadDefinitions();
}
// Loading state
@ -245,20 +269,15 @@ export default function SlotDefinitionsComponent({ clubId }: SlotDefinitionsComp
</div>
)}
{/* TODO: Create/Edit Modal */}
{/* Create/Edit Modal */}
{showCreateModal && (
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4">
<div className="bg-white rounded-2xl p-8 max-w-2xl w-full">
<h2 className="text-2xl font-bold text-slate-900 mb-4">Create Slot Definition</h2>
<p className="text-slate-600 mb-6">Form coming next...</p>
<button
onClick={() => setShowCreateModal(false)}
className="px-4 py-2 bg-slate-900 text-white rounded-lg hover:bg-slate-800"
>
Close
</button>
</div>
</div>
<SlotDefinitionForm
clubId={clubId}
courts={MOCK_COURTS}
definition={editingDefinition}
onClose={handleFormClose}
onSuccess={handleFormSuccess}
/>
)}
</div>
);

Loading…
Cancel
Save