698fd4239b
- Added FullCalendar component to display bookings in a calendar view. - Implemented BookingDashboardTab to show summary statistics including bookings today, revenue, checked-in guests, and outstanding payments. - Created BookingInvoicesTab to manage and display booking payment receipts separately from membership invoices. - Enhanced AdhocBookingTab to allow selection of existing members for guest bookings. - Updated BookingRosterTab to include payment methods and improved UI for guest identification. - Introduced new API endpoints for fetching booking invoices and dashboard summaries. - Updated models to support new features including payment methods and invoice structures.
84 lines
2.8 KiB
TypeScript
84 lines
2.8 KiB
TypeScript
"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 { DatesSetArg, EventInput } from "@fullcalendar/core"
|
|
|
|
import { getBookings } from "@/lib/api/bookings"
|
|
import { ApiError } from "@/lib/api/core-http"
|
|
import type { Booking, BookingStatus } from "@/lib/models/booking"
|
|
|
|
const STATUS_COLORS: Record<BookingStatus, string> = {
|
|
Confirmed: "#2563eb",
|
|
CheckedIn: "#16a34a",
|
|
Completed: "#6b7280",
|
|
NoShow: "#dc2626",
|
|
Cancelled: "#dc2626",
|
|
}
|
|
|
|
function bookingToEvent(booking: Booking): EventInput {
|
|
const color = STATUS_COLORS[booking.status] ?? "#2563eb"
|
|
return {
|
|
id: booking.bookingId,
|
|
title: `${booking.facilityName ?? "Facility"} — ${booking.isGuest ? booking.guestName ?? "Guest" : "Member"}`,
|
|
start: booking.startTime,
|
|
end: booking.endTime,
|
|
backgroundColor: color,
|
|
borderColor: color,
|
|
}
|
|
}
|
|
|
|
/** Read-only calendar view of bookings — reuses the existing roster's getBookings query,
|
|
* refetched for whatever range FullCalendar currently has visible. */
|
|
function BookingCalendar() {
|
|
const [events, setEvents] = React.useState<EventInput[]>([])
|
|
const [loading, setLoading] = React.useState(false)
|
|
const [error, setError] = React.useState<string | null>(null)
|
|
|
|
async function handleDatesSet(arg: DatesSetArg) {
|
|
setLoading(true)
|
|
setError(null)
|
|
try {
|
|
const bookings = await getBookings({
|
|
fromDate: arg.start.toISOString(),
|
|
toDate: arg.end.toISOString(),
|
|
})
|
|
setEvents(bookings.map(bookingToEvent))
|
|
} catch (err) {
|
|
setError(err instanceof ApiError ? err.message : "Couldn't load bookings for this range.")
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="rounded-lg border p-3">
|
|
<div className="mb-3 flex flex-wrap items-center gap-3 text-xs text-muted-foreground">
|
|
{loading && <span>Loading…</span>}
|
|
{error && <span className="text-destructive">{error}</span>}
|
|
{(Object.keys(STATUS_COLORS) as BookingStatus[]).map((status) => (
|
|
<span key={status} className="flex items-center gap-1.5">
|
|
<span className="inline-block size-2.5 rounded-full" style={{ backgroundColor: STATUS_COLORS[status] }} />
|
|
{status}
|
|
</span>
|
|
))}
|
|
</div>
|
|
<FullCalendar
|
|
plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}
|
|
initialView="timeGridWeek"
|
|
headerToolbar={{ left: "prev,next today", center: "title", right: "timeGridDay,timeGridWeek,dayGridMonth" }}
|
|
height="auto"
|
|
nowIndicator
|
|
events={events}
|
|
datesSet={handleDatesSet}
|
|
eventDisplay="block"
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export { BookingCalendar }
|