Commit Graph

357 Commits

Author SHA1 Message Date
66e4167c03 refactor: identical VDataTable for members on both organisation pages
Both org pages now use the same VDataTable with:
- Search field (name/email filter)
- Sortable columns (Naam, E-mail, Rol) with default sort on name
- Pagination (10 per page)
- Avatar with initials, role chips with color mapping
- Consistent empty state with icon

Platform page: replaced VTable with VDataTable, added role chips
(replacing inline AppSelect), role editing via menu on edit button.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 01:22:01 +02:00
ca275723db fix: use consistent text-body-1 text-disabled for timestamps
Replace custom text-caption span with the standard
<p class="text-body-1 text-disabled mb-0"> pattern used across
all other pages in the codebase.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 01:16:29 +02:00
c7dd6aa59c fix: slug in parentheses, capitalize status, lighter timestamps, rename button
Both organisation pages: slug wrapped in parentheses, billing status
label capitalized, timestamps use text-disabled for lighter appearance,
edit button labeled "Bewerken" consistently.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 01:15:16 +02:00
1629b514e2 fix: unify date formatting and add missing updated_at timestamp
Both organisation pages now use the same date format:
"14 april 2026 om 01:11 uur". Added missing "Gewijzigd op" timestamp
to the organizer organisation page header.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 01:13:22 +02:00
1e5aa3c06b fix: align organisation page header layout with platform design
Match the header structure of /organisation to /platform/organisations/[id]:
wrap name+chip in a flex row with gap-x-2, place timestamp as span
below it inside the same container div.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 01:10:25 +02:00
c1bacb5ee9 refactor: show organisation slug after name in header
Display the organisation slug in small muted text directly after the
organisation name on both the organizer page (/organisation) and the
platform admin detail page (/platform/organisations/[id]).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 01:08:31 +02:00
b69d7c9488 Revert "refactor: show event slug after name in header"
This reverts commit 13ea2304b3.
2026-04-15 01:07:17 +02:00
13ea2304b3 refactor: show event slug after name in header
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>
2026-04-15 01:06:05 +02:00
9923dab0f8 refactor: move KPI cards into Algemeen tab, Danger Zone into own tab
Platform org detail:
- KPI cards (events, users, persons) moved inside Algemeen tab
- Danger Zone moved from below tabs into a dedicated "Danger Zone" tab
  with red-colored tab icon
- Tab bar now shows: Algemeen | Leden | Danger Zone

Platform user detail:
- Added VTabs with Algemeen (profile info) and Organisaties tabs
- Timestamps moved below title as muted caption
- Content reorganised into tab structure matching org detail pattern

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 01:04:42 +02:00
a8a2bc92d1 feat: refactor organisation pages with tabs, members tab, and danger zone
Organizer org page (/organisation):
- Timestamps moved below title as muted caption
- VTabs with Algemeen (details) and Leden (members) tabs
- Members content embedded from separate page with full functionality:
  invite, edit role, change email, remove, pending invitations

Platform org detail (/platform/organisations/[id]):
- Timestamps moved below title alongside slug
- VTabs with Algemeen and Leden tabs
- Danger zone redesigned: type-to-confirm delete dialog, disabled
  Transfer Ownership button with "Nog niet beschikbaar" tooltip

Navigation:
- Removed standalone "Leden" menu item (now a tab on org page)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 00:59:45 +02:00
f2614f2b48 feat: platform admin member management — invite, remove, role update
Add member management to the platform admin organisation detail page:
- Backend: invite (creates invitation or directly adds existing user),
  remove member, update member role endpoints on AdminOrganisationController
- Backend: show endpoint now returns members alongside organisation data
- Frontend: members table with inline role editing, invite dialog,
  remove confirmation dialog on /platform/organisations/[id]
- Tests: 7 new tests covering happy paths and edge cases (self-removal,
  existing member, non-super_admin denied)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 00:37:29 +02:00
