feat(booking-ui): add confirmation dialog for cancelling bookings and improve mobile UI

This commit is contained in:
Jose Selesan
2026-06-04 16:39:45 -03:00
parent 7ca784d5f5
commit 1210854c22

View File

@@ -24,6 +24,7 @@ import {
Wrench, Wrench,
XCircle, XCircle,
} from 'lucide-react'; } from 'lucide-react';
import { useState } from 'react';
import { useBooking } from '../booking-provider'; import { useBooking } from '../booking-provider';
function formatDate(date: string | undefined) { function formatDate(date: string | undefined) {
@@ -70,6 +71,7 @@ export function BookingToolsDialog() {
useBooking(); useBooking();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const booking = selectedSegment?.booking; const booking = selectedSegment?.booking;
const [confirmCancelOpen, setConfirmCancelOpen] = useState(false);
const status = statusConfig[booking?.status ?? 'CONFIRMED'] ?? { const status = statusConfig[booking?.status ?? 'CONFIRMED'] ?? {
label: booking?.status, label: booking?.status,
@@ -93,151 +95,211 @@ export function BookingToolsDialog() {
closeBookingTools(); closeBookingTools();
}; };
const cancelConfirmDialog = (
<ResponsiveDialog
open={confirmCancelOpen}
onOpenChange={(open) => !open && setConfirmCancelOpen(false)}
>
<ResponsiveDialogContent>
<ResponsiveDialogHeader>
<ResponsiveDialogTitle>¿Cancelar reserva?</ResponsiveDialogTitle>
<ResponsiveDialogDescription>
Esta acción no se puede deshacer. Se eliminará la reserva y la cancha volverá a estar
disponible.
</ResponsiveDialogDescription>
</ResponsiveDialogHeader>
<div className="space-y-2 rounded-lg border bg-card p-4">
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Cliente</span>
<span className="text-sm font-medium">{booking?.customerName ?? '-'}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Cancha</span>
<span className="text-sm font-medium">{booking?.courtName ?? '-'}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Fecha</span>
<span className="text-sm font-medium">{formatDate(booking?.date) || '-'}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Horario</span>
<span className="text-sm font-medium">
{booking?.startTime ?? '--:--'} a {booking?.endTime ?? '--:--'}
</span>
</div>
</div>
<ResponsiveDialogFooter>
<ResponsiveDialogClose asChild>
<Button variant="outline">Volver</Button>
</ResponsiveDialogClose>
<Button
variant="destructive"
onClick={() => {
setConfirmCancelOpen(false);
updateStatus('CANCELLED');
}}
>
, cancelar reserva
</Button>
</ResponsiveDialogFooter>
</ResponsiveDialogContent>
</ResponsiveDialog>
);
if (isMobile) { if (isMobile) {
return ( return (
<>
<ResponsiveDialog
open={bookingToolsOpen}
onOpenChange={(open) => !open && closeBookingTools()}
>
<ResponsiveDialogContent
className="data-[variant=drawer]:!h-[92dvh] data-[variant=drawer]:!max-h-[92dvh] data-[variant=drawer]:overflow-hidden data-[variant=drawer]:px-0 data-[variant=drawer]:pb-0"
onOpenAutoFocus={(event) => event.preventDefault()}
>
<MobileBookingToolsSheet
status={status}
onComplete={() => updateStatus('COMPLETED')}
onCancel={() => setConfirmCancelOpen(true)}
onReminder={sendWhatsappReminder}
onNoShow={() => updateStatus('NOSHOW')}
/>
</ResponsiveDialogContent>
</ResponsiveDialog>
{cancelConfirmDialog}
</>
);
}
return (
<>
<ResponsiveDialog <ResponsiveDialog
open={bookingToolsOpen} open={bookingToolsOpen}
onOpenChange={(open) => !open && closeBookingTools()} onOpenChange={(open) => !open && closeBookingTools()}
> >
<ResponsiveDialogContent <ResponsiveDialogContent
className="data-[variant=drawer]:!h-[92dvh] data-[variant=drawer]:!max-h-[92dvh] data-[variant=drawer]:overflow-hidden data-[variant=drawer]:px-0 data-[variant=drawer]:pb-0" className="data-[variant=dialog]:max-w-5xl data-[variant=drawer]:max-h-[92dvh] data-[variant=drawer]:overflow-y-auto data-[variant=drawer]:px-4 data-[variant=drawer]:pb-5"
onOpenAutoFocus={(event) => event.preventDefault()} onOpenAutoFocus={(event) => event.preventDefault()}
> >
<MobileBookingToolsSheet <ResponsiveDialogHeader className="data-[variant=drawer]:px-0 data-[variant=drawer]:text-left">
status={status} <ResponsiveDialogTitle>Detalles de la reserva</ResponsiveDialogTitle>
onComplete={() => updateStatus('COMPLETED')} <ResponsiveDialogDescription>
onCancel={() => updateStatus('CANCELLED')} Administra la reserva y su estado.
onReminder={sendWhatsappReminder} </ResponsiveDialogDescription>
onNoShow={() => updateStatus('NOSHOW')} </ResponsiveDialogHeader>
/>
</ResponsiveDialogContent>
</ResponsiveDialog>
);
}
return ( <section className="mx-0 px-0 pt-3 pb-3 sm:-mx-6 sm:px-6 sm:pb-5">
<ResponsiveDialog open={bookingToolsOpen} onOpenChange={(open) => !open && closeBookingTools()}> <div className="grid grid-cols-1 gap-3 sm:grid-cols-3 sm:gap-0">
<ResponsiveDialogContent <DetailItem
className="data-[variant=dialog]:max-w-5xl data-[variant=drawer]:max-h-[92dvh] data-[variant=drawer]:overflow-y-auto data-[variant=drawer]:px-4 data-[variant=drawer]:pb-5" icon={<MapPin className="h-5 w-5" />}
onOpenAutoFocus={(event) => event.preventDefault()} label="Cancha"
> value={selectedSegment?.booking?.courtName ?? '-'}
<ResponsiveDialogHeader className="data-[variant=drawer]:px-0 data-[variant=drawer]:text-left"> helper={selectedSegment?.booking?.sport?.name}
<ResponsiveDialogTitle>Detalles de la reserva</ResponsiveDialogTitle> withDivider
<ResponsiveDialogDescription>
Administra la reserva y su estado.
</ResponsiveDialogDescription>
</ResponsiveDialogHeader>
<section className="mx-0 px-0 pt-3 pb-3 sm:-mx-6 sm:px-6 sm:pb-5">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3 sm:gap-0">
<DetailItem
icon={<MapPin className="h-5 w-5" />}
label="Cancha"
value={selectedSegment?.booking?.courtName ?? '-'}
helper={selectedSegment?.booking?.sport?.name}
withDivider
/>
<DetailItem
icon={<BadgeCheck className="h-5 w-5" />}
label="Estado actual"
value={status.label}
valueClassName="text-emerald-400 uppercase dark:text-emerald-500"
withDivider
/>
<DetailItem
icon={<User className="h-5 w-5" />}
label="Cliente"
value={selectedSegment?.booking?.customerName ?? '-'}
/>
<DetailItem
icon={<Clock className="h-5 w-5" />}
label="Fecha"
value={`${formatDate(selectedSegment?.booking?.date)}`}
withDivider
className="sm:pt-10"
/>
<DetailItem
icon={<Clock className="h-5 w-5" />}
label="Hora"
value={`${selectedSegment?.booking?.startTime} a ${selectedSegment?.booking?.endTime}`}
withDivider
className="sm:pt-10"
/>
<DetailItem
icon={<Phone className="h-5 w-5" />}
label="Teléfono"
value={selectedSegment?.booking?.customerPhone ?? '-'}
className="sm:pt-10"
/>
</div>
</section>
<Separator className="dark:bg-slate-800 bg-slate-300" />
{booking?.status !== 'COMPLETED' && (
<section className="space-y-3">
<div>
<h3 className="font-medium text-slate-100">Acciones disponibles</h3>
<p className="text-sm text-slate-400">
Elegí qué hacer según lo que ocurrió con el cliente.
</p>
</div>
<div className="grid gap-3 sm:grid-cols-2">
<ActionButton
icon={<CheckCircle2 className="h-5 w-5" />}
title="Completar reserva"
description="El cliente se presentó para usar la cancha."
className="border-emerald-500/40 bg-emerald-500/10 text-lg text-emerald-600 hover:bg-emerald-500/15 hover:text-emerald-800"
onClick={() => {
updateStatus('COMPLETED');
}}
/> />
<ActionButton <DetailItem
icon={<XCircle className="h-5 w-5" />} icon={<BadgeCheck className="h-5 w-5" />}
title="Cancelar reserva" label="Estado actual"
description="El cliente avisó que no va a venir. La cancha vuelve a estar disponible." value={status.label}
className="border-red-500/40 bg-red-500/10 text-lg text-red-600 hover:bg-red-500/15 hover:text-red-800" valueClassName="text-emerald-400 uppercase dark:text-emerald-500"
onClick={() => { withDivider
updateStatus('CANCELLED');
}}
/> />
<ActionButton <DetailItem
icon={<MessageCircle className="h-5 w-5" />} icon={<User className="h-5 w-5" />}
title="Enviar recordatorio" label="Cliente"
description="Enviar un mensaje por WhatsApp al cliente." value={selectedSegment?.booking?.customerName ?? '-'}
className="border-sky-500/40 bg-sky-500/10 text-lg text-sky-600 hover:bg-sky-500/15 hover:text-sky-800"
onClick={sendWhatsappReminder}
/> />
<ActionButton <DetailItem
icon={<AlertTriangle className="h-5 w-5" />} icon={<Clock className="h-5 w-5" />}
title="Marcar como No Show" label="Fecha"
description="El cliente no vino y no canceló." value={`${formatDate(selectedSegment?.booking?.date)}`}
className="border-amber-500/40 bg-amber-500/10 text-lg text-amber-600 hover:bg-amber-500/15 hover:text-amber-800" withDivider
onClick={() => { className="sm:pt-10"
updateStatus('NOSHOW'); />
}}
<DetailItem
icon={<Clock className="h-5 w-5" />}
label="Hora"
value={`${selectedSegment?.booking?.startTime} a ${selectedSegment?.booking?.endTime}`}
withDivider
className="sm:pt-10"
/>
<DetailItem
icon={<Phone className="h-5 w-5" />}
label="Teléfono"
value={selectedSegment?.booking?.customerPhone ?? '-'}
className="sm:pt-10"
/> />
</div> </div>
</section> </section>
)}
<ResponsiveDialogFooter className="data-[variant=drawer]:px-0"> <Separator className="dark:bg-slate-800 bg-slate-300" />
<ResponsiveDialogClose asChild>
<Button variant="outline" className="data-[variant=drawer]:h-11"> {booking?.status !== 'COMPLETED' && (
Cerrar <section className="space-y-3">
</Button> <div>
</ResponsiveDialogClose> <h3 className="font-medium text-slate-100">Acciones disponibles</h3>
</ResponsiveDialogFooter> <p className="text-sm text-slate-400">
</ResponsiveDialogContent> Elegí qué hacer según lo que ocurrió con el cliente.
</ResponsiveDialog> </p>
</div>
<div className="grid gap-3 sm:grid-cols-2">
<ActionButton
icon={<CheckCircle2 className="h-5 w-5" />}
title="Completar reserva"
description="El cliente se presentó para usar la cancha."
className="border-emerald-500/40 bg-emerald-500/10 text-lg text-emerald-600 hover:bg-emerald-500/15 hover:text-emerald-800"
onClick={() => {
updateStatus('COMPLETED');
}}
/>
<ActionButton
icon={<XCircle className="h-5 w-5" />}
title="Cancelar reserva"
description="El cliente avisó que no va a venir. La cancha vuelve a estar disponible."
className="border-red-500/40 bg-red-500/10 text-lg text-red-600 hover:bg-red-500/15 hover:text-red-800"
onClick={() => setConfirmCancelOpen(true)}
/>
<ActionButton
icon={<MessageCircle className="h-5 w-5" />}
title="Enviar recordatorio"
description="Enviar un mensaje por WhatsApp al cliente."
className="border-sky-500/40 bg-sky-500/10 text-lg text-sky-600 hover:bg-sky-500/15 hover:text-sky-800"
onClick={sendWhatsappReminder}
/>
<ActionButton
icon={<AlertTriangle className="h-5 w-5" />}
title="Marcar como No Show"
description="El cliente no vino y no canceló."
className="border-amber-500/40 bg-amber-500/10 text-lg text-amber-600 hover:bg-amber-500/15 hover:text-amber-800"
onClick={() => {
updateStatus('NOSHOW');
}}
/>
</div>
</section>
)}
<ResponsiveDialogFooter className="data-[variant=drawer]:px-0">
<ResponsiveDialogClose asChild>
<Button variant="outline" className="data-[variant=drawer]:h-11">
Cerrar
</Button>
</ResponsiveDialogClose>
</ResponsiveDialogFooter>
</ResponsiveDialogContent>
</ResponsiveDialog>
{cancelConfirmDialog}
</>
); );
} }