"use client" import * as React from "react" import FullCalendar from "@fullcalendar/react" import dayGridPlugin from "@fullcalendar/daygrid" import timeGridPlugin from "@fullcalendar/timegrid" import interactionPlugin from "@fullcalendar/interaction" import type { DateSelectArg, DatesSetArg, EventClickArg, EventInput } from "@fullcalendar/core" import { getBookings } from "@/lib/api/bookings" import { getFacilitySessions } from "@/lib/api/facilities" import { ApiError } from "@/lib/api/core-http" import type { Booking, BookingStatus } from "@/lib/models/booking" import type { FacilitySession } from "@/lib/models/facility" import { formatCurrency } from "@/lib/format" import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog" import { StatusBadge } from "@/components/hexa/status-badge" const STATUS_COLORS: Record = { Confirmed: "#2563eb", CheckedIn: "#16a34a", Completed: "#6b7280", NoShow: "#dc2626", Cancelled: "#dc2626", } const SESSION_COLOR = "#7c3aed" const NOT_HELD_COLOR = "#9ca3af" function bookingToEvent(booking: Booking): EventInput { const color = STATUS_COLORS[booking.status] ?? "#2563eb" return { id: `booking-${booking.bookingId}`, title: `${booking.facilityName ?? "Facility"} — ${booking.isGuest ? booking.guestName ?? "Guest" : "Member"}`, start: booking.startTime, end: booking.endTime, backgroundColor: color, borderColor: color, extendedProps: { kind: "booking", booking }, } } function sessionToEvent(session: FacilitySession): EventInput { const notHeld = session.isCancelled const label = session.facilityClassName ?? session.facilityName ?? "Session" const capacityLabel = session.isUnlimitedCapacity ? `${session.bookedCount} booked` : `${session.bookedCount}/${session.capacity} booked` return { id: `session-${session.facilitySessionId}`, title: notHeld ? `${label} — Not held` : `${label} — ${capacityLabel}`, start: session.startTime, end: session.endTime, backgroundColor: notHeld ? "transparent" : SESSION_COLOR, borderColor: notHeld ? NOT_HELD_COLOR : SESSION_COLOR, textColor: notHeld ? NOT_HELD_COLOR : undefined, classNames: notHeld ? ["opacity-70", "border-dashed"] : [], extendedProps: { kind: "session", session }, } } type SelectedEvent = { kind: "booking"; booking: Booking } | { kind: "session"; session: FacilitySession } | null function EventDetailDialog({ event, onOpenChange }: { event: SelectedEvent; onOpenChange: (open: boolean) => void }) { return ( {event?.kind === "booking" ? "Booking Details" : "Class Occurrence"} {event?.kind === "booking" && (

Facility

{event.booking.facilityName ?? "—"}

Status

{event.booking.status}

Who

{event.booking.isGuest ? event.booking.guestName ?? "Guest" : "Member"}

Payment

{event.booking.paymentStatus} {event.booking.amountCharged > 0 && ` · ${formatCurrency(event.booking.amountCharged)}`}

)} {event?.kind === "session" && (

Class

{event.session.facilityClassName ?? event.session.facilityName ?? "—"}

Status

{event.session.isCancelled ? "Not held" : "Scheduled"}

Booked

{event.session.bookedCount} {!event.session.isUnlimitedCapacity && ` / ${event.session.capacity}`}

{event.session.isCancelled && event.session.notHeldReason && (

Reason

{event.session.notHeldReason}

)}
)}
) } interface BookingCalendarProps { /** Fired when the user drags a time range in day/week view -- the parent switches to Book Now * and pre-fills the dragged time. */ onQuickBook?: (draft: { startTime: string; endTime: string }) => void } /** Calendar view of bookings and class occurrences -- one merged view, no per-facility split; * the facility is still chosen in Book Now, same as today. Refetches both for whatever range * FullCalendar currently has visible. */ function BookingCalendar({ onQuickBook }: BookingCalendarProps) { const [events, setEvents] = React.useState([]) const [loading, setLoading] = React.useState(false) const [error, setError] = React.useState(null) const [viewType, setViewType] = React.useState("dayGridMonth") const [selectedEvent, setSelectedEvent] = React.useState(null) async function handleDatesSet(arg: DatesSetArg) { setViewType(arg.view.type) setLoading(true) setError(null) try { const [bookings, sessions] = await Promise.all([ getBookings({ fromDate: arg.start.toISOString(), toDate: arg.end.toISOString(), }), getFacilitySessions({ fromDate: arg.start.toISOString(), toDate: arg.end.toISOString(), includeCancelled: true, }), ]) setEvents([...bookings.map(bookingToEvent), ...sessions.map(sessionToEvent)]) } catch (err) { setError(err instanceof ApiError ? err.message : "Couldn't load the calendar for this range.") } finally { setLoading(false) } } function handleEventClick(arg: EventClickArg) { const props = arg.event.extendedProps as { kind: "booking"; booking: Booking } | { kind: "session"; session: FacilitySession } setSelectedEvent(props) } function handleSelect(arg: DateSelectArg) { onQuickBook?.({ startTime: arg.start.toISOString(), endTime: arg.end.toISOString() }) } return (
{loading && Loading…} {error && {error}} {(Object.keys(STATUS_COLORS) as BookingStatus[]).map((status) => ( {status} ))} Class Not held — open Drag a time range in Day/Week view to start a booking.
!open && setSelectedEvent(null)} />
) } export { BookingCalendar }