You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

330 lines
11 KiB
TypeScript

'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<SlotInstanceError | null>(null);
const [validationErrors, setValidationErrors] = useState<Record<string, string>>({});
// Form state
const [courtId, setCourtId] = useState<number | null>(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<string, string> = {};
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<string, string> = {};
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 (
<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-lg w-full max-h-[90vh] overflow-y-auto">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div>
<h2 className="text-2xl font-bold text-slate-900">Create Manual Slot</h2>
<p className="text-sm text-slate-600 mt-1">
Create a one-off booking slot
</p>
</div>
<button
onClick={onClose}
className="p-2 hover:bg-slate-100 rounded-lg transition-colors"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Info box */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
<p className="text-sm text-blue-900">
Manual slots are independent of slot definitions and won&apos;t be affected
by the materializer&apos;s automatic slot generation or cleanup.
</p>
</div>
{/* Error display */}
{error && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-6">
<div className="flex items-start space-x-3">
<AlertCircle className="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5" />
<div>
<p className="text-sm text-red-900 font-medium">{error.title}</p>
<p className="text-sm text-red-700 mt-1">{error.detail}</p>
</div>
</div>
</div>
)}
{/* Form */}
<form onSubmit={handleSubmit} className="space-y-6">
{/* Court selection */}
<div>
<label className="block text-sm font-semibold text-slate-700 mb-2">
<MapPin className="w-4 h-4 inline mr-1" />
Court *
</label>
<select
value={courtId ?? ''}
onChange={(e) => setCourtId(parseInt(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.court_id ? 'border-red-300' : 'border-slate-200'
}`}
>
<option value="">Select a court...</option>
{courts.map((court) => (
<option key={court.court_id} value={court.court_id}>
{court.name}
</option>
))}
</select>
{validationErrors.court_id && (
<p className="mt-1 text-sm text-red-600">{validationErrors.court_id}</p>
)}
</div>
{/* Date */}
<div>
<label className="block text-sm font-semibold text-slate-700 mb-2">
<Calendar className="w-4 h-4 inline mr-1" />
Date *
</label>
<input
type="date"
value={date}
onChange={(e) => 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 && (
<p className="mt-1 text-sm text-red-600">{validationErrors.date}</p>
)}
</div>
{/* Time range */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-semibold text-slate-700 mb-2">
<Clock className="w-4 h-4 inline mr-1" />
Start Time *
</label>
<input
type="time"
value={startTime}
onChange={(e) => 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 && (
<p className="mt-1 text-sm text-red-600">{validationErrors.start_time}</p>
)}
</div>
<div>
<label className="block text-sm font-semibold text-slate-700 mb-2">
<Clock className="w-4 h-4 inline mr-1" />
End Time *
</label>
<input
type="time"
value={endTime}
onChange={(e) => 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 && (
<p className="mt-1 text-sm text-red-600">{validationErrors.end_time}</p>
)}
</div>
</div>
{/* Duration display */}
<div className="bg-slate-50 rounded-lg px-4 py-3 text-center">
<span className="text-sm text-slate-600">Duration: </span>
<span className="text-sm font-semibold text-slate-900">{getDuration()}</span>
</div>
{/* Capacity */}
<div>
<label className="block text-sm font-semibold text-slate-700 mb-2">
<Users className="w-4 h-4 inline mr-1" />
Capacity *
</label>
<input
type="number"
min="1"
max="20"
value={capacity}
onChange={(e) => 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 && (
<p className="mt-1 text-sm text-red-600">{validationErrors.capacity}</p>
)}
<p className="mt-1 text-xs text-slate-500">
Number of players this slot can accommodate
</p>
</div>
{/* Reason/Notes */}
<div>
<label className="block text-sm font-semibold text-slate-700 mb-2">
<FileText className="w-4 h-4 inline mr-1" />
Notes (optional)
</label>
<textarea
value={reason}
onChange={(e) => setReason(e.target.value)}
disabled={loading}
rows={2}
placeholder="e.g., Tournament slot, Private event, etc."
className="w-full px-4 py-2 border-2 border-slate-200 rounded-lg focus:ring-2 focus:ring-slate-500 focus:border-slate-500"
/>
</div>
{/* Actions */}
<div className="flex items-center justify-end space-x-3 pt-4 border-t border-slate-200">
<button
type="button"
onClick={onClose}
disabled={loading}
className="px-6 py-2 text-slate-700 font-medium hover:bg-slate-100 rounded-lg transition-colors"
>
Cancel
</button>
<button
type="submit"
disabled={loading}
className="inline-flex items-center px-6 py-2 bg-slate-900 text-white font-semibold rounded-lg hover:bg-slate-800 transition-colors disabled:opacity-50"
>
{loading && <Loader2 className="w-4 h-4 mr-2 animate-spin" />}
Create Slot
</button>
</div>
</form>
</div>
</div>
);
}