From 53ed72d032664a644b43b382a368c35bf24c4192 Mon Sep 17 00:00:00 2001 From: joshua Date: Wed, 21 Oct 2026 15:32:17 +0200 Subject: [PATCH] Add bundle deals to manual registration Lists the event's deals and shows the Complete bundle / + cards, as at the door and on the kiosk. Part of 1.28.1. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 +- .../app/dashboard/supervisor/manual/page.tsx | 23 +++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 347ebd2..dea28f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ and this project follows [Semantic Versioning](https://semver.org/). ### Added -- At the door and the self-service kiosk now list the event's bundle deals and show the same bundle cards as the register form: once a ticket from a bundle is picked, **Complete bundle** tops up the ticket numbers, and **+** adds another full set. +- At the door, manual registration and the self-service kiosk now list the event's bundle deals and show the same bundle cards as the register form: once a ticket from a bundle is picked, **Complete bundle** tops up the ticket numbers, and **+** adds another full set. ### Fixed diff --git a/frontend/src/app/dashboard/supervisor/manual/page.tsx b/frontend/src/app/dashboard/supervisor/manual/page.tsx index 837fef2..146ecef 100644 --- a/frontend/src/app/dashboard/supervisor/manual/page.tsx +++ b/frontend/src/app/dashboard/supervisor/manual/page.tsx @@ -7,12 +7,12 @@ import { apiFetch, fetchAllUsers } from "@/lib/api"; import { scoreUser } from "@/lib/fuzzyMatch"; import { useDismissingState } from "@/hooks/useDismissingState"; import { CouponCodeField, AppliedCoupon } from "@/components/shared/CouponCodeField"; -import { BundleSavings, useBundleQuote } from "@/components/shared/BundleDeals"; +import { BundleBuilder, BundleOffers, BundleSavings, useBundleQuote } from "@/components/shared/BundleDeals"; import { AttendeeNamesEditor, AttendeeEntries, TicketDelivery, attendeesPayload, consentDetailsMissing } from "@/components/shared/AttendeeNamesEditor"; import { UserPlus, CreditCard } from "lucide-react"; import { errorMessage } from "@/lib/utils"; import { optionUnitNow, variantUnitNow } from "@/lib/pricing"; -import type { Event, EventOption, Registration, RegistrationOption, User } from "@/types"; +import type { Event, EventOption, PublicBundle, Registration, RegistrationOption, User } from "@/types"; import { BackLink } from "@/components/shared/BackLink"; // ─── Pricing helpers ───────────────────────────────────────────────────────── @@ -60,6 +60,7 @@ export default function ManualRegistrationPage() { const [guest, setGuest] = useState({ name: "", email: "", phoneNumber: "" }); const [notifPref, setNotifPref] = useState<"email" | "whatsapp" | "both">("email"); const [quantities, setQuantities] = useState>({}); + const [bundles, setBundles] = useState([]); // User search state const [userQuery, setUserQuery] = useState(""); @@ -267,6 +268,16 @@ export default function ManualRegistrationPage() { } }, [selectedEventId, events]); + // The events list doesn't carry bundle deals; the single-event response does. + useEffect(() => { + if (!selectedEventId) { setBundles([]); return; } + let cancelled = false; + apiFetch(`/api/events/${selectedEventId}`, { authToken: token || undefined }) + .then(ev => { if (!cancelled) setBundles(ev?.bundles || []); }) + .catch(() => { if (!cancelled) setBundles([]); }); + return () => { cancelled = true; }; + }, [selectedEventId, token]); + const totalDue = useMemo(() => { return options.reduce((sum, o) => { if ((o.variants || []).length > 0) { @@ -535,6 +546,7 @@ export default function ManualRegistrationPage() {
3) Select ticket options
+ {options.length > 0 && } {options.length === 0 ? (
Select an event to view options.
) : ( @@ -598,6 +610,13 @@ export default function ManualRegistrationPage() { })}
)} + quantities[v ? `${o}::${v}` : o] || 0} + onSetQuantity={(o, v, n) => setQuantities(q => ({ ...q, [v ? `${o}::${v}` : o]: n }))} + /> {selectedEventAllowsNames && (