- Implemented API endpoints for listing and updating recurring booking groups. - Added handlers for listing and updating recurring groups in the backend. - Created frontend components for displaying and editing recurring bookings. - Enhanced booking provider to manage recurring groups and their states. - Updated API client to include new methods for recurring bookings. - Introduced new validation schemas for updating recurring groups. - Added UI elements for managing recurring bookings in the booking interface.
211 lines
6.9 KiB
TypeScript
211 lines
6.9 KiB
TypeScript
import { Button } from '@/components/ui/button';
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select';
|
|
import { apiClient } from '@/lib/api-client';
|
|
import type { RecurringBookingGroup } from '@repo/api-contract';
|
|
import { useEffect, useState } from 'react';
|
|
import { useBooking } from '../booking-provider';
|
|
import { minutesToTime, timeToMinutes } from '../lib/booking-time';
|
|
|
|
const DAY_OPTIONS = [
|
|
{ value: 'MONDAY', label: 'Lunes' },
|
|
{ value: 'TUESDAY', label: 'Martes' },
|
|
{ value: 'WEDNESDAY', label: 'Miércoles' },
|
|
{ value: 'THURSDAY', label: 'Jueves' },
|
|
{ value: 'FRIDAY', label: 'Viernes' },
|
|
{ value: 'SATURDAY', label: 'Sábado' },
|
|
{ value: 'SUNDAY', label: 'Domingo' },
|
|
];
|
|
|
|
interface RecurringBookingEditDialogProps {
|
|
group: RecurringBookingGroup;
|
|
open: boolean;
|
|
onClose: () => void;
|
|
}
|
|
|
|
export function RecurringBookingEditDialog({
|
|
group,
|
|
open,
|
|
onClose,
|
|
}: RecurringBookingEditDialogProps) {
|
|
const { courts, refreshRecurringGroups } = useBooking();
|
|
|
|
const [courtId, setCourtId] = useState<string>(group.courtId);
|
|
const [dayOfWeek, setDayOfWeek] = useState<string>(group.dayOfWeek);
|
|
const [startTime, setStartTime] = useState<string>(group.startTime);
|
|
const [customerName, setCustomerName] = useState(group.customerName);
|
|
const [customerPhone, setCustomerPhone] = useState(group.customerPhone);
|
|
const [customerEmail, setCustomerEmail] = useState(group.customerEmail);
|
|
const [error, setError] = useState('');
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
useEffect(() => {
|
|
setCourtId(group.courtId);
|
|
setDayOfWeek(group.dayOfWeek);
|
|
setStartTime(group.startTime);
|
|
setCustomerName(group.customerName);
|
|
setCustomerPhone(group.customerPhone);
|
|
setCustomerEmail(group.customerEmail);
|
|
}, [group]);
|
|
|
|
const selectedCourt = courts.find((c) => c.id === courtId);
|
|
const slotDuration = selectedCourt?.slotDurationMinutes ?? 60;
|
|
|
|
const availableSlots: string[] = [];
|
|
if (selectedCourt) {
|
|
const availabilities = selectedCourt.availability.filter((a) => a.dayOfWeek === dayOfWeek);
|
|
for (const avail of availabilities) {
|
|
const startMins = timeToMinutes(avail.startTime);
|
|
const endMins = timeToMinutes(avail.endTime);
|
|
for (let m = startMins; m + slotDuration <= endMins; m += slotDuration) {
|
|
availableSlots.push(minutesToTime(m));
|
|
}
|
|
}
|
|
}
|
|
|
|
const handleSave = async () => {
|
|
if (!customerName || customerName.trim().length < 2) {
|
|
setError('El nombre debe tener al menos 2 caracteres.');
|
|
return;
|
|
}
|
|
|
|
setError('');
|
|
setSaving(true);
|
|
|
|
try {
|
|
await apiClient.adminBookings.updateRecurringGroup(group.id, {
|
|
...(courtId !== group.courtId && { courtId }),
|
|
...(dayOfWeek !== group.dayOfWeek && {
|
|
dayOfWeek: dayOfWeek as
|
|
| 'MONDAY'
|
|
| 'TUESDAY'
|
|
| 'WEDNESDAY'
|
|
| 'THURSDAY'
|
|
| 'FRIDAY'
|
|
| 'SATURDAY'
|
|
| 'SUNDAY',
|
|
}),
|
|
...(startTime !== group.startTime && { startTime }),
|
|
...(customerName !== group.customerName && { customerName }),
|
|
...(customerPhone !== group.customerPhone && { customerPhone }),
|
|
...(customerEmail !== group.customerEmail && { customerEmail }),
|
|
});
|
|
|
|
await refreshRecurringGroups();
|
|
onClose();
|
|
} catch (e) {
|
|
setError(e instanceof Error ? e.message : 'Error al guardar los cambios.');
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={(open) => !open && onClose()}>
|
|
<DialogContent className="sm:max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle>Editar turno fijo</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-4">
|
|
<div className="space-y-1.5">
|
|
<Label>Cliente</Label>
|
|
<Input
|
|
value={customerName}
|
|
onChange={(e) => setCustomerName(e.target.value)}
|
|
placeholder="Nombre del cliente"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-1.5">
|
|
<Label>Teléfono</Label>
|
|
<Input
|
|
value={customerPhone}
|
|
onChange={(e) => setCustomerPhone(e.target.value)}
|
|
placeholder="Teléfono"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-1.5">
|
|
<Label>Email</Label>
|
|
<Input
|
|
value={customerEmail}
|
|
onChange={(e) => setCustomerEmail(e.target.value)}
|
|
placeholder="Email"
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div className="space-y-1.5">
|
|
<Label>Cancha</Label>
|
|
<Select value={courtId} onValueChange={setCourtId}>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Cancha" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{courts.map((court) => (
|
|
<SelectItem key={court.id} value={court.id}>
|
|
{court.name}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="space-y-1.5">
|
|
<Label>Día</Label>
|
|
<Select value={dayOfWeek} onValueChange={setDayOfWeek}>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Día" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{DAY_OPTIONS.map((day) => (
|
|
<SelectItem key={day.value} value={day.value}>
|
|
{day.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-1.5">
|
|
<Label>Horario</Label>
|
|
<Select value={startTime} onValueChange={setStartTime}>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Horario" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{availableSlots.map((slot) => (
|
|
<SelectItem key={slot} value={slot}>
|
|
{slot} - {minutesToTime(timeToMinutes(slot) + slotDuration)}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{error && <p className="text-sm text-destructive">{error}</p>}
|
|
|
|
<div className="flex justify-end gap-3 pt-2">
|
|
<Button type="button" variant="outline" onClick={onClose}>
|
|
Cancelar
|
|
</Button>
|
|
<Button type="button" onClick={handleSave} disabled={saving}>
|
|
{saving ? 'Guardando...' : 'Guardar cambios'}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|