b6ef6ec383 fix: login response missing app_roles — platform nav not showing
LoginController used UserResource (returns `roles`) but the frontend
authStore.setUser() expects MeResponse format with `app_roles`. After
login, appRoles was set to undefined, making isSuperAdmin always false.
Combined with isInitialized staying true after the initial failed
/auth/me call, the correct /auth/me was never re-fetched after login.

Fix: use MeResource in LoginController (same as MeController) so the
login response includes app_roles, permissions, and portal_events.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 00:18:19 +02:00
9e7f28420c feat: platform admin frontend — pages, composables, navigation, impersonation
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>
2026-04-14 23:49:36 +02:00
07ba791405 docs: complete event statuses documentation page
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 23:44:13 +02:00
ddf26dad33 feat: platform admin backend — controllers, services, routes, tests
Add cross-organisation admin API endpoints behind role:super_admin middleware:
- AdminOrganisationController: CRUD with search, filter, billing_status management
- AdminUserController: user management with role assignment across orgs
- AdminStatsController: platform-wide aggregate statistics
- AdminActivityLogController: filterable activity log viewer
- AdminImpersonationController + ImpersonationService: user impersonation with
  token-based session management and activity logging
- BillingStatus enum, form requests, API resources, 23 feature tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 23:33:16 +02:00
ec31646a93 fix: shift dialog info alert layout and full-width toggle
Made-with: Cursor
2026-04-14 22:58:33 +02:00
103d57c979 refactor: polish shift dialog with Vuexy-style alert and sticky footer
Made-with: Cursor
2026-04-14 22:54:47 +02:00
8afee801f8 feat: make shift dialog time-slot help collapsible
Made-with: Cursor
2026-04-14 22:47:20 +02:00
1c3ce547fa refactor: polish shift create dialog layout and hierarchy
Made-with: Cursor
2026-04-14 22:45:12 +02:00
1c6aed71fe fix: replace InfoTooltip v-tooltip with v-menu popover card
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>
2026-04-14 22:41:01 +02:00
cc7cbbf29d fix: use inline style for time slot dimming in teleported dropdown
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>
2026-04-14 22:37:48 +02:00
948965e664 fix: time slot dropdown group headers and dimming via boundary detection
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>
2026-04-14 22:31:32 +02:00
5bd028f408 refactor(app): event header status menu and volunteer share dialog
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
2026-04-14 22:19:09 +02:00
7bc0f1a0c7 feat: fix time slot hierarchy — seeder, API include_children, frontend dropdown, navigation
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>
2026-04-14 22:07:37 +02:00
acb7fb2c3a fix: show event_type_label on events list cards
Made-with: Cursor
2026-04-14 21:51:08 +02:00
c4712cea77 feat: edit event type label in dialog, drop non-functional status field
Made-with: Cursor
2026-04-14 21:48:07 +02:00
eec222d423 feat: toon leeftijd naast geboortedatum in persoon-detailpanel
Made-with: Cursor
2026-04-14 21:41:08 +02:00
cb16cf9091 fix: cross_event section shifts use festival-level time slots in seeder
EHBO and Accreditatiebalie shifts referenced sub-event time slots
(Vrijdag Early, Zaterdag Dag, etc.) but these cross_event sections
belong to the parent festival. The dropdown fetches time slots for
the festival event, so the sub-event time slot IDs had no matching
items — causing raw ULIDs in the dropdown and validation failures.

