feat: add recurring bookings feature with related handlers and services
- Introduced recurring booking groups with the ability to create and cancel them. - Added database migrations for recurring bookings, including new tables and relationships. - Implemented handlers for creating and canceling recurring bookings in the admin booking module. - Enhanced existing booking services to support recurring bookings logic. - Updated API contract to include new schemas for recurring bookings. - Refactored existing code for improved readability and maintainability.
This commit is contained in:
@@ -18,6 +18,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
@@ -60,12 +61,16 @@ export function BookingCreateDialog() {
|
||||
isCreatingBooking,
|
||||
closeCreateBooking,
|
||||
createBooking,
|
||||
createRecurringBooking,
|
||||
isRecurringEnabled,
|
||||
} = useBooking();
|
||||
|
||||
const [date, setDate] = useState(selectedSlot?.date ?? selectedDate);
|
||||
const [sportId, setSportId] = useState(selectedSlot?.sportId ?? 'all');
|
||||
const [courtId, setCourtId] = useState(selectedSlot?.courtId ?? '');
|
||||
const [startTime, setStartTime] = useState(selectedSlot?.startTime ?? '');
|
||||
const [isRecurring, setIsRecurring] = useState(false);
|
||||
const [recurringEndDate, setRecurringEndDate] = useState<string | undefined>(undefined);
|
||||
|
||||
const {
|
||||
register,
|
||||
@@ -90,6 +95,8 @@ export function BookingCreateDialog() {
|
||||
setSportId(selectedSlot?.sportId ?? 'all');
|
||||
setCourtId(selectedSlot?.courtId ?? '');
|
||||
setStartTime(selectedSlot?.startTime ?? '');
|
||||
setIsRecurring(false);
|
||||
setRecurringEndDate(undefined);
|
||||
reset();
|
||||
|
||||
if (selectedSlot?.courtId && selectedSlot?.startTime) {
|
||||
@@ -155,14 +162,26 @@ export function BookingCreateDialog() {
|
||||
}, [availableStartTimes, startTime]);
|
||||
|
||||
const onSubmit = async (values: BookingForm) => {
|
||||
await createBooking({
|
||||
courtId,
|
||||
date,
|
||||
startTime,
|
||||
customerName: values.customerName,
|
||||
customerPhone: values.customerPhone,
|
||||
customerEmail: values.customerEmail,
|
||||
});
|
||||
if (isRecurring) {
|
||||
await createRecurringBooking({
|
||||
courtId,
|
||||
date,
|
||||
startTime,
|
||||
customerName: values.customerName,
|
||||
customerPhone: values.customerPhone,
|
||||
customerEmail: values.customerEmail,
|
||||
recurringEndDate,
|
||||
});
|
||||
} else {
|
||||
await createBooking({
|
||||
courtId,
|
||||
date,
|
||||
startTime,
|
||||
customerName: values.customerName,
|
||||
customerPhone: values.customerPhone,
|
||||
customerEmail: values.customerEmail,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -260,6 +279,52 @@ export function BookingCreateDialog() {
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
{isRecurringEnabled && (
|
||||
<>
|
||||
<Separator />
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-sm font-medium">Repetir todas las semanas</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{isRecurring
|
||||
? `Se creará una reserva todos los ${getDayOfWeekLabel(date)} a las ${startTime || '...'}`
|
||||
: 'Crea turnos fijos en el mismo horario'}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant={isRecurring ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setIsRecurring(!isRecurring)}
|
||||
className="shrink-0"
|
||||
>
|
||||
{isRecurring ? 'Activado' : 'Desactivado'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{isRecurring && (
|
||||
<Field>
|
||||
<FieldLabel>
|
||||
Repetir hasta{' '}
|
||||
<span className="text-muted-foreground font-normal">(opcional)</span>
|
||||
</FieldLabel>
|
||||
<DatePicker
|
||||
value={fromIsoDateLocal(recurringEndDate ?? '')}
|
||||
onChange={(nextDate) => {
|
||||
setRecurringEndDate(nextDate ? toIsoDateLocal(nextDate) : undefined);
|
||||
}}
|
||||
/>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
Si no se selecciona una fecha, se repetirá por tiempo indefinido.
|
||||
</p>
|
||||
</Field>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<Separator />
|
||||
|
||||
<Field data-invalid={Boolean(errors.customerName)}>
|
||||
<FieldLabel htmlFor="customerName">Nombre</FieldLabel>
|
||||
<Input
|
||||
@@ -312,10 +377,29 @@ export function BookingCreateDialog() {
|
||||
form="booking-create-form"
|
||||
disabled={!isValid || isCreatingBooking || !courtId || !startTime}
|
||||
>
|
||||
{isCreatingBooking ? 'Guardando...' : 'Crear reserva'}
|
||||
{isCreatingBooking
|
||||
? isRecurring
|
||||
? 'Creando turnos...'
|
||||
: 'Guardando...'
|
||||
: isRecurring
|
||||
? 'Crear turno fijo'
|
||||
: 'Crear reserva'}
|
||||
</Button>
|
||||
</ResponsiveDialogFooter>
|
||||
</ResponsiveDialogContent>
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
}
|
||||
|
||||
function getDayOfWeekLabel(dateIso: string): string {
|
||||
const labels: Record<string, string> = {
|
||||
MONDAY: 'lunes',
|
||||
TUESDAY: 'martes',
|
||||
WEDNESDAY: 'miércoles',
|
||||
THURSDAY: 'jueves',
|
||||
FRIDAY: 'viernes',
|
||||
SATURDAY: 'sábado',
|
||||
SUNDAY: 'domingo',
|
||||
};
|
||||
return labels[getDayOfWeek(dateIso)] ?? '';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user