feat: add recurring booking management functionality
- 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.
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user