feat: integrate FullCalendar for booking management and add booking dashboard
- 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.
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
"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 }
|
||||
Reference in New Issue
Block a user