@burdenoff/microfe-eventfullymanaged 2026.731.1 → 2026.803.1

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.
Files changed (41) hide show
  1. package/dist/eventfullymanaged/EventfullyManagedRoutes.js +1 -1
  2. package/dist/eventfullymanaged/EventfullyManagedRoutes.js.map +1 -1
  3. package/dist/eventfullymanaged/pages/events/EventDetailPage.js +1 -1
  4. package/dist/eventfullymanaged/pages/events/EventDetailPage.js.map +1 -1
  5. package/dist/eventfullymanaged/pages/events/EventFormPage.js +1 -1
  6. package/dist/eventfullymanaged/pages/events/EventFormPage.js.map +1 -1
  7. package/dist/eventfullymanaged/pages/events/SessionFormPage.js +1 -1
  8. package/dist/eventfullymanaged/pages/events/SessionFormPage.js.map +1 -1
  9. package/dist/eventfullymanaged/pages/events/TicketTypeFormPage.js +1 -1
  10. package/dist/eventfullymanaged/pages/events/TicketTypeFormPage.js.map +1 -1
  11. package/dist/eventfullymanaged/pages/events/VirtualSessionPage.js +1 -1
  12. package/dist/eventfullymanaged/pages/events/VirtualSessionPage.js.map +1 -1
  13. package/dist/eventfullymanaged/pages/exhibitors/ExhibitorFormPage.js +1 -1
  14. package/dist/eventfullymanaged/pages/exhibitors/ExhibitorFormPage.js.map +1 -1
  15. package/dist/eventfullymanaged/pages/modules/AttendeeProfilePage.js +1 -1
  16. package/dist/eventfullymanaged/pages/modules/AttendeeProfilePage.js.map +1 -1
  17. package/dist/eventfullymanaged/pages/modules/SponsorDetailsPage.js +1 -1
  18. package/dist/eventfullymanaged/pages/modules/SponsorDetailsPage.js.map +1 -1
  19. package/dist/eventfullymanaged/pages/modules/SponsorLeadsPage.js +1 -1
  20. package/dist/eventfullymanaged/pages/modules/SponsorLeadsPage.js.map +1 -1
  21. package/dist/eventfullymanaged/pages/modules/VenueResourcesPage.js +1 -1
  22. package/dist/eventfullymanaged/pages/modules/VenueResourcesPage.js.map +1 -1
  23. package/dist/eventfullymanaged/pages/networking/MeetingFormPage.js +1 -1
  24. package/dist/eventfullymanaged/pages/networking/MeetingFormPage.js.map +1 -1
  25. package/dist/eventfullymanaged/pages/networking/NetworkingProfileFormPage.js +1 -1
  26. package/dist/eventfullymanaged/pages/networking/NetworkingProfileFormPage.js.map +1 -1
  27. package/dist/eventfullymanaged/pages/registration/AttendeeFormPage.js +1 -1
  28. package/dist/eventfullymanaged/pages/registration/AttendeeFormPage.js.map +1 -1
  29. package/dist/eventfullymanaged/pages/registration/PromoCodeFormPage.js +1 -1
  30. package/dist/eventfullymanaged/pages/registration/PromoCodeFormPage.js.map +1 -1
  31. package/dist/eventfullymanaged/pages/speakers/SpeakerFormPage.js +1 -1
  32. package/dist/eventfullymanaged/pages/speakers/SpeakerFormPage.js.map +1 -1
  33. package/dist/eventfullymanaged/pages/sponsors/SponsorFormPage.js +1 -1
  34. package/dist/eventfullymanaged/pages/sponsors/SponsorFormPage.js.map +1 -1
  35. package/dist/eventfullymanaged/pages/venues/FloorPlansPage.js +1 -1
  36. package/dist/eventfullymanaged/pages/venues/FloorPlansPage.js.map +1 -1
  37. package/dist/eventfullymanaged/pages/venues/VenueDetailPage.js +1 -1
  38. package/dist/eventfullymanaged/pages/venues/VenueDetailPage.js.map +1 -1
  39. package/dist/eventfullymanaged/pages/venues/VenueFormPage.js +1 -1
  40. package/dist/eventfullymanaged/pages/venues/VenueFormPage.js.map +1 -1
  41. package/package.json +9 -9
