Commit Graph

34 Commits

Author SHA1 Message Date
473b22ac9e feat(router): context-aware guards with meta-driven role/context resolution
Rewrites plugins/1.router/guards.ts per ARCH-CONSOLIDATION §4.3. The
B1 portal-context carve-out is removed; portal/organizer routing is
now declarative via meta.context, role gates via meta.requiresRole.

Guard pipeline:
1. Initialize auth store on first navigation
2. Public routes pass through (authenticated user on guest-only path
   is bounced to resolveLandingRoute)
3. Auth required → /login?to=<path>
4. MFA setup gate → /account-settings?tab=security
5. requiresRole declarative check (replaces hardcoded /platform path
   prefix + isSuperAdmin)
6. Context routing — portal returns early, organizer falls through
   and sets lastContext
7. Org-selection check (organizer routes only)

Page meta updates (mechanical, idempotent):
- 4 portal pages: removed `requiresAuth: true` (auth is implicit)
- 4 pages: replaced `requiresAuth: false` with `meta.public: true`
  (registreren, wachtwoord-instellen, advance/[token],
  invitations/[token])
- 22 organizer pages: added `context: 'organizer'`
  (account-settings, events/**, organisation/form-failures/**,
  select-organisation, dashboard, events/index, members,
  organisation/{index,companies,settings})
- 8 platform pages: added `context: 'organizer'` +
  `requiresRole: 'super_admin'`
- 6 organizer pages had no definePage block — one was added with
  `context: 'organizer'`

Adds plugins/1.router/__tests__/guards.spec.ts (11 tests) covering
public passthrough, unauthenticated redirect, portal/organizer
context branching, declarative requiresRole, org-selection
redirect, MFA gate.

Test count 178 → 189 (11 new). Lint + typecheck clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 21:32:54 +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
b4f5bbe7c2 fix(app): resolve Bucket A/C/D lint items (trivial / style / Vuetify class)
WS-3 session 1b-ii Task 4 (audit Buckets A, C, D — 26 items resolved
this commit; 24 indent items in useTimeSlotDropdown.ts remain — see
deviations).

Bucket A — Trivial fixes (12 items resolved):
- A.1: second-pass eslint --fix on App.vue resolved 4 multi-attribute
  warnings. AppKpiCard / PortalLayout / PublicLayout
  lines-around-comment items were attempted via blank-line addition,
  but that introduced an equal number of vue/block-tag-newline
  errors (the rules conflict at the SFC <script>-tag boundary). The
  blank-line additions were reverted; net-zero, the 3 items remain
  for a 1b-iii .eslintrc.cjs override decision.
- A.3: 6 unused-imports / unused-vars manual deletes:
  * OrganisationSwitcher.vue: removed orphan toggleMenu() function
  * CreateShiftDialog.vue: removed unused 'scenario' from destructure
  * pages/events/[id]/time-slots/index.vue: removed unused 'event'
    slot scope binding (template <#default="{ event }"> → <#default>)
  * pages/organisation/companies.vue: removed unused authStore
    declaration + import
  * pages/platform/activity-log/index.vue: removed unused
    search/searchDebounced pair
  * PersonDetailPanel.vue:77: removed redundant single-statement
    if-braces (curly autofix that the original pass didn't reach)

Bucket C — Style preference (8 items resolved):
- DismissFailureDialog.vue:43: collapsed two consecutive `if cond return false`
  branches into `return !(cond)`
- FormFailureDetail.vue:44: replaced `void clipboard.writeText(...)` with
  `clipboard.writeText(...).catch(() => {})` — fire-and-forget with
  silent rejection (the no-void rule wants the void operator gone;
  .catch() handles it semantically).
- AssignShiftDialog.vue:40-46: hasOverlapWarning collapsed from
  always-false branching to `computed(() => false)` (the early-return
  was dead code; backend enforces the constraint).
- SectionsShiftsPanel.vue:333 + registration-fields.vue:335: rewrote
  `:delay-on-touch-only="true"` to attribute-shorthand `delay-on-touch-only`.
- AssignPersonDialog.vue:120-128: collapsed two `if outer { if inner ... }`
  pairs into single `if (outer && inner)` form (sonarjs/no-collapsible-if).
- useImpersonationStore.ts:99-104: collapsed the same nested-if pattern
  into `if (!data.data.active && state.value)`.

Bucket D — Vuetify utility class rename (5 items, 3 files):
- ml-1 → ms-1 (PersonDetailPanel:271, SectionsShiftsPanel:357,
  AssignPersonDialog:496)
- pl-4 → ps-4 (AssignPersonDialog:457)
- ml-auto → ms-auto (AssignPersonDialog:471)
LTR/RTL-aware Vuetify utilities, matching the Vuexy reference idiom.

Tests + typecheck verified green.

Lint baseline: 62 → 36.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 14:20:34 +02:00
47bd533179 style(app): apply eslint --fix to Tier 1 (Vue templates)
WS-3 session 1b-i Tier 1.

Scope: src/components/**, src/pages/**, src/layouts/**, src/views/**
restricted to *.vue files. Mechanical formatting only — predominantly
vue/html-indent (506 fixes in CrowdListDetailPanel.vue alone),
padding-line-between-statements, antfu/if-newline.

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 TypeScript-only files in src/composables, src/lib, src/stores,
  src/plugins, src/types (Tier 2 — separate commit)

Includes session 1a layouts (PortalLayout.vue, PublicLayout.vue) where
2 'lines-around-comment' errors were flagged in the previous 1b-i
pre-flight inspection.

Tests + typecheck verified green post-fix:
- apps/app vitest: 49 passed (unchanged)
- apps/app vue-tsc: clean (unchanged)
- apps/portal vitest: 113 passed (unchanged — not touched)
- backend pest: 1486 passed (unchanged — not touched)

Lint baseline progression:
- Pre-Tier-1: 1451 problems
- Post-Tier-1: 422 problems

Visual smoke status:
- NOT YET SMOKED — Bert to verify before merge. This Claude Code
  session has no UI access; cannot run pnpm dev and click through
  affected routes. The high-traffic candidates are
  CrowdListDetailPanel (506 fixes), AssignPersonDialog (44),
  ShiftDetailPanel (36), and the events / form-failures pages.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 11:04:46 +02:00
2ae90ed57f feat(app): unify KPI tiles with AppKpiCard
Introduce AppKpiCard for consistent metric layout (icon + value, title,
subtitle row) and default VCard chrome without mixed border-shadow accents.
Use on organisation overview (all primary icons, equal stretch row) and
home dashboard. Regenerate component type declarations.

Made-with: Cursor
2026-04-29 00:46:48 +02:00
786bca8cf1 feat(form-failures): admin detail view (WS-6)
FormFailureDetail shared component drives both detail pages:
  - apps/app/src/pages/platform/form-failures/[id].vue
  - apps/app/src/pages/organisation/form-failures/[id].vue

Layout (per design schets):
  - Header with state badge (large) + title (Form failure {short-id})
    + relative-time subtitle + listener short-name
  - Action button row (Retry / Markeren als opgelost / Dismiss),
    disabled for non-open states
  - 60/40 two-column layout via VRow/VCol(md=7/md=5)

Left column:
  - Exception card: class + message in code blocks + "Bericht
    kopiëren" button (navigator.clipboard)
  - Context card (only when context is non-null): pretty-printed
    JSON in <pre> with copy-as-JSON button
  - Tijdlijn (VTimeline): Failed → Retry-pogingen → Opgelost or
    Dismissed → "In afwachting van actie..." for open with no retries

Right column:
  - Inzending card: form_submission_id with copy button. The
    submission detail-pagina link is documented as "nog niet
    beschikbaar in v1" inline; opening submissions in the SPA isn't
    yet implemented (forward-pointed).
  - Listener card: full FQN listener_class
  - Retry-geschiedenis card: count chip + caveat that per-attempt
    detail (timestamp + outcome) is not yet shipped by the backend
    resource (the FormSubmissionActionFailureResource ships only
    retry_count, not a retry history array)

Action dialogs reused from Task 2; refetch on success.

8 Vitest tests cover loading state, header rendering, all 6 cards
present, action button disabled-ness per state (open/resolved/
dismissed), and timeline content for resolved + open-no-retries
states.

Refs: WS-6 sessie 3b admin UI Task 4

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 00:14:18 +02:00
4c80289c47 feat(form-failures): admin list view with KPI tiles + filters (WS-6)
FormFailuresTable shared component drives both /platform/form-failures
(super_admin, all orgs) and /organisation/form-failures (org_admin,
scoped to the active organisation).

  - 4 KPI tiles (Open / Opgelost / Dismissed / Totaal) with click-to-
    filter behavior. Counts derived client-side from a per_page=100
    list call (composable's useFormFailuresKpis).
  - Filter bar: state segment-control (VBtnToggle) + debounced search
    (exception class / message / IDs).
  - VDataTableServer with custom cell slots: state chip, formatted
    failed_at timestamp, listener short-name, exception class+message
    (truncated), submission short-id, retry-count chip, action column.
  - Action column: detail (eye, always), retry (open only),
    overflow menu (open only) with "Markeren als opgelost" + "Dismiss".
  - Empty state with "Filters wissen" CTA.
  - All three action dialogs wired in; @success → refetch().

Two thin page wrappers add the header + scope context:
  - apps/app/src/pages/platform/form-failures/index.vue
  - apps/app/src/pages/organisation/form-failures/index.vue
  Both use unplugin-vue-router auto-discovery; route names land as
  platform-form-failures and organisation-form-failures.

Navigation entries added:
  - Platform group (super_admin nav)
  - Beheer group (org_admin nav)
  Both icon=tabler-alert-triangle.

Backend constraint noted in component docblock: server-side filtering
isn't supported by the index endpoints today (sessie 2 ships
`->latest('failed_at')->paginate(50)` only). Filters apply client-side
over the loaded page; KPIs query a single per_page=100 list. Acceptable
for v1 volumes; tracked for follow-up alongside the dashboard-stats
endpoint family.

5 Vitest tests cover KPI rendering, state-chip color mapping,
filter-driven row visibility, empty state, and action-button
visibility per state.

Refs: WS-6 sessie 3b admin UI Task 3

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 00:14:18 +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
80f0b535f5 refactor(settings): restructure sidebar and move danger zone to its own tab
Drop the Algemeen tab together with the Organisatie subheader — organisatie-
gegevens verhuizen naar /organisation. Voeg een GEVAARLIJK-subheader toe met
een Gevaarlijke acties tab, die de bestaande platform-beheerder-notitie bevat
(self-delete blijft buiten scope). Legacy ?tab=algemeen/general redirects
door naar /organisation; default tab valt terug op Crowd Types.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 10:27:45 +02:00
7695011f4b chore(settings): remove Leden tab from Instellingen sidebar 2026-04-16 22:28:20 +02:00
c18323de8e chore(companies): refactor filter row for responsive layout
- Wrap filter row so controls flow to a second line on narrow screens
- Search field now flex-fills available width instead of fixed 300px
- Type select: removed inline label, widened to 240px, prevented
  shrink with flex-shrink-0

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 22:12:21 +02:00
8774fff3e9 refactor(settings): move Verzendlog under new Systeem subheader
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 22:06:02 +02:00
47cb6b83d4 refactor: organisation settings — vertical sidebar layout with grouped sections
Replace horizontal tabs with VList-based vertical sidebar following the
Vuexy ecommerce settings pattern. Consolidate Tags, Crowd Types, Members,
and Registration Fields pages into the settings page as sidebar tabs.
Add SettingsGeneral panel with org details form and danger zone.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 02:10:50 +02:00
a77986334c fix: remove duplicate header on organisation crowd-types page
Made-with: Cursor
2026-04-15 22:34:50 +02:00
34cc57ac51 fix: remove duplicate header on organisation tags page
Made-with: Cursor
2026-04-15 22:34:36 +02:00
9f19c9ed37 feat: move organisation members to sidebar, drop tabs on org page
Made-with: Cursor
2026-04-15 22:31:21 +02:00
4e6d5eb4aa feat: move tags and crowd types to sidebar from org settings tabs
Made-with: Cursor
2026-04-15 22:30:12 +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
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
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
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
ef195a6777 feat(mail): center-align action button in email template
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-14 00:49:41 +02:00
63bc351c59 refactor(app): unify settings tab design for tags, templates & crowd types
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>
2026-04-12 23:18:10 +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
d2f282eb4c feat: split name into first_name + last_name across users, persons, and companies
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>
2026-04-10 23:04:55 +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
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