Added festival-level VOLUNTEER time slots mirroring the sub-event
schedule, and pointed cross_event section shifts at those instead.
Verified: all 81 shifts now reference time slots from the same event
as their section (zero mismatches).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 21:02:53 +02:00
cf02500453 fix: shift edit time slot dropdown loading state and test coverage
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>
2026-04-14 20:46:36 +02:00
ea159a34fe docs: add development prompts and vibe coding checklist
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 19:04:19 +02:00
11e379a5b9 feat: add useShiftDetailStore for shift detail panel state
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 19:04:17 +02:00
185637fa50 feat: add EmailBrandingTab component for organisation email branding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 19:04:13 +02:00
ae7ba63822 feat: add PersonStatus enum and PortalMeRequest form request
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 19:04:10 +02:00
312663fb02 chore: update admin typed-router definitions
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 19:04:06 +02:00
f1b92531c4 chore: update .gitignore and remove obsolete corporate identity assets
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 19:04:03 +02:00
3e93048461 docs: update BACKLOG.md with current project status
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 18:50:55 +02:00
a29fa32ac6 feat: add "Lid toevoegen als deelnemer" shortcut for org members
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>
2026-04-14 18:38:53 +02:00
624756e505 docs: add user documentation for core organizer concepts
Add three documentation pages:
- Leden vs Personen: explains the difference between platform members and event participants
- Crowd Types: full reference for the 7 person types with access matrix
- Diensten plannen: enhanced with 4-layer model, assignment strategies, and overbezetting

Also adds placeholder images to fix VitePress build and updates sidebar navigation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 18:30:23 +02:00
ed1eddd486 fix: allow organiser to approve shift assignments when shift is full
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>
2026-04-14 17:42:04 +02:00
a9ef384515 fix: prevent cross-app auth session sharing on localhost
Root cause: browsers don't scope cookies by port. With SESSION_DOMAIN=
localhost, all three SPAs share cookies. The CookieBearerToken middleware
iterated all cookie names and picked the first match, so logging into
the organizer app (port 5174) also authenticated the portal (port 5175).

Fix: CookieBearerToken now resolves the correct cookie name from the
Origin header (same logic as SetAuthCookie trait). It only reads the
cookie matching the requesting app — portal origin reads only
crewli_portal_token, app origin reads only crewli_app_token, etc.

Falls back to first-available cookie when no Origin header is present
(server-to-server requests, tests without explicit Origin).

Added 3 cross-app isolation tests:
- app cookie does NOT authenticate portal requests
- portal cookie does NOT authenticate app requests
- correct cookie + matching origin = authenticated

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 17:19:42 +02:00
821bfc5bcf chore: standardize dev-only debug logging across all three SPAs
Router guards:
- apps/app: added DEV-gated logging matching admin pattern
  (route info, auth decisions, org selection, access granted/denied)
- apps/portal: added DEV-gated logging matching admin pattern
  (route info, auth decisions, backward-compat redirects)
- apps/admin: already had full logging (unchanged)

Ungated console statements fixed:
- admin/main.ts: error handler, plugin registration, mount errors
- admin/pages/login.vue, register.vue: catch block errors
- admin/pages/events/index.vue: fetch error logging
- admin/pages/wizard-examples: demo form submit logging
- admin/pages/faq.vue: catch block error