@@ -1 +1 @@
1
- {"version":3,"file":"SessionFormPage.js","names":[],"sources":["../../../../src/eventfullymanaged/pages/events/SessionFormPage.tsx"],"sourcesContent":["/* eslint-disable react-hooks/set-state-in-effect */\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Textarea } from '@burdenoff/fe-libs/ui';\nimport { useEffect, useMemo, useState } from 'react';\nimport { useParams } from 'react-router-dom';\n\nimport {\n useCreateEventSessionMutation,\n useEventQuery,\n useSessionConflictsQuery,\n useSpeakersQuery,\n useUpdateEventSessionMutation,\n useVenuesQuery,\n} from '../../api/eventApi';\nimport { EntityFormShell } from '../../components/EntityFormShell';\nimport { EntityTagsField } from '../../components/EntityTagsField';\nimport { useEventfullyManaged } from '../../context/EventfullyManagedContext';\nimport { describeError, mutationSucceeded } from '../../lib/mutationResult';\nimport { notify } from '../../lib/toast';\nimport { inputValueToIso, isoToInputValue, SESSION_STATUS_OPTIONS, SESSION_TYPE_OPTIONS } from './eventFormUtils';\n\nconst SPEAKER_METADATA_KEY = 'speakerId';\n\n// The foundation EVENT_SESSIONS_QUERY omits description/capacity/metadata, but the\n// backend `updateEventSession` resolver is a FULL replace — editing with a partial\n// initial state would blank those columns. There is no by-id session resolver, so\n// this fuller list query is the edit-hydration source: it selects every editable\n// field and the form finds its row by id client-side.\nconst FULL_EVENT_SESSIONS_QUERY = gql`\n query EventfullymanagedSessionFormSessions($eventId: ID, $limit: Int, $offset: Int) {\n eventSessions: eventfullyManagedEventSessions(eventId: $eventId, limit: $limit, offset: $offset) {\n total\n items {\n id\n eventId\n venueId\n title\n description\n sessionType\n track\n startsAt\n endsAt\n capacity\n status\n metadata\n }\n }\n }\n`;\n\ntype FullSessionsResponse = {\n eventSessions: { total: number; items: SessionListItem[] };\n};\n\ntype SessionListItem = {\n id: string;\n eventId: string;\n venueId?: string | null;\n title: string;\n sessionType: string;\n track?: string | null;\n startsAt: string;\n endsAt: string;\n status: string;\n capacity?: number | null;\n description?: string | null;\n metadata?: Record<string, unknown> | null;\n};\n\ntype SessionFormState = {\n title: string;\n sessionType: string;\n track: string;\n venueId: string;\n speakerId: string;\n startsAt: string;\n endsAt: string;\n capacity: string;\n status: string;\n description: string;\n};\n\nfunction defaultState(): SessionFormState {\n const now = new Date();\n const later = new Date(now.getTime() + 60 * 60 * 1000);\n return {\n title: '',\n sessionType: 'TALK',\n track: '',\n venueId: '',\n speakerId: '',\n startsAt: isoToInputValue(now.toISOString()),\n endsAt: isoToInputValue(later.toISOString()),\n capacity: '',\n status: 'SCHEDULED',\n description: '',\n };\n}\n\nexport default function SessionFormPage() {\n const { id = '', sid = '' } = useParams<{ id: string; sid: string }>();\n const isEdit = useMemo(() => Boolean(sid), [sid]);\n const { navigate } = useEventfullyManaged();\n\n const { data: eventData } = useEventQuery(id);\n // No by-id session resolver — derive the edit initial state from the (fuller) list query.\n const { data: sessionsData } = useQuery<FullSessionsResponse, { eventId?: string; limit?: number; offset?: number }>(\n FULL_EVENT_SESSIONS_QUERY,\n { variables: { eventId: id, limit: 100, offset: 0 }, skip: !sid }\n );\n const { data: venuesData } = useVenuesQuery({ limit: 100, offset: 0 });\n const { data: speakersData } = useSpeakersQuery({ limit: 100, offset: 0 });\n // BOFF-4331 — surface venue/speaker double-bookings for this event's agenda\n // right where an organizer is scheduling a session.\n const { data: conflictsData } = useSessionConflictsQuery({ eventId: id });\n const relevantConflicts = useMemo(() => {\n const conflicts = conflictsData?.sessionConflicts ?? [];\n return isEdit ? conflicts.filter((c) => c.sessionAId === sid || c.sessionBId === sid) : conflicts;\n }, [conflictsData, isEdit, sid]);\n const [createSession, { loading: creating }] = useCreateEventSessionMutation();\n const [updateSession, { loading: updating }] = useUpdateEventSessionMutation();\n const [form, setForm] = useState<SessionFormState>(defaultState);\n\n const existingSession = useMemo(\n () => (sessionsData?.eventSessions?.items ?? []).find((item) => item.id === sid),\n [sessionsData, sid]\n );\n\n useEffect(() => {\n if (!isEdit || !existingSession) {\n return;\n }\n const metadata = (existingSession.metadata ?? {}) as Record<string, unknown>;\n setForm({\n title: existingSession.title ?? '',\n sessionType: existingSession.sessionType ?? 'TALK',\n track: existingSession.track ?? '',\n venueId: existingSession.venueId ?? '',\n speakerId: typeof metadata[SPEAKER_METADATA_KEY] === 'string' ? (metadata[SPEAKER_METADATA_KEY] as string) : '',\n startsAt: isoToInputValue(existingSession.startsAt),\n endsAt: isoToInputValue(existingSession.endsAt),\n capacity: existingSession.capacity != null ? String(existingSession.capacity) : '',\n status: existingSession.status ?? 'SCHEDULED',\n description: existingSession.description ?? '',\n });\n }, [existingSession, isEdit]);\n\n const venues = venuesData?.venues?.items ?? [];\n const speakers = (speakersData?.speakers?.items ?? []) as Array<{ id: string; fullName: string }>;\n const eventName = eventData?.event?.name ?? 'Event';\n\n const onChange = (field: keyof SessionFormState, 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.title.trim()) {\n notify.error('Title required', 'Give the session a title before saving.');\n return;\n }\n const startIso = inputValueToIso(form.startsAt);\n const endIso = inputValueToIso(form.endsAt);\n if (!startIso || !endIso) {\n notify.error('Times required', 'Set both a start and an end time.');\n return;\n }\n if (new Date(endIso).getTime() < new Date(startIso).getTime()) {\n notify.error('Invalid times', 'The end time cannot be before the start time.');\n return;\n }\n\n const existingMetadata = (existingSession?.metadata ?? {}) as Record<string, unknown>;\n const input = {\n eventId: id,\n title: form.title.trim(),\n sessionType: form.sessionType,\n track: form.track || null,\n venueId: form.venueId || null,\n startsAt: startIso,\n endsAt: endIso,\n capacity: form.capacity ? Number(form.capacity) : null,\n status: form.status,\n description: form.description || null,\n metadata: { ...existingMetadata, [SPEAKER_METADATA_KEY]: form.speakerId || null },\n };\n\n try {\n if (isEdit) {\n const response = await updateSession({ variables: { id: sid, input } });\n if (!mutationSucceeded(response, response.data?.updateEventSession?.id, 'Could not update session')) {\n return;\n }\n notify.success('Session updated');\n navigate(`/events/${id}`);\n return;\n }\n const response = await createSession({ variables: { input } });\n if (!mutationSucceeded(response, response.data?.createEventSession?.id, 'Could not create session')) {\n return;\n }\n notify.success('Session created');\n navigate(`/events/${id}`);\n } catch (error) {\n notify.error('Could not save session', describeError(error));\n }\n };\n\n const saving = creating || updating;\n\n return (\n <EntityFormShell\n title={isEdit ? 'Edit Session' : 'Add Session'}\n description={`Agenda item for ${eventName}.`}\n purpose=\"Sessions make up the agenda attendees follow on the day — a talk, workshop or panel with a time, a room and a speaker. Set the slot and assign a room so two sessions never clash, and pick the speaker who will present it.\"\n backLabel=\"Back to Event\"\n onBack={() => navigate(`/events/${id}`)}\n onSubmit={onSubmit}\n submitLabel=\"Save Session\"\n saving={saving}\n >\n {relevantConflicts.length > 0 ? (\n <div className=\"rounded-card border border-status-warning-border bg-status-warning-bg-subtle p-3 text-sm text-status-warning-text\">\n <p className=\"font-medium\">\n {relevantConflicts.length === 1\n ? '1 agenda conflict detected'\n : `${relevantConflicts.length} agenda conflicts detected`}\n </p>\n <ul className=\"mt-1 list-inside list-disc space-y-0.5\">\n {relevantConflicts.map((conflict, index) => (\n <li key={`${conflict.type}-${conflict.sessionAId}-${conflict.sessionBId}-${index}`}>\n {conflict.message}\n </li>\n ))}\n </ul>\n </div>\n ) : null}\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\">Title</span>\n <Input value={form.title} onChange={(e) => onChange('title', e.target.value)} required />\n </label>\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Track</span>\n <Input\n value={form.track}\n onChange={(e) => onChange('track', e.target.value)}\n placeholder=\"Main stage, Product, Engineering…\"\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\">Room / Venue</span>\n <Select\n value={form.venueId || 'none'}\n onValueChange={(value) => onChange('venueId', value === 'none' ? '' : value)}\n >\n <SelectTrigger aria-label=\"Room or venue\">\n <SelectValue placeholder=\"Select a room\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"none\">No room</SelectItem>\n {venues.map((venue: { id: string; name: string }) => (\n <SelectItem key={venue.id} value={venue.id}>\n {venue.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\">Speaker</span>\n <Select\n value={form.speakerId || 'none'}\n onValueChange={(value) => onChange('speakerId', value === 'none' ? '' : value)}\n >\n <SelectTrigger aria-label=\"Speaker\">\n <SelectValue placeholder=\"Select a speaker\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"none\">No speaker</SelectItem>\n {speakers.map((speaker) => (\n <SelectItem key={speaker.id} value={speaker.id}>\n {speaker.fullName}\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\">Capacity</span>\n <Input type=\"number\" min=\"0\" value={form.capacity} onChange={(e) => onChange('capacity', e.target.value)} />\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\">Start</span>\n <Input\n type=\"datetime-local\"\n value={form.startsAt}\n onChange={(e) => onChange('startsAt', e.target.value)}\n required\n />\n </label>\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">End</span>\n <Input\n type=\"datetime-local\"\n value={form.endsAt}\n onChange={(e) => onChange('endsAt', e.target.value)}\n required\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\">Type</span>\n <Select value={form.sessionType} onValueChange={(value) => onChange('sessionType', value)}>\n <SelectTrigger aria-label=\"Session type\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {SESSION_TYPE_OPTIONS.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n {option.label}\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\">Status</span>\n <Select value={form.status} onValueChange={(value) => onChange('status', value)}>\n <SelectTrigger aria-label=\"Status\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {SESSION_STATUS_OPTIONS.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\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={3} />\n </label>\n\n {isEdit ? <EntityTagsField entityType=\"EVENTFULLYMANAGED_SESSION\" entityId={sid} /> : null}\n </EntityFormShell>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"SessionFormPage.js","names":[],"sources":["../../../../src/eventfullymanaged/pages/events/SessionFormPage.tsx"],"sourcesContent":["/* eslint-disable react-hooks/set-state-in-effect */\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Textarea } from '@burdenoff/fe-libs/ui';\nimport { useEffect, useMemo, useState } from 'react';\nimport { useParams } from 'react-router';\n\nimport {\n useCreateEventSessionMutation,\n useEventQuery,\n useSessionConflictsQuery,\n useSpeakersQuery,\n useUpdateEventSessionMutation,\n useVenuesQuery,\n} from '../../api/eventApi';\nimport { EntityFormShell } from '../../components/EntityFormShell';\nimport { EntityTagsField } from '../../components/EntityTagsField';\nimport { useEventfullyManaged } from '../../context/EventfullyManagedContext';\nimport { describeError, mutationSucceeded } from '../../lib/mutationResult';\nimport { notify } from '../../lib/toast';\nimport { inputValueToIso, isoToInputValue, SESSION_STATUS_OPTIONS, SESSION_TYPE_OPTIONS } from './eventFormUtils';\n\nconst SPEAKER_METADATA_KEY = 'speakerId';\n\n// The foundation EVENT_SESSIONS_QUERY omits description/capacity/metadata, but the\n// backend `updateEventSession` resolver is a FULL replace — editing with a partial\n// initial state would blank those columns. There is no by-id session resolver, so\n// this fuller list query is the edit-hydration source: it selects every editable\n// field and the form finds its row by id client-side.\nconst FULL_EVENT_SESSIONS_QUERY = gql`\n query EventfullymanagedSessionFormSessions($eventId: ID, $limit: Int, $offset: Int) {\n eventSessions: eventfullyManagedEventSessions(eventId: $eventId, limit: $limit, offset: $offset) {\n total\n items {\n id\n eventId\n venueId\n title\n description\n sessionType\n track\n startsAt\n endsAt\n capacity\n status\n metadata\n }\n }\n }\n`;\n\ntype FullSessionsResponse = {\n eventSessions: { total: number; items: SessionListItem[] };\n};\n\ntype SessionListItem = {\n id: string;\n eventId: string;\n venueId?: string | null;\n title: string;\n sessionType: string;\n track?: string | null;\n startsAt: string;\n endsAt: string;\n status: string;\n capacity?: number | null;\n description?: string | null;\n metadata?: Record<string, unknown> | null;\n};\n\ntype SessionFormState = {\n title: string;\n sessionType: string;\n track: string;\n venueId: string;\n speakerId: string;\n startsAt: string;\n endsAt: string;\n capacity: string;\n status: string;\n description: string;\n};\n\nfunction defaultState(): SessionFormState {\n const now = new Date();\n const later = new Date(now.getTime() + 60 * 60 * 1000);\n return {\n title: '',\n sessionType: 'TALK',\n track: '',\n venueId: '',\n speakerId: '',\n startsAt: isoToInputValue(now.toISOString()),\n endsAt: isoToInputValue(later.toISOString()),\n capacity: '',\n status: 'SCHEDULED',\n description: '',\n };\n}\n\nexport default function SessionFormPage() {\n const { id = '', sid = '' } = useParams<{ id: string; sid: string }>();\n const isEdit = useMemo(() => Boolean(sid), [sid]);\n const { navigate } = useEventfullyManaged();\n\n const { data: eventData } = useEventQuery(id);\n // No by-id session resolver — derive the edit initial state from the (fuller) list query.\n const { data: sessionsData } = useQuery<FullSessionsResponse, { eventId?: string; limit?: number; offset?: number }>(\n FULL_EVENT_SESSIONS_QUERY,\n { variables: { eventId: id, limit: 100, offset: 0 }, skip: !sid }\n );\n const { data: venuesData } = useVenuesQuery({ limit: 100, offset: 0 });\n const { data: speakersData } = useSpeakersQuery({ limit: 100, offset: 0 });\n // BOFF-4331 — surface venue/speaker double-bookings for this event's agenda\n // right where an organizer is scheduling a session.\n const { data: conflictsData } = useSessionConflictsQuery({ eventId: id });\n const relevantConflicts = useMemo(() => {\n const conflicts = conflictsData?.sessionConflicts ?? [];\n return isEdit ? conflicts.filter((c) => c.sessionAId === sid || c.sessionBId === sid) : conflicts;\n }, [conflictsData, isEdit, sid]);\n const [createSession, { loading: creating }] = useCreateEventSessionMutation();\n const [updateSession, { loading: updating }] = useUpdateEventSessionMutation();\n const [form, setForm] = useState<SessionFormState>(defaultState);\n\n const existingSession = useMemo(\n () => (sessionsData?.eventSessions?.items ?? []).find((item) => item.id === sid),\n [sessionsData, sid]\n );\n\n useEffect(() => {\n if (!isEdit || !existingSession) {\n return;\n }\n const metadata = (existingSession.metadata ?? {}) as Record<string, unknown>;\n setForm({\n title: existingSession.title ?? '',\n sessionType: existingSession.sessionType ?? 'TALK',\n track: existingSession.track ?? '',\n venueId: existingSession.venueId ?? '',\n speakerId: typeof metadata[SPEAKER_METADATA_KEY] === 'string' ? (metadata[SPEAKER_METADATA_KEY] as string) : '',\n startsAt: isoToInputValue(existingSession.startsAt),\n endsAt: isoToInputValue(existingSession.endsAt),\n capacity: existingSession.capacity != null ? String(existingSession.capacity) : '',\n status: existingSession.status ?? 'SCHEDULED',\n description: existingSession.description ?? '',\n });\n }, [existingSession, isEdit]);\n\n const venues = venuesData?.venues?.items ?? [];\n const speakers = (speakersData?.speakers?.items ?? []) as Array<{ id: string; fullName: string }>;\n const eventName = eventData?.event?.name ?? 'Event';\n\n const onChange = (field: keyof SessionFormState, 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.title.trim()) {\n notify.error('Title required', 'Give the session a title before saving.');\n return;\n }\n const startIso = inputValueToIso(form.startsAt);\n const endIso = inputValueToIso(form.endsAt);\n if (!startIso || !endIso) {\n notify.error('Times required', 'Set both a start and an end time.');\n return;\n }\n if (new Date(endIso).getTime() < new Date(startIso).getTime()) {\n notify.error('Invalid times', 'The end time cannot be before the start time.');\n return;\n }\n\n const existingMetadata = (existingSession?.metadata ?? {}) as Record<string, unknown>;\n const input = {\n eventId: id,\n title: form.title.trim(),\n sessionType: form.sessionType,\n track: form.track || null,\n venueId: form.venueId || null,\n startsAt: startIso,\n endsAt: endIso,\n capacity: form.capacity ? Number(form.capacity) : null,\n status: form.status,\n description: form.description || null,\n metadata: { ...existingMetadata, [SPEAKER_METADATA_KEY]: form.speakerId || null },\n };\n\n try {\n if (isEdit) {\n const response = await updateSession({ variables: { id: sid, input } });\n if (!mutationSucceeded(response, response.data?.updateEventSession?.id, 'Could not update session')) {\n return;\n }\n notify.success('Session updated');\n navigate(`/events/${id}`);\n return;\n }\n const response = await createSession({ variables: { input } });\n if (!mutationSucceeded(response, response.data?.createEventSession?.id, 'Could not create session')) {\n return;\n }\n notify.success('Session created');\n navigate(`/events/${id}`);\n } catch (error) {\n notify.error('Could not save session', describeError(error));\n }\n };\n\n const saving = creating || updating;\n\n return (\n <EntityFormShell\n title={isEdit ? 'Edit Session' : 'Add Session'}\n description={`Agenda item for ${eventName}.`}\n purpose=\"Sessions make up the agenda attendees follow on the day — a talk, workshop or panel with a time, a room and a speaker. Set the slot and assign a room so two sessions never clash, and pick the speaker who will present it.\"\n backLabel=\"Back to Event\"\n onBack={() => navigate(`/events/${id}`)}\n onSubmit={onSubmit}\n submitLabel=\"Save Session\"\n saving={saving}\n >\n {relevantConflicts.length > 0 ? (\n <div className=\"rounded-card border border-status-warning-border bg-status-warning-bg-subtle p-3 text-sm text-status-warning-text\">\n <p className=\"font-medium\">\n {relevantConflicts.length === 1\n ? '1 agenda conflict detected'\n : `${relevantConflicts.length} agenda conflicts detected`}\n </p>\n <ul className=\"mt-1 list-inside list-disc space-y-0.5\">\n {relevantConflicts.map((conflict, index) => (\n <li key={`${conflict.type}-${conflict.sessionAId}-${conflict.sessionBId}-${index}`}>\n {conflict.message}\n </li>\n ))}\n </ul>\n </div>\n ) : null}\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\">Title</span>\n <Input value={form.title} onChange={(e) => onChange('title', e.target.value)} required />\n </label>\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Track</span>\n <Input\n value={form.track}\n onChange={(e) => onChange('track', e.target.value)}\n placeholder=\"Main stage, Product, Engineering…\"\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\">Room / Venue</span>\n <Select\n value={form.venueId || 'none'}\n onValueChange={(value) => onChange('venueId', value === 'none' ? '' : value)}\n >\n <SelectTrigger aria-label=\"Room or venue\">\n <SelectValue placeholder=\"Select a room\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"none\">No room</SelectItem>\n {venues.map((venue: { id: string; name: string }) => (\n <SelectItem key={venue.id} value={venue.id}>\n {venue.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\">Speaker</span>\n <Select\n value={form.speakerId || 'none'}\n onValueChange={(value) => onChange('speakerId', value === 'none' ? '' : value)}\n >\n <SelectTrigger aria-label=\"Speaker\">\n <SelectValue placeholder=\"Select a speaker\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"none\">No speaker</SelectItem>\n {speakers.map((speaker) => (\n <SelectItem key={speaker.id} value={speaker.id}>\n {speaker.fullName}\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\">Capacity</span>\n <Input type=\"number\" min=\"0\" value={form.capacity} onChange={(e) => onChange('capacity', e.target.value)} />\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\">Start</span>\n <Input\n type=\"datetime-local\"\n value={form.startsAt}\n onChange={(e) => onChange('startsAt', e.target.value)}\n required\n />\n </label>\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">End</span>\n <Input\n type=\"datetime-local\"\n value={form.endsAt}\n onChange={(e) => onChange('endsAt', e.target.value)}\n required\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\">Type</span>\n <Select value={form.sessionType} onValueChange={(value) => onChange('sessionType', value)}>\n <SelectTrigger aria-label=\"Session type\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {SESSION_TYPE_OPTIONS.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n {option.label}\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\">Status</span>\n <Select value={form.status} onValueChange={(value) => onChange('status', value)}>\n <SelectTrigger aria-label=\"Status\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {SESSION_STATUS_OPTIONS.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\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={3} />\n </label>\n\n {isEdit ? <EntityTagsField entityType=\"EVENTFULLYMANAGED_SESSION\" entityId={sid} /> : null}\n </EntityFormShell>\n );\n}\n"],"mappings":"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"}
@@ -5,7 +5,7 @@ import { CURRENCY_OPTIONS as o, inputValueToIso as s, isoToInputValue as c } fro
5
5
  import { EntityFormShell as l } from "../../components/EntityFormShell.js";
6
6
  import { describeError as u, mutationSucceeded as d } from "../../lib/mutationResult.js";
7
7
  import { useEffect as f, useMemo as p, useState as m } from "react";
8
- import { useParams as h } from "react-router-dom";
8
+ import { useParams as h } from "react-router";
9
9
  import { jsx as g, jsxs as _ } from "react/jsx-runtime";
10
10
  import { Input as v, Select as y, SelectContent as b, SelectItem as x, SelectTrigger as S, SelectValue as C, Switch as w, Textarea as T } from "@burdenoff/fe-libs/ui";
11
11
  //#region src/eventfullymanaged/pages/events/TicketTypeFormPage.tsx
@@ -1 +1 @@
1
- {"version":3,"file":"TicketTypeFormPage.js","names":[],"sources":["../../../../src/eventfullymanaged/pages/events/TicketTypeFormPage.tsx"],"sourcesContent":["/* eslint-disable react-hooks/set-state-in-effect */\nimport {\n Input,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n Switch,\n Textarea,\n} from '@burdenoff/fe-libs/ui';\nimport { useEffect, useMemo, useState } from 'react';\nimport { useParams } from 'react-router-dom';\n\nimport {\n useCreateTicketTypeMutation,\n useEventQuery,\n useTicketTypesQuery,\n useUpdateTicketTypeMutation,\n type TicketTypeGql,\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';\nimport { CURRENCY_OPTIONS, inputValueToIso, isoToInputValue } from './eventFormUtils';\n\ntype TicketTypeFormState = {\n name: string;\n description: string;\n price: string;\n currency: string;\n capacity: string;\n salesStartAt: string;\n salesEndAt: string;\n isActive: boolean;\n};\n\nconst initialState: TicketTypeFormState = {\n name: '',\n description: '',\n price: '0',\n currency: 'INR',\n capacity: '',\n salesStartAt: '',\n salesEndAt: '',\n isActive: true,\n};\n\nexport default function TicketTypeFormPage() {\n const { id = '', tid = '' } = useParams<{ id: string; tid: string }>();\n const isEdit = useMemo(() => Boolean(tid), [tid]);\n const { navigate } = useEventfullyManaged();\n\n const { data: eventData } = useEventQuery(id);\n // No by-id ticket-type resolver — derive the edit initial state from the list query.\n const { data: ticketsData } = useTicketTypesQuery({ eventId: id, limit: 100, offset: 0 });\n const [createTicketType, { loading: creating }] = useCreateTicketTypeMutation();\n const [updateTicketType, { loading: updating }] = useUpdateTicketTypeMutation();\n const [form, setForm] = useState<TicketTypeFormState>(initialState);\n\n const existingTicket = useMemo(\n () => (ticketsData?.ticketTypes?.items ?? []).find((item) => item.id === tid),\n [ticketsData, tid]\n );\n\n useEffect(() => {\n if (!isEdit || !existingTicket) {\n return;\n }\n const ticket = existingTicket as TicketTypeGql;\n setForm({\n name: ticket.name ?? '',\n description: ticket.description ?? '',\n price: ticket.price ?? '0',\n currency: ticket.currency ?? 'INR',\n capacity: ticket.capacity != null ? String(ticket.capacity) : '',\n salesStartAt: isoToInputValue(ticket.salesStartAt),\n salesEndAt: isoToInputValue(ticket.salesEndAt),\n isActive: ticket.isActive ?? true,\n });\n }, [existingTicket, isEdit]);\n\n const eventName = eventData?.event?.name ?? 'Event';\n\n const onChange = <K extends keyof TicketTypeFormState>(field: K, value: TicketTypeFormState[K]) => {\n setForm((prev) => ({ ...prev, [field]: value }));\n };\n\n const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n\n if (!form.name.trim()) {\n notify.error('Name required', 'Give the ticket type a name before saving.');\n return;\n }\n const priceValue = Number(form.price);\n if (Number.isNaN(priceValue) || priceValue < 0) {\n notify.error('Invalid price', 'Enter a price of zero or more.');\n return;\n }\n const salesStartIso = inputValueToIso(form.salesStartAt);\n const salesEndIso = inputValueToIso(form.salesEndAt);\n if (salesStartIso && salesEndIso && new Date(salesEndIso).getTime() < new Date(salesStartIso).getTime()) {\n notify.error('Invalid sales window', 'Sales end cannot be before sales start.');\n return;\n }\n\n const input = {\n eventId: id,\n name: form.name.trim(),\n description: form.description || null,\n price: String(priceValue),\n currency: form.currency,\n capacity: form.capacity ? Number(form.capacity) : null,\n salesStartAt: salesStartIso || null,\n salesEndAt: salesEndIso || null,\n isActive: form.isActive,\n metadata: {},\n };\n\n try {\n if (isEdit) {\n const response = await updateTicketType({ variables: { id: tid, input } });\n if (!mutationSucceeded(response, response.data?.updateTicketType?.id, 'Could not update ticket type')) {\n return;\n }\n notify.success('Ticket type updated');\n navigate(`/events/${id}`);\n return;\n }\n const response = await createTicketType({ variables: { input } });\n if (!mutationSucceeded(response, response.data?.createTicketType?.id, 'Could not create ticket type')) {\n return;\n }\n notify.success('Ticket type created');\n navigate(`/events/${id}`);\n } catch (error) {\n notify.error('Could not save ticket type', describeError(error));\n }\n };\n\n const saving = creating || updating;\n\n return (\n <EntityFormShell\n title={isEdit ? 'Edit Ticket Type' : 'Add Ticket Type'}\n description={`Sellable ticket for ${eventName}.`}\n purpose=\"Ticket types are what attendees actually buy — Early Bird, General, VIP. Set a price and currency, cap how many can be sold, and define the sales window so the ticket opens and closes automatically. Toggle it inactive to pull it from sale without deleting the history.\"\n backLabel=\"Back to Event\"\n onBack={() => navigate(`/events/${id}`)}\n onSubmit={onSubmit}\n submitLabel=\"Save Ticket Type\"\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\">Name</span>\n <Input value={form.name} onChange={(e) => onChange('name', e.target.value)} required />\n </label>\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Capacity</span>\n <Input\n type=\"number\"\n min=\"0\"\n value={form.capacity}\n onChange={(e) => onChange('capacity', 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\">Price</span>\n <Input\n type=\"number\"\n min=\"0\"\n step=\"0.01\"\n value={form.price}\n onChange={(e) => onChange('price', e.target.value)}\n required\n />\n </label>\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Currency</span>\n <Select value={form.currency} onValueChange={(value) => onChange('currency', value)}>\n <SelectTrigger aria-label=\"Currency\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {CURRENCY_OPTIONS.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\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\">Sales start</span>\n <Input\n type=\"datetime-local\"\n value={form.salesStartAt}\n onChange={(e) => onChange('salesStartAt', e.target.value)}\n />\n </label>\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Sales end</span>\n <Input\n type=\"datetime-local\"\n value={form.salesEndAt}\n onChange={(e) => onChange('salesEndAt', e.target.value)}\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={3} />\n </label>\n\n <div className=\"flex items-center justify-between rounded-card border border-border-seam p-3\">\n <div>\n <p className=\"text-sm font-medium text-text-primary\">Active</p>\n <p className=\"text-xs text-text-secondary\">Active ticket types are open for sale during their window.</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":"TicketTypeFormPage.js","names":[],"sources":["../../../../src/eventfullymanaged/pages/events/TicketTypeFormPage.tsx"],"sourcesContent":["/* eslint-disable react-hooks/set-state-in-effect */\nimport {\n Input,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n Switch,\n Textarea,\n} from '@burdenoff/fe-libs/ui';\nimport { useEffect, useMemo, useState } from 'react';\nimport { useParams } from 'react-router';\n\nimport {\n useCreateTicketTypeMutation,\n useEventQuery,\n useTicketTypesQuery,\n useUpdateTicketTypeMutation,\n type TicketTypeGql,\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';\nimport { CURRENCY_OPTIONS, inputValueToIso, isoToInputValue } from './eventFormUtils';\n\ntype TicketTypeFormState = {\n name: string;\n description: string;\n price: string;\n currency: string;\n capacity: string;\n salesStartAt: string;\n salesEndAt: string;\n isActive: boolean;\n};\n\nconst initialState: TicketTypeFormState = {\n name: '',\n description: '',\n price: '0',\n currency: 'INR',\n capacity: '',\n salesStartAt: '',\n salesEndAt: '',\n isActive: true,\n};\n\nexport default function TicketTypeFormPage() {\n const { id = '', tid = '' } = useParams<{ id: string; tid: string }>();\n const isEdit = useMemo(() => Boolean(tid), [tid]);\n const { navigate } = useEventfullyManaged();\n\n const { data: eventData } = useEventQuery(id);\n // No by-id ticket-type resolver — derive the edit initial state from the list query.\n const { data: ticketsData } = useTicketTypesQuery({ eventId: id, limit: 100, offset: 0 });\n const [createTicketType, { loading: creating }] = useCreateTicketTypeMutation();\n const [updateTicketType, { loading: updating }] = useUpdateTicketTypeMutation();\n const [form, setForm] = useState<TicketTypeFormState>(initialState);\n\n const existingTicket = useMemo(\n () => (ticketsData?.ticketTypes?.items ?? []).find((item) => item.id === tid),\n [ticketsData, tid]\n );\n\n useEffect(() => {\n if (!isEdit || !existingTicket) {\n return;\n }\n const ticket = existingTicket as TicketTypeGql;\n setForm({\n name: ticket.name ?? '',\n description: ticket.description ?? '',\n price: ticket.price ?? '0',\n currency: ticket.currency ?? 'INR',\n capacity: ticket.capacity != null ? String(ticket.capacity) : '',\n salesStartAt: isoToInputValue(ticket.salesStartAt),\n salesEndAt: isoToInputValue(ticket.salesEndAt),\n isActive: ticket.isActive ?? true,\n });\n }, [existingTicket, isEdit]);\n\n const eventName = eventData?.event?.name ?? 'Event';\n\n const onChange = <K extends keyof TicketTypeFormState>(field: K, value: TicketTypeFormState[K]) => {\n setForm((prev) => ({ ...prev, [field]: value }));\n };\n\n const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n\n if (!form.name.trim()) {\n notify.error('Name required', 'Give the ticket type a name before saving.');\n return;\n }\n const priceValue = Number(form.price);\n if (Number.isNaN(priceValue) || priceValue < 0) {\n notify.error('Invalid price', 'Enter a price of zero or more.');\n return;\n }\n const salesStartIso = inputValueToIso(form.salesStartAt);\n const salesEndIso = inputValueToIso(form.salesEndAt);\n if (salesStartIso && salesEndIso && new Date(salesEndIso).getTime() < new Date(salesStartIso).getTime()) {\n notify.error('Invalid sales window', 'Sales end cannot be before sales start.');\n return;\n }\n\n const input = {\n eventId: id,\n name: form.name.trim(),\n description: form.description || null,\n price: String(priceValue),\n currency: form.currency,\n capacity: form.capacity ? Number(form.capacity) : null,\n salesStartAt: salesStartIso || null,\n salesEndAt: salesEndIso || null,\n isActive: form.isActive,\n metadata: {},\n };\n\n try {\n if (isEdit) {\n const response = await updateTicketType({ variables: { id: tid, input } });\n if (!mutationSucceeded(response, response.data?.updateTicketType?.id, 'Could not update ticket type')) {\n return;\n }\n notify.success('Ticket type updated');\n navigate(`/events/${id}`);\n return;\n }\n const response = await createTicketType({ variables: { input } });\n if (!mutationSucceeded(response, response.data?.createTicketType?.id, 'Could not create ticket type')) {\n return;\n }\n notify.success('Ticket type created');\n navigate(`/events/${id}`);\n } catch (error) {\n notify.error('Could not save ticket type', describeError(error));\n }\n };\n\n const saving = creating || updating;\n\n return (\n <EntityFormShell\n title={isEdit ? 'Edit Ticket Type' : 'Add Ticket Type'}\n description={`Sellable ticket for ${eventName}.`}\n purpose=\"Ticket types are what attendees actually buy — Early Bird, General, VIP. Set a price and currency, cap how many can be sold, and define the sales window so the ticket opens and closes automatically. Toggle it inactive to pull it from sale without deleting the history.\"\n backLabel=\"Back to Event\"\n onBack={() => navigate(`/events/${id}`)}\n onSubmit={onSubmit}\n submitLabel=\"Save Ticket Type\"\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\">Name</span>\n <Input value={form.name} onChange={(e) => onChange('name', e.target.value)} required />\n </label>\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Capacity</span>\n <Input\n type=\"number\"\n min=\"0\"\n value={form.capacity}\n onChange={(e) => onChange('capacity', 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\">Price</span>\n <Input\n type=\"number\"\n min=\"0\"\n step=\"0.01\"\n value={form.price}\n onChange={(e) => onChange('price', e.target.value)}\n required\n />\n </label>\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Currency</span>\n <Select value={form.currency} onValueChange={(value) => onChange('currency', value)}>\n <SelectTrigger aria-label=\"Currency\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {CURRENCY_OPTIONS.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\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\">Sales start</span>\n <Input\n type=\"datetime-local\"\n value={form.salesStartAt}\n onChange={(e) => onChange('salesStartAt', e.target.value)}\n />\n </label>\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Sales end</span>\n <Input\n type=\"datetime-local\"\n value={form.salesEndAt}\n onChange={(e) => onChange('salesEndAt', e.target.value)}\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={3} />\n </label>\n\n <div className=\"flex items-center justify-between rounded-card border border-border-seam p-3\">\n <div>\n <p className=\"text-sm font-medium text-text-primary\">Active</p>\n <p className=\"text-xs text-text-secondary\">Active ticket types are open for sale during their window.</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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"}
@@ -5,7 +5,7 @@ import { describeError as c, mutationSucceeded as l } from "../../lib/mutationRe
5
5
  import { useEvmSubscription as u } from "../../hooks/useEvmSubscription.js";
6
6
  import { useVirtualCall as oe } from "../../hooks/useVirtualCall.js";
7
7
  import { useCallback as se, useEffect as d, useMemo as f, useRef as p, useState as m } from "react";
8
- import { useParams as ce } from "react-router-dom";
8
+ import { useParams as ce } from "react-router";
9
9
  import { Fragment as le, jsx as h, jsxs as g } from "react/jsx-runtime";
10
10
  import { ArrowLeft as _, Mic as v, MicOff as y, PhoneOff as ue, Radio as de, ScreenShare as b, Users as x, Video as S, VideoOff as C } from "lucide-react";
11
11
  import { Badge as w, Button as T, EmphasisPanel as fe, GlassCard as E, GradientText as pe, IllustratedEmptyState as D, PagePurpose as me, Select as he, SelectContent as ge, SelectItem as O, SelectTrigger as _e, SelectValue as ve, Skeleton as k } from "@burdenoff/fe-libs/ui";
@@ -1 +1 @@
1
- {"version":3,"file":"VirtualSessionPage.js","names":[],"sources":["../../../../src/eventfullymanaged/pages/events/VirtualSessionPage.tsx"],"sourcesContent":["/* eslint-disable react-hooks/set-state-in-effect -- mirrors server-confirmed mute/video state into local toggle state, same established pattern as the fleet's *FormPage.tsx entity-hydration effects */\n/**\n * VirtualSessionPage (BOFF-4318)\n *\n * Real join/host flow for an event's virtual/hybrid session, backed by\n * `wspace-eventfullymanaged-svc`'s bounded browser-native WebRTC core\n * (`VirtualSessionService`). MEETING mode is a full mesh (cap 8);\n * WEBINAR mode is a host/presenter-broadcast star (cap 30) — attendees\n * only peer with the host/presenters, never each other. See\n * `useVirtualCall` for the exact topology and signaling logic.\n *\n * Large-audience broadcast (hundreds+ viewers) needs a real SFU/media\n * server and is explicit deferred follow-up — not built here.\n */\nimport {\n Badge,\n Button,\n EmphasisPanel,\n GlassCard,\n GradientText,\n IllustratedEmptyState,\n PagePurpose,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n Skeleton,\n} from '@burdenoff/fe-libs/ui';\nimport { ArrowLeft, Mic, MicOff, PhoneOff, Radio, ScreenShare, Users, Video, VideoOff } from 'lucide-react';\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { useParams } from 'react-router-dom';\n\nimport {\n useEndVirtualSessionMutation,\n useEventQuery,\n useHostVirtualSessionMutation,\n useJoinVirtualSessionMutation,\n useLeaveVirtualSessionMutation,\n usePromoteVirtualPresenterMutation,\n useSendVirtualSignalMutation,\n useSetVirtualMuteMutation,\n useSetVirtualVideoMutation,\n useVirtualSessionQuery,\n VIRTUAL_PARTICIPANTS_CHANGED_SUBSCRIPTION_SDL,\n VIRTUAL_SIGNAL_SUBSCRIPTION_SDL,\n type VirtualParticipantGql,\n} from '../../api/eventApi';\nimport { useEventfullyManaged } from '../../context/EventfullyManagedContext';\nimport { useEvmSubscription } from '../../hooks/useEvmSubscription';\nimport { type IncomingVirtualSignal, type VirtualCallRole, useVirtualCall } from '../../hooks/useVirtualCall';\nimport { describeError, mutationSucceeded } from '../../lib/mutationResult';\nimport { notify } from '../../lib/toast';\n\nfunction initials(id: string): string {\n return id.slice(0, 2).toUpperCase();\n}\n\nfunction connectionDotClass(state: RTCPeerConnectionState | undefined): string {\n if (state === 'connected') return 'bg-status-success';\n if (state === 'connecting' || state === 'new') return 'bg-status-warning';\n if (state === 'failed' || state === 'disconnected' || state === 'closed') return 'bg-status-danger';\n return 'bg-text-muted';\n}\n\ninterface VideoTileProps {\n actorId: string;\n role: VirtualCallRole;\n isLocal: boolean;\n stream: MediaStream | null;\n /** Muted/videoEnabled reflect the persisted participant state (server-confirmed for remotes, local toggle for self). */\n muted: boolean;\n videoEnabled: boolean;\n connectionState?: RTCPeerConnectionState;\n}\n\nfunction VideoTile({ actorId, role, isLocal, stream, muted, videoEnabled, connectionState }: VideoTileProps) {\n const videoRef = useRef<HTMLVideoElement>(null);\n useEffect(() => {\n if (videoRef.current) videoRef.current.srcObject = stream;\n }, [stream]);\n\n const showVideo = Boolean(stream) && videoEnabled;\n\n return (\n <div className=\"relative aspect-video overflow-hidden rounded-card border border-border-seam bg-bg-sunken\">\n {showVideo ? (\n <video\n ref={videoRef}\n autoPlay\n playsInline\n muted={isLocal}\n className=\"h-full w-full object-cover\"\n style={isLocal ? { transform: 'scaleX(-1)' } : undefined}\n />\n ) : (\n <div className=\"flex h-full w-full items-center justify-center\">\n <span className=\"flex h-12 w-12 items-center justify-center rounded-full bg-accent-primary/10 text-sm font-semibold text-accent-primary\">\n {initials(actorId)}\n </span>\n </div>\n )}\n <div className=\"absolute inset-x-0 bottom-0 flex items-center justify-between gap-2 bg-gradient-to-t from-black/60 to-transparent p-2\">\n <span className=\"truncate text-xs font-medium text-white\">\n {isLocal ? 'You' : `Participant ${actorId.slice(0, 6)}`}\n {role === 'HOST' ? ' · Host' : role === 'PRESENTER' ? ' · Presenter' : ''}\n </span>\n <span className=\"flex items-center gap-1.5\">\n {muted ? <MicOff className=\"h-3.5 w-3.5 text-white\" /> : null}\n {!isLocal ? (\n <span\n className={`h-2 w-2 rounded-full ${connectionDotClass(connectionState)}`}\n title={connectionState ?? 'connecting'}\n />\n ) : null}\n </span>\n </div>\n </div>\n );\n}\n\nexport default function VirtualSessionPage() {\n const { id: eventId = '' } = useParams<{ id: string }>();\n const { navigate, currentUser } = useEventfullyManaged();\n const selfActorId = currentUser?.id ?? '';\n\n const { data: eventData, loading: eventLoading } = useEventQuery(eventId);\n const sessionQuery = useVirtualSessionQuery({ eventId });\n const session = sessionQuery.data?.virtualSession ?? null;\n\n const [mode, setMode] = useState<'MEETING' | 'WEBINAR'>('MEETING');\n const [hostSession, { loading: hosting }] = useHostVirtualSessionMutation();\n const [joinSession, { loading: joining }] = useJoinVirtualSessionMutation();\n const [leaveSession, { loading: leaving }] = useLeaveVirtualSessionMutation();\n const [endSession, { loading: ending }] = useEndVirtualSessionMutation();\n const [setMuteMutation] = useSetVirtualMuteMutation();\n const [setVideoMutation] = useSetVirtualVideoMutation();\n const [promotePresenter] = usePromoteVirtualPresenterMutation();\n const [sendSignalMutation] = useSendVirtualSignalMutation();\n\n const selfParticipant = useMemo(\n () => session?.participants.find((p) => p.actorId === selfActorId && !p.leftAt) ?? null,\n [session, selfActorId]\n );\n const activeParticipants = useMemo(() => (session?.participants ?? []).filter((p) => !p.leftAt), [session]);\n\n const [localMuted, setLocalMuted] = useState(false);\n const [localVideoOff, setLocalVideoOff] = useState(false);\n useEffect(() => {\n if (selfParticipant) {\n setLocalMuted(selfParticipant.muted);\n setLocalVideoOff(!selfParticipant.videoEnabled);\n }\n }, [selfParticipant?.muted, selfParticipant?.videoEnabled]); // eslint-disable-line react-hooks/exhaustive-deps\n\n const sendSignal = useCallback(\n async (args: { toActorId: string; signalType: 'OFFER' | 'ANSWER' | 'ICE_CANDIDATE'; payload: unknown }) => {\n if (!session) return;\n await sendSignalMutation({\n variables: {\n sessionId: session.id,\n toActorId: args.toActorId,\n signalType: args.signalType,\n payload: args.payload,\n },\n });\n },\n [session, sendSignalMutation]\n );\n\n const call = useVirtualCall({\n sessionId: selfParticipant ? session?.id : null,\n selfActorId,\n mode: session?.mode ?? 'MEETING',\n selfRole: selfParticipant?.role ?? null,\n participants: activeParticipants.map((p) => ({ actorId: p.actorId, role: p.role })),\n sendSignal,\n muted: localMuted,\n videoEnabled: !localVideoOff,\n });\n\n useEvmSubscription<{ eventfullyManagedVirtualSignal: IncomingVirtualSignal }>({\n query: VIRTUAL_SIGNAL_SUBSCRIPTION_SDL,\n variables: { sessionId: session?.id },\n operationName: 'EventfullymanagedVirtualSignal',\n enabled: Boolean(selfParticipant && session),\n onData: (data) => {\n const signal = data.eventfullyManagedVirtualSignal;\n if (signal) call.handleIncomingSignal(signal);\n },\n // No real polling substitute exists for signaling — a missed signal is\n // simply a missed negotiation step. The fallback here just retries the\n // subscription's own reconnect path via re-render, kept as a no-op.\n pollFn: () => {},\n pollIntervalMs: 15_000,\n });\n\n useEvmSubscription<{ eventfullyManagedVirtualParticipantsChanged: unknown }>({\n query: VIRTUAL_PARTICIPANTS_CHANGED_SUBSCRIPTION_SDL,\n variables: { sessionId: session?.id },\n operationName: 'EventfullymanagedVirtualParticipantsChanged',\n enabled: Boolean(session),\n onData: () => {\n void sessionQuery.refetch();\n },\n pollFn: () => {\n void sessionQuery.refetch();\n },\n pollIntervalMs: 5_000,\n });\n\n const onHost = async (chosenMode: 'MEETING' | 'WEBINAR') => {\n try {\n const response = await hostSession({ variables: { eventId, mode: chosenMode } });\n if (!mutationSucceeded(response, response.data?.hostVirtualSession?.id, 'Could not start session')) {\n return;\n }\n notify.success('Session started', chosenMode === 'WEBINAR' ? 'Hosting as a webinar.' : 'Hosting a meeting.');\n await sessionQuery.refetch();\n } catch (error) {\n notify.error('Could not start session', describeError(error));\n }\n };\n\n const onJoin = async () => {\n try {\n const response = await joinSession({ variables: { eventId } });\n if (!mutationSucceeded(response, response.data?.joinVirtualSession?.id, 'Could not join session')) {\n return;\n }\n await sessionQuery.refetch();\n } catch (error) {\n notify.error('Could not join session', describeError(error));\n }\n };\n\n const onLeave = async () => {\n if (!session) return;\n try {\n await leaveSession({ variables: { sessionId: session.id } });\n notify.success('Left session', 'You are no longer in the virtual session.');\n await sessionQuery.refetch();\n } catch (error) {\n notify.error('Could not leave session', describeError(error));\n }\n };\n\n const onEnd = async () => {\n if (!session) return;\n try {\n await endSession({ variables: { sessionId: session.id } });\n notify.success('Session ended', 'The virtual session has been ended for everyone.');\n await sessionQuery.refetch();\n } catch (error) {\n notify.error('Could not end session', describeError(error));\n }\n };\n\n const toggleMute = async () => {\n if (!session) return;\n const next = !localMuted;\n setLocalMuted(next);\n try {\n await setMuteMutation({ variables: { sessionId: session.id, muted: next } });\n } catch (error) {\n setLocalMuted(!next);\n notify.error('Could not update mute state', describeError(error));\n }\n };\n\n const toggleVideo = async () => {\n if (!session) return;\n const next = !localVideoOff;\n setLocalVideoOff(next);\n try {\n await setVideoMutation({ variables: { sessionId: session.id, videoEnabled: !next } });\n } catch (error) {\n setLocalVideoOff(!next);\n notify.error('Could not update camera state', describeError(error));\n }\n };\n\n const onPromote = async (participant: VirtualParticipantGql) => {\n if (!session) return;\n try {\n await promotePresenter({ variables: { sessionId: session.id, actorId: participant.actorId } });\n notify.success('Promoted to presenter', `Participant ${participant.actorId.slice(0, 6)} can now broadcast.`);\n await sessionQuery.refetch();\n } catch (error) {\n notify.error('Could not promote participant', describeError(error));\n }\n };\n\n const isHost = selfParticipant?.role === 'HOST';\n const busy = hosting || joining || leaving || ending;\n\n if (eventLoading || sessionQuery.loading) {\n return (\n <div className=\"space-y-4 p-6\">\n <Skeleton className=\"h-8 w-64\" />\n <Skeleton className=\"h-64 w-full\" />\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6 p-6\">\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => navigate(`/events/${eventId}`)}>\n <ArrowLeft className=\"h-4 w-4\" />\n Back to Event\n </Button>\n\n <PagePurpose>\n A virtual session is a real, browser-native WebRTC room for this event — no external meeting link. Hosts start a\n Meeting (small mesh, up to 8 people all connected directly) or a Webinar (up to 30 attendees who see and hear\n only the host/presenters). Signaling is relayed through EventfullyManaged; media never touches our servers.\n </PagePurpose>\n\n <EmphasisPanel 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\">\n <GradientText>Virtual Session</GradientText>\n </h1>\n <p className=\"mt-1 text-sm text-text-secondary\">{eventData?.event?.name ?? 'Event'}</p>\n </div>\n {session ? (\n <Badge variant={session.status === 'ACTIVE' ? 'success' : 'secondary'} className=\"uppercase\">\n {session.mode} · {session.status}\n </Badge>\n ) : null}\n </EmphasisPanel>\n\n {!session || session.status !== 'ACTIVE' ? (\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-8\">\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"No live virtual session\"\n description=\"Start a session to host attendees over WebRTC, or join once someone else has started one.\"\n className=\"py-6\"\n />\n <div className=\"mt-4 flex flex-wrap items-center justify-center gap-2\">\n <Select value={mode} onValueChange={(value) => setMode(value as 'MEETING' | 'WEBINAR')}>\n <SelectTrigger aria-label=\"Session mode\" className=\"w-48\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"MEETING\">Meeting (up to 8)</SelectItem>\n <SelectItem value=\"WEBINAR\">Webinar (up to 30)</SelectItem>\n </SelectContent>\n </Select>\n <Button type=\"button\" onClick={() => onHost(mode)} disabled={busy}>\n <Radio className=\"h-4 w-4\" />\n {hosting ? 'Starting…' : 'Start session'}\n </Button>\n </div>\n </GlassCard>\n ) : !selfParticipant ? (\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-8\">\n <IllustratedEmptyState\n illustration=\"empty-people\"\n title=\"A session is live\"\n description={`${session.participantCount} of ${session.maxParticipants} seats in use. Join to turn on your camera and mic.`}\n className=\"py-6\"\n />\n <div className=\"mt-4 flex justify-center\">\n <Button type=\"button\" onClick={onJoin} disabled={busy}>\n <Video className=\"h-4 w-4\" />\n {joining ? 'Joining…' : 'Join session'}\n </Button>\n </div>\n </GlassCard>\n ) : (\n <>\n {call.mediaError ? (\n <GlassCard\n treatment=\"plain\"\n className=\"border-status-danger/40 bg-status-danger/5 p-4 text-sm text-status-danger\"\n >\n Camera/microphone unavailable: {call.mediaError}. You are still connected to the session and can see other\n participants, but others will not see or hear you.\n </GlassCard>\n ) : null}\n\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3\">\n <VideoTile\n actorId={selfActorId}\n role={selfParticipant.role}\n isLocal\n stream={call.localStream}\n muted={localMuted}\n videoEnabled={!localVideoOff}\n />\n {activeParticipants\n .filter((p) => p.actorId !== selfActorId)\n .map((participant) => (\n <VideoTile\n key={participant.actorId}\n actorId={participant.actorId}\n role={participant.role}\n isLocal={false}\n stream={call.remoteStreams.get(participant.actorId) ?? null}\n muted={participant.muted}\n videoEnabled={participant.videoEnabled}\n connectionState={call.connectionStates.get(participant.actorId)}\n />\n ))}\n </div>\n\n <GlassCard\n treatment=\"plain\"\n className=\"flex flex-wrap items-center justify-center gap-2 border-border-seam p-4\"\n >\n <Button type=\"button\" variant={localMuted ? 'outline' : 'secondary'} onClick={toggleMute}>\n {localMuted ? <MicOff className=\"h-4 w-4\" /> : <Mic className=\"h-4 w-4\" />}\n {localMuted ? 'Unmute' : 'Mute'}\n </Button>\n <Button type=\"button\" variant={localVideoOff ? 'outline' : 'secondary'} onClick={toggleVideo}>\n {localVideoOff ? <VideoOff className=\"h-4 w-4\" /> : <Video className=\"h-4 w-4\" />}\n {localVideoOff ? 'Start Video' : 'Stop Video'}\n </Button>\n <Button type=\"button\" variant=\"outline\" onClick={onLeave} disabled={busy}>\n <PhoneOff className=\"h-4 w-4\" />\n {leaving ? 'Leaving…' : 'Leave'}\n </Button>\n {isHost ? (\n <Button type=\"button\" variant=\"destructive\" onClick={onEnd} disabled={busy}>\n <ScreenShare className=\"h-4 w-4\" />\n {ending ? 'Ending…' : 'End for everyone'}\n </Button>\n ) : null}\n </GlassCard>\n\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-4\">\n <h2 className=\"flex items-center gap-2 text-sm font-semibold text-text-primary\">\n <Users className=\"h-4 w-4 text-accent-primary\" />\n Participants ({activeParticipants.length}/{session.maxParticipants})\n </h2>\n <div className=\"mt-3 space-y-2\">\n {activeParticipants.map((participant) => (\n <div key={participant.id} className=\"flex items-center justify-between gap-2 text-sm\">\n <span className=\"flex items-center gap-2 text-text-secondary\">\n <span\n className={`h-2 w-2 rounded-full ${\n participant.actorId === selfActorId\n ? 'bg-status-success'\n : connectionDotClass(call.connectionStates.get(participant.actorId))\n }`}\n />\n {participant.actorId === selfActorId ? 'You' : `Participant ${participant.actorId.slice(0, 6)}`}\n <Badge variant=\"secondary\" className=\"uppercase\">\n {participant.role}\n </Badge>\n </span>\n {isHost && session.mode === 'WEBINAR' && participant.role === 'ATTENDEE' ? (\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => onPromote(participant)}>\n Promote to presenter\n </Button>\n ) : null}\n </div>\n ))}\n </div>\n </GlassCard>\n </>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"VirtualSessionPage.js","names":[],"sources":["../../../../src/eventfullymanaged/pages/events/VirtualSessionPage.tsx"],"sourcesContent":["/* eslint-disable react-hooks/set-state-in-effect -- mirrors server-confirmed mute/video state into local toggle state, same established pattern as the fleet's *FormPage.tsx entity-hydration effects */\n/**\n * VirtualSessionPage (BOFF-4318)\n *\n * Real join/host flow for an event's virtual/hybrid session, backed by\n * `wspace-eventfullymanaged-svc`'s bounded browser-native WebRTC core\n * (`VirtualSessionService`). MEETING mode is a full mesh (cap 8);\n * WEBINAR mode is a host/presenter-broadcast star (cap 30) — attendees\n * only peer with the host/presenters, never each other. See\n * `useVirtualCall` for the exact topology and signaling logic.\n *\n * Large-audience broadcast (hundreds+ viewers) needs a real SFU/media\n * server and is explicit deferred follow-up — not built here.\n */\nimport {\n Badge,\n Button,\n EmphasisPanel,\n GlassCard,\n GradientText,\n IllustratedEmptyState,\n PagePurpose,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n Skeleton,\n} from '@burdenoff/fe-libs/ui';\nimport { ArrowLeft, Mic, MicOff, PhoneOff, Radio, ScreenShare, Users, Video, VideoOff } from 'lucide-react';\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { useParams } from 'react-router';\n\nimport {\n useEndVirtualSessionMutation,\n useEventQuery,\n useHostVirtualSessionMutation,\n useJoinVirtualSessionMutation,\n useLeaveVirtualSessionMutation,\n usePromoteVirtualPresenterMutation,\n useSendVirtualSignalMutation,\n useSetVirtualMuteMutation,\n useSetVirtualVideoMutation,\n useVirtualSessionQuery,\n VIRTUAL_PARTICIPANTS_CHANGED_SUBSCRIPTION_SDL,\n VIRTUAL_SIGNAL_SUBSCRIPTION_SDL,\n type VirtualParticipantGql,\n} from '../../api/eventApi';\nimport { useEventfullyManaged } from '../../context/EventfullyManagedContext';\nimport { useEvmSubscription } from '../../hooks/useEvmSubscription';\nimport { type IncomingVirtualSignal, type VirtualCallRole, useVirtualCall } from '../../hooks/useVirtualCall';\nimport { describeError, mutationSucceeded } from '../../lib/mutationResult';\nimport { notify } from '../../lib/toast';\n\nfunction initials(id: string): string {\n return id.slice(0, 2).toUpperCase();\n}\n\nfunction connectionDotClass(state: RTCPeerConnectionState | undefined): string {\n if (state === 'connected') return 'bg-status-success';\n if (state === 'connecting' || state === 'new') return 'bg-status-warning';\n if (state === 'failed' || state === 'disconnected' || state === 'closed') return 'bg-status-danger';\n return 'bg-text-muted';\n}\n\ninterface VideoTileProps {\n actorId: string;\n role: VirtualCallRole;\n isLocal: boolean;\n stream: MediaStream | null;\n /** Muted/videoEnabled reflect the persisted participant state (server-confirmed for remotes, local toggle for self). */\n muted: boolean;\n videoEnabled: boolean;\n connectionState?: RTCPeerConnectionState;\n}\n\nfunction VideoTile({ actorId, role, isLocal, stream, muted, videoEnabled, connectionState }: VideoTileProps) {\n const videoRef = useRef<HTMLVideoElement>(null);\n useEffect(() => {\n if (videoRef.current) videoRef.current.srcObject = stream;\n }, [stream]);\n\n const showVideo = Boolean(stream) && videoEnabled;\n\n return (\n <div className=\"relative aspect-video overflow-hidden rounded-card border border-border-seam bg-bg-sunken\">\n {showVideo ? (\n <video\n ref={videoRef}\n autoPlay\n playsInline\n muted={isLocal}\n className=\"h-full w-full object-cover\"\n style={isLocal ? { transform: 'scaleX(-1)' } : undefined}\n />\n ) : (\n <div className=\"flex h-full w-full items-center justify-center\">\n <span className=\"flex h-12 w-12 items-center justify-center rounded-full bg-accent-primary/10 text-sm font-semibold text-accent-primary\">\n {initials(actorId)}\n </span>\n </div>\n )}\n <div className=\"absolute inset-x-0 bottom-0 flex items-center justify-between gap-2 bg-gradient-to-t from-black/60 to-transparent p-2\">\n <span className=\"truncate text-xs font-medium text-white\">\n {isLocal ? 'You' : `Participant ${actorId.slice(0, 6)}`}\n {role === 'HOST' ? ' · Host' : role === 'PRESENTER' ? ' · Presenter' : ''}\n </span>\n <span className=\"flex items-center gap-1.5\">\n {muted ? <MicOff className=\"h-3.5 w-3.5 text-white\" /> : null}\n {!isLocal ? (\n <span\n className={`h-2 w-2 rounded-full ${connectionDotClass(connectionState)}`}\n title={connectionState ?? 'connecting'}\n />\n ) : null}\n </span>\n </div>\n </div>\n );\n}\n\nexport default function VirtualSessionPage() {\n const { id: eventId = '' } = useParams<{ id: string }>();\n const { navigate, currentUser } = useEventfullyManaged();\n const selfActorId = currentUser?.id ?? '';\n\n const { data: eventData, loading: eventLoading } = useEventQuery(eventId);\n const sessionQuery = useVirtualSessionQuery({ eventId });\n const session = sessionQuery.data?.virtualSession ?? null;\n\n const [mode, setMode] = useState<'MEETING' | 'WEBINAR'>('MEETING');\n const [hostSession, { loading: hosting }] = useHostVirtualSessionMutation();\n const [joinSession, { loading: joining }] = useJoinVirtualSessionMutation();\n const [leaveSession, { loading: leaving }] = useLeaveVirtualSessionMutation();\n const [endSession, { loading: ending }] = useEndVirtualSessionMutation();\n const [setMuteMutation] = useSetVirtualMuteMutation();\n const [setVideoMutation] = useSetVirtualVideoMutation();\n const [promotePresenter] = usePromoteVirtualPresenterMutation();\n const [sendSignalMutation] = useSendVirtualSignalMutation();\n\n const selfParticipant = useMemo(\n () => session?.participants.find((p) => p.actorId === selfActorId && !p.leftAt) ?? null,\n [session, selfActorId]\n );\n const activeParticipants = useMemo(() => (session?.participants ?? []).filter((p) => !p.leftAt), [session]);\n\n const [localMuted, setLocalMuted] = useState(false);\n const [localVideoOff, setLocalVideoOff] = useState(false);\n useEffect(() => {\n if (selfParticipant) {\n setLocalMuted(selfParticipant.muted);\n setLocalVideoOff(!selfParticipant.videoEnabled);\n }\n }, [selfParticipant?.muted, selfParticipant?.videoEnabled]); // eslint-disable-line react-hooks/exhaustive-deps\n\n const sendSignal = useCallback(\n async (args: { toActorId: string; signalType: 'OFFER' | 'ANSWER' | 'ICE_CANDIDATE'; payload: unknown }) => {\n if (!session) return;\n await sendSignalMutation({\n variables: {\n sessionId: session.id,\n toActorId: args.toActorId,\n signalType: args.signalType,\n payload: args.payload,\n },\n });\n },\n [session, sendSignalMutation]\n );\n\n const call = useVirtualCall({\n sessionId: selfParticipant ? session?.id : null,\n selfActorId,\n mode: session?.mode ?? 'MEETING',\n selfRole: selfParticipant?.role ?? null,\n participants: activeParticipants.map((p) => ({ actorId: p.actorId, role: p.role })),\n sendSignal,\n muted: localMuted,\n videoEnabled: !localVideoOff,\n });\n\n useEvmSubscription<{ eventfullyManagedVirtualSignal: IncomingVirtualSignal }>({\n query: VIRTUAL_SIGNAL_SUBSCRIPTION_SDL,\n variables: { sessionId: session?.id },\n operationName: 'EventfullymanagedVirtualSignal',\n enabled: Boolean(selfParticipant && session),\n onData: (data) => {\n const signal = data.eventfullyManagedVirtualSignal;\n if (signal) call.handleIncomingSignal(signal);\n },\n // No real polling substitute exists for signaling — a missed signal is\n // simply a missed negotiation step. The fallback here just retries the\n // subscription's own reconnect path via re-render, kept as a no-op.\n pollFn: () => {},\n pollIntervalMs: 15_000,\n });\n\n useEvmSubscription<{ eventfullyManagedVirtualParticipantsChanged: unknown }>({\n query: VIRTUAL_PARTICIPANTS_CHANGED_SUBSCRIPTION_SDL,\n variables: { sessionId: session?.id },\n operationName: 'EventfullymanagedVirtualParticipantsChanged',\n enabled: Boolean(session),\n onData: () => {\n void sessionQuery.refetch();\n },\n pollFn: () => {\n void sessionQuery.refetch();\n },\n pollIntervalMs: 5_000,\n });\n\n const onHost = async (chosenMode: 'MEETING' | 'WEBINAR') => {\n try {\n const response = await hostSession({ variables: { eventId, mode: chosenMode } });\n if (!mutationSucceeded(response, response.data?.hostVirtualSession?.id, 'Could not start session')) {\n return;\n }\n notify.success('Session started', chosenMode === 'WEBINAR' ? 'Hosting as a webinar.' : 'Hosting a meeting.');\n await sessionQuery.refetch();\n } catch (error) {\n notify.error('Could not start session', describeError(error));\n }\n };\n\n const onJoin = async () => {\n try {\n const response = await joinSession({ variables: { eventId } });\n if (!mutationSucceeded(response, response.data?.joinVirtualSession?.id, 'Could not join session')) {\n return;\n }\n await sessionQuery.refetch();\n } catch (error) {\n notify.error('Could not join session', describeError(error));\n }\n };\n\n const onLeave = async () => {\n if (!session) return;\n try {\n await leaveSession({ variables: { sessionId: session.id } });\n notify.success('Left session', 'You are no longer in the virtual session.');\n await sessionQuery.refetch();\n } catch (error) {\n notify.error('Could not leave session', describeError(error));\n }\n };\n\n const onEnd = async () => {\n if (!session) return;\n try {\n await endSession({ variables: { sessionId: session.id } });\n notify.success('Session ended', 'The virtual session has been ended for everyone.');\n await sessionQuery.refetch();\n } catch (error) {\n notify.error('Could not end session', describeError(error));\n }\n };\n\n const toggleMute = async () => {\n if (!session) return;\n const next = !localMuted;\n setLocalMuted(next);\n try {\n await setMuteMutation({ variables: { sessionId: session.id, muted: next } });\n } catch (error) {\n setLocalMuted(!next);\n notify.error('Could not update mute state', describeError(error));\n }\n };\n\n const toggleVideo = async () => {\n if (!session) return;\n const next = !localVideoOff;\n setLocalVideoOff(next);\n try {\n await setVideoMutation({ variables: { sessionId: session.id, videoEnabled: !next } });\n } catch (error) {\n setLocalVideoOff(!next);\n notify.error('Could not update camera state', describeError(error));\n }\n };\n\n const onPromote = async (participant: VirtualParticipantGql) => {\n if (!session) return;\n try {\n await promotePresenter({ variables: { sessionId: session.id, actorId: participant.actorId } });\n notify.success('Promoted to presenter', `Participant ${participant.actorId.slice(0, 6)} can now broadcast.`);\n await sessionQuery.refetch();\n } catch (error) {\n notify.error('Could not promote participant', describeError(error));\n }\n };\n\n const isHost = selfParticipant?.role === 'HOST';\n const busy = hosting || joining || leaving || ending;\n\n if (eventLoading || sessionQuery.loading) {\n return (\n <div className=\"space-y-4 p-6\">\n <Skeleton className=\"h-8 w-64\" />\n <Skeleton className=\"h-64 w-full\" />\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6 p-6\">\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => navigate(`/events/${eventId}`)}>\n <ArrowLeft className=\"h-4 w-4\" />\n Back to Event\n </Button>\n\n <PagePurpose>\n A virtual session is a real, browser-native WebRTC room for this event — no external meeting link. Hosts start a\n Meeting (small mesh, up to 8 people all connected directly) or a Webinar (up to 30 attendees who see and hear\n only the host/presenters). Signaling is relayed through EventfullyManaged; media never touches our servers.\n </PagePurpose>\n\n <EmphasisPanel 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\">\n <GradientText>Virtual Session</GradientText>\n </h1>\n <p className=\"mt-1 text-sm text-text-secondary\">{eventData?.event?.name ?? 'Event'}</p>\n </div>\n {session ? (\n <Badge variant={session.status === 'ACTIVE' ? 'success' : 'secondary'} className=\"uppercase\">\n {session.mode} · {session.status}\n </Badge>\n ) : null}\n </EmphasisPanel>\n\n {!session || session.status !== 'ACTIVE' ? (\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-8\">\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"No live virtual session\"\n description=\"Start a session to host attendees over WebRTC, or join once someone else has started one.\"\n className=\"py-6\"\n />\n <div className=\"mt-4 flex flex-wrap items-center justify-center gap-2\">\n <Select value={mode} onValueChange={(value) => setMode(value as 'MEETING' | 'WEBINAR')}>\n <SelectTrigger aria-label=\"Session mode\" className=\"w-48\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"MEETING\">Meeting (up to 8)</SelectItem>\n <SelectItem value=\"WEBINAR\">Webinar (up to 30)</SelectItem>\n </SelectContent>\n </Select>\n <Button type=\"button\" onClick={() => onHost(mode)} disabled={busy}>\n <Radio className=\"h-4 w-4\" />\n {hosting ? 'Starting…' : 'Start session'}\n </Button>\n </div>\n </GlassCard>\n ) : !selfParticipant ? (\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-8\">\n <IllustratedEmptyState\n illustration=\"empty-people\"\n title=\"A session is live\"\n description={`${session.participantCount} of ${session.maxParticipants} seats in use. Join to turn on your camera and mic.`}\n className=\"py-6\"\n />\n <div className=\"mt-4 flex justify-center\">\n <Button type=\"button\" onClick={onJoin} disabled={busy}>\n <Video className=\"h-4 w-4\" />\n {joining ? 'Joining…' : 'Join session'}\n </Button>\n </div>\n </GlassCard>\n ) : (\n <>\n {call.mediaError ? (\n <GlassCard\n treatment=\"plain\"\n className=\"border-status-danger/40 bg-status-danger/5 p-4 text-sm text-status-danger\"\n >\n Camera/microphone unavailable: {call.mediaError}. You are still connected to the session and can see other\n participants, but others will not see or hear you.\n </GlassCard>\n ) : null}\n\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3\">\n <VideoTile\n actorId={selfActorId}\n role={selfParticipant.role}\n isLocal\n stream={call.localStream}\n muted={localMuted}\n videoEnabled={!localVideoOff}\n />\n {activeParticipants\n .filter((p) => p.actorId !== selfActorId)\n .map((participant) => (\n <VideoTile\n key={participant.actorId}\n actorId={participant.actorId}\n role={participant.role}\n isLocal={false}\n stream={call.remoteStreams.get(participant.actorId) ?? null}\n muted={participant.muted}\n videoEnabled={participant.videoEnabled}\n connectionState={call.connectionStates.get(participant.actorId)}\n />\n ))}\n </div>\n\n <GlassCard\n treatment=\"plain\"\n className=\"flex flex-wrap items-center justify-center gap-2 border-border-seam p-4\"\n >\n <Button type=\"button\" variant={localMuted ? 'outline' : 'secondary'} onClick={toggleMute}>\n {localMuted ? <MicOff className=\"h-4 w-4\" /> : <Mic className=\"h-4 w-4\" />}\n {localMuted ? 'Unmute' : 'Mute'}\n </Button>\n <Button type=\"button\" variant={localVideoOff ? 'outline' : 'secondary'} onClick={toggleVideo}>\n {localVideoOff ? <VideoOff className=\"h-4 w-4\" /> : <Video className=\"h-4 w-4\" />}\n {localVideoOff ? 'Start Video' : 'Stop Video'}\n </Button>\n <Button type=\"button\" variant=\"outline\" onClick={onLeave} disabled={busy}>\n <PhoneOff className=\"h-4 w-4\" />\n {leaving ? 'Leaving…' : 'Leave'}\n </Button>\n {isHost ? (\n <Button type=\"button\" variant=\"destructive\" onClick={onEnd} disabled={busy}>\n <ScreenShare className=\"h-4 w-4\" />\n {ending ? 'Ending…' : 'End for everyone'}\n </Button>\n ) : null}\n </GlassCard>\n\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-4\">\n <h2 className=\"flex items-center gap-2 text-sm font-semibold text-text-primary\">\n <Users className=\"h-4 w-4 text-accent-primary\" />\n Participants ({activeParticipants.length}/{session.maxParticipants})\n </h2>\n <div className=\"mt-3 space-y-2\">\n {activeParticipants.map((participant) => (\n <div key={participant.id} className=\"flex items-center justify-between gap-2 text-sm\">\n <span className=\"flex items-center gap-2 text-text-secondary\">\n <span\n className={`h-2 w-2 rounded-full ${\n participant.actorId === selfActorId\n ? 'bg-status-success'\n : connectionDotClass(call.connectionStates.get(participant.actorId))\n }`}\n />\n {participant.actorId === selfActorId ? 'You' : `Participant ${participant.actorId.slice(0, 6)}`}\n <Badge variant=\"secondary\" className=\"uppercase\">\n {participant.role}\n </Badge>\n </span>\n {isHost && session.mode === 'WEBINAR' && participant.role === 'ATTENDEE' ? (\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => onPromote(participant)}>\n Promote to presenter\n </Button>\n ) : null}\n </div>\n ))}\n </div>\n </GlassCard>\n </>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -5,7 +5,7 @@ import { EntityFormShell as o } from "../../components/EntityFormShell.js";
5
5
  import { describeError as s, mutationSucceeded as c } from "../../lib/mutationResult.js";
6
6
  import { safeUrl as l } from "../../components/SafeLink.js";
7
7
  import { useEffect as u, useMemo as d, useState as f } from "react";
8
- import { useParams as p } from "react-router-dom";
8
+ import { useParams as p } from "react-router";
9
9
  import { jsx as m, jsxs as h } from "react/jsx-runtime";
10
10
  import { Input as g, Select as _, SelectContent as v, SelectItem as y, SelectTrigger as b, SelectValue as x } from "@burdenoff/fe-libs/ui";
11
11
  //#region src/eventfullymanaged/pages/exhibitors/ExhibitorFormPage.tsx
@@ -1 +1 @@
1
- {"version":3,"file":"ExhibitorFormPage.js","names":[],"sources":["../../../../src/eventfullymanaged/pages/exhibitors/ExhibitorFormPage.tsx"],"sourcesContent":["/* eslint-disable react-hooks/set-state-in-effect */\nimport { Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@burdenoff/fe-libs/ui';\nimport { useEffect, useMemo, useState } from 'react';\nimport { useParams } from 'react-router-dom';\n\nimport {\n useCreateExhibitorMutation,\n useEventsQuery,\n useExhibitorsQuery,\n useUpdateExhibitorMutation,\n} from '../../api/eventApi';\nimport { EntityFormShell } from '../../components/EntityFormShell';\nimport { safeUrl } from '../../components/SafeLink';\nimport { useEventfullyManaged } from '../../context/EventfullyManagedContext';\nimport { describeError, mutationSucceeded } from '../../lib/mutationResult';\nimport { notify } from '../../lib/toast';\n\ntype ExhibitorFormState = {\n eventId: string;\n name: string;\n boothCode: string;\n category: string;\n status: string;\n website: string;\n contactName: string;\n contactEmail: string;\n};\n\nconst initialState: ExhibitorFormState = {\n eventId: '',\n name: '',\n boothCode: '',\n category: '',\n status: 'CONFIRMED',\n website: '',\n contactName: '',\n contactEmail: '',\n};\n\nconst STATUS_OPTIONS = [\n { value: 'INVITED', label: 'Invited' },\n { value: 'CONFIRMED', label: 'Confirmed' },\n { value: 'ACTIVE', label: 'Active' },\n { value: 'CANCELLED', label: 'Cancelled' },\n];\n\nexport default function ExhibitorFormPage() {\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 resolver exists for exhibitors, so the edit initial-state is derived\n // from the list query row.\n const { data: exhibitorsData } = useExhibitorsQuery({ limit: 100, offset: 0 });\n\n const existing = useMemo(\n () => (exhibitorsData?.exhibitors?.items ?? []).find((item) => item.id === id),\n [exhibitorsData, id]\n );\n\n const [createExhibitor, { loading: creating }] = useCreateExhibitorMutation();\n const [updateExhibitor, { loading: updating }] = useUpdateExhibitorMutation();\n const [form, setForm] = useState<ExhibitorFormState>(initialState);\n\n useEffect(() => {\n if (!isEdit || !existing) {\n return;\n }\n const metadata = existing.metadata ?? {};\n const website = typeof metadata.website === 'string' ? metadata.website : '';\n setForm({\n eventId: existing.eventId ?? '',\n name: existing.name ?? '',\n boothCode: existing.boothCode ?? '',\n category: existing.category ?? '',\n status: existing.status ?? 'CONFIRMED',\n website,\n contactName: existing.contactName ?? '',\n contactEmail: existing.contactEmail ?? '',\n });\n }, [existing, isEdit]);\n\n const onChange = (field: keyof ExhibitorFormState, 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.eventId) {\n notify.error('Event required', 'Choose the event this exhibitor belongs to.');\n return;\n }\n\n // The website is free text persisted verbatim, so normalise/reject it before\n // it is stored and later rendered as a link.\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 existingMetadata = existing?.metadata ?? {};\n const input = {\n eventId: form.eventId,\n name: form.name,\n boothCode: form.boothCode,\n category: form.category,\n status: form.status,\n contactName: form.contactName,\n contactEmail: form.contactEmail,\n metadata: { ...existingMetadata, website: normalizedWebsite ?? '' },\n };\n\n try {\n if (isEdit) {\n const response = await updateExhibitor({ variables: { id, input } });\n if (!mutationSucceeded(response, response.data?.updateExhibitor?.id, 'Could not update exhibitor')) {\n return;\n }\n navigate('/exhibitors');\n return;\n }\n\n const response = await createExhibitor({ variables: { input } });\n if (!mutationSucceeded(response, response.data?.createExhibitor?.id, 'Could not create exhibitor')) {\n return;\n }\n navigate('/exhibitors');\n } catch (error) {\n notify.error('Could not save exhibitor', describeError(error));\n }\n };\n\n const saving = creating || updating;\n const events = eventsData?.events?.items ?? [];\n\n return (\n <EntityFormShell\n title={isEdit ? 'Edit Exhibitor' : 'New Exhibitor'}\n description=\"Manage a booth exhibitor with live backend persistence.\"\n purpose=\"Register a company staffing a booth: link it to the event, give it a booth code and category, and record who to contact. Once saved, capture the leads their booth scans from the exhibitor list.\"\n backLabel=\"Back to Exhibitors\"\n onBack={() => navigate('/exhibitors')}\n onSubmit={onSubmit}\n submitLabel={isEdit ? 'Save Exhibitor' : 'Create Exhibitor'}\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\">Name</span>\n <Input value={form.name} onChange={(e) => onChange('name', e.target.value)} required />\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\">Booth Code</span>\n <Input value={form.boothCode} onChange={(e) => onChange('boothCode', e.target.value)} />\n </label>\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Category</span>\n <Input value={form.category} onChange={(e) => onChange('category', e.target.value)} />\n </label>\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Status</span>\n <Select value={form.status} onValueChange={(value) => onChange('status', value)}>\n <SelectTrigger aria-label=\"Status\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {STATUS_OPTIONS.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </label>\n </div>\n\n <label className=\"block space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Website</span>\n <Input\n value={form.website}\n onChange={(e) => onChange('website', e.target.value)}\n placeholder=\"https://example.com\"\n />\n </label>\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\">Contact Name</span>\n <Input value={form.contactName} onChange={(e) => onChange('contactName', e.target.value)} />\n </label>\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Contact Email</span>\n <Input type=\"email\" value={form.contactEmail} onChange={(e) => onChange('contactEmail', e.target.value)} />\n </label>\n </div>\n </EntityFormShell>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ExhibitorFormPage.js","names":[],"sources":["../../../../src/eventfullymanaged/pages/exhibitors/ExhibitorFormPage.tsx"],"sourcesContent":["/* eslint-disable react-hooks/set-state-in-effect */\nimport { Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@burdenoff/fe-libs/ui';\nimport { useEffect, useMemo, useState } from 'react';\nimport { useParams } from 'react-router';\n\nimport {\n useCreateExhibitorMutation,\n useEventsQuery,\n useExhibitorsQuery,\n useUpdateExhibitorMutation,\n} from '../../api/eventApi';\nimport { EntityFormShell } from '../../components/EntityFormShell';\nimport { safeUrl } from '../../components/SafeLink';\nimport { useEventfullyManaged } from '../../context/EventfullyManagedContext';\nimport { describeError, mutationSucceeded } from '../../lib/mutationResult';\nimport { notify } from '../../lib/toast';\n\ntype ExhibitorFormState = {\n eventId: string;\n name: string;\n boothCode: string;\n category: string;\n status: string;\n website: string;\n contactName: string;\n contactEmail: string;\n};\n\nconst initialState: ExhibitorFormState = {\n eventId: '',\n name: '',\n boothCode: '',\n category: '',\n status: 'CONFIRMED',\n website: '',\n contactName: '',\n contactEmail: '',\n};\n\nconst STATUS_OPTIONS = [\n { value: 'INVITED', label: 'Invited' },\n { value: 'CONFIRMED', label: 'Confirmed' },\n { value: 'ACTIVE', label: 'Active' },\n { value: 'CANCELLED', label: 'Cancelled' },\n];\n\nexport default function ExhibitorFormPage() {\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 resolver exists for exhibitors, so the edit initial-state is derived\n // from the list query row.\n const { data: exhibitorsData } = useExhibitorsQuery({ limit: 100, offset: 0 });\n\n const existing = useMemo(\n () => (exhibitorsData?.exhibitors?.items ?? []).find((item) => item.id === id),\n [exhibitorsData, id]\n );\n\n const [createExhibitor, { loading: creating }] = useCreateExhibitorMutation();\n const [updateExhibitor, { loading: updating }] = useUpdateExhibitorMutation();\n const [form, setForm] = useState<ExhibitorFormState>(initialState);\n\n useEffect(() => {\n if (!isEdit || !existing) {\n return;\n }\n const metadata = existing.metadata ?? {};\n const website = typeof metadata.website === 'string' ? metadata.website : '';\n setForm({\n eventId: existing.eventId ?? '',\n name: existing.name ?? '',\n boothCode: existing.boothCode ?? '',\n category: existing.category ?? '',\n status: existing.status ?? 'CONFIRMED',\n website,\n contactName: existing.contactName ?? '',\n contactEmail: existing.contactEmail ?? '',\n });\n }, [existing, isEdit]);\n\n const onChange = (field: keyof ExhibitorFormState, 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.eventId) {\n notify.error('Event required', 'Choose the event this exhibitor belongs to.');\n return;\n }\n\n // The website is free text persisted verbatim, so normalise/reject it before\n // it is stored and later rendered as a link.\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 existingMetadata = existing?.metadata ?? {};\n const input = {\n eventId: form.eventId,\n name: form.name,\n boothCode: form.boothCode,\n category: form.category,\n status: form.status,\n contactName: form.contactName,\n contactEmail: form.contactEmail,\n metadata: { ...existingMetadata, website: normalizedWebsite ?? '' },\n };\n\n try {\n if (isEdit) {\n const response = await updateExhibitor({ variables: { id, input } });\n if (!mutationSucceeded(response, response.data?.updateExhibitor?.id, 'Could not update exhibitor')) {\n return;\n }\n navigate('/exhibitors');\n return;\n }\n\n const response = await createExhibitor({ variables: { input } });\n if (!mutationSucceeded(response, response.data?.createExhibitor?.id, 'Could not create exhibitor')) {\n return;\n }\n navigate('/exhibitors');\n } catch (error) {\n notify.error('Could not save exhibitor', describeError(error));\n }\n };\n\n const saving = creating || updating;\n const events = eventsData?.events?.items ?? [];\n\n return (\n <EntityFormShell\n title={isEdit ? 'Edit Exhibitor' : 'New Exhibitor'}\n description=\"Manage a booth exhibitor with live backend persistence.\"\n purpose=\"Register a company staffing a booth: link it to the event, give it a booth code and category, and record who to contact. Once saved, capture the leads their booth scans from the exhibitor list.\"\n backLabel=\"Back to Exhibitors\"\n onBack={() => navigate('/exhibitors')}\n onSubmit={onSubmit}\n submitLabel={isEdit ? 'Save Exhibitor' : 'Create Exhibitor'}\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\">Name</span>\n <Input value={form.name} onChange={(e) => onChange('name', e.target.value)} required />\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\">Booth Code</span>\n <Input value={form.boothCode} onChange={(e) => onChange('boothCode', e.target.value)} />\n </label>\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Category</span>\n <Input value={form.category} onChange={(e) => onChange('category', e.target.value)} />\n </label>\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Status</span>\n <Select value={form.status} onValueChange={(value) => onChange('status', value)}>\n <SelectTrigger aria-label=\"Status\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {STATUS_OPTIONS.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </label>\n </div>\n\n <label className=\"block space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Website</span>\n <Input\n value={form.website}\n onChange={(e) => onChange('website', e.target.value)}\n placeholder=\"https://example.com\"\n />\n </label>\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\">Contact Name</span>\n <Input value={form.contactName} onChange={(e) => onChange('contactName', e.target.value)} />\n </label>\n <label className=\"space-y-1 text-sm\">\n <span className=\"font-medium text-text-primary\">Contact Email</span>\n <Input type=\"email\" value={form.contactEmail} onChange={(e) => onChange('contactEmail', e.target.value)} />\n </label>\n </div>\n </EntityFormShell>\n );\n}\n"],"mappings":"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"}
@@ -2,7 +2,7 @@ import { useEventfullyManaged as e } from "../../context/EventfullyManagedContex
2
2
  import { useAttendeesQuery as t, useRegistrationsQuery as n } from "../../api/eventApi.js";
3
3
  import { EntityListSection as r } from "./EntityListSection.js";
4
4
  import { useMemo as i, useRef as a } from "react";
5
- import { useParams as o } from "react-router-dom";
5
+ import { useParams as o } from "react-router";
6
6
  import { jsx as s, jsxs as c } from "react/jsx-runtime";
7
7
  import { MessageSquare as l, Ticket as u, Users as d } from "lucide-react";
8
8
  import { EmphasisPanel as f, GlassCard as p, GradientText as m, NextSteps as h, PagePurpose as g } from "@burdenoff/fe-libs/ui";
@@ -1 +1 @@
1
- {"version":3,"file":"AttendeeProfilePage.js","names":[],"sources":["../../../../src/eventfullymanaged/pages/modules/AttendeeProfilePage.tsx"],"sourcesContent":["import { MessageSquare, Ticket, Users } from 'lucide-react';\nimport { ContextConversationWidget } from '@burdenoff/fe-libs/shared';\nimport { useRegisterPageConversationContext } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { EmphasisPanel, GlassCard, GradientText, NextSteps, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { useMemo, useRef } from 'react';\nimport { useParams } from 'react-router-dom';\n\nimport { useAttendeesQuery, useRegistrationsQuery } from '../../api/eventApi';\nimport { useEventfullyManaged } from '../../context/EventfullyManagedContext';\nimport { EntityListSection } from './EntityListSection';\n\n/**\n * Direct message thread with a single attendee, backed by wspace-conversations-svc.\n * Kept as a dedicated component so `useRegisterPageConversationContext` scopes the\n * thread to this attendee (entityType + entityId) and unregisters on unmount.\n */\nfunction AttendeeConversation({ attendeeId, attendeeName }: { attendeeId: string; attendeeName: string }) {\n // Stable reference: `useRegisterPageConversationContext` keys its effect on this\n // object's identity, so a fresh inline object every render would loop the shared\n // PageConversationContextProvider (see NetworkingPage for the full rationale).\n const conversationContext = useMemo(\n () => ({\n entityType: 'EVENTFULLYMANAGED_ATTENDEE',\n entityId: attendeeId,\n contextTitle: `Attendee · ${attendeeName}`,\n }),\n [attendeeId, attendeeName]\n );\n useRegisterPageConversationContext(conversationContext);\n return <ContextConversationWidget widgetKey=\"attendee-message\" />;\n}\n\ntype AttendeeRow = {\n id: string;\n fullName?: string;\n email?: string;\n company?: string;\n role?: string;\n};\n\ntype RegistrationRow = {\n id: string;\n attendeeId?: string;\n status?: string;\n quantity?: number;\n currency?: string;\n totalAmount?: string;\n};\n\nexport default function AttendeeProfilePage() {\n const { id = '' } = useParams();\n const { navigate } = useEventfullyManaged();\n const attendeesQuery = useAttendeesQuery({ limit: 100, offset: 0 });\n const registrationsQuery = useRegistrationsQuery({ limit: 100, offset: 0 });\n const conversationRef = useRef<HTMLDivElement | null>(null);\n\n const attendee = useMemo(\n () => ((attendeesQuery.data?.attendees.items ?? []) as AttendeeRow[]).find((item) => item.id === id),\n [id, attendeesQuery.data]\n );\n const registrations = useMemo(\n () =>\n ((registrationsQuery.data?.registrations.items ?? []) as RegistrationRow[]).filter(\n (registration) => registration.attendeeId === id\n ),\n [id, registrationsQuery.data]\n );\n\n return (\n <div className=\"space-y-6 p-6\">\n <PagePurpose>\n A single view of one attendee — who they are and everything they have registered for. Use this when you need to\n look someone up: verify their ticket, answer a support question, or understand their history with your events\n before reaching out.\n </PagePurpose>\n\n <EmphasisPanel>\n <h1 className=\"text-2xl font-bold text-text-primary\">\n <GradientText>Attendee</GradientText> Profile\n </h1>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {attendeesQuery.loading\n ? 'Loading attendee profile...'\n : attendee\n ? `${attendee.fullName ?? attendee.id} profile from registration and attendee APIs.`\n : 'Attendee not found in current query scope.'}\n </p>\n {attendee ? (\n <div className=\"mt-4 grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4\">\n <GlassCard treatment=\"metric\" className=\"p-3\">\n <p className=\"text-xs uppercase tracking-wide text-text-secondary\">Name</p>\n <p className=\"mt-1 font-medium text-text-primary\">{attendee.fullName ?? attendee.id}</p>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-3\">\n <p className=\"text-xs uppercase tracking-wide text-text-secondary\">Email</p>\n <p className=\"mt-1 font-medium text-text-primary\">{attendee.email ?? 'Unavailable'}</p>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-3\">\n <p className=\"text-xs uppercase tracking-wide text-text-secondary\">Company</p>\n <p className=\"mt-1 font-medium text-text-primary\">{attendee.company ?? 'Unavailable'}</p>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-3\">\n <p className=\"text-xs uppercase tracking-wide text-text-secondary\">Role</p>\n <p className=\"mt-1 font-medium text-text-primary\">{attendee.role ?? 'Unavailable'}</p>\n </GlassCard>\n </div>\n ) : null}\n </EmphasisPanel>\n\n <EntityListSection\n heading={\n registrationsQuery.loading ? 'Loading attendee registrations...' : `Registrations (${registrations.length})`\n }\n emptyLabel=\"No registrations linked to this attendee.\"\n rows={registrations.map((registration) => ({\n id: registration.id,\n title: `Registration ${registration.id}`,\n subtitle: [\n registration.quantity ? `Qty ${registration.quantity}` : undefined,\n registration.totalAmount ? `${registration.totalAmount} ${registration.currency ?? ''}` : undefined,\n ]\n .filter(Boolean)\n .join(' • '),\n status: registration.status,\n }))}\n />\n\n {id ? (\n <div ref={conversationRef} className=\"space-y-3\">\n <div>\n <h2 className=\"text-lg font-semibold text-text-primary\">Messages</h2>\n <p className=\"mt-1 text-sm text-text-secondary\">\n Start a direct conversation with this attendee. Threads and attachments are handled by the shared\n conversations service.\n </p>\n </div>\n <AttendeeConversation attendeeId={id} attendeeName={attendee?.fullName ?? attendee?.id ?? id} />\n </div>\n ) : null}\n\n <NextSteps\n storageKey=\"eventfullymanaged-attendee-profile\"\n title=\"Act on this attendee\"\n steps={[\n {\n id: 'view-registrations',\n label: 'Review all registrations',\n description: 'See the full registration list to manage tickets and payments.',\n icon: <Ticket className=\"h-4 w-4\" />,\n onClick: () => navigate('/registration'),\n },\n {\n id: 'networking',\n label: 'Find networking matches',\n description: 'Connect this attendee with relevant people and meetings.',\n icon: <Users className=\"h-4 w-4\" />,\n onClick: () => navigate('/networking'),\n },\n {\n id: 'reach-out',\n label: 'Send a message',\n description: 'Open a direct conversation thread with this attendee.',\n icon: <MessageSquare className=\"h-4 w-4\" />,\n onClick: () => conversationRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }),\n },\n ]}\n />\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"AttendeeProfilePage.js","names":[],"sources":["../../../../src/eventfullymanaged/pages/modules/AttendeeProfilePage.tsx"],"sourcesContent":["import { MessageSquare, Ticket, Users } from 'lucide-react';\nimport { ContextConversationWidget } from '@burdenoff/fe-libs/shared';\nimport { useRegisterPageConversationContext } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { EmphasisPanel, GlassCard, GradientText, NextSteps, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { useMemo, useRef } from 'react';\nimport { useParams } from 'react-router';\n\nimport { useAttendeesQuery, useRegistrationsQuery } from '../../api/eventApi';\nimport { useEventfullyManaged } from '../../context/EventfullyManagedContext';\nimport { EntityListSection } from './EntityListSection';\n\n/**\n * Direct message thread with a single attendee, backed by wspace-conversations-svc.\n * Kept as a dedicated component so `useRegisterPageConversationContext` scopes the\n * thread to this attendee (entityType + entityId) and unregisters on unmount.\n */\nfunction AttendeeConversation({ attendeeId, attendeeName }: { attendeeId: string; attendeeName: string }) {\n // Stable reference: `useRegisterPageConversationContext` keys its effect on this\n // object's identity, so a fresh inline object every render would loop the shared\n // PageConversationContextProvider (see NetworkingPage for the full rationale).\n const conversationContext = useMemo(\n () => ({\n entityType: 'EVENTFULLYMANAGED_ATTENDEE',\n entityId: attendeeId,\n contextTitle: `Attendee · ${attendeeName}`,\n }),\n [attendeeId, attendeeName]\n );\n useRegisterPageConversationContext(conversationContext);\n return <ContextConversationWidget widgetKey=\"attendee-message\" />;\n}\n\ntype AttendeeRow = {\n id: string;\n fullName?: string;\n email?: string;\n company?: string;\n role?: string;\n};\n\ntype RegistrationRow = {\n id: string;\n attendeeId?: string;\n status?: string;\n quantity?: number;\n currency?: string;\n totalAmount?: string;\n};\n\nexport default function AttendeeProfilePage() {\n const { id = '' } = useParams();\n const { navigate } = useEventfullyManaged();\n const attendeesQuery = useAttendeesQuery({ limit: 100, offset: 0 });\n const registrationsQuery = useRegistrationsQuery({ limit: 100, offset: 0 });\n const conversationRef = useRef<HTMLDivElement | null>(null);\n\n const attendee = useMemo(\n () => ((attendeesQuery.data?.attendees.items ?? []) as AttendeeRow[]).find((item) => item.id === id),\n [id, attendeesQuery.data]\n );\n const registrations = useMemo(\n () =>\n ((registrationsQuery.data?.registrations.items ?? []) as RegistrationRow[]).filter(\n (registration) => registration.attendeeId === id\n ),\n [id, registrationsQuery.data]\n );\n\n return (\n <div className=\"space-y-6 p-6\">\n <PagePurpose>\n A single view of one attendee — who they are and everything they have registered for. Use this when you need to\n look someone up: verify their ticket, answer a support question, or understand their history with your events\n before reaching out.\n </PagePurpose>\n\n <EmphasisPanel>\n <h1 className=\"text-2xl font-bold text-text-primary\">\n <GradientText>Attendee</GradientText> Profile\n </h1>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {attendeesQuery.loading\n ? 'Loading attendee profile...'\n : attendee\n ? `${attendee.fullName ?? attendee.id} profile from registration and attendee APIs.`\n : 'Attendee not found in current query scope.'}\n </p>\n {attendee ? (\n <div className=\"mt-4 grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4\">\n <GlassCard treatment=\"metric\" className=\"p-3\">\n <p className=\"text-xs uppercase tracking-wide text-text-secondary\">Name</p>\n <p className=\"mt-1 font-medium text-text-primary\">{attendee.fullName ?? attendee.id}</p>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-3\">\n <p className=\"text-xs uppercase tracking-wide text-text-secondary\">Email</p>\n <p className=\"mt-1 font-medium text-text-primary\">{attendee.email ?? 'Unavailable'}</p>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-3\">\n <p className=\"text-xs uppercase tracking-wide text-text-secondary\">Company</p>\n <p className=\"mt-1 font-medium text-text-primary\">{attendee.company ?? 'Unavailable'}</p>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-3\">\n <p className=\"text-xs uppercase tracking-wide text-text-secondary\">Role</p>\n <p className=\"mt-1 font-medium text-text-primary\">{attendee.role ?? 'Unavailable'}</p>\n </GlassCard>\n </div>\n ) : null}\n </EmphasisPanel>\n\n <EntityListSection\n heading={\n registrationsQuery.loading ? 'Loading attendee registrations...' : `Registrations (${registrations.length})`\n }\n emptyLabel=\"No registrations linked to this attendee.\"\n rows={registrations.map((registration) => ({\n id: registration.id,\n title: `Registration ${registration.id}`,\n subtitle: [\n registration.quantity ? `Qty ${registration.quantity}` : undefined,\n registration.totalAmount ? `${registration.totalAmount} ${registration.currency ?? ''}` : undefined,\n ]\n .filter(Boolean)\n .join(' • '),\n status: registration.status,\n }))}\n />\n\n {id ? (\n <div ref={conversationRef} className=\"space-y-3\">\n <div>\n <h2 className=\"text-lg font-semibold text-text-primary\">Messages</h2>\n <p className=\"mt-1 text-sm text-text-secondary\">\n Start a direct conversation with this attendee. Threads and attachments are handled by the shared\n conversations service.\n </p>\n </div>\n <AttendeeConversation attendeeId={id} attendeeName={attendee?.fullName ?? attendee?.id ?? id} />\n </div>\n ) : null}\n\n <NextSteps\n storageKey=\"eventfullymanaged-attendee-profile\"\n title=\"Act on this attendee\"\n steps={[\n {\n id: 'view-registrations',\n label: 'Review all registrations',\n description: 'See the full registration list to manage tickets and payments.',\n icon: <Ticket className=\"h-4 w-4\" />,\n onClick: () => navigate('/registration'),\n },\n {\n id: 'networking',\n label: 'Find networking matches',\n description: 'Connect this attendee with relevant people and meetings.',\n icon: <Users className=\"h-4 w-4\" />,\n onClick: () => navigate('/networking'),\n },\n {\n id: 'reach-out',\n label: 'Send a message',\n description: 'Open a direct conversation thread with this attendee.',\n icon: <MessageSquare className=\"h-4 w-4\" />,\n onClick: () => conversationRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }),\n },\n ]}\n />\n </div>\n );\n}\n"],"mappings":"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"}
@@ -4,7 +4,7 @@ import { EntityTagsField as r } from "../../components/EntityTagsField.js";
4
4
  import { SafeLink as i } from "../../components/SafeLink.js";
5
5
  import { EntityListSection as a } from "./EntityListSection.js";
6
6
  import { useMemo as o } from "react";
7
- import { useParams as s } from "react-router-dom";
7
+ import { useParams as s } from "react-router";
8
8
  import { jsx as c, jsxs as l } from "react/jsx-runtime";
9
9
  import { Building2 as u, Coins as d, Magnet as f, Megaphone as p, Pencil as m, TrendingUp as h } from "lucide-react";
10
10
  import { Badge as g, EmphasisPanel as _, GlassCard as v, GradientText as y, NextSteps as b, PagePurpose as x } from "@burdenoff/fe-libs/ui";
@@ -1 +1 @@
1
- {"version":3,"file":"SponsorDetailsPage.js","names":[],"sources":["../../../../src/eventfullymanaged/pages/modules/SponsorDetailsPage.tsx"],"sourcesContent":["import { Building2, Coins, Magnet, Megaphone, Pencil, TrendingUp } from 'lucide-react';\nimport { ContextConversationWidget } from '@burdenoff/fe-libs/shared';\nimport { useRegisterPageConversationContext } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { Badge, EmphasisPanel, GlassCard, GradientText, NextSteps, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { useMemo } from 'react';\nimport { useParams } from 'react-router-dom';\n\nimport { useCommunicationCampaignsQuery, useSponsorQuery } from '../../api/eventApi';\nimport { EntityTagsField } from '../../components/EntityTagsField';\nimport { SafeLink } from '../../components/SafeLink';\nimport { useEventfullyManaged } from '../../context/EventfullyManagedContext';\nimport { EntityListSection } from './EntityListSection';\n\n/**\n * Sponsor ↔ organizer message thread, backed by wspace-conversations-svc and\n * scoped to this sponsor so the conversation follows the partnership.\n */\nfunction SponsorConversation({ sponsorId, sponsorName }: { sponsorId: string; sponsorName: string }) {\n // Stable reference: `useRegisterPageConversationContext` keys its effect on this\n // object's identity, so a fresh inline object every render would loop the shared\n // PageConversationContextProvider (see NetworkingPage for the full rationale).\n const conversationContext = useMemo(\n () => ({\n entityType: 'EVENTFULLYMANAGED_SPONSOR',\n entityId: sponsorId,\n contextTitle: `Sponsor · ${sponsorName}`,\n }),\n [sponsorId, sponsorName]\n );\n useRegisterPageConversationContext(conversationContext);\n return <ContextConversationWidget widgetKey=\"sponsor-message\" />;\n}\n\ntype CampaignRow = {\n id: string;\n name?: string;\n title?: string;\n status?: string;\n};\n\nconst compactNumber = new Intl.NumberFormat(undefined, { notation: 'compact', maximumFractionDigits: 1 });\n\nexport default function SponsorDetailsPage() {\n const { id = '' } = useParams();\n const { navigate } = useEventfullyManaged();\n const sponsorQuery = useSponsorQuery(id);\n const campaignsQuery = useCommunicationCampaignsQuery({ limit: 100, offset: 0 });\n\n const sponsor = sponsorQuery.data?.sponsor ?? null;\n const campaigns = useMemo(\n () => (campaignsQuery.data?.communicationCampaigns.items ?? []) as CampaignRow[],\n [campaignsQuery.data]\n );\n\n const metrics = (sponsor?.metrics ?? {}) as { value?: number; leads?: number; leadsTarget?: number };\n const value = Number(metrics.value ?? 0);\n const leads = Number(metrics.leads ?? 0);\n const target = Number(metrics.leadsTarget ?? 0);\n const leadsPct = target > 0 ? Math.min(100, Math.round((leads / target) * 100)) : 0;\n\n return (\n <div className=\"space-y-6 p-6\">\n <PagePurpose>\n Everything about one sponsor in one place — their tier, contract value, leads, contacts, and the campaigns they\n appear in. Use this to prep for a sponsor conversation, confirm details, or check how their branded campaigns\n are performing.\n </PagePurpose>\n\n <EmphasisPanel>\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between\">\n <div className=\"flex items-center gap-4\">\n <span className=\"flex h-14 w-14 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-6 w-6 text-text-muted\" />\n )}\n </span>\n <div>\n <h1 className=\"text-2xl font-bold text-text-primary\">\n <GradientText>{sponsor?.name ?? 'Sponsor'}</GradientText>\n </h1>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {sponsorQuery.loading\n ? 'Loading sponsor details...'\n : sponsor\n ? 'Live sponsor profile and partnership metrics.'\n : 'Sponsor not found in current query scope.'}\n </p>\n </div>\n </div>\n {sponsor ? (\n <Badge variant=\"secondary\" className=\"shrink-0 self-start uppercase\">\n {sponsor.level}\n </Badge>\n ) : null}\n </div>\n\n {sponsor ? (\n <div className=\"mt-5 grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3\">\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-text-secondary\">\n <Coins className=\"h-4 w-4 text-accent-primary\" />\n <p className=\"text-xs font-medium uppercase tracking-wide\">Contract value</p>\n </div>\n <p className=\"mt-1.5 text-2xl font-bold tabular-nums text-text-primary\">${compactNumber.format(value)}</p>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-text-secondary\">\n <Magnet className=\"h-4 w-4 text-accent-primary\" />\n <p className=\"text-xs font-medium uppercase tracking-wide\">Leads</p>\n </div>\n <p className=\"mt-1.5 text-2xl font-bold tabular-nums text-text-primary\">{leads.toLocaleString()}</p>\n {target > 0 ? (\n <div className=\"mt-2\">\n <div className=\"h-1.5 w-full overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full\"\n style={{\n width: `${leadsPct}%`,\n background: 'linear-gradient(90deg, var(--color-chart-1), var(--color-chart-2))',\n }}\n />\n </div>\n <p className=\"mt-1 text-xs text-text-secondary\">\n {leadsPct}% of {target.toLocaleString()} target\n </p>\n </div>\n ) : null}\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <p className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">Primary contact</p>\n <p className=\"mt-1.5 truncate font-medium text-text-primary\">{sponsor.contactName ?? 'Unassigned'}</p>\n <p className=\"truncate text-xs text-text-secondary\">{sponsor.contactEmail ?? 'No email'}</p>\n {sponsor.website ? (\n <SafeLink\n href={sponsor.website}\n className=\"mt-1 block truncate text-xs text-text-link hover:underline\"\n fallbackClassName=\"mt-1 block truncate text-xs text-text-secondary\"\n >\n {sponsor.website}\n </SafeLink>\n ) : null}\n </GlassCard>\n </div>\n ) : null}\n </EmphasisPanel>\n\n {sponsor ? (\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-5\">\n <h2 className=\"text-lg font-semibold text-text-primary\">Tags</h2>\n <p className=\"mt-1 text-sm text-text-secondary\">\n Label this sponsor so you can segment your portfolio — for example by industry, tier or renewal status.\n </p>\n <EntityTagsField\n entityType=\"EVENTFULLYMANAGED_SPONSOR\"\n entityId={sponsor.id}\n label=\"Sponsor tags\"\n className=\"mt-3\"\n />\n </GlassCard>\n ) : null}\n\n <EntityListSection\n heading={campaignsQuery.loading ? 'Loading campaigns...' : `Campaign Activity (${campaigns.length})`}\n emptyLabel=\"No communication campaigns available for sponsor tracking.\"\n rows={campaigns.map((campaign) => ({\n id: campaign.id,\n title: campaign.title ?? campaign.name ?? `Campaign ${campaign.id}`,\n status: campaign.status,\n }))}\n />\n\n {sponsor ? (\n <div className=\"space-y-3\">\n <div>\n <h2 className=\"text-lg font-semibold text-text-primary\">Sponsor messages</h2>\n <p className=\"mt-1 text-sm text-text-secondary\">\n Coordinate with this sponsor in a dedicated thread, backed by the conversations service.\n </p>\n </div>\n <SponsorConversation sponsorId={sponsor.id} sponsorName={sponsor.name} />\n </div>\n ) : null}\n\n <NextSteps\n storageKey=\"eventfullymanaged-sponsor-details\"\n title=\"Grow this partnership\"\n steps={[\n {\n id: 'edit-sponsor',\n label: 'Update sponsor details',\n description: 'Keep tier, contacts and contract value current.',\n icon: <Pencil className=\"h-4 w-4\" />,\n onClick: () => navigate(id ? `/sponsors/${id}/edit` : '/sponsors'),\n },\n {\n id: 'track-leads',\n label: 'Track sponsor leads',\n description: 'See the leads this sponsor is generating at the event.',\n icon: <TrendingUp className=\"h-4 w-4\" />,\n onClick: () => navigate(id ? `/sponsors/${id}/leads` : '/sponsors'),\n },\n {\n id: 'run-campaign',\n label: 'Run a co-branded campaign',\n description: 'Promote this sponsor through a communication campaign.',\n icon: <Megaphone className=\"h-4 w-4\" />,\n onClick: () => navigate('/communication'),\n },\n ]}\n />\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"SponsorDetailsPage.js","names":[],"sources":["../../../../src/eventfullymanaged/pages/modules/SponsorDetailsPage.tsx"],"sourcesContent":["import { Building2, Coins, Magnet, Megaphone, Pencil, TrendingUp } from 'lucide-react';\nimport { ContextConversationWidget } from '@burdenoff/fe-libs/shared';\nimport { useRegisterPageConversationContext } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { Badge, EmphasisPanel, GlassCard, GradientText, NextSteps, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { useMemo } from 'react';\nimport { useParams } from 'react-router';\n\nimport { useCommunicationCampaignsQuery, useSponsorQuery } from '../../api/eventApi';\nimport { EntityTagsField } from '../../components/EntityTagsField';\nimport { SafeLink } from '../../components/SafeLink';\nimport { useEventfullyManaged } from '../../context/EventfullyManagedContext';\nimport { EntityListSection } from './EntityListSection';\n\n/**\n * Sponsor ↔ organizer message thread, backed by wspace-conversations-svc and\n * scoped to this sponsor so the conversation follows the partnership.\n */\nfunction SponsorConversation({ sponsorId, sponsorName }: { sponsorId: string; sponsorName: string }) {\n // Stable reference: `useRegisterPageConversationContext` keys its effect on this\n // object's identity, so a fresh inline object every render would loop the shared\n // PageConversationContextProvider (see NetworkingPage for the full rationale).\n const conversationContext = useMemo(\n () => ({\n entityType: 'EVENTFULLYMANAGED_SPONSOR',\n entityId: sponsorId,\n contextTitle: `Sponsor · ${sponsorName}`,\n }),\n [sponsorId, sponsorName]\n );\n useRegisterPageConversationContext(conversationContext);\n return <ContextConversationWidget widgetKey=\"sponsor-message\" />;\n}\n\ntype CampaignRow = {\n id: string;\n name?: string;\n title?: string;\n status?: string;\n};\n\nconst compactNumber = new Intl.NumberFormat(undefined, { notation: 'compact', maximumFractionDigits: 1 });\n\nexport default function SponsorDetailsPage() {\n const { id = '' } = useParams();\n const { navigate } = useEventfullyManaged();\n const sponsorQuery = useSponsorQuery(id);\n const campaignsQuery = useCommunicationCampaignsQuery({ limit: 100, offset: 0 });\n\n const sponsor = sponsorQuery.data?.sponsor ?? null;\n const campaigns = useMemo(\n () => (campaignsQuery.data?.communicationCampaigns.items ?? []) as CampaignRow[],\n [campaignsQuery.data]\n );\n\n const metrics = (sponsor?.metrics ?? {}) as { value?: number; leads?: number; leadsTarget?: number };\n const value = Number(metrics.value ?? 0);\n const leads = Number(metrics.leads ?? 0);\n const target = Number(metrics.leadsTarget ?? 0);\n const leadsPct = target > 0 ? Math.min(100, Math.round((leads / target) * 100)) : 0;\n\n return (\n <div className=\"space-y-6 p-6\">\n <PagePurpose>\n Everything about one sponsor in one place — their tier, contract value, leads, contacts, and the campaigns they\n appear in. Use this to prep for a sponsor conversation, confirm details, or check how their branded campaigns\n are performing.\n </PagePurpose>\n\n <EmphasisPanel>\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between\">\n <div className=\"flex items-center gap-4\">\n <span className=\"flex h-14 w-14 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-6 w-6 text-text-muted\" />\n )}\n </span>\n <div>\n <h1 className=\"text-2xl font-bold text-text-primary\">\n <GradientText>{sponsor?.name ?? 'Sponsor'}</GradientText>\n </h1>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {sponsorQuery.loading\n ? 'Loading sponsor details...'\n : sponsor\n ? 'Live sponsor profile and partnership metrics.'\n : 'Sponsor not found in current query scope.'}\n </p>\n </div>\n </div>\n {sponsor ? (\n <Badge variant=\"secondary\" className=\"shrink-0 self-start uppercase\">\n {sponsor.level}\n </Badge>\n ) : null}\n </div>\n\n {sponsor ? (\n <div className=\"mt-5 grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3\">\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-text-secondary\">\n <Coins className=\"h-4 w-4 text-accent-primary\" />\n <p className=\"text-xs font-medium uppercase tracking-wide\">Contract value</p>\n </div>\n <p className=\"mt-1.5 text-2xl font-bold tabular-nums text-text-primary\">${compactNumber.format(value)}</p>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-text-secondary\">\n <Magnet className=\"h-4 w-4 text-accent-primary\" />\n <p className=\"text-xs font-medium uppercase tracking-wide\">Leads</p>\n </div>\n <p className=\"mt-1.5 text-2xl font-bold tabular-nums text-text-primary\">{leads.toLocaleString()}</p>\n {target > 0 ? (\n <div className=\"mt-2\">\n <div className=\"h-1.5 w-full overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full\"\n style={{\n width: `${leadsPct}%`,\n background: 'linear-gradient(90deg, var(--color-chart-1), var(--color-chart-2))',\n }}\n />\n </div>\n <p className=\"mt-1 text-xs text-text-secondary\">\n {leadsPct}% of {target.toLocaleString()} target\n </p>\n </div>\n ) : null}\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <p className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">Primary contact</p>\n <p className=\"mt-1.5 truncate font-medium text-text-primary\">{sponsor.contactName ?? 'Unassigned'}</p>\n <p className=\"truncate text-xs text-text-secondary\">{sponsor.contactEmail ?? 'No email'}</p>\n {sponsor.website ? (\n <SafeLink\n href={sponsor.website}\n className=\"mt-1 block truncate text-xs text-text-link hover:underline\"\n fallbackClassName=\"mt-1 block truncate text-xs text-text-secondary\"\n >\n {sponsor.website}\n </SafeLink>\n ) : null}\n </GlassCard>\n </div>\n ) : null}\n </EmphasisPanel>\n\n {sponsor ? (\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-5\">\n <h2 className=\"text-lg font-semibold text-text-primary\">Tags</h2>\n <p className=\"mt-1 text-sm text-text-secondary\">\n Label this sponsor so you can segment your portfolio — for example by industry, tier or renewal status.\n </p>\n <EntityTagsField\n entityType=\"EVENTFULLYMANAGED_SPONSOR\"\n entityId={sponsor.id}\n label=\"Sponsor tags\"\n className=\"mt-3\"\n />\n </GlassCard>\n ) : null}\n\n <EntityListSection\n heading={campaignsQuery.loading ? 'Loading campaigns...' : `Campaign Activity (${campaigns.length})`}\n emptyLabel=\"No communication campaigns available for sponsor tracking.\"\n rows={campaigns.map((campaign) => ({\n id: campaign.id,\n title: campaign.title ?? campaign.name ?? `Campaign ${campaign.id}`,\n status: campaign.status,\n }))}\n />\n\n {sponsor ? (\n <div className=\"space-y-3\">\n <div>\n <h2 className=\"text-lg font-semibold text-text-primary\">Sponsor messages</h2>\n <p className=\"mt-1 text-sm text-text-secondary\">\n Coordinate with this sponsor in a dedicated thread, backed by the conversations service.\n </p>\n </div>\n <SponsorConversation sponsorId={sponsor.id} sponsorName={sponsor.name} />\n </div>\n ) : null}\n\n <NextSteps\n storageKey=\"eventfullymanaged-sponsor-details\"\n title=\"Grow this partnership\"\n steps={[\n {\n id: 'edit-sponsor',\n label: 'Update sponsor details',\n description: 'Keep tier, contacts and contract value current.',\n icon: <Pencil className=\"h-4 w-4\" />,\n onClick: () => navigate(id ? `/sponsors/${id}/edit` : '/sponsors'),\n },\n {\n id: 'track-leads',\n label: 'Track sponsor leads',\n description: 'See the leads this sponsor is generating at the event.',\n icon: <TrendingUp className=\"h-4 w-4\" />,\n onClick: () => navigate(id ? `/sponsors/${id}/leads` : '/sponsors'),\n },\n {\n id: 'run-campaign',\n label: 'Run a co-branded campaign',\n description: 'Promote this sponsor through a communication campaign.',\n icon: <Megaphone className=\"h-4 w-4\" />,\n onClick: () => navigate('/communication'),\n },\n ]}\n />\n </div>\n );\n}\n"],"mappings":"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"}
@@ -3,7 +3,7 @@ import { useLeadsQuery as t, useMeetingsQuery as n, useNetworkingMatchesQuery as
3
3
  import { LeadCaptureDialog as a } from "../leads/LeadCaptureDialog.js";
4
4
  import { EntityListSection as o } from "./EntityListSection.js";
5
5
  import { useMemo as s, useState as c } from "react";
6
- import { useParams as l } from "react-router-dom";
6
+ import { useParams as l } from "react-router";
7
7
  import { jsx as u, jsxs as d } from "react/jsx-runtime";
8
8
  import { Megaphone as f, User as p, UserPlus as m, Users as h } from "lucide-react";
9
9
  import { Button as g, EmphasisPanel as _, GradientText as v, NextSteps as y, PagePurpose as b } from "@burdenoff/fe-libs/ui";