Commit Graph

52 Commits

Author SHA1 Message Date
5c689f42a0 feat(router): wire portal/register pages, portal-context guard carve-out, lint cleanup
Routing wiring (Phase D of WS-3 PR-B1):

- apps/app/src/plugins/1.router/guards.ts: add a single early-return
  carve-out before the org-selection redirect — `if (to.meta.context
  === 'portal') return`. Per ARCH-CONSOLIDATION-2026-04 §4.3,
  meta.context is the canonical contract; PR-B2 evolves the guards
  from this key to full context-aware logic (post-login landing,
  context-switcher, role checks).
- apps/app/env.d.ts: extend RouteMeta with the new layout names
  ('OrganizerLayout' | 'PortalLayout' | 'PublicLayout'), context,
  requiresAuth, requiresToken, navMode, navTitle.
- apps/app/typed-router.d.ts: regenerated by unplugin-vue-router to
  pick up portal/* and register/* route names.
- Page meta finalisation: portal pages have layout: 'PortalLayout',
  context: 'portal', preserving original requiresAuth + nav fields;
  register pages have layout: 'PublicLayout' + public: true (the
  apps/app guard convention for public routes, since meta.public is
  what the existing guard recognises).

Form-types restructure (boundaries cleanup):

- apps/app/src/composables/forms/types/formBuilder.ts → src/types/forms/
- apps/app/src/composables/forms/utils/{formValidation,validators}.ts
  → src/utils/forms/
- All `@/composables/forms/{types,utils}/*` imports rewritten across
  pages, components, composables, tests.
- This avoids a `types → composables` boundaries violation at
  src/types/formSchema.ts which re-exports primitives from the
  inlined form-schema. types/formSchema.ts now imports from
  @/types/forms/formBuilder which is in the same boundaries zone.

Lint cleanup for moved portal sources (apps/portal had no
.eslintrc.cjs; the migrated code now has to pass apps/app's stricter
config):

- axios.isAxiosError → named import { isAxiosError }
  (ClaimenTab, RoosterTab, profiel.vue)
- void schemaQuery.refetch() → schemaQuery.refetch()
  (register/[public_token].vue)
- if-then-else collapsed to single boolean return (formatFieldValue)
- :delay-on-touch-only="true" → delay-on-touch-only shorthand
  (FieldSectionPriority)
- ml-2 class → ms-2 (FieldAvailabilityPicker)
- multi-statement-per-line splits in profiel.vue + spec files
- unused emailConfigured ref removed (profiel.vue)
- one-component-per-file disabled with TODO TECH-WS3-PORTAL-LINT-CLEANUP
  ref (FieldOptionsLocale.spec.ts — multi-Wrapper test pattern)
- restored `import Draggable from 'vuedraggable'` after lint:fix
  removed it (template-only usage; the import IS needed)
- camelcase param renamed in FieldOptionsLocale harness factory
- typecheck nudge: spec state.data typed via PublicFormSectionOption[] /
  PublicFormTimeSlot[] aliases instead of Record<string, unknown>
- PortalLayout.vue: explicit `import { useRoute, useRouter }` so the
  vitest mock can intercept (the trimmed AutoImport set doesn't pull
  vue-router's auto-imports)

Vitest: 23 / 162 passing. Lint: 0 errors / 0 new warnings (only the
pre-existing boundaries v5→v6 deprecation warnings remain). Typecheck:
clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:26:46 +02:00
e3452312d1 refactor(layouts): merge portal navbar/drawer into PortalLayout.vue
Migrates the navbar (event/platform two-mode toggle), mobile drawer
with avatar header + logout, RouterView Suspense wrapper, and footer
from apps/portal/src/layouts/portal.vue into the PortalLayout.vue
skeleton from PR-A. The skeleton's structure (VApp / VAppBar / VMain
/ VFooter) is preserved as the outer shell.

Notable adaptations:
  - useAuthStore → usePortalAuthStore (renamed in C.3)
  - usePortalStore import path → @/stores/portal/usePortalStore
  - mobile nav links now point at /portal/evenementen and /portal/profiel
    (the new sub-zone paths) instead of /evenementen and /profiel
  - explicit `import { useRoute, useRouter }` from vue-router so the
    vitest mock can intercept (auto-import not configured for these in
    the trimmed test config)

Updated PortalLayout.spec.ts to mock the two pinia stores plus
useSkins, vue-router, UserAvatarMenu, and AppLoadingIndicator. Tests
now assert the auth-conditional rendering: header + drawer hidden
when unauthenticated, main + footer always present.

Also pulls in the @form-schema → @/composables/forms/* import
rewrites in the C.4-moved composables that the previous commit's
rename-only diff left unstaged.

Vitest: 23 files / 162 tests, no errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:11:58 +02:00
7282861a7e refactor(portal): move composables, types, schemas; drop duplicates
Composables (apps/portal/src/composables → apps/app/src/composables/):
  - useFormDraft, publicFormInjection → composables/ (root, used by
    shared/public-form components)
  - api/usePublicForm, api/usePublicFormSections,
    api/usePublicFormTimeSlots → composables/api/ (no collisions)
  - api/usePortalShifts, api/usePortalProfile, api/useVolunteerRegistration
    → composables/api/portal/ (subfolder per WS-3 PR-B1 charter to
    leave room for organizer-side namesakes without clashes)
  - api/useMfa → DELETED (apps/app version is a strict superset
    with extra invalidateQueries calls and the admin-reset mutation)

Types (apps/portal/src/types → apps/app/src/types/):
  - api, portal-shift, portal, registration → moved
  - mfa → DELETED (byte-identical to apps/app/src/types/mfa.ts)

Schemas:
  - apps/portal/src/schemas/registrationSchema.ts → apps/app/src/schemas/

Utils:
  - deviceFingerprint, paginationMeta → DELETED (byte-identical
    duplicates already in apps/app/src/utils/)

Lib:
  - apps/portal/src/lib/{axios,query-client}.ts → DELETED. apps/app's
    callback-bound axios (post-PR-A) and query-client are the
    canonical versions. Portal pages currently importing
    `@/lib/axios#apiClient` resolve to apps/app's apiClient with no
    behavioral change for cookie-based requests.

Tests: 4 composable specs (useFormDraft x2, usePublicFormSections,
usePublicFormTimeSlots) moved into __tests__/ subdirs alongside
their composables.

@form-schema imports inside the moved files rewritten to
@/composables/forms/*.

Vitest now: 23 files / 162 tests passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:08:53 +02:00
79954aace6 refactor(forms): move packages/form-schema → apps/app/src/composables/forms
Inlines the form-schema source folder (no package.json, alias-only)
into apps/app/src/composables/forms. Drops the @form-schema alias
from apps/app/vite.config.ts (replaced by @/composables/forms via
the existing @ alias). apps/portal vite + vitest configs keep
@form-schema as a temporary alias pointing at the new location so
portal tests/build keep working until apps/portal is removed at the
end of this PR. Two pure-logic form-schema tests moved alongside.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 18:50:52 +02:00
966ded3e44 chore(monorepo): scaffold target sub-folders for WS-3 PR-B1
Creates portal/register/shared/forms sub-folders ahead of the moves
in subsequent commits. Empty .gitkeep markers will be replaced by
real content as the moves land.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 18:44:24 +02:00
1289b217d0 fix(app): resolve Bucket E.2-E.5 lint findings
WS-3 session 1b-ii Task 5b+c (audit Bucket E.2-E.5 — 6 items resolved,
2 promise/no-promise-in-callback warnings remain on dynamic-import
sites — see deviations).

This commit is split out from the originally-planned grouped Task 5
because the API stream timed out mid-session. E.1 (isAxiosError) is in
the preceding commit 0f155d9.

E.2 — vitest spec to Composition API (1× vue/component-api-style):
- useFormFailures.spec.ts: rewrote the test wrapper from
  \`{ setup() { return { result } }, render: () => h('div') }\`
  to \`setup(_, { expose }) { expose({ result }); return () => h('div') }\`.
  Pure Composition API: setup returns the render function; expose()
  declares the instance-visible \`result\` that the 7 \`vm.result.*\`
  assertions consume. Tests still pass green (49 tests).

E.3 — REAL BUG: missing return in computed (1× vue/return-in-computed-property):
- useTimeSlotDropdown.ts:80: the \`fetchParams\` computed had a switch
  over the \`DropdownScenario\` type (4 string-literal cases) without
  a \`default\` branch. If \`scenario.value\` ever returned a value
  outside the four narrowed cases (e.g. via a future type-assertion
  drift), the computed silently returned \`undefined\`, and the
  consumer code (\`fetchParams.value.includeParent\`) would throw
  \`Cannot read property 'includeParent' of undefined\`. Added a
  \`default\` branch returning \`{ includeParent: false, includeChildren: false }\`
  — same as the 'flat' case (the safest baseline: include only own
  slots, no hierarchy).

E.4 — SECURITY (1× vue/no-template-target-blank):
- pages/organisation/index.vue:343: the external website anchor had
  \`target='_blank'\` with \`rel='noopener'\` (only one). The rule
  requires the full \`rel='noopener noreferrer'\` pair. Updated.
  Mitigates reverse-tabnabbing (window.opener) AND referrer-leakage
  to the linked third-party site.

E.5 — axios fire-and-forget (3× promise/no-promise-in-callback,
1 fully resolved + 2 warnings remain):
- lib/axios.ts:42: changed \`error => Promise.reject(error)\` to
  \`async error => { throw error }\`. Semantically identical (axios
  interceptor onRejected returns a rejected promise either way) and
  satisfies the lint rule.
- lib/axios.ts:61, 73: prefixed the dynamic-import chains with \`void\`
  per Q4's option-a decision (\`void import('@/stores/...').then(...)\`).
  This makes the discard intent explicit, but empirically does NOT
  satisfy promise/no-promise-in-callback — the rule fires on any
  promise creation inside a callback, regardless of the discard
  pattern. The 2 warnings remain in the post-Task-5 baseline.
  Resolution path is Bert's call: either keep \`void\` and accept
  the warnings as documentation, or rewrite to \`async error => {
  const { useStore } = await import(...); ... }\` which sequentializes
  the dynamic-import resolution with the rejection. Out of scope for
  this session per the literal Q4 recipe.

Tests + typecheck verified green.

Lint baseline: 34 → 32.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 15:15:29 +02:00
d407cd17de fix(app): resolve Bucket B (type safety) lint items
WS-3 session 1b-ii Task 3 (audit Bucket B — 34 items: 21 absorbed
via ignorePatterns + 14 real fixes; the count of 21 is the actual
non-Tier-3 lint-count drop from the .eslintrc edit, slightly above
the audit's predicted 20 because additional vendored-Vuexy items
beyond the 23 no-explicit-any landed in those paths too).

Config:
- .eslintrc.cjs: add src/@core/** and src/@layouts/** to ignorePatterns.
  Vendored Vuexy code, precedent: src/plugins/iconify/*.js. The
  CLAUDE.md no-any rule remains in force for our own code under src/.

Real type-safety fixes:
- B.1 ref<any> in our code (3 occurrences):
  * blank.vue / default.vue: AppLoadingIndicator template ref now
    typed as InstanceType<typeof AppLoadingIndicator> | null. Picks
    up the defineExpose'd fallbackHandle / resolveHandle methods.
  * NavSearchBar.vue:109: useApi<any>(...) → useApi<SearchResults[]>(...)
    matching the existing searchResult ref type.
- B.2 ShiftDetailPanel.vue: moved the Cancel-dialog ref declarations
  (isCancelDialogOpen, cancellingAssignment) from line 305-307 to
  line 248 — directly above the onCancel handler that uses them.
  Resolves all 7 no-use-before-define items in one move. Same-file,
  no logic change.
- B.3 useImpersonationStore.ts:119: renamed inner 'stored' to
  'storedSnapshot' to resolve shadowing of the outer 'stored' on
  line 18.
- B.4 useFormSchemas.ts:97-99: renamed local mutationFn parameter
  'confirmed_name' to camelCase 'confirmedName'. Wire-format key
  stays snake_case via destructure-alias:
    params: confirmedName ? { confirmed_name: confirmedName } : undefined
  No callers found in apps/app/src — safe rename.

Tests + typecheck verified green.

Lint baseline: 97 → 62.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 14:11:05 +02:00
a7eaf0f948 style(app): apply eslint --fix to Tier 2 (TypeScript plumbing)
WS-3 session 1b-i Tier 2.

Scope: composables, lib, stores, plugins, types, utils, navigation,
main.ts. Mechanical fixes only — predominantly newline-before-return,
arrow-parens, antfu/if-newline, padding-line-between-statements, plus
one unicorn/prefer-includes (.some(p => x === p) → .includes(x))
in router guards.

Excludes (per session prompt):
- apps/app/vite.config.ts (Tier 3)
- apps/app/themeConfig.ts (Tier 3)
- apps/app/vitest.config.ts (Tier 3)
- All .vue files (already in Tier 1)

Hand-reviewed diffs for the three auth/router-critical files before
committing:
- src/lib/axios.ts: reviewed clean. Pure mechanical (quote-props on
  Accept header, curly-strip on single-statement ifs, one blank line
  before impersonationStore.clearState()). No type-import changes,
  no logic touched.
- src/stores/useAuthStore.ts: reviewed clean. curly-strip + padding
  before returns. The initialize()/doInitialize() race-condition guard
  on isInitialized is preserved verbatim.
- src/plugins/1.router/guards.ts: reviewed clean. if-newline reformat
  + one .some() → .includes() rewrite that's behaviorally identical
  for primitive equality on the guestOnlyPaths string array.

Tests + typecheck verified green post-fix:
- apps/app vitest: 49 passed (unchanged)
- apps/app vue-tsc: clean (unchanged)

Lint baseline progression:
- Pre-Tier-2: 422 problems (post-Tier-1)
- Post-Tier-2: 246 problems

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 11:06:46 +02:00
192353f4bc feat(form-builder): admin UI completion — server filters, KPIs, resource expansion (WS-6 sessie 3c)
Closes the four production gaps that emerged from sessie 3b's admin UI.
What we ship here is final: no further rework planned before production.

Backend
- IndexFailuresRequest validates state/search/failed_at_from/failed_at_to/
  listener_class. orgIndex + platformIndex apply them via a single
  applyIndexFilters() helper. Search runs case-insensitive `LIKE` on
  exception_message; SQL wildcards in user input are escaped.
- New /kpis aggregate endpoint per scope (orgKpis, platformKpis) returns
  open / resolved_30d / dismissed_30d / total_submissions in O(1) COUNTs.
  Replaces sessie 3b's client-side bucketing of an oversized list.
- Resource expansion: organisation_name, form_schema_label,
  resolved_by_user_name, dismissed_by_user_name, exception_trace,
  retry_history[]. Eager-loading via indexEagerLoads()/detailEagerLoads()
  prevents N+1 (verified by query-count assertion in test).
- New 2026_04_28_181000 migration adds exception_trace (longtext nullable)
  to form_submission_action_failures. ApplyBindingsOnFormSubmit listener
  now captures $e->getTraceAsString() at failure time.
- New FormSubmissionActionFailureRetryAttemptResource exposes per-attempt
  data (timestamp, actor name, outcome, exception details) inside
  retry_history[]. Index payloads omit the field via whenLoaded() to keep
  list responses lean.

Frontend (apps/app)
- Types updated to mirror the expanded resource shape and the new KPI
  endpoint contract. FormFailuresKpis is now { open, resolved_30d,
  dismissed_30d, total_submissions } (server-aggregate).
- useFormFailures composable forwards all 5 server filters via
  buildIndexParams() (strips empty/whitespace). useFormFailuresKpis hits
  the dedicated /kpis endpoint per scope.
- FormFailuresTable replaces client-side bucketing with server-side
  filtering, adds listener_class + date-range filter inputs, and renames
  the 4th KPI tile to "Submissions" (was "Totaal").
- FormFailureDetail renders organisation_name + form_schema_label in the
  header, surfaces an expandable stack-trace card, names the resolved/
  dismissed actor in the timeline, and replaces the "v1 placeholder"
  retry-history card with a full per-attempt timeline.

ESLint config gap (apps/app)
- New .eslintrc.cjs adapted from the Vuexy reference, minus Vuexy-internal
  rules. `pnpm lint` now runs successfully (was previously broken — the
  package.json script referenced a missing config). The 80 baseline
  violations across the codebase are pre-existing and out of scope for
  this session.

Tests + gates
- 24 new backend tests across filter, kpis, and resource-shape suites.
  Backend: 1462 → 1486 passing, 0 → 0 failing. Larastan clean. Rector
  dry-run unchanged at 354 (pre-Task-1 baseline from f18b55b).
- 3 new vitest tests in apps/app (filter wiring, KPI endpoint, KPI tile
  values from /kpis). Vitest: 38 → 41 passing. tsc clean. Portal
  unchanged (113 vitest, tsc clean).
- 5 backfill rollback tests bumped --step counts +1 for the new migration.
- Ws6FoundationMigrationTest down/up chain now includes exception_trace
  before the parent table is restored.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 00:14:20 +02:00
4cbe2c453b feat(form-failures): useFormFailures composable + types (WS-6)
TanStack Vue Query composables for the FormSubmissionActionFailure
admin endpoints landed in WS-6 sessie 2:

  - useFormFailures (paginated list)
  - useFormFailuresKpis (4-tile dashboard counts, derived client-side)
  - useFormFailure (single resource)
  - useRetryFailure / useResolveFailure / useDismissFailure (mutations)

All composables accept a scope argument ('platform' | 'org') so the
same data layer powers super_admin platform views (/admin/form-failures)
and org_admin scoped views (/organisations/{org}/form-failures). Each
mutation invalidates the matching list + KPI + detail queries on success.

Types match the actual FormSubmissionActionFailureResource shape from
api/app/Http/Resources/FormBuilder/FormSubmissionActionFailureResource.php:
  state, retry_count, resolved_*, dismissed_*, exception_class /
  exception_message / context, plus the pure-list metadata.

Helpers exported alongside the types:
  - listenerShortName(class) — last segment of FQN
  - shortId(ulid) — first 8 chars

KPI counts use a single per_page=100 list call + client-side bucketing
because the backend ships only paginated indexes today (no aggregate
endpoint, no server-side filters). Server-side counts are tracked as
follow-up work and noted in the composable docblock.

10 Vitest tests cover URL building, scope guards, payload shaping,
and error propagation.

Refs: WS-6 sessie 2 (backend), sessie 3b admin UI Task 1

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 00:14:18 +02:00
7df37b8823 feat(form-builder): form schema types and TanStack Vue Query composables
Adds apps/app/src/types/formSchema.ts with FormSchema, FormSchemaSummary,
FormSchemaPurpose, FormSubmissionMode, FormSchemaSnapshotMode, and the
payload/response shapes for schema CRUD plus lifecycle operations
(publish, unpublish, duplicate, rotate-public-token).

Adds apps/app/src/composables/api/useFormSchemas.ts mirroring the
useSections pattern: useFormSchemaList, useFormSchema, plus seven
mutations covering CRUD, duplicate, publish/unpublish and public-token
rotation. All queries and mutations invalidate the right cache keys.

Fields and sections on the full FormSchema are typed as unknown[] with
a TODO pointing to PR-b3 when the organizer field types land. No UI,
routes, or navigation — those come in PR-b2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 01:52:44 +02:00
027c5dac4e feat(organisation): expand /organisation page to full dashboard
Replace the minimal placeholder with a dashboard: header + edit action,
drie stat-tegels (Leden / Evenementen / Personen — de eerste twee
clickable), organisatiegegevens + leden-top-5 infokaarten en een recente-
activiteit lijst. Nieuwe TypeScript-types en useOrganisationDashboardStats
composable sluiten aan op de nieuwe backend-endpoint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 10:27:51 +02:00
0ca7c0f20f refactor(members): consolidate Platform Admin + Org members into shared useMembers
- useMembers.ts gains a scope param ('organisation' | 'platform') on list,
  invite, update-role, and remove; endpoints branch accordingly.
- Platform Admin's [id].vue now consumes useMembers via scope='platform';
  deleted the duplicated useInviteOrganisationMember / useRemoveOrganisationMember
  / useUpdateOrganisationMemberRole helpers from useAdmin.ts.
- Deduplicated InviteMemberPayload / UpdateMemberRolePayload / AdminOrganisationMember
  from types/admin.ts; Member is now the canonical type.
- SettingsMembers.vue and EditMemberRoleDialog.vue removed (no remaining imports).
- InviteMemberDialog accepts an optional scope prop and is restricted to the
  two organisation-level roles matching the /members UX.
2026-04-16 22:30:42 +02:00
4df668b5b8 feat: replace token-based impersonation with enterprise-grade header-based system
Replaces the insecure token-in-localStorage approach with a header-based
impersonation system backed by cache sessions and MFA verification.

Key changes:
- New impersonation_sessions audit table (immutable, ULID PK)
- MFA verification required to start impersonation (TOTP/email/backup)
- X-Impersonate-User header + HandleImpersonation middleware
- Per-request auth context swap (admin session never modified)
- IP pinning, sensitive route blocking, no nesting, sliding 60-min TTL
- Activity log auto-tagged with impersonated_by during sessions
- Frontend: sessionStorage, BroadcastChannel sync, countdown timer
- ImpersonateDialog with reason + MFA verification flow
- 26 comprehensive tests covering core, middleware, audit, lifecycle

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 02:42:53 +02:00
d5fb15e5fe feat: set preferred MFA method from account settings
Adds the ability for users to change their preferred/primary MFA method
when both TOTP and email are available.

Backend:
- Add PUT /auth/mfa/preferred-method endpoint with validation
  (method must be totp/email, MFA must be enabled, TOTP must be
  configured if selecting totp)
- Add totp_configured and email_configured fields to MFA status
  endpoint (totp = has secret + enabled, email = always when enabled)
- Fix setupEmail() to preserve mfa_secret so TOTP config survives
  when email is set up as a second method

Frontend (organizer + portal):
- Add useSetPreferredMethod() composable to useMfa.ts
- Add totp_configured/email_configured to MfaStatus type
- SecurityTab method cards now show "Primaire methode" chip on the
  preferred method and "Als primair instellen" button on the other
- Portal security section shows per-method rows with status chips
  and primary switching

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 22:47:34 +02:00
79b7fe0b42 feat: account settings with Vuexy tab pattern and MFA banner fix
Restructures account/profile pages to match Vuexy's account-settings
tab pattern (Account, Security, Notifications) and fixes the MFA
enforcement banner that stayed visible after successful setup.

Backend:
- Add phone column to users table with migration
- Add PUT /me/profile endpoint for profile updates
- Create UpdateProfileRequest form request
- Update MeResource to include phone field

Organizer app:
- Rewrite account-settings as tabbed page (VTabs pill style + VWindow)
- Create AccountTab: avatar, profile form, email change, danger zone
- Create SecurityTab: password change, MFA method cards, backup codes,
  trusted devices, disable MFA danger zone
- Create NotificationsTab: placeholder with disabled toggles
- Fix MFA banner: set authStore.mfaSetupRequired = false on setup complete
- Update router guard to redirect to ?tab=security for MFA enforcement
- Update UserProfile menu links to use tab query params

Portal:
- Restructure profiel.vue with VTabs (Mijn profiel + Beveiliging)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 22:18:16 +02:00
0be2956ea4 feat: MFA frontend with auth page restyling, challenge screen, and setup wizard
- Restyle organizer auth pages: Dutch text, remove placeholder social login
- Restyle portal auth pages to Vuexy v1 centered card pattern with decorative shapes
- MFA challenge card component with VOtpInput, method tabs, backup code input,
  trusted device checkbox, and session countdown timer
- Login pages handle mfa_required response with device fingerprint header
- Security settings page with TOTP setup (QR code), email setup, disable MFA,
  backup codes regeneration, and trusted devices management
- Portal profile page includes MFA security section
- Admin user detail page shows MFA status with reset button
- MFA enforcement route guard redirects to security settings when required
- Device fingerprint utility for trusted device identification
- MFA types, composables with TanStack Query for both apps

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 21:32:17 +02:00
df68aa8aef feat: email infrastructure frontend — settings, templates, and log tabs
Adds three new tabs to the organisation settings page:

- E-mail opmaak: replaces old EmailBrandingTab to use the new
  organisation_email_settings API (logo, colors, footer, reply-to)
- E-mail templates: list/edit/preview/test/reset all 6 template types
  with variable hints, defaults comparison, and iframe preview
- E-mail log: server-side paginated table with filters (search, status,
  type, date range), status chips, and expandable row details

Supporting files:
- types/email.ts: TypeScript interfaces for settings, templates, logs
- composables/api/useEmail.ts: TanStack Query hooks for all email endpoints

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 20:28:38 +02:00
2933d957a6 feat: add create organisation button and dialog on platform page
Add "Nieuwe organisatie" button to the platform organisations list page.
Dialog with name field (auto-generates slug) and slug field. Uses the
existing POST /organisations endpoint. On success, navigates to the
new organisation's detail page.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 01:27:40 +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
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
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
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
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
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
eb1a0ac666 feat: complete person identity matching system with fuzzy detection, revert, and manual link
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>
2026-04-14 08:44:24 +02:00
7932e53daf security: A01-13 — nest all event routes under organisation prefix
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>
2026-04-14 08:16:36 +02:00
a9dcee0fc7 feat(app): registration fields management page in event settings
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>
2026-04-12 23:44:14 +02:00
1c0ac488b0 feat(app): organisation settings page with tags & registration field templates
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 23:02:07 +02:00
1172c41d33 feat(app): event status transitions on detail header
Add transition buttons from allowed_transitions with Dutch labels,
confirmation dialog, TanStack mutation + cache invalidation, and
422/generic error handling via notification store.

Made-with: Cursor
2026-04-12 22:20:36 +02:00
5b173e59c1 fix: ververs crowd list tellers in detailzijbalk na verwijderen persoon
Made-with: Cursor
2026-04-12 14:00:02 +02:00
04ceecc51d feat: enrich assignable-persons with tags, preferences, availability and cascading filters
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 22:05:02 +02:00
0d741550a8 feat: event registration branding with vertical wizard layout
- Add registration_banner_url, registration_welcome_text, registration_logo_url
  columns to events table with migration
- Add uploadImage endpoint (POST .../upload-image) with form request validation
  for banner and logo images (jpg/png/webp, max 5MB)
- Include branding fields in EventResource and PublicRegistrationDataController
- Build registration settings UI in organizer event settings page with
  banner/logo upload and welcome text editor
- Redesign portal registration page: hero banner with gradient overlay,
  welcome text card, vertical step navigation (desktop) / horizontal chips
  (mobile), two-column form fields with density="comfortable"
- Update success page with event banner and consistent branding
- Seed welcome text for Echt Feesten 2026
- Add 9 PHPUnit tests covering image upload, branding fields in API responses

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 21:09:49 +02:00
d1ad0e1f89 fix: refresh assignable persons list after assignment and keep dialog open
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>
2026-04-10 20:37:38 +02:00
968e17c6d6 feat: smart assign person dialog with conflict details and assignable-persons endpoint
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>
2026-04-10 20:32:31 +02:00
874eeee770 feat: event dashboard metric cards with stats endpoint (UX-02)
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>
2026-04-10 16:19:31 +02:00
e70904741d feat(app): dedicated Tijdsloten tab with grouped view and fill rates
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>
2026-04-10 15:47:36 +02:00
ee1ee6f41d feat(app): enhanced crowd list detail panel with person management
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 15:04:36 +02:00
e14cfe8ae2 fix: remove non-existent GET persons endpoint from crowd list detail panel
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>
2026-04-10 14:19:40 +02:00
331f662c67 feat: crowd lists frontend with list view, create/edit dialog and person management
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 14:14:17 +02:00
03ca1a50a7 feat: local sections in sub-events can use festival-level time slots
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 11:16:32 +02:00
4388811be9 feat: companies CRUD with person dialog integration and navigation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 11:16:01 +02:00
169a078a92 feat: crowd types management UI with create/edit/deactivate
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 11:15:51 +02:00
10bd55b8ae feat: festival/series model with sub-events, cross-event sections, tab navigation, SectionsShiftsPanel extraction
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 11:15:19 +02:00
c776331cf8 feat: festival/event model frontend + topbar activeren
- Events lijst: card grid met festival/serie chips
- Festival detail: programmaonderdelen grid
- CreateSubEventDialog voor sub-events binnen festival
- EventTabsNav: breadcrumb terug naar festival
- Sessie A: festival-bewuste EventResource + children endpoint
- Topbar: zoekbalk, theme switcher, shortcuts, notificaties
- Schema v1.7 + BACKLOG.md toegevoegd
- 121 tests groen
2026-04-08 10:06:47 +02:00
6848bc2c49 feat: schema v1.7 + sections/shifts frontend
- Universeel festival/event model (parent_event_id, event_type)
- event_person_activations pivot tabel
- Event model: parent/children relaties + helper scopes
- DevSeeder: festival structuur met sub-events
- Sections & Shifts frontend (twee-kolom layout)
- BACKLOG.md aangemaakt met 22 gedocumenteerde wensen
2026-04-08 07:23:56 +02:00
6f69b30fb6 feat: frontend fase 2 sessies 1-3
- Member management pagina + invite flow
- Persons module met filters, KPI tiles, detail panel
- Event horizontale tabs navigatie (EventTabsNav component)
- Route conflict opgelost
- OrganisationSwitcher verbeterd (collapsed staat WIP)
2026-04-08 03:15:45 +02:00
230e11cc8d feat: frontend member management
- Leden pagina met VDataTable, rol chips, uitnodigingen sectie
- InviteMemberDialog + EditMemberRoleDialog
- Publieke acceptatiepagina /invitations/[token]
- Router guard uitgebreid met requiresAuth: false support
- MemberCollection backend uitgebreid met volledige pending_invitations lijst
2026-04-08 01:50:38 +02:00
c417a6647a feat(app): auth, orgs/events UI, router guards, and dev tooling
- Add Sanctum auth flow (store, composables, login, axios interceptors)
- Add dashboard, organisation list/detail, events CRUD dialogs
- Wire router guards, navigation, organisation switcher in layout
- Replace Vuexy @db types in NavSearchBar; add @iconify/types; themeConfig title typing
- Vuetify settings.scss + resolve configFile via fileURLToPath; drop dead path aliases
- Root index redirects to dashboard; fix events table route name
- API: DevSeeder + DatabaseSeeder updates; docs TEST_SCENARIO; corporate identity assets

Made-with: Cursor
2026-04-07 21:51:10 +02:00