diff --git a/documentation.json b/documentation.json index 800816d..1329948 100644 --- a/documentation.json +++ b/documentation.json @@ -2397,54 +2397,7 @@ }, { "name": "Draft", - "id": "interface-Draft-3f75a0ff51d12ccab50bfa13789cace778d5acff9f4feaa1607cdf1acb4a5ab4fcc687512424cb2252ec07568f6aea07a0b4f04469cbeed733c28654443ffd9e", - "file": "src/app/herregistratie/domain/herregistratie.machine.ts", - "deprecated": false, - "deprecationMessage": "", - "type": "interface", - "sourceCode": "import { Result, assertNever } from '@shared/kernel/fp';\nimport { Uren, parseUren } from '@registratie/domain/value-objects/uren';\nimport {\n UploadState,\n UploadMsg,\n initialUpload,\n reduceUpload,\n requiredCategoriesSatisfied,\n deliveryRefs,\n} from '@shared/upload/upload.machine';\n\n/** What the user is typing (raw, possibly invalid). */\nexport interface Draft {\n uren: string;\n jaren: string;\n punten: string;\n}\n\nexport type StepErrors = Partial>;\n\n/** What we have AFTER parsing — branded/typed, guaranteed valid. */\nexport interface Valid {\n uren: Uren;\n jaren: number;\n punten: number;\n documents: Array<{ categoryId: string; channel: 'digital' | 'post'; documentId?: string }>;\n}\n\n/**\n * The whole wizard as one tagged union. `step` and `errors` exist ONLY while\n * Editing; Submitting/Submitted/Failed carry a `Valid` payload and nothing else.\n * So \"submitting while a field is invalid\" or \"showing the success screen with\n * errors set\" are unrepresentable — the bug class is gone by construction.\n */\nexport type WizardState =\n | { tag: 'Editing'; step: 1 | 2 | 3; draft: Draft; errors: StepErrors; upload: UploadState }\n | { tag: 'Submitting'; data: Valid }\n | { tag: 'Submitted'; data: Valid }\n | { tag: 'Failed'; data: Valid; error: string };\n\nexport const initial: WizardState = {\n tag: 'Editing',\n step: 1,\n draft: { uren: '', jaren: '', punten: '' },\n errors: {},\n upload: initialUpload,\n};\n\n/** Has the user meaningfully started, so it's worth persisting as a Concept? */\nexport function hasProgress(s: Extract): boolean {\n return (\n s.step > 1 ||\n !!s.draft.uren ||\n !!s.draft.jaren ||\n !!s.draft.punten ||\n deliveryRefs(s.upload).some((r) => r.channel === 'digital' && !!r.documentId)\n );\n}\n\n/** Parse every field; on success hand back a Valid, else the per-field errors. */\nfunction validate(draft: Draft, upload: UploadState): Result {\n const uren = parseUren(draft.uren);\n const jaren = parseUren(draft.jaren);\n const punten = parseUren(draft.punten);\n const errors: StepErrors = {};\n if (!uren.ok) errors.uren = uren.error;\n if (!jaren.ok) errors.jaren = jaren.error;\n if (!punten.ok) errors.punten = punten.error;\n if (!requiredCategoriesSatisfied(upload)) {\n errors.documenten = $localize`:@@validation.documenten:Lever de verplichte documenten aan (upload of kies \"per post nasturen\").`;\n }\n if (uren.ok && jaren.ok && punten.ok && !errors.documenten) {\n return {\n ok: true,\n value: {\n uren: uren.value,\n jaren: jaren.value,\n punten: punten.value,\n documents: deliveryRefs(upload),\n },\n };\n }\n return { ok: false, error: errors };\n}\n\n/** Advance one step, gating on that step's fields. Illegal elsewhere = no-op. */\nexport function next(s: WizardState): WizardState {\n if (s.tag !== 'Editing') return s;\n const errors: StepErrors = {};\n if (s.step === 1) {\n const uren = parseUren(s.draft.uren);\n const jaren = parseUren(s.draft.jaren);\n if (!uren.ok) errors.uren = uren.error;\n if (!jaren.ok) errors.jaren = jaren.error;\n return Object.keys(errors).length === 0 ? { ...s, step: 2, errors: {} } : { ...s, errors };\n }\n if (s.step === 2) {\n const punten = parseUren(s.draft.punten);\n if (!punten.ok) errors.punten = punten.error;\n return punten.ok ? { ...s, step: 3, errors: {} } : { ...s, errors };\n }\n return s;\n}\n\nexport function back(s: WizardState): WizardState {\n if (s.tag !== 'Editing' || s.step === 1) return s;\n return { ...s, step: (s.step - 1) as 1 | 2, errors: {} };\n}\n\n/** Jump back to an earlier step to correct data (controle → step N). Forward\n jumps are not allowed (would skip validation). */\nexport function gaNaarStap(s: WizardState, step: 1 | 2 | 3): WizardState {\n if (s.tag !== 'Editing' || step >= s.step) return s;\n return { ...s, step, errors: {} };\n}\n\n/** Step 3 submit: parse everything + require documents; Submitting only with Valid. */\nexport function submit(s: WizardState): WizardState {\n if (s.tag !== 'Editing' || s.step !== 3) return s;\n const result = validate(s.draft, s.upload);\n return result.ok ? { tag: 'Submitting', data: result.value } : { ...s, errors: result.error };\n}\n\n/** Route an upload sub-message through the pure upload reducer (Editing only). */\nexport function upload(s: WizardState, msg: UploadMsg): WizardState {\n if (s.tag !== 'Editing') return s;\n return { ...s, upload: reduceUpload(s.upload, msg) };\n}\n\n/** Resolve the async submit. Only meaningful while Submitting. */\nexport function resolve(s: WizardState, r: Result): WizardState {\n if (s.tag !== 'Submitting') return s;\n return r.ok\n ? { tag: 'Submitted', data: s.data }\n : { tag: 'Failed', data: s.data, error: r.error };\n}\n\n/** Update one draft field while editing; ignored in any other state. */\nexport function setField(s: WizardState, key: keyof Draft, value: string): WizardState {\n if (s.tag !== 'Editing') return s;\n return { ...s, draft: { ...s.draft, [key]: value } };\n}\n\n/**\n * Every event that can happen to the wizard, as one message type. The component\n * sends a WizardMsg; `reduce` decides the next state. This is the Elm\n * Model+Msg+update pattern: ONE pure function describes all state changes.\n */\nexport type WizardMsg =\n | { tag: 'SetField'; key: keyof Draft; value: string }\n | { tag: 'Next' }\n | { tag: 'Back' }\n | { tag: 'GaNaarStap'; step: 1 | 2 | 3 }\n | { tag: 'Submit' }\n | { tag: 'Retry' }\n | { tag: 'SubmitConfirmed' }\n | { tag: 'SubmitFailed'; error: string }\n | { tag: 'Upload'; msg: UploadMsg }\n | { tag: 'Seed'; state: WizardState }; // mount a specific state (stories/showcase)\n\nexport function reduce(s: WizardState, m: WizardMsg): WizardState {\n switch (m.tag) {\n case 'SetField':\n return setField(s, m.key, m.value);\n case 'Next':\n return next(s);\n case 'Back':\n return back(s);\n case 'GaNaarStap':\n return gaNaarStap(s, m.step);\n case 'Submit':\n return submit(s);\n case 'Retry':\n return s.tag === 'Failed' ? { tag: 'Submitting', data: s.data } : s;\n case 'SubmitConfirmed':\n return s.tag === 'Submitting' ? { tag: 'Submitted', data: s.data } : s;\n case 'SubmitFailed':\n return s.tag === 'Submitting' ? { tag: 'Failed', data: s.data, error: m.error } : s;\n case 'Upload':\n return upload(s, m.msg);\n case 'Seed':\n return m.state;\n default:\n return assertNever(m);\n }\n}\n", - "properties": [ - { - "name": "jaren", - "deprecated": false, - "deprecationMessage": "", - "type": "string", - "indexKey": "", - "optional": false, - "description": "", - "line": 15 - }, - { - "name": "punten", - "deprecated": false, - "deprecationMessage": "", - "type": "string", - "indexKey": "", - "optional": false, - "description": "", - "line": 16 - }, - { - "name": "uren", - "deprecated": false, - "deprecationMessage": "", - "type": "string", - "indexKey": "", - "optional": false, - "description": "", - "line": 14 - } - ], - "indexSignatures": [], - "kind": 172, - "description": "

What the user is typing (raw, possibly invalid).

\n", - "rawdescription": "\nWhat the user is typing (raw, possibly invalid).", - "methods": [], - "extends": [] - }, - { - "name": "Draft", - "id": "interface-Draft-51c29a3fca3c5bd3eba53c9bc57714c79817ae8c86b1ed9c1cd76652d31b45ee7c635d35f7eb0a3a7e5bf9ea7c5da5c066cee3908c8fe4b9fe053586f834b133-1", + "id": "interface-Draft-51c29a3fca3c5bd3eba53c9bc57714c79817ae8c86b1ed9c1cd76652d31b45ee7c635d35f7eb0a3a7e5bf9ea7c5da5c066cee3908c8fe4b9fe053586f834b133", "file": "src/app/registratie/domain/change-request.machine.ts", "deprecated": false, "deprecationMessage": "", @@ -2487,14 +2440,11 @@ "description": "

What the user is typing (raw, possibly invalid).

\n", "rawdescription": "\nWhat the user is typing (raw, possibly invalid).", "methods": [], - "extends": [], - "isDuplicate": true, - "duplicateId": 1, - "duplicateName": "Draft-1" + "extends": [] }, { "name": "Draft", - "id": "interface-Draft-9669bb22f8d4cc591a3bd64d9fc83275b70ad9e7275d59b4d1d394db7252e8820908b20c593f0b3a16c415c40ad1232ed7d04ba684d0dee9d72bc1ae820d06b6-2", + "id": "interface-Draft-9669bb22f8d4cc591a3bd64d9fc83275b70ad9e7275d59b4d1d394db7252e8820908b20c593f0b3a16c415c40ad1232ed7d04ba684d0dee9d72bc1ae820d06b6-1", "file": "src/app/registratie/domain/registratie-wizard.machine.ts", "deprecated": false, "deprecationMessage": "", @@ -2619,6 +2569,56 @@ "methods": [], "extends": [], "isDuplicate": true, + "duplicateId": 1, + "duplicateName": "Draft-1" + }, + { + "name": "Draft", + "id": "interface-Draft-3f75a0ff51d12ccab50bfa13789cace778d5acff9f4feaa1607cdf1acb4a5ab4fcc687512424cb2252ec07568f6aea07a0b4f04469cbeed733c28654443ffd9e-2", + "file": "src/app/herregistratie/domain/herregistratie.machine.ts", + "deprecated": false, + "deprecationMessage": "", + "type": "interface", + "sourceCode": "import { Result, assertNever } from '@shared/kernel/fp';\nimport { Uren, parseUren } from '@registratie/domain/value-objects/uren';\nimport {\n UploadState,\n UploadMsg,\n initialUpload,\n reduceUpload,\n requiredCategoriesSatisfied,\n deliveryRefs,\n} from '@shared/upload/upload.machine';\n\n/** What the user is typing (raw, possibly invalid). */\nexport interface Draft {\n uren: string;\n jaren: string;\n punten: string;\n}\n\nexport type StepErrors = Partial>;\n\n/** What we have AFTER parsing — branded/typed, guaranteed valid. */\nexport interface Valid {\n uren: Uren;\n jaren: number;\n punten: number;\n documents: Array<{ categoryId: string; channel: 'digital' | 'post'; documentId?: string }>;\n}\n\n/**\n * The whole wizard as one tagged union. `step` and `errors` exist ONLY while\n * Editing; Submitting/Submitted/Failed carry a `Valid` payload and nothing else.\n * So \"submitting while a field is invalid\" or \"showing the success screen with\n * errors set\" are unrepresentable — the bug class is gone by construction.\n */\nexport type WizardState =\n | { tag: 'Editing'; step: 1 | 2 | 3; draft: Draft; errors: StepErrors; upload: UploadState }\n | { tag: 'Submitting'; data: Valid }\n | { tag: 'Submitted'; data: Valid }\n | { tag: 'Failed'; data: Valid; error: string };\n\nexport const initial: WizardState = {\n tag: 'Editing',\n step: 1,\n draft: { uren: '', jaren: '', punten: '' },\n errors: {},\n upload: initialUpload,\n};\n\n/** Has the user meaningfully started, so it's worth persisting as a Concept? */\nexport function hasProgress(s: Extract): boolean {\n return (\n s.step > 1 ||\n !!s.draft.uren ||\n !!s.draft.jaren ||\n !!s.draft.punten ||\n deliveryRefs(s.upload).some((r) => r.channel === 'digital' && !!r.documentId)\n );\n}\n\n/** Parse every field; on success hand back a Valid, else the per-field errors. */\nfunction validate(draft: Draft, upload: UploadState): Result {\n const uren = parseUren(draft.uren);\n const jaren = parseUren(draft.jaren);\n const punten = parseUren(draft.punten);\n const errors: StepErrors = {};\n if (!uren.ok) errors.uren = uren.error;\n if (!jaren.ok) errors.jaren = jaren.error;\n if (!punten.ok) errors.punten = punten.error;\n if (!requiredCategoriesSatisfied(upload)) {\n errors.documenten = $localize`:@@validation.documenten:Lever de verplichte documenten aan (upload of kies \"per post nasturen\").`;\n }\n if (uren.ok && jaren.ok && punten.ok && !errors.documenten) {\n return {\n ok: true,\n value: {\n uren: uren.value,\n jaren: jaren.value,\n punten: punten.value,\n documents: deliveryRefs(upload),\n },\n };\n }\n return { ok: false, error: errors };\n}\n\n/** Advance one step, gating on that step's fields. Illegal elsewhere = no-op. */\nexport function next(s: WizardState): WizardState {\n if (s.tag !== 'Editing') return s;\n const errors: StepErrors = {};\n if (s.step === 1) {\n const uren = parseUren(s.draft.uren);\n const jaren = parseUren(s.draft.jaren);\n if (!uren.ok) errors.uren = uren.error;\n if (!jaren.ok) errors.jaren = jaren.error;\n return Object.keys(errors).length === 0 ? { ...s, step: 2, errors: {} } : { ...s, errors };\n }\n if (s.step === 2) {\n const punten = parseUren(s.draft.punten);\n if (!punten.ok) errors.punten = punten.error;\n return punten.ok ? { ...s, step: 3, errors: {} } : { ...s, errors };\n }\n return s;\n}\n\nexport function back(s: WizardState): WizardState {\n if (s.tag !== 'Editing' || s.step === 1) return s;\n return { ...s, step: (s.step - 1) as 1 | 2, errors: {} };\n}\n\n/** Jump back to an earlier step to correct data (controle → step N). Forward\n jumps are not allowed (would skip validation). */\nexport function gaNaarStap(s: WizardState, step: 1 | 2 | 3): WizardState {\n if (s.tag !== 'Editing' || step >= s.step) return s;\n return { ...s, step, errors: {} };\n}\n\n/** Step 3 submit: parse everything + require documents; Submitting only with Valid. */\nexport function submit(s: WizardState): WizardState {\n if (s.tag !== 'Editing' || s.step !== 3) return s;\n const result = validate(s.draft, s.upload);\n return result.ok ? { tag: 'Submitting', data: result.value } : { ...s, errors: result.error };\n}\n\n/** Route an upload sub-message through the pure upload reducer (Editing only). */\nexport function upload(s: WizardState, msg: UploadMsg): WizardState {\n if (s.tag !== 'Editing') return s;\n return { ...s, upload: reduceUpload(s.upload, msg) };\n}\n\n/** Resolve the async submit. Only meaningful while Submitting. */\nexport function resolve(s: WizardState, r: Result): WizardState {\n if (s.tag !== 'Submitting') return s;\n return r.ok\n ? { tag: 'Submitted', data: s.data }\n : { tag: 'Failed', data: s.data, error: r.error };\n}\n\n/** Update one draft field while editing; ignored in any other state. */\nexport function setField(s: WizardState, key: keyof Draft, value: string): WizardState {\n if (s.tag !== 'Editing') return s;\n return { ...s, draft: { ...s.draft, [key]: value } };\n}\n\n/**\n * Every event that can happen to the wizard, as one message type. The component\n * sends a WizardMsg; `reduce` decides the next state. This is the Elm\n * Model+Msg+update pattern: ONE pure function describes all state changes.\n */\nexport type WizardMsg =\n | { tag: 'SetField'; key: keyof Draft; value: string }\n | { tag: 'Next' }\n | { tag: 'Back' }\n | { tag: 'GaNaarStap'; step: 1 | 2 | 3 }\n | { tag: 'Submit' }\n | { tag: 'Retry' }\n | { tag: 'SubmitConfirmed' }\n | { tag: 'SubmitFailed'; error: string }\n | { tag: 'Upload'; msg: UploadMsg }\n | { tag: 'Seed'; state: WizardState }; // mount a specific state (stories/showcase)\n\nexport function reduce(s: WizardState, m: WizardMsg): WizardState {\n switch (m.tag) {\n case 'SetField':\n return setField(s, m.key, m.value);\n case 'Next':\n return next(s);\n case 'Back':\n return back(s);\n case 'GaNaarStap':\n return gaNaarStap(s, m.step);\n case 'Submit':\n return submit(s);\n case 'Retry':\n return s.tag === 'Failed' ? { tag: 'Submitting', data: s.data } : s;\n case 'SubmitConfirmed':\n return s.tag === 'Submitting' ? { tag: 'Submitted', data: s.data } : s;\n case 'SubmitFailed':\n return s.tag === 'Submitting' ? { tag: 'Failed', data: s.data, error: m.error } : s;\n case 'Upload':\n return upload(s, m.msg);\n case 'Seed':\n return m.state;\n default:\n return assertNever(m);\n }\n}\n", + "properties": [ + { + "name": "jaren", + "deprecated": false, + "deprecationMessage": "", + "type": "string", + "indexKey": "", + "optional": false, + "description": "", + "line": 15 + }, + { + "name": "punten", + "deprecated": false, + "deprecationMessage": "", + "type": "string", + "indexKey": "", + "optional": false, + "description": "", + "line": 16 + }, + { + "name": "uren", + "deprecated": false, + "deprecationMessage": "", + "type": "string", + "indexKey": "", + "optional": false, + "description": "", + "line": 14 + } + ], + "indexSignatures": [], + "kind": 172, + "description": "

What the user is typing (raw, possibly invalid).

\n", + "rawdescription": "\nWhat the user is typing (raw, possibly invalid).", + "methods": [], + "extends": [], + "isDuplicate": true, "duplicateId": 2, "duplicateName": "Draft-2" }, @@ -3164,12 +3164,12 @@ }, { "name": "HeaderNavItem", - "id": "interface-HeaderNavItem-a0c115949f8f7f8c794708a232dd899ff4f8cef791c7c67a268bdab2addb5706bdb2657580819aff599f17276dd46cb703c40aa4db1191cdfa5d5de6e252b16b", + "id": "interface-HeaderNavItem-5fc019799e1a7e63f899d40eb618b7209f4a666481cad865b1e33e7d6e0a691a6cacfbca89de3e7a199d0ee46d80b931b335c7a3560667f3b57d8072fa1d2b98", "file": "src/app/shared/layout/site-header/site-header.component.ts", "deprecated": false, "deprecationMessage": "", "type": "interface", - "sourceCode": "import { Component, computed, inject } from '@angular/core';\nimport { toSignal } from '@angular/core/rxjs-interop';\nimport { NavigationEnd, Router, RouterLink, RouterLinkActive } from '@angular/router';\nimport { filter, map } from 'rxjs/operators';\nimport { SESSION_PORT } from '@shared/application/session.port';\nimport { BreadcrumbComponent } from '@shared/layout/breadcrumb/breadcrumb.component';\nimport { trailFor } from '@shared/layout/breadcrumb/breadcrumb-trail';\n\ninterface HeaderNavItem {\n readonly label: string;\n readonly to: string;\n}\n\nconst NAV_ITEMS: readonly HeaderNavItem[] = [\n { label: $localize`:@@header.nav.overzicht:Overzicht`, to: '/dashboard' },\n { label: $localize`:@@header.nav.gegevens:Mijn gegevens`, to: '/registratie' },\n { label: $localize`:@@header.nav.herregistratie:Herregistratie`, to: '/herregistratie' },\n { label: $localize`:@@header.nav.inschrijven:Inschrijven`, to: '/registreren' },\n];\n\n/** Organism: CIBG Huisstijl site header — logo block, robijn titlebar (breadcrumb +\n user menu), horizontal nav. ponytail: text wordmark, not the licensed Rijksoverheid\n beeldmerk; no search box (no search feature yet). */\n@Component({\n selector: 'app-site-header',\n imports: [RouterLink, RouterLinkActive, BreadcrumbComponent],\n styles: [\n `\n .logout {\n background: none;\n border: 0;\n padding: 0;\n cursor: pointer;\n text-decoration: underline;\n font: inherit;\n color: inherit;\n }\n /* CIBG's header nav has no bg by default in this build — the grey bar is ours.\n (.titlebar keeps its own robijn fill — --ro-layout — untouched; the breadcrumb\n inside it has no background of its own, so the bar's colour shows through.) */\n nav {\n background-color: var(--rhc-color-cool-grey-200);\n }\n `,\n ],\n template: `\n
\n \n
\n
\n
\n
\n @if (trail().length) {\n \n }\n
\n
\n @if (session(); as s) {\n
\n {{ s.naam }}\n
\n
\n \n
\n }\n
\n
\n
\n
\n \n
\n `,\n})\nexport class SiteHeaderComponent {\n protected readonly navItems = NAV_ITEMS;\n\n private router = inject(Router);\n private sessionPort = inject(SESSION_PORT, { optional: true });\n\n readonly session = computed(() => this.sessionPort?.session() ?? null);\n private url = toSignal(\n this.router.events.pipe(\n filter((e) => e instanceof NavigationEnd),\n map(() => this.router.url),\n ),\n { initialValue: this.router.url },\n );\n protected trail = computed(() => trailFor(this.url()));\n\n logout() {\n this.sessionPort?.logout();\n this.router.navigate(['/login']);\n }\n}\n", + "sourceCode": "import { Component, computed, inject } from '@angular/core';\nimport { toSignal } from '@angular/core/rxjs-interop';\nimport { NavigationEnd, Router, RouterLink, RouterLinkActive } from '@angular/router';\nimport { filter, map } from 'rxjs/operators';\nimport { SESSION_PORT } from '@shared/application/session.port';\nimport { AccessStore } from '@shared/application/access.store';\nimport { Capability } from '@shared/domain/capability';\nimport { BreadcrumbComponent } from '@shared/layout/breadcrumb/breadcrumb.component';\nimport { trailFor } from '@shared/layout/breadcrumb/breadcrumb-trail';\n\ninterface HeaderNavItem {\n readonly label: string;\n readonly to: string;\n}\n\nconst NAV_ITEMS: readonly HeaderNavItem[] = [\n { label: $localize`:@@header.nav.overzicht:Overzicht`, to: '/dashboard' },\n { label: $localize`:@@header.nav.gegevens:Mijn gegevens`, to: '/registratie' },\n { label: $localize`:@@header.nav.herregistratie:Herregistratie`, to: '/herregistratie' },\n { label: $localize`:@@header.nav.inschrijven:Inschrijven`, to: '/registreren' },\n];\n\n/** Admin-only nav, shown only when `/me` grants the matching capability — the pages\n are otherwise reachable by URL alone. */\nconst ADMIN_NAV_ITEMS: readonly (HeaderNavItem & { readonly cap: Capability })[] = [\n {\n label: $localize`:@@header.nav.huisstijl:Huisstijl`,\n to: '/brief/huisstijl',\n cap: 'orgtemplate:edit',\n },\n {\n label: $localize`:@@header.nav.stamdata:Stamdata`,\n to: '/beheer/stamdata',\n cap: 'stamdata:edit',\n },\n];\n\n/** Organism: CIBG Huisstijl site header — logo block, robijn titlebar (breadcrumb +\n user menu), horizontal nav. ponytail: text wordmark, not the licensed Rijksoverheid\n beeldmerk; no search box (no search feature yet). */\n@Component({\n selector: 'app-site-header',\n imports: [RouterLink, RouterLinkActive, BreadcrumbComponent],\n styles: [\n `\n .logout {\n background: none;\n border: 0;\n padding: 0;\n cursor: pointer;\n text-decoration: underline;\n font: inherit;\n color: inherit;\n }\n /* CIBG's header nav has no bg by default in this build — the grey bar is ours.\n (.titlebar keeps its own robijn fill — --ro-layout — untouched; the breadcrumb\n inside it has no background of its own, so the bar's colour shows through.) */\n nav {\n background-color: var(--rhc-color-cool-grey-200);\n }\n `,\n ],\n template: `\n
\n \n
\n
\n
\n
\n @if (trail().length) {\n \n }\n
\n
\n @if (session(); as s) {\n
\n {{ s.naam }}\n
\n
\n \n
\n }\n
\n
\n
\n
\n \n
\n `,\n})\nexport class SiteHeaderComponent {\n protected readonly navItems = NAV_ITEMS;\n\n private router = inject(Router);\n private sessionPort = inject(SESSION_PORT, { optional: true });\n private access = inject(AccessStore);\n /** Injecting AccessStore here also warms `/me` at app start (the header renders on\n every page), so the admin routes' guard usually finds caps already resolved. */\n protected adminItems = computed(() => ADMIN_NAV_ITEMS.filter((i) => this.access.can(i.cap)));\n\n readonly session = computed(() => this.sessionPort?.session() ?? null);\n private url = toSignal(\n this.router.events.pipe(\n filter((e) => e instanceof NavigationEnd),\n map(() => this.router.url),\n ),\n { initialValue: this.router.url },\n );\n protected trail = computed(() => trailFor(this.url()));\n\n logout() {\n this.sessionPort?.logout();\n this.router.navigate(['/login']);\n }\n}\n", "properties": [ { "name": "label", @@ -3179,7 +3179,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 10, + "line": 12, "modifierKind": [ 148 ] @@ -3192,7 +3192,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 11, + "line": 13, "modifierKind": [ 148 ] @@ -7334,7 +7334,54 @@ }, { "name": "Valid", - "id": "interface-Valid-3f75a0ff51d12ccab50bfa13789cace778d5acff9f4feaa1607cdf1acb4a5ab4fcc687512424cb2252ec07568f6aea07a0b4f04469cbeed733c28654443ffd9e", + "id": "interface-Valid-51c29a3fca3c5bd3eba53c9bc57714c79817ae8c86b1ed9c1cd76652d31b45ee7c635d35f7eb0a3a7e5bf9ea7c5da5c066cee3908c8fe4b9fe053586f834b133", + "file": "src/app/registratie/domain/change-request.machine.ts", + "deprecated": false, + "deprecationMessage": "", + "type": "interface", + "sourceCode": "import { Result, assertNever } from '@shared/kernel/fp';\nimport { Postcode, parsePostcode } from '@registratie/domain/value-objects/postcode';\n\n/** What the user is typing (raw, possibly invalid). */\nexport interface Draft {\n straat: string;\n postcode: string;\n woonplaats: string;\n}\n\n/** After parsing — postcode is the branded type, so downstream can't get a raw one. */\nexport interface Valid {\n straat: string;\n postcode: Postcode;\n woonplaats: string;\n}\n\nexport type Errors = Partial>;\n\n/**\n * The change-request (adreswijziging) form as one tagged union — the SAME idiom\n * as the wizards, just single-step. `draft`/`errors` exist only while Editing;\n * Submitting/Submitted/Failed carry the parsed `Valid`. Illegal states (submitting\n * an invalid draft, a success screen with errors) are unrepresentable.\n */\nexport type ChangeRequestState =\n | { tag: 'Editing'; draft: Draft; errors: Errors }\n | { tag: 'Submitting'; data: Valid }\n | { tag: 'Submitted'; data: Valid; referentie: string }\n | { tag: 'Failed'; data: Valid; error: string };\n\nexport const initial: ChangeRequestState = {\n tag: 'Editing',\n draft: { straat: '', postcode: '', woonplaats: '' },\n errors: {},\n};\n\n/** Parse via the value objects; on success hand back a Valid, else per-field errors. */\nfunction validate(draft: Draft): Result {\n const straat = draft.straat.trim();\n const postcode = parsePostcode(draft.postcode);\n const errors: Errors = {};\n if (!straat) errors.straat = $localize`:@@validation.straat:Vul straat en huisnummer in.`;\n if (!postcode.ok) errors.postcode = postcode.error;\n if (straat && postcode.ok) {\n return {\n ok: true,\n value: { straat, postcode: postcode.value, woonplaats: draft.woonplaats.trim() },\n };\n }\n return { ok: false, error: errors };\n}\n\nexport type ChangeRequestMsg =\n | { tag: 'SetField'; key: keyof Draft; value: string }\n | { tag: 'Submit' }\n | { tag: 'Retry' }\n | { tag: 'SubmitConfirmed'; referentie: string }\n | { tag: 'SubmitFailed'; error: string }\n | { tag: 'Reset' }\n | { tag: 'Seed'; state: ChangeRequestState }; // mount a specific state (stories/tests)\n\nexport function reduce(s: ChangeRequestState, m: ChangeRequestMsg): ChangeRequestState {\n switch (m.tag) {\n case 'SetField':\n return s.tag === 'Editing' ? { ...s, draft: { ...s.draft, [m.key]: m.value } } : s;\n case 'Submit': {\n if (s.tag !== 'Editing') return s;\n const r = validate(s.draft);\n return r.ok ? { tag: 'Submitting', data: r.value } : { ...s, errors: r.error };\n }\n case 'Retry':\n return s.tag === 'Failed' ? { tag: 'Submitting', data: s.data } : s;\n case 'SubmitConfirmed':\n return s.tag === 'Submitting'\n ? { tag: 'Submitted', data: s.data, referentie: m.referentie }\n : s;\n case 'SubmitFailed':\n return s.tag === 'Submitting' ? { tag: 'Failed', data: s.data, error: m.error } : s;\n case 'Reset':\n return initial;\n case 'Seed':\n return m.state;\n default:\n return assertNever(m);\n }\n}\n", + "properties": [ + { + "name": "postcode", + "deprecated": false, + "deprecationMessage": "", + "type": "Postcode", + "indexKey": "", + "optional": false, + "description": "", + "line": 14 + }, + { + "name": "straat", + "deprecated": false, + "deprecationMessage": "", + "type": "string", + "indexKey": "", + "optional": false, + "description": "", + "line": 13 + }, + { + "name": "woonplaats", + "deprecated": false, + "deprecationMessage": "", + "type": "string", + "indexKey": "", + "optional": false, + "description": "", + "line": 15 + } + ], + "indexSignatures": [], + "kind": 172, + "description": "

After parsing — postcode is the branded type, so downstream can't get a raw one.

\n", + "rawdescription": "\nAfter parsing — postcode is the branded type, so downstream can't get a raw one.", + "methods": [], + "extends": [] + }, + { + "name": "Valid", + "id": "interface-Valid-3f75a0ff51d12ccab50bfa13789cace778d5acff9f4feaa1607cdf1acb4a5ab4fcc687512424cb2252ec07568f6aea07a0b4f04469cbeed733c28654443ffd9e-1", "file": "src/app/herregistratie/domain/herregistratie.machine.ts", "deprecated": false, "deprecationMessage": "", @@ -7387,53 +7434,6 @@ "description": "

What we have AFTER parsing — branded/typed, guaranteed valid.

\n", "rawdescription": "\nWhat we have AFTER parsing — branded/typed, guaranteed valid.", "methods": [], - "extends": [] - }, - { - "name": "Valid", - "id": "interface-Valid-51c29a3fca3c5bd3eba53c9bc57714c79817ae8c86b1ed9c1cd76652d31b45ee7c635d35f7eb0a3a7e5bf9ea7c5da5c066cee3908c8fe4b9fe053586f834b133-1", - "file": "src/app/registratie/domain/change-request.machine.ts", - "deprecated": false, - "deprecationMessage": "", - "type": "interface", - "sourceCode": "import { Result, assertNever } from '@shared/kernel/fp';\nimport { Postcode, parsePostcode } from '@registratie/domain/value-objects/postcode';\n\n/** What the user is typing (raw, possibly invalid). */\nexport interface Draft {\n straat: string;\n postcode: string;\n woonplaats: string;\n}\n\n/** After parsing — postcode is the branded type, so downstream can't get a raw one. */\nexport interface Valid {\n straat: string;\n postcode: Postcode;\n woonplaats: string;\n}\n\nexport type Errors = Partial>;\n\n/**\n * The change-request (adreswijziging) form as one tagged union — the SAME idiom\n * as the wizards, just single-step. `draft`/`errors` exist only while Editing;\n * Submitting/Submitted/Failed carry the parsed `Valid`. Illegal states (submitting\n * an invalid draft, a success screen with errors) are unrepresentable.\n */\nexport type ChangeRequestState =\n | { tag: 'Editing'; draft: Draft; errors: Errors }\n | { tag: 'Submitting'; data: Valid }\n | { tag: 'Submitted'; data: Valid; referentie: string }\n | { tag: 'Failed'; data: Valid; error: string };\n\nexport const initial: ChangeRequestState = {\n tag: 'Editing',\n draft: { straat: '', postcode: '', woonplaats: '' },\n errors: {},\n};\n\n/** Parse via the value objects; on success hand back a Valid, else per-field errors. */\nfunction validate(draft: Draft): Result {\n const straat = draft.straat.trim();\n const postcode = parsePostcode(draft.postcode);\n const errors: Errors = {};\n if (!straat) errors.straat = $localize`:@@validation.straat:Vul straat en huisnummer in.`;\n if (!postcode.ok) errors.postcode = postcode.error;\n if (straat && postcode.ok) {\n return {\n ok: true,\n value: { straat, postcode: postcode.value, woonplaats: draft.woonplaats.trim() },\n };\n }\n return { ok: false, error: errors };\n}\n\nexport type ChangeRequestMsg =\n | { tag: 'SetField'; key: keyof Draft; value: string }\n | { tag: 'Submit' }\n | { tag: 'Retry' }\n | { tag: 'SubmitConfirmed'; referentie: string }\n | { tag: 'SubmitFailed'; error: string }\n | { tag: 'Reset' }\n | { tag: 'Seed'; state: ChangeRequestState }; // mount a specific state (stories/tests)\n\nexport function reduce(s: ChangeRequestState, m: ChangeRequestMsg): ChangeRequestState {\n switch (m.tag) {\n case 'SetField':\n return s.tag === 'Editing' ? { ...s, draft: { ...s.draft, [m.key]: m.value } } : s;\n case 'Submit': {\n if (s.tag !== 'Editing') return s;\n const r = validate(s.draft);\n return r.ok ? { tag: 'Submitting', data: r.value } : { ...s, errors: r.error };\n }\n case 'Retry':\n return s.tag === 'Failed' ? { tag: 'Submitting', data: s.data } : s;\n case 'SubmitConfirmed':\n return s.tag === 'Submitting'\n ? { tag: 'Submitted', data: s.data, referentie: m.referentie }\n : s;\n case 'SubmitFailed':\n return s.tag === 'Submitting' ? { tag: 'Failed', data: s.data, error: m.error } : s;\n case 'Reset':\n return initial;\n case 'Seed':\n return m.state;\n default:\n return assertNever(m);\n }\n}\n", - "properties": [ - { - "name": "postcode", - "deprecated": false, - "deprecationMessage": "", - "type": "Postcode", - "indexKey": "", - "optional": false, - "description": "", - "line": 14 - }, - { - "name": "straat", - "deprecated": false, - "deprecationMessage": "", - "type": "string", - "indexKey": "", - "optional": false, - "description": "", - "line": 13 - }, - { - "name": "woonplaats", - "deprecated": false, - "deprecationMessage": "", - "type": "string", - "indexKey": "", - "optional": false, - "description": "", - "line": 15 - } - ], - "indexSignatures": [], - "kind": 172, - "description": "

After parsing — postcode is the branded type, so downstream can't get a raw one.

\n", - "rawdescription": "\nAfter parsing — postcode is the branded type, so downstream can't get a raw one.", - "methods": [], "extends": [], "isDuplicate": true, "duplicateId": 1, @@ -7762,7 +7762,7 @@ "injectables": [ { "name": "AccessStore", - "id": "injectable-AccessStore-655afbfb523088e06215496431afe718aef62dc7a9cae7a08846bd9c95551ddab4b8c4916e4f7f7bc0418c8bac0439e7f04b13c7fb75dade52454f960c9a9476", + "id": "injectable-AccessStore-9e2ea473e1c1f9d8a6b2496d58a44d9689446d0a8c3af4e1c685cd4891345f34f9f67194b8a76156ad3215c19fd9e68e0ad5f82b24924abf68c0a405c7258f46", "file": "src/app/shared/application/access.store.ts", "properties": [ { @@ -7774,7 +7774,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 20, + "line": 22, "modifierKind": [ 123 ] @@ -7788,7 +7788,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 23, + "line": 25, "modifierKind": [ 123 ] @@ -7802,7 +7802,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 21, + "line": 23, "modifierKind": [ 123 ] @@ -7816,11 +7816,25 @@ "indexKey": "", "optional": false, "description": "

True once /me has resolved (success or failure) — lets a page-level gate tell\n"still loading" apart from "denied", so an admin doesn't flash the denial alert.

\n", - "line": 39, + "line": 41, "rawdescription": "\nTrue once `/me` has resolved (success or failure) — lets a page-level gate tell\n\"still loading\" apart from \"denied\", so an admin doesn't flash the denial alert.", "modifierKind": [ 148 ] + }, + { + "name": "ready$", + "defaultValue": "toObservable(this.ready)", + "deprecated": false, + "deprecationMessage": "", + "type": "unknown", + "indexKey": "", + "optional": false, + "description": "", + "line": 46, + "modifierKind": [ + 123 + ] } ], "methods": [ @@ -7839,7 +7853,7 @@ "optional": false, "returnType": "boolean", "typeParameters": [], - "line": 32, + "line": 34, "deprecated": false, "deprecationMessage": "", "jsdoctags": [ @@ -7855,13 +7869,28 @@ } } ] + }, + { + "name": "whenReady", + "args": [], + "optional": false, + "returnType": "Promise", + "typeParameters": [], + "line": 50, + "deprecated": false, + "deprecationMessage": "", + "rawdescription": "\nResolves once `/me` has settled (success or failure). The `capabilityGuard` awaits\nthis before deciding — otherwise it reads `can()` while `/me` is still loading and\nwrongly denies (deny-by-default), bouncing even an entitled user.", + "description": "

Resolves once /me has settled (success or failure). The capabilityGuard awaits\nthis before deciding — otherwise it reads can() while /me is still loading and\nwrongly denies (deny-by-default), bouncing even an entitled user.

\n", + "modifierKind": [ + 134 + ] } ], "deprecated": false, "deprecationMessage": "", "description": "

The current principal's capabilities (PRD-0002 §6) — one root singleton, like\nSessionStore/BigProfileStore. Global capabilities load once from GET /me;\na screen's own decision DTO (e.g. BriefViewDto.decisions) covers anything tied\nto a specific resource's live status — no extra round-trip needed for that.

\n

can() is deny-by-default: loading, failed, or an unrecognized capability all\nresolve to false. This store never derives a capability from a role — it only\nmirrors what the server already resolved.

\n", "rawdescription": "\n\nThe current principal's capabilities (PRD-0002 §6) — one root singleton, like\n`SessionStore`/`BigProfileStore`. Global capabilities load once from `GET /me`;\na screen's own decision DTO (e.g. `BriefViewDto.decisions`) covers anything tied\nto a specific resource's live status — no extra round-trip needed for that.\n\n`can()` is deny-by-default: loading, failed, or an unrecognized capability all\nresolve to `false`. This store never derives a capability from a role — it only\nmirrors what the server already resolved.\n", - "sourceCode": "import { Injectable, computed, inject } from '@angular/core';\nimport { RemoteData, fromResource } from '@shared/application/remote-data';\nimport { Capability } from '@shared/domain/capability';\nimport { MeAdapter, parseMe } from '@shared/infrastructure/me.adapter';\n\ntype Err = Error | undefined;\n\n/**\n * The current principal's capabilities (PRD-0002 §6) — one root singleton, like\n * `SessionStore`/`BigProfileStore`. Global capabilities load once from `GET /me`;\n * a screen's own decision DTO (e.g. `BriefViewDto.decisions`) covers anything tied\n * to a specific resource's live status — no extra round-trip needed for that.\n *\n * `can()` is deny-by-default: loading, failed, or an unrecognized capability all\n * resolve to `false`. This store never derives a capability from a role — it only\n * mirrors what the server already resolved.\n */\n@Injectable({ providedIn: 'root' })\nexport class AccessStore {\n private adapter = inject(MeAdapter);\n private meRes = this.adapter.meResource();\n\n private capabilities = computed>(() => {\n const rd = fromResource(this.meRes);\n if (rd.tag !== 'Success') return rd;\n const parsed = parseMe(rd.value);\n return parsed.ok\n ? { tag: 'Success', value: parsed.value }\n : { tag: 'Failure', error: new Error(parsed.error) };\n });\n\n can(capability: Capability): boolean {\n const rd = this.capabilities();\n return rd.tag === 'Success' && rd.value.includes(capability);\n }\n\n /** True once `/me` has resolved (success or failure) — lets a page-level gate tell\n \"still loading\" apart from \"denied\", so an admin doesn't flash the denial alert. */\n readonly ready = computed(() => {\n const tag = this.capabilities().tag;\n return tag === 'Success' || tag === 'Failure';\n });\n}\n", + "sourceCode": "import { Injectable, computed, inject } from '@angular/core';\nimport { toObservable } from '@angular/core/rxjs-interop';\nimport { filter, firstValueFrom } from 'rxjs';\nimport { RemoteData, fromResource } from '@shared/application/remote-data';\nimport { Capability } from '@shared/domain/capability';\nimport { MeAdapter, parseMe } from '@shared/infrastructure/me.adapter';\n\ntype Err = Error | undefined;\n\n/**\n * The current principal's capabilities (PRD-0002 §6) — one root singleton, like\n * `SessionStore`/`BigProfileStore`. Global capabilities load once from `GET /me`;\n * a screen's own decision DTO (e.g. `BriefViewDto.decisions`) covers anything tied\n * to a specific resource's live status — no extra round-trip needed for that.\n *\n * `can()` is deny-by-default: loading, failed, or an unrecognized capability all\n * resolve to `false`. This store never derives a capability from a role — it only\n * mirrors what the server already resolved.\n */\n@Injectable({ providedIn: 'root' })\nexport class AccessStore {\n private adapter = inject(MeAdapter);\n private meRes = this.adapter.meResource();\n\n private capabilities = computed>(() => {\n const rd = fromResource(this.meRes);\n if (rd.tag !== 'Success') return rd;\n const parsed = parseMe(rd.value);\n return parsed.ok\n ? { tag: 'Success', value: parsed.value }\n : { tag: 'Failure', error: new Error(parsed.error) };\n });\n\n can(capability: Capability): boolean {\n const rd = this.capabilities();\n return rd.tag === 'Success' && rd.value.includes(capability);\n }\n\n /** True once `/me` has resolved (success or failure) — lets a page-level gate tell\n \"still loading\" apart from \"denied\", so an admin doesn't flash the denial alert. */\n readonly ready = computed(() => {\n const tag = this.capabilities().tag;\n return tag === 'Success' || tag === 'Failure';\n });\n\n private ready$ = toObservable(this.ready);\n /** Resolves once `/me` has settled (success or failure). The `capabilityGuard` awaits\n this before deciding — otherwise it reads `can()` while `/me` is still loading and\n wrongly denies (deny-by-default), bouncing even an entitled user. */\n async whenReady(): Promise {\n if (this.ready()) return;\n await firstValueFrom(this.ready$.pipe(filter((r) => r)));\n }\n}\n", "extends": [], "type": "injectable" }, @@ -28151,7 +28180,7 @@ }, { "name": "SiteHeaderComponent", - "id": "component-SiteHeaderComponent-a0c115949f8f7f8c794708a232dd899ff4f8cef791c7c67a268bdab2addb5706bdb2657580819aff599f17276dd46cb703c40aa4db1191cdfa5d5de6e252b16b", + "id": "component-SiteHeaderComponent-5fc019799e1a7e63f899d40eb618b7209f4a666481cad865b1e33e7d6e0a691a6cacfbca89de3e7a199d0ee46d80b931b335c7a3560667f3b57d8072fa1d2b98", "file": "src/app/shared/layout/site-header/site-header.component.ts", "encapsulation": [], "entryComponents": [], @@ -28163,13 +28192,42 @@ "styles": [ "\n .logout {\n background: none;\n border: 0;\n padding: 0;\n cursor: pointer;\n text-decoration: underline;\n font: inherit;\n color: inherit;\n }\n /* CIBG's header nav has no bg by default in this build — the grey bar is ours.\n (.titlebar keeps its own robijn fill — --ro-layout — untouched; the breadcrumb\n inside it has no background of its own, so the bar's colour shows through.) */\n nav {\n background-color: var(--rhc-color-cool-grey-200);\n }\n " ], - "template": "
\n \n
\n
\n
\n
\n @if (trail().length) {\n \n }\n
\n
\n @if (session(); as s) {\n
\n {{ s.naam }}\n
\n
\n \n
\n }\n
\n
\n
\n
\n \n
\n", + "template": "
\n \n
\n
\n
\n
\n @if (trail().length) {\n \n }\n
\n
\n @if (session(); as s) {\n
\n {{ s.naam }}\n
\n
\n \n
\n }\n
\n
\n
\n
\n \n
\n", "templateUrl": [], "viewProviders": [], "hostDirectives": [], "inputsClass": [], "outputsClass": [], "propertiesClass": [ + { + "name": "access", + "defaultValue": "inject(AccessStore)", + "deprecated": false, + "deprecationMessage": "", + "type": "unknown", + "indexKey": "", + "optional": false, + "description": "", + "line": 126, + "modifierKind": [ + 123 + ] + }, + { + "name": "adminItems", + "defaultValue": "computed(() => ADMIN_NAV_ITEMS.filter((i) => this.access.can(i.cap)))", + "deprecated": false, + "deprecationMessage": "", + "type": "unknown", + "indexKey": "", + "optional": false, + "description": "

Injecting AccessStore here also warms /me at app start (the header renders on\nevery page), so the admin routes' guard usually finds caps already resolved.

\n", + "line": 129, + "rawdescription": "\nInjecting AccessStore here also warms `/me` at app start (the header renders on\nevery page), so the admin routes' guard usually finds caps already resolved.", + "modifierKind": [ + 124 + ] + }, { "name": "navItems", "defaultValue": "NAV_ITEMS", @@ -28179,7 +28237,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 100, + "line": 122, "modifierKind": [ 124, 148 @@ -28194,7 +28252,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 102, + "line": 124, "modifierKind": [ 123 ] @@ -28208,7 +28266,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 105, + "line": 131, "modifierKind": [ 148 ] @@ -28222,7 +28280,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 103, + "line": 125, "modifierKind": [ 123 ] @@ -28236,7 +28294,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 113, + "line": 139, "modifierKind": [ 124 ] @@ -28250,7 +28308,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 106, + "line": 132, "modifierKind": [ 123 ] @@ -28263,7 +28321,7 @@ "optional": false, "returnType": "void", "typeParameters": [], - "line": 115, + "line": 141, "deprecated": false, "deprecationMessage": "" } @@ -28288,7 +28346,7 @@ "description": "

Organism: CIBG Huisstijl site header — logo block, robijn titlebar (breadcrumb +\nuser menu), horizontal nav. ponytail: text wordmark, not the licensed Rijksoverheid\nbeeldmerk; no search box (no search feature yet).

\n", "rawdescription": "\nOrganism: CIBG Huisstijl site header — logo block, robijn titlebar (breadcrumb +\nuser menu), horizontal nav. ponytail: text wordmark, not the licensed Rijksoverheid\nbeeldmerk; no search box (no search feature yet).", "type": "component", - "sourceCode": "import { Component, computed, inject } from '@angular/core';\nimport { toSignal } from '@angular/core/rxjs-interop';\nimport { NavigationEnd, Router, RouterLink, RouterLinkActive } from '@angular/router';\nimport { filter, map } from 'rxjs/operators';\nimport { SESSION_PORT } from '@shared/application/session.port';\nimport { BreadcrumbComponent } from '@shared/layout/breadcrumb/breadcrumb.component';\nimport { trailFor } from '@shared/layout/breadcrumb/breadcrumb-trail';\n\ninterface HeaderNavItem {\n readonly label: string;\n readonly to: string;\n}\n\nconst NAV_ITEMS: readonly HeaderNavItem[] = [\n { label: $localize`:@@header.nav.overzicht:Overzicht`, to: '/dashboard' },\n { label: $localize`:@@header.nav.gegevens:Mijn gegevens`, to: '/registratie' },\n { label: $localize`:@@header.nav.herregistratie:Herregistratie`, to: '/herregistratie' },\n { label: $localize`:@@header.nav.inschrijven:Inschrijven`, to: '/registreren' },\n];\n\n/** Organism: CIBG Huisstijl site header — logo block, robijn titlebar (breadcrumb +\n user menu), horizontal nav. ponytail: text wordmark, not the licensed Rijksoverheid\n beeldmerk; no search box (no search feature yet). */\n@Component({\n selector: 'app-site-header',\n imports: [RouterLink, RouterLinkActive, BreadcrumbComponent],\n styles: [\n `\n .logout {\n background: none;\n border: 0;\n padding: 0;\n cursor: pointer;\n text-decoration: underline;\n font: inherit;\n color: inherit;\n }\n /* CIBG's header nav has no bg by default in this build — the grey bar is ours.\n (.titlebar keeps its own robijn fill — --ro-layout — untouched; the breadcrumb\n inside it has no background of its own, so the bar's colour shows through.) */\n nav {\n background-color: var(--rhc-color-cool-grey-200);\n }\n `,\n ],\n template: `\n
\n \n
\n
\n
\n
\n @if (trail().length) {\n \n }\n
\n
\n @if (session(); as s) {\n
\n {{ s.naam }}\n
\n
\n \n
\n }\n
\n
\n
\n
\n \n
\n `,\n})\nexport class SiteHeaderComponent {\n protected readonly navItems = NAV_ITEMS;\n\n private router = inject(Router);\n private sessionPort = inject(SESSION_PORT, { optional: true });\n\n readonly session = computed(() => this.sessionPort?.session() ?? null);\n private url = toSignal(\n this.router.events.pipe(\n filter((e) => e instanceof NavigationEnd),\n map(() => this.router.url),\n ),\n { initialValue: this.router.url },\n );\n protected trail = computed(() => trailFor(this.url()));\n\n logout() {\n this.sessionPort?.logout();\n this.router.navigate(['/login']);\n }\n}\n", + "sourceCode": "import { Component, computed, inject } from '@angular/core';\nimport { toSignal } from '@angular/core/rxjs-interop';\nimport { NavigationEnd, Router, RouterLink, RouterLinkActive } from '@angular/router';\nimport { filter, map } from 'rxjs/operators';\nimport { SESSION_PORT } from '@shared/application/session.port';\nimport { AccessStore } from '@shared/application/access.store';\nimport { Capability } from '@shared/domain/capability';\nimport { BreadcrumbComponent } from '@shared/layout/breadcrumb/breadcrumb.component';\nimport { trailFor } from '@shared/layout/breadcrumb/breadcrumb-trail';\n\ninterface HeaderNavItem {\n readonly label: string;\n readonly to: string;\n}\n\nconst NAV_ITEMS: readonly HeaderNavItem[] = [\n { label: $localize`:@@header.nav.overzicht:Overzicht`, to: '/dashboard' },\n { label: $localize`:@@header.nav.gegevens:Mijn gegevens`, to: '/registratie' },\n { label: $localize`:@@header.nav.herregistratie:Herregistratie`, to: '/herregistratie' },\n { label: $localize`:@@header.nav.inschrijven:Inschrijven`, to: '/registreren' },\n];\n\n/** Admin-only nav, shown only when `/me` grants the matching capability — the pages\n are otherwise reachable by URL alone. */\nconst ADMIN_NAV_ITEMS: readonly (HeaderNavItem & { readonly cap: Capability })[] = [\n {\n label: $localize`:@@header.nav.huisstijl:Huisstijl`,\n to: '/brief/huisstijl',\n cap: 'orgtemplate:edit',\n },\n {\n label: $localize`:@@header.nav.stamdata:Stamdata`,\n to: '/beheer/stamdata',\n cap: 'stamdata:edit',\n },\n];\n\n/** Organism: CIBG Huisstijl site header — logo block, robijn titlebar (breadcrumb +\n user menu), horizontal nav. ponytail: text wordmark, not the licensed Rijksoverheid\n beeldmerk; no search box (no search feature yet). */\n@Component({\n selector: 'app-site-header',\n imports: [RouterLink, RouterLinkActive, BreadcrumbComponent],\n styles: [\n `\n .logout {\n background: none;\n border: 0;\n padding: 0;\n cursor: pointer;\n text-decoration: underline;\n font: inherit;\n color: inherit;\n }\n /* CIBG's header nav has no bg by default in this build — the grey bar is ours.\n (.titlebar keeps its own robijn fill — --ro-layout — untouched; the breadcrumb\n inside it has no background of its own, so the bar's colour shows through.) */\n nav {\n background-color: var(--rhc-color-cool-grey-200);\n }\n `,\n ],\n template: `\n
\n \n
\n
\n
\n
\n @if (trail().length) {\n \n }\n
\n
\n @if (session(); as s) {\n
\n {{ s.naam }}\n
\n
\n \n
\n }\n
\n
\n
\n
\n \n
\n `,\n})\nexport class SiteHeaderComponent {\n protected readonly navItems = NAV_ITEMS;\n\n private router = inject(Router);\n private sessionPort = inject(SESSION_PORT, { optional: true });\n private access = inject(AccessStore);\n /** Injecting AccessStore here also warms `/me` at app start (the header renders on\n every page), so the admin routes' guard usually finds caps already resolved. */\n protected adminItems = computed(() => ADMIN_NAV_ITEMS.filter((i) => this.access.can(i.cap)));\n\n readonly session = computed(() => this.sessionPort?.session() ?? null);\n private url = toSignal(\n this.router.events.pipe(\n filter((e) => e instanceof NavigationEnd),\n map(() => this.router.url),\n ),\n { initialValue: this.router.url },\n );\n protected trail = computed(() => trailFor(this.url()));\n\n logout() {\n this.sessionPort?.logout();\n this.router.navigate(['/login']);\n }\n}\n", "assetsDirs": [], "styleUrlsData": "", "stylesData": "\n .logout {\n background: none;\n border: 0;\n padding: 0;\n cursor: pointer;\n text-decoration: underline;\n font: inherit;\n color: inherit;\n }\n /* CIBG's header nav has no bg by default in this build — the grey bar is ours.\n (.titlebar keeps its own robijn fill — --ro-layout — untouched; the breadcrumb\n inside it has no background of its own, so the bar's colour shows through.) */\n nav {\n background-color: var(--rhc-color-cool-grey-200);\n }\n \n", @@ -30589,6 +30647,18 @@ "type": "ReadonlyArray", "defaultValue": "['queued', 'uploading', 'complete']" }, + { + "name": "ADMIN_NAV_ITEMS", + "ctype": "miscellaneous", + "subtype": "variable", + "file": "src/app/shared/layout/site-header/site-header.component.ts", + "deprecated": false, + "deprecationMessage": "", + "type": "(unknown)[]", + "defaultValue": "[\n {\n label: $localize`:@@header.nav.huisstijl:Huisstijl`,\n to: '/brief/huisstijl',\n cap: 'orgtemplate:edit',\n },\n {\n label: $localize`:@@header.nav.stamdata:Stamdata`,\n to: '/beheer/stamdata',\n cap: 'stamdata:edit',\n },\n]", + "rawdescription": "Admin-only nav, shown only when `/me` grants the matching capability — the pages\nare otherwise reachable by URL alone.", + "description": "

Admin-only nav, shown only when /me grants the matching capability — the pages\nare otherwise reachable by URL alone.

\n" + }, { "name": "appConfig", "ctype": "miscellaneous", @@ -30873,26 +30943,6 @@ "type": "OrgTemplateState", "defaultValue": "{ tag: 'loading' }" }, - { - "name": "initial", - "ctype": "miscellaneous", - "subtype": "variable", - "file": "src/app/herregistratie/domain/herregistratie.machine.ts", - "deprecated": false, - "deprecationMessage": "", - "type": "WizardState", - "defaultValue": "{\n tag: 'Editing',\n step: 1,\n draft: { uren: '', jaren: '', punten: '' },\n errors: {},\n upload: initialUpload,\n}" - }, - { - "name": "initial", - "ctype": "miscellaneous", - "subtype": "variable", - "file": "src/app/herregistratie/domain/intake.machine.ts", - "deprecated": false, - "deprecationMessage": "", - "type": "IntakeState", - "defaultValue": "{\n tag: 'Answering',\n answers: {},\n cursor: 0,\n errors: {},\n scholingThreshold: SCHOLING_THRESHOLD_DEFAULT,\n}" - }, { "name": "initial", "ctype": "miscellaneous", @@ -30913,6 +30963,26 @@ "type": "RegistratieState", "defaultValue": "{\n tag: 'Invullen',\n draft: emptyDraft,\n cursor: 0,\n errors: {},\n upload: initialUpload,\n}" }, + { + "name": "initial", + "ctype": "miscellaneous", + "subtype": "variable", + "file": "src/app/herregistratie/domain/herregistratie.machine.ts", + "deprecated": false, + "deprecationMessage": "", + "type": "WizardState", + "defaultValue": "{\n tag: 'Editing',\n step: 1,\n draft: { uren: '', jaren: '', punten: '' },\n errors: {},\n upload: initialUpload,\n}" + }, + { + "name": "initial", + "ctype": "miscellaneous", + "subtype": "variable", + "file": "src/app/herregistratie/domain/intake.machine.ts", + "deprecated": false, + "deprecationMessage": "", + "type": "IntakeState", + "defaultValue": "{\n tag: 'Answering',\n answers: {},\n cursor: 0,\n errors: {},\n scholingThreshold: SCHOLING_THRESHOLD_DEFAULT,\n}" + }, { "name": "initialUpload", "ctype": "miscellaneous", @@ -30923,6 +30993,16 @@ "type": "UploadState", "defaultValue": "{\n categories: [],\n uploads: [],\n deliveryChannel: {},\n rejections: {},\n backgroundSyncAvailable: false,\n}" }, + { + "name": "isRole", + "ctype": "miscellaneous", + "subtype": "variable", + "file": "src/app/shared/infrastructure/role.ts", + "deprecated": false, + "deprecationMessage": "", + "type": "unknown", + "defaultValue": "(v: string | null): v is Role =>\n v === 'drafter' || v === 'approver' || v === 'admin'" + }, { "name": "JA_NEE", "ctype": "miscellaneous", @@ -31234,9 +31314,9 @@ "deprecated": false, "deprecationMessage": "", "type": "[]", - "defaultValue": "['/api/v1/brief', '/api/v1/admin/org-template', '/api/v1/me']", - "rawdescription": "Dev-only: stamps role-aware requests with the current `?role=` as an `X-Role`\nheader so the backend can enforce the drafter/approver/admin rules. Only the\nbrief, org-template and /me endpoints carry it (WP-23 widened the set — /me must\nsee the role or `AccessStore` could never learn a capability); everything else\nis untouched.", - "description": "

Dev-only: stamps role-aware requests with the current ?role= as an X-Role\nheader so the backend can enforce the drafter/approver/admin rules. Only the\nbrief, org-template and /me endpoints carry it (WP-23 widened the set — /me must\nsee the role or AccessStore could never learn a capability); everything else\nis untouched.

\n" + "defaultValue": "[\n '/api/v1/brief',\n '/api/v1/admin/org-template',\n '/api/v1/stamdata',\n '/api/v1/me',\n]", + "rawdescription": "Dev-only: stamps role-aware requests with the current `?role=` as an `X-Role`\nheader so the backend can enforce the drafter/approver/admin rules. Only the\nbrief, org-template, stamdata and /me endpoints carry it (WP-23 widened the set —\n/me must see the role or `AccessStore` could never learn a capability; WP-29 added\n/stamdata, whose admin-only reads 403 without it); everything else is untouched.\nA new admin-gated endpoint MUST be added here or its page silently 403s.", + "description": "

Dev-only: stamps role-aware requests with the current ?role= as an X-Role\nheader so the backend can enforce the drafter/approver/admin rules. Only the\nbrief, org-template, stamdata and /me endpoints carry it (WP-23 widened the set —\n/me must see the role or AccessStore could never learn a capability; WP-29 added\n/stamdata, whose admin-only reads 403 without it); everything else is untouched.\nA new admin-gated endpoint MUST be added here or its page silently 403s.

\n" }, { "name": "roleInterceptor", @@ -31338,11 +31418,11 @@ "name": "STEPS", "ctype": "miscellaneous", "subtype": "variable", - "file": "src/app/herregistratie/domain/intake.machine.ts", + "file": "src/app/registratie/domain/registratie-wizard.machine.ts", "deprecated": false, "deprecationMessage": "", "type": "StepId[]", - "defaultValue": "['buitenland', 'werk', 'review']", + "defaultValue": "['adres', 'beroep', 'controle']", "rawdescription": "The fixed step list. Number of steps never changes; questions reveal inline.", "description": "

The fixed step list. Number of steps never changes; questions reveal inline.

\n" }, @@ -31350,11 +31430,11 @@ "name": "STEPS", "ctype": "miscellaneous", "subtype": "variable", - "file": "src/app/registratie/domain/registratie-wizard.machine.ts", + "file": "src/app/herregistratie/domain/intake.machine.ts", "deprecated": false, "deprecationMessage": "", "type": "StepId[]", - "defaultValue": "['adres', 'beroep', 'controle']", + "defaultValue": "['buitenland', 'werk', 'review']", "rawdescription": "The fixed step list. Number of steps never changes; questions reveal inline.", "description": "

The fixed step list. Number of steps never changes; questions reveal inline.

\n" }, @@ -31368,6 +31448,18 @@ "type": "string", "defaultValue": "'session-v1'" }, + { + "name": "STORAGE_KEY", + "ctype": "miscellaneous", + "subtype": "variable", + "file": "src/app/shared/infrastructure/role.ts", + "deprecated": false, + "deprecationMessage": "", + "type": "string", + "defaultValue": "'dev-role'", + "rawdescription": "Dev-only role stand-in (the reading MECHANISM; the `Role` type is domain). This\nPOC has one faked self-service user and no real identities, so the two-person\nletter workflow (drafter vs approver) plus admin is driven by a `?role=` query\nparam. The backend receives it as an `X-Role` header (see role.interceptor),\nresolves it into a `Principal` server-side, and is the sole authority on what that\nprincipal may do (PRD-0002 phase P1, `Authz.Can`) — the FE only renders the\nresulting decision flags, it no longer derives permission from this value itself.\n\n**Sticky within the tab (sessionStorage):** the interceptor reads this per request,\nbut navigation drops the query param (login redirects to /dashboard, RouterLinks\ndon't carry it), which would silently revert an admin to drafter mid-session and\n403 the admin endpoints. So a `?role=` seen in the URL is remembered for the tab;\nlater requests use the remembered value. Set `?role=drafter` (or a fresh tab) to\nreset. Dev-only — the interceptor itself is only wired under `isDevMode()`.", + "description": "

Dev-only role stand-in (the reading MECHANISM; the Role type is domain). This\nPOC has one faked self-service user and no real identities, so the two-person\nletter workflow (drafter vs approver) plus admin is driven by a ?role= query\nparam. The backend receives it as an X-Role header (see role.interceptor),\nresolves it into a Principal server-side, and is the sole authority on what that\nprincipal may do (PRD-0002 phase P1, Authz.Can) — the FE only renders the\nresulting decision flags, it no longer derives permission from this value itself.

\n

Sticky within the tab (sessionStorage): the interceptor reads this per request,\nbut navigation drops the query param (login redirects to /dashboard, RouterLinks\ndon't carry it), which would silently revert an admin to drafter mid-session and\n403 the admin endpoints. So a ?role= seen in the URL is remembered for the tab;\nlater requests use the remembered value. Set ?role=drafter (or a fresh tab) to\nreset. Dev-only — the interceptor itself is only wired under isDevMode().

\n" + }, { "name": "SUBMIT_FAILED", "ctype": "miscellaneous", @@ -31725,6 +31817,35 @@ } ] }, + { + "name": "back", + "file": "src/app/registratie/domain/registratie-wizard.machine.ts", + "ctype": "miscellaneous", + "subtype": "function", + "deprecated": false, + "deprecationMessage": "", + "description": "", + "args": [ + { + "name": "s", + "type": "RegistratieState", + "deprecated": false, + "deprecationMessage": "" + } + ], + "returnType": "RegistratieState", + "jsdoctags": [ + { + "name": "s", + "type": "RegistratieState", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + } + ] + }, { "name": "back", "file": "src/app/herregistratie/domain/herregistratie.machine.ts", @@ -31783,35 +31904,6 @@ } ] }, - { - "name": "back", - "file": "src/app/registratie/domain/registratie-wizard.machine.ts", - "ctype": "miscellaneous", - "subtype": "function", - "deprecated": false, - "deprecationMessage": "", - "description": "", - "args": [ - { - "name": "s", - "type": "RegistratieState", - "deprecated": false, - "deprecationMessage": "" - } - ], - "returnType": "RegistratieState", - "jsdoctags": [ - { - "name": "s", - "type": "RegistratieState", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - } - ] - }, { "name": "blockActions", "file": "src/app/registratie/domain/block-actions.ts", @@ -31975,7 +32067,7 @@ "subtype": "function", "deprecated": false, "deprecationMessage": "", - "description": "

Route guard factory (PRD-0002 §6): authenticated AND holding capability, else\nredirect. No route in this app currently needs a capability gate — brief's\ncanApprove/canReject/canSend are per-action, not per-page (both actors land on\nthe same /brief page and see different actions) — so this exists as the\navailable building block for the day a route-level gate is needed, e.g. a future\napprover-only page.

\n", + "description": "

Route guard factory (PRD-0002 §6): authenticated AND holding capability, else\nredirect. Used by the admin pages (/brief/huisstijl, /beheer/stamdata).

\n

Async on purpose: can() is deny-by-default, so it must not be read while /me\nis still loading — it would deny an entitled admin and bounce them. We await\nAccessStore.whenReady() (caps resolved) before deciding. An unauthenticated user\ngoes to /login; an authenticated-but-unentitled user goes to /dashboard (they're\nlogged in, just not allowed here — no re-login loop). The backend re-enforces\nregardless (403); this guard is the UX pre-gate.

\n", "args": [ { "name": "capability", @@ -32521,7 +32613,7 @@ "subtype": "function", "deprecated": false, "deprecationMessage": "", - "description": "

Dev-only role stand-in (the reading MECHANISM; the Role type is domain). This\nPOC has one faked self-service user and no real identities, so the two-person\nletter workflow (drafter vs approver) is driven by a ?role= query param —\nexactly the pattern of the ?scenario= toggle. The backend receives it as an\nX-Role header (see role.interceptor), resolves it into a Principal\nserver-side, and is the sole authority on what that principal may do (PRD-0002\nphase P1, Authz.Can) — the FE only renders the resulting decision flags, it no\nlonger derives permission from this value itself.

\n", + "description": "", "args": [], "returnType": "Role" }, @@ -32538,7 +32630,7 @@ }, { "name": "currentStep", - "file": "src/app/herregistratie/domain/intake.machine.ts", + "file": "src/app/registratie/domain/registratie-wizard.machine.ts", "ctype": "miscellaneous", "subtype": "function", "deprecated": false, @@ -32567,7 +32659,7 @@ }, { "name": "currentStep", - "file": "src/app/registratie/domain/registratie-wizard.machine.ts", + "file": "src/app/herregistratie/domain/intake.machine.ts", "ctype": "miscellaneous", "subtype": "function", "deprecated": false, @@ -33292,6 +33384,50 @@ } ] }, + { + "name": "gaNaarStap", + "file": "src/app/registratie/domain/registratie-wizard.machine.ts", + "ctype": "miscellaneous", + "subtype": "function", + "deprecated": false, + "deprecationMessage": "", + "description": "

Jump back to an earlier step to correct data (controle → step N). Forward\njumps are not allowed (would skip validation). Preserves the draft.

\n", + "args": [ + { + "name": "s", + "type": "RegistratieState", + "deprecated": false, + "deprecationMessage": "" + }, + { + "name": "cursor", + "type": "number", + "deprecated": false, + "deprecationMessage": "" + } + ], + "returnType": "RegistratieState", + "jsdoctags": [ + { + "name": "s", + "type": "RegistratieState", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + }, + { + "name": "cursor", + "type": "number", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + } + ] + }, { "name": "gaNaarStap", "file": "src/app/herregistratie/domain/herregistratie.machine.ts", @@ -33378,50 +33514,6 @@ } ] }, - { - "name": "gaNaarStap", - "file": "src/app/registratie/domain/registratie-wizard.machine.ts", - "ctype": "miscellaneous", - "subtype": "function", - "deprecated": false, - "deprecationMessage": "", - "description": "

Jump back to an earlier step to correct data (controle → step N). Forward\njumps are not allowed (would skip validation). Preserves the draft.

\n", - "args": [ - { - "name": "s", - "type": "RegistratieState", - "deprecated": false, - "deprecationMessage": "" - }, - { - "name": "cursor", - "type": "number", - "deprecated": false, - "deprecationMessage": "" - } - ], - "returnType": "RegistratieState", - "jsdoctags": [ - { - "name": "s", - "type": "RegistratieState", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - }, - { - "name": "cursor", - "type": "number", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - } - ] - }, { "name": "groupParagraphs", "file": "src/app/brief/ui/letter-canvas/letter-canvas.component.ts", @@ -33476,6 +33568,35 @@ } ] }, + { + "name": "hasProgress", + "file": "src/app/registratie/domain/registratie-wizard.machine.ts", + "ctype": "miscellaneous", + "subtype": "function", + "deprecated": false, + "deprecationMessage": "", + "description": "

Has the user meaningfully started, so it's worth persisting as a Concept? Excludes\nthe automatic BRP address prefill on step 0 — a bare page visit creates nothing.\nponytail: an address typed at step 0 without any of these signals is not yet\npersisted (created once they advance/choose); accepted regression vs. sessionStorage.

\n", + "args": [ + { + "name": "s", + "type": "Extract", + "deprecated": false, + "deprecationMessage": "" + } + ], + "returnType": "boolean", + "jsdoctags": [ + { + "name": "s", + "type": "Extract", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + } + ] + }, { "name": "hasProgress", "file": "src/app/herregistratie/domain/herregistratie.machine.ts", @@ -33534,35 +33655,6 @@ } ] }, - { - "name": "hasProgress", - "file": "src/app/registratie/domain/registratie-wizard.machine.ts", - "ctype": "miscellaneous", - "subtype": "function", - "deprecated": false, - "deprecationMessage": "", - "description": "

Has the user meaningfully started, so it's worth persisting as a Concept? Excludes\nthe automatic BRP address prefill on step 0 — a bare page visit creates nothing.\nponytail: an address typed at step 0 without any of these signals is not yet\npersisted (created once they advance/choose); accepted regression vs. sessionStorage.

\n", - "args": [ - { - "name": "s", - "type": "Extract", - "deprecated": false, - "deprecationMessage": "" - } - ], - "returnType": "boolean", - "jsdoctags": [ - { - "name": "s", - "type": "Extract", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - } - ] - }, { "name": "herregistratieDeadline", "file": "src/app/registratie/domain/registration.policy.ts", @@ -34723,6 +34815,35 @@ } ] }, + { + "name": "next", + "file": "src/app/registratie/domain/registratie-wizard.machine.ts", + "ctype": "miscellaneous", + "subtype": "function", + "deprecated": false, + "deprecationMessage": "", + "description": "", + "args": [ + { + "name": "s", + "type": "RegistratieState", + "deprecated": false, + "deprecationMessage": "" + } + ], + "returnType": "RegistratieState", + "jsdoctags": [ + { + "name": "s", + "type": "RegistratieState", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + } + ] + }, { "name": "next", "file": "src/app/herregistratie/domain/herregistratie.machine.ts", @@ -34781,35 +34902,6 @@ } ] }, - { - "name": "next", - "file": "src/app/registratie/domain/registratie-wizard.machine.ts", - "ctype": "miscellaneous", - "subtype": "function", - "deprecated": false, - "deprecationMessage": "", - "description": "", - "args": [ - { - "name": "s", - "type": "RegistratieState", - "deprecated": false, - "deprecationMessage": "" - } - ], - "returnType": "RegistratieState", - "jsdoctags": [ - { - "name": "s", - "type": "RegistratieState", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - } - ] - }, { "name": "nextLocalIndex", "file": "src/app/brief/domain/brief.machine.ts", @@ -36437,94 +36529,6 @@ } ] }, - { - "name": "reduce", - "file": "src/app/herregistratie/domain/herregistratie.machine.ts", - "ctype": "miscellaneous", - "subtype": "function", - "deprecated": false, - "deprecationMessage": "", - "description": "", - "args": [ - { - "name": "s", - "type": "WizardState", - "deprecated": false, - "deprecationMessage": "" - }, - { - "name": "m", - "type": "WizardMsg", - "deprecated": false, - "deprecationMessage": "" - } - ], - "returnType": "WizardState", - "jsdoctags": [ - { - "name": "s", - "type": "WizardState", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - }, - { - "name": "m", - "type": "WizardMsg", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - } - ] - }, - { - "name": "reduce", - "file": "src/app/herregistratie/domain/intake.machine.ts", - "ctype": "miscellaneous", - "subtype": "function", - "deprecated": false, - "deprecationMessage": "", - "description": "", - "args": [ - { - "name": "s", - "type": "IntakeState", - "deprecated": false, - "deprecationMessage": "" - }, - { - "name": "m", - "type": "IntakeMsg", - "deprecated": false, - "deprecationMessage": "" - } - ], - "returnType": "IntakeState", - "jsdoctags": [ - { - "name": "s", - "type": "IntakeState", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - }, - { - "name": "m", - "type": "IntakeMsg", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - } - ] - }, { "name": "reduce", "file": "src/app/registratie/domain/change-request.machine.ts", @@ -36613,6 +36617,94 @@ } ] }, + { + "name": "reduce", + "file": "src/app/herregistratie/domain/herregistratie.machine.ts", + "ctype": "miscellaneous", + "subtype": "function", + "deprecated": false, + "deprecationMessage": "", + "description": "", + "args": [ + { + "name": "s", + "type": "WizardState", + "deprecated": false, + "deprecationMessage": "" + }, + { + "name": "m", + "type": "WizardMsg", + "deprecated": false, + "deprecationMessage": "" + } + ], + "returnType": "WizardState", + "jsdoctags": [ + { + "name": "s", + "type": "WizardState", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + }, + { + "name": "m", + "type": "WizardMsg", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + } + ] + }, + { + "name": "reduce", + "file": "src/app/herregistratie/domain/intake.machine.ts", + "ctype": "miscellaneous", + "subtype": "function", + "deprecated": false, + "deprecationMessage": "", + "description": "", + "args": [ + { + "name": "s", + "type": "IntakeState", + "deprecated": false, + "deprecationMessage": "" + }, + { + "name": "m", + "type": "IntakeMsg", + "deprecated": false, + "deprecationMessage": "" + } + ], + "returnType": "IntakeState", + "jsdoctags": [ + { + "name": "s", + "type": "IntakeState", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + }, + { + "name": "m", + "type": "IntakeMsg", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + } + ] + }, { "name": "reduceUpload", "file": "src/app/shared/upload/upload.machine.ts", @@ -36930,6 +37022,50 @@ } ] }, + { + "name": "resolve", + "file": "src/app/registratie/domain/registratie-wizard.machine.ts", + "ctype": "miscellaneous", + "subtype": "function", + "deprecated": false, + "deprecationMessage": "", + "description": "", + "args": [ + { + "name": "s", + "type": "RegistratieState", + "deprecated": false, + "deprecationMessage": "" + }, + { + "name": "r", + "type": "Result", + "deprecated": false, + "deprecationMessage": "" + } + ], + "returnType": "RegistratieState", + "jsdoctags": [ + { + "name": "s", + "type": "RegistratieState", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + }, + { + "name": "r", + "type": "Result", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + } + ] + }, { "name": "resolve", "file": "src/app/herregistratie/domain/herregistratie.machine.ts", @@ -37018,50 +37154,6 @@ } ] }, - { - "name": "resolve", - "file": "src/app/registratie/domain/registratie-wizard.machine.ts", - "ctype": "miscellaneous", - "subtype": "function", - "deprecated": false, - "deprecationMessage": "", - "description": "", - "args": [ - { - "name": "s", - "type": "RegistratieState", - "deprecated": false, - "deprecationMessage": "" - }, - { - "name": "r", - "type": "Result", - "deprecated": false, - "deprecationMessage": "" - } - ], - "returnType": "RegistratieState", - "jsdoctags": [ - { - "name": "s", - "type": "RegistratieState", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - }, - { - "name": "r", - "type": "Result", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - } - ] - }, { "name": "restore", "file": "src/app/auth/application/session.store.ts", @@ -37449,63 +37541,6 @@ } ] }, - { - "name": "setField", - "file": "src/app/herregistratie/domain/herregistratie.machine.ts", - "ctype": "miscellaneous", - "subtype": "function", - "deprecated": false, - "deprecationMessage": "", - "description": "

Update one draft field while editing; ignored in any other state.

\n", - "args": [ - { - "name": "s", - "type": "WizardState", - "deprecated": false, - "deprecationMessage": "" - }, - { - "name": "key", - "deprecated": false, - "deprecationMessage": "" - }, - { - "name": "value", - "type": "string", - "deprecated": false, - "deprecationMessage": "" - } - ], - "returnType": "WizardState", - "jsdoctags": [ - { - "name": "s", - "type": "WizardState", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - }, - { - "name": "key", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - }, - { - "name": "value", - "type": "string", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - } - ] - }, { "name": "setField", "file": "src/app/registratie/domain/registratie-wizard.machine.ts", @@ -37565,6 +37600,63 @@ } ] }, + { + "name": "setField", + "file": "src/app/herregistratie/domain/herregistratie.machine.ts", + "ctype": "miscellaneous", + "subtype": "function", + "deprecated": false, + "deprecationMessage": "", + "description": "

Update one draft field while editing; ignored in any other state.

\n", + "args": [ + { + "name": "s", + "type": "WizardState", + "deprecated": false, + "deprecationMessage": "" + }, + { + "name": "key", + "deprecated": false, + "deprecationMessage": "" + }, + { + "name": "value", + "type": "string", + "deprecated": false, + "deprecationMessage": "" + } + ], + "returnType": "WizardState", + "jsdoctags": [ + { + "name": "s", + "type": "WizardState", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + }, + { + "name": "key", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + }, + { + "name": "value", + "type": "string", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + } + ] + }, { "name": "setPolicy", "file": "src/app/herregistratie/domain/intake.machine.ts", @@ -37765,6 +37857,35 @@ } ] }, + { + "name": "submit", + "file": "src/app/registratie/domain/registratie-wizard.machine.ts", + "ctype": "miscellaneous", + "subtype": "function", + "deprecated": false, + "deprecationMessage": "", + "description": "", + "args": [ + { + "name": "s", + "type": "RegistratieState", + "deprecated": false, + "deprecationMessage": "" + } + ], + "returnType": "RegistratieState", + "jsdoctags": [ + { + "name": "s", + "type": "RegistratieState", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + } + ] + }, { "name": "submit", "file": "src/app/herregistratie/domain/herregistratie.machine.ts", @@ -37823,35 +37944,6 @@ } ] }, - { - "name": "submit", - "file": "src/app/registratie/domain/registratie-wizard.machine.ts", - "ctype": "miscellaneous", - "subtype": "function", - "deprecated": false, - "deprecationMessage": "", - "description": "", - "args": [ - { - "name": "s", - "type": "RegistratieState", - "deprecated": false, - "deprecationMessage": "" - } - ], - "returnType": "RegistratieState", - "jsdoctags": [ - { - "name": "s", - "type": "RegistratieState", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - } - ] - }, { "name": "submittedRow", "file": "src/app/registratie/domain/aanvraag-view.ts", @@ -38315,50 +38407,6 @@ } ] }, - { - "name": "upload", - "file": "src/app/herregistratie/domain/herregistratie.machine.ts", - "ctype": "miscellaneous", - "subtype": "function", - "deprecated": false, - "deprecationMessage": "", - "description": "

Route an upload sub-message through the pure upload reducer (Editing only).

\n", - "args": [ - { - "name": "s", - "type": "WizardState", - "deprecated": false, - "deprecationMessage": "" - }, - { - "name": "msg", - "type": "UploadMsg", - "deprecated": false, - "deprecationMessage": "" - } - ], - "returnType": "WizardState", - "jsdoctags": [ - { - "name": "s", - "type": "WizardState", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - }, - { - "name": "msg", - "type": "UploadMsg", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - } - ] - }, { "name": "upload", "file": "src/app/registratie/domain/registratie-wizard.machine.ts", @@ -38403,6 +38451,50 @@ } ] }, + { + "name": "upload", + "file": "src/app/herregistratie/domain/herregistratie.machine.ts", + "ctype": "miscellaneous", + "subtype": "function", + "deprecated": false, + "deprecationMessage": "", + "description": "

Route an upload sub-message through the pure upload reducer (Editing only).

\n", + "args": [ + { + "name": "s", + "type": "WizardState", + "deprecated": false, + "deprecationMessage": "" + }, + { + "name": "msg", + "type": "UploadMsg", + "deprecated": false, + "deprecationMessage": "" + } + ], + "returnType": "WizardState", + "jsdoctags": [ + { + "name": "s", + "type": "WizardState", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + }, + { + "name": "msg", + "type": "UploadMsg", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + } + ] + }, { "name": "uploadContentUrl", "file": "src/app/shared/upload/upload.adapter.ts", @@ -38432,6 +38524,35 @@ } ] }, + { + "name": "validate", + "file": "src/app/registratie/domain/change-request.machine.ts", + "ctype": "miscellaneous", + "subtype": "function", + "deprecated": false, + "deprecationMessage": "", + "description": "

Parse via the value objects; on success hand back a Valid, else per-field errors.

\n", + "args": [ + { + "name": "draft", + "type": "Draft", + "deprecated": false, + "deprecationMessage": "" + } + ], + "returnType": "Result", + "jsdoctags": [ + { + "name": "draft", + "type": "Draft", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + } + ] + }, { "name": "validate", "file": "src/app/herregistratie/domain/herregistratie.machine.ts", @@ -38477,31 +38598,46 @@ ] }, { - "name": "validate", - "file": "src/app/registratie/domain/change-request.machine.ts", + "name": "validateAll", + "file": "src/app/registratie/domain/registratie-wizard.machine.ts", "ctype": "miscellaneous", "subtype": "function", "deprecated": false, "deprecationMessage": "", - "description": "

Parse via the value objects; on success hand back a Valid, else per-field errors.

\n", + "description": "

Parse the whole wizard into a ValidRegistratie (called on submit).

\n", "args": [ { - "name": "draft", + "name": "d", "type": "Draft", "deprecated": false, "deprecationMessage": "" + }, + { + "name": "upload", + "type": "UploadState", + "deprecated": false, + "deprecationMessage": "" } ], - "returnType": "Result", + "returnType": "Result", "jsdoctags": [ { - "name": "draft", + "name": "d", "type": "Draft", "deprecated": false, "deprecationMessage": "", "tagName": { "text": "param" } + }, + { + "name": "upload", + "type": "UploadState", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } } ] }, @@ -38550,14 +38686,20 @@ ] }, { - "name": "validateAll", + "name": "validateStep", "file": "src/app/registratie/domain/registratie-wizard.machine.ts", "ctype": "miscellaneous", "subtype": "function", "deprecated": false, "deprecationMessage": "", - "description": "

Parse the whole wizard into a ValidRegistratie (called on submit).

\n", + "description": "

Validate every question currently visible in ONE step. Errors keyed per field.

\n", "args": [ + { + "name": "step", + "type": "StepId", + "deprecated": false, + "deprecationMessage": "" + }, { "name": "d", "type": "Draft", @@ -38571,8 +38713,17 @@ "deprecationMessage": "" } ], - "returnType": "Result", + "returnType": "Result", "jsdoctags": [ + { + "name": "step", + "type": "StepId", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + }, { "name": "d", "type": "Draft", @@ -38652,65 +38803,6 @@ } ] }, - { - "name": "validateStep", - "file": "src/app/registratie/domain/registratie-wizard.machine.ts", - "ctype": "miscellaneous", - "subtype": "function", - "deprecated": false, - "deprecationMessage": "", - "description": "

Validate every question currently visible in ONE step. Errors keyed per field.

\n", - "args": [ - { - "name": "step", - "type": "StepId", - "deprecated": false, - "deprecationMessage": "" - }, - { - "name": "d", - "type": "Draft", - "deprecated": false, - "deprecationMessage": "" - }, - { - "name": "upload", - "type": "UploadState", - "deprecated": false, - "deprecationMessage": "" - } - ], - "returnType": "Result", - "jsdoctags": [ - { - "name": "step", - "type": "StepId", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - }, - { - "name": "d", - "type": "Draft", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - }, - { - "name": "upload", - "type": "UploadState", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - } - ] - }, { "name": "whenTag", "file": "src/app/shared/kernel/fp.ts", @@ -39197,22 +39289,22 @@ "name": "Errors", "ctype": "miscellaneous", "subtype": "typealias", - "rawtype": "Partial>", - "file": "src/app/herregistratie/domain/intake.machine.ts", + "rawtype": "Partial>", + "file": "src/app/registratie/domain/change-request.machine.ts", "deprecated": false, "deprecationMessage": "", - "description": "

Per-field error map: one message per question, since a step holds several.

\n", + "description": "", "kind": 184 }, { "name": "Errors", "ctype": "miscellaneous", "subtype": "typealias", - "rawtype": "Partial>", - "file": "src/app/registratie/domain/change-request.machine.ts", + "rawtype": "Partial>", + "file": "src/app/herregistratie/domain/intake.machine.ts", "deprecated": false, "deprecationMessage": "", - "description": "", + "description": "

Per-field error map: one message per question, since a step holds several.

\n", "kind": 184 }, { @@ -39571,22 +39663,22 @@ "name": "StepId", "ctype": "miscellaneous", "subtype": "typealias", - "rawtype": "\"buitenland\" | \"werk\" | \"review\"", - "file": "src/app/herregistratie/domain/intake.machine.ts", + "rawtype": "\"adres\" | \"beroep\" | \"controle\"", + "file": "src/app/registratie/domain/registratie-wizard.machine.ts", "deprecated": false, "deprecationMessage": "", - "description": "

The three fixed steps. Each step groups one or more questions.

\n", + "description": "

A FIXED 3-step registration wizard. The steps never change in number (always\nSTEPS): (1) adres + correspondentievoorkeur, (2) beroep o.b.v. diploma,\n(3) controle & indienen. Follow-up questions appear inline within a step\n(e.g. choosing 'email' reveals the e-mail field). "Is this field required\nright now" is a pure function (validateStep), so it is trivial to test and\nimpossible to get out of sync with the data. Invariants live here, not in the\nUI: the wizard reaches Indienen only when a complete ValidRegistratie parses.

\n", "kind": 193 }, { "name": "StepId", "ctype": "miscellaneous", "subtype": "typealias", - "rawtype": "\"adres\" | \"beroep\" | \"controle\"", - "file": "src/app/registratie/domain/registratie-wizard.machine.ts", + "rawtype": "\"buitenland\" | \"werk\" | \"review\"", + "file": "src/app/herregistratie/domain/intake.machine.ts", "deprecated": false, "deprecationMessage": "", - "description": "

A FIXED 3-step registration wizard. The steps never change in number (always\nSTEPS): (1) adres + correspondentievoorkeur, (2) beroep o.b.v. diploma,\n(3) controle & indienen. Follow-up questions appear inline within a step\n(e.g. choosing 'email' reveals the e-mail field). "Is this field required\nright now" is a pure function (validateStep), so it is trivial to test and\nimpossible to get out of sync with the data. Invariants live here, not in the\nUI: the wizard reaches Indienen only when a complete ValidRegistratie parses.

\n", + "description": "

The three fixed steps. Each step groups one or more questions.

\n", "kind": 193 }, { @@ -39781,6 +39873,30 @@ "defaultValue": "{\n type: $localize`:@@upload.reject.type:Dit bestandstype is niet toegestaan voor deze categorie.`,\n size: $localize`:@@upload.reject.size:Dit bestand is te groot.`,\n multiple: $localize`:@@upload.reject.multiple:U kunt voor deze categorie maar één bestand uploaden.`,\n}" } ], + "src/app/shared/layout/site-header/site-header.component.ts": [ + { + "name": "ADMIN_NAV_ITEMS", + "ctype": "miscellaneous", + "subtype": "variable", + "file": "src/app/shared/layout/site-header/site-header.component.ts", + "deprecated": false, + "deprecationMessage": "", + "type": "(unknown)[]", + "defaultValue": "[\n {\n label: $localize`:@@header.nav.huisstijl:Huisstijl`,\n to: '/brief/huisstijl',\n cap: 'orgtemplate:edit',\n },\n {\n label: $localize`:@@header.nav.stamdata:Stamdata`,\n to: '/beheer/stamdata',\n cap: 'stamdata:edit',\n },\n]", + "rawdescription": "Admin-only nav, shown only when `/me` grants the matching capability — the pages\nare otherwise reachable by URL alone.", + "description": "

Admin-only nav, shown only when /me grants the matching capability — the pages\nare otherwise reachable by URL alone.

\n" + }, + { + "name": "NAV_ITEMS", + "ctype": "miscellaneous", + "subtype": "variable", + "file": "src/app/shared/layout/site-header/site-header.component.ts", + "deprecated": false, + "deprecationMessage": "", + "type": "HeaderNavItem[]", + "defaultValue": "[\n { label: $localize`:@@header.nav.overzicht:Overzicht`, to: '/dashboard' },\n { label: $localize`:@@header.nav.gegevens:Mijn gegevens`, to: '/registratie' },\n { label: $localize`:@@header.nav.herregistratie:Herregistratie`, to: '/herregistratie' },\n { label: $localize`:@@header.nav.inschrijven:Inschrijven`, to: '/registreren' },\n]" + } + ], "src/app/app.config.ts": [ { "name": "appConfig", @@ -40211,6 +40327,18 @@ "defaultValue": "{ tag: 'loading' }" } ], + "src/app/registratie/domain/change-request.machine.ts": [ + { + "name": "initial", + "ctype": "miscellaneous", + "subtype": "variable", + "file": "src/app/registratie/domain/change-request.machine.ts", + "deprecated": false, + "deprecationMessage": "", + "type": "ChangeRequestState", + "defaultValue": "{\n tag: 'Editing',\n draft: { straat: '', postcode: '', woonplaats: '' },\n errors: {},\n}" + } + ], "src/app/herregistratie/domain/herregistratie.machine.ts": [ { "name": "initial", @@ -40259,16 +40387,28 @@ "description": "

The fixed step list. Number of steps never changes; questions reveal inline.

\n" } ], - "src/app/registratie/domain/change-request.machine.ts": [ + "src/app/shared/infrastructure/role.ts": [ { - "name": "initial", + "name": "isRole", "ctype": "miscellaneous", "subtype": "variable", - "file": "src/app/registratie/domain/change-request.machine.ts", + "file": "src/app/shared/infrastructure/role.ts", "deprecated": false, "deprecationMessage": "", - "type": "ChangeRequestState", - "defaultValue": "{\n tag: 'Editing',\n draft: { straat: '', postcode: '', woonplaats: '' },\n errors: {},\n}" + "type": "unknown", + "defaultValue": "(v: string | null): v is Role =>\n v === 'drafter' || v === 'approver' || v === 'admin'" + }, + { + "name": "STORAGE_KEY", + "ctype": "miscellaneous", + "subtype": "variable", + "file": "src/app/shared/infrastructure/role.ts", + "deprecated": false, + "deprecationMessage": "", + "type": "string", + "defaultValue": "'dev-role'", + "rawdescription": "Dev-only role stand-in (the reading MECHANISM; the `Role` type is domain). This\nPOC has one faked self-service user and no real identities, so the two-person\nletter workflow (drafter vs approver) plus admin is driven by a `?role=` query\nparam. The backend receives it as an `X-Role` header (see role.interceptor),\nresolves it into a `Principal` server-side, and is the sole authority on what that\nprincipal may do (PRD-0002 phase P1, `Authz.Can`) — the FE only renders the\nresulting decision flags, it no longer derives permission from this value itself.\n\n**Sticky within the tab (sessionStorage):** the interceptor reads this per request,\nbut navigation drops the query param (login redirects to /dashboard, RouterLinks\ndon't carry it), which would silently revert an admin to drafter mid-session and\n403 the admin endpoints. So a `?role=` seen in the URL is remembered for the tab;\nlater requests use the remembered value. Set `?role=drafter` (or a fresh tab) to\nreset. Dev-only — the interceptor itself is only wired under `isDevMode()`.", + "description": "

Dev-only role stand-in (the reading MECHANISM; the Role type is domain). This\nPOC has one faked self-service user and no real identities, so the two-person\nletter workflow (drafter vs approver) plus admin is driven by a ?role= query\nparam. The backend receives it as an X-Role header (see role.interceptor),\nresolves it into a Principal server-side, and is the sole authority on what that\nprincipal may do (PRD-0002 phase P1, Authz.Can) — the FE only renders the\nresulting decision flags, it no longer derives permission from this value itself.

\n

Sticky within the tab (sessionStorage): the interceptor reads this per request,\nbut navigation drops the query param (login redirects to /dashboard, RouterLinks\ndon't carry it), which would silently revert an admin to drafter mid-session and\n403 the admin endpoints. So a ?role= seen in the URL is remembered for the tab;\nlater requests use the remembered value. Set ?role=drafter (or a fresh tab) to\nreset. Dev-only — the interceptor itself is only wired under isDevMode().

\n" } ], "src/app/shared/ui/radio-group/radio-group.component.ts": [ @@ -40381,18 +40521,6 @@ "description": "

CIBG procesnavigatie primary-button copy for a non-final step: "Naar stap 2 - Werk".\nShared so every wizard's primaryLabel reads the same way.

\n" } ], - "src/app/shared/layout/site-header/site-header.component.ts": [ - { - "name": "NAV_ITEMS", - "ctype": "miscellaneous", - "subtype": "variable", - "file": "src/app/shared/layout/site-header/site-header.component.ts", - "deprecated": false, - "deprecationMessage": "", - "type": "HeaderNavItem[]", - "defaultValue": "[\n { label: $localize`:@@header.nav.overzicht:Overzicht`, to: '/dashboard' },\n { label: $localize`:@@header.nav.gegevens:Mijn gegevens`, to: '/registratie' },\n { label: $localize`:@@header.nav.herregistratie:Herregistratie`, to: '/herregistratie' },\n { label: $localize`:@@header.nav.inschrijven:Inschrijven`, to: '/registreren' },\n]" - } - ], "src/app/shared/ui/checkbox/checkbox.component.ts": [ { "name": "nextCheckboxId", @@ -40535,9 +40663,9 @@ "deprecated": false, "deprecationMessage": "", "type": "[]", - "defaultValue": "['/api/v1/brief', '/api/v1/admin/org-template', '/api/v1/me']", - "rawdescription": "Dev-only: stamps role-aware requests with the current `?role=` as an `X-Role`\nheader so the backend can enforce the drafter/approver/admin rules. Only the\nbrief, org-template and /me endpoints carry it (WP-23 widened the set — /me must\nsee the role or `AccessStore` could never learn a capability); everything else\nis untouched.", - "description": "

Dev-only: stamps role-aware requests with the current ?role= as an X-Role\nheader so the backend can enforce the drafter/approver/admin rules. Only the\nbrief, org-template and /me endpoints carry it (WP-23 widened the set — /me must\nsee the role or AccessStore could never learn a capability); everything else\nis untouched.

\n" + "defaultValue": "[\n '/api/v1/brief',\n '/api/v1/admin/org-template',\n '/api/v1/stamdata',\n '/api/v1/me',\n]", + "rawdescription": "Dev-only: stamps role-aware requests with the current `?role=` as an `X-Role`\nheader so the backend can enforce the drafter/approver/admin rules. Only the\nbrief, org-template, stamdata and /me endpoints carry it (WP-23 widened the set —\n/me must see the role or `AccessStore` could never learn a capability; WP-29 added\n/stamdata, whose admin-only reads 403 without it); everything else is untouched.\nA new admin-gated endpoint MUST be added here or its page silently 403s.", + "description": "

Dev-only: stamps role-aware requests with the current ?role= as an X-Role\nheader so the backend can enforce the drafter/approver/admin rules. Only the\nbrief, org-template, stamdata and /me endpoints carry it (WP-23 widened the set —\n/me must see the role or AccessStore could never learn a capability; WP-29 added\n/stamdata, whose admin-only reads 403 without it); everything else is untouched.\nA new admin-gated endpoint MUST be added here or its page silently 403s.

\n" }, { "name": "roleInterceptor", @@ -42764,6 +42892,826 @@ ] } ], + "src/app/registratie/domain/registratie-wizard.machine.ts": [ + { + "name": "back", + "file": "src/app/registratie/domain/registratie-wizard.machine.ts", + "ctype": "miscellaneous", + "subtype": "function", + "deprecated": false, + "deprecationMessage": "", + "description": "", + "args": [ + { + "name": "s", + "type": "RegistratieState", + "deprecated": false, + "deprecationMessage": "" + } + ], + "returnType": "RegistratieState", + "jsdoctags": [ + { + "name": "s", + "type": "RegistratieState", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + } + ] + }, + { + "name": "currentStep", + "file": "src/app/registratie/domain/registratie-wizard.machine.ts", + "ctype": "miscellaneous", + "subtype": "function", + "deprecated": false, + "deprecationMessage": "", + "description": "

Which step the cursor currently points at (clamped to the fixed list).

\n", + "args": [ + { + "name": "s", + "type": "Extract", + "deprecated": false, + "deprecationMessage": "" + } + ], + "returnType": "StepId", + "jsdoctags": [ + { + "name": "s", + "type": "Extract", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + } + ] + }, + { + "name": "declareerBeroep", + "file": "src/app/registratie/domain/registratie-wizard.machine.ts", + "ctype": "miscellaneous", + "subtype": "function", + "deprecated": false, + "deprecationMessage": "", + "description": "

Declare the beroep for a manually-entered diploma (chosen from a fixed list).

\n", + "args": [ + { + "name": "s", + "type": "RegistratieState", + "deprecated": false, + "deprecationMessage": "" + }, + { + "name": "beroep", + "type": "string", + "deprecated": false, + "deprecationMessage": "" + } + ], + "returnType": "RegistratieState", + "jsdoctags": [ + { + "name": "s", + "type": "RegistratieState", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + }, + { + "name": "beroep", + "type": "string", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + } + ] + }, + { + "name": "gaNaarStap", + "file": "src/app/registratie/domain/registratie-wizard.machine.ts", + "ctype": "miscellaneous", + "subtype": "function", + "deprecated": false, + "deprecationMessage": "", + "description": "

Jump back to an earlier step to correct data (controle → step N). Forward\njumps are not allowed (would skip validation). Preserves the draft.

\n", + "args": [ + { + "name": "s", + "type": "RegistratieState", + "deprecated": false, + "deprecationMessage": "" + }, + { + "name": "cursor", + "type": "number", + "deprecated": false, + "deprecationMessage": "" + } + ], + "returnType": "RegistratieState", + "jsdoctags": [ + { + "name": "s", + "type": "RegistratieState", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + }, + { + "name": "cursor", + "type": "number", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + } + ] + }, + { + "name": "hasProgress", + "file": "src/app/registratie/domain/registratie-wizard.machine.ts", + "ctype": "miscellaneous", + "subtype": "function", + "deprecated": false, + "deprecationMessage": "", + "description": "

Has the user meaningfully started, so it's worth persisting as a Concept? Excludes\nthe automatic BRP address prefill on step 0 — a bare page visit creates nothing.\nponytail: an address typed at step 0 without any of these signals is not yet\npersisted (created once they advance/choose); accepted regression vs. sessionStorage.

\n", + "args": [ + { + "name": "s", + "type": "Extract", + "deprecated": false, + "deprecationMessage": "" + } + ], + "returnType": "boolean", + "jsdoctags": [ + { + "name": "s", + "type": "Extract", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + } + ] + }, + { + "name": "kiesDiploma", + "file": "src/app/registratie/domain/registratie-wizard.machine.ts", + "ctype": "miscellaneous", + "subtype": "function", + "deprecated": false, + "deprecationMessage": "", + "description": "

Pick a DUO diploma; the beroep is derived from it and the applicable policy\nquestions (vraagIds) come with it (both server-computed, passed in).

\n", + "args": [ + { + "name": "s", + "type": "RegistratieState", + "deprecated": false, + "deprecationMessage": "" + }, + { + "name": "diplomaId", + "type": "string", + "deprecated": false, + "deprecationMessage": "" + }, + { + "name": "beroep", + "type": "string", + "deprecated": false, + "deprecationMessage": "" + }, + { + "name": "vraagIds", + "deprecated": false, + "deprecationMessage": "" + } + ], + "returnType": "RegistratieState", + "jsdoctags": [ + { + "name": "s", + "type": "RegistratieState", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + }, + { + "name": "diplomaId", + "type": "string", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + }, + { + "name": "beroep", + "type": "string", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + }, + { + "name": "vraagIds", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + } + ] + }, + { + "name": "kiesHandmatig", + "file": "src/app/registratie/domain/registratie-wizard.machine.ts", + "ctype": "miscellaneous", + "subtype": "function", + "deprecated": false, + "deprecationMessage": "", + "description": "

Switch to manual diploma entry: the diploma isn't in DUO, so the MAXIMAL\npolicy-question set applies and the entry is flagged handmatig/unverified. The\nberoep is declared separately (declareerBeroep).

\n", + "args": [ + { + "name": "s", + "type": "RegistratieState", + "deprecated": false, + "deprecationMessage": "" + }, + { + "name": "vraagIds", + "deprecated": false, + "deprecationMessage": "" + } + ], + "returnType": "RegistratieState", + "jsdoctags": [ + { + "name": "s", + "type": "RegistratieState", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + }, + { + "name": "vraagIds", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + } + ] + }, + { + "name": "next", + "file": "src/app/registratie/domain/registratie-wizard.machine.ts", + "ctype": "miscellaneous", + "subtype": "function", + "deprecated": false, + "deprecationMessage": "", + "description": "", + "args": [ + { + "name": "s", + "type": "RegistratieState", + "deprecated": false, + "deprecationMessage": "" + } + ], + "returnType": "RegistratieState", + "jsdoctags": [ + { + "name": "s", + "type": "RegistratieState", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + } + ] + }, + { + "name": "prefillAdres", + "file": "src/app/registratie/domain/registratie-wizard.machine.ts", + "ctype": "miscellaneous", + "subtype": "function", + "deprecated": false, + "deprecationMessage": "", + "description": "

Prefill the address from a BRP lookup and flag its origin (PRD §7).

\n", + "args": [ + { + "name": "s", + "type": "RegistratieState", + "deprecated": false, + "deprecationMessage": "" + }, + { + "name": "straat", + "type": "string", + "deprecated": false, + "deprecationMessage": "" + }, + { + "name": "postcode", + "type": "string", + "deprecated": false, + "deprecationMessage": "" + }, + { + "name": "woonplaats", + "type": "string", + "deprecated": false, + "deprecationMessage": "" + } + ], + "returnType": "RegistratieState", + "jsdoctags": [ + { + "name": "s", + "type": "RegistratieState", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + }, + { + "name": "straat", + "type": "string", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + }, + { + "name": "postcode", + "type": "string", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + }, + { + "name": "woonplaats", + "type": "string", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + } + ] + }, + { + "name": "reduce", + "file": "src/app/registratie/domain/registratie-wizard.machine.ts", + "ctype": "miscellaneous", + "subtype": "function", + "deprecated": false, + "deprecationMessage": "", + "description": "", + "args": [ + { + "name": "s", + "type": "RegistratieState", + "deprecated": false, + "deprecationMessage": "" + }, + { + "name": "m", + "type": "RegistratieMsg", + "deprecated": false, + "deprecationMessage": "" + } + ], + "returnType": "RegistratieState", + "jsdoctags": [ + { + "name": "s", + "type": "RegistratieState", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + }, + { + "name": "m", + "type": "RegistratieMsg", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + } + ] + }, + { + "name": "resolve", + "file": "src/app/registratie/domain/registratie-wizard.machine.ts", + "ctype": "miscellaneous", + "subtype": "function", + "deprecated": false, + "deprecationMessage": "", + "description": "", + "args": [ + { + "name": "s", + "type": "RegistratieState", + "deprecated": false, + "deprecationMessage": "" + }, + { + "name": "r", + "type": "Result", + "deprecated": false, + "deprecationMessage": "" + } + ], + "returnType": "RegistratieState", + "jsdoctags": [ + { + "name": "s", + "type": "RegistratieState", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + }, + { + "name": "r", + "type": "Result", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + } + ] + }, + { + "name": "setAntwoord", + "file": "src/app/registratie/domain/registratie-wizard.machine.ts", + "ctype": "miscellaneous", + "subtype": "function", + "deprecated": false, + "deprecationMessage": "", + "description": "", + "args": [ + { + "name": "s", + "type": "RegistratieState", + "deprecated": false, + "deprecationMessage": "" + }, + { + "name": "vraagId", + "type": "string", + "deprecated": false, + "deprecationMessage": "" + }, + { + "name": "value", + "type": "string", + "deprecated": false, + "deprecationMessage": "" + } + ], + "returnType": "RegistratieState", + "jsdoctags": [ + { + "name": "s", + "type": "RegistratieState", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + }, + { + "name": "vraagId", + "type": "string", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + }, + { + "name": "value", + "type": "string", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + } + ] + }, + { + "name": "setCorrespondentie", + "file": "src/app/registratie/domain/registratie-wizard.machine.ts", + "ctype": "miscellaneous", + "subtype": "function", + "deprecated": false, + "deprecationMessage": "", + "description": "", + "args": [ + { + "name": "s", + "type": "RegistratieState", + "deprecated": false, + "deprecationMessage": "" + }, + { + "name": "value", + "type": "Correspondentie", + "deprecated": false, + "deprecationMessage": "" + } + ], + "returnType": "RegistratieState", + "jsdoctags": [ + { + "name": "s", + "type": "RegistratieState", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + }, + { + "name": "value", + "type": "Correspondentie", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + } + ] + }, + { + "name": "setField", + "file": "src/app/registratie/domain/registratie-wizard.machine.ts", + "ctype": "miscellaneous", + "subtype": "function", + "deprecated": false, + "deprecationMessage": "", + "description": "", + "args": [ + { + "name": "s", + "type": "RegistratieState", + "deprecated": false, + "deprecationMessage": "" + }, + { + "name": "key", + "type": "DraftField", + "deprecated": false, + "deprecationMessage": "" + }, + { + "name": "value", + "type": "string", + "deprecated": false, + "deprecationMessage": "" + } + ], + "returnType": "RegistratieState", + "jsdoctags": [ + { + "name": "s", + "type": "RegistratieState", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + }, + { + "name": "key", + "type": "DraftField", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + }, + { + "name": "value", + "type": "string", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + } + ] + }, + { + "name": "submit", + "file": "src/app/registratie/domain/registratie-wizard.machine.ts", + "ctype": "miscellaneous", + "subtype": "function", + "deprecated": false, + "deprecationMessage": "", + "description": "", + "args": [ + { + "name": "s", + "type": "RegistratieState", + "deprecated": false, + "deprecationMessage": "" + } + ], + "returnType": "RegistratieState", + "jsdoctags": [ + { + "name": "s", + "type": "RegistratieState", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + } + ] + }, + { + "name": "upload", + "file": "src/app/registratie/domain/registratie-wizard.machine.ts", + "ctype": "miscellaneous", + "subtype": "function", + "deprecated": false, + "deprecationMessage": "", + "description": "

Route an upload sub-message through the pure upload reducer (Invullen only).

\n", + "args": [ + { + "name": "s", + "type": "RegistratieState", + "deprecated": false, + "deprecationMessage": "" + }, + { + "name": "msg", + "type": "UploadMsg", + "deprecated": false, + "deprecationMessage": "" + } + ], + "returnType": "RegistratieState", + "jsdoctags": [ + { + "name": "s", + "type": "RegistratieState", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + }, + { + "name": "msg", + "type": "UploadMsg", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + } + ] + }, + { + "name": "validateAll", + "file": "src/app/registratie/domain/registratie-wizard.machine.ts", + "ctype": "miscellaneous", + "subtype": "function", + "deprecated": false, + "deprecationMessage": "", + "description": "

Parse the whole wizard into a ValidRegistratie (called on submit).

\n", + "args": [ + { + "name": "d", + "type": "Draft", + "deprecated": false, + "deprecationMessage": "" + }, + { + "name": "upload", + "type": "UploadState", + "deprecated": false, + "deprecationMessage": "" + } + ], + "returnType": "Result", + "jsdoctags": [ + { + "name": "d", + "type": "Draft", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + }, + { + "name": "upload", + "type": "UploadState", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + } + ] + }, + { + "name": "validateStep", + "file": "src/app/registratie/domain/registratie-wizard.machine.ts", + "ctype": "miscellaneous", + "subtype": "function", + "deprecated": false, + "deprecationMessage": "", + "description": "

Validate every question currently visible in ONE step. Errors keyed per field.

\n", + "args": [ + { + "name": "step", + "type": "StepId", + "deprecated": false, + "deprecationMessage": "" + }, + { + "name": "d", + "type": "Draft", + "deprecated": false, + "deprecationMessage": "" + }, + { + "name": "upload", + "type": "UploadState", + "deprecated": false, + "deprecationMessage": "" + } + ], + "returnType": "Result", + "jsdoctags": [ + { + "name": "step", + "type": "StepId", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + }, + { + "name": "d", + "type": "Draft", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + }, + { + "name": "upload", + "type": "UploadState", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + } + ] + } + ], "src/app/herregistratie/domain/herregistratie.machine.ts": [ { "name": "back", @@ -43686,826 +44634,6 @@ ] } ], - "src/app/registratie/domain/registratie-wizard.machine.ts": [ - { - "name": "back", - "file": "src/app/registratie/domain/registratie-wizard.machine.ts", - "ctype": "miscellaneous", - "subtype": "function", - "deprecated": false, - "deprecationMessage": "", - "description": "", - "args": [ - { - "name": "s", - "type": "RegistratieState", - "deprecated": false, - "deprecationMessage": "" - } - ], - "returnType": "RegistratieState", - "jsdoctags": [ - { - "name": "s", - "type": "RegistratieState", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - } - ] - }, - { - "name": "currentStep", - "file": "src/app/registratie/domain/registratie-wizard.machine.ts", - "ctype": "miscellaneous", - "subtype": "function", - "deprecated": false, - "deprecationMessage": "", - "description": "

Which step the cursor currently points at (clamped to the fixed list).

\n", - "args": [ - { - "name": "s", - "type": "Extract", - "deprecated": false, - "deprecationMessage": "" - } - ], - "returnType": "StepId", - "jsdoctags": [ - { - "name": "s", - "type": "Extract", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - } - ] - }, - { - "name": "declareerBeroep", - "file": "src/app/registratie/domain/registratie-wizard.machine.ts", - "ctype": "miscellaneous", - "subtype": "function", - "deprecated": false, - "deprecationMessage": "", - "description": "

Declare the beroep for a manually-entered diploma (chosen from a fixed list).

\n", - "args": [ - { - "name": "s", - "type": "RegistratieState", - "deprecated": false, - "deprecationMessage": "" - }, - { - "name": "beroep", - "type": "string", - "deprecated": false, - "deprecationMessage": "" - } - ], - "returnType": "RegistratieState", - "jsdoctags": [ - { - "name": "s", - "type": "RegistratieState", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - }, - { - "name": "beroep", - "type": "string", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - } - ] - }, - { - "name": "gaNaarStap", - "file": "src/app/registratie/domain/registratie-wizard.machine.ts", - "ctype": "miscellaneous", - "subtype": "function", - "deprecated": false, - "deprecationMessage": "", - "description": "

Jump back to an earlier step to correct data (controle → step N). Forward\njumps are not allowed (would skip validation). Preserves the draft.

\n", - "args": [ - { - "name": "s", - "type": "RegistratieState", - "deprecated": false, - "deprecationMessage": "" - }, - { - "name": "cursor", - "type": "number", - "deprecated": false, - "deprecationMessage": "" - } - ], - "returnType": "RegistratieState", - "jsdoctags": [ - { - "name": "s", - "type": "RegistratieState", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - }, - { - "name": "cursor", - "type": "number", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - } - ] - }, - { - "name": "hasProgress", - "file": "src/app/registratie/domain/registratie-wizard.machine.ts", - "ctype": "miscellaneous", - "subtype": "function", - "deprecated": false, - "deprecationMessage": "", - "description": "

Has the user meaningfully started, so it's worth persisting as a Concept? Excludes\nthe automatic BRP address prefill on step 0 — a bare page visit creates nothing.\nponytail: an address typed at step 0 without any of these signals is not yet\npersisted (created once they advance/choose); accepted regression vs. sessionStorage.

\n", - "args": [ - { - "name": "s", - "type": "Extract", - "deprecated": false, - "deprecationMessage": "" - } - ], - "returnType": "boolean", - "jsdoctags": [ - { - "name": "s", - "type": "Extract", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - } - ] - }, - { - "name": "kiesDiploma", - "file": "src/app/registratie/domain/registratie-wizard.machine.ts", - "ctype": "miscellaneous", - "subtype": "function", - "deprecated": false, - "deprecationMessage": "", - "description": "

Pick a DUO diploma; the beroep is derived from it and the applicable policy\nquestions (vraagIds) come with it (both server-computed, passed in).

\n", - "args": [ - { - "name": "s", - "type": "RegistratieState", - "deprecated": false, - "deprecationMessage": "" - }, - { - "name": "diplomaId", - "type": "string", - "deprecated": false, - "deprecationMessage": "" - }, - { - "name": "beroep", - "type": "string", - "deprecated": false, - "deprecationMessage": "" - }, - { - "name": "vraagIds", - "deprecated": false, - "deprecationMessage": "" - } - ], - "returnType": "RegistratieState", - "jsdoctags": [ - { - "name": "s", - "type": "RegistratieState", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - }, - { - "name": "diplomaId", - "type": "string", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - }, - { - "name": "beroep", - "type": "string", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - }, - { - "name": "vraagIds", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - } - ] - }, - { - "name": "kiesHandmatig", - "file": "src/app/registratie/domain/registratie-wizard.machine.ts", - "ctype": "miscellaneous", - "subtype": "function", - "deprecated": false, - "deprecationMessage": "", - "description": "

Switch to manual diploma entry: the diploma isn't in DUO, so the MAXIMAL\npolicy-question set applies and the entry is flagged handmatig/unverified. The\nberoep is declared separately (declareerBeroep).

\n", - "args": [ - { - "name": "s", - "type": "RegistratieState", - "deprecated": false, - "deprecationMessage": "" - }, - { - "name": "vraagIds", - "deprecated": false, - "deprecationMessage": "" - } - ], - "returnType": "RegistratieState", - "jsdoctags": [ - { - "name": "s", - "type": "RegistratieState", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - }, - { - "name": "vraagIds", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - } - ] - }, - { - "name": "next", - "file": "src/app/registratie/domain/registratie-wizard.machine.ts", - "ctype": "miscellaneous", - "subtype": "function", - "deprecated": false, - "deprecationMessage": "", - "description": "", - "args": [ - { - "name": "s", - "type": "RegistratieState", - "deprecated": false, - "deprecationMessage": "" - } - ], - "returnType": "RegistratieState", - "jsdoctags": [ - { - "name": "s", - "type": "RegistratieState", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - } - ] - }, - { - "name": "prefillAdres", - "file": "src/app/registratie/domain/registratie-wizard.machine.ts", - "ctype": "miscellaneous", - "subtype": "function", - "deprecated": false, - "deprecationMessage": "", - "description": "

Prefill the address from a BRP lookup and flag its origin (PRD §7).

\n", - "args": [ - { - "name": "s", - "type": "RegistratieState", - "deprecated": false, - "deprecationMessage": "" - }, - { - "name": "straat", - "type": "string", - "deprecated": false, - "deprecationMessage": "" - }, - { - "name": "postcode", - "type": "string", - "deprecated": false, - "deprecationMessage": "" - }, - { - "name": "woonplaats", - "type": "string", - "deprecated": false, - "deprecationMessage": "" - } - ], - "returnType": "RegistratieState", - "jsdoctags": [ - { - "name": "s", - "type": "RegistratieState", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - }, - { - "name": "straat", - "type": "string", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - }, - { - "name": "postcode", - "type": "string", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - }, - { - "name": "woonplaats", - "type": "string", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - } - ] - }, - { - "name": "reduce", - "file": "src/app/registratie/domain/registratie-wizard.machine.ts", - "ctype": "miscellaneous", - "subtype": "function", - "deprecated": false, - "deprecationMessage": "", - "description": "", - "args": [ - { - "name": "s", - "type": "RegistratieState", - "deprecated": false, - "deprecationMessage": "" - }, - { - "name": "m", - "type": "RegistratieMsg", - "deprecated": false, - "deprecationMessage": "" - } - ], - "returnType": "RegistratieState", - "jsdoctags": [ - { - "name": "s", - "type": "RegistratieState", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - }, - { - "name": "m", - "type": "RegistratieMsg", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - } - ] - }, - { - "name": "resolve", - "file": "src/app/registratie/domain/registratie-wizard.machine.ts", - "ctype": "miscellaneous", - "subtype": "function", - "deprecated": false, - "deprecationMessage": "", - "description": "", - "args": [ - { - "name": "s", - "type": "RegistratieState", - "deprecated": false, - "deprecationMessage": "" - }, - { - "name": "r", - "type": "Result", - "deprecated": false, - "deprecationMessage": "" - } - ], - "returnType": "RegistratieState", - "jsdoctags": [ - { - "name": "s", - "type": "RegistratieState", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - }, - { - "name": "r", - "type": "Result", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - } - ] - }, - { - "name": "setAntwoord", - "file": "src/app/registratie/domain/registratie-wizard.machine.ts", - "ctype": "miscellaneous", - "subtype": "function", - "deprecated": false, - "deprecationMessage": "", - "description": "", - "args": [ - { - "name": "s", - "type": "RegistratieState", - "deprecated": false, - "deprecationMessage": "" - }, - { - "name": "vraagId", - "type": "string", - "deprecated": false, - "deprecationMessage": "" - }, - { - "name": "value", - "type": "string", - "deprecated": false, - "deprecationMessage": "" - } - ], - "returnType": "RegistratieState", - "jsdoctags": [ - { - "name": "s", - "type": "RegistratieState", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - }, - { - "name": "vraagId", - "type": "string", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - }, - { - "name": "value", - "type": "string", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - } - ] - }, - { - "name": "setCorrespondentie", - "file": "src/app/registratie/domain/registratie-wizard.machine.ts", - "ctype": "miscellaneous", - "subtype": "function", - "deprecated": false, - "deprecationMessage": "", - "description": "", - "args": [ - { - "name": "s", - "type": "RegistratieState", - "deprecated": false, - "deprecationMessage": "" - }, - { - "name": "value", - "type": "Correspondentie", - "deprecated": false, - "deprecationMessage": "" - } - ], - "returnType": "RegistratieState", - "jsdoctags": [ - { - "name": "s", - "type": "RegistratieState", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - }, - { - "name": "value", - "type": "Correspondentie", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - } - ] - }, - { - "name": "setField", - "file": "src/app/registratie/domain/registratie-wizard.machine.ts", - "ctype": "miscellaneous", - "subtype": "function", - "deprecated": false, - "deprecationMessage": "", - "description": "", - "args": [ - { - "name": "s", - "type": "RegistratieState", - "deprecated": false, - "deprecationMessage": "" - }, - { - "name": "key", - "type": "DraftField", - "deprecated": false, - "deprecationMessage": "" - }, - { - "name": "value", - "type": "string", - "deprecated": false, - "deprecationMessage": "" - } - ], - "returnType": "RegistratieState", - "jsdoctags": [ - { - "name": "s", - "type": "RegistratieState", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - }, - { - "name": "key", - "type": "DraftField", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - }, - { - "name": "value", - "type": "string", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - } - ] - }, - { - "name": "submit", - "file": "src/app/registratie/domain/registratie-wizard.machine.ts", - "ctype": "miscellaneous", - "subtype": "function", - "deprecated": false, - "deprecationMessage": "", - "description": "", - "args": [ - { - "name": "s", - "type": "RegistratieState", - "deprecated": false, - "deprecationMessage": "" - } - ], - "returnType": "RegistratieState", - "jsdoctags": [ - { - "name": "s", - "type": "RegistratieState", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - } - ] - }, - { - "name": "upload", - "file": "src/app/registratie/domain/registratie-wizard.machine.ts", - "ctype": "miscellaneous", - "subtype": "function", - "deprecated": false, - "deprecationMessage": "", - "description": "

Route an upload sub-message through the pure upload reducer (Invullen only).

\n", - "args": [ - { - "name": "s", - "type": "RegistratieState", - "deprecated": false, - "deprecationMessage": "" - }, - { - "name": "msg", - "type": "UploadMsg", - "deprecated": false, - "deprecationMessage": "" - } - ], - "returnType": "RegistratieState", - "jsdoctags": [ - { - "name": "s", - "type": "RegistratieState", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - }, - { - "name": "msg", - "type": "UploadMsg", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - } - ] - }, - { - "name": "validateAll", - "file": "src/app/registratie/domain/registratie-wizard.machine.ts", - "ctype": "miscellaneous", - "subtype": "function", - "deprecated": false, - "deprecationMessage": "", - "description": "

Parse the whole wizard into a ValidRegistratie (called on submit).

\n", - "args": [ - { - "name": "d", - "type": "Draft", - "deprecated": false, - "deprecationMessage": "" - }, - { - "name": "upload", - "type": "UploadState", - "deprecated": false, - "deprecationMessage": "" - } - ], - "returnType": "Result", - "jsdoctags": [ - { - "name": "d", - "type": "Draft", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - }, - { - "name": "upload", - "type": "UploadState", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - } - ] - }, - { - "name": "validateStep", - "file": "src/app/registratie/domain/registratie-wizard.machine.ts", - "ctype": "miscellaneous", - "subtype": "function", - "deprecated": false, - "deprecationMessage": "", - "description": "

Validate every question currently visible in ONE step. Errors keyed per field.

\n", - "args": [ - { - "name": "step", - "type": "StepId", - "deprecated": false, - "deprecationMessage": "" - }, - { - "name": "d", - "type": "Draft", - "deprecated": false, - "deprecationMessage": "" - }, - { - "name": "upload", - "type": "UploadState", - "deprecated": false, - "deprecationMessage": "" - } - ], - "returnType": "Result", - "jsdoctags": [ - { - "name": "step", - "type": "StepId", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - }, - { - "name": "d", - "type": "Draft", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - }, - { - "name": "upload", - "type": "UploadState", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - } - ] - } - ], "src/app/registratie/domain/block-actions.ts": [ { "name": "blockActions", @@ -45001,7 +45129,7 @@ "subtype": "function", "deprecated": false, "deprecationMessage": "", - "description": "

Route guard factory (PRD-0002 §6): authenticated AND holding capability, else\nredirect. No route in this app currently needs a capability gate — brief's\ncanApprove/canReject/canSend are per-action, not per-page (both actors land on\nthe same /brief page and see different actions) — so this exists as the\navailable building block for the day a route-level gate is needed, e.g. a future\napprover-only page.

\n", + "description": "

Route guard factory (PRD-0002 §6): authenticated AND holding capability, else\nredirect. Used by the admin pages (/brief/huisstijl, /beheer/stamdata).

\n

Async on purpose: can() is deny-by-default, so it must not be read while /me\nis still loading — it would deny an entitled admin and bounce them. We await\nAccessStore.whenReady() (caps resolved) before deciding. An unauthenticated user\ngoes to /login; an authenticated-but-unentitled user goes to /dashboard (they're\nlogged in, just not allowed here — no re-login loop). The backend re-enforces\nregardless (403); this guard is the UX pre-gate.

\n", "args": [ { "name": "capability", @@ -45606,7 +45734,7 @@ "subtype": "function", "deprecated": false, "deprecationMessage": "", - "description": "

Dev-only role stand-in (the reading MECHANISM; the Role type is domain). This\nPOC has one faked self-service user and no real identities, so the two-person\nletter workflow (drafter vs approver) is driven by a ?role= query param —\nexactly the pattern of the ?scenario= toggle. The backend receives it as an\nX-Role header (see role.interceptor), resolves it into a Principal\nserver-side, and is the sole authority on what that principal may do (PRD-0002\nphase P1, Authz.Can) — the FE only renders the resulting decision flags, it no\nlonger derives permission from this value itself.

\n", + "description": "", "args": [], "returnType": "Role" } @@ -52683,8 +52811,8 @@ "type": "injectable", "linktype": "injectable", "name": "AccessStore", - "coveragePercent": 33, - "coverageCount": "2/6", + "coveragePercent": 37, + "coverageCount": "3/8", "status": "medium" }, { @@ -53550,6 +53678,26 @@ "linktype": "miscellaneous", "linksubtype": "function", "name": "currentRole", + "coveragePercent": 0, + "coverageCount": "0/1", + "status": "low" + }, + { + "filePath": "src/app/shared/infrastructure/role.ts", + "type": "variable", + "linktype": "miscellaneous", + "linksubtype": "variable", + "name": "isRole", + "coveragePercent": 0, + "coverageCount": "0/1", + "status": "low" + }, + { + "filePath": "src/app/shared/infrastructure/role.ts", + "type": "variable", + "linktype": "miscellaneous", + "linksubtype": "variable", + "name": "STORAGE_KEY", "coveragePercent": 100, "coverageCount": "1/1", "status": "very-good" @@ -53841,8 +53989,8 @@ "type": "component", "linktype": "component", "name": "SiteHeaderComponent", - "coveragePercent": 12, - "coverageCount": "1/8", + "coveragePercent": 20, + "coverageCount": "2/10", "status": "low" }, { @@ -53854,6 +54002,16 @@ "coverageCount": "0/3", "status": "low" }, + { + "filePath": "src/app/shared/layout/site-header/site-header.component.ts", + "type": "variable", + "linktype": "miscellaneous", + "linksubtype": "variable", + "name": "ADMIN_NAV_ITEMS", + "coveragePercent": 100, + "coverageCount": "1/1", + "status": "very-good" + }, { "filePath": "src/app/shared/layout/site-header/site-header.component.ts", "type": "variable", diff --git a/src/app/auth/auth.guard.spec.ts b/src/app/auth/auth.guard.spec.ts new file mode 100644 index 0000000..dfc3fb5 --- /dev/null +++ b/src/app/auth/auth.guard.spec.ts @@ -0,0 +1,62 @@ +import { TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; +import { describe, it, expect, vi } from 'vitest'; +import { AccessStore } from '@shared/application/access.store'; +import { SessionStore } from './application/session.store'; +import { authGuard, capabilityGuard } from './auth.guard'; + +type Opts = { + authed: boolean; + can?: (c: string) => boolean; + whenReady?: () => Promise; +}; + +function setup({ authed, can = () => false, whenReady = () => Promise.resolve() }: Opts) { + const createUrlTree = vi.fn((cmds: string[]) => ({ tree: cmds })); + const readySpy = vi.fn(whenReady); + TestBed.configureTestingModule({ + providers: [ + { provide: SessionStore, useValue: { isAuthenticated: () => authed } }, + { provide: AccessStore, useValue: { whenReady: readySpy, can } }, + { provide: Router, useValue: { createUrlTree } }, + ], + }); + return { createUrlTree, readySpy }; +} + +// The guards ignore their (route, state) args; cast to call with none. +const call = (fn: unknown) => TestBed.runInInjectionContext(() => (fn as () => T)()); + +describe('authGuard', () => { + it('allows an authenticated user', () => { + setup({ authed: true }); + expect(call(authGuard)).toBe(true); + }); + + it('redirects an anonymous user to /login', () => { + const { createUrlTree } = setup({ authed: false }); + expect(call(authGuard)).toEqual({ tree: ['/login'] }); + expect(createUrlTree).toHaveBeenCalledWith(['/login']); + }); +}); + +describe('capabilityGuard', () => { + const guard = () => capabilityGuard('stamdata:edit'); + + it('waits for /me, then allows an entitled admin', async () => { + const { readySpy } = setup({ authed: true, can: (c) => c === 'stamdata:edit' }); + await expect(call>(guard())).resolves.toBe(true); + expect(readySpy).toHaveBeenCalledOnce(); // it awaited caps before deciding + }); + + it('sends an authenticated-but-unentitled user to /dashboard (not a login loop)', async () => { + setup({ authed: true, can: () => false }); + await expect(call>(guard())).resolves.toEqual({ tree: ['/dashboard'] }); + }); + + it('redirects an anonymous user to /login without waiting for caps', async () => { + const { readySpy } = setup({ authed: false, can: () => true }); + await expect(call>(guard())).resolves.toEqual({ tree: ['/login'] }); + expect(readySpy).not.toHaveBeenCalled(); + }); +}); diff --git a/src/app/auth/auth.guard.ts b/src/app/auth/auth.guard.ts index 56de8e5..3eadd8f 100644 --- a/src/app/auth/auth.guard.ts +++ b/src/app/auth/auth.guard.ts @@ -13,19 +13,22 @@ export const authGuard: CanActivateFn = () => { /** * Route guard factory (PRD-0002 §6): authenticated AND holding `capability`, else - * redirect. No route in this app currently needs a capability gate — brief's - * canApprove/canReject/canSend are per-action, not per-page (both actors land on - * the same `/brief` page and see different actions) — so this exists as the - * available building block for the day a route-level gate is needed, e.g. a future - * approver-only page. + * redirect. Used by the admin pages (`/brief/huisstijl`, `/beheer/stamdata`). + * + * **Async on purpose:** `can()` is deny-by-default, so it must not be read while `/me` + * is still loading — it would deny an entitled admin and bounce them. We await + * `AccessStore.whenReady()` (caps resolved) before deciding. An unauthenticated user + * goes to `/login`; an authenticated-but-unentitled user goes to `/dashboard` (they're + * logged in, just not allowed here — no re-login loop). The backend re-enforces + * regardless (403); this guard is the UX pre-gate. */ export function capabilityGuard(capability: Capability): CanActivateFn { - return () => { + return async () => { const session = inject(SessionStore); const access = inject(AccessStore); const router = inject(Router); - return session.isAuthenticated() && access.can(capability) - ? true - : router.createUrlTree(['/login']); + if (!session.isAuthenticated()) return router.createUrlTree(['/login']); + await access.whenReady(); + return access.can(capability) ? true : router.createUrlTree(['/dashboard']); }; } diff --git a/src/app/shared/application/access.store.ts b/src/app/shared/application/access.store.ts index 6f815ea..d2b8235 100644 --- a/src/app/shared/application/access.store.ts +++ b/src/app/shared/application/access.store.ts @@ -1,4 +1,6 @@ import { Injectable, computed, inject } from '@angular/core'; +import { toObservable } from '@angular/core/rxjs-interop'; +import { filter, firstValueFrom } from 'rxjs'; import { RemoteData, fromResource } from '@shared/application/remote-data'; import { Capability } from '@shared/domain/capability'; import { MeAdapter, parseMe } from '@shared/infrastructure/me.adapter'; @@ -40,4 +42,13 @@ export class AccessStore { const tag = this.capabilities().tag; return tag === 'Success' || tag === 'Failure'; }); + + private ready$ = toObservable(this.ready); + /** Resolves once `/me` has settled (success or failure). The `capabilityGuard` awaits + this before deciding — otherwise it reads `can()` while `/me` is still loading and + wrongly denies (deny-by-default), bouncing even an entitled user. */ + async whenReady(): Promise { + if (this.ready()) return; + await firstValueFrom(this.ready$.pipe(filter((r) => r))); + } } diff --git a/src/app/shared/infrastructure/role.interceptor.spec.ts b/src/app/shared/infrastructure/role.interceptor.spec.ts index 38cf215..f66d8e9 100644 --- a/src/app/shared/infrastructure/role.interceptor.spec.ts +++ b/src/app/shared/infrastructure/role.interceptor.spec.ts @@ -1,12 +1,17 @@ -import { describe, it, expect, vi } from 'vitest'; +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { roleInterceptor } from './role.interceptor'; -// currentRole() reads window.location; pin it so the test is about routing, not the shim. -vi.mock('./role', () => ({ currentRole: () => 'admin' })); +// currentRole() reads window.location.search; set it via the real URL rather than +// vi.mock (the Angular unit-test system forbids mocking relative imports). +beforeEach(() => window.history.replaceState({}, '', '/?role=admin')); +afterEach(() => { + window.history.replaceState({}, '', '/'); + sessionStorage.clear(); // currentRole() now persists the dev role; don't leak across tests +}); // Minimal stand-in for HttpRequest — the interceptor only reads `url` and calls -// `clone({ setHeaders })`. Avoids importing @angular/common/http at runtime (its XHR -// chunk needs the JIT compiler under vitest). +// `clone({ setHeaders })`. Avoids importing @angular/common/http (its XHR chunk needs +// the JIT compiler under vitest). function fakeReq(url: string) { const make = (headers: Map) => ({ url, diff --git a/src/app/shared/infrastructure/role.ts b/src/app/shared/infrastructure/role.ts index 7384a8d..b663d76 100644 --- a/src/app/shared/infrastructure/role.ts +++ b/src/app/shared/infrastructure/role.ts @@ -3,14 +3,29 @@ import { Role } from '@shared/domain/role'; /** * Dev-only role stand-in (the reading MECHANISM; the `Role` type is domain). This * POC has one faked self-service user and no real identities, so the two-person - * letter workflow (drafter vs approver) is driven by a `?role=` query param — - * exactly the pattern of the `?scenario=` toggle. The backend receives it as an - * `X-Role` header (see role.interceptor), resolves it into a `Principal` - * server-side, and is the sole authority on what that principal may do (PRD-0002 - * phase P1, `Authz.Can`) — the FE only renders the resulting decision flags, it no - * longer derives permission from this value itself. + * letter workflow (drafter vs approver) plus admin is driven by a `?role=` query + * param. The backend receives it as an `X-Role` header (see role.interceptor), + * resolves it into a `Principal` server-side, and is the sole authority on what that + * principal may do (PRD-0002 phase P1, `Authz.Can`) — the FE only renders the + * resulting decision flags, it no longer derives permission from this value itself. + * + * **Sticky within the tab (sessionStorage):** the interceptor reads this per request, + * but navigation drops the query param (login redirects to /dashboard, RouterLinks + * don't carry it), which would silently revert an admin to drafter mid-session and + * 403 the admin endpoints. So a `?role=` seen in the URL is remembered for the tab; + * later requests use the remembered value. Set `?role=drafter` (or a fresh tab) to + * reset. Dev-only — the interceptor itself is only wired under `isDevMode()`. */ +const STORAGE_KEY = 'dev-role'; +const isRole = (v: string | null): v is Role => + v === 'drafter' || v === 'approver' || v === 'admin'; + export function currentRole(): Role { - const role = new URLSearchParams(window.location.search).get('role'); - return role === 'approver' || role === 'admin' ? role : 'drafter'; + const fromUrl = new URLSearchParams(window.location.search).get('role'); + if (isRole(fromUrl)) { + sessionStorage.setItem(STORAGE_KEY, fromUrl); + return fromUrl; + } + const stored = sessionStorage.getItem(STORAGE_KEY); + return isRole(stored) ? stored : 'drafter'; } diff --git a/src/app/shared/layout/site-header/site-header.component.ts b/src/app/shared/layout/site-header/site-header.component.ts index 8dfae57..0c14415 100644 --- a/src/app/shared/layout/site-header/site-header.component.ts +++ b/src/app/shared/layout/site-header/site-header.component.ts @@ -3,6 +3,8 @@ import { toSignal } from '@angular/core/rxjs-interop'; import { NavigationEnd, Router, RouterLink, RouterLinkActive } from '@angular/router'; import { filter, map } from 'rxjs/operators'; import { SESSION_PORT } from '@shared/application/session.port'; +import { AccessStore } from '@shared/application/access.store'; +import { Capability } from '@shared/domain/capability'; import { BreadcrumbComponent } from '@shared/layout/breadcrumb/breadcrumb.component'; import { trailFor } from '@shared/layout/breadcrumb/breadcrumb-trail'; @@ -18,6 +20,21 @@ const NAV_ITEMS: readonly HeaderNavItem[] = [ { label: $localize`:@@header.nav.inschrijven:Inschrijven`, to: '/registreren' }, ]; +/** Admin-only nav, shown only when `/me` grants the matching capability — the pages + are otherwise reachable by URL alone. */ +const ADMIN_NAV_ITEMS: readonly (HeaderNavItem & { readonly cap: Capability })[] = [ + { + label: $localize`:@@header.nav.huisstijl:Huisstijl`, + to: '/brief/huisstijl', + cap: 'orgtemplate:edit', + }, + { + label: $localize`:@@header.nav.stamdata:Stamdata`, + to: '/beheer/stamdata', + cap: 'stamdata:edit', + }, +]; + /** Organism: CIBG Huisstijl site header — logo block, robijn titlebar (breadcrumb + user menu), horizontal nav. ponytail: text wordmark, not the licensed Rijksoverheid beeldmerk; no search box (no search feature yet). */ @@ -90,6 +107,11 @@ const NAV_ITEMS: readonly HeaderNavItem[] = [ {{ item.label }} } + @for (item of adminItems(); track item.to) { +
  • + {{ item.label }} +
  • + } @@ -101,6 +123,10 @@ export class SiteHeaderComponent { private router = inject(Router); private sessionPort = inject(SESSION_PORT, { optional: true }); + private access = inject(AccessStore); + /** Injecting AccessStore here also warms `/me` at app start (the header renders on + every page), so the admin routes' guard usually finds caps already resolved. */ + protected adminItems = computed(() => ADMIN_NAV_ITEMS.filter((i) => this.access.can(i.cap))); readonly session = computed(() => this.sessionPort?.session() ?? null); private url = toSignal( diff --git a/src/app/shared/layout/site-header/site-header.stories.ts b/src/app/shared/layout/site-header/site-header.stories.ts index 7a9dcd6..cad2cd6 100644 --- a/src/app/shared/layout/site-header/site-header.stories.ts +++ b/src/app/shared/layout/site-header/site-header.stories.ts @@ -1,12 +1,24 @@ import type { Meta, StoryObj } from '@storybook/angular'; import { applicationConfig } from '@storybook/angular'; import { provideRouter } from '@angular/router'; +import { AccessStore } from '@shared/application/access.store'; +import { Capability } from '@shared/domain/capability'; import { SiteHeaderComponent } from './site-header.component'; +// The header injects AccessStore for the capability-gated admin links; stub it so the +// story needs no HTTP/ApiClient. `can` decides which admin links appear. +const withCaps = (caps: Capability[]) => + applicationConfig({ + providers: [ + provideRouter([]), + { provide: AccessStore, useValue: { can: (c: Capability) => caps.includes(c) } }, + ], + }); + const meta: Meta = { title: 'Design System/Organisms/Site Header', component: SiteHeaderComponent, - decorators: [applicationConfig({ providers: [provideRouter([])] })], + decorators: [withCaps([])], render: (args) => ({ props: args, template: ``, @@ -15,4 +27,10 @@ const meta: Meta = { export default meta; type Story = StoryObj; +/** Standard user — no admin links. */ export const Default: Story = {}; + +/** Admin — the capability-gated Huisstijl + Stamdata links appear. */ +export const AsAdmin: Story = { + decorators: [withCaps(['orgtemplate:edit', 'stamdata:edit'])], +}; diff --git a/src/locale/messages.en.xlf b/src/locale/messages.en.xlf index de12c74..9dba848 100644 --- a/src/locale/messages.en.xlf +++ b/src/locale/messages.en.xlf @@ -3582,6 +3582,22 @@ 102 + + Huisstijl + House style + + src/app/shared/layout/site-header/site-header.component.ts + 26 + + + + Stamdata + Master data + + src/app/shared/layout/site-header/site-header.component.ts + 27 + + diff --git a/src/locale/messages.xlf b/src/locale/messages.xlf index 8916631..d0e08ae 100644 --- a/src/locale/messages.xlf +++ b/src/locale/messages.xlf @@ -2592,56 +2592,70 @@ Overzicht src/app/shared/layout/site-header/site-header.component.ts - 15 + 17 Mijn gegevens src/app/shared/layout/site-header/site-header.component.ts - 16 + 18 Herregistratie src/app/shared/layout/site-header/site-header.component.ts - 17 + 19 Inschrijven src/app/shared/layout/site-header/site-header.component.ts - 18 + 20 + + + + Huisstijl + + src/app/shared/layout/site-header/site-header.component.ts + 26 + + + + Stamdata + + src/app/shared/layout/site-header/site-header.component.ts + 27 BIG-register src/app/shared/layout/site-header/site-header.component.ts - 53,54 + 62,63 Ministerie van Volksgezondheid, Welzijn en Sport src/app/shared/layout/site-header/site-header.component.ts - 55,57 + 64,66 Uitloggen src/app/shared/layout/site-header/site-header.component.ts - 77,78 + 86,87 Hoofdnavigatie src/app/shared/layout/site-header/site-header.component.ts - 85,86 + 94,95