@burdenoff/microfe-eventfullymanaged 2026.704.1 → 2026.704.3
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/EventfullyManagedRoutes.js +0 -4
- package/dist/eventfullymanaged/EventfullyManagedRoutes.js.map +1 -1
- package/dist/eventfullymanaged/components/EntityTagsField.js.map +1 -1
- package/dist/eventfullymanaged/pages/events/EventDetailPage.js.map +1 -1
- package/dist/eventfullymanaged/pages/events/EventFormPage.js.map +1 -1
- package/dist/eventfullymanaged/pages/events/SessionFormPage.js.map +1 -1
- package/dist/eventfullymanaged/pages/exhibitors/ExhibitorFormPage.js.map +1 -1
- package/dist/eventfullymanaged/pages/modules/AnalyticsPage.js.map +1 -1
- package/dist/eventfullymanaged/pages/modules/AttendeeProfilePage.js +2 -2
- package/dist/eventfullymanaged/pages/modules/AttendeeProfilePage.js.map +1 -1
- package/dist/eventfullymanaged/pages/modules/KioskPage.js.map +1 -1
- package/dist/eventfullymanaged/pages/modules/NetworkingPage.js +13 -12
- package/dist/eventfullymanaged/pages/modules/NetworkingPage.js.map +1 -1
- package/dist/eventfullymanaged/pages/modules/RegistrationPage.js +18 -11
- package/dist/eventfullymanaged/pages/modules/RegistrationPage.js.map +1 -1
- package/dist/eventfullymanaged/pages/modules/RoadmapPanel.js.map +1 -1
- package/dist/eventfullymanaged/pages/modules/SponsorDetailsPage.js +2 -2
- package/dist/eventfullymanaged/pages/modules/SponsorDetailsPage.js.map +1 -1
- package/dist/eventfullymanaged/pages/networking/MeetingFormPage.js.map +1 -1
- package/dist/eventfullymanaged/pages/networking/NetworkingProfileFormPage.js.map +1 -1
- package/dist/eventfullymanaged/pages/registration/AttendeeFormPage.js.map +1 -1
- package/dist/eventfullymanaged/pages/registration/PromoCodeFormPage.js.map +1 -1
- package/dist/eventfullymanaged/pages/registration/RegisterAttendeePage.js +2 -1
- package/dist/eventfullymanaged/pages/registration/RegisterAttendeePage.js.map +1 -1
- package/dist/eventfullymanaged/pages/speakers/SpeakerFormPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PromoCodeFormPage.js","names":[],"sources":["../../../../src/eventfullymanaged/pages/registration/PromoCodeFormPage.tsx"],"sourcesContent":["/* eslint-disable react-hooks/set-state-in-effect */\nimport { useEffect, useMemo, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport {\n Input,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n Switch,\n Textarea,\n} from '@burdenoff/fe-libs/ui';\n\nimport {\n useCreatePromoCodeMutation,\n useEventsQuery,\n usePromoCodesQuery,\n useUpdatePromoCodeMutation,\n type PromoCodeGql,\n} from '../../api/eventApi';\nimport { EntityFormShell } from '../../components/EntityFormShell';\nimport { useEventfullyManaged } from '../../context/EventfullyManagedContext';\nimport { describeError, mutationSucceeded } from '../../lib/mutationResult';\nimport { notify } from '../../lib/toast';\n\ntype PromoCodeFormState = {\n eventId: string;\n code: string;\n description: string;\n discountType: string;\n discountValue: string;\n maxUses: string;\n startsAt: string;\n endsAt: string;\n isActive: boolean;\n};\n\nconst emptyState: PromoCodeFormState = {\n eventId: '',\n code: '',\n description: '',\n discountType: 'PERCENT',\n discountValue: '',\n maxUses: '',\n startsAt: '',\n endsAt: '',\n isActive: true,\n};\n\n/** ISO timestamp -> value for <input type=\"datetime-local\"> (local time, minute precision). */\nfunction isoToLocalInput(iso?: string | null): string {\n if (!iso) {\n return '';\n }\n const date = new Date(iso);\n if (Number.isNaN(date.getTime())) {\n return '';\n }\n const offsetMs = date.getTimezoneOffset() * 60_000;\n return new Date(date.getTime() - offsetMs).toISOString().slice(0, 16);\n}\n\n/** datetime-local value -> ISO timestamp for the DateTime scalar. */\nfunction localInputToIso(value: string): string | null {\n if (!value) {\n return null;\n }\n const date = new Date(value);\n if (Number.isNaN(date.getTime())) {\n return null;\n }\n return date.toISOString();\n}\n\nfunction toFormState(promo: PromoCodeGql): PromoCodeFormState {\n return {\n eventId: promo.eventId ?? '',\n code: promo.code ?? '',\n description: promo.description ?? '',\n discountType: promo.discountType ?? 'PERCENT',\n discountValue: promo.discountValue ?? '',\n maxUses: promo.maxUses != null ? String(promo.maxUses) : '',\n startsAt: isoToLocalInput(promo.startsAt),\n endsAt: isoToLocalInput(promo.endsAt),\n isActive: promo.isActive ?? true,\n };\n}\n\nexport default function PromoCodeFormPage() {\n const { id = '' } = useParams<{ id: string }>();\n const isEdit = useMemo(() => Boolean(id), [id]);\n const { navigate } = useEventfullyManaged();\n\n const { data: eventsData } = useEventsQuery({ limit: 100, offset: 0 });\n // No by-id promo resolver exists — derive edit initial-state from the list.\n const { data: promoData } = usePromoCodesQuery({ limit: 100, offset: 0 });\n const existing = useMemo(\n () => (promoData?.promoCodes.items ?? []).find((item) => item.id === id),\n [promoData, id]\n );\n\n const [createPromoCode, { loading: creating }] = useCreatePromoCodeMutation();\n const [updatePromoCode, { loading: updating }] = useUpdatePromoCodeMutation();\n const [form, setForm] = useState<PromoCodeFormState>(() => (existing ? toFormState(existing) : emptyState));\n\n useEffect(() => {\n if (existing) {\n setForm(toFormState(existing));\n }\n }, [existing]);\n\n const onChange = <K extends keyof PromoCodeFormState>(field: K, value: PromoCodeFormState[K]) => {\n setForm((prev) => ({ ...prev, [field]: value }));\n };\n\n const events = eventsData?.events.items ?? [];\n const saving = creating || updating;\n\n const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n\n if (!form.eventId) {\n notify.error('Event required', 'Choose the event this promo code applies to.');\n return;\n }\n if (!form.code.trim()) {\n notify.error('Code required', 'Enter the code attendees will type at checkout.');\n return;\n }\n if (!form.discountValue.trim()) {\n notify.error('Discount required', 'Enter a discount value.');\n return;\n }\n\n const input = {\n eventId: form.eventId,\n code: form.code.trim().toUpperCase(),\n description: form.description.trim() || null,\n discountType: form.discountType,\n discountValue: form.discountValue.trim(),\n maxUses: form.maxUses.trim() ? Number(form.maxUses) : null,\n startsAt: localInputToIso(form.startsAt),\n endsAt: localInputToIso(form.endsAt),\n isActive: form.isActive,\n metadata: {},\n };\n\n try {\n if (isEdit) {\n const response = await updatePromoCode({ variables: { id, input } });\n if (!mutationSucceeded(response, response.data?.updatePromoCode?.id, 'Could not update promo code')) {\n return;\n }\n notify.success('Promo code updated', input.code);\n navigate('/registration');\n return;\n }\n\n const response = await createPromoCode({ variables: { input } });\n if (!mutationSucceeded(response, response.data?.createPromoCode?.id, 'Could not create promo code')) {\n return;\n }\n notify.success('Promo code created', input.code);\n navigate('/registration');\n } catch (error) {\n notify.error('Could not save promo code', describeError(error));\n }\n };\n\n const valueHint =\n form.discountType === 'PERCENT' ? 'Percentage off, e.g. 10 for 10%.' : 'Fixed amount off, e.g. 500.';\n\n return (\n <EntityFormShell\n title={isEdit ? 'Edit Promo Code' : 'New Promo Code'}\n description=\"Promo codes let you offer discounts to drive registrations.\"\n purpose=\"Create a discount attendees can apply at checkout. Choose whether it takes a percentage or a fixed amount off, cap how many times it can be used, and set an active window. Deactivate a code to stop it being redeemed without deleting its history.\"\n backLabel=\"Back to Registration\"\n onBack={() => navigate('/registration')}\n onSubmit={onSubmit}\n submitLabel={isEdit ? 'Save Promo Code' : 'Create Promo Code'}\n saving={saving}\n >\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2\">\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Event</span>\n <Select value={form.eventId} onValueChange={(value) => onChange('eventId', value)}>\n <SelectTrigger aria-label=\"Event\">\n <SelectValue placeholder=\"Select Event\" />\n </SelectTrigger>\n <SelectContent>\n {events.map((eventItem) => (\n <SelectItem key={eventItem.id} value={eventItem.id}>\n {eventItem.name}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </label>\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Code</span>\n <Input\n value={form.code}\n onChange={(e) => onChange('code', e.target.value)}\n placeholder=\"EARLYBIRD\"\n required\n />\n </label>\n </div>\n\n <label className=\"block space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Description</span>\n <Textarea value={form.description} onChange={(e) => onChange('description', e.target.value)} rows={2} />\n </label>\n\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-3\">\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Discount Type</span>\n <Select value={form.discountType} onValueChange={(value) => onChange('discountType', value)}>\n <SelectTrigger aria-label=\"Discount Type\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"PERCENT\">Percentage (%)</SelectItem>\n <SelectItem value=\"FIXED\">Fixed amount</SelectItem>\n </SelectContent>\n </Select>\n </label>\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Discount Value</span>\n <Input\n type=\"number\"\n min=\"0\"\n step=\"any\"\n value={form.discountValue}\n onChange={(e) => onChange('discountValue', e.target.value)}\n required\n />\n <span className=\"text-xs text-text-secondary\">{valueHint}</span>\n </label>\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Max Uses</span>\n <Input\n type=\"number\"\n min=\"0\"\n value={form.maxUses}\n onChange={(e) => onChange('maxUses', e.target.value)}\n placeholder=\"Unlimited\"\n />\n </label>\n </div>\n\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2\">\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Starts At</span>\n <Input type=\"datetime-local\" value={form.startsAt} onChange={(e) => onChange('startsAt', e.target.value)} />\n </label>\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Ends At</span>\n <Input type=\"datetime-local\" value={form.endsAt} onChange={(e) => onChange('endsAt', e.target.value)} />\n </label>\n </div>\n\n <div className=\"flex items-center justify-between rounded-input border border-border-default bg-bg-surface px-3 py-2\">\n <div className=\"text-sm\">\n <span className=\"font-medium text-text-primary\">Active</span>\n <p className=\"text-xs text-text-secondary\">Only active codes can be redeemed at checkout.</p>\n </div>\n <Switch checked={form.isActive} onCheckedChange={(checked) => onChange('isActive', checked)} />\n </div>\n </EntityFormShell>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PromoCodeFormPage.js","names":[],"sources":["../../../../src/eventfullymanaged/pages/registration/PromoCodeFormPage.tsx"],"sourcesContent":["/* eslint-disable react-hooks/set-state-in-effect */\nimport { useEffect, useMemo, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport {\n Input,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n Switch,\n Textarea,\n} from '@burdenoff/fe-libs/ui';\n\nimport {\n useCreatePromoCodeMutation,\n useEventsQuery,\n usePromoCodesQuery,\n useUpdatePromoCodeMutation,\n type PromoCodeGql,\n} from '../../api/eventApi';\nimport { EntityFormShell } from '../../components/EntityFormShell';\nimport { useEventfullyManaged } from '../../context/EventfullyManagedContext';\nimport { describeError, mutationSucceeded } from '../../lib/mutationResult';\nimport { notify } from '../../lib/toast';\n\ntype PromoCodeFormState = {\n eventId: string;\n code: string;\n description: string;\n discountType: string;\n discountValue: string;\n maxUses: string;\n startsAt: string;\n endsAt: string;\n isActive: boolean;\n};\n\nconst emptyState: PromoCodeFormState = {\n eventId: '',\n code: '',\n description: '',\n discountType: 'PERCENT',\n discountValue: '',\n maxUses: '',\n startsAt: '',\n endsAt: '',\n isActive: true,\n};\n\n/** ISO timestamp -> value for <input type=\"datetime-local\"> (local time, minute precision). */\nfunction isoToLocalInput(iso?: string | null): string {\n if (!iso) {\n return '';\n }\n const date = new Date(iso);\n if (Number.isNaN(date.getTime())) {\n return '';\n }\n const offsetMs = date.getTimezoneOffset() * 60_000;\n return new Date(date.getTime() - offsetMs).toISOString().slice(0, 16);\n}\n\n/** datetime-local value -> ISO timestamp for the DateTime scalar. */\nfunction localInputToIso(value: string): string | null {\n if (!value) {\n return null;\n }\n const date = new Date(value);\n if (Number.isNaN(date.getTime())) {\n return null;\n }\n return date.toISOString();\n}\n\nfunction toFormState(promo: PromoCodeGql): PromoCodeFormState {\n return {\n eventId: promo.eventId ?? '',\n code: promo.code ?? '',\n description: promo.description ?? '',\n discountType: promo.discountType ?? 'PERCENT',\n discountValue: promo.discountValue ?? '',\n maxUses: promo.maxUses != null ? String(promo.maxUses) : '',\n startsAt: isoToLocalInput(promo.startsAt),\n endsAt: isoToLocalInput(promo.endsAt),\n isActive: promo.isActive ?? true,\n };\n}\n\nexport default function PromoCodeFormPage() {\n const { id = '' } = useParams<{ id: string }>();\n const isEdit = useMemo(() => Boolean(id), [id]);\n const { navigate } = useEventfullyManaged();\n\n const { data: eventsData } = useEventsQuery({ limit: 100, offset: 0 });\n // No by-id promo resolver exists — derive edit initial-state from the list.\n const { data: promoData } = usePromoCodesQuery({ limit: 100, offset: 0 });\n const existing = useMemo(() => (promoData?.promoCodes.items ?? []).find((item) => item.id === id), [promoData, id]);\n\n const [createPromoCode, { loading: creating }] = useCreatePromoCodeMutation();\n const [updatePromoCode, { loading: updating }] = useUpdatePromoCodeMutation();\n const [form, setForm] = useState<PromoCodeFormState>(() => (existing ? toFormState(existing) : emptyState));\n\n useEffect(() => {\n if (existing) {\n setForm(toFormState(existing));\n }\n }, [existing]);\n\n const onChange = <K extends keyof PromoCodeFormState>(field: K, value: PromoCodeFormState[K]) => {\n setForm((prev) => ({ ...prev, [field]: value }));\n };\n\n const events = eventsData?.events.items ?? [];\n const saving = creating || updating;\n\n const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n\n if (!form.eventId) {\n notify.error('Event required', 'Choose the event this promo code applies to.');\n return;\n }\n if (!form.code.trim()) {\n notify.error('Code required', 'Enter the code attendees will type at checkout.');\n return;\n }\n if (!form.discountValue.trim()) {\n notify.error('Discount required', 'Enter a discount value.');\n return;\n }\n\n const input = {\n eventId: form.eventId,\n code: form.code.trim().toUpperCase(),\n description: form.description.trim() || null,\n discountType: form.discountType,\n discountValue: form.discountValue.trim(),\n maxUses: form.maxUses.trim() ? Number(form.maxUses) : null,\n startsAt: localInputToIso(form.startsAt),\n endsAt: localInputToIso(form.endsAt),\n isActive: form.isActive,\n metadata: {},\n };\n\n try {\n if (isEdit) {\n const response = await updatePromoCode({ variables: { id, input } });\n if (!mutationSucceeded(response, response.data?.updatePromoCode?.id, 'Could not update promo code')) {\n return;\n }\n notify.success('Promo code updated', input.code);\n navigate('/registration');\n return;\n }\n\n const response = await createPromoCode({ variables: { input } });\n if (!mutationSucceeded(response, response.data?.createPromoCode?.id, 'Could not create promo code')) {\n return;\n }\n notify.success('Promo code created', input.code);\n navigate('/registration');\n } catch (error) {\n notify.error('Could not save promo code', describeError(error));\n }\n };\n\n const valueHint =\n form.discountType === 'PERCENT' ? 'Percentage off, e.g. 10 for 10%.' : 'Fixed amount off, e.g. 500.';\n\n return (\n <EntityFormShell\n title={isEdit ? 'Edit Promo Code' : 'New Promo Code'}\n description=\"Promo codes let you offer discounts to drive registrations.\"\n purpose=\"Create a discount attendees can apply at checkout. Choose whether it takes a percentage or a fixed amount off, cap how many times it can be used, and set an active window. Deactivate a code to stop it being redeemed without deleting its history.\"\n backLabel=\"Back to Registration\"\n onBack={() => navigate('/registration')}\n onSubmit={onSubmit}\n submitLabel={isEdit ? 'Save Promo Code' : 'Create Promo Code'}\n saving={saving}\n >\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2\">\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Event</span>\n <Select value={form.eventId} onValueChange={(value) => onChange('eventId', value)}>\n <SelectTrigger aria-label=\"Event\">\n <SelectValue placeholder=\"Select Event\" />\n </SelectTrigger>\n <SelectContent>\n {events.map((eventItem) => (\n <SelectItem key={eventItem.id} value={eventItem.id}>\n {eventItem.name}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </label>\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Code</span>\n <Input\n value={form.code}\n onChange={(e) => onChange('code', e.target.value)}\n placeholder=\"EARLYBIRD\"\n required\n />\n </label>\n </div>\n\n <label className=\"block space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Description</span>\n <Textarea value={form.description} onChange={(e) => onChange('description', e.target.value)} rows={2} />\n </label>\n\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-3\">\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Discount Type</span>\n <Select value={form.discountType} onValueChange={(value) => onChange('discountType', value)}>\n <SelectTrigger aria-label=\"Discount Type\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"PERCENT\">Percentage (%)</SelectItem>\n <SelectItem value=\"FIXED\">Fixed amount</SelectItem>\n </SelectContent>\n </Select>\n </label>\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Discount Value</span>\n <Input\n type=\"number\"\n min=\"0\"\n step=\"any\"\n value={form.discountValue}\n onChange={(e) => onChange('discountValue', e.target.value)}\n required\n />\n <span className=\"text-xs text-text-secondary\">{valueHint}</span>\n </label>\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Max Uses</span>\n <Input\n type=\"number\"\n min=\"0\"\n value={form.maxUses}\n onChange={(e) => onChange('maxUses', e.target.value)}\n placeholder=\"Unlimited\"\n />\n </label>\n </div>\n\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2\">\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Starts At</span>\n <Input type=\"datetime-local\" value={form.startsAt} onChange={(e) => onChange('startsAt', e.target.value)} />\n </label>\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Ends At</span>\n <Input type=\"datetime-local\" value={form.endsAt} onChange={(e) => onChange('endsAt', e.target.value)} />\n </label>\n </div>\n\n <div className=\"flex items-center justify-between rounded-input border border-border-default bg-bg-surface px-3 py-2\">\n <div className=\"text-sm\">\n <span className=\"font-medium text-text-primary\">Active</span>\n <p className=\"text-xs text-text-secondary\">Only active codes can be redeemed at checkout.</p>\n </div>\n <Switch checked={form.isActive} onCheckedChange={(checked) => onChange('isActive', checked)} />\n </div>\n </EntityFormShell>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RegisterAttendeePage.js","names":[],"sources":["../../../../src/eventfullymanaged/pages/registration/RegisterAttendeePage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport { ArrowLeft, CheckCircle2, Ticket } from 'lucide-react';\nimport {\n Button,\n EmphasisPanel,\n GlassCard,\n Input,\n PagePurpose,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@burdenoff/fe-libs/ui';\n\nimport {\n useAttendeesQuery,\n useCreateAttendeeMutation,\n useCreateRegistrationMutation,\n useEventsQuery,\n usePromoCodesQuery,\n useTicketTypesQuery,\n} from '../../api/eventApi';\nimport { useEventfullyManaged } from '../../context/EventfullyManagedContext';\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 amountLabel: string;\n};\n\nconst NEW_ATTENDEE = '__new__';\n\nexport default function RegisterAttendeePage() {\n const { navigate } = useEventfullyManaged();\n\n const [eventId, setEventId] = useState('');\n const [ticketTypeId, setTicketTypeId] = useState('');\n const [attendeeId, setAttendeeId] = useState(NEW_ATTENDEE);\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 [promoCode, setPromoCode] = useState('');\n const [billingAccountId, setBillingAccountId] = useState('');\n const [confirmation, setConfirmation] = useState<Confirmation | null>(null);\n\n const { data: eventsData } = useEventsQuery({ limit: 100, offset: 0 });\n const { data: ticketData } = useTicketTypesQuery({ eventId: eventId || undefined, limit: 100, offset: 0 });\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 [createAttendee, { loading: creatingAttendee }] = useCreateAttendeeMutation();\n const [createRegistration, { loading: creatingRegistration }] = useCreateRegistrationMutation();\n\n const events = eventsData?.events.items ?? [];\n const tickets = useMemo(\n () => (ticketData?.ticketTypes.items ?? []).filter((ticket) => !eventId || ticket.eventId === eventId),\n [ticketData, eventId]\n );\n const attendees = useMemo(\n () => (attendeesData?.attendees.items ?? []).filter((attendee) => !eventId || attendee.eventId === eventId),\n [attendeesData, eventId]\n );\n const promoCodes = useMemo(\n () =>\n (promoData?.promoCodes.items ?? []).filter(\n (promo) => promo.isActive && (!eventId || promo.eventId === eventId)\n ),\n [promoData, eventId]\n );\n\n const selectedTicket = useMemo(() => tickets.find((ticket) => ticket.id === ticketTypeId), [tickets, ticketTypeId]);\n const qty = Math.max(1, Number(quantity) || 1);\n const unitPrice = selectedTicket ? Number(selectedTicket.price) : 0;\n const currency = selectedTicket?.currency ?? 'USD';\n const grossAmount = Number.isFinite(unitPrice) ? unitPrice * qty : 0;\n const hasPositiveAmount = grossAmount > 0;\n\n const onEventChange = (value: string) => {\n setEventId(value);\n // Reset dependent selections so the user can't submit a stale ticket/attendee.\n setTicketTypeId('');\n setAttendeeId(NEW_ATTENDEE);\n setPromoCode('');\n };\n\n const saving = creatingAttendee || creatingRegistration;\n\n const resetForm = () => {\n setTicketTypeId('');\n setAttendeeId(NEW_ATTENDEE);\n setFullName('');\n setEmail('');\n setCompany('');\n setPhone('');\n setQuantity('1');\n setPromoCode('');\n setBillingAccountId('');\n };\n\n const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n\n if (!eventId) {\n notify.error('Event required', 'Choose the event to register for.');\n return;\n }\n if (!ticketTypeId) {\n notify.error('Ticket required', 'Choose a ticket type.');\n return;\n }\n\n const isNewAttendee = attendeeId === NEW_ATTENDEE;\n if (isNewAttendee && (!fullName.trim() || !email.trim())) {\n notify.error('Attendee required', 'Enter a name and email, or pick an existing attendee.');\n return;\n }\n\n try {\n let resolvedAttendeeId = attendeeId;\n let resolvedAttendeeName = attendees.find((attendee) => attendee.id === attendeeId)?.fullName ?? '';\n\n if (isNewAttendee) {\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 // only forward a billing account reference (when supplied) via metadata so\n // the backend can attach the charge; totalAmount is left for the backend.\n const metadata: Record<string, unknown> = {};\n if (promoCode) {\n metadata.promoCode = promoCode;\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: events.find((item) => item.id === eventId)?.name ?? 'Event',\n ticketName: selectedTicket?.name ?? 'Ticket',\n quantity: qty,\n status: registration?.status ?? 'CONFIRMED',\n amountLabel: hasPositiveAmount\n ? `${grossAmount.toLocaleString()} ${currency} — billed by the billing service`\n : 'Free',\n });\n notify.success('Registration confirmed', resolvedAttendeeName || undefined);\n resetForm();\n } catch (error) {\n notify.error('Could not register attendee', describeError(error));\n }\n };\n\n if (confirmation) {\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 </div>\n <GlassCard treatment=\"glass\" glow className=\"p-8 text-center\">\n <CheckCircle2 className=\"mx-auto h-12 w-12 text-status-success-text\" aria-hidden=\"true\" />\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 <dl className=\"mx-auto mt-6 max-w-sm space-y-2 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\">{confirmation.ticketName}</dd>\n </div>\n <div className=\"flex justify-between\">\n <dt className=\"text-text-secondary\">Quantity</dt>\n <dd className=\"font-medium tabular-nums text-text-primary\">{confirmation.quantity}</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 <div className=\"flex justify-between\">\n <dt className=\"text-text-secondary\">Amount</dt>\n <dd className=\"font-medium tabular-nums text-text-primary\">{confirmation.amountLabel}</dd>\n </div>\n </dl>\n <div className=\"mt-8 flex justify-center gap-3\">\n <Button type=\"button\" variant=\"outline\" onClick={() => navigate('/registration')}>\n Back to Registrations\n </Button>\n <Button type=\"button\" onClick={() => setConfirmation(null)}>\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\">Register Attendee</h1>\n <p className=\"mt-1 text-sm text-text-secondary\">Sign someone up for an event against a ticket type.</p>\n </div>\n\n <PagePurpose>\n Register a person for an event in one step. Pick the event and the ticket they are buying, then either choose an\n existing attendee or enter a new one. Apply a promo code if they have one. Pricing and payment are handled by the\n billing service — you just confirm who is coming and on which ticket.\n </PagePurpose>\n\n <EmphasisPanel>\n <form onSubmit={onSubmit} className=\"space-y-4\">\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2\">\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Event</span>\n <Select value={eventId} onValueChange={onEventChange}>\n <SelectTrigger aria-label=\"Event\">\n <SelectValue placeholder=\"Select Event\" />\n </SelectTrigger>\n <SelectContent>\n {events.map((eventItem) => (\n <SelectItem key={eventItem.id} value={eventItem.id}>\n {eventItem.name}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </label>\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Ticket Type</span>\n <Select value={ticketTypeId} onValueChange={setTicketTypeId} disabled={!eventId}>\n <SelectTrigger aria-label=\"Ticket Type\">\n <SelectValue placeholder={eventId ? 'Select Ticket' : 'Select an event first'} />\n </SelectTrigger>\n <SelectContent>\n {tickets.map((ticket) => (\n <SelectItem key={ticket.id} value={ticket.id}>\n {ticket.name} — {ticket.price} {ticket.currency}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </label>\n </div>\n\n <label className=\"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 />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value={NEW_ATTENDEE}>+ New attendee</SelectItem>\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 {attendeeId === NEW_ATTENDEE ? (\n <div className=\"grid grid-cols-1 gap-4 rounded-input border border-border-seam bg-bg-surface p-4 md: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)} required />\n </label>\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Email</span>\n <Input type=\"email\" value={email} onChange={(e) => setEmail(e.target.value)} required />\n </label>\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Company</span>\n <Input value={company} onChange={(e) => setCompany(e.target.value)} />\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)} />\n </label>\n </div>\n ) : null}\n\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2\">\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Quantity</span>\n <Input\n type=\"number\"\n min=\"1\"\n value={quantity}\n onChange={(e) => setQuantity(e.target.value)}\n required\n />\n </label>\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Promo Code (optional)</span>\n <Select value={promoCode} onValueChange={setPromoCode} disabled={promoCodes.length === 0}>\n <SelectTrigger aria-label=\"Promo Code\">\n <SelectValue placeholder={promoCodes.length === 0 ? 'No active codes' : 'No promo code'} />\n </SelectTrigger>\n <SelectContent>\n {promoCodes.map((promo) => (\n <SelectItem key={promo.id} value={promo.code}>\n {promo.code} — {promo.discountType === 'PERCENT' ? `${promo.discountValue}%` : promo.discountValue}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </label>\n </div>\n\n {hasPositiveAmount ? (\n <div className=\"space-y-3 rounded-input border border-border-seam bg-bg-surface p-4\">\n <div className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <Ticket className=\"h-4 w-4 text-text-muted\" aria-hidden=\"true\" />\n <span>\n Estimated total{' '}\n <span className=\"font-semibold tabular-nums text-text-primary\">\n {grossAmount.toLocaleString()} {currency}\n </span>{' '}\n — final pricing and payment are handled by the billing service.\n </span>\n </div>\n <label className=\"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 </div>\n ) : null}\n\n <div className=\"flex justify-end gap-3\">\n <Button type=\"button\" variant=\"outline\" onClick={() => navigate('/registration')} disabled={saving}>\n Cancel\n </Button>\n <Button type=\"submit\" disabled={saving}>\n <Ticket className=\"h-4 w-4\" />\n {saving ? 'Registering…' : 'Register Attendee'}\n </Button>\n </div>\n </form>\n </EmphasisPanel>\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 { ArrowLeft, CheckCircle2, Ticket } from 'lucide-react';\nimport {\n Button,\n EmphasisPanel,\n GlassCard,\n Input,\n PagePurpose,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@burdenoff/fe-libs/ui';\n\nimport {\n useAttendeesQuery,\n useCreateAttendeeMutation,\n useCreateRegistrationMutation,\n useEventsQuery,\n usePromoCodesQuery,\n useTicketTypesQuery,\n} from '../../api/eventApi';\nimport { useEventfullyManaged } from '../../context/EventfullyManagedContext';\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 amountLabel: string;\n};\n\nconst NEW_ATTENDEE = '__new__';\n\nexport default function RegisterAttendeePage() {\n const { navigate } = useEventfullyManaged();\n\n const [eventId, setEventId] = useState('');\n const [ticketTypeId, setTicketTypeId] = useState('');\n const [attendeeId, setAttendeeId] = useState(NEW_ATTENDEE);\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 [promoCode, setPromoCode] = useState('');\n const [billingAccountId, setBillingAccountId] = useState('');\n const [confirmation, setConfirmation] = useState<Confirmation | null>(null);\n\n const { data: eventsData } = useEventsQuery({ limit: 100, offset: 0 });\n const { data: ticketData } = useTicketTypesQuery({ eventId: eventId || undefined, limit: 100, offset: 0 });\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 [createAttendee, { loading: creatingAttendee }] = useCreateAttendeeMutation();\n const [createRegistration, { loading: creatingRegistration }] = useCreateRegistrationMutation();\n\n const events = eventsData?.events.items ?? [];\n const tickets = useMemo(\n () => (ticketData?.ticketTypes.items ?? []).filter((ticket) => !eventId || ticket.eventId === eventId),\n [ticketData, eventId]\n );\n const attendees = useMemo(\n () => (attendeesData?.attendees.items ?? []).filter((attendee) => !eventId || attendee.eventId === eventId),\n [attendeesData, eventId]\n );\n const promoCodes = useMemo(\n () =>\n (promoData?.promoCodes.items ?? []).filter((promo) => promo.isActive && (!eventId || promo.eventId === eventId)),\n [promoData, eventId]\n );\n\n const selectedTicket = useMemo(() => tickets.find((ticket) => ticket.id === ticketTypeId), [tickets, ticketTypeId]);\n const qty = Math.max(1, Number(quantity) || 1);\n const unitPrice = selectedTicket ? Number(selectedTicket.price) : 0;\n const currency = selectedTicket?.currency ?? 'USD';\n const grossAmount = Number.isFinite(unitPrice) ? unitPrice * qty : 0;\n const hasPositiveAmount = grossAmount > 0;\n\n const onEventChange = (value: string) => {\n setEventId(value);\n // Reset dependent selections so the user can't submit a stale ticket/attendee.\n setTicketTypeId('');\n setAttendeeId(NEW_ATTENDEE);\n setPromoCode('');\n };\n\n const saving = creatingAttendee || creatingRegistration;\n\n const resetForm = () => {\n setTicketTypeId('');\n setAttendeeId(NEW_ATTENDEE);\n setFullName('');\n setEmail('');\n setCompany('');\n setPhone('');\n setQuantity('1');\n setPromoCode('');\n setBillingAccountId('');\n };\n\n const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n\n if (!eventId) {\n notify.error('Event required', 'Choose the event to register for.');\n return;\n }\n if (!ticketTypeId) {\n notify.error('Ticket required', 'Choose a ticket type.');\n return;\n }\n\n const isNewAttendee = attendeeId === NEW_ATTENDEE;\n if (isNewAttendee && (!fullName.trim() || !email.trim())) {\n notify.error('Attendee required', 'Enter a name and email, or pick an existing attendee.');\n return;\n }\n\n try {\n let resolvedAttendeeId = attendeeId;\n let resolvedAttendeeName = attendees.find((attendee) => attendee.id === attendeeId)?.fullName ?? '';\n\n if (isNewAttendee) {\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 // only forward a billing account reference (when supplied) via metadata so\n // the backend can attach the charge; totalAmount is left for the backend.\n const metadata: Record<string, unknown> = {};\n if (promoCode) {\n metadata.promoCode = promoCode;\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: events.find((item) => item.id === eventId)?.name ?? 'Event',\n ticketName: selectedTicket?.name ?? 'Ticket',\n quantity: qty,\n status: registration?.status ?? 'CONFIRMED',\n amountLabel: hasPositiveAmount\n ? `${grossAmount.toLocaleString()} ${currency} — billed by the billing service`\n : 'Free',\n });\n notify.success('Registration confirmed', resolvedAttendeeName || undefined);\n resetForm();\n } catch (error) {\n notify.error('Could not register attendee', describeError(error));\n }\n };\n\n if (confirmation) {\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 </div>\n <GlassCard treatment=\"glass\" glow className=\"p-8 text-center\">\n <CheckCircle2 className=\"mx-auto h-12 w-12 text-status-success-text\" aria-hidden=\"true\" />\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 <dl className=\"mx-auto mt-6 max-w-sm space-y-2 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\">{confirmation.ticketName}</dd>\n </div>\n <div className=\"flex justify-between\">\n <dt className=\"text-text-secondary\">Quantity</dt>\n <dd className=\"font-medium tabular-nums text-text-primary\">{confirmation.quantity}</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 <div className=\"flex justify-between\">\n <dt className=\"text-text-secondary\">Amount</dt>\n <dd className=\"font-medium tabular-nums text-text-primary\">{confirmation.amountLabel}</dd>\n </div>\n </dl>\n <div className=\"mt-8 flex justify-center gap-3\">\n <Button type=\"button\" variant=\"outline\" onClick={() => navigate('/registration')}>\n Back to Registrations\n </Button>\n <Button type=\"button\" onClick={() => setConfirmation(null)}>\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\">Register Attendee</h1>\n <p className=\"mt-1 text-sm text-text-secondary\">Sign someone up for an event against a ticket type.</p>\n </div>\n\n <PagePurpose>\n Register a person for an event in one step. Pick the event and the ticket they are buying, then either choose an\n existing attendee or enter a new one. Apply a promo code if they have one. Pricing and payment are handled by\n the billing service — you just confirm who is coming and on which ticket.\n </PagePurpose>\n\n <EmphasisPanel>\n <form onSubmit={onSubmit} className=\"space-y-4\">\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2\">\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Event</span>\n <Select value={eventId} onValueChange={onEventChange}>\n <SelectTrigger aria-label=\"Event\">\n <SelectValue placeholder=\"Select Event\" />\n </SelectTrigger>\n <SelectContent>\n {events.map((eventItem) => (\n <SelectItem key={eventItem.id} value={eventItem.id}>\n {eventItem.name}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </label>\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Ticket Type</span>\n <Select value={ticketTypeId} onValueChange={setTicketTypeId} disabled={!eventId}>\n <SelectTrigger aria-label=\"Ticket Type\">\n <SelectValue placeholder={eventId ? 'Select Ticket' : 'Select an event first'} />\n </SelectTrigger>\n <SelectContent>\n {tickets.map((ticket) => (\n <SelectItem key={ticket.id} value={ticket.id}>\n {ticket.name} — {ticket.price} {ticket.currency}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </label>\n </div>\n\n <label className=\"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 />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value={NEW_ATTENDEE}>+ New attendee</SelectItem>\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 {attendeeId === NEW_ATTENDEE ? (\n <div className=\"grid grid-cols-1 gap-4 rounded-input border border-border-seam bg-bg-surface p-4 md: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)} required />\n </label>\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Email</span>\n <Input type=\"email\" value={email} onChange={(e) => setEmail(e.target.value)} required />\n </label>\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Company</span>\n <Input value={company} onChange={(e) => setCompany(e.target.value)} />\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)} />\n </label>\n </div>\n ) : null}\n\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2\">\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Quantity</span>\n <Input type=\"number\" min=\"1\" value={quantity} onChange={(e) => setQuantity(e.target.value)} required />\n </label>\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Promo Code (optional)</span>\n <Select value={promoCode} onValueChange={setPromoCode} disabled={promoCodes.length === 0}>\n <SelectTrigger aria-label=\"Promo Code\">\n <SelectValue placeholder={promoCodes.length === 0 ? 'No active codes' : 'No promo code'} />\n </SelectTrigger>\n <SelectContent>\n {promoCodes.map((promo) => (\n <SelectItem key={promo.id} value={promo.code}>\n {promo.code} —{' '}\n {promo.discountType === 'PERCENT' ? `${promo.discountValue}%` : promo.discountValue}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </label>\n </div>\n\n {hasPositiveAmount ? (\n <div className=\"space-y-3 rounded-input border border-border-seam bg-bg-surface p-4\">\n <div className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <Ticket className=\"h-4 w-4 text-text-muted\" aria-hidden=\"true\" />\n <span>\n Estimated total{' '}\n <span className=\"font-semibold tabular-nums text-text-primary\">\n {grossAmount.toLocaleString()} {currency}\n </span>{' '}\n — final pricing and payment are handled by the billing service.\n </span>\n </div>\n <label className=\"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 </div>\n ) : null}\n\n <div className=\"flex justify-end gap-3\">\n <Button type=\"button\" variant=\"outline\" onClick={() => navigate('/registration')} disabled={saving}>\n Cancel\n </Button>\n <Button type=\"submit\" disabled={saving}>\n <Ticket className=\"h-4 w-4\" />\n {saving ? 'Registering…' : 'Register Attendee'}\n </Button>\n </div>\n </form>\n </EmphasisPanel>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SpeakerFormPage.js","names":[],"sources":["../../../../src/eventfullymanaged/pages/speakers/SpeakerFormPage.tsx"],"sourcesContent":["/* eslint-disable react-hooks/set-state-in-effect */\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Input, Textarea } from '@burdenoff/fe-libs/ui';\nimport { useEffect, useMemo, useState } from 'react';\nimport { useParams } from 'react-router-dom';\n\nimport { useCreateSpeakerMutation, useUpdateSpeakerMutation } from '../../api/eventApi';\nimport { EntityFormShell } from '../../components/EntityFormShell';\nimport { EntityTagsField } from '../../components/EntityTagsField';\nimport { ImageUploadField } from '../../components/ImageUploadField';\nimport { safeUrl } from '../../components/SafeLink';\nimport { useEventfullyManaged } from '../../context/EventfullyManagedContext';\nimport { describeError, mutationSucceeded } from '../../lib/mutationResult';\nimport { notify } from '../../lib/toast';\n\n// The backend `updateSpeaker` resolver is a FULL replace and there is no by-id\n// speaker resolver, so this fuller list query — selecting every editable field,\n// not just the columns the foundation SPEAKERS_QUERY lists — is the edit-hydration\n// source. The form finds its row by id client-side. Without the full selection an\n// edit would blank bio / avatar / phone / socialLinks.\nconst FULL_SPEAKERS_QUERY = gql`\n query EventfullymanagedSpeakerFormSpeakers($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 email\n phone\n socialLinks\n }\n }\n }\n`;\n\ntype SpeakerRecord = {\n id: string;\n fullName: string;\n title?: string | null;\n organization?: string | null;\n bio?: string | null;\n avatarUrl?: string | null;\n email?: string | null;\n phone?: string | null;\n socialLinks?: Record<string, unknown> | null;\n};\n\ntype FullSpeakersResponse = { speakers: { total: number; items: SpeakerRecord[] } };\n\ntype SpeakerFormState = {\n fullName: string;\n title: string;\n organization: string;\n email: string;\n phone: string;\n bio: string;\n avatarUrl: string;\n website: string;\n};\n\nconst initialState: SpeakerFormState = {\n fullName: '',\n title: '',\n organization: '',\n email: '',\n phone: '',\n bio: '',\n avatarUrl: '',\n website: '',\n};\n\nfunction readWebsite(socialLinks: Record<string, unknown> | null | undefined): string {\n if (socialLinks && typeof socialLinks.website === 'string') {\n return socialLinks.website;\n }\n return '';\n}\n\nexport default function SpeakerFormPage() {\n const { id = '' } = useParams<{ id: string }>();\n const isEdit = useMemo(() => Boolean(id), [id]);\n const { navigate } = useEventfullyManaged();\n\n const { data } = useQuery<FullSpeakersResponse, { limit?: number; offset?: number }>(FULL_SPEAKERS_QUERY, {\n variables: { limit: 100, offset: 0 },\n skip: !id,\n });\n const [createSpeaker, { loading: creating }] = useCreateSpeakerMutation();\n const [updateSpeaker, { loading: updating }] = useUpdateSpeakerMutation();\n const [form, setForm] = useState<SpeakerFormState>(initialState);\n\n const existingSpeaker = useMemo(\n () => (data?.speakers?.items ?? []).find((item) => item.id === id),\n [data, id]\n );\n\n useEffect(() => {\n if (!isEdit || !existingSpeaker) {\n return;\n }\n setForm({\n fullName: existingSpeaker.fullName ?? '',\n title: existingSpeaker.title ?? '',\n organization: existingSpeaker.organization ?? '',\n email: existingSpeaker.email ?? '',\n phone: existingSpeaker.phone ?? '',\n bio: existingSpeaker.bio ?? '',\n avatarUrl: existingSpeaker.avatarUrl ?? '',\n website: readWebsite(existingSpeaker.socialLinks),\n });\n }, [existingSpeaker, isEdit]);\n\n const onChange = (field: keyof SpeakerFormState, value: string) => {\n setForm((prev) => ({ ...prev, [field]: value }));\n };\n\n const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n\n if (!form.fullName.trim()) {\n notify.error('Name required', 'Give the speaker a name before saving.');\n return;\n }\n\n const normalizedWebsite = safeUrl(form.website);\n if (form.website.trim() && !normalizedWebsite) {\n notify.error('Invalid website', 'Enter a valid http(s) URL, or leave it blank.');\n return;\n }\n\n const input = {\n fullName: form.fullName.trim(),\n title: form.title || null,\n organization: form.organization || null,\n email: form.email || null,\n phone: form.phone || null,\n bio: form.bio || null,\n avatarUrl: form.avatarUrl || null,\n socialLinks: normalizedWebsite ? { website: normalizedWebsite } : {},\n };\n\n try {\n if (isEdit) {\n const response = await updateSpeaker({ variables: { id, input } });\n if (!mutationSucceeded(response, response.data?.updateSpeaker?.id, 'Could not update speaker')) {\n return;\n }\n notify.success('Speaker updated');\n navigate('/speakers');\n return;\n }\n const response = await createSpeaker({ variables: { input } });\n if (!mutationSucceeded(response, response.data?.createSpeaker?.id, 'Could not create speaker')) {\n return;\n }\n notify.success('Speaker created');\n navigate('/speakers');\n } catch (error) {\n notify.error('Could not save speaker', describeError(error));\n }\n };\n\n const saving = creating || updating;\n\n return (\n <EntityFormShell\n title={isEdit ? 'Edit Speaker' : 'Add Speaker'}\n description=\"A person who presents sessions at your events.\"\n purpose=\"A speaker profile is what you attach to sessions and show to attendees. Capture who they are, where they work, and a short bio or talk abstract so the agenda tells a compelling story. The headshot appears anywhere the speaker is listed.\"\n backLabel=\"Back to Speakers\"\n onBack={() => navigate('/speakers')}\n onSubmit={onSubmit}\n submitLabel=\"Save Speaker\"\n saving={saving}\n >\n <div className=\"grid grid-cols-1 gap-4 md: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={form.fullName} onChange={(e) => onChange('fullName', e.target.value)} required />\n </label>\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Title</span>\n <Input\n value={form.title}\n onChange={(e) => onChange('title', e.target.value)}\n placeholder=\"Head of Product\"\n />\n </label>\n </div>\n\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-3\">\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Company</span>\n <Input value={form.organization} onChange={(e) => onChange('organization', e.target.value)} />\n </label>\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Email</span>\n <Input type=\"email\" value={form.email} onChange={(e) => onChange('email', e.target.value)} />\n </label>\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Phone</span>\n <Input value={form.phone} onChange={(e) => onChange('phone', e.target.value)} />\n </label>\n </div>\n\n <div className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Headshot</span>\n <ImageUploadField\n label=\"Upload headshot\"\n folderPath=\"/EventfullyManaged/Speakers\"\n value={form.avatarUrl || null}\n onUploaded={(url) => onChange('avatarUrl', url)}\n hint=\"Stored securely in Files. Shown wherever the speaker is listed.\"\n />\n </div>\n\n <label className=\"block space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Bio & talk abstract</span>\n <Textarea\n value={form.bio}\n onChange={(e) => onChange('bio', e.target.value)}\n rows={5}\n placeholder=\"Short professional bio and a summary of the talk they will give.\"\n />\n </label>\n\n <label className=\"block space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Website / profile URL</span>\n <Input\n value={form.website}\n onChange={(e) => onChange('website', e.target.value)}\n placeholder=\"https://…\"\n />\n </label>\n\n {isEdit ? <EntityTagsField entityType=\"speaker\" entityId={id} /> : null}\n </EntityFormShell>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SpeakerFormPage.js","names":[],"sources":["../../../../src/eventfullymanaged/pages/speakers/SpeakerFormPage.tsx"],"sourcesContent":["/* eslint-disable react-hooks/set-state-in-effect */\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Input, Textarea } from '@burdenoff/fe-libs/ui';\nimport { useEffect, useMemo, useState } from 'react';\nimport { useParams } from 'react-router-dom';\n\nimport { useCreateSpeakerMutation, useUpdateSpeakerMutation } from '../../api/eventApi';\nimport { EntityFormShell } from '../../components/EntityFormShell';\nimport { EntityTagsField } from '../../components/EntityTagsField';\nimport { ImageUploadField } from '../../components/ImageUploadField';\nimport { safeUrl } from '../../components/SafeLink';\nimport { useEventfullyManaged } from '../../context/EventfullyManagedContext';\nimport { describeError, mutationSucceeded } from '../../lib/mutationResult';\nimport { notify } from '../../lib/toast';\n\n// The backend `updateSpeaker` resolver is a FULL replace and there is no by-id\n// speaker resolver, so this fuller list query — selecting every editable field,\n// not just the columns the foundation SPEAKERS_QUERY lists — is the edit-hydration\n// source. The form finds its row by id client-side. Without the full selection an\n// edit would blank bio / avatar / phone / socialLinks.\nconst FULL_SPEAKERS_QUERY = gql`\n query EventfullymanagedSpeakerFormSpeakers($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 email\n phone\n socialLinks\n }\n }\n }\n`;\n\ntype SpeakerRecord = {\n id: string;\n fullName: string;\n title?: string | null;\n organization?: string | null;\n bio?: string | null;\n avatarUrl?: string | null;\n email?: string | null;\n phone?: string | null;\n socialLinks?: Record<string, unknown> | null;\n};\n\ntype FullSpeakersResponse = { speakers: { total: number; items: SpeakerRecord[] } };\n\ntype SpeakerFormState = {\n fullName: string;\n title: string;\n organization: string;\n email: string;\n phone: string;\n bio: string;\n avatarUrl: string;\n website: string;\n};\n\nconst initialState: SpeakerFormState = {\n fullName: '',\n title: '',\n organization: '',\n email: '',\n phone: '',\n bio: '',\n avatarUrl: '',\n website: '',\n};\n\nfunction readWebsite(socialLinks: Record<string, unknown> | null | undefined): string {\n if (socialLinks && typeof socialLinks.website === 'string') {\n return socialLinks.website;\n }\n return '';\n}\n\nexport default function SpeakerFormPage() {\n const { id = '' } = useParams<{ id: string }>();\n const isEdit = useMemo(() => Boolean(id), [id]);\n const { navigate } = useEventfullyManaged();\n\n const { data } = useQuery<FullSpeakersResponse, { limit?: number; offset?: number }>(FULL_SPEAKERS_QUERY, {\n variables: { limit: 100, offset: 0 },\n skip: !id,\n });\n const [createSpeaker, { loading: creating }] = useCreateSpeakerMutation();\n const [updateSpeaker, { loading: updating }] = useUpdateSpeakerMutation();\n const [form, setForm] = useState<SpeakerFormState>(initialState);\n\n const existingSpeaker = useMemo(() => (data?.speakers?.items ?? []).find((item) => item.id === id), [data, id]);\n\n useEffect(() => {\n if (!isEdit || !existingSpeaker) {\n return;\n }\n setForm({\n fullName: existingSpeaker.fullName ?? '',\n title: existingSpeaker.title ?? '',\n organization: existingSpeaker.organization ?? '',\n email: existingSpeaker.email ?? '',\n phone: existingSpeaker.phone ?? '',\n bio: existingSpeaker.bio ?? '',\n avatarUrl: existingSpeaker.avatarUrl ?? '',\n website: readWebsite(existingSpeaker.socialLinks),\n });\n }, [existingSpeaker, isEdit]);\n\n const onChange = (field: keyof SpeakerFormState, value: string) => {\n setForm((prev) => ({ ...prev, [field]: value }));\n };\n\n const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n\n if (!form.fullName.trim()) {\n notify.error('Name required', 'Give the speaker a name before saving.');\n return;\n }\n\n const normalizedWebsite = safeUrl(form.website);\n if (form.website.trim() && !normalizedWebsite) {\n notify.error('Invalid website', 'Enter a valid http(s) URL, or leave it blank.');\n return;\n }\n\n const input = {\n fullName: form.fullName.trim(),\n title: form.title || null,\n organization: form.organization || null,\n email: form.email || null,\n phone: form.phone || null,\n bio: form.bio || null,\n avatarUrl: form.avatarUrl || null,\n socialLinks: normalizedWebsite ? { website: normalizedWebsite } : {},\n };\n\n try {\n if (isEdit) {\n const response = await updateSpeaker({ variables: { id, input } });\n if (!mutationSucceeded(response, response.data?.updateSpeaker?.id, 'Could not update speaker')) {\n return;\n }\n notify.success('Speaker updated');\n navigate('/speakers');\n return;\n }\n const response = await createSpeaker({ variables: { input } });\n if (!mutationSucceeded(response, response.data?.createSpeaker?.id, 'Could not create speaker')) {\n return;\n }\n notify.success('Speaker created');\n navigate('/speakers');\n } catch (error) {\n notify.error('Could not save speaker', describeError(error));\n }\n };\n\n const saving = creating || updating;\n\n return (\n <EntityFormShell\n title={isEdit ? 'Edit Speaker' : 'Add Speaker'}\n description=\"A person who presents sessions at your events.\"\n purpose=\"A speaker profile is what you attach to sessions and show to attendees. Capture who they are, where they work, and a short bio or talk abstract so the agenda tells a compelling story. The headshot appears anywhere the speaker is listed.\"\n backLabel=\"Back to Speakers\"\n onBack={() => navigate('/speakers')}\n onSubmit={onSubmit}\n submitLabel=\"Save Speaker\"\n saving={saving}\n >\n <div className=\"grid grid-cols-1 gap-4 md: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={form.fullName} onChange={(e) => onChange('fullName', e.target.value)} required />\n </label>\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Title</span>\n <Input value={form.title} onChange={(e) => onChange('title', e.target.value)} placeholder=\"Head of Product\" />\n </label>\n </div>\n\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-3\">\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Company</span>\n <Input value={form.organization} onChange={(e) => onChange('organization', e.target.value)} />\n </label>\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Email</span>\n <Input type=\"email\" value={form.email} onChange={(e) => onChange('email', e.target.value)} />\n </label>\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Phone</span>\n <Input value={form.phone} onChange={(e) => onChange('phone', e.target.value)} />\n </label>\n </div>\n\n <div className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Headshot</span>\n <ImageUploadField\n label=\"Upload headshot\"\n folderPath=\"/EventfullyManaged/Speakers\"\n value={form.avatarUrl || null}\n onUploaded={(url) => onChange('avatarUrl', url)}\n hint=\"Stored securely in Files. Shown wherever the speaker is listed.\"\n />\n </div>\n\n <label className=\"block space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Bio & talk abstract</span>\n <Textarea\n value={form.bio}\n onChange={(e) => onChange('bio', e.target.value)}\n rows={5}\n placeholder=\"Short professional bio and a summary of the talk they will give.\"\n />\n </label>\n\n <label className=\"block space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Website / profile URL</span>\n <Input value={form.website} onChange={(e) => onChange('website', e.target.value)} placeholder=\"https://…\" />\n </label>\n\n {isEdit ? <EntityTagsField entityType=\"speaker\" entityId={id} /> : null}\n </EntityFormShell>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;AAqBA,IAAM,IAAsB,CAAG;;;;;;;;;;;;;;;;;GA4CzB,IAAiC;CACrC,UAAU;CACV,OAAO;CACP,cAAc;CACd,OAAO;CACP,OAAO;CACP,KAAK;CACL,WAAW;CACX,SAAS;CACV;AAED,SAAS,EAAY,GAAiE;AAIpF,QAHI,KAAe,OAAO,EAAY,WAAY,WACzC,EAAY,UAEd;;AAGT,SAAwB,IAAkB;CACxC,IAAM,EAAE,QAAK,OAAO,GAA2B,EACzC,IAAS,QAAc,EAAQ,GAAK,CAAC,EAAG,CAAC,EACzC,EAAE,gBAAa,GAAsB,EAErC,EAAE,YAAS,EAAoE,GAAqB;EACxG,WAAW;GAAE,OAAO;GAAK,QAAQ;GAAG;EACpC,MAAM,CAAC;EACR,CAAC,EACI,CAAC,GAAe,EAAE,SAAS,OAAc,GAA0B,EACnE,CAAC,GAAe,EAAE,SAAS,OAAc,GAA0B,EACnE,CAAC,GAAM,KAAW,EAA2B,EAAa,EAE1D,IAAkB,SAAe,GAAM,UAAU,SAAS,EAAE,EAAE,MAAM,MAAS,EAAK,OAAO,EAAG,EAAE,CAAC,GAAM,EAAG,CAAC;AAE/G,SAAgB;AACV,GAAC,KAAU,CAAC,KAGhB,EAAQ;GACN,UAAU,EAAgB,YAAY;GACtC,OAAO,EAAgB,SAAS;GAChC,cAAc,EAAgB,gBAAgB;GAC9C,OAAO,EAAgB,SAAS;GAChC,OAAO,EAAgB,SAAS;GAChC,KAAK,EAAgB,OAAO;GAC5B,WAAW,EAAgB,aAAa;GACxC,SAAS,EAAY,EAAgB,YAAY;GAClD,CAAC;IACD,CAAC,GAAiB,EAAO,CAAC;CAE7B,IAAM,KAAY,GAA+B,MAAkB;AACjE,KAAS,OAAU;GAAE,GAAG;IAAO,IAAQ;GAAO,EAAE;;AAmDlD,QACE,kBAAC,GAAD;EACE,OAAO,IAAS,iBAAiB;EACjC,aAAY;EACZ,SAAQ;EACR,WAAU;EACV,cAAc,EAAS,YAAY;EACzB,UAvDG,OAAO,MAA4C;AAGlE,OAFA,EAAM,gBAAgB,EAElB,CAAC,EAAK,SAAS,MAAM,EAAE;AACzB,MAAO,MAAM,iBAAiB,yCAAyC;AACvE;;GAGF,IAAM,IAAoB,EAAQ,EAAK,QAAQ;AAC/C,OAAI,EAAK,QAAQ,MAAM,IAAI,CAAC,GAAmB;AAC7C,MAAO,MAAM,mBAAmB,gDAAgD;AAChF;;GAGF,IAAM,IAAQ;IACZ,UAAU,EAAK,SAAS,MAAM;IAC9B,OAAO,EAAK,SAAS;IACrB,cAAc,EAAK,gBAAgB;IACnC,OAAO,EAAK,SAAS;IACrB,OAAO,EAAK,SAAS;IACrB,KAAK,EAAK,OAAO;IACjB,WAAW,EAAK,aAAa;IAC7B,aAAa,IAAoB,EAAE,SAAS,GAAmB,GAAG,EAAE;IACrE;AAED,OAAI;AACF,QAAI,GAAQ;KACV,IAAM,IAAW,MAAM,EAAc,EAAE,WAAW;MAAE;MAAI;MAAO,EAAE,CAAC;AAClE,SAAI,CAAC,EAAkB,GAAU,EAAS,MAAM,eAAe,IAAI,2BAA2B,CAC5F;AAGF,KADA,EAAO,QAAQ,kBAAkB,EACjC,EAAS,YAAY;AACrB;;IAEF,IAAM,IAAW,MAAM,EAAc,EAAE,WAAW,EAAE,UAAO,EAAE,CAAC;AAC9D,QAAI,CAAC,EAAkB,GAAU,EAAS,MAAM,eAAe,IAAI,2BAA2B,CAC5F;AAGF,IADA,EAAO,QAAQ,kBAAkB,EACjC,EAAS,YAAY;YACd,GAAO;AACd,MAAO,MAAM,0BAA0B,EAAc,EAAM,CAAC;;;EAc5D,aAAY;EACJ,QAXG,KAAY;YAGzB;GAUE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,SAAD;KAAO,WAAU;eAAjB,CACE,kBAAC,QAAD;MAAM,WAAU;gBAAgC;MAAgB,CAAA,EAChE,kBAAC,GAAD;MAAO,OAAO,EAAK;MAAU,WAAW,MAAM,EAAS,YAAY,EAAE,OAAO,MAAM;MAAE,UAAA;MAAW,CAAA,CACzF;QACR,kBAAC,SAAD;KAAO,WAAU;eAAjB,CACE,kBAAC,QAAD;MAAM,WAAU;gBAAgC;MAAY,CAAA,EAC5D,kBAAC,GAAD;MAAO,OAAO,EAAK;MAAO,WAAW,MAAM,EAAS,SAAS,EAAE,OAAO,MAAM;MAAE,aAAY;MAAoB,CAAA,CACxG;OACJ;;GAEN,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,SAAD;MAAO,WAAU;gBAAjB,CACE,kBAAC,QAAD;OAAM,WAAU;iBAAgC;OAAc,CAAA,EAC9D,kBAAC,GAAD;OAAO,OAAO,EAAK;OAAc,WAAW,MAAM,EAAS,gBAAgB,EAAE,OAAO,MAAM;OAAI,CAAA,CACxF;;KACR,kBAAC,SAAD;MAAO,WAAU;gBAAjB,CACE,kBAAC,QAAD;OAAM,WAAU;iBAAgC;OAAY,CAAA,EAC5D,kBAAC,GAAD;OAAO,MAAK;OAAQ,OAAO,EAAK;OAAO,WAAW,MAAM,EAAS,SAAS,EAAE,OAAO,MAAM;OAAI,CAAA,CACvF;;KACR,kBAAC,SAAD;MAAO,WAAU;gBAAjB,CACE,kBAAC,QAAD;OAAM,WAAU;iBAAgC;OAAY,CAAA,EAC5D,kBAAC,GAAD;OAAO,OAAO,EAAK;OAAO,WAAW,MAAM,EAAS,SAAS,EAAE,OAAO,MAAM;OAAI,CAAA,CAC1E;;KACJ;;GAEN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,QAAD;KAAM,WAAU;eAAgC;KAAe,CAAA,EAC/D,kBAAC,GAAD;KACE,OAAM;KACN,YAAW;KACX,OAAO,EAAK,aAAa;KACzB,aAAa,MAAQ,EAAS,aAAa,EAAI;KAC/C,MAAK;KACL,CAAA,CACE;;GAEN,kBAAC,SAAD;IAAO,WAAU;cAAjB,CACE,kBAAC,QAAD;KAAM,WAAU;eAAgC;KAA8B,CAAA,EAC9E,kBAAC,GAAD;KACE,OAAO,EAAK;KACZ,WAAW,MAAM,EAAS,OAAO,EAAE,OAAO,MAAM;KAChD,MAAM;KACN,aAAY;KACZ,CAAA,CACI;;GAER,kBAAC,SAAD;IAAO,WAAU;cAAjB,CACE,kBAAC,QAAD;KAAM,WAAU;eAAgC;KAA4B,CAAA,EAC5E,kBAAC,GAAD;KAAO,OAAO,EAAK;KAAS,WAAW,MAAM,EAAS,WAAW,EAAE,OAAO,MAAM;KAAE,aAAY;KAAc,CAAA,CACtG;;GAEP,IAAS,kBAAC,GAAD;IAAiB,YAAW;IAAU,UAAU;IAAM,CAAA,GAAG;GACnD"}
|