diff --git a/package-lock.json b/package-lock.json index cedba90..bd69310 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,11 @@ "version": "0.1.0", "dependencies": { "@base-ui/react": "^1.6.0", + "@fullcalendar/core": "^6.1.21", + "@fullcalendar/daygrid": "^6.1.21", + "@fullcalendar/interaction": "^6.1.21", + "@fullcalendar/react": "^6.1.21", + "@fullcalendar/timegrid": "^6.1.21", "@microsoft/signalr": "^8.0.17", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -985,6 +990,56 @@ "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", "license": "MIT" }, + "node_modules/@fullcalendar/core": { + "version": "6.1.21", + "resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-6.1.21.tgz", + "integrity": "sha512-t3u/+sqh3Iq7TWtUnVLcGDUE6OWZh0UD3c04bI/l7lSLAgAKr3kngBmhHiQD1QXpwC8ZN5iNqG7a7gOVixhSKQ==", + "license": "MIT", + "dependencies": { + "preact": "~10.12.1" + } + }, + "node_modules/@fullcalendar/daygrid": { + "version": "6.1.21", + "resolved": "https://registry.npmjs.org/@fullcalendar/daygrid/-/daygrid-6.1.21.tgz", + "integrity": "sha512-QYb1y40RGYLlOxKpYWg8O+7njEnKnFG8Tt7qjnubJGR35s1phQg67E+81y2TyAbbm59p2JFOCXGDk9t6KDujIA==", + "license": "MIT", + "peerDependencies": { + "@fullcalendar/core": "~6.1.21" + } + }, + "node_modules/@fullcalendar/interaction": { + "version": "6.1.21", + "resolved": "https://registry.npmjs.org/@fullcalendar/interaction/-/interaction-6.1.21.tgz", + "integrity": "sha512-WPYpqtljDWmU0Xm2cOtFrLlocgxv7cgkOppj34Q6OUUat8a6Cnd6kYo2JR+irP223PE5lBYHFNp1qh7SIpJc0w==", + "license": "MIT", + "peerDependencies": { + "@fullcalendar/core": "~6.1.21" + } + }, + "node_modules/@fullcalendar/react": { + "version": "6.1.21", + "resolved": "https://registry.npmjs.org/@fullcalendar/react/-/react-6.1.21.tgz", + "integrity": "sha512-TLpmGUd5k/PMdCh8XbeFC9PW9wuGvMms1oCxWgXyjK3EFPXAAd0PLfcvwKdyxoAS5eK1E4RJFkjMHvsYHpimcg==", + "license": "MIT", + "peerDependencies": { + "@fullcalendar/core": "~6.1.21", + "react": "^16.7.0 || ^17 || ^18 || ^19", + "react-dom": "^16.7.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/@fullcalendar/timegrid": { + "version": "6.1.21", + "resolved": "https://registry.npmjs.org/@fullcalendar/timegrid/-/timegrid-6.1.21.tgz", + "integrity": "sha512-2DnShx/jallGmb8QCkr6pAOu/zuPhJrP7+uTrAtSnbqsX7GF3lTxqSeNGkTQwsgF5g/ia8udhQ+JNYaE+TN1cQ==", + "license": "MIT", + "dependencies": { + "@fullcalendar/daygrid": "~6.1.21" + }, + "peerDependencies": { + "@fullcalendar/core": "~6.1.21" + } + }, "node_modules/@hono/node-server": { "version": "1.19.14", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", @@ -8682,6 +8737,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/preact": { + "version": "10.12.1", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.12.1.tgz", + "integrity": "sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", diff --git a/package.json b/package.json index 53a9a40..f78c047 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,11 @@ }, "dependencies": { "@base-ui/react": "^1.6.0", + "@fullcalendar/core": "^6.1.21", + "@fullcalendar/daygrid": "^6.1.21", + "@fullcalendar/interaction": "^6.1.21", + "@fullcalendar/react": "^6.1.21", + "@fullcalendar/timegrid": "^6.1.21", "@microsoft/signalr": "^8.0.17", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/src/components/hexa/views/adhoc-booking-tab.tsx b/src/components/hexa/views/adhoc-booking-tab.tsx index af0cfbb..3e70807 100644 --- a/src/components/hexa/views/adhoc-booking-tab.tsx +++ b/src/components/hexa/views/adhoc-booking-tab.tsx @@ -6,9 +6,11 @@ import { toast } from "sonner" import { getFacilityAvailability } from "@/lib/api/facilities" import { createGuestAdhocBooking } from "@/lib/api/bookings" +import { getMembers } from "@/lib/api/customers" import { ApiError } from "@/lib/api/core-http" import type { FacilityAvailability } from "@/lib/models/facility" import type { Facility } from "@/lib/models/facility" +import { useFetch } from "@/lib/use-fetch" import { useMutation } from "@/lib/use-mutation" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" @@ -43,9 +45,26 @@ function AdhocBookingTab({ facilities }: AdhocBookingTabProps) { const [minutes, setMinutes] = React.useState("0") const [pricePerHour, setPricePerHour] = React.useState(() => String(adhocFacilities[0]?.pricePerHour ?? 0)) const [partySize, setPartySize] = React.useState("") + const [customerId, setCustomerId] = React.useState("") const [guestName, setGuestName] = React.useState("") const [guestPhone, setGuestPhone] = React.useState("") + const membersFetch = useFetch(() => getMembers()) + const members = (membersFetch.data ?? []).filter((m) => !!m.customer) + const memberOptions = [ + { label: "New guest (not registered)", value: "none" }, + ...members.map((m) => ({ label: m.customer!.cusName ?? m.customer!.primaryPH ?? "Member", value: m.fkCustomerId })), + ] + + function selectMember(id: string) { + setCustomerId(id === "none" ? "" : id) + const member = id !== "none" ? members.find((m) => m.fkCustomerId === id) : undefined + if (member?.customer) { + setGuestName(member.customer.cusName ?? "") + setGuestPhone(member.customer.primaryPH ?? "") + } + } + const [availability, setAvailability] = React.useState(null) const [availabilityError, setAvailabilityError] = React.useState(null) const { busy: checking, run: runCheck } = useMutation() @@ -96,6 +115,7 @@ function AdhocBookingTab({ facilities }: AdhocBookingTabProps) { guestPhone: guestPhone.trim(), partySize: partySize ? Number(partySize) : undefined, pricePerHour: Number(pricePerHour), + customerId: customerId || undefined, }) ) if (bookingResult.shareUrl) { @@ -111,6 +131,7 @@ function AdhocBookingTab({ facilities }: AdhocBookingTabProps) { setGuestName("") setGuestPhone("") setPartySize("") + setCustomerId("") setAvailability(null) handleCheckAvailability() } catch { @@ -247,7 +268,23 @@ function AdhocBookingTab({ facilities }: AdhocBookingTabProps) { )} -
+
+ + +
+ +
{row.status}
@@ -464,7 +469,7 @@ function BookingRosterTab({ facilities }: BookingRosterTabProps) { )} - {row.status === "Confirmed" && row.isGuest && row.paymentStatus === "Unpaid" && ( + {canMarkPaid(row) && ( @@ -540,20 +545,14 @@ function BookingRosterTab({ facilities }: BookingRosterTabProps) { - !open && setCompleteTarget(null)}> - - - Complete this booking? - - Ends the session and releases the RFID tag (if one was assigned) back to the available pool. - - - - Not Yet - Complete - - - + !open && setCompleteTarget(null)} + onCompleted={() => { + reload() + availableTagsFetch.reload() + }} + />
) } @@ -568,6 +567,7 @@ interface AddWalkInGuestDialogProps { function AddWalkInGuestDialog({ open, onOpenChange, facilities, onCreated }: AddWalkInGuestDialogProps) { const [facilityId, setFacilityId] = React.useState("") const [sessionId, setSessionId] = React.useState("") + const [customerId, setCustomerId] = React.useState("") const [guestName, setGuestName] = React.useState("") const [guestPhone, setGuestPhone] = React.useState("") const { busy, run } = useMutation() @@ -578,6 +578,7 @@ function AddWalkInGuestDialog({ open, onOpenChange, facilities, onCreated }: Add if (open) { setFacilityId(facilities[0]?.facilityId ?? "") setSessionId("") + setCustomerId("") setGuestName("") setGuestPhone("") } @@ -586,6 +587,22 @@ function AddWalkInGuestDialog({ open, onOpenChange, facilities, onCreated }: Add const sessionsFetch = useFetch(() => (facilityId ? getFacilitySessions({ facilityId }) : Promise.resolve([]))) const sessions = (sessionsFetch.data ?? []).filter((s) => !s.isCancelled && new Date(s.startTime) > new Date()) + const membersFetch = useFetch(() => getMembers()) + const members = (membersFetch.data ?? []).filter((m) => !!m.customer) + const memberOptions = [ + { label: "New guest (not registered)", value: "none" }, + ...members.map((m) => ({ label: m.customer!.cusName ?? m.customer!.primaryPH ?? "Member", value: m.fkCustomerId })), + ] + + function selectMember(id: string) { + setCustomerId(id === "none" ? "" : id) + const member = id !== "none" ? members.find((m) => m.fkCustomerId === id) : undefined + if (member?.customer) { + setGuestName(member.customer.cusName ?? "") + setGuestPhone(member.customer.primaryPH ?? "") + } + } + const facilityOptions = facilities.map((f) => ({ label: f.name, value: f.facilityId })) const sessionOptions = sessions.map((s) => ({ label: `${formatDateTime(s.startTime)} (${s.bookedCount}/${s.capacity})`, @@ -599,7 +616,12 @@ function AddWalkInGuestDialog({ open, onOpenChange, facilities, onCreated }: Add if (!canSubmit) return try { const booking = await run(() => - createGuestBooking({ facilitySessionId: sessionId, guestName: guestName.trim(), guestPhone: guestPhone.trim() }) + createGuestBooking({ + facilitySessionId: sessionId, + guestName: guestName.trim(), + guestPhone: guestPhone.trim(), + customerId: customerId || undefined, + }) ) if (booking.shareUrl) { navigator.clipboard.writeText(booking.shareUrl) @@ -670,6 +692,22 @@ function AddWalkInGuestDialog({ open, onOpenChange, facilities, onCreated }: Add +
+ + +
+