From 6a80607430574bcbc6fef2d838ffc21d939265e2 Mon Sep 17 00:00:00 2001 From: Guillermo Pages Date: Sat, 8 Nov 2025 14:11:01 +0100 Subject: [PATCH] feat(bookings): add dry_run slot picker with live validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implemented complete slot picker flow with real-time validation: **Components Created:** 1. **SlotPickerTile** (src/components/bookings/SlotPickerTile.tsx) - Individual slot tile with dry_run validation - 4 validation states: idle, validating, valid, invalid - Real-time API calls to validateMoveBooking() on selection - Visual feedback: checkmark (✓) for valid, X for invalid, spinner during validation - Displays validation errors inline with bullet points - Shows success message "✓ Valid - ready to confirm" - Integrates CapacityDisplay component - Color-coded borders (green=valid, red=invalid, gray=idle) - Disabled state for full/cancelled slots - onValidationResult callback for parent coordination 2. **SlotPickerModal** (src/components/bookings/SlotPickerModal.tsx) - Full-screen modal for slot selection - Groups slots by date (weekday, month, day, year) - Sorts dates chronologically - Filters out current slot - 2-column grid layout for slots - Info banner with policy hints (14-day window, product-type, club match) - Confirm button disabled until valid slot selected - Loading states with spinner - Empty state with helpful message - Tracks validation state from child tiles 3. **CancelBookingModal** (src/components/bookings/CancelBookingModal.tsx) - Confirmation modal for booking cancellation - Optional reason textarea (500 char limit) - Displays grace period policy (reads from booking.policies or defaults to 15 min) - Shows booking details (court, time, attendees count) - Warning banner: "This action cannot be undone" - Policy info banner with grace period details - Two-button layout: "Keep Booking" / "Yes, Cancel Booking" - Loading state with spinner **Features:** - Live dry_run validation on slot selection (no manual trigger needed) - Real-time feedback with checkmarks/errors - Graceful error handling (network failures, validation errors) - Parameterized error messages from getBookingErrorMessage() - SSR-safe with proper disabled states - Professional slate theme matching existing UI - Responsive grid layouts **Integration:** - SlotPickerTile calls validateMoveBooking() API automatically - SlotPickerModal coordinates validation state - CancelBookingModal reads policy fields from BookingDetail - All components ready for integration into BookingDrawer Related: Phase 3, Booking Admin API v1.1, BUILD #23 --- .../bookings/CancelBookingModal.tsx | 144 ++++++++++++++ src/components/bookings/SlotPickerModal.tsx | 177 +++++++++++++++++ src/components/bookings/SlotPickerTile.tsx | 178 ++++++++++++++++++ 3 files changed, 499 insertions(+) create mode 100644 src/components/bookings/CancelBookingModal.tsx create mode 100644 src/components/bookings/SlotPickerModal.tsx create mode 100644 src/components/bookings/SlotPickerTile.tsx diff --git a/src/components/bookings/CancelBookingModal.tsx b/src/components/bookings/CancelBookingModal.tsx new file mode 100644 index 0000000..b25fbd6 --- /dev/null +++ b/src/components/bookings/CancelBookingModal.tsx @@ -0,0 +1,144 @@ +/** + * Cancel Booking Modal Component + * + * Confirmation modal for cancelling bookings with optional reason. + * Displays grace period policy information. + */ + +'use client'; + +import { useState } from 'react'; +import { X, AlertTriangle, Loader2 } from 'lucide-react'; +import type { BookingDetail } from '@/src/types/bookings'; + +interface CancelBookingModalProps { + booking: BookingDetail; + onConfirm: (reason?: string) => void; + onCancel: () => void; + loading?: boolean; +} + +export default function CancelBookingModal({ + booking, + onConfirm, + onCancel, + loading = false, +}: CancelBookingModalProps) { + const [reason, setReason] = useState(''); + + function handleConfirm() { + onConfirm(reason.trim() || undefined); + } + + // Extract grace period from policies if available + const graceMinutes = booking.policies?.cancel_grace_minutes || 15; + const graceAnchor = booking.policies?.cancel_grace_anchor || 'start'; + + return ( +
+
+ {/* Header */} +
+
+
+ +
+

Cancel Booking

+
+ +
+ + {/* Warning */} +
+

+ This action cannot be undone +

+

+ Cancelling this booking will free up the slot and notify all attendees. +

+
+ + {/* Grace Period Info */} +
+

Policy

+

+ Changes allowed until {graceMinutes} minutes after slot{' '} + {graceAnchor === 'end' ? 'ends' : 'starts'} +

+
+ + {/* Booking Details */} +
+

Booking Details

+
+

+ Court: {booking.slot.court.name} +

+

+ Time:{' '} + {new Date(booking.slot.starts_at).toLocaleString('en-US', { + weekday: 'short', + month: 'short', + day: 'numeric', + hour: 'numeric', + minute: '2-digit', + })} +

+

+ Attendees: {booking.attendees.length} +

+
+
+ + {/* Reason (Optional) */} +
+ +