All console statements in Crewli-authored code are now gated behind
import.meta.env.DEV — zero console output in production builds.
Vuexy template demo files (views/demos/*) left as-is.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 16:43:31 +02:00
e53f364929 fix: guard against Pinia not ready in admin router (plugin load order)
The admin app's 1.router plugin loads before 2.pinia. During
router.install(), the initial navigation triggers the index redirect
and beforeEach guard, both of which call useAuthStore() — but Pinia
isn't registered yet, causing a crash.

Fix: wrap useAuthStore() in try/catch in both additional-routes.ts and
guards.ts. On the initial router install navigation, the catch falls
back to redirecting to login / allowing navigation. Once Pinia is
available on subsequent navigations, the store works normally.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 16:36:43 +02:00
887a8fccde chore: move VitePress dev server to port 5176 (avoid admin port conflict)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 16:31:23 +02:00
8ace0480ae fix: handle 401 gracefully in auth initialization after httpOnly migration
Race condition: the axios 401 interceptor uses a dynamic import, so
handleUnauthorized() fires AFTER doInitialize() sets isInitialized=true.
handleUnauthorized() then reset isInitialized to false, leaving the app
stuck on a loading spinner with no way to recover.

Fix: remove isInitialized=false from handleUnauthorized() in all three
apps. When handleUnauthorized() redirects via window.location.href, all
JS state resets naturally. When it skips the redirect (already on a
public page like /login), the app should render normally in an
unauthenticated state.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 16:24:58 +02:00
b5fcb7c14a fix: add Google Fonts domains to CSP policy
Vuexy loads fonts via webfontloader from fonts.googleapis.com and
fonts.gstatic.com. The previous CSP blocked these, causing a white screen.

- style-src: added https://fonts.googleapis.com
- font-src: added https://fonts.gstatic.com
- Removed frame-ancestors from meta tags (ignored in meta, console warnings)

Updated in all three index.html dev meta tags and both Nginx SPA/portal configs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 16:20:51 +02:00
940297f214 security: implement CSP headers (API middleware + Nginx configs + dev meta tags)
API middleware:
- SecurityHeaders now sets Content-Security-Policy from config/security.php
- Default API policy: "default-src 'none'; frame-ancestors 'none'"
- Supports report-only mode via CSP_REPORT_ONLY env var
- Policy value configurable via CSP_POLICY env var

Nginx deployment configs (deploy/nginx/):
- security-headers.conf: shared headers for all server blocks
- csp-api.conf: restrictive JSON-only policy for api.crewli.app
- csp-spa.conf: SPA policy for app/admin (self + unsafe-inline styles)
- csp-portal.conf: portal policy matching SPA

Development:
- CSP meta tags added to all three index.html files
- Includes 'unsafe-inline' + 'unsafe-eval' for Vite HMR/loader script
- Each app allows its own ws:// port for HMR websocket

Resolves security finding A13-9.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 16:14:37 +02:00
513ca519b2 security: migrate auth tokens to httpOnly cookies (hybrid bearer token approach)
Backend:
- CookieBearerToken middleware reads httpOnly cookie and injects Authorization
  header before Sanctum validates (prepended to API middleware group)
- SetAuthCookie trait provides cookie creation/expiry helpers with per-app
  cookie names (crewli_admin_token, crewli_app_token, crewli_portal_token)
- LoginController sets token via Set-Cookie, removes it from JSON body
- LogoutController expires the auth cookie on logout
- AuthRefreshController (POST /auth/refresh) rotates tokens with new cookie
- InvitationController accept also sets token via cookie, not JSON body
- All cookies: httpOnly, SameSite=Strict, Secure (in production)

Frontend (all three SPAs):
- Removed all localStorage token storage (apps/app, apps/portal)
- Removed all JS-readable cookie token storage (apps/admin)
- Removed Authorization: Bearer header interceptors from axios
- Auth stores now rely on GET /auth/me to validate httpOnly cookie
- Admin app: new Pinia auth store replaces useCookie-based auth pattern
- withCredentials: true ensures browser sends cookies automatically

Fixes security findings A13-1 (localStorage tokens) and A13-2 (admin
cookie flags). Tokens are now invisible to JavaScript.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 16:06:44 +02:00
836cffa232 feat: password reset, email change with verification, and password change
Password reset: multi-app support with custom notification linking to correct
frontend (app/portal/admin). Email change: self-service with password
confirmation and admin-initiated, both sending verification to new address
with 24h expiry. Confirmation sent to old email on completion. Password
change: authenticated endpoint revoking other sessions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 15:38:54 +02:00
53100d4f6d feat: portal cross-event my-shifts endpoint and dashboard page
GET /portal/my-shifts aggregates shift assignments across all events
the logged-in user is linked to via Person records. Groups by event
then date, showing only active assignments (approved/pending_approval)
for approved/pending persons.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 15:07:08 +02:00
d4004c798c feat: show identity match hint on registration success page
When a pending identity match is detected after volunteer registration,
the API now returns has_existing_account in the response. The success
page shows a login suggestion card so the volunteer can link their
registration to their existing account.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 08:56:25 +02:00