'use client'; import { useState } from 'react'; import { X, Loader2, AlertCircle, Calendar, Clock, Users, FileText, MapPin } from 'lucide-react'; import { createSlotInstance } from '@/src/lib/api/slot-instances'; import type { CreateSlotInstanceRequest, SlotInstanceError } from '@/src/types/slot-instances'; import type { Court } from '@/src/types/courts'; interface ManualSlotModalProps { clubId: number; courts: Court[]; initialDate: string; onClose: () => void; onSuccess: () => void; } export default function ManualSlotModal({ clubId, courts, initialDate, onClose, onSuccess, }: ManualSlotModalProps) { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [validationErrors, setValidationErrors] = useState>({}); // Form state const [courtId, setCourtId] = useState(courts[0]?.court_id ?? null); const [date, setDate] = useState(initialDate); const [startTime, setStartTime] = useState('09:00'); const [endTime, setEndTime] = useState('10:30'); const [capacity, setCapacity] = useState(4); const [reason, setReason] = useState(''); function validateForm(): boolean { const errors: Record = {}; if (!courtId) { errors.court_id = 'Please select a court'; } if (!date) { errors.date = 'Please select a date'; } if (!startTime) { errors.start_time = 'Please enter a start time'; } if (!endTime) { errors.end_time = 'Please enter an end time'; } if (startTime && endTime && startTime >= endTime) { errors.end_time = 'End time must be after start time'; } if (capacity < 1) { errors.capacity = 'Capacity must be at least 1'; } setValidationErrors(errors); return Object.keys(errors).length === 0; } async function handleSubmit(e: React.FormEvent) { e.preventDefault(); if (!validateForm()) { return; } setLoading(true); setError(null); // Build the datetime strings // Note: We're creating local datetime strings - the backend will handle timezone conversion const startsAt = `${date}T${startTime}:00`; const endsAt = `${date}T${endTime}:00`; const request: CreateSlotInstanceRequest = { court_id: courtId!, starts_at: startsAt, ends_at: endsAt, capacity, reason: reason.trim() || undefined, }; const result = await createSlotInstance(clubId, request); if (result.success) { onSuccess(); } else { setError(result.error); // Handle field-level errors if (result.error.errors) { const fieldErrors: Record = {}; for (const err of result.error.errors) { fieldErrors[err.field] = err.message; } setValidationErrors(fieldErrors); } } setLoading(false); } // Calculate duration for display function getDuration(): string { if (!startTime || !endTime) return '-'; const [startH, startM] = startTime.split(':').map(Number); const [endH, endM] = endTime.split(':').map(Number); const startMins = startH * 60 + startM; const endMins = endH * 60 + endM; const diffMins = endMins - startMins; if (diffMins <= 0) return '-'; const hours = Math.floor(diffMins / 60); const mins = diffMins % 60; if (hours === 0) return `${mins}min`; if (mins === 0) return `${hours}h`; return `${hours}h ${mins}m`; } return (
{/* Header */}

Create Manual Slot

Create a one-off booking slot

{/* Info box */}

Manual slots are independent of slot definitions and won't be affected by the materializer's automatic slot generation or cleanup.

{/* Error display */} {error && (

{error.title}

{error.detail}

)} {/* Form */}
{/* Court selection */}
{validationErrors.court_id && (

{validationErrors.court_id}

)}
{/* Date */}
setDate(e.target.value)} disabled={loading} className={`w-full px-4 py-2 border-2 rounded-lg focus:ring-2 focus:ring-slate-500 focus:border-slate-500 ${ validationErrors.date ? 'border-red-300' : 'border-slate-200' }`} /> {validationErrors.date && (

{validationErrors.date}

)}
{/* Time range */}
setStartTime(e.target.value)} disabled={loading} className={`w-full px-4 py-2 border-2 rounded-lg focus:ring-2 focus:ring-slate-500 focus:border-slate-500 ${ validationErrors.start_time ? 'border-red-300' : 'border-slate-200' }`} /> {validationErrors.start_time && (

{validationErrors.start_time}

)}
setEndTime(e.target.value)} disabled={loading} className={`w-full px-4 py-2 border-2 rounded-lg focus:ring-2 focus:ring-slate-500 focus:border-slate-500 ${ validationErrors.end_time ? 'border-red-300' : 'border-slate-200' }`} /> {validationErrors.end_time && (

{validationErrors.end_time}

)}
{/* Duration display */}
Duration: {getDuration()}
{/* Capacity */}
setCapacity(parseInt(e.target.value) || 1)} disabled={loading} className={`w-full px-4 py-2 border-2 rounded-lg focus:ring-2 focus:ring-slate-500 focus:border-slate-500 ${ validationErrors.capacity ? 'border-red-300' : 'border-slate-200' }`} /> {validationErrors.capacity && (

{validationErrors.capacity}

)}

Number of players this slot can accommodate

{/* Reason/Notes */}