From 80321f04e79f5e4b7ecc9965de5a105c189a4ba0 Mon Sep 17 00:00:00 2001 From: bunker-admin Date: Sat, 11 Apr 2026 11:45:15 -0600 Subject: [PATCH] Link event staffing shifts to ticketed events with auto pre-fill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Shift.ticketedEventId (nullable FK to TicketedEvent) persists the link between a staffing shift and its parent event. The shift list response now includes the linked event (id, slug, title, date) so the admin UI can show context without a second request. The Create/Edit Shift drawer gains a conditional Event picker that only appears when kind is EVENT_STAFFING. Picking an event pre-fills the form's date, startTime, endTime, and location (venue name + address) — and seeds the title with "Staff: {event.title}" only if the title field is still empty, so hand-typed overrides aren't stomped. Switching kind away from EVENT_STAFFING clears the link. The shifts table's Kind tag wraps in a tooltip showing the linked event title when one is present, so organizers can see at a glance which staffing shifts belong to which event without opening the drawer. Bunker Admin --- admin/src/pages/ShiftsPage.tsx | 91 ++++++++++++++++++- admin/src/types/api.ts | 2 + .../migration.sql | 7 ++ api/prisma/schema.prisma | 15 ++- api/src/modules/map/shifts/shifts.schemas.ts | 2 + api/src/modules/map/shifts/shifts.service.ts | 2 + 6 files changed, 111 insertions(+), 8 deletions(-) create mode 100644 api/prisma/migrations/20260411120000_shift_event_link/migration.sql diff --git a/admin/src/pages/ShiftsPage.tsx b/admin/src/pages/ShiftsPage.tsx index 2ea4d652..3ef27e22 100644 --- a/admin/src/pages/ShiftsPage.tsx +++ b/admin/src/pages/ShiftsPage.tsx @@ -144,6 +144,16 @@ export default function ShiftsPage() { // Cuts for area dropdown const [cuts, setCuts] = useState([]); + const [ticketedEvents, setTicketedEvents] = useState>([]); // Calendar view state const [activeTab, setActiveTab] = useState<'table' | 'calendar'>('table'); @@ -208,6 +218,15 @@ export default function ShiftsPage() { } }, []); + const fetchTicketedEvents = useCallback(async () => { + try { + const { data } = await api.get('/ticketed-events/admin', { params: { limit: 100 } }); + setTicketedEvents(data.events ?? data.items ?? data ?? []); + } catch { + // Non-critical — picker stays empty + } + }, []); + const fetchCalendarData = useCallback(async (month: dayjs.Dayjs) => { setCalendarLoading(true); try { @@ -251,6 +270,7 @@ export default function ShiftsPage() { fetchShifts({ page: 1 }); fetchStats(); fetchCuts(); + fetchTicketedEvents(); }, [debouncedSearch, statusFilter, kindFilter]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { @@ -279,6 +299,10 @@ export default function ShiftsPage() { isPublic: values.isPublic || false, cutId: values.cutId || undefined, kind: (values.kind as ShiftKind) || 'CANVASS', + ticketedEventId: + values.kind === 'EVENT_STAFFING' + ? ((values.ticketedEventId as string) || undefined) + : undefined, }; await api.post('/map/shifts', payload); message.success(`${SHIFT_KIND_LABELS[payload.kind]} shift created`); @@ -336,6 +360,12 @@ export default function ShiftsPage() { if (values.status !== undefined) payload.status = values.status; if (values.cutId !== undefined) payload.cutId = values.cutId || null; if (values.kind !== undefined) payload.kind = values.kind; + if (values.kind === 'EVENT_STAFFING') { + payload.ticketedEventId = (values.ticketedEventId as string) || null; + } else if (values.kind !== undefined) { + // Changing away from EVENT_STAFFING clears the link + payload.ticketedEventId = null; + } await api.put(`/map/shifts/${editingShift.id}`, payload); message.success('Shift updated'); @@ -414,6 +444,7 @@ export default function ShiftsPage() { status: shift.status, cutId: shift.cutId, kind: shift.kind || 'CANVASS', + ticketedEventId: shift.ticketedEventId || undefined, }); setEditDrawerOpen(true); }; @@ -538,14 +569,22 @@ export default function ShiftsPage() { title: 'Kind', dataIndex: 'kind', key: 'kind', - width: 130, - render: (kind: ShiftKind) => { + width: 150, + render: (kind: ShiftKind, record: Shift) => { const k = kind || 'CANVASS'; - return ( + const tag = ( {SHIFT_KIND_LABELS[k]} ); + if (record.ticketedEvent) { + return ( + + {tag} + + ); + } + return tag; }, responsive: ['md'], }, @@ -713,8 +752,54 @@ export default function ShiftsPage() { ), }))} + onChange={(value) => { + // Clear event link when leaving EVENT_STAFFING + if (value !== 'EVENT_STAFFING') { + (isEdit ? editForm : createForm).setFieldValue('ticketedEventId', undefined); + } + }} /> + prev.kind !== curr.kind}> + {({ getFieldValue }) => + getFieldValue('kind') === 'EVENT_STAFFING' ? ( + +