Display the event slug in small muted text directly after the event
name in the EventTabsNav header.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Build the frontend for platform admin in apps/app/:
- TypeScript types (admin.ts) and API composable (useAdmin.ts) with
TanStack Query for all admin endpoints
- ImpersonationStore (Pinia) + ImpersonationBanner component integrated
in the main layout, with token-based session management
- Platform navigation section (conditionally shown for super_admin users)
- Route guard blocking /platform/* for non-super_admin users
- 6 pages: dashboard with stats cards, organisations list/detail,
users list/detail with impersonation, activity log with expandable rows
- All pages implement loading/error/empty states per conventions
- Vite build passes cleanly
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
v-tooltip renders a forced-dark background unsuitable for multi-line
help content. Switch to v-menu + v-card which follows the app theme.
Use surface-variant background for the "Tip:" block.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
VAutocomplete renders its dropdown list in a teleported overlay outside
the component DOM tree. Scoped :deep() CSS cannot reach teleported
content. Switch from class-based opacity to inline style on VListItem.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
VAutocomplete ignores interleaved fake header items — they were filtered
out before reaching the template. Replace with Approach A: keep only
real selectable items sorted by group, detect group boundaries in the
#item template by comparing adjacent groupName values, and render
VListSubheader before each new group.
- Remove _isGroupHeader from TimeSlotDropdownItem interface
- Rename groupTimeSlots → sortedItems (returns only selectable items)
- Add hasGroups computed for conditional header rendering
- Add isNewGroup(index) boundary detection in CreateShiftDialog
- Add scoped .time-slot-dimmed CSS class (opacity: 0.65)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace separate status chips/buttons with one status dropdown next to
edit, move dates under the title, add share dialog for registration URL,
and remove RegistrationLinkCard.
Made-with: Cursor
Restructure the festival hierarchy end-to-end:
Seeder: Remove duplicate festival-level VOLUNTEER time slots, keep only CREW
operational slots. Rename sub-events to "Dag 1/2/3 — ..." pattern. Change
Nachtsecurity to Security (cross_event). EHBO/Security shifts now use sub-event
time slots via cross_event exception. Add flat event "Braderie Dorpstown 2026".
API: Add ?include_children=true to TimeSlotController for festivals, returning
all sub-event time slots with source and event_name fields. Update
StoreShiftRequest and UpdateShiftRequest to accept child time slots for
cross_event sections.
Frontend: Create useTimeSlotDropdown composable with 4-scenario dropdown logic.
Replace AppSelect with VAutocomplete in CreateShiftDialog with grouped items,
dimmed festival slots, and info tooltips. Add InfoTooltip reusable component.
Show festival context labels on cross_event sections in sub-event section lists.
Add read-only festival time slots on sub-event time-slots page. Add cross_event
context banner with "Bekijk alle diensten" link.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The time slot dropdown in the shift edit dialog could flash the
"create a time slot first" alert during loading, and show raw ULIDs
when time slot data hadn't loaded yet. Fixed by:
- Adding loading state indicator to the time slot dropdown
- Using the shift's existing time_slot object as a fallback item
while the full list is fetching
- Showing the dropdown (with loading spinner) instead of the
misleading "no time slots" alert during fetch
Added test coverage for time_slot_id validation on shift updates:
- Update with valid same-event time slot (200)
- Update with cross-org time slot (422)
- Update on sub-event with parent festival time slot (200)
- Store/update responses include nested time_slot object
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds two new API endpoints to quickly add organisation members as event
persons with user_id pre-linked and status approved:
- GET /organisations/{org}/members/available-for-event/{event}
- POST /organisations/{org}/events/{event}/persons/from-member
Includes frontend dialog with member search, crowd type selection, and
click-to-add behavior in the Personen tab.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The approve() and bulkApprove() methods in ShiftAssignmentService
hard-blocked with a 422 when all slots were filled. This was incorrect
for organiser actions — only volunteer claims (portal self-service)
should enforce capacity limits. Organiser assign() already allowed
overbooking, making the approve block inconsistent.
Changes:
- Remove capacity hard-block from approve() and bulkApprove(), replace
with audit log entry (shift.overbooked_approval)
- Add overbook confirmation dialog in ShiftDetailPanel before approving
a full shift (single + bulk approve)
- Add onError handlers to all mutations in ShiftDetailPanel (approve,
reject, cancel, bulk-approve) so errors display in the snackbar
- Add global 422 validation error display in axios interceptor via
useNotificationStore as safety net for all components
- Add PHPUnit test for approve-when-full scenario
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Implements the full identity matching engine: email matching (HIGH confidence),
fuzzy name matching with Levenshtein distance (MEDIUM confidence, upgradable to
HIGH with DOB tiebreaker), manual link/unlink, revert confirmed matches, and
automatic detection via PersonObserver. Includes 33 comprehensive tests, frontend
integration with confirm/dismiss/unlink UI, and match indicators in the persons list.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move all authenticated organiser-facing event sub-resource routes from
/events/{event}/... to /organisations/{organisation}/events/{event}/...
to enforce multi-tenancy at the routing layer.
Changes:
- Routes: restructured api.php to nest all event sub-resources under
the existing organisation prefix group
- Controllers: added Organisation parameter and VerifiesOrganisationEvent
trait to all 12 affected controllers (sections, time-slots, shifts,
persons, crowd-lists, locations, shift-assignments, registration-fields,
availabilities, field-values, section-preferences, stats)
- Tests: updated all 20 feature test files with new route paths
- Frontend: updated 8 API composables and 20 Vue components/pages
- API.md: updated documentation to reflect new route structure
Portal routes, public routes (volunteer-register), and invitation routes
remain unchanged as they operate without organisation context.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds a new settings sub-page for managing dynamic registration form fields
per event. Includes sortable field list, create/edit dialog, template picker,
and import-from-event functionality.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The backend validates tag_category as 'prohibited' for non-tag_picker
field types. Sending null triggered a 422 validation error.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move crowd types management to organisation settings as a new tab and
align all three settings tabs (Tags, Registration Field Templates, Crowd
Types) to the same layout pattern: header with title/subtitle, VDataTable
for active items, and a separate inactive section with VList. Also fix
the API to return inactive records for person tags and registration field
templates so the frontend can display them.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Cross-cutting migration affecting the entire stack:
- Database: 3 migrations splitting name columns with data migration
- Models: first_name/last_name on User, Person; contact_first_name/contact_last_name on Company; backward-compatible name accessors
- API: all resources return first_name, last_name, full_name; assignablePersons endpoint updated
- Requests: validation rules updated for all person/user/company forms
- Services: VolunteerRegistrationService, ShiftAssignmentService, InvitationService updated
- Frontend: TypeScript types, Zod schemas, all forms split into Voornaam/Achternaam fields
- Display: all person/user name references use full_name; initials use first_name[0]+last_name[0]
- Tests: all 371 tests passing
- Docs: SCHEMA.md and API.md updated
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add cancelled_by, cancellation_source (organiser|volunteer|system), and
cancelled_at columns to shift_assignments. Cancel flow now records who
cancelled and why. Assign flow reactivates existing cancelled/rejected
records instead of creating duplicates, preventing UNIQUE constraint
violations. Assignable-persons endpoint returns previous_assignment data
for contextual UI indicators. Frontend shows cancellation source labels,
previous assignment history in assign dialog, and "Opnieuw toewijzen"
buttons with volunteer-cancelled confirmation dialogs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove capacity and status validation from organizer assign flow so
organizers can intentionally overbook shifts. Log overbooked assignments
for audit trail. Volunteer claims still enforce hard limits. Frontend
shows a warning banner when a shift is full and requires confirmation
before overbooking.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Invalidate assignable-persons query cache in useAssignPersonToShift
onSuccess so the list reflects the new assignment immediately. Keep the
dialog open after assigning a person to allow sequential assignments,
showing a brief success snackbar instead of closing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add GET /events/{event}/shifts/{shift}/assignable-persons endpoint that
returns approved persons with availability status, conflict details, and
already-assigned flags. Improve ShiftAssignmentService conflict errors to
include section name, time slot, and time range. Replace both assign
dialogs with a new AssignPersonDialog featuring search, crowd type
filtering, availability toggle, and inline conflict warnings.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add show_in_registration and registration_description columns to festival_sections.
Registration form now shows deduplicated sections by name (across sub-events),
filtered by show_in_registration=true, grouped by category with card-based UI.
Section preferences use section_name instead of section_id.
Add GET/PUT registration-settings endpoints for festival-level bulk management.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add GET /events/{event}/stats endpoint returning aggregate counts for
persons (by status, approved without shift), pending identity matches,
and shift fill rates. Frontend metric cards component shows four
actionable KPIs on the event overview tab.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract time slots from Secties & Shifts into a dedicated Tijdsloten tab.
New tab groups time slots by date with Dutch date headers, person type
filter pills, fill rate progress bars, and sections count. Includes
duplicate, edit, and delete actions with confirmation dialog.
- Create types/timeSlot.ts with enriched TimeSlot interface
- Add Tijdsloten tab to EventTabsNav between Publiekslijsten and Secties
- Create time-slots page with loading, error, and empty states
- Remove time slots panel from SectionsShiftsPanel
- Update CreateShiftDialog to navigate to time slots tab
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The backend only has POST (add) and DELETE (remove) for crowd list
persons — no GET to list them. Reworked the detail panel to show
person count from the crowd list data instead of fetching individual
persons.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>