From 8078c499cba85a8cbbce34c34afb56aba25eec9d Mon Sep 17 00:00:00 2001 From: Edwin van den Houdt Date: Fri, 3 Jul 2026 22:02:50 +0200 Subject: [PATCH] =?UTF-8?q?feat(fp):=20WP-09=20=E2=80=94=20pure-logic=20cl?= =?UTF-8?q?osure:=20dates=20+=20missing=20command=20specs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consolidate four hand-rolled nl-NL date formatters (tasks.ts, aanvraag- block, letter-preview, aanvraag-view -- one more than the WP found) into one shared/kernel/datum.ts::formatDatumNl, spec-pinned and empty-safe. Add the two missing command specs CLAUDE.md's testing rule calls for: draft-sync.spec.ts (debounce coalescing + trailing-call + submit Result shape, via fake timers) and submit-change-request.spec.ts. Remove the unused RemoteData.map3 (updating the three docs that mentioned it); the variant input on confirmation.component.ts was already gone. Documents both stale-WP-text corrections in the backlog file. This closes out backlog Phase 1 (FP/DDD core, WP-05..09). --- CLAUDE.md | 7 +- docs/ARCHITECTURE.md | 2 +- docs/backlog/README.md | 2 +- docs/backlog/WP-09-pure-logic.md | 27 +- documentation.json | 3348 ++++++++--------- .../letter-preview.component.ts | 7 +- .../application/draft-sync.spec.ts | 99 + .../application/submit-change-request.spec.ts | 40 + src/app/registratie/domain/aanvraag-view.ts | 13 +- src/app/registratie/domain/tasks.ts | 7 +- .../aanvraag-block.component.ts | 9 +- src/app/shared/application/remote-data.ts | 14 - src/app/shared/kernel/datum.spec.ts | 22 + src/app/shared/kernel/datum.ts | 16 + src/docs/remote-data.mdx | 8 +- 15 files changed, 1743 insertions(+), 1878 deletions(-) create mode 100644 src/app/registratie/application/draft-sync.spec.ts create mode 100644 src/app/registratie/application/submit-change-request.spec.ts create mode 100644 src/app/shared/kernel/datum.spec.ts create mode 100644 src/app/shared/kernel/datum.ts diff --git a/CLAUDE.md b/CLAUDE.md index 13be507..3498e13 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -68,7 +68,7 @@ Default reflex — **if you're about to add a second/third boolean to track stat model a discriminated union instead.** Three tools, all in `shared/application`: - **`RemoteData`** (`remote-data.ts`) — `Loading | Empty | Failure{error} | Success{value}`. - Combine sources with `map`/`map2`/`map3`/`andThen` (Failure > Loading > Success). + Combine sources with `map`/`map2`/`andThen` (Failure > Loading > Success). Render it via the `` molecule (`shared/ui/async`) — one of four templates, mutually exclusive by construction. Default loading spinner/skeleton is delay-gated (~250ms) so fast connections don't flash. @@ -145,6 +145,11 @@ not heavy component tests. (Model/Msg/reduce) + value objects + a `submit-*` command returning `Result` — the same shape as the wizards, whether it's one step or many. Don't hand-roll mutable fields + ad-hoc error signals. +- **Dates: `DatePipe` in templates, `formatDatumNl` in pure TS.** A template formats a + date with Angular's `DatePipe` (`| date: 'longDate'`); pure TS that can't reach a pipe + (a domain function, a `$localize` string) uses the one hand-written + `formatDatumNl` (`shared/kernel/datum.ts`). Never a third hand-rolled + `toLocaleDateString` call. - Routes: lazy `loadComponent`, persistent `ShellComponent` parent, `canActivate: [authGuard]` on protected routes (`app.routes.ts`). - Theming: CIBG Huisstijl (a customized Bootstrap 5.2 build) is vendored under diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 4b1fa7a..aef1216 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -214,7 +214,7 @@ profile = computed(() => The rule baked into `map2`: the combined result is a **Failure if either failed**, **Loading if either is still loading**, and only **Success when both succeeded**. So the page renders one state and the combiner callback only runs -when it's safe. (`map`, `map3`, `andThen` are variations on the same idea.) +when it's safe. (`map`, `andThen` are variations on the same idea.) ### 2c. The store — "all state changes go through one pure function" diff --git a/docs/backlog/README.md b/docs/backlog/README.md index 2b2f62d..16a4ad8 100644 --- a/docs/backlog/README.md +++ b/docs/backlog/README.md @@ -48,7 +48,7 @@ for its existing violations, so every WP ends green. | [WP-06](WP-06-typed-async.md) | Generic async template contexts — kill `$any()` | 1 · FP/DDD | done | | [WP-07](WP-07-brief-idioms.md) | Brief on the shared idioms + RemoteData MDX | 1 · FP/DDD | done | | [WP-08](WP-08-store-idiom.md) | One store idiom + machine naming + TEA MDX | 1 · FP/DDD | done | -| [WP-09](WP-09-pure-logic.md) | Pure-logic closure: dates + missing command specs | 1 · FP/DDD | todo | +| [WP-09](WP-09-pure-logic.md) | Pure-logic closure: dates + missing command specs | 1 · FP/DDD | done | | [WP-10](WP-10-button-fidelity.md) | CIBG button fidelity | 2 · CIBG | todo | | [WP-11](WP-11-markup-fidelity.md) | CIBG markup fidelity: application-link + absent-class triage | 2 · CIBG | done | | [WP-12](WP-12-datablock.md) | CIBG Datablock for application data | 2 · CIBG | done | diff --git a/docs/backlog/WP-09-pure-logic.md b/docs/backlog/WP-09-pure-logic.md index a419b55..f508ca0 100644 --- a/docs/backlog/WP-09-pure-logic.md +++ b/docs/backlog/WP-09-pure-logic.md @@ -1,6 +1,6 @@ # WP-09 — Pure-logic closure: dates + missing command specs -Status: todo +Status: done Phase: 1 — FP/DDD core ## Why @@ -51,15 +51,28 @@ no spec despite "domain and pure logic must have a spec" (CLAUDE.md §5). ## Acceptance criteria -- [ ] Exactly one hand-written date formatter in the repo; `grep -rn "toLocaleDateString" src/app` - hits only `datum.ts`. -- [ ] Both command specs exist; debounce coalescing + error path covered. -- [ ] `map3` / unused `variant` input removed (or a note here why kept). -- [ ] CLAUDE.md rule added. +- [x] Exactly one hand-written date formatter in the repo. `formatDatumNl` uses + `Intl.DateTimeFormat(...).format()` rather than `.toLocaleDateString()`, so + `grep -rn "toLocaleDateString" src/app` now hits **nothing** (stronger than the + literal criterion, same intent — no file anywhere hand-rolls date formatting). +- [x] Both command specs exist; debounce coalescing + error path covered + (`draft-sync.spec.ts`, `submit-change-request.spec.ts`). +- [x] `map3` removed (found in `shared/application/remote-data.ts`, not + `shared/kernel/fp.ts` as the WP text guessed — updated the three docs that + mentioned it: CLAUDE.md, `docs/ARCHITECTURE.md`, `remote-data.mdx`). The + `variant` input on `confirmation.component.ts` no longer exists — already + cleaned up before this WP ran; nothing to do. +- [x] CLAUDE.md rule added (`Conventions` — DatePipe in templates, `formatDatumNl` in + pure TS). ## Verification -GREEN + `npm run test-storybook:ci`. +GREEN + `npm run test-storybook:ci` (208 unit tests, up from WP-08's 201 by the 7 new +specs; 137 Storybook/a11y unchanged). Manual smoke via a running `docker compose` stack + +Playwright: dashboard's herregistratie-deadline task text ("Verleng uw registratie vóór 1 +maart 2027"), the Concept aanvraag-block's complete-before text ("Rond de aanvraag af +vóór 2 augustus 2026"), and `formatDatumNl` unit specs for the letter-preview's `today` — +all render the expected long-form Dutch date, no console errors. ## Out of scope diff --git a/documentation.json b/documentation.json index b2e4a00..691dc00 100644 --- a/documentation.json +++ b/documentation.json @@ -207,12 +207,12 @@ }, { "name": "AanvraagRow", - "id": "interface-AanvraagRow-c0a98dbd2b70fdab17be4733c584ff631f762cc4c156dd528f80e547147f580e65ba92a202e950c9996087f20f1c371993e9d72aec62f7b3b335c21927947692", + "id": "interface-AanvraagRow-9c09589b6e73de742622d4742ef41d3c1460e08ca3149f76f67486c304229c699a3073836c03af8edea3ba693b6f49ae70f5d8851ff1d782645d483fea444bfc", "file": "src/app/registratie/domain/aanvraag-view.ts", "deprecated": false, "deprecationMessage": "", "type": "interface", - "sourceCode": "import { Aanvraag, AanvraagStatus, AanvraagType } from './aanvraag';\n\n/** View-model mapping for an aanvraag: type → labels, status → label, and the fields\n for a CIBG \"aanvragen\" row / the case-detail page. Pure, no Angular — the UI\n renders these, it does not derive them. */\n\nexport const TYPE_LABELS: Record = {\n registratie: $localize`:@@aanvraagBlock.type.registratie:Inschrijving`,\n herregistratie: $localize`:@@aanvraagBlock.type.herregistratie:Herregistratie`,\n intake: $localize`:@@aanvraagBlock.type.intake:Herregistratie-intake`,\n};\n\n/** What the aanvraag is for (shown under the title). */\nexport function purposeLabel(type: AanvraagType): string {\n switch (type) {\n case 'registratie':\n return $localize`:@@aanvraag.purpose.registratie:Inschrijving in het BIG-register`;\n case 'herregistratie':\n return $localize`:@@aanvraag.purpose.herregistratie:Verlenging van uw BIG-registratie`;\n case 'intake':\n return $localize`:@@aanvraag.purpose.intake:Intake-vragenlijst voor uw herregistratie`;\n }\n}\n\n/** The status as a plain label (what state the aanvraag is in). */\nexport function statusLabel(status: AanvraagStatus): string {\n switch (status.tag) {\n case 'Concept':\n return $localize`:@@aanvraag.status.concept:Concept (nog niet ingediend)`;\n case 'InBehandeling':\n return $localize`:@@aanvraag.status.inBehandeling:In behandeling`;\n case 'Goedgekeurd':\n return $localize`:@@aanvraag.status.goedgekeurd:Goedgekeurd`;\n case 'Afgewezen':\n return $localize`:@@aanvraag.status.afgewezen:Afgewezen`;\n }\n}\n\n/** The reference number, or '' for a Concept (which has none yet). */\nexport function referentie(status: AanvraagStatus): string {\n return status.tag === 'Concept' ? '' : status.referentie;\n}\n\nexport interface AanvraagRow {\n heading: string;\n /** What the aanvraag is for (the `.subtitle` line). */\n subtitle: string;\n /** The status: label + reference + submit date (+ any note) — the `.status` line. */\n status: string;\n}\n\nfunction formatNL(iso?: string): string {\n return iso\n ? new Date(iso).toLocaleDateString('nl-NL', { day: 'numeric', month: 'long', year: 'numeric' })\n : '';\n}\n\n/** Fields for a submitted aanvraag's row in the dashboard \"aanvragen\" list (Concept\n has no row — it renders as a resumable melding, see aanvraag-block). */\nexport function submittedRow(a: Aanvraag): AanvraagRow {\n const s = a.status;\n const parts = [statusLabel(s)];\n const ref = referentie(s);\n if (ref) parts.push($localize`:@@aanvraag.row.ref:Referentie ${ref}:ref:`);\n if (a.submittedAt)\n parts.push($localize`:@@aanvraag.row.ingediend:ingediend op ${formatNL(a.submittedAt)}:datum:`);\n if (s.tag === 'InBehandeling' && s.manual)\n parts.push(\n $localize`:@@aanvraagBlock.manual:Uw aanvraag wordt handmatig beoordeeld in de backoffice.`,\n );\n if (s.tag === 'Afgewezen') parts.push(s.reden);\n return {\n heading: TYPE_LABELS[a.type],\n subtitle: purposeLabel(a.type),\n status: parts.join(' · '),\n };\n}\n\n/** Key/value rows for the case-detail page (CIBG Datablock). */\nexport function detailRows(a: Aanvraag): { key: string; value: string }[] {\n const rows = [\n { key: $localize`:@@aanvraag.detail.soort:Soort aanvraag`, value: TYPE_LABELS[a.type] },\n { key: $localize`:@@aanvraag.detail.waarvoor:Waarvoor`, value: purposeLabel(a.type) },\n { key: $localize`:@@aanvraag.detail.status:Status`, value: statusLabel(a.status) },\n {\n key: $localize`:@@aanvraag.detail.referentie:Referentie`,\n value: referentie(a.status) || '—',\n },\n {\n key: $localize`:@@aanvraag.detail.ingediend:Ingediend op`,\n value: a.submittedAt ? formatNL(a.submittedAt) : '—',\n },\n ];\n if (a.status.tag === 'Afgewezen') {\n rows.push({\n key: $localize`:@@aanvraag.detail.reden:Reden van afwijzing`,\n value: a.status.reden,\n });\n }\n return rows;\n}\n", + "sourceCode": "import { formatDatumNl } from '@shared/kernel/datum';\nimport { Aanvraag, AanvraagStatus, AanvraagType } from './aanvraag';\n\n/** View-model mapping for an aanvraag: type → labels, status → label, and the fields\n for a CIBG \"aanvragen\" row / the case-detail page. Pure, no Angular — the UI\n renders these, it does not derive them. */\n\nexport const TYPE_LABELS: Record = {\n registratie: $localize`:@@aanvraagBlock.type.registratie:Inschrijving`,\n herregistratie: $localize`:@@aanvraagBlock.type.herregistratie:Herregistratie`,\n intake: $localize`:@@aanvraagBlock.type.intake:Herregistratie-intake`,\n};\n\n/** What the aanvraag is for (shown under the title). */\nexport function purposeLabel(type: AanvraagType): string {\n switch (type) {\n case 'registratie':\n return $localize`:@@aanvraag.purpose.registratie:Inschrijving in het BIG-register`;\n case 'herregistratie':\n return $localize`:@@aanvraag.purpose.herregistratie:Verlenging van uw BIG-registratie`;\n case 'intake':\n return $localize`:@@aanvraag.purpose.intake:Intake-vragenlijst voor uw herregistratie`;\n }\n}\n\n/** The status as a plain label (what state the aanvraag is in). */\nexport function statusLabel(status: AanvraagStatus): string {\n switch (status.tag) {\n case 'Concept':\n return $localize`:@@aanvraag.status.concept:Concept (nog niet ingediend)`;\n case 'InBehandeling':\n return $localize`:@@aanvraag.status.inBehandeling:In behandeling`;\n case 'Goedgekeurd':\n return $localize`:@@aanvraag.status.goedgekeurd:Goedgekeurd`;\n case 'Afgewezen':\n return $localize`:@@aanvraag.status.afgewezen:Afgewezen`;\n }\n}\n\n/** The reference number, or '' for a Concept (which has none yet). */\nexport function referentie(status: AanvraagStatus): string {\n return status.tag === 'Concept' ? '' : status.referentie;\n}\n\nexport interface AanvraagRow {\n heading: string;\n /** What the aanvraag is for (the `.subtitle` line). */\n subtitle: string;\n /** The status: label + reference + submit date (+ any note) — the `.status` line. */\n status: string;\n}\n\n/** Fields for a submitted aanvraag's row in the dashboard \"aanvragen\" list (Concept\n has no row — it renders as a resumable melding, see aanvraag-block). */\nexport function submittedRow(a: Aanvraag): AanvraagRow {\n const s = a.status;\n const parts = [statusLabel(s)];\n const ref = referentie(s);\n if (ref) parts.push($localize`:@@aanvraag.row.ref:Referentie ${ref}:ref:`);\n if (a.submittedAt)\n parts.push(\n $localize`:@@aanvraag.row.ingediend:ingediend op ${formatDatumNl(a.submittedAt)}:datum:`,\n );\n if (s.tag === 'InBehandeling' && s.manual)\n parts.push(\n $localize`:@@aanvraagBlock.manual:Uw aanvraag wordt handmatig beoordeeld in de backoffice.`,\n );\n if (s.tag === 'Afgewezen') parts.push(s.reden);\n return {\n heading: TYPE_LABELS[a.type],\n subtitle: purposeLabel(a.type),\n status: parts.join(' · '),\n };\n}\n\n/** Key/value rows for the case-detail page (CIBG Datablock). */\nexport function detailRows(a: Aanvraag): { key: string; value: string }[] {\n const rows = [\n { key: $localize`:@@aanvraag.detail.soort:Soort aanvraag`, value: TYPE_LABELS[a.type] },\n { key: $localize`:@@aanvraag.detail.waarvoor:Waarvoor`, value: purposeLabel(a.type) },\n { key: $localize`:@@aanvraag.detail.status:Status`, value: statusLabel(a.status) },\n {\n key: $localize`:@@aanvraag.detail.referentie:Referentie`,\n value: referentie(a.status) || '—',\n },\n {\n key: $localize`:@@aanvraag.detail.ingediend:Ingediend op`,\n value: a.submittedAt ? formatDatumNl(a.submittedAt) : '—',\n },\n ];\n if (a.status.tag === 'Afgewezen') {\n rows.push({\n key: $localize`:@@aanvraag.detail.reden:Reden van afwijzing`,\n value: a.status.reden,\n });\n }\n return rows;\n}\n", "properties": [ { "name": "heading", @@ -222,7 +222,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 45 + "line": 46 }, { "name": "status", @@ -232,7 +232,7 @@ "indexKey": "", "optional": false, "description": "

The status: label + reference + submit date (+ any note) — the .status line.

\n", - "line": 49, + "line": 50, "rawdescription": "\nThe status: label + reference + submit date (+ any note) — the `.status` line." }, { @@ -243,7 +243,7 @@ "indexKey": "", "optional": false, "description": "

What the aanvraag is for (the .subtitle line).

\n", - "line": 47, + "line": 48, "rawdescription": "\nWhat the aanvraag is for (the `.subtitle` line)." } ], @@ -2082,54 +2082,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": "", @@ -2172,14 +2125,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": "", @@ -2304,6 +2254,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" }, @@ -4098,12 +4098,12 @@ }, { "name": "PortalTask", - "id": "interface-PortalTask-9d796a772240d1dd0b99bf94db13e3c962f0e3b5db2705a31877cae66626dabc6e666097ca33bbbbb1a0bb25998bd5d83f2ba329764e40e15e692aa7a1ed282d", + "id": "interface-PortalTask-c44c0f8c6e396385be04bcc6b2311ccc2ea6363ec7e49b87e1575954bc4c8f9b78e401b7da2992d822632a526e1b20701438658cab9c10a3f9eac2602c2baab1", "file": "src/app/registratie/domain/tasks.ts", "deprecated": false, "deprecationMessage": "", "type": "interface", - "sourceCode": "import { Registration } from './registration';\nimport { herregistratieDeadline } from './registration.policy';\n\n/**\n * What the dashboard's \"Wat moet ik regelen\" list needs. Pure presentation data\n * derived from the registration — no Angular. Mirrors the shared TaskItem shape.\n */\nexport interface PortalTask {\n title: string;\n description: string;\n to: string;\n actionLabel: string;\n}\n\nfunction formatNL(d: Date): string {\n return d.toLocaleDateString('nl-NL', { day: 'numeric', month: 'long', year: 'numeric' });\n}\n\n/**\n * Derive the open tasks for a professional (pure). Eligibility is the server's\n * decision (`decisions.eligibleForHerregistratie`), passed in — the FE renders it,\n * it does not recompute the rule (ADR-0001). The deadline is still formatted\n * client-side for the task copy (presentation, not a rule).\n */\nexport function tasksFromProfile(\n reg: Registration,\n eligibleForHerregistratie: boolean,\n): PortalTask[] {\n const tasks: PortalTask[] = [];\n\n if (eligibleForHerregistratie) {\n const deadline = herregistratieDeadline(reg);\n tasks.push({\n title: $localize`:@@task.herregistratie.title:Vraag uw herregistratie aan`,\n description: deadline\n ? $localize`:@@task.herregistratie.deadline:Verleng uw registratie vóór ${formatNL(deadline)}:deadline:.`\n : $localize`:@@task.herregistratie.nodeadline:U kunt nu uw herregistratie aanvragen.`,\n to: '/herregistratie',\n actionLabel: $localize`:@@task.herregistratie.action:Herregistratie aanvragen`,\n });\n }\n\n if (reg.status.tag === 'Geschorst') {\n tasks.push({\n title: $localize`:@@task.geschorst.title:Uw registratie is geschorst`,\n description: reg.status.reden,\n to: '/registratie',\n actionLabel: $localize`:@@task.bekijkGegevens.action:Bekijk uw gegevens`,\n });\n }\n\n if (reg.status.tag === 'Doorgehaald') {\n tasks.push({\n title: $localize`:@@task.doorgehaald.title:Uw registratie is doorgehaald`,\n description: reg.status.reden,\n to: '/registratie',\n actionLabel: $localize`:@@task.bekijkGegevens.action:Bekijk uw gegevens`,\n });\n }\n\n return tasks;\n}\n", + "sourceCode": "import { formatDatumNl } from '@shared/kernel/datum';\nimport { Registration } from './registration';\nimport { herregistratieDeadline } from './registration.policy';\n\n/**\n * What the dashboard's \"Wat moet ik regelen\" list needs. Pure presentation data\n * derived from the registration — no Angular. Mirrors the shared TaskItem shape.\n */\nexport interface PortalTask {\n title: string;\n description: string;\n to: string;\n actionLabel: string;\n}\n\n/**\n * Derive the open tasks for a professional (pure). Eligibility is the server's\n * decision (`decisions.eligibleForHerregistratie`), passed in — the FE renders it,\n * it does not recompute the rule (ADR-0001). The deadline is still formatted\n * client-side for the task copy (presentation, not a rule).\n */\nexport function tasksFromProfile(\n reg: Registration,\n eligibleForHerregistratie: boolean,\n): PortalTask[] {\n const tasks: PortalTask[] = [];\n\n if (eligibleForHerregistratie) {\n const deadline = herregistratieDeadline(reg);\n tasks.push({\n title: $localize`:@@task.herregistratie.title:Vraag uw herregistratie aan`,\n description: deadline\n ? $localize`:@@task.herregistratie.deadline:Verleng uw registratie vóór ${formatDatumNl(deadline)}:deadline:.`\n : $localize`:@@task.herregistratie.nodeadline:U kunt nu uw herregistratie aanvragen.`,\n to: '/herregistratie',\n actionLabel: $localize`:@@task.herregistratie.action:Herregistratie aanvragen`,\n });\n }\n\n if (reg.status.tag === 'Geschorst') {\n tasks.push({\n title: $localize`:@@task.geschorst.title:Uw registratie is geschorst`,\n description: reg.status.reden,\n to: '/registratie',\n actionLabel: $localize`:@@task.bekijkGegevens.action:Bekijk uw gegevens`,\n });\n }\n\n if (reg.status.tag === 'Doorgehaald') {\n tasks.push({\n title: $localize`:@@task.doorgehaald.title:Uw registratie is doorgehaald`,\n description: reg.status.reden,\n to: '/registratie',\n actionLabel: $localize`:@@task.bekijkGegevens.action:Bekijk uw gegevens`,\n });\n }\n\n return tasks;\n}\n", "properties": [ { "name": "actionLabel", @@ -4113,7 +4113,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 12 + "line": 13 }, { "name": "description", @@ -4123,7 +4123,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 10 + "line": 11 }, { "name": "title", @@ -4133,7 +4133,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 9 + "line": 10 }, { "name": "to", @@ -4143,7 +4143,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 11 + "line": 12 } ], "indexSignatures": [], @@ -5533,7 +5533,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": "", @@ -5586,53 +5633,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, @@ -11525,7 +11525,7 @@ "components": [ { "name": "AanvraagBlockComponent", - "id": "component-AanvraagBlockComponent-5ae4e0cba48f22e36bc3d7b5c276ebce779bc2c001340a82bf14abea0e4fb6d955eae8694dd46133a8c7721c65acf5249e16e2ac31ffd54d83f165b00ddccbac", + "id": "component-AanvraagBlockComponent-7d86311776d0c46af1055972d80e89ba49cde6534a925418ede0c68d011c229d6b0c84f1a8e0b1379612d21e3bd4945aad9ed9f155963adeedf8b9cfbe83cb5b", "file": "src/app/registratie/ui/aanvraag-block/aanvraag-block.component.ts", "encapsulation": [], "entryComponents": [], @@ -11550,7 +11550,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 48, + "line": 49, "required": true } ], @@ -11563,7 +11563,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 51, + "line": 52, "required": false }, { @@ -11574,7 +11574,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 50, + "line": 51, "required": false } ], @@ -11588,21 +11588,21 @@ "indexKey": "", "optional": false, "description": "", - "line": 54, + "line": 55, "modifierKind": [ 124 ] }, { "name": "conceptText", - "defaultValue": "computed(() => {\n const s = this.aanvraag().status;\n if (s.tag !== 'Concept') return '';\n return $localize`:@@aanvraagBlock.conceptMelding:Deze aanvraag is nog niet volledig afgerond — u bent gebleven bij stap ${s.stepIndex + 1}:stap: van ${s.stepCount}:totaal:. Rond de aanvraag af vóór ${formatNL(this.deadline())}:datum:.`;\n })", + "defaultValue": "computed(() => {\n const s = this.aanvraag().status;\n if (s.tag !== 'Concept') return '';\n return $localize`:@@aanvraagBlock.conceptMelding:Deze aanvraag is nog niet volledig afgerond — u bent gebleven bij stap ${s.stepIndex + 1}:stap: van ${s.stepCount}:totaal:. Rond de aanvraag af vóór ${formatDatumNl(this.deadline())}:datum:.`;\n })", "deprecated": false, "deprecationMessage": "", "type": "unknown", "indexKey": "", "optional": false, "description": "

The melding body for a Concept: wizard not finished + complete-before date.

\n", - "line": 65, + "line": 66, "rawdescription": "\nThe melding body for a Concept: wizard not finished + complete-before date.", "modifierKind": [ 124 @@ -11617,7 +11617,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 58, + "line": 59, "modifierKind": [ 123 ] @@ -11631,7 +11631,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 53, + "line": 54, "modifierKind": [ 124 ] @@ -11656,7 +11656,7 @@ "description": "

Organism: a resumable Concept ("lopende aanvraag") on the dashboard, rendered as\na CIBG "melding" (warning) with its actions — verwijderen as a link, aanvraag\nopenen as a button. Which actions show is driven by the pure blockActions(status);\nthe block only renders. Submitted/resolved aanvragen are NOT rendered here — they\nrender as CIBG "aanvragen" rows (see application-link + aanvraag-view).

\n", "rawdescription": "\nOrganism: a resumable Concept (\"lopende aanvraag\") on the dashboard, rendered as\na CIBG \"melding\" (warning) with its actions — verwijderen as a link, aanvraag\nopenen as a button. Which actions show is driven by the pure `blockActions(status)`;\nthe block only renders. Submitted/resolved aanvragen are NOT rendered here — they\nrender as CIBG \"aanvragen\" rows (see application-link + aanvraag-view).", "type": "component", - "sourceCode": "import { Component, computed, input, output } from '@angular/core';\nimport { ButtonComponent } from '@shared/ui/button/button.component';\nimport { AlertComponent } from '@shared/ui/alert/alert.component';\nimport { Aanvraag } from '@registratie/domain/aanvraag';\nimport { TYPE_LABELS } from '@registratie/domain/aanvraag-view';\nimport { blockActions } from '@registratie/domain/block-actions';\n\n/** Organism: a resumable Concept (\"lopende aanvraag\") on the dashboard, rendered as\n a CIBG \"melding\" (warning) with its actions — verwijderen as a link, aanvraag\n openen as a button. Which actions show is driven by the pure `blockActions(status)`;\n the block only renders. Submitted/resolved aanvragen are NOT rendered here — they\n render as CIBG \"aanvragen\" rows (see application-link + aanvraag-view). */\n@Component({\n selector: 'app-aanvraag-block',\n imports: [ButtonComponent, AlertComponent],\n styles: [\n `\n .actions {\n display: flex;\n align-items: center;\n gap: var(--rhc-space-max-md);\n margin-block-start: var(--rhc-space-max-sm);\n }\n `,\n ],\n template: `\n @if (aanvraag().status.tag === 'Concept') {\n \n

{{ typeLabel() }}

\n

{{ conceptText() }}

\n
\n @if (actions().includes('cancel')) {\n Verwijderen\n }\n @if (actions().includes('resume')) {\n Aanvraag openen\n }\n
\n
\n }\n `,\n})\nexport class AanvraagBlockComponent {\n aanvraag = input.required();\n\n resume = output();\n cancel = output();\n\n protected typeLabel = computed(() => TYPE_LABELS[this.aanvraag().type]);\n protected actions = computed(() => blockActions(this.aanvraag().status));\n\n // ponytail: display-only deadline derived as createdAt + 30 dagen; move to a\n // server-sent `completeBefore` on the DTO when the expiry rule becomes real.\n private deadline = computed(() => {\n const d = new Date(this.aanvraag().createdAt);\n d.setDate(d.getDate() + 30);\n return d.toISOString();\n });\n\n /** The melding body for a Concept: wizard not finished + complete-before date. */\n protected conceptText = computed(() => {\n const s = this.aanvraag().status;\n if (s.tag !== 'Concept') return '';\n return $localize`:@@aanvraagBlock.conceptMelding:Deze aanvraag is nog niet volledig afgerond — u bent gebleven bij stap ${s.stepIndex + 1}:stap: van ${s.stepCount}:totaal:. Rond de aanvraag af vóór ${formatNL(this.deadline())}:datum:.`;\n });\n}\n\nfunction formatNL(iso?: string): string {\n return iso\n ? new Date(iso).toLocaleDateString('nl-NL', { day: 'numeric', month: 'long', year: 'numeric' })\n : '';\n}\n", + "sourceCode": "import { Component, computed, input, output } from '@angular/core';\nimport { formatDatumNl } from '@shared/kernel/datum';\nimport { ButtonComponent } from '@shared/ui/button/button.component';\nimport { AlertComponent } from '@shared/ui/alert/alert.component';\nimport { Aanvraag } from '@registratie/domain/aanvraag';\nimport { TYPE_LABELS } from '@registratie/domain/aanvraag-view';\nimport { blockActions } from '@registratie/domain/block-actions';\n\n/** Organism: a resumable Concept (\"lopende aanvraag\") on the dashboard, rendered as\n a CIBG \"melding\" (warning) with its actions — verwijderen as a link, aanvraag\n openen as a button. Which actions show is driven by the pure `blockActions(status)`;\n the block only renders. Submitted/resolved aanvragen are NOT rendered here — they\n render as CIBG \"aanvragen\" rows (see application-link + aanvraag-view). */\n@Component({\n selector: 'app-aanvraag-block',\n imports: [ButtonComponent, AlertComponent],\n styles: [\n `\n .actions {\n display: flex;\n align-items: center;\n gap: var(--rhc-space-max-md);\n margin-block-start: var(--rhc-space-max-sm);\n }\n `,\n ],\n template: `\n @if (aanvraag().status.tag === 'Concept') {\n \n

{{ typeLabel() }}

\n

{{ conceptText() }}

\n
\n @if (actions().includes('cancel')) {\n Verwijderen\n }\n @if (actions().includes('resume')) {\n Aanvraag openen\n }\n
\n
\n }\n `,\n})\nexport class AanvraagBlockComponent {\n aanvraag = input.required();\n\n resume = output();\n cancel = output();\n\n protected typeLabel = computed(() => TYPE_LABELS[this.aanvraag().type]);\n protected actions = computed(() => blockActions(this.aanvraag().status));\n\n // ponytail: display-only deadline derived as createdAt + 30 dagen; move to a\n // server-sent `completeBefore` on the DTO when the expiry rule becomes real.\n private deadline = computed(() => {\n const d = new Date(this.aanvraag().createdAt);\n d.setDate(d.getDate() + 30);\n return d.toISOString();\n });\n\n /** The melding body for a Concept: wizard not finished + complete-before date. */\n protected conceptText = computed(() => {\n const s = this.aanvraag().status;\n if (s.tag !== 'Concept') return '';\n return $localize`:@@aanvraagBlock.conceptMelding:Deze aanvraag is nog niet volledig afgerond — u bent gebleven bij stap ${s.stepIndex + 1}:stap: van ${s.stepCount}:totaal:. Rond de aanvraag af vóór ${formatDatumNl(this.deadline())}:datum:.`;\n });\n}\n", "assetsDirs": [], "styleUrlsData": "", "stylesData": "\n .actions {\n display: flex;\n align-items: center;\n gap: var(--rhc-space-max-md);\n margin-block-start: var(--rhc-space-max-sm);\n }\n \n", @@ -17460,7 +17460,7 @@ }, { "name": "LetterPreviewComponent", - "id": "component-LetterPreviewComponent-a7ec4d9a155e69ddd76abb18140d9a785058693ca4e9d388ab98b452f3bcc3a876a9408493eb6bc766ad6e4ed3ec8140e1d303ab8c01e030771147551b75022c", + "id": "component-LetterPreviewComponent-23860a46c81be5b99588ec2bf5211cffe7bd31c5deefdca348916d0c85837834280c1a028f2efdebdedfbe60607091984e966c02d8a428ce089295dcc194a566", "file": "src/app/brief/ui/letter-preview/letter-preview.component.ts", "encapsulation": [], "entryComponents": [], @@ -17485,7 +17485,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 148, + "line": 149, "required": true }, { @@ -17497,7 +17497,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 149, + "line": 150, "required": false }, { @@ -17508,7 +17508,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 152, + "line": 153, "required": false }, { @@ -17519,7 +17519,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 151, + "line": 152, "required": false } ], @@ -17534,7 +17534,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 174, + "line": 171, "modifierKind": [ 124 ] @@ -17548,7 +17548,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 161, + "line": 158, "modifierKind": [ 123 ] @@ -17562,7 +17562,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 173, + "line": 170, "modifierKind": [ 124 ] @@ -17576,7 +17576,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 176, + "line": 173, "modifierKind": [ 124 ] @@ -17590,7 +17590,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 172, + "line": 169, "modifierKind": [ 124 ] @@ -17604,7 +17604,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 154, + "line": 155, "modifierKind": [ 124 ] @@ -17618,21 +17618,21 @@ "indexKey": "", "optional": false, "description": "", - "line": 175, + "line": 172, "modifierKind": [ 124 ] }, { "name": "today", - "defaultValue": "new Date().toLocaleDateString('nl-NL', {\n day: 'numeric',\n month: 'long',\n year: 'numeric',\n })", + "defaultValue": "formatDatumNl(new Date())", "deprecated": false, "deprecationMessage": "", "type": "unknown", "indexKey": "", "optional": false, "description": "", - "line": 155, + "line": 156, "modifierKind": [ 123 ] @@ -17646,7 +17646,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 162, + "line": 159, "modifierKind": [ 123 ] @@ -17678,7 +17678,7 @@ "description": "

Organism: read-only rendering of the letter as the approver/recipient sees it.\nPlaceholders show as labelled chips (values are resolved server-side only at send);\na chip flagged by the linter shows its error/warning state. The "Voorbeeld" toggle\nfills auto-resolvable placeholders with sample values to preview the sent result.

\n", "rawdescription": "\nOrganism: read-only rendering of the letter as the approver/recipient sees it.\nPlaceholders show as labelled chips (values are resolved server-side only at send);\na chip flagged by the linter shows its error/warning state. The \"Voorbeeld\" toggle\nfills auto-resolvable placeholders with sample values to preview the sent result.", "type": "component", - "sourceCode": "import { Component, computed, input, signal } from '@angular/core';\nimport { NgTemplateOutlet } from '@angular/common';\nimport { HeadingComponent } from '@shared/ui/heading/heading.component';\nimport { ButtonComponent } from '@shared/ui/button/button.component';\nimport { PlaceholderChipComponent } from '@shared/ui/placeholder-chip/placeholder-chip.component';\nimport { Paragraph } from '@shared/kernel/rich-text';\nimport { Brief, LetterBlock } from '@brief/domain/brief';\nimport { Diagnostic } from '@brief/domain/placeholders';\n\n/** A run of consecutive lines to render together: a list (bullet/number) or a single plain line. */\ntype PreviewSegment = {\n readonly list: 'bullet' | 'number' | null;\n readonly items: readonly Paragraph[];\n};\n\nfunction groupParagraphs(paras: readonly Paragraph[]): PreviewSegment[] {\n const out: { list: 'bullet' | 'number' | null; items: Paragraph[] }[] = [];\n for (const para of paras) {\n const kind = para.list ?? null;\n const last = out[out.length - 1];\n if (kind && last && last.list === kind) last.items.push(para);\n else out.push({ list: kind, items: [para] });\n }\n return out;\n}\n\n// Illustrative values for the \"Voorbeeld\" toggle — what send resolves server-side.\nconst SAMPLE_VALUES: Record = {\n naam_zorgverlener: 'J. Jansen',\n big_nummer: '12345678901',\n};\n\n/** Organism: read-only rendering of the letter as the approver/recipient sees it.\n Placeholders show as labelled chips (values are resolved server-side only at send);\n a chip flagged by the linter shows its error/warning state. The \"Voorbeeld\" toggle\n fills auto-resolvable placeholders with sample values to preview the sent result. */\n@Component({\n selector: 'app-letter-preview',\n imports: [NgTemplateOutlet, HeadingComponent, ButtonComponent, PlaceholderChipComponent],\n styles: [\n `\n :host {\n display: block;\n }\n .toolbar {\n display: flex;\n justify-content: flex-end;\n margin-block-end: var(--rhc-space-max-sm);\n }\n .letter {\n background: var(--rhc-color-wit);\n border: var(--rhc-border-width-sm) solid var(--rhc-color-border-default);\n border-radius: var(--rhc-border-radius-md);\n padding: var(--rhc-space-max-2xl);\n }\n section {\n margin-block-end: var(--rhc-space-max-xl);\n }\n p {\n margin: 0 0 var(--rhc-space-max-sm);\n }\n ul,\n ol {\n margin: 0 0 var(--rhc-space-max-sm);\n padding-inline-start: 1.4em;\n }\n `,\n ],\n template: `\n \n @for (node of nodes; track $index) {\n @switch (node.type) {\n @case ('text') {\n {{ node.text }}\n }\n @case ('lineBreak') {\n
\n }\n @case ('placeholder') {\n @if (showSample() && autoFor(node.key)) {\n {{ sampleFor(node.key) }}\n } @else {\n \n }\n }\n }\n }\n
\n\n
\n \n {{ showSample() ? hideSampleLabel() : showSampleLabel() }}\n \n
\n\n
\n @for (section of brief().sections; track section.sectionKey) {\n
\n {{ section.title }}\n @for (block of section.blocks; track block.blockId) {\n @for (seg of segmentsOf(block); track $index) {\n @if (seg.list === 'bullet') {\n
    \n @for (para of seg.items; track $index) {\n
  • \n \n
  • \n }\n
\n } @else if (seg.list === 'number') {\n
    \n @for (para of seg.items; track $index) {\n
  1. \n \n
  2. \n }\n
\n } @else {\n

\n \n

\n }\n }\n }\n
\n }\n
\n `,\n})\nexport class LetterPreviewComponent {\n brief = input.required();\n diagnostics = input([]);\n\n showSampleLabel = input($localize`:@@brief.preview.showSample:Voorbeeld met testwaarden`);\n hideSampleLabel = input($localize`:@@brief.preview.hideSample:Testwaarden verbergen`);\n\n protected showSample = signal(false);\n private today = new Date().toLocaleDateString('nl-NL', {\n day: 'numeric',\n month: 'long',\n year: 'numeric',\n });\n\n private defs = computed(() => new Map(this.brief().placeholders.map((p) => [p.key, p])));\n private worst = computed(() => {\n const m = new Map();\n for (const d of this.diagnostics()) {\n if (!d.placeholderKey) continue;\n if (d.severity === 'error') m.set(d.placeholderKey, 'error');\n else if (!m.has(d.placeholderKey)) m.set(d.placeholderKey, 'warning');\n }\n return m;\n });\n\n protected segmentsOf = (block: LetterBlock) => groupParagraphs(block.content.paragraphs);\n protected labelFor = (key: string) => this.defs().get(key)?.label ?? key;\n protected autoFor = (key: string) => this.defs().get(key)?.autoResolvable ?? false;\n protected stateFor = (key: string): 'ok' | 'warning' | 'error' => this.worst().get(key) ?? 'ok';\n protected sampleFor = (key: string) =>\n SAMPLE_VALUES[key] ?? (key === 'datum' ? this.today : this.labelFor(key));\n}\n", + "sourceCode": "import { Component, computed, input, signal } from '@angular/core';\nimport { NgTemplateOutlet } from '@angular/common';\nimport { HeadingComponent } from '@shared/ui/heading/heading.component';\nimport { ButtonComponent } from '@shared/ui/button/button.component';\nimport { PlaceholderChipComponent } from '@shared/ui/placeholder-chip/placeholder-chip.component';\nimport { formatDatumNl } from '@shared/kernel/datum';\nimport { Paragraph } from '@shared/kernel/rich-text';\nimport { Brief, LetterBlock } from '@brief/domain/brief';\nimport { Diagnostic } from '@brief/domain/placeholders';\n\n/** A run of consecutive lines to render together: a list (bullet/number) or a single plain line. */\ntype PreviewSegment = {\n readonly list: 'bullet' | 'number' | null;\n readonly items: readonly Paragraph[];\n};\n\nfunction groupParagraphs(paras: readonly Paragraph[]): PreviewSegment[] {\n const out: { list: 'bullet' | 'number' | null; items: Paragraph[] }[] = [];\n for (const para of paras) {\n const kind = para.list ?? null;\n const last = out[out.length - 1];\n if (kind && last && last.list === kind) last.items.push(para);\n else out.push({ list: kind, items: [para] });\n }\n return out;\n}\n\n// Illustrative values for the \"Voorbeeld\" toggle — what send resolves server-side.\nconst SAMPLE_VALUES: Record = {\n naam_zorgverlener: 'J. Jansen',\n big_nummer: '12345678901',\n};\n\n/** Organism: read-only rendering of the letter as the approver/recipient sees it.\n Placeholders show as labelled chips (values are resolved server-side only at send);\n a chip flagged by the linter shows its error/warning state. The \"Voorbeeld\" toggle\n fills auto-resolvable placeholders with sample values to preview the sent result. */\n@Component({\n selector: 'app-letter-preview',\n imports: [NgTemplateOutlet, HeadingComponent, ButtonComponent, PlaceholderChipComponent],\n styles: [\n `\n :host {\n display: block;\n }\n .toolbar {\n display: flex;\n justify-content: flex-end;\n margin-block-end: var(--rhc-space-max-sm);\n }\n .letter {\n background: var(--rhc-color-wit);\n border: var(--rhc-border-width-sm) solid var(--rhc-color-border-default);\n border-radius: var(--rhc-border-radius-md);\n padding: var(--rhc-space-max-2xl);\n }\n section {\n margin-block-end: var(--rhc-space-max-xl);\n }\n p {\n margin: 0 0 var(--rhc-space-max-sm);\n }\n ul,\n ol {\n margin: 0 0 var(--rhc-space-max-sm);\n padding-inline-start: 1.4em;\n }\n `,\n ],\n template: `\n \n @for (node of nodes; track $index) {\n @switch (node.type) {\n @case ('text') {\n {{ node.text }}\n }\n @case ('lineBreak') {\n
\n }\n @case ('placeholder') {\n @if (showSample() && autoFor(node.key)) {\n {{ sampleFor(node.key) }}\n } @else {\n \n }\n }\n }\n }\n
\n\n
\n \n {{ showSample() ? hideSampleLabel() : showSampleLabel() }}\n \n
\n\n
\n @for (section of brief().sections; track section.sectionKey) {\n
\n {{ section.title }}\n @for (block of section.blocks; track block.blockId) {\n @for (seg of segmentsOf(block); track $index) {\n @if (seg.list === 'bullet') {\n
    \n @for (para of seg.items; track $index) {\n
  • \n \n
  • \n }\n
\n } @else if (seg.list === 'number') {\n
    \n @for (para of seg.items; track $index) {\n
  1. \n \n
  2. \n }\n
\n } @else {\n

\n \n

\n }\n }\n }\n
\n }\n
\n `,\n})\nexport class LetterPreviewComponent {\n brief = input.required();\n diagnostics = input([]);\n\n showSampleLabel = input($localize`:@@brief.preview.showSample:Voorbeeld met testwaarden`);\n hideSampleLabel = input($localize`:@@brief.preview.hideSample:Testwaarden verbergen`);\n\n protected showSample = signal(false);\n private today = formatDatumNl(new Date());\n\n private defs = computed(() => new Map(this.brief().placeholders.map((p) => [p.key, p])));\n private worst = computed(() => {\n const m = new Map();\n for (const d of this.diagnostics()) {\n if (!d.placeholderKey) continue;\n if (d.severity === 'error') m.set(d.placeholderKey, 'error');\n else if (!m.has(d.placeholderKey)) m.set(d.placeholderKey, 'warning');\n }\n return m;\n });\n\n protected segmentsOf = (block: LetterBlock) => groupParagraphs(block.content.paragraphs);\n protected labelFor = (key: string) => this.defs().get(key)?.label ?? key;\n protected autoFor = (key: string) => this.defs().get(key)?.autoResolvable ?? false;\n protected stateFor = (key: string): 'ok' | 'warning' | 'error' => this.worst().get(key) ?? 'ok';\n protected sampleFor = (key: string) =>\n SAMPLE_VALUES[key] ?? (key === 'datum' ? this.today : this.labelFor(key));\n}\n", "assetsDirs": [], "styleUrlsData": "", "stylesData": "\n :host {\n display: block;\n }\n .toolbar {\n display: flex;\n justify-content: flex-end;\n margin-block-end: var(--rhc-space-max-sm);\n }\n .letter {\n background: var(--rhc-color-wit);\n border: var(--rhc-border-width-sm) solid var(--rhc-color-border-default);\n border-radius: var(--rhc-border-radius-md);\n padding: var(--rhc-space-max-2xl);\n }\n section {\n margin-block-end: var(--rhc-space-max-xl);\n }\n p {\n margin: 0 0 var(--rhc-space-max-sm);\n }\n ul,\n ol {\n margin: 0 0 var(--rhc-space-max-sm);\n padding-inline-start: 1.4em;\n }\n \n", @@ -23028,26 +23028,6 @@ "rawdescription": "Used by the shell to find what to poll on return: still-in-flight uploads.", "description": "

Used by the shell to find what to poll on return: still-in-flight uploads.

\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": "initial", "ctype": "miscellaneous", @@ -23078,6 +23058,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", @@ -23358,11 +23358,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" }, @@ -23370,11 +23370,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" }, @@ -23686,6 +23686,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", @@ -23744,35 +23773,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", @@ -24252,7 +24252,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, @@ -24281,7 +24281,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, @@ -24763,48 +24763,16 @@ ] }, { - "name": "formatNL", - "file": "src/app/registratie/domain/aanvraag-view.ts", + "name": "formatDatumNl", + "file": "src/app/shared/kernel/datum.ts", "ctype": "miscellaneous", "subtype": "function", "deprecated": false, "deprecationMessage": "", - "description": "", - "args": [ - { - "name": "iso", - "type": "string", - "deprecated": false, - "deprecationMessage": "", - "optional": true - } - ], - "returnType": "string", - "jsdoctags": [ - { - "name": "iso", - "type": "string", - "deprecated": false, - "deprecationMessage": "", - "optional": true, - "tagName": { - "text": "param" - } - } - ] - }, - { - "name": "formatNL", - "file": "src/app/registratie/domain/tasks.ts", - "ctype": "miscellaneous", - "subtype": "function", - "deprecated": false, - "deprecationMessage": "", - "description": "", + "description": "

The one hand-written date formatter for pure TS (non-template) code — a domain\nrule or a $localize string can't reach for Angular's DatePipe, so this covers\nthat gap. Templates use DatePipe (| date: 'longDate') instead; don't add a\nsecond hand-rolled formatter for either case.

\n", "args": [ { "name": "d", - "type": "Date", "deprecated": false, "deprecationMessage": "" } @@ -24813,7 +24781,6 @@ "jsdoctags": [ { "name": "d", - "type": "Date", "deprecated": false, "deprecationMessage": "", "tagName": { @@ -24822,37 +24789,6 @@ } ] }, - { - "name": "formatNL", - "file": "src/app/registratie/ui/aanvraag-block/aanvraag-block.component.ts", - "ctype": "miscellaneous", - "subtype": "function", - "deprecated": false, - "deprecationMessage": "", - "description": "", - "args": [ - { - "name": "iso", - "type": "string", - "deprecated": false, - "deprecationMessage": "", - "optional": true - } - ], - "returnType": "string", - "jsdoctags": [ - { - "name": "iso", - "type": "string", - "deprecated": false, - "deprecationMessage": "", - "optional": true, - "tagName": { - "text": "param" - } - } - ] - }, { "name": "fromResource", "file": "src/app/shared/application/remote-data.ts", @@ -24897,6 +24833,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", @@ -24983,50 +24963,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-preview/letter-preview.component.ts", @@ -25081,6 +25017,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", @@ -25139,35 +25104,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", @@ -25842,78 +25778,6 @@ } ] }, - { - "name": "map3", - "file": "src/app/shared/application/remote-data.ts", - "ctype": "miscellaneous", - "subtype": "function", - "deprecated": false, - "deprecationMessage": "", - "description": "

Combine three sources (built on map2).

\n", - "args": [ - { - "name": "a", - "type": "RemoteData", - "deprecated": false, - "deprecationMessage": "" - }, - { - "name": "b", - "type": "RemoteData", - "deprecated": false, - "deprecationMessage": "" - }, - { - "name": "c", - "type": "RemoteData", - "deprecated": false, - "deprecationMessage": "" - }, - { - "name": "f", - "deprecated": false, - "deprecationMessage": "" - } - ], - "returnType": "RemoteData", - "jsdoctags": [ - { - "name": "a", - "type": "RemoteData", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - }, - { - "name": "b", - "type": "RemoteData", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - }, - { - "name": "c", - "type": "RemoteData", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - }, - { - "name": "f", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - } - ] - }, { "name": "mapBlocks", "file": "src/app/brief/domain/brief.machine.ts", @@ -26302,6 +26166,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", @@ -26360,35 +26253,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", @@ -27474,94 +27338,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/brief/domain/brief.machine.ts", @@ -27694,6 +27470,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", @@ -27982,6 +27846,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", @@ -28070,50 +27978,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", @@ -28400,63 +28264,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", @@ -28516,6 +28323,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", @@ -28716,6 +28580,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", @@ -28774,35 +28667,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", @@ -29166,6 +29030,50 @@ } ] }, + { + "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": "upload", "file": "src/app/herregistratie/domain/herregistratie.machine.ts", @@ -29211,41 +29119,26 @@ ] }, { - "name": "upload", - "file": "src/app/registratie/domain/registratie-wizard.machine.ts", + "name": "validate", + "file": "src/app/registratie/domain/change-request.machine.ts", "ctype": "miscellaneous", "subtype": "function", "deprecated": false, "deprecationMessage": "", - "description": "

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

\n", + "description": "

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

\n", "args": [ { - "name": "s", - "type": "RegistratieState", - "deprecated": false, - "deprecationMessage": "" - }, - { - "name": "msg", - "type": "UploadMsg", + "name": "draft", + "type": "Draft", "deprecated": false, "deprecationMessage": "" } ], - "returnType": "RegistratieState", + "returnType": "Result", "jsdoctags": [ { - "name": "s", - "type": "RegistratieState", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - }, - { - "name": "msg", - "type": "UploadMsg", + "name": "draft", + "type": "Draft", "deprecated": false, "deprecationMessage": "", "tagName": { @@ -29299,31 +29192,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" + } } ] }, @@ -29372,14 +29280,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", @@ -29393,8 +29307,17 @@ "deprecationMessage": "" } ], - "returnType": "Result", + "returnType": "Result", "jsdoctags": [ + { + "name": "step", + "type": "StepId", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + }, { "name": "d", "type": "Draft", @@ -29474,65 +29397,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", @@ -29955,22 +29819,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 }, { @@ -30208,22 +30072,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 }, { @@ -30652,6 +30516,30 @@ "defaultValue": "{\n info: $localize`:@@alert.icon.info:Informatie`,\n ok: $localize`:@@alert.icon.ok:Gelukt`,\n warning: $localize`:@@alert.icon.warning:Waarschuwing`,\n error: $localize`:@@alert.icon.error:Foutmelding`,\n}" } ], + "src/app/brief/domain/brief.machine.ts": [ + { + "name": "initial", + "ctype": "miscellaneous", + "subtype": "variable", + "file": "src/app/brief/domain/brief.machine.ts", + "deprecated": false, + "deprecationMessage": "", + "type": "BriefState", + "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", @@ -30700,30 +30588,6 @@ "description": "

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

\n" } ], - "src/app/brief/domain/brief.machine.ts": [ - { - "name": "initial", - "ctype": "miscellaneous", - "subtype": "variable", - "file": "src/app/brief/domain/brief.machine.ts", - "deprecated": false, - "deprecationMessage": "", - "type": "BriefState", - "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/shared/ui/radio-group/radio-group.component.ts": [ { "name": "JA_NEE", @@ -32581,78 +32445,6 @@ } } ] - }, - { - "name": "map3", - "file": "src/app/shared/application/remote-data.ts", - "ctype": "miscellaneous", - "subtype": "function", - "deprecated": false, - "deprecationMessage": "", - "description": "

Combine three sources (built on map2).

\n", - "args": [ - { - "name": "a", - "type": "RemoteData", - "deprecated": false, - "deprecationMessage": "" - }, - { - "name": "b", - "type": "RemoteData", - "deprecated": false, - "deprecationMessage": "" - }, - { - "name": "c", - "type": "RemoteData", - "deprecated": false, - "deprecationMessage": "" - }, - { - "name": "f", - "deprecated": false, - "deprecationMessage": "" - } - ], - "returnType": "RemoteData", - "jsdoctags": [ - { - "name": "a", - "type": "RemoteData", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - }, - { - "name": "b", - "type": "RemoteData", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - }, - { - "name": "c", - "type": "RemoteData", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - }, - { - "name": "f", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - } - ] } ], "src/app/shared/kernel/fp.ts": [ @@ -32730,6 +32522,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", @@ -33652,826 +34264,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", @@ -35455,37 +35247,6 @@ } ] }, - { - "name": "formatNL", - "file": "src/app/registratie/domain/aanvraag-view.ts", - "ctype": "miscellaneous", - "subtype": "function", - "deprecated": false, - "deprecationMessage": "", - "description": "", - "args": [ - { - "name": "iso", - "type": "string", - "deprecated": false, - "deprecationMessage": "", - "optional": true - } - ], - "returnType": "string", - "jsdoctags": [ - { - "name": "iso", - "type": "string", - "deprecated": false, - "deprecationMessage": "", - "optional": true, - "tagName": { - "text": "param" - } - } - ] - }, { "name": "purposeLabel", "file": "src/app/registratie/domain/aanvraag-view.ts", @@ -35863,19 +35624,18 @@ ] } ], - "src/app/registratie/domain/tasks.ts": [ + "src/app/shared/kernel/datum.ts": [ { - "name": "formatNL", - "file": "src/app/registratie/domain/tasks.ts", + "name": "formatDatumNl", + "file": "src/app/shared/kernel/datum.ts", "ctype": "miscellaneous", "subtype": "function", "deprecated": false, "deprecationMessage": "", - "description": "", + "description": "

The one hand-written date formatter for pure TS (non-template) code — a domain\nrule or a $localize string can't reach for Angular's DatePipe, so this covers\nthat gap. Templates use DatePipe (| date: 'longDate') instead; don't add a\nsecond hand-rolled formatter for either case.

\n", "args": [ { "name": "d", - "type": "Date", "deprecated": false, "deprecationMessage": "" } @@ -35884,7 +35644,6 @@ "jsdoctags": [ { "name": "d", - "type": "Date", "deprecated": false, "deprecationMessage": "", "tagName": { @@ -35892,83 +35651,6 @@ } } ] - }, - { - "name": "tasksFromProfile", - "file": "src/app/registratie/domain/tasks.ts", - "ctype": "miscellaneous", - "subtype": "function", - "deprecated": false, - "deprecationMessage": "", - "description": "

Derive the open tasks for a professional (pure). Eligibility is the server's\ndecision (decisions.eligibleForHerregistratie), passed in — the FE renders it,\nit does not recompute the rule (ADR-0001). The deadline is still formatted\nclient-side for the task copy (presentation, not a rule).

\n", - "args": [ - { - "name": "reg", - "type": "Registration", - "deprecated": false, - "deprecationMessage": "" - }, - { - "name": "eligibleForHerregistratie", - "type": "boolean", - "deprecated": false, - "deprecationMessage": "" - } - ], - "returnType": "PortalTask[]", - "jsdoctags": [ - { - "name": "reg", - "type": "Registration", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - }, - { - "name": "eligibleForHerregistratie", - "type": "boolean", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - } - ] - } - ], - "src/app/registratie/ui/aanvraag-block/aanvraag-block.component.ts": [ - { - "name": "formatNL", - "file": "src/app/registratie/ui/aanvraag-block/aanvraag-block.component.ts", - "ctype": "miscellaneous", - "subtype": "function", - "deprecated": false, - "deprecationMessage": "", - "description": "", - "args": [ - { - "name": "iso", - "type": "string", - "deprecated": false, - "deprecationMessage": "", - "optional": true - } - ], - "returnType": "string", - "jsdoctags": [ - { - "name": "iso", - "type": "string", - "deprecated": false, - "deprecationMessage": "", - "optional": true, - "tagName": { - "text": "param" - } - } - ] } ], "src/app/brief/ui/letter-preview/letter-preview.component.ts": [ @@ -37151,6 +36833,52 @@ ] } ], + "src/app/registratie/domain/tasks.ts": [ + { + "name": "tasksFromProfile", + "file": "src/app/registratie/domain/tasks.ts", + "ctype": "miscellaneous", + "subtype": "function", + "deprecated": false, + "deprecationMessage": "", + "description": "

Derive the open tasks for a professional (pure). Eligibility is the server's\ndecision (decisions.eligibleForHerregistratie), passed in — the FE renders it,\nit does not recompute the rule (ADR-0001). The deadline is still formatted\nclient-side for the task copy (presentation, not a rule).

\n", + "args": [ + { + "name": "reg", + "type": "Registration", + "deprecated": false, + "deprecationMessage": "" + }, + { + "name": "eligibleForHerregistratie", + "type": "boolean", + "deprecated": false, + "deprecationMessage": "" + } + ], + "returnType": "PortalTask[]", + "jsdoctags": [ + { + "name": "reg", + "type": "Registration", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + }, + { + "name": "eligibleForHerregistratie", + "type": "boolean", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + } + ] + } + ], "src/app/shared/infrastructure/api-client.ts": [ { "name": "throwException", @@ -39612,16 +39340,6 @@ "coverageCount": "1/1", "status": "very-good" }, - { - "filePath": "src/app/registratie/domain/aanvraag-view.ts", - "type": "function", - "linktype": "miscellaneous", - "linksubtype": "function", - "name": "formatNL", - "coveragePercent": 0, - "coverageCount": "0/1", - "status": "low" - }, { "filePath": "src/app/registratie/domain/aanvraag-view.ts", "type": "function", @@ -40249,16 +39967,6 @@ "coverageCount": "1/5", "status": "low" }, - { - "filePath": "src/app/registratie/domain/tasks.ts", - "type": "function", - "linktype": "miscellaneous", - "linksubtype": "function", - "name": "formatNL", - "coveragePercent": 0, - "coverageCount": "0/1", - "status": "low" - }, { "filePath": "src/app/registratie/domain/tasks.ts", "type": "function", @@ -40541,16 +40249,6 @@ "coverageCount": "2/8", "status": "low" }, - { - "filePath": "src/app/registratie/ui/aanvraag-block/aanvraag-block.component.ts", - "type": "function", - "linktype": "miscellaneous", - "linksubtype": "function", - "name": "formatNL", - "coveragePercent": 0, - "coverageCount": "0/1", - "status": "low" - }, { "filePath": "src/app/registratie/ui/aanvraag-detail.page.ts", "type": "component", @@ -40750,16 +40448,6 @@ "coverageCount": "1/1", "status": "very-good" }, - { - "filePath": "src/app/shared/application/remote-data.ts", - "type": "function", - "linktype": "miscellaneous", - "linksubtype": "function", - "name": "map3", - "coveragePercent": 100, - "coverageCount": "1/1", - "status": "very-good" - }, { "filePath": "src/app/shared/application/remote-data.ts", "type": "type alias", @@ -41420,6 +41108,16 @@ "coverageCount": "0/1", "status": "low" }, + { + "filePath": "src/app/shared/kernel/datum.ts", + "type": "function", + "linktype": "miscellaneous", + "linksubtype": "function", + "name": "formatDatumNl", + "coveragePercent": 100, + "coverageCount": "1/1", + "status": "very-good" + }, { "filePath": "src/app/shared/kernel/fp.ts", "type": "function", diff --git a/src/app/brief/ui/letter-preview/letter-preview.component.ts b/src/app/brief/ui/letter-preview/letter-preview.component.ts index a5b7953..7f1b9ce 100644 --- a/src/app/brief/ui/letter-preview/letter-preview.component.ts +++ b/src/app/brief/ui/letter-preview/letter-preview.component.ts @@ -3,6 +3,7 @@ import { NgTemplateOutlet } from '@angular/common'; import { HeadingComponent } from '@shared/ui/heading/heading.component'; import { ButtonComponent } from '@shared/ui/button/button.component'; import { PlaceholderChipComponent } from '@shared/ui/placeholder-chip/placeholder-chip.component'; +import { formatDatumNl } from '@shared/kernel/datum'; import { Paragraph } from '@shared/kernel/rich-text'; import { Brief, LetterBlock } from '@brief/domain/brief'; import { Diagnostic } from '@brief/domain/placeholders'; @@ -152,11 +153,7 @@ export class LetterPreviewComponent { hideSampleLabel = input($localize`:@@brief.preview.hideSample:Testwaarden verbergen`); protected showSample = signal(false); - private today = new Date().toLocaleDateString('nl-NL', { - day: 'numeric', - month: 'long', - year: 'numeric', - }); + private today = formatDatumNl(new Date()); private defs = computed(() => new Map(this.brief().placeholders.map((p) => [p.key, p]))); private worst = computed(() => { diff --git a/src/app/registratie/application/draft-sync.spec.ts b/src/app/registratie/application/draft-sync.spec.ts new file mode 100644 index 0000000..d1a6eff --- /dev/null +++ b/src/app/registratie/application/draft-sync.spec.ts @@ -0,0 +1,99 @@ +import { ApplicationRef, signal } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { ActivatedRoute, Router } from '@angular/router'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { ApplicationsAdapter } from '@registratie/infrastructure/applications.adapter'; +import { createDraftSync, DraftSnapshot } from './draft-sync'; + +function setup(adapter: Partial) { + const navigate = vi.fn().mockResolvedValue(true); + TestBed.configureTestingModule({ + providers: [ + { provide: ApplicationsAdapter, useValue: adapter }, + { provide: Router, useValue: { navigate } }, + { provide: ActivatedRoute, useValue: { snapshot: { queryParamMap: { get: () => null } } } }, + ], + }); + const snap = signal(null); + const onResume = vi.fn(); + const draftSync = TestBed.runInInjectionContext(() => + createDraftSync({ + type: 'registratie', + snapshot: () => snap(), + onResume, + enabled: () => true, + }), + ); + TestBed.inject(ApplicationRef).tick(); // flush the effect's initial run + return { draftSync, snap, navigate, onResume }; +} + +const tick = () => TestBed.inject(ApplicationRef).tick(); + +describe('createDraftSync', () => { + beforeEach(() => vi.useFakeTimers()); + afterEach(() => vi.useRealTimers()); + + it('coalesces rapid snapshot changes into ONE debounced sync of the latest value', async () => { + const create = vi.fn().mockResolvedValue('a1'); + const syncDraft = vi.fn().mockResolvedValue(undefined); + const { snap } = setup({ create, syncDraft }); + + snap.set({ draft: { step: 1 }, stepIndex: 0, stepCount: 3, documentIds: [] }); + tick(); + snap.set({ draft: { step: 1, x: 'a' }, stepIndex: 0, stepCount: 3, documentIds: [] }); + tick(); + snap.set({ draft: { step: 1, x: 'ab' }, stepIndex: 0, stepCount: 3, documentIds: [] }); + tick(); + + // still inside the 600ms debounce window — nothing has synced yet + await vi.advanceTimersByTimeAsync(500); + expect(syncDraft).not.toHaveBeenCalled(); + + await vi.advanceTimersByTimeAsync(200); + expect(create).toHaveBeenCalledTimes(1); // one Concept created, not three + expect(syncDraft).toHaveBeenCalledTimes(1); // one sync, not three + expect(syncDraft).toHaveBeenCalledWith( + 'a1', + expect.objectContaining({ draft: { step: 1, x: 'ab' } }), // the LAST snapshot wins + ); + }); + + it('a trailing change after the debounce fires schedules its own sync', async () => { + const create = vi.fn().mockResolvedValue('a1'); + const syncDraft = vi.fn().mockResolvedValue(undefined); + const { snap } = setup({ create, syncDraft }); + + snap.set({ draft: { step: 1 }, stepIndex: 0, stepCount: 3, documentIds: [] }); + tick(); + await vi.advanceTimersByTimeAsync(600); + expect(syncDraft).toHaveBeenCalledTimes(1); + + snap.set({ draft: { step: 2 }, stepIndex: 1, stepCount: 3, documentIds: [] }); + tick(); + await vi.advanceTimersByTimeAsync(600); + expect(syncDraft).toHaveBeenCalledTimes(2); + expect(syncDraft).toHaveBeenLastCalledWith('a1', expect.objectContaining({ stepIndex: 1 })); + }); + + describe('submit', () => { + it('resolves ok with the server response on success', async () => { + const create = vi.fn().mockResolvedValue('a1'); + const submit = vi.fn().mockResolvedValue({ id: 'a1', autoApprovable: true }); + const { draftSync } = setup({ create, submit }); + + const r = await draftSync.submit({}); + expect(r).toEqual({ ok: true, value: { id: 'a1', autoApprovable: true } }); + expect(submit).toHaveBeenCalledWith('a1', {}); + }); + + it('folds a rejected submit into a Result error, never throwing', async () => { + const create = vi.fn().mockResolvedValue('a1'); + const submit = vi.fn().mockRejectedValue(new Error('boom')); + const { draftSync } = setup({ create, submit }); + + const r = await draftSync.submit({}); + expect(r.ok).toBe(false); + }); + }); +}); diff --git a/src/app/registratie/application/submit-change-request.spec.ts b/src/app/registratie/application/submit-change-request.spec.ts new file mode 100644 index 0000000..2d42bf9 --- /dev/null +++ b/src/app/registratie/application/submit-change-request.spec.ts @@ -0,0 +1,40 @@ +import { TestBed } from '@angular/core/testing'; +import { describe, it, expect } from 'vitest'; +import { Valid } from '@registratie/domain/change-request.machine'; +import { ChangeRequestAdapter } from '@registratie/infrastructure/change-request.adapter'; +import { createSubmitChangeRequest } from './submit-change-request'; +import { parsePostcode } from '@registratie/domain/value-objects/postcode'; + +const postcode = parsePostcode('2514 EA'); +if (!postcode.ok) throw new Error('fixture postcode should parse'); + +const data: Valid = { straat: 'Lange Voorhout 9', postcode: postcode.value, woonplaats: 'Den Haag' }; + +function setup(adapter: Partial) { + TestBed.configureTestingModule({ providers: [{ provide: ChangeRequestAdapter, useValue: adapter }] }); + return TestBed.runInInjectionContext(() => createSubmitChangeRequest()); +} + +describe('createSubmitChangeRequest', () => { + it('resolves ok with the referentie on success', async () => { + const submit = setup({ changeRequest: () => Promise.resolve('BIG-2026-000123') }); + const r = await submit(data); + expect(r).toEqual({ ok: true, value: 'BIG-2026-000123' }); + }); + + it('folds a rejected call into a Result error, never throwing', async () => { + const submit = setup({ + changeRequest: () => Promise.reject(new Error('network kaput')), + }); + const r = await submit(data); + expect(r.ok).toBe(false); + }); + + it('surfaces a ProblemDetails detail message when the server rejects with one', async () => { + const submit = setup({ + changeRequest: () => Promise.reject({ detail: 'Postcode komt niet overeen met de straat.' }), + }); + const r = await submit(data); + expect(r).toEqual({ ok: false, error: 'Postcode komt niet overeen met de straat.' }); + }); +}); diff --git a/src/app/registratie/domain/aanvraag-view.ts b/src/app/registratie/domain/aanvraag-view.ts index 24ec77a..f8c75d5 100644 --- a/src/app/registratie/domain/aanvraag-view.ts +++ b/src/app/registratie/domain/aanvraag-view.ts @@ -1,3 +1,4 @@ +import { formatDatumNl } from '@shared/kernel/datum'; import { Aanvraag, AanvraagStatus, AanvraagType } from './aanvraag'; /** View-model mapping for an aanvraag: type → labels, status → label, and the fields @@ -49,12 +50,6 @@ export interface AanvraagRow { status: string; } -function formatNL(iso?: string): string { - return iso - ? new Date(iso).toLocaleDateString('nl-NL', { day: 'numeric', month: 'long', year: 'numeric' }) - : ''; -} - /** Fields for a submitted aanvraag's row in the dashboard "aanvragen" list (Concept has no row — it renders as a resumable melding, see aanvraag-block). */ export function submittedRow(a: Aanvraag): AanvraagRow { @@ -63,7 +58,9 @@ export function submittedRow(a: Aanvraag): AanvraagRow { const ref = referentie(s); if (ref) parts.push($localize`:@@aanvraag.row.ref:Referentie ${ref}:ref:`); if (a.submittedAt) - parts.push($localize`:@@aanvraag.row.ingediend:ingediend op ${formatNL(a.submittedAt)}:datum:`); + parts.push( + $localize`:@@aanvraag.row.ingediend:ingediend op ${formatDatumNl(a.submittedAt)}:datum:`, + ); if (s.tag === 'InBehandeling' && s.manual) parts.push( $localize`:@@aanvraagBlock.manual:Uw aanvraag wordt handmatig beoordeeld in de backoffice.`, @@ -88,7 +85,7 @@ export function detailRows(a: Aanvraag): { key: string; value: string }[] { }, { key: $localize`:@@aanvraag.detail.ingediend:Ingediend op`, - value: a.submittedAt ? formatNL(a.submittedAt) : '—', + value: a.submittedAt ? formatDatumNl(a.submittedAt) : '—', }, ]; if (a.status.tag === 'Afgewezen') { diff --git a/src/app/registratie/domain/tasks.ts b/src/app/registratie/domain/tasks.ts index 4b30d17..31b47f0 100644 --- a/src/app/registratie/domain/tasks.ts +++ b/src/app/registratie/domain/tasks.ts @@ -1,3 +1,4 @@ +import { formatDatumNl } from '@shared/kernel/datum'; import { Registration } from './registration'; import { herregistratieDeadline } from './registration.policy'; @@ -12,10 +13,6 @@ export interface PortalTask { actionLabel: string; } -function formatNL(d: Date): string { - return d.toLocaleDateString('nl-NL', { day: 'numeric', month: 'long', year: 'numeric' }); -} - /** * Derive the open tasks for a professional (pure). Eligibility is the server's * decision (`decisions.eligibleForHerregistratie`), passed in — the FE renders it, @@ -33,7 +30,7 @@ export function tasksFromProfile( tasks.push({ title: $localize`:@@task.herregistratie.title:Vraag uw herregistratie aan`, description: deadline - ? $localize`:@@task.herregistratie.deadline:Verleng uw registratie vóór ${formatNL(deadline)}:deadline:.` + ? $localize`:@@task.herregistratie.deadline:Verleng uw registratie vóór ${formatDatumNl(deadline)}:deadline:.` : $localize`:@@task.herregistratie.nodeadline:U kunt nu uw herregistratie aanvragen.`, to: '/herregistratie', actionLabel: $localize`:@@task.herregistratie.action:Herregistratie aanvragen`, diff --git a/src/app/registratie/ui/aanvraag-block/aanvraag-block.component.ts b/src/app/registratie/ui/aanvraag-block/aanvraag-block.component.ts index 5a51ed9..f9b8ceb 100644 --- a/src/app/registratie/ui/aanvraag-block/aanvraag-block.component.ts +++ b/src/app/registratie/ui/aanvraag-block/aanvraag-block.component.ts @@ -1,4 +1,5 @@ import { Component, computed, input, output } from '@angular/core'; +import { formatDatumNl } from '@shared/kernel/datum'; import { ButtonComponent } from '@shared/ui/button/button.component'; import { AlertComponent } from '@shared/ui/alert/alert.component'; import { Aanvraag } from '@registratie/domain/aanvraag'; @@ -65,12 +66,6 @@ export class AanvraagBlockComponent { protected conceptText = computed(() => { const s = this.aanvraag().status; if (s.tag !== 'Concept') return ''; - return $localize`:@@aanvraagBlock.conceptMelding:Deze aanvraag is nog niet volledig afgerond — u bent gebleven bij stap ${s.stepIndex + 1}:stap: van ${s.stepCount}:totaal:. Rond de aanvraag af vóór ${formatNL(this.deadline())}:datum:.`; + return $localize`:@@aanvraagBlock.conceptMelding:Deze aanvraag is nog niet volledig afgerond — u bent gebleven bij stap ${s.stepIndex + 1}:stap: van ${s.stepCount}:totaal:. Rond de aanvraag af vóór ${formatDatumNl(this.deadline())}:datum:.`; }); } - -function formatNL(iso?: string): string { - return iso - ? new Date(iso).toLocaleDateString('nl-NL', { day: 'numeric', month: 'long', year: 'numeric' }) - : ''; -} diff --git a/src/app/shared/application/remote-data.ts b/src/app/shared/application/remote-data.ts index 15cb863..363fbea 100644 --- a/src/app/shared/application/remote-data.ts +++ b/src/app/shared/application/remote-data.ts @@ -71,20 +71,6 @@ export function map2( return { tag: 'Success', value: f(a.value, b.value) }; } -/** Combine three sources (built on map2). */ -export function map3( - a: RemoteData, - b: RemoteData, - c: RemoteData, - f: (a: A, b: B, c: C) => R, -): RemoteData { - return map2( - map2(a, b, (x, y) => [x, y] as const), - c, - ([x, y], z) => f(x, y, z), - ); -} - /** Chain a second source that depends on the first one's value. */ export function andThen( rd: RemoteData, diff --git a/src/app/shared/kernel/datum.spec.ts b/src/app/shared/kernel/datum.spec.ts new file mode 100644 index 0000000..58c7c48 --- /dev/null +++ b/src/app/shared/kernel/datum.spec.ts @@ -0,0 +1,22 @@ +import { describe, it, expect } from 'vitest'; +import { formatDatumNl } from './datum'; + +describe('formatDatumNl', () => { + it('formats a Date in long Dutch form', () => { + expect(formatDatumNl(new Date(2026, 6, 2))).toBe('2 juli 2026'); + }); + + it('formats an ISO string the same way', () => { + expect(formatDatumNl('2026-07-02')).toBe('2 juli 2026'); + }); + + it('is empty-safe: undefined, null, and empty string all yield the empty string', () => { + expect(formatDatumNl(undefined)).toBe(''); + expect(formatDatumNl(null)).toBe(''); + expect(formatDatumNl('')).toBe(''); + }); + + it('returns empty for an unparseable string rather than "Invalid Date"', () => { + expect(formatDatumNl('not-a-date')).toBe(''); + }); +}); diff --git a/src/app/shared/kernel/datum.ts b/src/app/shared/kernel/datum.ts new file mode 100644 index 0000000..39f4d50 --- /dev/null +++ b/src/app/shared/kernel/datum.ts @@ -0,0 +1,16 @@ +/** + * The one hand-written date formatter for pure TS (non-template) code — a domain + * rule or a `$localize` string can't reach for Angular's `DatePipe`, so this covers + * that gap. Templates use `DatePipe` (`| date: 'longDate'`) instead; don't add a + * second hand-rolled formatter for either case. + */ +export function formatDatumNl(d: Date | string | undefined | null): string { + if (!d) return ''; + const date = typeof d === 'string' ? new Date(d) : d; + if (Number.isNaN(date.getTime())) return ''; + return new Intl.DateTimeFormat('nl-NL', { + day: 'numeric', + month: 'long', + year: 'numeric', + }).format(date); +} diff --git a/src/docs/remote-data.mdx b/src/docs/remote-data.mdx index 22d3ac9..f8048f9 100644 --- a/src/docs/remote-data.mdx +++ b/src/docs/remote-data.mdx @@ -18,10 +18,10 @@ type RemoteData = { tag: 'Loading' } | { tag: 'Empty' } | { tag: 'Failure' ## Combining sources Two or more independent fetches often need to render as ONE state (e.g. a registration -call and a BRP call feeding the same page). `map`/`map2`/`map3`/`andThen` combine them -with one precedence rule: **Failure beats Loading beats Empty beats Success** — if either -source failed, the combined result is a failure; only when every source succeeded do you -get a combined value. +call and a BRP call feeding the same page). `map`/`map2`/`andThen` combine them with one +precedence rule: **Failure beats Loading beats Empty beats Success** — if either source +failed, the combined result is a failure; only when every source succeeded do you get a +combined value. ```ts map2(registration, person, (reg, p) => ({ registration: reg, person: p }));