@burdenoff/microfe-eventfullymanaged 2026.704.7 → 2026.704.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/eventfullymanaged/components/EventCharts.js.map +1 -1
- package/dist/eventfullymanaged/pages/dashboard/DashboardPage.js.map +1 -1
- package/dist/eventfullymanaged/pages/events/EventDetailPage.js +1 -2
- package/dist/eventfullymanaged/pages/events/EventDetailPage.js.map +1 -1
- package/dist/eventfullymanaged/pages/modules/AnalyticsPage.js.map +1 -1
- package/dist/eventfullymanaged/pages/modules/CommunicationPage.js.map +1 -1
- package/dist/eventfullymanaged/pages/modules/EventsPage.js +223 -118
- package/dist/eventfullymanaged/pages/modules/EventsPage.js.map +1 -1
- package/dist/eventfullymanaged/pages/modules/RegistrationPage.js.map +1 -1
- package/dist/eventfullymanaged/pages/modules/SponsorDetailsPage.js.map +1 -1
- package/dist/eventfullymanaged/pages/registration/AttendeeFormPage.js.map +1 -1
- package/dist/eventfullymanaged/pages/registration/RegisterAttendeePage.js.map +1 -1
- package/dist/eventfullymanaged/pages/speakers/SpeakersPage.js.map +1 -1
- package/dist/eventfullymanaged/pages/sponsors/SponsorsListPage.js.map +1 -1
- package/dist/eventfullymanaged/pages/venues/FloorPlansPage.js.map +1 -1
- package/dist/eventfullymanaged/pages/venues/VenueDetailPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RegisterAttendeePage.js","names":[],"sources":["../../../../src/eventfullymanaged/pages/registration/RegisterAttendeePage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport {\n ArrowLeft,\n ArrowRight,\n BadgePercent,\n CalendarDays,\n Check,\n CheckCircle2,\n Minus,\n Plus,\n Ticket,\n UserRound,\n} from 'lucide-react';\nimport {\n Button,\n GlassCard,\n GradientText,\n Input,\n PagePurpose,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n Stepper,\n} from '@burdenoff/fe-libs/ui';\n\nimport {\n useAttendeesQuery,\n useCreateAttendeeMutation,\n useCreateRegistrationMutation,\n useEventsQuery,\n usePromoCodesQuery,\n useTicketTypesQuery,\n type PromoCodeGql,\n} from '../../api/eventApi';\nimport { useEventfullyManaged } from '../../context/EventfullyManagedContext';\nimport { computeDiscount, describeDiscount, formatMoney } from '../../lib/format';\nimport { describeError, mutationSucceeded } from '../../lib/mutationResult';\nimport { notify } from '../../lib/toast';\n\ntype Confirmation = {\n attendeeName: string;\n eventName: string;\n ticketName: string;\n quantity: number;\n status: string;\n subtotal: number;\n discount: number;\n total: number;\n currency: string;\n promoCode: string | null;\n free: boolean;\n};\n\nconst STEPS = [\n { key: 'event', label: 'Event', description: 'Choose the event' },\n { key: 'ticket', label: 'Ticket', description: 'Pick a ticket type' },\n { key: 'attendee', label: 'Attendee', description: 'Who is coming' },\n { key: 'confirm', label: 'Confirm', description: 'Review & submit' },\n];\n\nexport default function RegisterAttendeePage() {\n const { navigate } = useEventfullyManaged();\n\n const [step, setStep] = useState(0);\n const [eventId, setEventId] = useState('');\n const [ticketTypeId, setTicketTypeId] = useState('');\n const [attendeeMode, setAttendeeMode] = useState<'new' | 'existing'>('new');\n const [attendeeId, setAttendeeId] = useState('');\n const [fullName, setFullName] = useState('');\n const [email, setEmail] = useState('');\n const [company, setCompany] = useState('');\n const [phone, setPhone] = useState('');\n const [quantity, setQuantity] = useState(1);\n const [promoInput, setPromoInput] = useState('');\n const [appliedPromo, setAppliedPromo] = useState<PromoCodeGql | null>(null);\n const [promoError, setPromoError] = useState('');\n const [billingAccountId, setBillingAccountId] = useState('');\n const [confirmation, setConfirmation] = useState<Confirmation | null>(null);\n\n const { data: eventsData, loading: eventsLoading } = useEventsQuery({ limit: 100, offset: 0 });\n const { data: ticketData, loading: ticketsLoading } = useTicketTypesQuery({\n eventId: eventId || undefined,\n limit: 100,\n offset: 0,\n });\n const { data: attendeesData } = useAttendeesQuery({ eventId: eventId || undefined, limit: 100, offset: 0 });\n const { data: promoData } = usePromoCodesQuery({ eventId: eventId || undefined, limit: 100, offset: 0 });\n\n const events = useMemo(() => eventsData?.events.items ?? [], [eventsData]);\n const tickets = useMemo(\n () =>\n (ticketData?.ticketTypes.items ?? []).filter((ticket) => (!eventId || ticket.eventId === eventId) && ticket.isActive),\n [ticketData, eventId]\n );\n const attendees = useMemo(\n () => (attendeesData?.attendees.items ?? []).filter((attendee) => !eventId || attendee.eventId === eventId),\n [attendeesData, eventId]\n );\n const activePromos = useMemo(\n () =>\n (promoData?.promoCodes.items ?? []).filter((promo) => promo.isActive && (!eventId || promo.eventId === eventId)),\n [promoData, eventId]\n );\n\n const [createAttendee, { loading: creatingAttendee }] = useCreateAttendeeMutation();\n const [createRegistration, { loading: creatingRegistration }] = useCreateRegistrationMutation();\n const saving = creatingAttendee || creatingRegistration;\n\n const selectedEvent = useMemo(() => events.find((item) => item.id === eventId), [events, eventId]);\n const selectedTicket = useMemo(() => tickets.find((ticket) => ticket.id === ticketTypeId), [tickets, ticketTypeId]);\n const selectedAttendee = useMemo(\n () => attendees.find((attendee) => attendee.id === attendeeId),\n [attendees, attendeeId]\n );\n\n const qty = Math.max(1, quantity);\n const unitPrice = selectedTicket ? Number(selectedTicket.price) : 0;\n const currency = selectedTicket?.currency ?? 'USD';\n const subtotal = Number.isFinite(unitPrice) ? unitPrice * qty : 0;\n const discount = appliedPromo\n ? computeDiscount(subtotal, appliedPromo.discountType, Number(appliedPromo.discountValue))\n : 0;\n const total = Math.max(subtotal - discount, 0);\n const hasPositiveAmount = subtotal > 0;\n\n const attendeeName =\n attendeeMode === 'existing' ? (selectedAttendee?.fullName ?? '') : fullName.trim();\n\n const onEventChange = (value: string) => {\n setEventId(value);\n setTicketTypeId('');\n setAttendeeMode('new');\n setAttendeeId('');\n setAppliedPromo(null);\n setPromoInput('');\n setPromoError('');\n };\n\n const isStepValid = (index: number): boolean => {\n switch (index) {\n case 0:\n return Boolean(eventId);\n case 1:\n return Boolean(ticketTypeId);\n case 2:\n return attendeeMode === 'existing' ? Boolean(attendeeId) : Boolean(fullName.trim() && email.trim());\n default:\n return true;\n }\n };\n\n const canGoTo = (index: number): boolean => {\n if (index <= step) return true;\n for (let i = 0; i < index; i += 1) {\n if (!isStepValid(i)) return false;\n }\n return true;\n };\n\n const goToStep = (index: number) => {\n if (canGoTo(index)) {\n setStep(index);\n }\n };\n\n const goNext = () => {\n if (!isStepValid(step)) {\n const messages = [\n 'Choose an event to continue.',\n 'Pick a ticket type to continue.',\n 'Select or enter an attendee to continue.',\n ];\n notify.error('One more thing', messages[step] ?? 'Complete this step to continue.');\n return;\n }\n setStep((current) => Math.min(current + 1, STEPS.length - 1));\n };\n\n const goBack = () => setStep((current) => Math.max(current - 1, 0));\n\n const applyPromo = () => {\n const code = promoInput.trim().toUpperCase();\n if (!code) {\n setAppliedPromo(null);\n setPromoError('');\n return;\n }\n const match = activePromos.find((promo) => promo.code.toUpperCase() === code);\n if (!match) {\n setAppliedPromo(null);\n setPromoError('That code is not active for this event.');\n return;\n }\n setAppliedPromo(match);\n setPromoError('');\n notify.success('Promo applied', describeDiscount(match.discountType, match.discountValue, currency));\n };\n\n const clearPromo = () => {\n setAppliedPromo(null);\n setPromoInput('');\n setPromoError('');\n };\n\n const resetAll = () => {\n setStep(0);\n setEventId('');\n setTicketTypeId('');\n setAttendeeMode('new');\n setAttendeeId('');\n setFullName('');\n setEmail('');\n setCompany('');\n setPhone('');\n setQuantity(1);\n clearPromo();\n setBillingAccountId('');\n };\n\n const onConfirm = async () => {\n if (!eventId || !ticketTypeId) {\n notify.error('Incomplete', 'Pick an event and ticket first.');\n return;\n }\n if (attendeeMode === 'new' && (!fullName.trim() || !email.trim())) {\n notify.error('Attendee required', 'Enter a name and email, or pick an existing attendee.');\n return;\n }\n if (attendeeMode === 'existing' && !attendeeId) {\n notify.error('Attendee required', 'Select an existing attendee.');\n return;\n }\n\n try {\n let resolvedAttendeeId = attendeeId;\n let resolvedAttendeeName = selectedAttendee?.fullName ?? '';\n\n if (attendeeMode === 'new') {\n const attendeeResponse = await createAttendee({\n variables: {\n input: {\n eventId,\n fullName: fullName.trim(),\n email: email.trim(),\n company: company.trim() || null,\n phone: phone.trim() || null,\n profile: {},\n metadata: {},\n },\n },\n });\n const createdId = attendeeResponse.data?.createAttendee?.id;\n if (!mutationSucceeded(attendeeResponse, createdId, 'Could not create attendee') || !createdId) {\n return;\n }\n resolvedAttendeeId = createdId;\n resolvedAttendeeName = fullName.trim();\n }\n\n // The money leg is owned by global-billing-svc — never price locally. We\n // forward the promo code + optional billing-account reference via metadata\n // so the backend can attach the charge; totalAmount is left for the backend.\n const metadata: Record<string, unknown> = {};\n if (appliedPromo) {\n metadata.promoCode = appliedPromo.code;\n }\n if (hasPositiveAmount && billingAccountId.trim()) {\n metadata.billingAccountId = billingAccountId.trim();\n }\n\n const registrationResponse = await createRegistration({\n variables: {\n input: {\n eventId,\n attendeeId: resolvedAttendeeId,\n ticketTypeId,\n quantity: qty,\n currency,\n status: 'CONFIRMED',\n metadata,\n },\n },\n });\n if (\n !mutationSucceeded(\n registrationResponse,\n registrationResponse.data?.createRegistration?.id,\n 'Could not create registration'\n )\n ) {\n return;\n }\n\n const registration = registrationResponse.data?.createRegistration;\n setConfirmation({\n attendeeName: resolvedAttendeeName || 'Attendee',\n eventName: selectedEvent?.name ?? 'Event',\n ticketName: selectedTicket?.name ?? 'Ticket',\n quantity: qty,\n status: registration?.status ?? 'CONFIRMED',\n subtotal,\n discount,\n total,\n currency,\n promoCode: appliedPromo?.code ?? null,\n free: !hasPositiveAmount,\n });\n notify.success('Registration confirmed', resolvedAttendeeName || undefined);\n } catch (error) {\n notify.error('Could not register attendee', describeError(error));\n }\n };\n\n if (confirmation) {\n return (\n <div className=\"mx-auto max-w-2xl space-y-6 p-6\">\n <div>\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => navigate('/registration')}>\n <ArrowLeft className=\"h-4 w-4\" />\n Back to Registration\n </Button>\n </div>\n <GlassCard treatment=\"glass\" glow className=\"p-8 text-center\">\n <div className=\"mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-status-success-bg-subtle\">\n <CheckCircle2 className=\"h-8 w-8 text-status-success-text\" aria-hidden=\"true\" />\n </div>\n <h1 className=\"mt-4 text-2xl font-bold text-text-primary\">Registration confirmed</h1>\n <p className=\"mt-2 text-sm text-text-secondary\">\n {confirmation.attendeeName} is registered for {confirmation.eventName}.\n </p>\n\n <dl className=\"mx-auto mt-6 max-w-sm space-y-2 rounded-card border border-border-seam bg-bg-surface p-4 text-left text-sm\">\n <div className=\"flex justify-between\">\n <dt className=\"text-text-secondary\">Ticket</dt>\n <dd className=\"font-medium text-text-primary\">\n {confirmation.ticketName}\n {confirmation.quantity > 1 ? ` × ${confirmation.quantity}` : ''}\n </dd>\n </div>\n {!confirmation.free ? (\n <>\n <div className=\"flex justify-between\">\n <dt className=\"text-text-secondary\">Subtotal</dt>\n <dd className=\"tabular-nums text-text-primary\">\n {formatMoney(confirmation.subtotal, confirmation.currency)}\n </dd>\n </div>\n {confirmation.discount > 0 ? (\n <div className=\"flex justify-between\">\n <dt className=\"text-text-secondary\">\n Promo{confirmation.promoCode ? ` · ${confirmation.promoCode}` : ''}\n </dt>\n <dd className=\"tabular-nums text-status-success-text\">\n −{formatMoney(confirmation.discount, confirmation.currency)}\n </dd>\n </div>\n ) : null}\n </>\n ) : null}\n <div className=\"flex justify-between border-t border-border-seam pt-2\">\n <dt className=\"font-semibold text-text-primary\">Total</dt>\n <dd className=\"font-bold tabular-nums text-text-primary\">\n {confirmation.free ? 'Free' : formatMoney(confirmation.total, confirmation.currency)}\n </dd>\n </div>\n <div className=\"flex justify-between\">\n <dt className=\"text-text-secondary\">Status</dt>\n <dd className=\"font-medium text-text-primary\">{confirmation.status}</dd>\n </div>\n </dl>\n {!confirmation.free ? (\n <p className=\"mt-3 text-xs text-text-muted\">Final pricing and payment are settled by the billing service.</p>\n ) : null}\n\n <div className=\"mt-8 flex flex-col justify-center gap-3 sm:flex-row\">\n <Button type=\"button\" variant=\"outline\" onClick={() => navigate('/registration')}>\n Back to Registrations\n </Button>\n <Button\n type=\"button\"\n onClick={() => {\n setConfirmation(null);\n resetAll();\n }}\n >\n Register another\n </Button>\n </div>\n </GlassCard>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6 p-6\">\n <div>\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => navigate('/registration')}>\n <ArrowLeft className=\"h-4 w-4\" />\n Back to Registration\n </Button>\n <h1 className=\"mt-3 text-2xl font-bold text-text-primary\">\n <GradientText>Register Attendee</GradientText>\n </h1>\n <p className=\"mt-1 text-sm text-text-secondary\">\n Walk through the event, ticket and attendee in a few quick steps.\n </p>\n </div>\n\n <PagePurpose>\n Register a person for an event against a ticket type. Pick the event and ticket, choose an existing attendee or\n add a new one, apply a promo code if they have one, then confirm. The order summary previews the price — final\n pricing and payment are handled by the billing service.\n </PagePurpose>\n\n <Stepper steps={STEPS} active={step} onStepClick={goToStep} />\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-[minmax(0,1fr)_20rem]\">\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-5 sm:p-6\">\n {/* STEP 0 — EVENT */}\n {step === 0 ? (\n <div className=\"space-y-4\">\n <div>\n <h2 className=\"text-lg font-semibold text-text-primary\">Which event?</h2>\n <p className=\"mt-1 text-sm text-text-secondary\">Choose the event this attendee is signing up for.</p>\n </div>\n {eventsLoading ? (\n <p className=\"text-sm text-text-muted\">Loading events…</p>\n ) : events.length === 0 ? (\n <div className=\"rounded-card border border-dashed border-border-default p-6 text-center text-sm text-text-secondary\">\n No events yet.{' '}\n <button\n type=\"button\"\n className=\"font-medium text-text-link underline-offset-2 hover:underline\"\n onClick={() => navigate('/events/create')}\n >\n Create an event\n </button>{' '}\n first.\n </div>\n ) : (\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2\">\n {events.map((eventItem) => {\n const selected = eventItem.id === eventId;\n return (\n <button\n key={eventItem.id}\n type=\"button\"\n onClick={() => onEventChange(eventItem.id)}\n aria-pressed={selected}\n className={`flex items-center gap-3 rounded-card border p-4 text-left transition-colors ${\n selected\n ? 'border-action-primary-border bg-accent-wash'\n : 'border-border-seam bg-bg-surface hover:border-border-strong'\n }`}\n >\n <span\n className={`flex h-10 w-10 shrink-0 items-center justify-center rounded-input ${\n selected ? 'bg-action-primary-bg text-action-primary-text' : 'bg-bg-sunken text-text-muted'\n }`}\n >\n <CalendarDays className=\"h-5 w-5\" aria-hidden=\"true\" />\n </span>\n <span className=\"min-w-0 flex-1 truncate text-sm font-medium text-text-primary\">\n {eventItem.name}\n </span>\n {selected ? <Check className=\"h-4 w-4 shrink-0 text-action-primary-bg\" aria-hidden=\"true\" /> : null}\n </button>\n );\n })}\n </div>\n )}\n </div>\n ) : null}\n\n {/* STEP 1 — TICKET */}\n {step === 1 ? (\n <div className=\"space-y-4\">\n <div>\n <h2 className=\"text-lg font-semibold text-text-primary\">Ticket type</h2>\n <p className=\"mt-1 text-sm text-text-secondary\">Pick the ticket and how many they need.</p>\n </div>\n {ticketsLoading ? (\n <p className=\"text-sm text-text-muted\">Loading tickets…</p>\n ) : tickets.length === 0 ? (\n <div className=\"rounded-card border border-dashed border-border-default p-6 text-center text-sm text-text-secondary\">\n This event has no active ticket types.\n </div>\n ) : (\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3\">\n {tickets.map((ticket) => {\n const selected = ticket.id === ticketTypeId;\n const cap = ticket.capacity ?? null;\n const sold = ticket.soldCount ?? 0;\n const pct = cap && cap > 0 ? Math.min(100, Math.round((sold / cap) * 100)) : null;\n const soldOut = cap != null && sold >= cap;\n return (\n <button\n key={ticket.id}\n type=\"button\"\n disabled={soldOut && !selected}\n onClick={() => setTicketTypeId(ticket.id)}\n aria-pressed={selected}\n className={`flex flex-col gap-3 rounded-card border p-4 text-left transition-colors disabled:cursor-not-allowed disabled:opacity-60 ${\n selected\n ? 'border-action-primary-border bg-accent-wash ring-1 ring-border-focus'\n : 'border-border-seam bg-bg-surface hover:border-border-strong'\n }`}\n >\n <div className=\"flex items-start justify-between gap-2\">\n <span className=\"text-sm font-semibold text-text-primary\">{ticket.name}</span>\n {selected ? (\n <Check className=\"h-4 w-4 shrink-0 text-action-primary-bg\" aria-hidden=\"true\" />\n ) : (\n <Ticket className=\"h-4 w-4 shrink-0 text-text-muted\" aria-hidden=\"true\" />\n )}\n </div>\n <span className=\"text-lg font-bold tabular-nums text-text-primary\">\n {Number(ticket.price) > 0 ? formatMoney(Number(ticket.price), ticket.currency) : 'Free'}\n </span>\n {cap != null ? (\n <div className=\"space-y-1\">\n <div className=\"h-1.5 w-full overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-action-primary-bg\"\n style={{ width: `${pct ?? 0}%` }}\n />\n </div>\n <span className=\"text-xs text-text-muted\">\n {soldOut ? 'Sold out' : `${sold} / ${cap} sold`}\n </span>\n </div>\n ) : (\n <span className=\"text-xs text-text-muted\">{sold} sold</span>\n )}\n </button>\n );\n })}\n </div>\n )}\n\n <div className=\"flex flex-wrap items-end gap-4 border-t border-border-seam pt-4\">\n <div className=\"space-y-1\">\n <span className=\"text-sm font-medium text-text-primary\">Quantity</span>\n <div className=\"flex items-center gap-1\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"icon\"\n aria-label=\"Decrease quantity\"\n disabled={qty <= 1}\n onClick={() => setQuantity((value) => Math.max(1, value - 1))}\n >\n <Minus className=\"h-4 w-4\" />\n </Button>\n <span className=\"w-10 text-center text-base font-semibold tabular-nums text-text-primary\">{qty}</span>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"icon\"\n aria-label=\"Increase quantity\"\n onClick={() => setQuantity((value) => value + 1)}\n >\n <Plus className=\"h-4 w-4\" />\n </Button>\n </div>\n </div>\n </div>\n </div>\n ) : null}\n\n {/* STEP 2 — ATTENDEE */}\n {step === 2 ? (\n <div className=\"space-y-4\">\n <div>\n <h2 className=\"text-lg font-semibold text-text-primary\">Attendee information</h2>\n <p className=\"mt-1 text-sm text-text-secondary\">Add someone new or pick an existing attendee.</p>\n </div>\n\n <div className=\"inline-flex rounded-input border border-border-seam bg-bg-sunken p-1\">\n <button\n type=\"button\"\n onClick={() => setAttendeeMode('new')}\n className={`rounded-[calc(var(--radius-input)-2px)] px-3 py-1.5 text-sm font-medium transition-colors ${\n attendeeMode === 'new'\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-secondary hover:text-text-primary'\n }`}\n >\n New attendee\n </button>\n <button\n type=\"button\"\n onClick={() => setAttendeeMode('existing')}\n disabled={attendees.length === 0}\n className={`rounded-[calc(var(--radius-input)-2px)] px-3 py-1.5 text-sm font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50 ${\n attendeeMode === 'existing'\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-secondary hover:text-text-primary'\n }`}\n >\n Existing attendee\n </button>\n </div>\n\n {attendeeMode === 'existing' ? (\n <label className=\"block space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Attendee</span>\n <Select value={attendeeId} onValueChange={setAttendeeId}>\n <SelectTrigger aria-label=\"Attendee\">\n <SelectValue placeholder=\"Select an attendee\" />\n </SelectTrigger>\n <SelectContent>\n {attendees.map((attendee) => (\n <SelectItem key={attendee.id} value={attendee.id}>\n {attendee.fullName} · {attendee.email}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </label>\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Full name</span>\n <Input value={fullName} onChange={(e) => setFullName(e.target.value)} placeholder=\"Enter full name\" />\n </label>\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Email</span>\n <Input\n type=\"email\"\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder=\"Enter email address\"\n />\n </label>\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Company</span>\n <Input\n value={company}\n onChange={(e) => setCompany(e.target.value)}\n placeholder=\"Enter company name\"\n />\n </label>\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Phone</span>\n <Input value={phone} onChange={(e) => setPhone(e.target.value)} placeholder=\"Optional\" />\n </label>\n </div>\n )}\n\n {/* Promo code */}\n <div className=\"space-y-2 border-t border-border-seam pt-4\">\n <span className=\"text-sm font-medium text-text-primary\">Promo code (optional)</span>\n <div className=\"flex flex-col gap-2 sm:flex-row\">\n <Input\n value={promoInput}\n onChange={(e) => {\n setPromoInput(e.target.value);\n setPromoError('');\n }}\n placeholder=\"Enter promo code\"\n className=\"sm:flex-1\"\n onKeyDown={(e) => {\n if (e.key === 'Enter') {\n e.preventDefault();\n applyPromo();\n }\n }}\n />\n {appliedPromo ? (\n <Button type=\"button\" variant=\"outline\" onClick={clearPromo}>\n Remove\n </Button>\n ) : (\n <Button type=\"button\" variant=\"outline\" onClick={applyPromo} disabled={!promoInput.trim()}>\n Apply\n </Button>\n )}\n </div>\n {appliedPromo ? (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-status-success-bg-subtle px-2.5 py-0.5 text-xs font-medium text-status-success-text\">\n <BadgePercent className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />\n {describeDiscount(appliedPromo.discountType, appliedPromo.discountValue, currency)} applied\n </span>\n ) : promoError ? (\n <span className=\"text-xs text-status-error-text\">{promoError}</span>\n ) : activePromos.length > 0 ? (\n <div className=\"flex flex-wrap items-center gap-1.5 text-xs text-text-muted\">\n <span>Available:</span>\n {activePromos.slice(0, 4).map((promo) => (\n <button\n key={promo.id}\n type=\"button\"\n onClick={() => {\n setPromoInput(promo.code);\n setAppliedPromo(promo);\n setPromoError('');\n }}\n className=\"rounded-full border border-border-seam bg-bg-surface px-2 py-0.5 font-medium text-text-secondary hover:border-border-strong hover:text-text-primary\"\n >\n {promo.code}\n </button>\n ))}\n </div>\n ) : null}\n </div>\n </div>\n ) : null}\n\n {/* STEP 3 — CONFIRM */}\n {step === 3 ? (\n <div className=\"space-y-4\">\n <div>\n <h2 className=\"text-lg font-semibold text-text-primary\">Review & confirm</h2>\n <p className=\"mt-1 text-sm text-text-secondary\">Double-check the details before registering.</p>\n </div>\n <dl className=\"divide-y divide-border-seam rounded-card border border-border-seam bg-bg-surface text-sm\">\n <div className=\"flex items-center gap-3 p-3\">\n <CalendarDays className=\"h-4 w-4 text-text-muted\" aria-hidden=\"true\" />\n <dt className=\"text-text-secondary\">Event</dt>\n <dd className=\"ml-auto font-medium text-text-primary\">{selectedEvent?.name ?? '—'}</dd>\n </div>\n <div className=\"flex items-center gap-3 p-3\">\n <Ticket className=\"h-4 w-4 text-text-muted\" aria-hidden=\"true\" />\n <dt className=\"text-text-secondary\">Ticket</dt>\n <dd className=\"ml-auto font-medium text-text-primary\">\n {selectedTicket?.name ?? '—'}\n {qty > 1 ? ` × ${qty}` : ''}\n </dd>\n </div>\n <div className=\"flex items-center gap-3 p-3\">\n <UserRound className=\"h-4 w-4 text-text-muted\" aria-hidden=\"true\" />\n <dt className=\"text-text-secondary\">Attendee</dt>\n <dd className=\"ml-auto text-right font-medium text-text-primary\">\n {attendeeName || '—'}\n <span className=\"block text-xs font-normal text-text-secondary\">\n {attendeeMode === 'existing' ? selectedAttendee?.email : email.trim()}\n </span>\n </dd>\n </div>\n </dl>\n\n {hasPositiveAmount ? (\n <label className=\"block space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Billing account ID (optional)</span>\n <Input\n value={billingAccountId}\n onChange={(e) => setBillingAccountId(e.target.value)}\n placeholder=\"Attach an existing billing account\"\n />\n <span className=\"text-xs text-text-secondary\">\n When provided, the charge is attached to this billing account.\n </span>\n </label>\n ) : null}\n </div>\n ) : null}\n\n {/* Wizard controls */}\n <div className=\"mt-6 flex items-center justify-between gap-3 border-t border-border-seam pt-4\">\n <Button type=\"button\" variant=\"ghost\" onClick={goBack} disabled={step === 0 || saving}>\n <ArrowLeft className=\"h-4 w-4\" />\n Back\n </Button>\n {step < STEPS.length - 1 ? (\n <Button type=\"button\" onClick={goNext}>\n Continue\n <ArrowRight className=\"h-4 w-4\" />\n </Button>\n ) : (\n <Button type=\"button\" onClick={onConfirm} disabled={saving}>\n <Ticket className=\"h-4 w-4\" />\n {saving ? 'Registering…' : 'Confirm registration'}\n </Button>\n )}\n </div>\n </GlassCard>\n\n {/* ORDER SUMMARY */}\n <GlassCard treatment=\"glass\" className=\"h-fit p-5 lg:sticky lg:top-6\">\n <h2 className=\"text-base font-semibold text-text-primary\">Order summary</h2>\n {selectedTicket ? (\n <div className=\"mt-4 space-y-3 text-sm\">\n <div className=\"flex items-start justify-between gap-3\">\n <span className=\"min-w-0 text-text-secondary\">\n {selectedEvent?.name ? `${selectedEvent.name} — ` : ''}\n {selectedTicket.name}\n {qty > 1 ? <span className=\"text-text-muted\"> × {qty}</span> : null}\n </span>\n <span className=\"shrink-0 tabular-nums text-text-primary\">\n {subtotal > 0 ? formatMoney(subtotal, currency) : 'Free'}\n </span>\n </div>\n {appliedPromo && discount > 0 ? (\n <div className=\"flex items-start justify-between gap-3\">\n <span className=\"text-text-secondary\">Promo · {appliedPromo.code}</span>\n <span className=\"shrink-0 tabular-nums text-status-success-text\">\n −{formatMoney(discount, currency)}\n </span>\n </div>\n ) : null}\n <div className=\"flex items-center justify-between border-t border-border-seam pt-3\">\n <span className=\"font-semibold text-text-primary\">Total</span>\n <span className=\"text-lg font-bold tabular-nums text-text-primary\">\n {subtotal > 0 ? formatMoney(total, currency) : 'Free'}\n </span>\n </div>\n {subtotal > 0 ? (\n <p className=\"text-xs text-text-muted\">Final pricing & payment handled by the billing service.</p>\n ) : null}\n </div>\n ) : (\n <p className=\"mt-4 text-sm text-text-muted\">\n Pick a ticket to see the price breakdown. Your order total updates as you go.\n </p>\n )}\n </GlassCard>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"RegisterAttendeePage.js","names":[],"sources":["../../../../src/eventfullymanaged/pages/registration/RegisterAttendeePage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport {\n ArrowLeft,\n ArrowRight,\n BadgePercent,\n CalendarDays,\n Check,\n CheckCircle2,\n Minus,\n Plus,\n Ticket,\n UserRound,\n} from 'lucide-react';\nimport {\n Button,\n GlassCard,\n GradientText,\n Input,\n PagePurpose,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n Stepper,\n} from '@burdenoff/fe-libs/ui';\n\nimport {\n useAttendeesQuery,\n useCreateAttendeeMutation,\n useCreateRegistrationMutation,\n useEventsQuery,\n usePromoCodesQuery,\n useTicketTypesQuery,\n type PromoCodeGql,\n} from '../../api/eventApi';\nimport { useEventfullyManaged } from '../../context/EventfullyManagedContext';\nimport { computeDiscount, describeDiscount, formatMoney } from '../../lib/format';\nimport { describeError, mutationSucceeded } from '../../lib/mutationResult';\nimport { notify } from '../../lib/toast';\n\ntype Confirmation = {\n attendeeName: string;\n eventName: string;\n ticketName: string;\n quantity: number;\n status: string;\n subtotal: number;\n discount: number;\n total: number;\n currency: string;\n promoCode: string | null;\n free: boolean;\n};\n\nconst STEPS = [\n { key: 'event', label: 'Event', description: 'Choose the event' },\n { key: 'ticket', label: 'Ticket', description: 'Pick a ticket type' },\n { key: 'attendee', label: 'Attendee', description: 'Who is coming' },\n { key: 'confirm', label: 'Confirm', description: 'Review & submit' },\n];\n\nexport default function RegisterAttendeePage() {\n const { navigate } = useEventfullyManaged();\n\n const [step, setStep] = useState(0);\n const [eventId, setEventId] = useState('');\n const [ticketTypeId, setTicketTypeId] = useState('');\n const [attendeeMode, setAttendeeMode] = useState<'new' | 'existing'>('new');\n const [attendeeId, setAttendeeId] = useState('');\n const [fullName, setFullName] = useState('');\n const [email, setEmail] = useState('');\n const [company, setCompany] = useState('');\n const [phone, setPhone] = useState('');\n const [quantity, setQuantity] = useState(1);\n const [promoInput, setPromoInput] = useState('');\n const [appliedPromo, setAppliedPromo] = useState<PromoCodeGql | null>(null);\n const [promoError, setPromoError] = useState('');\n const [billingAccountId, setBillingAccountId] = useState('');\n const [confirmation, setConfirmation] = useState<Confirmation | null>(null);\n\n const { data: eventsData, loading: eventsLoading } = useEventsQuery({ limit: 100, offset: 0 });\n const { data: ticketData, loading: ticketsLoading } = useTicketTypesQuery({\n eventId: eventId || undefined,\n limit: 100,\n offset: 0,\n });\n const { data: attendeesData } = useAttendeesQuery({ eventId: eventId || undefined, limit: 100, offset: 0 });\n const { data: promoData } = usePromoCodesQuery({ eventId: eventId || undefined, limit: 100, offset: 0 });\n\n const events = useMemo(() => eventsData?.events.items ?? [], [eventsData]);\n const tickets = useMemo(\n () =>\n (ticketData?.ticketTypes.items ?? []).filter(\n (ticket) => (!eventId || ticket.eventId === eventId) && ticket.isActive\n ),\n [ticketData, eventId]\n );\n const attendees = useMemo(\n () => (attendeesData?.attendees.items ?? []).filter((attendee) => !eventId || attendee.eventId === eventId),\n [attendeesData, eventId]\n );\n const activePromos = useMemo(\n () =>\n (promoData?.promoCodes.items ?? []).filter((promo) => promo.isActive && (!eventId || promo.eventId === eventId)),\n [promoData, eventId]\n );\n\n const [createAttendee, { loading: creatingAttendee }] = useCreateAttendeeMutation();\n const [createRegistration, { loading: creatingRegistration }] = useCreateRegistrationMutation();\n const saving = creatingAttendee || creatingRegistration;\n\n const selectedEvent = useMemo(() => events.find((item) => item.id === eventId), [events, eventId]);\n const selectedTicket = useMemo(() => tickets.find((ticket) => ticket.id === ticketTypeId), [tickets, ticketTypeId]);\n const selectedAttendee = useMemo(\n () => attendees.find((attendee) => attendee.id === attendeeId),\n [attendees, attendeeId]\n );\n\n const qty = Math.max(1, quantity);\n const unitPrice = selectedTicket ? Number(selectedTicket.price) : 0;\n const currency = selectedTicket?.currency ?? 'USD';\n const subtotal = Number.isFinite(unitPrice) ? unitPrice * qty : 0;\n const discount = appliedPromo\n ? computeDiscount(subtotal, appliedPromo.discountType, Number(appliedPromo.discountValue))\n : 0;\n const total = Math.max(subtotal - discount, 0);\n const hasPositiveAmount = subtotal > 0;\n\n const attendeeName = attendeeMode === 'existing' ? (selectedAttendee?.fullName ?? '') : fullName.trim();\n\n const onEventChange = (value: string) => {\n setEventId(value);\n setTicketTypeId('');\n setAttendeeMode('new');\n setAttendeeId('');\n setAppliedPromo(null);\n setPromoInput('');\n setPromoError('');\n };\n\n const isStepValid = (index: number): boolean => {\n switch (index) {\n case 0:\n return Boolean(eventId);\n case 1:\n return Boolean(ticketTypeId);\n case 2:\n return attendeeMode === 'existing' ? Boolean(attendeeId) : Boolean(fullName.trim() && email.trim());\n default:\n return true;\n }\n };\n\n const canGoTo = (index: number): boolean => {\n if (index <= step) return true;\n for (let i = 0; i < index; i += 1) {\n if (!isStepValid(i)) return false;\n }\n return true;\n };\n\n const goToStep = (index: number) => {\n if (canGoTo(index)) {\n setStep(index);\n }\n };\n\n const goNext = () => {\n if (!isStepValid(step)) {\n const messages = [\n 'Choose an event to continue.',\n 'Pick a ticket type to continue.',\n 'Select or enter an attendee to continue.',\n ];\n notify.error('One more thing', messages[step] ?? 'Complete this step to continue.');\n return;\n }\n setStep((current) => Math.min(current + 1, STEPS.length - 1));\n };\n\n const goBack = () => setStep((current) => Math.max(current - 1, 0));\n\n const applyPromo = () => {\n const code = promoInput.trim().toUpperCase();\n if (!code) {\n setAppliedPromo(null);\n setPromoError('');\n return;\n }\n const match = activePromos.find((promo) => promo.code.toUpperCase() === code);\n if (!match) {\n setAppliedPromo(null);\n setPromoError('That code is not active for this event.');\n return;\n }\n setAppliedPromo(match);\n setPromoError('');\n notify.success('Promo applied', describeDiscount(match.discountType, match.discountValue, currency));\n };\n\n const clearPromo = () => {\n setAppliedPromo(null);\n setPromoInput('');\n setPromoError('');\n };\n\n const resetAll = () => {\n setStep(0);\n setEventId('');\n setTicketTypeId('');\n setAttendeeMode('new');\n setAttendeeId('');\n setFullName('');\n setEmail('');\n setCompany('');\n setPhone('');\n setQuantity(1);\n clearPromo();\n setBillingAccountId('');\n };\n\n const onConfirm = async () => {\n if (!eventId || !ticketTypeId) {\n notify.error('Incomplete', 'Pick an event and ticket first.');\n return;\n }\n if (attendeeMode === 'new' && (!fullName.trim() || !email.trim())) {\n notify.error('Attendee required', 'Enter a name and email, or pick an existing attendee.');\n return;\n }\n if (attendeeMode === 'existing' && !attendeeId) {\n notify.error('Attendee required', 'Select an existing attendee.');\n return;\n }\n\n try {\n let resolvedAttendeeId = attendeeId;\n let resolvedAttendeeName = selectedAttendee?.fullName ?? '';\n\n if (attendeeMode === 'new') {\n const attendeeResponse = await createAttendee({\n variables: {\n input: {\n eventId,\n fullName: fullName.trim(),\n email: email.trim(),\n company: company.trim() || null,\n phone: phone.trim() || null,\n profile: {},\n metadata: {},\n },\n },\n });\n const createdId = attendeeResponse.data?.createAttendee?.id;\n if (!mutationSucceeded(attendeeResponse, createdId, 'Could not create attendee') || !createdId) {\n return;\n }\n resolvedAttendeeId = createdId;\n resolvedAttendeeName = fullName.trim();\n }\n\n // The money leg is owned by global-billing-svc — never price locally. We\n // forward the promo code + optional billing-account reference via metadata\n // so the backend can attach the charge; totalAmount is left for the backend.\n const metadata: Record<string, unknown> = {};\n if (appliedPromo) {\n metadata.promoCode = appliedPromo.code;\n }\n if (hasPositiveAmount && billingAccountId.trim()) {\n metadata.billingAccountId = billingAccountId.trim();\n }\n\n const registrationResponse = await createRegistration({\n variables: {\n input: {\n eventId,\n attendeeId: resolvedAttendeeId,\n ticketTypeId,\n quantity: qty,\n currency,\n status: 'CONFIRMED',\n metadata,\n },\n },\n });\n if (\n !mutationSucceeded(\n registrationResponse,\n registrationResponse.data?.createRegistration?.id,\n 'Could not create registration'\n )\n ) {\n return;\n }\n\n const registration = registrationResponse.data?.createRegistration;\n setConfirmation({\n attendeeName: resolvedAttendeeName || 'Attendee',\n eventName: selectedEvent?.name ?? 'Event',\n ticketName: selectedTicket?.name ?? 'Ticket',\n quantity: qty,\n status: registration?.status ?? 'CONFIRMED',\n subtotal,\n discount,\n total,\n currency,\n promoCode: appliedPromo?.code ?? null,\n free: !hasPositiveAmount,\n });\n notify.success('Registration confirmed', resolvedAttendeeName || undefined);\n } catch (error) {\n notify.error('Could not register attendee', describeError(error));\n }\n };\n\n if (confirmation) {\n return (\n <div className=\"mx-auto max-w-2xl space-y-6 p-6\">\n <div>\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => navigate('/registration')}>\n <ArrowLeft className=\"h-4 w-4\" />\n Back to Registration\n </Button>\n </div>\n <GlassCard treatment=\"glass\" glow className=\"p-8 text-center\">\n <div className=\"mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-status-success-bg-subtle\">\n <CheckCircle2 className=\"h-8 w-8 text-status-success-text\" aria-hidden=\"true\" />\n </div>\n <h1 className=\"mt-4 text-2xl font-bold text-text-primary\">Registration confirmed</h1>\n <p className=\"mt-2 text-sm text-text-secondary\">\n {confirmation.attendeeName} is registered for {confirmation.eventName}.\n </p>\n\n <dl className=\"mx-auto mt-6 max-w-sm space-y-2 rounded-card border border-border-seam bg-bg-surface p-4 text-left text-sm\">\n <div className=\"flex justify-between\">\n <dt className=\"text-text-secondary\">Ticket</dt>\n <dd className=\"font-medium text-text-primary\">\n {confirmation.ticketName}\n {confirmation.quantity > 1 ? ` × ${confirmation.quantity}` : ''}\n </dd>\n </div>\n {!confirmation.free ? (\n <>\n <div className=\"flex justify-between\">\n <dt className=\"text-text-secondary\">Subtotal</dt>\n <dd className=\"tabular-nums text-text-primary\">\n {formatMoney(confirmation.subtotal, confirmation.currency)}\n </dd>\n </div>\n {confirmation.discount > 0 ? (\n <div className=\"flex justify-between\">\n <dt className=\"text-text-secondary\">\n Promo{confirmation.promoCode ? ` · ${confirmation.promoCode}` : ''}\n </dt>\n <dd className=\"tabular-nums text-status-success-text\">\n −{formatMoney(confirmation.discount, confirmation.currency)}\n </dd>\n </div>\n ) : null}\n </>\n ) : null}\n <div className=\"flex justify-between border-t border-border-seam pt-2\">\n <dt className=\"font-semibold text-text-primary\">Total</dt>\n <dd className=\"font-bold tabular-nums text-text-primary\">\n {confirmation.free ? 'Free' : formatMoney(confirmation.total, confirmation.currency)}\n </dd>\n </div>\n <div className=\"flex justify-between\">\n <dt className=\"text-text-secondary\">Status</dt>\n <dd className=\"font-medium text-text-primary\">{confirmation.status}</dd>\n </div>\n </dl>\n {!confirmation.free ? (\n <p className=\"mt-3 text-xs text-text-muted\">\n Final pricing and payment are settled by the billing service.\n </p>\n ) : null}\n\n <div className=\"mt-8 flex flex-col justify-center gap-3 sm:flex-row\">\n <Button type=\"button\" variant=\"outline\" onClick={() => navigate('/registration')}>\n Back to Registrations\n </Button>\n <Button\n type=\"button\"\n onClick={() => {\n setConfirmation(null);\n resetAll();\n }}\n >\n Register another\n </Button>\n </div>\n </GlassCard>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6 p-6\">\n <div>\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => navigate('/registration')}>\n <ArrowLeft className=\"h-4 w-4\" />\n Back to Registration\n </Button>\n <h1 className=\"mt-3 text-2xl font-bold text-text-primary\">\n <GradientText>Register Attendee</GradientText>\n </h1>\n <p className=\"mt-1 text-sm text-text-secondary\">\n Walk through the event, ticket and attendee in a few quick steps.\n </p>\n </div>\n\n <PagePurpose>\n Register a person for an event against a ticket type. Pick the event and ticket, choose an existing attendee or\n add a new one, apply a promo code if they have one, then confirm. The order summary previews the price — final\n pricing and payment are handled by the billing service.\n </PagePurpose>\n\n <Stepper steps={STEPS} active={step} onStepClick={goToStep} />\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-[minmax(0,1fr)_20rem]\">\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-5 sm:p-6\">\n {/* STEP 0 — EVENT */}\n {step === 0 ? (\n <div className=\"space-y-4\">\n <div>\n <h2 className=\"text-lg font-semibold text-text-primary\">Which event?</h2>\n <p className=\"mt-1 text-sm text-text-secondary\">Choose the event this attendee is signing up for.</p>\n </div>\n {eventsLoading ? (\n <p className=\"text-sm text-text-muted\">Loading events…</p>\n ) : events.length === 0 ? (\n <div className=\"rounded-card border border-dashed border-border-default p-6 text-center text-sm text-text-secondary\">\n No events yet.{' '}\n <button\n type=\"button\"\n className=\"font-medium text-text-link underline-offset-2 hover:underline\"\n onClick={() => navigate('/events/create')}\n >\n Create an event\n </button>{' '}\n first.\n </div>\n ) : (\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2\">\n {events.map((eventItem) => {\n const selected = eventItem.id === eventId;\n return (\n <button\n key={eventItem.id}\n type=\"button\"\n onClick={() => onEventChange(eventItem.id)}\n aria-pressed={selected}\n className={`flex items-center gap-3 rounded-card border p-4 text-left transition-colors ${\n selected\n ? 'border-action-primary-border bg-accent-wash'\n : 'border-border-seam bg-bg-surface hover:border-border-strong'\n }`}\n >\n <span\n className={`flex h-10 w-10 shrink-0 items-center justify-center rounded-input ${\n selected ? 'bg-action-primary-bg text-action-primary-text' : 'bg-bg-sunken text-text-muted'\n }`}\n >\n <CalendarDays className=\"h-5 w-5\" aria-hidden=\"true\" />\n </span>\n <span className=\"min-w-0 flex-1 truncate text-sm font-medium text-text-primary\">\n {eventItem.name}\n </span>\n {selected ? (\n <Check className=\"h-4 w-4 shrink-0 text-action-primary-bg\" aria-hidden=\"true\" />\n ) : null}\n </button>\n );\n })}\n </div>\n )}\n </div>\n ) : null}\n\n {/* STEP 1 — TICKET */}\n {step === 1 ? (\n <div className=\"space-y-4\">\n <div>\n <h2 className=\"text-lg font-semibold text-text-primary\">Ticket type</h2>\n <p className=\"mt-1 text-sm text-text-secondary\">Pick the ticket and how many they need.</p>\n </div>\n {ticketsLoading ? (\n <p className=\"text-sm text-text-muted\">Loading tickets…</p>\n ) : tickets.length === 0 ? (\n <div className=\"rounded-card border border-dashed border-border-default p-6 text-center text-sm text-text-secondary\">\n This event has no active ticket types.\n </div>\n ) : (\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3\">\n {tickets.map((ticket) => {\n const selected = ticket.id === ticketTypeId;\n const cap = ticket.capacity ?? null;\n const sold = ticket.soldCount ?? 0;\n const pct = cap && cap > 0 ? Math.min(100, Math.round((sold / cap) * 100)) : null;\n const soldOut = cap != null && sold >= cap;\n return (\n <button\n key={ticket.id}\n type=\"button\"\n disabled={soldOut && !selected}\n onClick={() => setTicketTypeId(ticket.id)}\n aria-pressed={selected}\n className={`flex flex-col gap-3 rounded-card border p-4 text-left transition-colors disabled:cursor-not-allowed disabled:opacity-60 ${\n selected\n ? 'border-action-primary-border bg-accent-wash ring-1 ring-border-focus'\n : 'border-border-seam bg-bg-surface hover:border-border-strong'\n }`}\n >\n <div className=\"flex items-start justify-between gap-2\">\n <span className=\"text-sm font-semibold text-text-primary\">{ticket.name}</span>\n {selected ? (\n <Check className=\"h-4 w-4 shrink-0 text-action-primary-bg\" aria-hidden=\"true\" />\n ) : (\n <Ticket className=\"h-4 w-4 shrink-0 text-text-muted\" aria-hidden=\"true\" />\n )}\n </div>\n <span className=\"text-lg font-bold tabular-nums text-text-primary\">\n {Number(ticket.price) > 0 ? formatMoney(Number(ticket.price), ticket.currency) : 'Free'}\n </span>\n {cap != null ? (\n <div className=\"space-y-1\">\n <div className=\"h-1.5 w-full overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-action-primary-bg\"\n style={{ width: `${pct ?? 0}%` }}\n />\n </div>\n <span className=\"text-xs text-text-muted\">\n {soldOut ? 'Sold out' : `${sold} / ${cap} sold`}\n </span>\n </div>\n ) : (\n <span className=\"text-xs text-text-muted\">{sold} sold</span>\n )}\n </button>\n );\n })}\n </div>\n )}\n\n <div className=\"flex flex-wrap items-end gap-4 border-t border-border-seam pt-4\">\n <div className=\"space-y-1\">\n <span className=\"text-sm font-medium text-text-primary\">Quantity</span>\n <div className=\"flex items-center gap-1\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"icon\"\n aria-label=\"Decrease quantity\"\n disabled={qty <= 1}\n onClick={() => setQuantity((value) => Math.max(1, value - 1))}\n >\n <Minus className=\"h-4 w-4\" />\n </Button>\n <span className=\"w-10 text-center text-base font-semibold tabular-nums text-text-primary\">\n {qty}\n </span>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"icon\"\n aria-label=\"Increase quantity\"\n onClick={() => setQuantity((value) => value + 1)}\n >\n <Plus className=\"h-4 w-4\" />\n </Button>\n </div>\n </div>\n </div>\n </div>\n ) : null}\n\n {/* STEP 2 — ATTENDEE */}\n {step === 2 ? (\n <div className=\"space-y-4\">\n <div>\n <h2 className=\"text-lg font-semibold text-text-primary\">Attendee information</h2>\n <p className=\"mt-1 text-sm text-text-secondary\">Add someone new or pick an existing attendee.</p>\n </div>\n\n <div className=\"inline-flex rounded-input border border-border-seam bg-bg-sunken p-1\">\n <button\n type=\"button\"\n onClick={() => setAttendeeMode('new')}\n className={`rounded-[calc(var(--radius-input)-2px)] px-3 py-1.5 text-sm font-medium transition-colors ${\n attendeeMode === 'new'\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-secondary hover:text-text-primary'\n }`}\n >\n New attendee\n </button>\n <button\n type=\"button\"\n onClick={() => setAttendeeMode('existing')}\n disabled={attendees.length === 0}\n className={`rounded-[calc(var(--radius-input)-2px)] px-3 py-1.5 text-sm font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50 ${\n attendeeMode === 'existing'\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-secondary hover:text-text-primary'\n }`}\n >\n Existing attendee\n </button>\n </div>\n\n {attendeeMode === 'existing' ? (\n <label className=\"block space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Attendee</span>\n <Select value={attendeeId} onValueChange={setAttendeeId}>\n <SelectTrigger aria-label=\"Attendee\">\n <SelectValue placeholder=\"Select an attendee\" />\n </SelectTrigger>\n <SelectContent>\n {attendees.map((attendee) => (\n <SelectItem key={attendee.id} value={attendee.id}>\n {attendee.fullName} · {attendee.email}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </label>\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Full name</span>\n <Input\n value={fullName}\n onChange={(e) => setFullName(e.target.value)}\n placeholder=\"Enter full name\"\n />\n </label>\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Email</span>\n <Input\n type=\"email\"\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder=\"Enter email address\"\n />\n </label>\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Company</span>\n <Input\n value={company}\n onChange={(e) => setCompany(e.target.value)}\n placeholder=\"Enter company name\"\n />\n </label>\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Phone</span>\n <Input value={phone} onChange={(e) => setPhone(e.target.value)} placeholder=\"Optional\" />\n </label>\n </div>\n )}\n\n {/* Promo code */}\n <div className=\"space-y-2 border-t border-border-seam pt-4\">\n <span className=\"text-sm font-medium text-text-primary\">Promo code (optional)</span>\n <div className=\"flex flex-col gap-2 sm:flex-row\">\n <Input\n value={promoInput}\n onChange={(e) => {\n setPromoInput(e.target.value);\n setPromoError('');\n }}\n placeholder=\"Enter promo code\"\n className=\"sm:flex-1\"\n onKeyDown={(e) => {\n if (e.key === 'Enter') {\n e.preventDefault();\n applyPromo();\n }\n }}\n />\n {appliedPromo ? (\n <Button type=\"button\" variant=\"outline\" onClick={clearPromo}>\n Remove\n </Button>\n ) : (\n <Button type=\"button\" variant=\"outline\" onClick={applyPromo} disabled={!promoInput.trim()}>\n Apply\n </Button>\n )}\n </div>\n {appliedPromo ? (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-status-success-bg-subtle px-2.5 py-0.5 text-xs font-medium text-status-success-text\">\n <BadgePercent className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />\n {describeDiscount(appliedPromo.discountType, appliedPromo.discountValue, currency)} applied\n </span>\n ) : promoError ? (\n <span className=\"text-xs text-status-error-text\">{promoError}</span>\n ) : activePromos.length > 0 ? (\n <div className=\"flex flex-wrap items-center gap-1.5 text-xs text-text-muted\">\n <span>Available:</span>\n {activePromos.slice(0, 4).map((promo) => (\n <button\n key={promo.id}\n type=\"button\"\n onClick={() => {\n setPromoInput(promo.code);\n setAppliedPromo(promo);\n setPromoError('');\n }}\n className=\"rounded-full border border-border-seam bg-bg-surface px-2 py-0.5 font-medium text-text-secondary hover:border-border-strong hover:text-text-primary\"\n >\n {promo.code}\n </button>\n ))}\n </div>\n ) : null}\n </div>\n </div>\n ) : null}\n\n {/* STEP 3 — CONFIRM */}\n {step === 3 ? (\n <div className=\"space-y-4\">\n <div>\n <h2 className=\"text-lg font-semibold text-text-primary\">Review & confirm</h2>\n <p className=\"mt-1 text-sm text-text-secondary\">Double-check the details before registering.</p>\n </div>\n <dl className=\"divide-y divide-border-seam rounded-card border border-border-seam bg-bg-surface text-sm\">\n <div className=\"flex items-center gap-3 p-3\">\n <CalendarDays className=\"h-4 w-4 text-text-muted\" aria-hidden=\"true\" />\n <dt className=\"text-text-secondary\">Event</dt>\n <dd className=\"ml-auto font-medium text-text-primary\">{selectedEvent?.name ?? '—'}</dd>\n </div>\n <div className=\"flex items-center gap-3 p-3\">\n <Ticket className=\"h-4 w-4 text-text-muted\" aria-hidden=\"true\" />\n <dt className=\"text-text-secondary\">Ticket</dt>\n <dd className=\"ml-auto font-medium text-text-primary\">\n {selectedTicket?.name ?? '—'}\n {qty > 1 ? ` × ${qty}` : ''}\n </dd>\n </div>\n <div className=\"flex items-center gap-3 p-3\">\n <UserRound className=\"h-4 w-4 text-text-muted\" aria-hidden=\"true\" />\n <dt className=\"text-text-secondary\">Attendee</dt>\n <dd className=\"ml-auto text-right font-medium text-text-primary\">\n {attendeeName || '—'}\n <span className=\"block text-xs font-normal text-text-secondary\">\n {attendeeMode === 'existing' ? selectedAttendee?.email : email.trim()}\n </span>\n </dd>\n </div>\n </dl>\n\n {hasPositiveAmount ? (\n <label className=\"block space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Billing account ID (optional)</span>\n <Input\n value={billingAccountId}\n onChange={(e) => setBillingAccountId(e.target.value)}\n placeholder=\"Attach an existing billing account\"\n />\n <span className=\"text-xs text-text-secondary\">\n When provided, the charge is attached to this billing account.\n </span>\n </label>\n ) : null}\n </div>\n ) : null}\n\n {/* Wizard controls */}\n <div className=\"mt-6 flex items-center justify-between gap-3 border-t border-border-seam pt-4\">\n <Button type=\"button\" variant=\"ghost\" onClick={goBack} disabled={step === 0 || saving}>\n <ArrowLeft className=\"h-4 w-4\" />\n Back\n </Button>\n {step < STEPS.length - 1 ? (\n <Button type=\"button\" onClick={goNext}>\n Continue\n <ArrowRight className=\"h-4 w-4\" />\n </Button>\n ) : (\n <Button type=\"button\" onClick={onConfirm} disabled={saving}>\n <Ticket className=\"h-4 w-4\" />\n {saving ? 'Registering…' : 'Confirm registration'}\n </Button>\n )}\n </div>\n </GlassCard>\n\n {/* ORDER SUMMARY */}\n <GlassCard treatment=\"glass\" className=\"h-fit p-5 lg:sticky lg:top-6\">\n <h2 className=\"text-base font-semibold text-text-primary\">Order summary</h2>\n {selectedTicket ? (\n <div className=\"mt-4 space-y-3 text-sm\">\n <div className=\"flex items-start justify-between gap-3\">\n <span className=\"min-w-0 text-text-secondary\">\n {selectedEvent?.name ? `${selectedEvent.name} — ` : ''}\n {selectedTicket.name}\n {qty > 1 ? <span className=\"text-text-muted\"> × {qty}</span> : null}\n </span>\n <span className=\"shrink-0 tabular-nums text-text-primary\">\n {subtotal > 0 ? formatMoney(subtotal, currency) : 'Free'}\n </span>\n </div>\n {appliedPromo && discount > 0 ? (\n <div className=\"flex items-start justify-between gap-3\">\n <span className=\"text-text-secondary\">Promo · {appliedPromo.code}</span>\n <span className=\"shrink-0 tabular-nums text-status-success-text\">\n −{formatMoney(discount, currency)}\n </span>\n </div>\n ) : null}\n <div className=\"flex items-center justify-between border-t border-border-seam pt-3\">\n <span className=\"font-semibold text-text-primary\">Total</span>\n <span className=\"text-lg font-bold tabular-nums text-text-primary\">\n {subtotal > 0 ? formatMoney(total, currency) : 'Free'}\n </span>\n </div>\n {subtotal > 0 ? (\n <p className=\"text-xs text-text-muted\">Final pricing & payment handled by the billing service.</p>\n ) : null}\n </div>\n ) : (\n <p className=\"mt-4 text-sm text-text-muted\">\n Pick a ticket to see the price breakdown. Your order total updates as you go.\n </p>\n )}\n </GlassCard>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SpeakersPage.js","names":[],"sources":["../../../../src/eventfullymanaged/pages/speakers/SpeakersPage.tsx"],"sourcesContent":["import { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Plus, Search } from 'lucide-react';\nimport {\n Badge,\n Button,\n GlassCard,\n GradientText,\n IllustratedEmptyState,\n Input,\n PagePurpose,\n Skeleton,\n} from '@burdenoff/fe-libs/ui';\nimport { useMemo, useState } from 'react';\n\nimport { useDeleteSpeakerMutation, useEntityTagsQuery } from '../../api/eventApi';\nimport { ActionMenu } from '../../components/ActionMenu';\nimport { Pagination, usePagination } from '../../components/Pagination';\nimport { useEventfullyManaged } from '../../context/EventfullyManagedContext';\nimport { notify } from '../../lib/toast';\n\n// The foundation SPEAKERS_QUERY only selects the columns other screens need\n// (id/name/title/org/email). The roster grid additionally shows the headshot\n// and the one-line talk abstract, so it reads from this fuller selection rather\n// than mutating the shared hook that Event/Session pages also consume.\nconst SPEAKER_GRID_QUERY = gql`\n query EventfullymanagedSpeakerGrid($limit: Int, $offset: Int) {\n speakers: eventfullyManagedSpeakers(limit: $limit, offset: $offset) {\n total\n items {\n id\n fullName\n title\n organization\n bio\n avatarUrl\n }\n }\n }\n`;\n\ntype SpeakerRow = {\n id: string;\n fullName: string;\n title?: string | null;\n organization?: string | null;\n bio?: string | null;\n avatarUrl?: string | null;\n};\n\ntype SpeakerGridResponse = { speakers: { total: number; items: SpeakerRow[] } };\n\nfunction initials(name: string): string {\n const parts = name.trim().split(/\\s+/).filter(Boolean);\n if (parts.length === 0) {\n return '?';\n }\n if (parts.length === 1) {\n return parts[0].slice(0, 2).toUpperCase();\n }\n return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();\n}\n\nfunction SpeakerAvatar({ speaker }: { speaker: SpeakerRow }) {\n const [failed, setFailed] = useState(false);\n const showImage = Boolean(speaker.avatarUrl) && !failed;\n return (\n <span className=\"relative flex h-16 w-16 shrink-0 items-center justify-center overflow-hidden rounded-full bg-bg-sunken ring-1 ring-border-subtle\">\n {showImage ? (\n <img\n src={speaker.avatarUrl ?? ''}\n alt={speaker.fullName}\n loading=\"lazy\"\n onError={() => setFailed(true)}\n className=\"h-full w-full object-cover\"\n />\n ) : (\n <span className=\"text-base font-semibold text-text-secondary\">{initials(speaker.fullName)}</span>\n )}\n </span>\n );\n}\n\n// Read-only topic chips sourced from the platform tags service for this speaker.\n// The mock shows two topic tags per speaker; these are the real assignments.\nfunction SpeakerTopics({ speakerId }: { speakerId: string }) {\n const { data } = useEntityTagsQuery({ input: { entityType: 'EVENTFULLYMANAGED_SPEAKER', entityId: speakerId } });\n const assignments = data?.eventfullyManagedEntityTags ?? [];\n if (assignments.length === 0) {\n return null;\n }\n return (\n <div className=\"mt-3 flex flex-wrap gap-1.5\" role=\"list\" aria-label=\"Topics\">\n {assignments.slice(0, 4).map((assignment) => {\n const info = assignment.tagInfo;\n const color = info?.color ?? undefined;\n return (\n <Badge key={assignment.id} variant=\"secondary\" className=\"gap-1.5\" role=\"listitem\">\n {color ? (\n <span className=\"h-2 w-2 rounded-full\" style={{ backgroundColor: color }} aria-hidden=\"true\" />\n ) : null}\n {info?.label ?? info?.key ?? 'Topic'}\n </Badge>\n );\n })}\n </div>\n );\n}\n\nfunction SpeakerCardSkeleton() {\n return (\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-5\">\n <div className=\"flex items-start gap-4\">\n <Skeleton className=\"h-16 w-16 shrink-0 rounded-full\" />\n <div className=\"min-w-0 flex-1 space-y-2\">\n <Skeleton className=\"h-5 w-32\" />\n <Skeleton className=\"h-4 w-40\" />\n <Skeleton className=\"h-4 w-full\" />\n <div className=\"flex gap-1.5 pt-1\">\n <Skeleton className=\"h-5 w-12 rounded-full\" />\n <Skeleton className=\"h-5 w-16 rounded-full\" />\n </div>\n </div>\n </div>\n </GlassCard>\n );\n}\n\nexport default function SpeakersPage() {\n const { navigate } = useEventfullyManaged();\n const pagination = usePagination({ pageSize: 12 });\n const [search, setSearch] = useState('');\n const { data, loading, refetch } = useQuery<SpeakerGridResponse, { limit?: number; offset?: number }>(\n SPEAKER_GRID_QUERY,\n { variables: { limit: pagination.limit, offset: pagination.offset } }\n );\n const [deleteSpeaker] = useDeleteSpeakerMutation();\n\n const total = data?.speakers?.total ?? 0;\n const speakers = useMemo(() => data?.speakers?.items ?? [], [data]);\n\n const filtered = useMemo(() => {\n const term = search.trim().toLowerCase();\n if (!term) {\n return speakers;\n }\n return speakers.filter((speaker) =>\n [speaker.fullName, speaker.title, speaker.organization, speaker.bio]\n .filter(Boolean)\n .some((field) => (field as string).toLowerCase().includes(term))\n );\n }, [speakers, search]);\n\n const onDelete = async (speaker: SpeakerRow) => {\n const response = await deleteSpeaker({ variables: { id: speaker.id } });\n if (response.error) {\n notify.error('Could not delete speaker', response.error.message);\n throw response.error;\n }\n if (!response.data?.deleteSpeaker) {\n notify.error('Could not delete speaker', 'The server did not confirm the change.');\n throw new Error('delete-unconfirmed');\n }\n notify.success('Speaker deleted', speaker.fullName);\n await refetch();\n };\n\n return (\n <div className=\"space-y-6 p-6\">\n <PagePurpose>\n Speakers are the people who present your sessions. Keep a roster here — their name, role, company and bio — so\n that when you build the agenda you can assign a speaker to each session in a couple of clicks. A good speaker\n profile also gives attendees a reason to attend.\n </PagePurpose>\n\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between\">\n <div>\n <GradientText as=\"h1\" className=\"text-2xl font-bold\">\n Speakers\n </GradientText>\n <p className=\"mt-1 text-sm text-text-secondary\">Manage your speaker roster with live API data.</p>\n </div>\n <div className=\"flex flex-col gap-2 sm:flex-row sm:items-center\">\n <div className=\"relative\">\n <Search className=\"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-muted\" />\n <Input\n type=\"search\"\n value={search}\n onChange={(event) => setSearch(event.target.value)}\n placeholder=\"Search speakers…\"\n aria-label=\"Search speakers on this page\"\n className=\"pl-10 sm:w-64\"\n />\n </div>\n <Button type=\"button\" onClick={() => navigate('/speakers/create')}>\n <Plus className=\"h-4 w-4\" />\n Add Speaker\n </Button>\n </div>\n </div>\n\n {loading ? (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3\">\n {Array.from({ length: 6 }).map((_, index) => (\n <SpeakerCardSkeleton key={index} />\n ))}\n </div>\n ) : speakers.length === 0 ? (\n <GlassCard treatment=\"plain\" className=\"border-border-seam\">\n <IllustratedEmptyState\n illustration=\"empty-people\"\n title=\"No speakers yet.\"\n description=\"Add your first speaker to start building the agenda.\"\n action={\n <Button type=\"button\" onClick={() => navigate('/speakers/create')}>\n <Plus className=\"h-4 w-4\" />\n Add Speaker\n </Button>\n }\n />\n </GlassCard>\n ) : filtered.length === 0 ? (\n <GlassCard treatment=\"plain\" className=\"border-border-seam\">\n <IllustratedEmptyState\n illustration=\"empty-search\"\n title=\"No speakers match your search.\"\n description=\"Try a different name, title or company.\"\n />\n </GlassCard>\n ) : (\n <div className=\"space-y-4\">\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3\">\n {filtered.map((speaker) => {\n const roleLine = [speaker.title, speaker.organization].filter(Boolean).join(', ');\n return (\n <GlassCard\n key={speaker.id}\n treatment=\"plain\"\n className=\"group flex flex-col border-border-seam p-5 transition-shadow duration-200 hover:shadow-elevation-1\"\n >\n <div className=\"flex items-start gap-4\">\n <SpeakerAvatar speaker={speaker} />\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-start justify-between gap-2\">\n <button\n type=\"button\"\n onClick={() => navigate(`/speakers/${speaker.id}/edit`)}\n className=\"rounded-button text-left text-base font-semibold text-text-primary transition-colors hover:text-text-link focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border-focus\"\n >\n {speaker.fullName}\n </button>\n <ActionMenu\n ariaLabel={`Speaker actions for ${speaker.fullName}`}\n entityName={speaker.fullName}\n deleteLabel=\"Delete speaker\"\n onEdit={() => navigate(`/speakers/${speaker.id}/edit`)}\n onDelete={() => onDelete(speaker)}\n />\n </div>\n <p className=\"mt-0.5 truncate text-sm text-text-secondary\">{roleLine || 'Speaker'}</p>\n </div>\n </div>\n\n {speaker.bio ? (\n <p className=\"mt-3 line-clamp-2 text-sm text-text-secondary\">{speaker.bio}</p>\n ) : null}\n\n <SpeakerTopics speakerId={speaker.id} />\n </GlassCard>\n );\n })}\n </div>\n\n {search.trim() ? null : (\n <Pagination\n offset={pagination.offset}\n limit={pagination.limit}\n loadedCount={speakers.length}\n total={total}\n onPrev={pagination.prevPage}\n onNext={pagination.nextPage}\n itemLabel=\"speakers\"\n loading={loading}\n />\n )}\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SpeakersPage.js","names":[],"sources":["../../../../src/eventfullymanaged/pages/speakers/SpeakersPage.tsx"],"sourcesContent":["import { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Plus, Search } from 'lucide-react';\nimport {\n Badge,\n Button,\n GlassCard,\n GradientText,\n IllustratedEmptyState,\n Input,\n PagePurpose,\n Skeleton,\n} from '@burdenoff/fe-libs/ui';\nimport { useMemo, useState } from 'react';\n\nimport { useDeleteSpeakerMutation, useEntityTagsQuery } from '../../api/eventApi';\nimport { ActionMenu } from '../../components/ActionMenu';\nimport { Pagination, usePagination } from '../../components/Pagination';\nimport { useEventfullyManaged } from '../../context/EventfullyManagedContext';\nimport { notify } from '../../lib/toast';\n\n// The foundation SPEAKERS_QUERY only selects the columns other screens need\n// (id/name/title/org/email). The roster grid additionally shows the headshot\n// and the one-line talk abstract, so it reads from this fuller selection rather\n// than mutating the shared hook that Event/Session pages also consume.\nconst SPEAKER_GRID_QUERY = gql`\n query EventfullymanagedSpeakerGrid($limit: Int, $offset: Int) {\n speakers: eventfullyManagedSpeakers(limit: $limit, offset: $offset) {\n total\n items {\n id\n fullName\n title\n organization\n bio\n avatarUrl\n }\n }\n }\n`;\n\ntype SpeakerRow = {\n id: string;\n fullName: string;\n title?: string | null;\n organization?: string | null;\n bio?: string | null;\n avatarUrl?: string | null;\n};\n\ntype SpeakerGridResponse = { speakers: { total: number; items: SpeakerRow[] } };\n\nfunction initials(name: string): string {\n const parts = name.trim().split(/\\s+/).filter(Boolean);\n if (parts.length === 0) {\n return '?';\n }\n if (parts.length === 1) {\n return parts[0].slice(0, 2).toUpperCase();\n }\n return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();\n}\n\nfunction SpeakerAvatar({ speaker }: { speaker: SpeakerRow }) {\n const [failed, setFailed] = useState(false);\n const showImage = Boolean(speaker.avatarUrl) && !failed;\n return (\n <span className=\"relative flex h-16 w-16 shrink-0 items-center justify-center overflow-hidden rounded-full bg-bg-sunken ring-1 ring-border-subtle\">\n {showImage ? (\n <img\n src={speaker.avatarUrl ?? ''}\n alt={speaker.fullName}\n loading=\"lazy\"\n onError={() => setFailed(true)}\n className=\"h-full w-full object-cover\"\n />\n ) : (\n <span className=\"text-base font-semibold text-text-secondary\">{initials(speaker.fullName)}</span>\n )}\n </span>\n );\n}\n\n// Read-only topic chips sourced from the platform tags service for this speaker.\n// The mock shows two topic tags per speaker; these are the real assignments.\nfunction SpeakerTopics({ speakerId }: { speakerId: string }) {\n const { data } = useEntityTagsQuery({ input: { entityType: 'EVENTFULLYMANAGED_SPEAKER', entityId: speakerId } });\n const assignments = data?.eventfullyManagedEntityTags ?? [];\n if (assignments.length === 0) {\n return null;\n }\n return (\n <div className=\"mt-3 flex flex-wrap gap-1.5\" role=\"list\" aria-label=\"Topics\">\n {assignments.slice(0, 4).map((assignment) => {\n const info = assignment.tagInfo;\n const color = info?.color ?? undefined;\n return (\n <Badge key={assignment.id} variant=\"secondary\" className=\"gap-1.5\" role=\"listitem\">\n {color ? (\n <span className=\"h-2 w-2 rounded-full\" style={{ backgroundColor: color }} aria-hidden=\"true\" />\n ) : null}\n {info?.label ?? info?.key ?? 'Topic'}\n </Badge>\n );\n })}\n </div>\n );\n}\n\nfunction SpeakerCardSkeleton() {\n return (\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-5\">\n <div className=\"flex items-start gap-4\">\n <Skeleton className=\"h-16 w-16 shrink-0 rounded-full\" />\n <div className=\"min-w-0 flex-1 space-y-2\">\n <Skeleton className=\"h-5 w-32\" />\n <Skeleton className=\"h-4 w-40\" />\n <Skeleton className=\"h-4 w-full\" />\n <div className=\"flex gap-1.5 pt-1\">\n <Skeleton className=\"h-5 w-12 rounded-full\" />\n <Skeleton className=\"h-5 w-16 rounded-full\" />\n </div>\n </div>\n </div>\n </GlassCard>\n );\n}\n\nexport default function SpeakersPage() {\n const { navigate } = useEventfullyManaged();\n const pagination = usePagination({ pageSize: 12 });\n const [search, setSearch] = useState('');\n const { data, loading, refetch } = useQuery<SpeakerGridResponse, { limit?: number; offset?: number }>(\n SPEAKER_GRID_QUERY,\n { variables: { limit: pagination.limit, offset: pagination.offset } }\n );\n const [deleteSpeaker] = useDeleteSpeakerMutation();\n\n const total = data?.speakers?.total ?? 0;\n const speakers = useMemo(() => data?.speakers?.items ?? [], [data]);\n\n const filtered = useMemo(() => {\n const term = search.trim().toLowerCase();\n if (!term) {\n return speakers;\n }\n return speakers.filter((speaker) =>\n [speaker.fullName, speaker.title, speaker.organization, speaker.bio]\n .filter(Boolean)\n .some((field) => (field as string).toLowerCase().includes(term))\n );\n }, [speakers, search]);\n\n const onDelete = async (speaker: SpeakerRow) => {\n const response = await deleteSpeaker({ variables: { id: speaker.id } });\n if (response.error) {\n notify.error('Could not delete speaker', response.error.message);\n throw response.error;\n }\n if (!response.data?.deleteSpeaker) {\n notify.error('Could not delete speaker', 'The server did not confirm the change.');\n throw new Error('delete-unconfirmed');\n }\n notify.success('Speaker deleted', speaker.fullName);\n await refetch();\n };\n\n return (\n <div className=\"space-y-6 p-6\">\n <PagePurpose>\n Speakers are the people who present your sessions. Keep a roster here — their name, role, company and bio — so\n that when you build the agenda you can assign a speaker to each session in a couple of clicks. A good speaker\n profile also gives attendees a reason to attend.\n </PagePurpose>\n\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between\">\n <div>\n <GradientText as=\"h1\" className=\"text-2xl font-bold\">\n Speakers\n </GradientText>\n <p className=\"mt-1 text-sm text-text-secondary\">Manage your speaker roster with live API data.</p>\n </div>\n <div className=\"flex flex-col gap-2 sm:flex-row sm:items-center\">\n <div className=\"relative\">\n <Search className=\"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-muted\" />\n <Input\n type=\"search\"\n value={search}\n onChange={(event) => setSearch(event.target.value)}\n placeholder=\"Search speakers…\"\n aria-label=\"Search speakers on this page\"\n className=\"pl-10 sm:w-64\"\n />\n </div>\n <Button type=\"button\" onClick={() => navigate('/speakers/create')}>\n <Plus className=\"h-4 w-4\" />\n Add Speaker\n </Button>\n </div>\n </div>\n\n {loading ? (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3\">\n {Array.from({ length: 6 }).map((_, index) => (\n <SpeakerCardSkeleton key={index} />\n ))}\n </div>\n ) : speakers.length === 0 ? (\n <GlassCard treatment=\"plain\" className=\"border-border-seam\">\n <IllustratedEmptyState\n illustration=\"empty-people\"\n title=\"No speakers yet.\"\n description=\"Add your first speaker to start building the agenda.\"\n action={\n <Button type=\"button\" onClick={() => navigate('/speakers/create')}>\n <Plus className=\"h-4 w-4\" />\n Add Speaker\n </Button>\n }\n />\n </GlassCard>\n ) : filtered.length === 0 ? (\n <GlassCard treatment=\"plain\" className=\"border-border-seam\">\n <IllustratedEmptyState\n illustration=\"empty-search\"\n title=\"No speakers match your search.\"\n description=\"Try a different name, title or company.\"\n />\n </GlassCard>\n ) : (\n <div className=\"space-y-4\">\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3\">\n {filtered.map((speaker) => {\n const roleLine = [speaker.title, speaker.organization].filter(Boolean).join(', ');\n return (\n <GlassCard\n key={speaker.id}\n treatment=\"plain\"\n className=\"group flex flex-col border-border-seam p-5 transition-shadow duration-200 hover:shadow-elevation-1\"\n >\n <div className=\"flex items-start gap-4\">\n <SpeakerAvatar speaker={speaker} />\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-start justify-between gap-2\">\n <button\n type=\"button\"\n onClick={() => navigate(`/speakers/${speaker.id}/edit`)}\n className=\"rounded-button text-left text-base font-semibold text-text-primary transition-colors hover:text-text-link focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border-focus\"\n >\n {speaker.fullName}\n </button>\n <ActionMenu\n ariaLabel={`Speaker actions for ${speaker.fullName}`}\n entityName={speaker.fullName}\n deleteLabel=\"Delete speaker\"\n onEdit={() => navigate(`/speakers/${speaker.id}/edit`)}\n onDelete={() => onDelete(speaker)}\n />\n </div>\n <p className=\"mt-0.5 truncate text-sm text-text-secondary\">{roleLine || 'Speaker'}</p>\n </div>\n </div>\n\n {speaker.bio ? <p className=\"mt-3 line-clamp-2 text-sm text-text-secondary\">{speaker.bio}</p> : null}\n\n <SpeakerTopics speakerId={speaker.id} />\n </GlassCard>\n );\n })}\n </div>\n\n {search.trim() ? null : (\n <Pagination\n offset={pagination.offset}\n limit={pagination.limit}\n loadedCount={speakers.length}\n total={total}\n onPrev={pagination.prevPage}\n onNext={pagination.nextPage}\n itemLabel=\"speakers\"\n loading={loading}\n />\n )}\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SponsorsListPage.js","names":[],"sources":["../../../../src/eventfullymanaged/pages/sponsors/SponsorsListPage.tsx"],"sourcesContent":["import { Building2, Coins, Magnet, Plus, Search, TrendingUp, Users } from 'lucide-react';\nimport type { LucideIcon } from 'lucide-react';\nimport { Badge, Button, GlassCard, IllustratedEmptyState, Input, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { useMemo, useState } from 'react';\n\nimport { useDeleteSponsorMutation, useSponsorsQuery } from '../../api/eventApi';\nimport { ActionMenu } from '../../components/ActionMenu';\nimport { SafeLink } from '../../components/SafeLink';\nimport { useEventfullyManaged } from '../../context/EventfullyManagedContext';\nimport { notify } from '../../lib/toast';\n\ntype SponsorMetrics = { value?: number; leads?: number; leadsTarget?: number };\n\ntype SponsorItem = {\n id: string;\n eventId: string;\n name: string;\n level: string;\n logoUrl?: string | null;\n website?: string | null;\n contactName?: string | null;\n contactEmail?: string | null;\n contactPhone?: string | null;\n metrics?: SponsorMetrics | null;\n};\n\n/** Tier order + accent token. Colours resolve to semantic chart tokens only. */\nconst TIERS: { key: string; label: string; accent: string }[] = [\n { key: 'PLATINUM', label: 'Platinum', accent: 'var(--color-chart-2)' },\n { key: 'GOLD', label: 'Gold', accent: 'var(--color-chart-4)' },\n { key: 'SILVER', label: 'Silver', accent: 'var(--color-chart-3)' },\n { key: 'BRONZE', label: 'Bronze', accent: 'var(--color-chart-5)' },\n { key: 'COMMUNITY', label: 'Community', accent: 'var(--color-chart-1)' },\n];\nconst OTHER_TIER = { key: 'OTHER', label: 'Other', accent: 'var(--color-chart-1)' };\n\nconst compactNumber = new Intl.NumberFormat(undefined, { notation: 'compact', maximumFractionDigits: 1 });\n\nfunction StatTile({ icon: Icon, label, value }: { icon: LucideIcon; label: string; value: string }) {\n return (\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-3\">\n <span className=\"flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-bg-sunken text-accent-primary\">\n <Icon className=\"h-5 w-5\" />\n </span>\n <div className=\"min-w-0\">\n <p className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">{label}</p>\n <p className=\"mt-0.5 text-2xl font-bold tabular-nums text-text-primary\">{value}</p>\n </div>\n </div>\n </GlassCard>\n );\n}\n\nfunction LeadsMeter({ leads, target }: { leads: number; target: number }) {\n const pct = target > 0 ? Math.min(100, Math.round((leads / target) * 100)) : 0;\n return (\n <div>\n <div\n className=\"h-2 w-full overflow-hidden rounded-full bg-bg-sunken\"\n role=\"progressbar\"\n aria-valuenow={pct}\n aria-valuemin={0}\n aria-valuemax={100}\n aria-label=\"Leads progress\"\n >\n <div\n className=\"h-full rounded-full transition-[width] duration-500\"\n style={{\n width: `${pct}%`,\n background: 'linear-gradient(90deg, var(--color-chart-1), var(--color-chart-2))',\n }}\n />\n </div>\n <div className=\"mt-1.5 flex items-center justify-between text-xs text-text-secondary\">\n <span>{pct}%</span>\n <span className=\"tabular-nums\">\n {leads.toLocaleString()}\n {target > 0 ? ` / ${target.toLocaleString()} leads target` : ' leads'}\n </span>\n </div>\n </div>\n );\n}\n\nfunction SponsorCard({\n sponsor,\n maxLeads,\n onView,\n onEdit,\n onDelete,\n onViewLeads,\n}: {\n sponsor: SponsorItem;\n maxLeads: number;\n onView: () => void;\n onEdit: () => void;\n onDelete: () => void | Promise<void>;\n onViewLeads: () => void;\n}) {\n const value = Number(sponsor.metrics?.value ?? 0);\n const leads = Number(sponsor.metrics?.leads ?? 0);\n const target = Number(sponsor.metrics?.leadsTarget ?? 0) || maxLeads;\n\n return (\n <GlassCard treatment=\"plain\" className=\"flex flex-col gap-3 border-border-seam p-4\">\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"flex min-w-0 items-center gap-3\">\n <span className=\"flex h-11 w-11 shrink-0 items-center justify-center overflow-hidden rounded-card border border-border-subtle bg-bg-sunken\">\n {sponsor.logoUrl ? (\n <img src={sponsor.logoUrl} alt=\"\" className=\"h-full w-full object-cover\" />\n ) : (\n <Building2 className=\"h-5 w-5 text-text-muted\" />\n )}\n </span>\n <div className=\"min-w-0\">\n <button\n type=\"button\"\n onClick={onView}\n className=\"rounded-button text-left text-base font-semibold text-text-primary transition-colors hover:text-text-link focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n {sponsor.name}\n </button>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n Contract value: <span className=\"font-semibold text-text-primary\">${compactNumber.format(value)}</span>\n </p>\n </div>\n </div>\n <div className=\"flex shrink-0 items-center gap-1\">\n <Badge variant=\"secondary\" className=\"uppercase\">\n {sponsor.level}\n </Badge>\n <ActionMenu\n ariaLabel={`Actions for ${sponsor.name}`}\n entityName={sponsor.name}\n onView={onView}\n onEdit={onEdit}\n onDelete={onDelete}\n deleteLabel=\"Delete sponsor\"\n />\n </div>\n </div>\n\n <LeadsMeter leads={leads} target={target} />\n\n <div className=\"flex items-center justify-between border-t border-border-seam pt-3 text-xs\">\n {sponsor.website ? (\n <SafeLink\n href={sponsor.website}\n className=\"truncate text-text-link hover:underline\"\n fallbackClassName=\"truncate text-text-secondary\"\n >\n {sponsor.website}\n </SafeLink>\n ) : (\n <span className=\"text-text-muted\">No website</span>\n )}\n <button\n type=\"button\"\n onClick={onViewLeads}\n className=\"ml-2 flex shrink-0 items-center gap-1 rounded-button font-medium text-text-link transition-colors hover:text-accent-primary focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <TrendingUp className=\"h-3.5 w-3.5\" />\n View leads\n </button>\n </div>\n </GlassCard>\n );\n}\n\nexport default function SponsorsListPage() {\n const { navigate } = useEventfullyManaged();\n const [search, setSearch] = useState('');\n // Load the full portfolio (not paginated) so tier grouping + rollups are\n // accurate; the sponsor roster is a small, curated list per workspace.\n const { data, loading, refetch } = useSponsorsQuery({ search, limit: 200, offset: 0 });\n const [deleteSponsor] = useDeleteSponsorMutation();\n\n const sponsors = useMemo(() => (data?.sponsors?.items ?? []) as SponsorItem[], [data]);\n const total = data?.sponsors?.total ?? 0;\n\n const portfolioValue = sponsors.reduce((sum, sponsor) => sum + Number(sponsor.metrics?.value ?? 0), 0);\n const portfolioLeads = sponsors.reduce((sum, sponsor) => sum + Number(sponsor.metrics?.leads ?? 0), 0);\n const maxLeads = sponsors.reduce((max, sponsor) => Math.max(max, Number(sponsor.metrics?.leads ?? 0)), 0);\n\n const grouped = useMemo(() => {\n const buckets = new Map<string, SponsorItem[]>();\n for (const sponsor of sponsors) {\n const tier = TIERS.find((t) => t.key === sponsor.level.toUpperCase()) ? sponsor.level.toUpperCase() : OTHER_TIER.key;\n const list = buckets.get(tier) ?? [];\n list.push(sponsor);\n buckets.set(tier, list);\n }\n const ordered = [...TIERS, OTHER_TIER]\n .map((tier) => ({ tier, items: buckets.get(tier.key) ?? [] }))\n .filter((group) => group.items.length > 0);\n return ordered;\n }, [sponsors]);\n\n const handleDelete = async (sponsor: SponsorItem) => {\n const response = await deleteSponsor({ variables: { id: sponsor.id } });\n if (response.error) {\n throw new Error(response.error.message);\n }\n if (!response.data?.deleteSponsor) {\n notify.error('Could not delete sponsor', 'The server did not confirm the deletion. Please try again.');\n throw new Error('Deletion not confirmed');\n }\n await refetch();\n notify.success('Sponsor deleted');\n };\n\n return (\n <div className=\"space-y-6 p-6\">\n <PagePurpose>\n Sponsors fund your event in exchange for visibility and leads. Grouped by tier, each card shows the contract\n value they bring and their progress toward a leads target — so you can prove ROI and keep the relationship warm\n for next time. The portfolio tiles summarise the whole roster at a glance.\n </PagePurpose>\n\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div>\n <h1 className=\"text-2xl font-bold text-text-primary\">Sponsors</h1>\n <p className=\"mt-1 text-sm text-text-secondary\">Manage your event partners by category.</p>\n </div>\n <Button type=\"button\" onClick={() => navigate('/sponsors/create')}>\n <Plus className=\"h-4 w-4\" />\n Add Sponsor\n </Button>\n </div>\n\n <div>\n <h2 className=\"text-sm font-semibold uppercase tracking-wide text-text-secondary\">Portfolio</h2>\n <div className=\"mt-3 grid grid-cols-1 gap-4 md:grid-cols-3\">\n <StatTile icon={Users} label=\"Total Sponsors\" value={total.toLocaleString()} />\n <StatTile icon={Coins} label=\"Contract Value\" value={`$${compactNumber.format(portfolioValue)}`} />\n <StatTile icon={Magnet} label=\"Leads\" value={portfolioLeads.toLocaleString()} />\n </div>\n </div>\n\n <div className=\"relative\">\n <Search className=\"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-muted\" />\n <Input\n type=\"text\"\n value={search}\n onChange={(event) => setSearch(event.target.value)}\n placeholder=\"Search sponsors\"\n aria-label=\"Search sponsors\"\n className=\"pl-10\"\n />\n </div>\n\n {loading && sponsors.length === 0 ? (\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-8 text-center text-sm text-text-secondary\">\n Loading sponsors...\n </GlassCard>\n ) : sponsors.length === 0 ? (\n <GlassCard treatment=\"plain\" className=\"border-border-seam\">\n <IllustratedEmptyState\n illustration={search ? 'empty-search' : 'empty-people'}\n title={search ? 'No sponsors match your search.' : 'No sponsors yet.'}\n description={search ? 'Try a different name.' : 'Add your first sponsor to start tracking value and leads.'}\n action={\n !search ? (\n <Button type=\"button\" onClick={() => navigate('/sponsors/create')}>\n <Plus className=\"h-4 w-4\" />\n Add Sponsor\n </Button>\n ) : undefined\n }\n />\n </GlassCard>\n ) : (\n <div className=\"space-y-8\">\n <div>\n <h2 className=\"text-lg font-semibold text-text-primary\">Sponsor Tiers</h2>\n <p className=\"text-sm text-text-secondary\">Manage your event partners by category.</p>\n </div>\n {grouped.map(({ tier, items }) => (\n <section key={tier.key} className=\"space-y-3\">\n <div\n className=\"flex items-center justify-between rounded-card border border-border-seam px-4 py-2.5 text-sm font-semibold uppercase tracking-wide\"\n style={{\n background: `color-mix(in oklab, ${tier.accent} 14%, var(--color-bg-surface))`,\n color: `color-mix(in oklab, ${tier.accent} 72%, var(--color-text-primary))`,\n }}\n >\n <span className=\"flex items-center gap-2.5\">\n <span aria-hidden className=\"h-4 w-1.5 rounded-full\" style={{ backgroundColor: tier.accent }} />\n {tier.label}\n </span>\n <span className=\"tabular-nums\">{items.length}</span>\n </div>\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3\">\n {items.map((sponsor) => (\n <SponsorCard\n key={sponsor.id}\n sponsor={sponsor}\n maxLeads={maxLeads}\n onView={() => navigate(`/sponsors/${sponsor.id}`)}\n onEdit={() => navigate(`/sponsors/${sponsor.id}/edit`)}\n onDelete={() => handleDelete(sponsor)}\n onViewLeads={() => navigate(`/sponsors/${sponsor.id}/leads`)}\n />\n ))}\n </div>\n </section>\n ))}\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SponsorsListPage.js","names":[],"sources":["../../../../src/eventfullymanaged/pages/sponsors/SponsorsListPage.tsx"],"sourcesContent":["import { Building2, Coins, Magnet, Plus, Search, TrendingUp, Users } from 'lucide-react';\nimport type { LucideIcon } from 'lucide-react';\nimport { Badge, Button, GlassCard, IllustratedEmptyState, Input, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { useMemo, useState } from 'react';\n\nimport { useDeleteSponsorMutation, useSponsorsQuery } from '../../api/eventApi';\nimport { ActionMenu } from '../../components/ActionMenu';\nimport { SafeLink } from '../../components/SafeLink';\nimport { useEventfullyManaged } from '../../context/EventfullyManagedContext';\nimport { notify } from '../../lib/toast';\n\ntype SponsorMetrics = { value?: number; leads?: number; leadsTarget?: number };\n\ntype SponsorItem = {\n id: string;\n eventId: string;\n name: string;\n level: string;\n logoUrl?: string | null;\n website?: string | null;\n contactName?: string | null;\n contactEmail?: string | null;\n contactPhone?: string | null;\n metrics?: SponsorMetrics | null;\n};\n\n/** Tier order + accent token. Colours resolve to semantic chart tokens only. */\nconst TIERS: { key: string; label: string; accent: string }[] = [\n { key: 'PLATINUM', label: 'Platinum', accent: 'var(--color-chart-2)' },\n { key: 'GOLD', label: 'Gold', accent: 'var(--color-chart-4)' },\n { key: 'SILVER', label: 'Silver', accent: 'var(--color-chart-3)' },\n { key: 'BRONZE', label: 'Bronze', accent: 'var(--color-chart-5)' },\n { key: 'COMMUNITY', label: 'Community', accent: 'var(--color-chart-1)' },\n];\nconst OTHER_TIER = { key: 'OTHER', label: 'Other', accent: 'var(--color-chart-1)' };\n\nconst compactNumber = new Intl.NumberFormat(undefined, { notation: 'compact', maximumFractionDigits: 1 });\n\nfunction StatTile({ icon: Icon, label, value }: { icon: LucideIcon; label: string; value: string }) {\n return (\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-3\">\n <span className=\"flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-bg-sunken text-accent-primary\">\n <Icon className=\"h-5 w-5\" />\n </span>\n <div className=\"min-w-0\">\n <p className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">{label}</p>\n <p className=\"mt-0.5 text-2xl font-bold tabular-nums text-text-primary\">{value}</p>\n </div>\n </div>\n </GlassCard>\n );\n}\n\nfunction LeadsMeter({ leads, target }: { leads: number; target: number }) {\n const pct = target > 0 ? Math.min(100, Math.round((leads / target) * 100)) : 0;\n return (\n <div>\n <div\n className=\"h-2 w-full overflow-hidden rounded-full bg-bg-sunken\"\n role=\"progressbar\"\n aria-valuenow={pct}\n aria-valuemin={0}\n aria-valuemax={100}\n aria-label=\"Leads progress\"\n >\n <div\n className=\"h-full rounded-full transition-[width] duration-500\"\n style={{\n width: `${pct}%`,\n background: 'linear-gradient(90deg, var(--color-chart-1), var(--color-chart-2))',\n }}\n />\n </div>\n <div className=\"mt-1.5 flex items-center justify-between text-xs text-text-secondary\">\n <span>{pct}%</span>\n <span className=\"tabular-nums\">\n {leads.toLocaleString()}\n {target > 0 ? ` / ${target.toLocaleString()} leads target` : ' leads'}\n </span>\n </div>\n </div>\n );\n}\n\nfunction SponsorCard({\n sponsor,\n maxLeads,\n onView,\n onEdit,\n onDelete,\n onViewLeads,\n}: {\n sponsor: SponsorItem;\n maxLeads: number;\n onView: () => void;\n onEdit: () => void;\n onDelete: () => void | Promise<void>;\n onViewLeads: () => void;\n}) {\n const value = Number(sponsor.metrics?.value ?? 0);\n const leads = Number(sponsor.metrics?.leads ?? 0);\n const target = Number(sponsor.metrics?.leadsTarget ?? 0) || maxLeads;\n\n return (\n <GlassCard treatment=\"plain\" className=\"flex flex-col gap-3 border-border-seam p-4\">\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"flex min-w-0 items-center gap-3\">\n <span className=\"flex h-11 w-11 shrink-0 items-center justify-center overflow-hidden rounded-card border border-border-subtle bg-bg-sunken\">\n {sponsor.logoUrl ? (\n <img src={sponsor.logoUrl} alt=\"\" className=\"h-full w-full object-cover\" />\n ) : (\n <Building2 className=\"h-5 w-5 text-text-muted\" />\n )}\n </span>\n <div className=\"min-w-0\">\n <button\n type=\"button\"\n onClick={onView}\n className=\"rounded-button text-left text-base font-semibold text-text-primary transition-colors hover:text-text-link focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n {sponsor.name}\n </button>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n Contract value: <span className=\"font-semibold text-text-primary\">${compactNumber.format(value)}</span>\n </p>\n </div>\n </div>\n <div className=\"flex shrink-0 items-center gap-1\">\n <Badge variant=\"secondary\" className=\"uppercase\">\n {sponsor.level}\n </Badge>\n <ActionMenu\n ariaLabel={`Actions for ${sponsor.name}`}\n entityName={sponsor.name}\n onView={onView}\n onEdit={onEdit}\n onDelete={onDelete}\n deleteLabel=\"Delete sponsor\"\n />\n </div>\n </div>\n\n <LeadsMeter leads={leads} target={target} />\n\n <div className=\"flex items-center justify-between border-t border-border-seam pt-3 text-xs\">\n {sponsor.website ? (\n <SafeLink\n href={sponsor.website}\n className=\"truncate text-text-link hover:underline\"\n fallbackClassName=\"truncate text-text-secondary\"\n >\n {sponsor.website}\n </SafeLink>\n ) : (\n <span className=\"text-text-muted\">No website</span>\n )}\n <button\n type=\"button\"\n onClick={onViewLeads}\n className=\"ml-2 flex shrink-0 items-center gap-1 rounded-button font-medium text-text-link transition-colors hover:text-accent-primary focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <TrendingUp className=\"h-3.5 w-3.5\" />\n View leads\n </button>\n </div>\n </GlassCard>\n );\n}\n\nexport default function SponsorsListPage() {\n const { navigate } = useEventfullyManaged();\n const [search, setSearch] = useState('');\n // Load the full portfolio (not paginated) so tier grouping + rollups are\n // accurate; the sponsor roster is a small, curated list per workspace.\n const { data, loading, refetch } = useSponsorsQuery({ search, limit: 200, offset: 0 });\n const [deleteSponsor] = useDeleteSponsorMutation();\n\n const sponsors = useMemo(() => (data?.sponsors?.items ?? []) as SponsorItem[], [data]);\n const total = data?.sponsors?.total ?? 0;\n\n const portfolioValue = sponsors.reduce((sum, sponsor) => sum + Number(sponsor.metrics?.value ?? 0), 0);\n const portfolioLeads = sponsors.reduce((sum, sponsor) => sum + Number(sponsor.metrics?.leads ?? 0), 0);\n const maxLeads = sponsors.reduce((max, sponsor) => Math.max(max, Number(sponsor.metrics?.leads ?? 0)), 0);\n\n const grouped = useMemo(() => {\n const buckets = new Map<string, SponsorItem[]>();\n for (const sponsor of sponsors) {\n const tier = TIERS.find((t) => t.key === sponsor.level.toUpperCase())\n ? sponsor.level.toUpperCase()\n : OTHER_TIER.key;\n const list = buckets.get(tier) ?? [];\n list.push(sponsor);\n buckets.set(tier, list);\n }\n const ordered = [...TIERS, OTHER_TIER]\n .map((tier) => ({ tier, items: buckets.get(tier.key) ?? [] }))\n .filter((group) => group.items.length > 0);\n return ordered;\n }, [sponsors]);\n\n const handleDelete = async (sponsor: SponsorItem) => {\n const response = await deleteSponsor({ variables: { id: sponsor.id } });\n if (response.error) {\n throw new Error(response.error.message);\n }\n if (!response.data?.deleteSponsor) {\n notify.error('Could not delete sponsor', 'The server did not confirm the deletion. Please try again.');\n throw new Error('Deletion not confirmed');\n }\n await refetch();\n notify.success('Sponsor deleted');\n };\n\n return (\n <div className=\"space-y-6 p-6\">\n <PagePurpose>\n Sponsors fund your event in exchange for visibility and leads. Grouped by tier, each card shows the contract\n value they bring and their progress toward a leads target — so you can prove ROI and keep the relationship warm\n for next time. The portfolio tiles summarise the whole roster at a glance.\n </PagePurpose>\n\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div>\n <h1 className=\"text-2xl font-bold text-text-primary\">Sponsors</h1>\n <p className=\"mt-1 text-sm text-text-secondary\">Manage your event partners by category.</p>\n </div>\n <Button type=\"button\" onClick={() => navigate('/sponsors/create')}>\n <Plus className=\"h-4 w-4\" />\n Add Sponsor\n </Button>\n </div>\n\n <div>\n <h2 className=\"text-sm font-semibold uppercase tracking-wide text-text-secondary\">Portfolio</h2>\n <div className=\"mt-3 grid grid-cols-1 gap-4 md:grid-cols-3\">\n <StatTile icon={Users} label=\"Total Sponsors\" value={total.toLocaleString()} />\n <StatTile icon={Coins} label=\"Contract Value\" value={`$${compactNumber.format(portfolioValue)}`} />\n <StatTile icon={Magnet} label=\"Leads\" value={portfolioLeads.toLocaleString()} />\n </div>\n </div>\n\n <div className=\"relative\">\n <Search className=\"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-muted\" />\n <Input\n type=\"text\"\n value={search}\n onChange={(event) => setSearch(event.target.value)}\n placeholder=\"Search sponsors\"\n aria-label=\"Search sponsors\"\n className=\"pl-10\"\n />\n </div>\n\n {loading && sponsors.length === 0 ? (\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-8 text-center text-sm text-text-secondary\">\n Loading sponsors...\n </GlassCard>\n ) : sponsors.length === 0 ? (\n <GlassCard treatment=\"plain\" className=\"border-border-seam\">\n <IllustratedEmptyState\n illustration={search ? 'empty-search' : 'empty-people'}\n title={search ? 'No sponsors match your search.' : 'No sponsors yet.'}\n description={search ? 'Try a different name.' : 'Add your first sponsor to start tracking value and leads.'}\n action={\n !search ? (\n <Button type=\"button\" onClick={() => navigate('/sponsors/create')}>\n <Plus className=\"h-4 w-4\" />\n Add Sponsor\n </Button>\n ) : undefined\n }\n />\n </GlassCard>\n ) : (\n <div className=\"space-y-8\">\n <div>\n <h2 className=\"text-lg font-semibold text-text-primary\">Sponsor Tiers</h2>\n <p className=\"text-sm text-text-secondary\">Manage your event partners by category.</p>\n </div>\n {grouped.map(({ tier, items }) => (\n <section key={tier.key} className=\"space-y-3\">\n <div\n className=\"flex items-center justify-between rounded-card border border-border-seam px-4 py-2.5 text-sm font-semibold uppercase tracking-wide\"\n style={{\n background: `color-mix(in oklab, ${tier.accent} 14%, var(--color-bg-surface))`,\n color: `color-mix(in oklab, ${tier.accent} 72%, var(--color-text-primary))`,\n }}\n >\n <span className=\"flex items-center gap-2.5\">\n <span aria-hidden className=\"h-4 w-1.5 rounded-full\" style={{ backgroundColor: tier.accent }} />\n {tier.label}\n </span>\n <span className=\"tabular-nums\">{items.length}</span>\n </div>\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3\">\n {items.map((sponsor) => (\n <SponsorCard\n key={sponsor.id}\n sponsor={sponsor}\n maxLeads={maxLeads}\n onView={() => navigate(`/sponsors/${sponsor.id}`)}\n onEdit={() => navigate(`/sponsors/${sponsor.id}/edit`)}\n onDelete={() => handleDelete(sponsor)}\n onViewLeads={() => navigate(`/sponsors/${sponsor.id}/leads`)}\n />\n ))}\n </div>\n </section>\n ))}\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FloorPlansPage.js","names":[],"sources":["../../../../src/eventfullymanaged/pages/venues/FloorPlansPage.tsx"],"sourcesContent":["import { ArrowLeft, LayoutGrid, Map as MapIcon, Plus, SquareStack } from 'lucide-react';\nimport {\n Badge,\n Button,\n GlassCard,\n GradientText,\n IllustratedEmptyState,\n Input,\n Skeleton,\n} from '@burdenoff/fe-libs/ui';\nimport { useMemo, useState } from 'react';\nimport { useParams } from 'react-router-dom';\n\nimport {\n useCreateFloorPlanMutation,\n useDeleteFloorPlanMutation,\n useUpdateFloorPlanMutation,\n useVenueQuery,\n} from '../../api/eventApi';\nimport { ActionMenu } from '../../components/ActionMenu';\nimport { ImageUploadField } from '../../components/ImageUploadField';\nimport { useEventfullyManaged } from '../../context/EventfullyManagedContext';\nimport { describeError, mutationSucceeded } from '../../lib/mutationResult';\nimport { notify } from '../../lib/toast';\n\ntype Json = Record<string, unknown> | null | undefined;\n\ntype FloorPlan = {\n id: string;\n venueId: string;\n name: string;\n imageUrl?: string | null;\n dimensions?: Json;\n interactiveAreas?: Json;\n};\n\ntype Zone = {\n key: string;\n label: string;\n type?: string;\n capacity?: number;\n rect?: { x: number; y: number; w: number; h: number };\n};\n\nconst FLOOR_PLAN_FOLDER = '/EventfullyManaged/FloorPlans';\n\nfunction asNumber(value: unknown): number | undefined {\n if (typeof value === 'number' && Number.isFinite(value)) {\n return value;\n }\n if (typeof value === 'string' && value.trim() !== '' && Number.isFinite(Number(value))) {\n return Number(value);\n }\n return undefined;\n}\n\nfunction asString(value: unknown): string | undefined {\n return typeof value === 'string' && value.trim() !== '' ? value : undefined;\n}\n\n// interactiveAreas is free-form JSON persisted verbatim by the backend. Parse it\n// defensively so the viewer renders whatever real shape a plan happens to hold —\n// an array of areas, a `{ zones: [...] }` / `{ areas: [...] }` wrapper, or a\n// keyed object — without ever fabricating booth data that isn't there.\nfunction extractZones(interactiveAreas: Json): Zone[] {\n if (!interactiveAreas) {\n return [];\n }\n let raw: unknown = interactiveAreas;\n if (!Array.isArray(raw) && typeof raw === 'object') {\n const obj = raw as Record<string, unknown>;\n if (Array.isArray(obj.zones)) {\n raw = obj.zones;\n } else if (Array.isArray(obj.areas)) {\n raw = obj.areas;\n } else if (Array.isArray(obj.booths)) {\n raw = obj.booths;\n }\n }\n\n const entries: Array<[string, unknown]> = Array.isArray(raw)\n ? raw.map((item, index) => [String(index), item])\n : typeof raw === 'object' && raw !== null\n ? Object.entries(raw as Record<string, unknown>)\n : [];\n\n const zones: Zone[] = [];\n for (const [key, value] of entries) {\n if (value === null || typeof value !== 'object') {\n continue;\n }\n const item = value as Record<string, unknown>;\n const label = asString(item.label) ?? asString(item.name) ?? asString(item.title) ?? asString(item.id) ?? key;\n const x = asNumber(item.x);\n const y = asNumber(item.y);\n const w = asNumber(item.w) ?? asNumber(item.width);\n const h = asNumber(item.h) ?? asNumber(item.height);\n let rect: Zone['rect'];\n if (x !== undefined && y !== undefined && w !== undefined && h !== undefined) {\n // Normalise fractional (0–1) or percentage (0–100) coordinates to percent.\n const scale = x <= 1 && y <= 1 && w <= 1 && h <= 1 ? 100 : 1;\n if (scale === 100 || (x <= 100 && y <= 100 && w <= 100 && h <= 100)) {\n rect = { x: x * scale, y: y * scale, w: w * scale, h: h * scale };\n }\n }\n zones.push({\n key: `${key}-${label}`,\n label,\n type: asString(item.type) ?? asString(item.kind),\n capacity: asNumber(item.capacity) ?? asNumber(item.seats),\n rect,\n });\n }\n return zones;\n}\n\ntype PlanMetric = { label: string; value: string };\n\nfunction buildMetrics(plan: FloorPlan, zones: Zone[]): PlanMetric[] {\n const metrics: PlanMetric[] = [];\n if (zones.length > 0) {\n metrics.push({ label: 'Zones', value: zones.length.toLocaleString() });\n }\n\n const boothCount = zones.filter((zone) => (zone.type ?? '').toLowerCase().includes('booth')).length;\n if (boothCount > 0) {\n metrics.push({ label: 'Booths', value: boothCount.toLocaleString() });\n }\n\n const zoneCapacity = zones.reduce((sum, zone) => sum + (zone.capacity ?? 0), 0);\n const dims = (plan.dimensions ?? {}) as Record<string, unknown>;\n const declaredCapacity = asNumber(dims.capacity) ?? asNumber(dims.totalCapacity);\n const capacity = zoneCapacity > 0 ? zoneCapacity : declaredCapacity;\n if (capacity !== undefined && capacity > 0) {\n metrics.push({ label: 'Total capacity', value: capacity.toLocaleString() });\n }\n\n const width = asNumber(dims.width);\n const height = asNumber(dims.height);\n if (width !== undefined && height !== undefined) {\n const unit = asString(dims.unit) ?? 'm';\n metrics.push({ label: 'Dimensions', value: `${width} × ${height} ${unit}` });\n }\n\n return metrics;\n}\n\nfunction FloorPlanViewer({\n plan,\n zones,\n onAttachImage,\n}: {\n plan: FloorPlan;\n zones: Zone[];\n onAttachImage: (plan: FloorPlan, url: string) => void;\n}) {\n const overlayZones = zones.filter((zone) => zone.rect);\n return (\n <GlassCard treatment=\"plain\" className=\"overflow-hidden border-border-seam p-0\">\n <div className=\"flex items-center justify-between gap-2 border-b border-border-seam px-5 py-3\">\n <div className=\"flex min-w-0 items-center gap-2 text-text-primary\">\n <MapIcon className=\"h-4 w-4 shrink-0 text-action-primary-bg\" />\n <h2 className=\"truncate text-base font-semibold\">{plan.name}</h2>\n </div>\n {zones.length > 0 ? (\n <Badge variant=\"secondary\" className=\"shrink-0\">\n {zones.length} {zones.length === 1 ? 'zone' : 'zones'}\n </Badge>\n ) : null}\n </div>\n\n {plan.imageUrl ? (\n <div className=\"relative bg-bg-sunken\">\n <img\n src={plan.imageUrl}\n alt={`${plan.name} floor plan`}\n className=\"mx-auto max-h-[32rem] w-full object-contain\"\n />\n {overlayZones.map((zone) => (\n <div\n key={zone.key}\n className=\"absolute flex items-center justify-center rounded-md border border-accent-primary/70 bg-action-primary-bg/15 text-[10px] font-medium text-accent-primary\"\n style={{\n left: `${zone.rect?.x}%`,\n top: `${zone.rect?.y}%`,\n width: `${zone.rect?.w}%`,\n height: `${zone.rect?.h}%`,\n }}\n title={zone.label}\n >\n <span className=\"truncate px-1\">{zone.label}</span>\n </div>\n ))}\n </div>\n ) : (\n <div className=\"p-6\">\n <div className=\"rounded-card border border-dashed border-border-default bg-bg-sunken p-6\">\n <p className=\"mb-3 text-center text-sm text-text-secondary\">\n No image attached yet. Upload the hall map for this plan.\n </p>\n <ImageUploadField\n label=\"Upload floor plan image\"\n folderPath={FLOOR_PLAN_FOLDER}\n value={null}\n onUploaded={(url) => onAttachImage(plan, url)}\n hint=\"Stored securely in Files and shown here once attached.\"\n />\n </div>\n </div>\n )}\n\n {zones.length > 0 ? (\n <div className=\"border-t border-border-seam px-5 py-4\">\n <div className=\"mb-2 flex items-center gap-2 text-sm font-medium text-text-primary\">\n <LayoutGrid className=\"h-4 w-4 text-text-muted\" />\n Labeled zones\n </div>\n <div className=\"flex flex-wrap gap-2\">\n {zones.map((zone) => (\n <Badge key={zone.key} variant=\"outline\" className=\"gap-1.5\">\n {zone.label}\n {zone.capacity ? <span className=\"text-text-muted\">· {zone.capacity}</span> : null}\n </Badge>\n ))}\n </div>\n </div>\n ) : null}\n </GlassCard>\n );\n}\n\nexport default function FloorPlansPage() {\n const { id = '' } = useParams<{ id: string }>();\n const { navigate } = useEventfullyManaged();\n const { data, loading, refetch } = useVenueQuery(id);\n const [createFloorPlan, { loading: creating }] = useCreateFloorPlanMutation();\n const [updateFloorPlan] = useUpdateFloorPlanMutation();\n const [deleteFloorPlan] = useDeleteFloorPlanMutation();\n const [name, setName] = useState('');\n const [imageUrl, setImageUrl] = useState('');\n const [selectedId, setSelectedId] = useState<string | null>(null);\n const [showCreate, setShowCreate] = useState(false);\n\n const venue = data?.venue;\n const floorPlans = useMemo(() => (data?.floorPlans?.items ?? []) as FloorPlan[], [data]);\n\n const selectedPlan = useMemo(\n () => floorPlans.find((plan) => plan.id === selectedId) ?? floorPlans[0] ?? null,\n [floorPlans, selectedId]\n );\n const selectedZones = useMemo(() => extractZones(selectedPlan?.interactiveAreas), [selectedPlan]);\n const metrics = useMemo(() => (selectedPlan ? buildMetrics(selectedPlan, selectedZones) : []), [selectedPlan, selectedZones]);\n\n const handleCreate = async () => {\n if (!name.trim() || !id) {\n return;\n }\n try {\n const response = await createFloorPlan({\n variables: {\n input: { venueId: id, name: name.trim(), imageUrl, dimensions: {}, interactiveAreas: {} },\n },\n });\n if (!mutationSucceeded(response, response.data?.createFloorPlan?.id, 'Could not create floor plan')) {\n return;\n }\n setName('');\n setImageUrl('');\n setShowCreate(false);\n const createdId = response.data?.createFloorPlan?.id;\n if (createdId) {\n setSelectedId(createdId);\n }\n notify.success('Floor plan created');\n await refetch();\n } catch (error) {\n notify.error('Could not create floor plan', describeError(error));\n }\n };\n\n const handleAttachImage = async (plan: FloorPlan, url: string) => {\n try {\n const response = await updateFloorPlan({\n variables: {\n id: plan.id,\n input: {\n venueId: plan.venueId,\n name: plan.name,\n imageUrl: url,\n dimensions: plan.dimensions ?? {},\n interactiveAreas: plan.interactiveAreas ?? {},\n },\n },\n });\n if (!mutationSucceeded(response, response.data?.updateFloorPlan?.id, 'Could not attach image')) {\n return;\n }\n await refetch();\n } catch (error) {\n notify.error('Could not attach image', describeError(error));\n }\n };\n\n const handleDelete = async (plan: FloorPlan) => {\n const response = await deleteFloorPlan({ variables: { id: plan.id } });\n if (response.error) {\n notify.error('Could not delete floor plan', response.error.message);\n throw response.error;\n }\n if (!response.data?.deleteFloorPlan) {\n notify.error('Could not delete floor plan', 'The server did not confirm the change.');\n throw new Error('delete-unconfirmed');\n }\n if (selectedId === plan.id) {\n setSelectedId(null);\n }\n notify.success('Floor plan deleted', plan.name);\n await refetch();\n };\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between\">\n <div className=\"space-y-3\">\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => navigate(`/venues/${id}`)}>\n <ArrowLeft className=\"h-4 w-4\" />\n Back to Venue\n </Button>\n <GradientText as=\"h1\" className=\"text-2xl font-bold\">\n {venue?.name ? `${venue.name} — Floor Plans` : 'Floor Plans'}\n </GradientText>\n <p className=\"text-sm text-text-secondary\">\n Map the hall layout, booth positions, stage and entrances so exhibitors and attendees can navigate on the\n day.\n </p>\n </div>\n <Button type=\"button\" onClick={() => setShowCreate((value) => !value)}>\n <Plus className=\"h-4 w-4\" />\n Add floor plan\n </Button>\n </div>\n\n {loading ? (\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n <Skeleton className=\"h-96 rounded-card lg:col-span-2\" />\n <Skeleton className=\"h-96 rounded-card\" />\n </div>\n ) : floorPlans.length === 0 && !showCreate ? (\n <GlassCard treatment=\"plain\" className=\"border-border-seam\">\n <IllustratedEmptyState\n illustration=\"empty-files\"\n title=\"No floor plans yet.\"\n description=\"Add one to start mapping this venue — upload the hall map and label its zones.\"\n action={\n <Button type=\"button\" onClick={() => setShowCreate(true)}>\n <Plus className=\"h-4 w-4\" />\n Add floor plan\n </Button>\n }\n />\n </GlassCard>\n ) : (\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n <div className=\"lg:col-span-2\">\n {selectedPlan ? (\n <FloorPlanViewer plan={selectedPlan} zones={selectedZones} onAttachImage={handleAttachImage} />\n ) : (\n <GlassCard treatment=\"plain\" className=\"flex h-full items-center justify-center border-border-seam p-10\">\n <p className=\"text-sm text-text-secondary\">Add a floor plan to preview it here.</p>\n </GlassCard>\n )}\n </div>\n\n <div className=\"space-y-6\">\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-5\">\n <h2 className=\"text-sm font-semibold text-text-primary\">Available Floor Plans</h2>\n <ul className=\"mt-3 space-y-2\" role=\"list\">\n {floorPlans.length === 0 ? (\n <li className=\"text-sm text-text-secondary\">None yet — create one below.</li>\n ) : (\n floorPlans.map((plan) => {\n const active = selectedPlan?.id === plan.id;\n return (\n <li key={plan.id}>\n <div\n className={`flex items-center gap-3 rounded-card border p-3 transition-colors ${\n active\n ? 'border-accent-primary bg-action-primary-bg/10'\n : 'border-border-seam hover:border-border-strong'\n }`}\n >\n <button\n type=\"button\"\n onClick={() => setSelectedId(plan.id)}\n className=\"flex min-w-0 flex-1 items-center gap-3 text-left focus-visible:outline-none\"\n aria-pressed={active}\n >\n <span\n className={`flex h-9 w-9 shrink-0 items-center justify-center rounded-md ${\n active ? 'bg-action-primary-bg text-action-primary-text' : 'bg-bg-sunken text-text-secondary'\n }`}\n >\n <SquareStack className=\"h-4 w-4\" />\n </span>\n <span className=\"min-w-0\">\n <span className=\"block truncate text-sm font-medium text-text-primary\">{plan.name}</span>\n <span className=\"block text-xs text-text-muted\">\n {plan.imageUrl ? 'Map attached' : 'No image yet'}\n </span>\n </span>\n </button>\n <ActionMenu\n ariaLabel={`Floor plan actions for ${plan.name}`}\n entityName={plan.name}\n deleteLabel=\"Delete floor plan\"\n onDelete={() => handleDelete(plan)}\n />\n </div>\n </li>\n );\n })\n )}\n </ul>\n </GlassCard>\n\n {selectedPlan ? (\n <GlassCard treatment=\"metric\" className=\"p-5\">\n <h2 className=\"text-sm font-semibold text-text-primary\">Plan Details</h2>\n {metrics.length > 0 ? (\n <dl className=\"mt-3 space-y-2\">\n {metrics.map((metric) => (\n <div key={metric.label} className=\"flex items-center justify-between text-sm\">\n <dt className=\"text-text-secondary\">{metric.label}</dt>\n <dd className=\"font-semibold tabular-nums text-text-primary\">{metric.value}</dd>\n </div>\n ))}\n </dl>\n ) : (\n <p className=\"mt-3 text-sm text-text-secondary\">\n No zones or dimensions captured for this plan yet.\n </p>\n )}\n </GlassCard>\n ) : null}\n\n {showCreate || floorPlans.length === 0 ? (\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-5\">\n <h2 className=\"mb-3 text-sm font-semibold text-text-primary\">Add a floor plan</h2>\n <div className=\"space-y-4\">\n <Input\n type=\"text\"\n value={name}\n onChange={(event) => setName(event.target.value)}\n placeholder=\"Main Hall Layout\"\n aria-label=\"Floor plan name\"\n />\n <ImageUploadField\n label=\"Upload floor plan image\"\n folderPath={FLOOR_PLAN_FOLDER}\n value={imageUrl || null}\n onUploaded={setImageUrl}\n hint=\"Optional now — you can attach it later from the viewer.\"\n />\n <div className=\"flex justify-end gap-2\">\n {floorPlans.length > 0 ? (\n <Button type=\"button\" variant=\"ghost\" onClick={() => setShowCreate(false)}>\n Cancel\n </Button>\n ) : null}\n <Button type=\"button\" onClick={handleCreate} disabled={creating || !name.trim()}>\n <Plus className=\"h-4 w-4\" />\n {creating ? 'Creating…' : 'Create'}\n </Button>\n </div>\n </div>\n </GlassCard>\n ) : null}\n </div>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"FloorPlansPage.js","names":[],"sources":["../../../../src/eventfullymanaged/pages/venues/FloorPlansPage.tsx"],"sourcesContent":["import { ArrowLeft, LayoutGrid, Map as MapIcon, Plus, SquareStack } from 'lucide-react';\nimport { Badge, Button, GlassCard, GradientText, IllustratedEmptyState, Input, Skeleton } from '@burdenoff/fe-libs/ui';\nimport { useMemo, useState } from 'react';\nimport { useParams } from 'react-router-dom';\n\nimport {\n useCreateFloorPlanMutation,\n useDeleteFloorPlanMutation,\n useUpdateFloorPlanMutation,\n useVenueQuery,\n} from '../../api/eventApi';\nimport { ActionMenu } from '../../components/ActionMenu';\nimport { ImageUploadField } from '../../components/ImageUploadField';\nimport { useEventfullyManaged } from '../../context/EventfullyManagedContext';\nimport { describeError, mutationSucceeded } from '../../lib/mutationResult';\nimport { notify } from '../../lib/toast';\n\ntype Json = Record<string, unknown> | null | undefined;\n\ntype FloorPlan = {\n id: string;\n venueId: string;\n name: string;\n imageUrl?: string | null;\n dimensions?: Json;\n interactiveAreas?: Json;\n};\n\ntype Zone = {\n key: string;\n label: string;\n type?: string;\n capacity?: number;\n rect?: { x: number; y: number; w: number; h: number };\n};\n\nconst FLOOR_PLAN_FOLDER = '/EventfullyManaged/FloorPlans';\n\nfunction asNumber(value: unknown): number | undefined {\n if (typeof value === 'number' && Number.isFinite(value)) {\n return value;\n }\n if (typeof value === 'string' && value.trim() !== '' && Number.isFinite(Number(value))) {\n return Number(value);\n }\n return undefined;\n}\n\nfunction asString(value: unknown): string | undefined {\n return typeof value === 'string' && value.trim() !== '' ? value : undefined;\n}\n\n// interactiveAreas is free-form JSON persisted verbatim by the backend. Parse it\n// defensively so the viewer renders whatever real shape a plan happens to hold —\n// an array of areas, a `{ zones: [...] }` / `{ areas: [...] }` wrapper, or a\n// keyed object — without ever fabricating booth data that isn't there.\nfunction extractZones(interactiveAreas: Json): Zone[] {\n if (!interactiveAreas) {\n return [];\n }\n let raw: unknown = interactiveAreas;\n if (!Array.isArray(raw) && typeof raw === 'object') {\n const obj = raw as Record<string, unknown>;\n if (Array.isArray(obj.zones)) {\n raw = obj.zones;\n } else if (Array.isArray(obj.areas)) {\n raw = obj.areas;\n } else if (Array.isArray(obj.booths)) {\n raw = obj.booths;\n }\n }\n\n const entries: Array<[string, unknown]> = Array.isArray(raw)\n ? raw.map((item, index) => [String(index), item])\n : typeof raw === 'object' && raw !== null\n ? Object.entries(raw as Record<string, unknown>)\n : [];\n\n const zones: Zone[] = [];\n for (const [key, value] of entries) {\n if (value === null || typeof value !== 'object') {\n continue;\n }\n const item = value as Record<string, unknown>;\n const label = asString(item.label) ?? asString(item.name) ?? asString(item.title) ?? asString(item.id) ?? key;\n const x = asNumber(item.x);\n const y = asNumber(item.y);\n const w = asNumber(item.w) ?? asNumber(item.width);\n const h = asNumber(item.h) ?? asNumber(item.height);\n let rect: Zone['rect'];\n if (x !== undefined && y !== undefined && w !== undefined && h !== undefined) {\n // Normalise fractional (0–1) or percentage (0–100) coordinates to percent.\n const scale = x <= 1 && y <= 1 && w <= 1 && h <= 1 ? 100 : 1;\n if (scale === 100 || (x <= 100 && y <= 100 && w <= 100 && h <= 100)) {\n rect = { x: x * scale, y: y * scale, w: w * scale, h: h * scale };\n }\n }\n zones.push({\n key: `${key}-${label}`,\n label,\n type: asString(item.type) ?? asString(item.kind),\n capacity: asNumber(item.capacity) ?? asNumber(item.seats),\n rect,\n });\n }\n return zones;\n}\n\ntype PlanMetric = { label: string; value: string };\n\nfunction buildMetrics(plan: FloorPlan, zones: Zone[]): PlanMetric[] {\n const metrics: PlanMetric[] = [];\n if (zones.length > 0) {\n metrics.push({ label: 'Zones', value: zones.length.toLocaleString() });\n }\n\n const boothCount = zones.filter((zone) => (zone.type ?? '').toLowerCase().includes('booth')).length;\n if (boothCount > 0) {\n metrics.push({ label: 'Booths', value: boothCount.toLocaleString() });\n }\n\n const zoneCapacity = zones.reduce((sum, zone) => sum + (zone.capacity ?? 0), 0);\n const dims = (plan.dimensions ?? {}) as Record<string, unknown>;\n const declaredCapacity = asNumber(dims.capacity) ?? asNumber(dims.totalCapacity);\n const capacity = zoneCapacity > 0 ? zoneCapacity : declaredCapacity;\n if (capacity !== undefined && capacity > 0) {\n metrics.push({ label: 'Total capacity', value: capacity.toLocaleString() });\n }\n\n const width = asNumber(dims.width);\n const height = asNumber(dims.height);\n if (width !== undefined && height !== undefined) {\n const unit = asString(dims.unit) ?? 'm';\n metrics.push({ label: 'Dimensions', value: `${width} × ${height} ${unit}` });\n }\n\n return metrics;\n}\n\nfunction FloorPlanViewer({\n plan,\n zones,\n onAttachImage,\n}: {\n plan: FloorPlan;\n zones: Zone[];\n onAttachImage: (plan: FloorPlan, url: string) => void;\n}) {\n const overlayZones = zones.filter((zone) => zone.rect);\n return (\n <GlassCard treatment=\"plain\" className=\"overflow-hidden border-border-seam p-0\">\n <div className=\"flex items-center justify-between gap-2 border-b border-border-seam px-5 py-3\">\n <div className=\"flex min-w-0 items-center gap-2 text-text-primary\">\n <MapIcon className=\"h-4 w-4 shrink-0 text-action-primary-bg\" />\n <h2 className=\"truncate text-base font-semibold\">{plan.name}</h2>\n </div>\n {zones.length > 0 ? (\n <Badge variant=\"secondary\" className=\"shrink-0\">\n {zones.length} {zones.length === 1 ? 'zone' : 'zones'}\n </Badge>\n ) : null}\n </div>\n\n {plan.imageUrl ? (\n <div className=\"relative bg-bg-sunken\">\n <img\n src={plan.imageUrl}\n alt={`${plan.name} floor plan`}\n className=\"mx-auto max-h-[32rem] w-full object-contain\"\n />\n {overlayZones.map((zone) => (\n <div\n key={zone.key}\n className=\"absolute flex items-center justify-center rounded-md border border-accent-primary/70 bg-action-primary-bg/15 text-[10px] font-medium text-accent-primary\"\n style={{\n left: `${zone.rect?.x}%`,\n top: `${zone.rect?.y}%`,\n width: `${zone.rect?.w}%`,\n height: `${zone.rect?.h}%`,\n }}\n title={zone.label}\n >\n <span className=\"truncate px-1\">{zone.label}</span>\n </div>\n ))}\n </div>\n ) : (\n <div className=\"p-6\">\n <div className=\"rounded-card border border-dashed border-border-default bg-bg-sunken p-6\">\n <p className=\"mb-3 text-center text-sm text-text-secondary\">\n No image attached yet. Upload the hall map for this plan.\n </p>\n <ImageUploadField\n label=\"Upload floor plan image\"\n folderPath={FLOOR_PLAN_FOLDER}\n value={null}\n onUploaded={(url) => onAttachImage(plan, url)}\n hint=\"Stored securely in Files and shown here once attached.\"\n />\n </div>\n </div>\n )}\n\n {zones.length > 0 ? (\n <div className=\"border-t border-border-seam px-5 py-4\">\n <div className=\"mb-2 flex items-center gap-2 text-sm font-medium text-text-primary\">\n <LayoutGrid className=\"h-4 w-4 text-text-muted\" />\n Labeled zones\n </div>\n <div className=\"flex flex-wrap gap-2\">\n {zones.map((zone) => (\n <Badge key={zone.key} variant=\"outline\" className=\"gap-1.5\">\n {zone.label}\n {zone.capacity ? <span className=\"text-text-muted\">· {zone.capacity}</span> : null}\n </Badge>\n ))}\n </div>\n </div>\n ) : null}\n </GlassCard>\n );\n}\n\nexport default function FloorPlansPage() {\n const { id = '' } = useParams<{ id: string }>();\n const { navigate } = useEventfullyManaged();\n const { data, loading, refetch } = useVenueQuery(id);\n const [createFloorPlan, { loading: creating }] = useCreateFloorPlanMutation();\n const [updateFloorPlan] = useUpdateFloorPlanMutation();\n const [deleteFloorPlan] = useDeleteFloorPlanMutation();\n const [name, setName] = useState('');\n const [imageUrl, setImageUrl] = useState('');\n const [selectedId, setSelectedId] = useState<string | null>(null);\n const [showCreate, setShowCreate] = useState(false);\n\n const venue = data?.venue;\n const floorPlans = useMemo(() => (data?.floorPlans?.items ?? []) as FloorPlan[], [data]);\n\n const selectedPlan = useMemo(\n () => floorPlans.find((plan) => plan.id === selectedId) ?? floorPlans[0] ?? null,\n [floorPlans, selectedId]\n );\n const selectedZones = useMemo(() => extractZones(selectedPlan?.interactiveAreas), [selectedPlan]);\n const metrics = useMemo(\n () => (selectedPlan ? buildMetrics(selectedPlan, selectedZones) : []),\n [selectedPlan, selectedZones]\n );\n\n const handleCreate = async () => {\n if (!name.trim() || !id) {\n return;\n }\n try {\n const response = await createFloorPlan({\n variables: {\n input: { venueId: id, name: name.trim(), imageUrl, dimensions: {}, interactiveAreas: {} },\n },\n });\n if (!mutationSucceeded(response, response.data?.createFloorPlan?.id, 'Could not create floor plan')) {\n return;\n }\n setName('');\n setImageUrl('');\n setShowCreate(false);\n const createdId = response.data?.createFloorPlan?.id;\n if (createdId) {\n setSelectedId(createdId);\n }\n notify.success('Floor plan created');\n await refetch();\n } catch (error) {\n notify.error('Could not create floor plan', describeError(error));\n }\n };\n\n const handleAttachImage = async (plan: FloorPlan, url: string) => {\n try {\n const response = await updateFloorPlan({\n variables: {\n id: plan.id,\n input: {\n venueId: plan.venueId,\n name: plan.name,\n imageUrl: url,\n dimensions: plan.dimensions ?? {},\n interactiveAreas: plan.interactiveAreas ?? {},\n },\n },\n });\n if (!mutationSucceeded(response, response.data?.updateFloorPlan?.id, 'Could not attach image')) {\n return;\n }\n await refetch();\n } catch (error) {\n notify.error('Could not attach image', describeError(error));\n }\n };\n\n const handleDelete = async (plan: FloorPlan) => {\n const response = await deleteFloorPlan({ variables: { id: plan.id } });\n if (response.error) {\n notify.error('Could not delete floor plan', response.error.message);\n throw response.error;\n }\n if (!response.data?.deleteFloorPlan) {\n notify.error('Could not delete floor plan', 'The server did not confirm the change.');\n throw new Error('delete-unconfirmed');\n }\n if (selectedId === plan.id) {\n setSelectedId(null);\n }\n notify.success('Floor plan deleted', plan.name);\n await refetch();\n };\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between\">\n <div className=\"space-y-3\">\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => navigate(`/venues/${id}`)}>\n <ArrowLeft className=\"h-4 w-4\" />\n Back to Venue\n </Button>\n <GradientText as=\"h1\" className=\"text-2xl font-bold\">\n {venue?.name ? `${venue.name} — Floor Plans` : 'Floor Plans'}\n </GradientText>\n <p className=\"text-sm text-text-secondary\">\n Map the hall layout, booth positions, stage and entrances so exhibitors and attendees can navigate on the\n day.\n </p>\n </div>\n <Button type=\"button\" onClick={() => setShowCreate((value) => !value)}>\n <Plus className=\"h-4 w-4\" />\n Add floor plan\n </Button>\n </div>\n\n {loading ? (\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n <Skeleton className=\"h-96 rounded-card lg:col-span-2\" />\n <Skeleton className=\"h-96 rounded-card\" />\n </div>\n ) : floorPlans.length === 0 && !showCreate ? (\n <GlassCard treatment=\"plain\" className=\"border-border-seam\">\n <IllustratedEmptyState\n illustration=\"empty-files\"\n title=\"No floor plans yet.\"\n description=\"Add one to start mapping this venue — upload the hall map and label its zones.\"\n action={\n <Button type=\"button\" onClick={() => setShowCreate(true)}>\n <Plus className=\"h-4 w-4\" />\n Add floor plan\n </Button>\n }\n />\n </GlassCard>\n ) : (\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n <div className=\"lg:col-span-2\">\n {selectedPlan ? (\n <FloorPlanViewer plan={selectedPlan} zones={selectedZones} onAttachImage={handleAttachImage} />\n ) : (\n <GlassCard treatment=\"plain\" className=\"flex h-full items-center justify-center border-border-seam p-10\">\n <p className=\"text-sm text-text-secondary\">Add a floor plan to preview it here.</p>\n </GlassCard>\n )}\n </div>\n\n <div className=\"space-y-6\">\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-5\">\n <h2 className=\"text-sm font-semibold text-text-primary\">Available Floor Plans</h2>\n <ul className=\"mt-3 space-y-2\" role=\"list\">\n {floorPlans.length === 0 ? (\n <li className=\"text-sm text-text-secondary\">None yet — create one below.</li>\n ) : (\n floorPlans.map((plan) => {\n const active = selectedPlan?.id === plan.id;\n return (\n <li key={plan.id}>\n <div\n className={`flex items-center gap-3 rounded-card border p-3 transition-colors ${\n active\n ? 'border-accent-primary bg-action-primary-bg/10'\n : 'border-border-seam hover:border-border-strong'\n }`}\n >\n <button\n type=\"button\"\n onClick={() => setSelectedId(plan.id)}\n className=\"flex min-w-0 flex-1 items-center gap-3 text-left focus-visible:outline-none\"\n aria-pressed={active}\n >\n <span\n className={`flex h-9 w-9 shrink-0 items-center justify-center rounded-md ${\n active\n ? 'bg-action-primary-bg text-action-primary-text'\n : 'bg-bg-sunken text-text-secondary'\n }`}\n >\n <SquareStack className=\"h-4 w-4\" />\n </span>\n <span className=\"min-w-0\">\n <span className=\"block truncate text-sm font-medium text-text-primary\">{plan.name}</span>\n <span className=\"block text-xs text-text-muted\">\n {plan.imageUrl ? 'Map attached' : 'No image yet'}\n </span>\n </span>\n </button>\n <ActionMenu\n ariaLabel={`Floor plan actions for ${plan.name}`}\n entityName={plan.name}\n deleteLabel=\"Delete floor plan\"\n onDelete={() => handleDelete(plan)}\n />\n </div>\n </li>\n );\n })\n )}\n </ul>\n </GlassCard>\n\n {selectedPlan ? (\n <GlassCard treatment=\"metric\" className=\"p-5\">\n <h2 className=\"text-sm font-semibold text-text-primary\">Plan Details</h2>\n {metrics.length > 0 ? (\n <dl className=\"mt-3 space-y-2\">\n {metrics.map((metric) => (\n <div key={metric.label} className=\"flex items-center justify-between text-sm\">\n <dt className=\"text-text-secondary\">{metric.label}</dt>\n <dd className=\"font-semibold tabular-nums text-text-primary\">{metric.value}</dd>\n </div>\n ))}\n </dl>\n ) : (\n <p className=\"mt-3 text-sm text-text-secondary\">No zones or dimensions captured for this plan yet.</p>\n )}\n </GlassCard>\n ) : null}\n\n {showCreate || floorPlans.length === 0 ? (\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-5\">\n <h2 className=\"mb-3 text-sm font-semibold text-text-primary\">Add a floor plan</h2>\n <div className=\"space-y-4\">\n <Input\n type=\"text\"\n value={name}\n onChange={(event) => setName(event.target.value)}\n placeholder=\"Main Hall Layout\"\n aria-label=\"Floor plan name\"\n />\n <ImageUploadField\n label=\"Upload floor plan image\"\n folderPath={FLOOR_PLAN_FOLDER}\n value={imageUrl || null}\n onUploaded={setImageUrl}\n hint=\"Optional now — you can attach it later from the viewer.\"\n />\n <div className=\"flex justify-end gap-2\">\n {floorPlans.length > 0 ? (\n <Button type=\"button\" variant=\"ghost\" onClick={() => setShowCreate(false)}>\n Cancel\n </Button>\n ) : null}\n <Button type=\"button\" onClick={handleCreate} disabled={creating || !name.trim()}>\n <Plus className=\"h-4 w-4\" />\n {creating ? 'Creating…' : 'Create'}\n </Button>\n </div>\n </div>\n </GlassCard>\n ) : null}\n </div>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|