From e5a3030dca31b876c9aa978d8e523bccf91ce8a3 Mon Sep 17 00:00:00 2001 From: Edwin van den Houdt Date: Fri, 26 Jun 2026 17:40:38 +0200 Subject: [PATCH] Add FP + Elm Architecture + atomic design learning guide A progressive teaching guide (docs/fp-tea-atomic-design.md): FP fundamentals, The Elm Architecture, and atomic design, taught Elm-then-this-app with the real store/machine/value-object code, plus four recipes and a glossary. It owns the teaching arc and cross-references ARCHITECTURE.md/ADR-0001 rather than duplicating them. Documents reality where the PRD diverged (no state-debug-view feature; per- wizard stores; reduce vs update naming) and flags the absent debug view as an open question. Adds pointer links from ARCHITECTURE.md and CLAUDE.md. Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 38 +- docs/ARCHITECTURE.md | 86 +- docs/fp-tea-atomic-design.md | 640 +++++++++ documentation.json | 2460 +++++++++++++++++++--------------- 4 files changed, 2062 insertions(+), 1162 deletions(-) create mode 100644 docs/fp-tea-atomic-design.md diff --git a/CLAUDE.md b/CLAUDE.md index 88d0d00..6223513 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,8 +1,10 @@ # CLAUDE.md -Agent guide for this repo. The *why* lives in `docs/ARCHITECTURE.md` and -`docs/architecture/0001-bff-lite-decision-dtos.md`; this file is the *rules*. -When a decision below and those docs disagree, the docs win — update this file. +Agent guide for this repo. The _why_ lives in `docs/ARCHITECTURE.md`, +`docs/architecture/0001-bff-lite-decision-dtos.md`, and the learning guide +`docs/fp-tea-atomic-design.md` (FP + The Elm Architecture + atomic design); this +file is the _rules_. When a decision below and those docs disagree, the docs win — +update this file. POC of a Dutch BIG-register self-service portal (healthcare professionals log in, view their registration, apply for re-registration). Angular 22, standalone, @@ -24,16 +26,17 @@ advisories are pinned via `package.json` `overrides`; the shipped bundle audits ## The decisions (non-negotiable working agreements) ### 1. DDD: contexts then layers, dependencies point inward + `src/app///`. Contexts: `shared`, `auth`, `registratie`, `herregistratie`, `showcase` (teaching page, not a feature). -| Layer | Job | Angular allowed? | -|---|---|---| -| `domain/` | business rules + data types | **No — pure TS.** Has `.spec.ts` | -| `application/` | coordinate state/tasks (stores, commands) | yes (signals) | -| `infrastructure/` | where data comes from (HTTP adapters) | yes (HTTP) | -| `contracts/` | wire DTOs (the FE⇄BE seam) | no | -| `ui/` | how it looks (components, pages) | yes | +| Layer | Job | Angular allowed? | +| ----------------- | ----------------------------------------- | -------------------------------- | +| `domain/` | business rules + data types | **No — pure TS.** Has `.spec.ts` | +| `application/` | coordinate state/tasks (stores, commands) | yes (signals) | +| `infrastructure/` | where data comes from (HTTP adapters) | yes (HTTP) | +| `contracts/` | wire DTOs (the FE⇄BE seam) | no | +| `ui/` | how it looks (components, pages) | yes | **Dependencies only point inward**: `ui → application → domain`; everyone may use `shared`; never the reverse. Cross-context only `herregistratie → registratie → shared`, @@ -41,6 +44,7 @@ advisories are pinned via `package.json` `overrides`; the shipped bundle audits @registratie/* @herregistratie/*`. `domain/` imports nothing from Angular. ### 2. Atomic design: folder = layer + `shared/ui` atoms → molecules → organisms; `shared/layout` templates (`shell`, `page-shell`); context `ui/` pages. Each level only uses levels below. A new page should be **composition of existing blocks** — adding building blocks is the @@ -48,6 +52,7 @@ exception, not the default. Atoms are thin wrappers over Utrecht/RHC CSS classes we own only a small typed `input()` API, the design system does the visuals. ### 3. State: make illegal states unrepresentable + Default reflex — **if you're about to add a second/third boolean to track state, model a discriminated union instead.** Three tools, all in `shared/application`: @@ -67,7 +72,7 @@ model a discriminated union instead.** Three tools, all in `shared/application`: **Derive, don't store** what you can compute — e.g. the wizard's visible steps are `visibleSteps(answers)`, not a stored field (`intake.machine.ts`). -**Side effects stay out of the reducer.** A *command* (`application/submit-*.ts`) +**Side effects stay out of the reducer.** A _command_ (`application/submit-*.ts`) does the HTTP, then dispatches a message describing the outcome. Reducer = "what the new state is"; command = "go do it, then say what happened." @@ -77,11 +82,12 @@ NgRx, no extra lib. Optimistic update pattern: `begin*` (flip pending) → `confirm*` (clear + `resource.reload()`) / `rollback*` (undo). ### 4. BFF-lite + decision DTOs (ADR-0001) + `infrastructure/` is the **only** layer that touches the network — the anti-corruption boundary. Each screen gets one screen-shaped endpoint returning a decision-enriched DTO; **the FE renders decisions, it does not recompute business -rules.** Per rule, pick: *decision flag* (server computes the boolean — e.g. -herregistratie eligibility) or *config value* (server sends threshold, FE applies +rules.** Per rule, pick: _decision flag_ (server computes the boolean — e.g. +herregistratie eligibility) or _config value_ (server sends threshold, FE applies for instant feedback, server re-validates as authority — e.g. scholing threshold). FE keeps only **format** validation, never as authority. @@ -92,6 +98,7 @@ rules stay in `domain/*.policy.ts` as reference impl + unit test, marked server- but the FE doesn't call them. ### 5. Testing + Vitest. Co-locate `*.spec.ts` next to the unit. **Domain and pure logic must have a spec** (reducers, combinators, `visibleSteps`, parsers, boundary `parse*` adapters). Test the pure function directly — no Angular TestBed for domain. UI is exercised via @@ -99,13 +106,14 @@ Storybook stories (`*.stories.ts` co-located, titled `Layer/Name`, a11y addon on not heavy component tests. ## Conventions + - Standalone components only; no NgModules. Signal inputs (`input()`), `inject()` over constructor DI (constructor only for `effect()`/template-ref injection). - Angular-native control flow `@if/@for`; native `httpResource` for fetching; `withViewTransitions()` for page transitions (header/footer have stable `view-transition-name`, excluded from the fade). - Routes: lazy `loadComponent`, persistent `ShellComponent` parent, `canActivate: - [authGuard]` on protected routes (`app.routes.ts`). +[authGuard]` on protected routes (`app.routes.ts`). - Theming is one import — `src/styles.scss` pulls the RHC palette; no hand-written theme. - Scenario toggle: `?scenario=slow|loading|empty|error` on data pages (`scenario.interceptor.ts`) to see every async state. @@ -113,11 +121,13 @@ not heavy component tests. `noPropertyAccessFromIndexSignature`, `noFallthroughCasesInSwitch`, `isolatedModules`. ## Adding a feature (recipe) + Domain first (types + pure rules + spec, no Angular) → infrastructure (adapter: `httpResource` or command returning `Result`) → application (store if shared state; union + pure reduce) → UI last (compose `shared/ui` atoms, wrap async in ``, dispatch messages). Worked example: the intake wizard (`herregistratie/`). ## Out of scope (POC, don't build unprompted) + Real auth/DigiD, real backend, i18n, NgRx, licensed Rijkshuisstijl font/logo, runtime DTO validation on every endpoint, multi-tab session sync, OpenAPI codegen. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 7d965f3..5629459 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,7 +1,7 @@ # Architecture guide A walkthrough of how this app is organised and, especially, **how state is -managed** — written for a developer who has *not* done functional programming +managed** — written for a developer who has _not_ done functional programming before. No prior FP knowledge assumed. Where we use an FP idea, we explain it in plain language first. @@ -9,6 +9,11 @@ This is a demo of a Dutch BIG-register self-service portal (a healthcare professional logs in, sees their registration, and can apply for re-registration — "herregistratie"). +> New to functional programming or The Elm Architecture? Start with the progressive +> learning guide [`fp-tea-atomic-design.md`](./fp-tea-atomic-design.md), which teaches +> the concepts (with Elm ↔ this-app examples) and the recipes; this document is the +> reference deep-dive it points back to. + --- ## 1. The big picture: three "contexts", four "layers" @@ -51,14 +56,14 @@ hierarchy. Inside a context you'll see the same four folders. They answer four different questions: -| Layer | Answers… | May import Angular? | Example here | -|------------------|---------------------------------------|---------------------|--------------| -| `domain/` | What are the business rules and data? | **No** (pure TS) | `registration.ts`, `registration.policy.ts` | -| `application/` | How do we coordinate a task / state? | Yes (signals) | `big-profile.store.ts` | -| `infrastructure/`| Where does data come from? | Yes (HTTP) | `big-register.adapter.ts`, `brp.adapter.ts` | -| `ui/` | How does it look? | Yes (components) | `dashboard.page.ts` | +| Layer | Answers… | May import Angular? | Example here | +| ----------------- | ------------------------------------- | ------------------- | ------------------------------------------- | +| `domain/` | What are the business rules and data? | **No** (pure TS) | `registration.ts`, `registration.policy.ts` | +| `application/` | How do we coordinate a task / state? | Yes (signals) | `big-profile.store.ts` | +| `infrastructure/` | Where does data come from? | Yes (HTTP) | `big-register.adapter.ts`, `brp.adapter.ts` | +| `ui/` | How does it look? | Yes (components) | `dashboard.page.ts` | -**The one rule that keeps it sane: dependencies only point *inward*.** UI may use +**The one rule that keeps it sane: dependencies only point _inward_.** UI may use application, application may use domain, everyone may use `shared`. Never the other way around. The `domain/` layer imports nothing from Angular, so the business rules are plain functions you can read and test in isolation. @@ -66,6 +71,7 @@ business rules are plain functions you can read and test in isolation. Allowed direction: `herregistratie → registratie → shared`, `auth → shared`. ### Why the `shared/` kernel is split too + - `shared/kernel/` — tiny generic helpers (no Angular). - `shared/application/` — generic state tools (RemoteData, the store). - `shared/ui/` — the atomic-design building blocks (buttons, inputs, the async renderer). These know nothing about BIG-register. @@ -86,7 +92,7 @@ better types.** Three tools do the work. ### Why not "just signals"? -You *can* track a network call with three signals — `isLoading`, `error`, `data`. The +You _can_ track a network call with three signals — `isLoading`, `error`, `data`. The problem is the **state space**: three booleans is 2³ = **8** combinations, and most are nonsense the compiler still lets you write. A single discriminated union has **exactly the 4 states that are real** — the illegal ones can't be expressed at all. @@ -114,7 +120,7 @@ graph LR class b1,b2,b3,g1,g2,g3,g4 ok; class b4,b5,b6 no; ``` -The same argument applies to forms (a `submitting` boolean that can be true *with* +The same argument applies to forms (a `submitting` boolean that can be true _with_ validation errors) and to the branching wizard (don't store "which step is next" — it can drift out of sync with the answers; **derive** it instead, see §5). Signals are still the engine underneath; we just give them types that can't lie. @@ -132,20 +138,20 @@ data = signal(null); Three signals = eight combinations, and most are nonsense (loading **and** has data **and** has an error?). You end up writing defensive `if`s everywhere. -Instead we use **one** value that is *exactly one of* four shapes +Instead we use **one** value that is _exactly one of_ four shapes (`shared/application/remote-data.ts`): ```ts type RemoteData = | { tag: 'Loading' } | { tag: 'Empty' } - | { tag: 'Failure'; error: E } // only this shape has an error - | { tag: 'Success'; value: T }; // only this shape has a value + | { tag: 'Failure'; error: E } // only this shape has an error + | { tag: 'Success'; value: T }; // only this shape has a value ``` This is called a **discriminated union** (a.k.a. "tagged union" or "sum type"): a value that is one of several labelled shapes, where the `tag` tells you which. -Notice the data lives *on* the shape — you literally cannot read `.value` unless +Notice the data lives _on_ the shape — you literally cannot read `.value` unless you're in the `Success` case, so "loaded but no data" can't be written down. To use it, you handle every case once. The `` component @@ -173,7 +179,7 @@ stateDiagram-v2 is still loading, Success only when both succeeded** — so a page renders one state, never a contradictory mix. -> **FP term:** a *pure function* is one whose output depends only on its inputs +> **FP term:** a _pure function_ is one whose output depends only on its inputs > and which changes nothing else (no network, no writing to variables outside > it). Pure functions are easy to test and reason about. We push impure things > (HTTP, timers) to the edges. @@ -189,9 +195,9 @@ both by hand means juggling two loading flags, two errors… ```ts profile = computed(() => map2( - fromResource(this.registrationRes), // RemoteData from service A - fromResource(this.personRes), // RemoteData from service B - (registration, person) => ({ registration, person }), // runs only if BOTH succeeded + fromResource(this.registrationRes), // RemoteData from service A + fromResource(this.personRes), // RemoteData from service B + (registration, person) => ({ registration, person }), // runs only if BOTH succeeded ), ); ``` @@ -205,12 +211,12 @@ when it's safe. (`map`, `map3`, `andThen` are variations on the same idea.) This is the "Elm-style" pattern. The idea in one sentence: -> **Keep all state in one value (the *Model*). The only way to change it is to -> send a *message* (*Msg*) to a pure function `update(model, msg)` that returns +> **Keep all state in one value (the _Model_). The only way to change it is to +> send a _message_ (_Msg_) to a pure function `update(model, msg)` that returns > the next Model.** -Why bother? Because to understand *every* way the screen can change, you read -*one* function. No state is mutated anywhere else. +Why bother? Because to understand _every_ way the screen can change, you read +_one_ function. No state is mutated anywhere else. ```mermaid sequenceDiagram @@ -226,7 +232,7 @@ sequenceDiagram Note over Reduce: the ONLY place state changes;
no HTTP, no timers, no mutation ``` -Side effects (HTTP) sit *outside* this loop: a command does the I/O, then `dispatch`es a +Side effects (HTTP) sit _outside_ this loop: a command does the I/O, then `dispatch`es a message describing the outcome (§2d). So the reducer stays pure and testable. The wizard (`herregistratie/domain/herregistratie.machine.ts`) is the clearest @@ -240,17 +246,23 @@ type WizardState = | { tag: 'Failed'; data: Valid; error: string }; ``` -Because `step` and `errors` exist *only* on `Editing`, and the other states +Because `step` and `errors` exist _only_ on `Editing`, and the other states carry already-validated `data`, "submitting with validation errors showing" is not expressible. The messages and the pure reducer: ```ts type WizardMsg = - | { tag: 'SetField'; key; value } | { tag: 'Next' } | { tag: 'Back' } - | { tag: 'Submit' } | { tag: 'Retry' } - | { tag: 'SubmitConfirmed' } | { tag: 'SubmitFailed'; error }; + | { tag: 'SetField'; key; value } + | { tag: 'Next' } + | { tag: 'Back' } + | { tag: 'Submit' } + | { tag: 'Retry' } + | { tag: 'SubmitConfirmed' } + | { tag: 'SubmitFailed'; error }; -function reduce(state, msg) { /* returns the next state; no side effects */ } +function reduce(state, msg) { + /* returns the next state; no side effects */ +} ``` The component (`herregistratie-wizard.component.ts`) wires it to a signal with @@ -288,10 +300,11 @@ then tell the reducer what happened."** `BigProfileStore` is marked `providedIn: 'root'`, which means Angular creates **one** instance for the whole app. Every page that injects it sees the same -signals. That single shared instance *is* our cross-page state — no extra +signals. That single shared instance _is_ our cross-page state — no extra library needed. When the user submits a herregistratie: + 1. **Optimistic:** `beginHerregistratie()` flips a `pendingHerregistratie` signal **before** the server answers. The dashboard already reads that signal, so it instantly shows "in behandeling" (in progress). The UI feels @@ -316,14 +329,15 @@ Protected routes list `canActivate: [authGuard]` in `app.routes.ts`. ## 3. "Parse, don't validate" — value objects A raw `string` could be anything. After you've checked a postcode is valid, the -*type* should remember that. So we have a `Postcode` type that can only be +_type_ should remember that. So we have a `Postcode` type that can only be created by `parsePostcode`, which returns a `Result` (success-or-error) (`registratie/domain/value-objects/`): ```ts const r = parsePostcode(userInput); -if (r.ok) save(r.value); // r.value is a Postcode — guaranteed well-formed -else showError(r.error); // r.error is the message +if (r.ok) + save(r.value); // r.value is a Postcode — guaranteed well-formed +else showError(r.error); // r.error is the message ``` Once something hands you a `Postcode`, you never re-check it. The validity is @@ -358,7 +372,7 @@ discriminated union instead. --- -## 5. Branching by *deriving*, not storing +## 5. Branching by _deriving_, not storing The intake wizard (`herregistratie/domain/intake.machine.ts`) shows the most important state-management habit: **don't store what you can derive.** Naively you'd track "which @@ -371,14 +385,14 @@ function visibleSteps(a: Answers): StepId[] { const steps: StepId[] = ['buitenland']; if (a.buitenlandGewerkt === 'ja') steps.push('buitenlandDetails'); // branch appears steps.push('uren'); - if (lageUren(a)) steps.push('scholing'); // branch appears + if (lageUren(a)) steps.push('scholing'); // branch appears steps.push('punten', 'review'); return steps; } ``` The state keeps only the raw `answers` and a numeric `cursor`; the visible step is -`visibleSteps(answers)[cursor]`. Change "buiten Nederland gewerkt?" to *ja* and the country +`visibleSteps(answers)[cursor]`. Change "buiten Nederland gewerkt?" to _ja_ and the country question simply exists; change it back and it's gone — the cursor is clamped to the new list. There's no synchronisation code to get wrong, and `visibleSteps` is a one-line unit test. Answers persist to `localStorage` (an `effect` in the component) so a reload resumes @@ -403,7 +417,7 @@ update as you type. Today the adapters read static JSON (`mock/*.json`). Because `infrastructure/` is the only layer that touches the network — the **anti-corruption boundary** — pointing the app at a -real ASP.NET API touches *only these files*. Domain, application and UI don't change. +real ASP.NET API touches _only these files_. Domain, application and UI don't change. The one concrete change per adapter: a **DTO** type matching the .NET response, a `toDomain` mapper, and a real URL. diff --git a/docs/fp-tea-atomic-design.md b/docs/fp-tea-atomic-design.md new file mode 100644 index 0000000..b38f977 --- /dev/null +++ b/docs/fp-tea-atomic-design.md @@ -0,0 +1,640 @@ +# Functional programming, The Elm Architecture & atomic design — a guide + +A **progressive learning guide** for developers who are strong programmers but have +done little or no **functional programming (FP)** in the frontend. It teaches three +ideas and shows they are one idea: + +1. **FP for the frontend** — pure functions, immutability, types that can't lie. +2. **The Elm Architecture (TEA)** — one state, one direction, one pure update. +3. **Atomic design** — small pure components composed into bigger ones. + +The claim of Part 5 is that **TEA and atomic design are the same principle at two +scales**, and this app already lives that way. + +**How to read it.** A junior can read top-to-bottom and arrive at "I can add a +feature." A senior can skim Parts 1–4 and jump to **Part 5** (FP × atomic design), +**Part 6** (why it reduces complexity), and **Part 7** (the recipes). Every term is +defined in plain words on first use and again in the **glossary** (Part 8). + +This guide is the _teaching_ layer. For the reference deep-dives it points to +[`ARCHITECTURE.md`](./ARCHITECTURE.md) and +[ADR-0001](./architecture/0001-bff-lite-decision-dtos.md) rather than repeating them. +Every code snippet below is real code from this repo, with its file path. + +--- + +## Part 1 — Why this exists (the complexity problem) + +Most UI bugs are not algorithmic. They come from **state that can lie**: + +- Two booleans that disagree — `isLoading` is `true` _and_ `data` is set. +- A screen that shows an error _and_ a success at the same time. +- A "Submit" that fires while a field is still invalid. +- A wizard whose "next step" field drifts out of sync with the answers. +- The 3am question: _"who changed this value, and when?"_ + +The root cause is the same each time: state is **scattered** across many mutable +variables, and it changes from **many places**. The number of states explodes, and +most of them are nonsense the compiler still lets you write. + +The promise of this architecture, in one line: + +> **One state. One direction. Pure logic. Predictable everything.** + +Keep all the state in a single value; change it only by sending a message to one pure +function; let the view be a function of that state; push side effects (HTTP, timers) +to the edges. The illegal states stop being reachable, the update logic becomes a unit +test with no mocks, and onboarding becomes "learn one small pattern, apply it +everywhere." The rest of this guide builds that up from first principles. + +--- + +## Part 2 — FP fundamentals for the frontend + +FP here is not category theory. It is four habits that make state predictable. Each is +shown in **Elm** (a tiny, canonical functional UI language — our teaching device) and +then in **this app's real TypeScript**. + +### 2a. Pure functions + +A **pure function**'s output depends _only_ on its inputs, and it changes nothing else +— no network, no writing to outside variables, no clock. Same input → same output, +every time. That is what makes it trivially testable (no mocks) and easy to reason +about. + +```elm +-- Elm: pure by default — there is no way to do I/O inside this +add : Int -> Int -> Int +add a b = a + b +``` + +In this app, the parsers and reducers are pure. For example +(`src/app/registratie/domain/value-objects/uren.ts`): + +```ts +export function parseUren(raw: string): Result { + const t = raw.trim(); + const n = Number(t); + if (t === '' || !Number.isInteger(n) || n < 0) { + return err('Vul een geheel aantal in (0 of meer).'); + } + return ok(n as Uren); +} +``` + +Give it `"4160"`, you always get the same `ok(4160)`. No surprises. **Why it helps:** +its unit test is one line per case and never flakes. + +### 2b. Immutability + +Never mutate a value in place; produce a **new** value instead. The spread `{ ...s, x }` +copies the old fields and overrides one. + +```elm +-- Elm: { model | count = model.count + 1 } makes a NEW record +``` + +This app's reducers always return a fresh object — e.g. +`src/app/herregistratie/domain/herregistratie.machine.ts`: + +```ts +export function setField(s: WizardState, key: keyof Draft, value: string): WizardState { + if (s.tag !== 'Editing') return s; + return { ...s, draft: { ...s.draft, [key]: value } }; +} +``` + +**Why it helps:** because old states are never overwritten, back-navigation and +"resume where you left off" are free (the previous value still exists), and Angular's +change detection can tell something changed by identity. Time-travel/replay is possible +_because_ nothing is destroyed. + +### 2c. Unidirectional data flow + +Data flows **down**; events flow **up**; there is exactly one loop. A view never reaches +sideways to mutate another component's state — it emits an event, which becomes a +message, which goes through the one update function, which produces the next state, +which flows back down. Part 3 makes this loop concrete. + +### 2d. Modelling state with types (make illegal states unrepresentable) + +Two kinds of type do most of the work: + +- **Product type** — a record that holds several things _at once_ (`interface Draft { uren; jaren; punten }`). +- **Sum type / discriminated union** — a value that is _exactly one of_ several + labelled shapes, where a `tag` says which, and **each shape carries only the data that + makes sense for it**. + +The decisive move is choosing types so that **illegal states can't be written down**. +Compare three booleans (2³ = 8 combinations, most nonsense) with one union of the 4 real +states — see [`ARCHITECTURE.md` §2a](./ARCHITECTURE.md#2a-remotedata--one-value-instead-of-three-booleans) +for the full `RemoteData` treatment and diagram. The wizard's own Model is the same +idea (`herregistratie.machine.ts`): + +```ts +export type WizardState = + | { tag: 'Editing'; step: 1 | 2; draft: Draft; errors: Partial> } + | { tag: 'Submitting'; data: Valid } + | { tag: 'Submitted'; data: Valid } + | { tag: 'Failed'; data: Valid; error: string }; +``` + +Because `step` and `errors` exist **only** on `Editing`, and `Submitting`/`Submitted` +carry already-validated `Valid` data and _no_ error field, "submitting while a field is +invalid" or "success screen with errors still set" cannot be constructed. The bug class +is gone at compile time. + +> **FP term — sum type / discriminated union:** one value that is one-of-several +> labelled shapes. The `tag` discriminates; the compiler then knows which fields exist. + +### 2e. Side effects at the edges (functional core, imperative shell) + +A **side effect** is anything beyond computing a return value: HTTP, timers, +`localStorage`, focus. Pure code can't do them. So we keep a **pure core** (parsers, +reducers, `visibleSteps`) and push every effect to a thin **imperative shell** (the +Angular component/service). The core decides _what the state is_; the shell _goes and +does things_, then feeds the result back in as a message. Part 4d shows exactly how. + +> **FP term — pure core / imperative shell:** all decisions in pure functions; all I/O +> in a thin outer layer that calls them. + +--- + +## Part 3 — The Elm Architecture (TEA) + +TEA is four pieces and one loop. In Elm: + +- **Model** — the single source of truth (all your state, one value). +- **Msg** — every thing that can happen, as a union. +- **`update : Msg -> Model -> Model`** — the _only_ place state changes; pure. +- **`view : Model -> Html Msg`** — a pure function of the state that emits messages. + +```elm +type alias Model = { count : Int } + +type Msg = Increment | Decrement + +update : Msg -> Model -> Model +update msg model = + case msg of + Increment -> { model | count = model.count + 1 } + Decrement -> { model | count = model.count - 1 } + +view : Model -> Html Msg +view model = + div [] + [ button [ onClick Decrement ] [ text "-" ] + , text (String.fromInt model.count) + , button [ onClick Increment ] [ text "+" ] + ] +``` + +The runtime wires it into a loop: + +```mermaid +graph LR + S["Model (state)"] --> V["view(Model)"] + V -->|"user event"| M["Msg"] + M --> U["update(Msg, Model) — PURE"] + U -->|"next Model"| S + classDef l fill:#e5f1fb,stroke:#007bc7,color:#00567d; + class S,V,M,U l; +``` + +**Effects** don't break the loop. In Elm, `update` can return a `Cmd` (a _description_ +of an effect — "go do this HTTP call"); the runtime performs it and feeds the result +back in as another `Msg`. **Subscriptions** are the same for incoming events (time, +websockets). The key property survives: `update` itself stays pure — it only ever +_describes_ effects, never performs them. This app does the same with a small twist +(Part 4d): the effect lives in the component, and its outcome is dispatched as a `Msg`. + +--- + +## Part 4 — How we do TEA in Angular with signals + +This app implements TEA with Angular **signals**. There is no extra state library. One +important shape difference from textbook Elm: **state is per-wizard, not one global +Model** — each flow (`herregistratie`, `intake`, `registratie`) has its own little +store. Cross-page state that _must_ be shared lives in one root singleton +(`BigProfileStore`, see [`ARCHITECTURE.md` §2e](./ARCHITECTURE.md#2e-optimistic-update--rollback-and-shared-state-across-pages)). + +### 4a. The store — TEA's runtime in ~10 lines + +`src/app/shared/application/store.ts`: + +```ts +export interface Store { + /** The current state, as a read-only Angular signal. */ + readonly model: Signal; + /** Send a message; the model becomes update(model, msg). */ + dispatch(msg: Msg): void; +} + +export function createStore( + init: Model, + update: (model: Model, msg: Msg) => Model, +): Store { + const model = signal(init); + return { + model: model.asReadonly(), + dispatch: (msg) => model.set(update(model(), msg)), + }; +} +``` + +This _is_ the Elm runtime: a `signal` holds the Model, and `dispatch` is the only way +to change it — it runs the pure `update` and `set`s the new value. + +> **Naming note (read the code, not the textbook):** the factory parameter is called +> `update` (the Elm word), but each feature exports its reducer as **`reduce`** and +> passes it in: `createStore(initial, reduce)`. "update" and "reduce" are the same role. + +### 4b. Model + Msg + reduce + +Mapping the four TEA pieces to real code, using the herregistratie wizard (the smallest +machine) as the example — `src/app/herregistratie/domain/herregistratie.machine.ts`: + +- **Model** → `WizardState` (the discriminated union from §2d). +- **Msg** → `WizardMsg`, every event as one union: + +```ts +export type WizardMsg = + | { tag: 'SetField'; key: keyof Draft; value: string } + | { tag: 'Next' } + | { tag: 'Back' } + | { tag: 'Submit' } + | { tag: 'Retry' } + | { tag: 'SubmitConfirmed' } + | { tag: 'SubmitFailed'; error: string } + | { tag: 'Seed'; state: WizardState }; // mount a specific state (stories/showcase) +``` + +- **update** → the pure `reduce(state, msg)` — no injection, no HTTP, no mutation: + +```ts +export function reduce(s: WizardState, m: WizardMsg): WizardState { + switch (m.tag) { + case 'SetField': + return setField(s, m.key, m.value); + case 'Next': + return next(s); + case 'Back': + return back(s); + case 'Submit': + return submit(s); + case 'Retry': + return s.tag === 'Failed' ? { tag: 'Submitting', data: s.data } : s; + case 'SubmitConfirmed': + return s.tag === 'Submitting' ? { tag: 'Submitted', data: s.data } : s; + case 'SubmitFailed': + return s.tag === 'Submitting' ? { tag: 'Failed', data: s.data, error: m.error } : s; + case 'Seed': + return m.state; + default: + return assertNever(m); // compiler error if a Msg is unhandled + } +} +``` + +`assertNever` (`src/app/shared/kernel/fp.ts`) makes the switch **exhaustive**: add a new +`Msg` variant and forget to handle it, and the build fails. (`intake.machine.ts` and +`registratie-wizard.machine.ts` have larger unions, same exact shape.) + +### 4c. view → template + `computed()` + `dispatch` + +The container component (`src/app/herregistratie/ui/herregistratie-wizard/herregistratie-wizard.component.ts`) +creates the store and derives view values with `computed()`: + +```ts +private store = createStore(initial, reduce); +readonly state = this.store.model; // a read-only signal of the Model +protected dispatch = this.store.dispatch; // the only way to change it + +private editing = computed(() => (this.state().tag === 'Editing' ? (this.state() as Extract) : null)); +protected step = computed(() => this.editing()?.step ?? 1); +protected draft = computed(() => this.editing()?.draft ?? { uren: '', jaren: '', punten: '' }); +protected errUren = computed(() => this.editing()?.errors.uren ?? ''); +``` + +The template is a **function of the state**: it reads those `computed()` signals and +sends messages on events — it never mutates: + +```html + +... +Vorige +``` + +That is the loop: `state → template → event → dispatch(Msg) → reduce → new state → +template`. + +### 4d. Effects → a command that dispatches the outcome + +`reduce` is pure, so it can't call the network. The component holds a small **command** +method. It does the impure work, then dispatches a `Msg` describing what happened — the +result re-enters through the same pure loop: + +```ts +private async runIfSubmitting() { + const s = this.state(); + if (s.tag !== 'Submitting') return; + this.profile.beginHerregistratie(); // optimistic flag (shared store) + const r = await submitHerregistratie(s.data); // the actual I/O — a Result + if (r.ok) { this.dispatch({ tag: 'SubmitConfirmed' }); this.profile.confirmHerregistratie(); } + else { this.dispatch({ tag: 'SubmitFailed', error: r.error }); this.profile.rollbackHerregistratie(); } +} +``` + +The command itself (`src/app/herregistratie/application/submit-herregistratie.ts`) +returns a `Result` — success-or-error as a value, never a thrown exception: + +```ts +export async function submitHerregistratie(data: Valid): Promise> { + await new Promise((r) => setTimeout(r, 800)); + if (data.uren === 0) return err('Aanvraag afgewezen: geen gewerkte uren geregistreerd.'); + return ok(undefined); +} +``` + +The split, in one line: **reducer = "what the new state is"; command = "go do the +thing, then say what happened."** Incoming effects (an arriving HTTP value, a +server-owned config) are wired with `effect()` and `untracked()` so the dispatch +doesn't loop on its own write — see the BRP prefill and policy-threshold effects in +`registratie-wizard.component.ts` / `intake-wizard.component.ts`. + +### 4e. Because state is one value, you can watch it + +Each wizard exposes `state` as a **read-only signal**, deliberately public so the +teaching page can highlight the live state. See it on the in-app showcase +(`src/app/showcase/concepts.page.ts`, route `/concepts`): section 4 lights up the +current `WizardState` among `Editing → Submitting → Submitted/Failed` as you drive the +form, and section 5 shows the intake steps re-deriving as you type. + +> **Discrepancy with the PRD — open question.** The PRD refers to a dedicated "state +> debug view" / inspector. **No such feature exists** in the code today. What exists is +> the `/concepts` showcase (live state highlight) and the `?scenario=slow|loading|empty|error` +> interceptor (`src/app/shared/infrastructure/scenario.ts`) for exercising async states. +> A JSON state inspector _would be trivial here_ — single one-way state means you could +> render `JSON.stringify(state())` in a panel and watch every transition — precisely +> because of everything in Part 6. Treat building one as a future task, not documented +> reality. + +--- + +## Part 5 — FP × atomic design (the unifying chapter) + +The central idea of this guide: + +> **FP and atomic design are the same principle at two scales — composition of pure +> pieces, with state pushed to the boundary.** + +### 5a. Atoms & molecules _are_ view functions + +A pure function maps inputs → output with no side effects. A **presentational +component** does exactly that: it maps **inputs → DOM**, emits events, and has **no +injected services, no internal mutable state, no effects**. Same inputs → same DOM. +That is referential transparency at the component scale. + +In this codebase the form **atoms** (`text-input`, `radio-group`) are thin wrappers over +the design system. They take config via `input()` and — because they implement +Angular's `ControlValueAccessor` — emit changes through `[ngModel]` / `(ngModelChange)`. +The `form-field` **molecule** composes a label + projected control + error. The +`address-fields` **organism** (`src/app/registratie/ui/address-fields/address-fields.component.ts`) +composes three `form-field`s and emits with `output()`: + +```ts +export class AddressFieldsComponent { + value = input.required(); // data in + errors = input({}); // data in + fieldChange = output<{ key: keyof AdresValue; value: string }>(); // events out +} +``` + +> **Read the code, not the slogan:** "events up" has two real forms here. Design-system +> form atoms emit via `ControlValueAccessor`/`ngModel`; higher composites use `output()`. +> Both are "data down, events up" — just different Angular mechanisms. + +### 5b. Composition stays pure + +Molecules compose atoms; organisms compose molecules — exactly like composing pure +functions, where the composite is still pure. `address-fields` is pure because the +`form-field` and `text-input` it's built from are pure. Each atomic level only uses the +level(s) below it (see the hierarchy diagram in +[`ARCHITECTURE.md` §1](./ARCHITECTURE.md#1-the-big-picture-three-contexts-four-layers)). + +### 5c. Pages / containers are the TEA runtime (the shell) + +The boundary between "pure presentational" and "stateful container" **is** the +functional-core / imperative-shell line from §2e. The container (e.g. +`herregistratie-wizard.component.ts`) is where the Model signal lives, where +`dispatch`/`reduce` run, and where effects are wired. Everything below it is pure view. + +### 5d. The loop, overlaid on the atomic layers + +```mermaid +graph TD + subgraph shell["Container = TEA runtime (imperative shell)"] + ST["Model signal + dispatch + reduce + effects"] + end + subgraph pure["Pure presentational (functional core)"] + O["Organism (e.g. address-fields)"] + M["Molecules (form-field, async)"] + A["Atoms (text-input, radio-group, button)"] + end + ST -->|"state flows DOWN as inputs"| O + O --> M --> A + A -.->|"events flow UP (output / ngModelChange)"| O + O -.->|"(fieldChange, click)"| ST + ST -->|"dispatch(Msg) → reduce → new state"| ST + classDef l fill:#e5f1fb,stroke:#007bc7,color:#00567d; + classDef c fill:#e8f5e9,stroke:#39870c,color:#1b5e20; + class ST l; class O,M,A c; +``` + +It is the identical Elm loop of Part 3 — just composed through the atomic hierarchy. + +--- + +## Part 6 — How this reduces complexity (concrete payoffs) + +Each property maps to a tangible benefit you can point at in this repo: + +- **Single source of truth.** All wizard state is one `WizardState` value. To learn + every way the screen can change, you read **one** function (`reduce`). There is no + "who else mutates this?" + +- **Pure, exhaustive `reduce`.** It's a unit test with **no mocks** — pass a state and a + msg, assert the next state (`herregistratie.machine.spec.ts`). `assertNever` makes the + compiler reject an unhandled `Msg`, so adding an event can't silently do nothing. + +- **Illegal states won't compile.** `Submitting` carries `Valid` data and has no `errors` + field, so "submit with errors showing" is unwritable. A whole bug class disappears + before runtime — contrast the 8-state boolean soup in + [`ARCHITECTURE.md` §2a](./ARCHITECTURE.md#2a-remotedata--one-value-instead-of-three-booleans). + +- **Pure presentational components.** `address-fields` is tested by inputs → DOM and + reused in two call-sites (the registratie wizard and the change-request form) with no + hidden state to surprise you. + +- **Isolated effects.** Every async path is a `submit-*` command returning a `Result`, + invoked from one `runIf*` method. Async reasoning happens in one place, not sprinkled + through the view. + +- **Debuggability.** One value flowing one way means you can render and watch it (§4e) — + reproduction is "set the Model to this," nothing more. + +- **Onboarding.** It's one small pattern repeated everywhere. Learn it once; the recipes + below are that pattern written down for the four common tasks. + +--- + +## Part 7 — Recipes + +Each recipe follows the existing pattern and naming, and ends with the same reminder: +**this is the same loop, again.** + +### Recipe A — Add an atomic component (atom / molecule / organism) + +**When:** you genuinely need a new building block (not a one-off; reuse must earn it — +see [CLAUDE.md §2](../CLAUDE.md)). + +**Where:** `shared/ui/` if generic; a context's `ui/` if domain-specific. Pick the level +by composition: composes nothing → **atom**; composes atoms → **molecule**; composes +molecules into a domain block → **organism**. + +**Steps:** build it **pure/presentational** — `input()`s for data/config, `output()`s +for events, `computed()` for derived display; **no inject, no state, no effects**. Theme +only with design tokens (no hardcoded hex — CI checks via `npm run check:tokens`). +Add a co-located `*.stories.ts` titled `Layer/Name`. + +```ts +// shape — see src/app/registratie/ui/address-fields/address-fields.component.ts +export class AddressFieldsComponent { + value = input.required(); + errors = input({}); + fieldChange = output<{ key: keyof AdresValue; value: string }>(); +} +``` + +**Tests:** Storybook stories + the a11y addon are the UI coverage (repo convention — no +component DOM tests; pure logic gets a `.spec.ts`, presentational components don't). + +_This is the same loop, again: data down via `input()`, events up via `output()`._ + +### Recipe B — Add state + a state update (Elm-style) + +**When:** a new thing can happen to a feature's state. + +**Steps:** extend the `Model` immutably; add a `Msg` variant; handle it in the pure +`reduce` returning a **new** model (keep the union exhaustive — `assertNever` guards +you); expose derived values with `computed()`; `dispatch` the `Msg` where the event +originates. Keep effects **out** of `reduce`. + +```ts +// 1. Msg variant (herregistratie.machine.ts) +| { tag: 'Reset' } +// 2. reduce arm +case 'Reset': return initial; +// 3. dispatch from the view +(click)="dispatch({ tag: 'Reset' })" +``` + +**Tests:** `reduce(model, msg)` → expected model. Pure, no mocks +(`herregistratie.machine.spec.ts`). + +_This is the same loop, again._ + +### Recipe C — Add a field + a validation rule + +**When:** the form needs a new input with its own rule. + +**Steps:** combine A and B. Add the field to the `Draft`; add/extend a `SetField`-style +`Msg`; handle it in `reduce`. Write validation as a **pure** function returning `Result` +(model it on `parseUren` / `parsePostcode`). **Derive** the error/validity with +`computed()` — don't store what you can compute. Render with the `form-field` molecule + +a field atom, and wire validity into the step/submit gating via a `computed()`. + +```ts +// pure rule (value-objects/) — Result +export function parsePostcode(raw: string): Result { + /* ... */ +} +// reduce uses it; the view shows the message via +``` + +**Tests:** the parser's `.spec.ts` (each accept/reject case) + a `reduce` spec for the +new field. + +_This is the same loop, again — the rule is just another pure function._ + +### Recipe D — Add a wizard step + +**When:** a flow needs another step. + +**Steps:** compose A–C. Model the step's state in the Model; **derive** the visible steps +rather than storing "next" — copy `visibleSteps(answers)` from +`src/app/herregistratie/domain/intake.machine.ts`: + +```ts +export function visibleSteps(a: Answers): StepId[] { + const steps: StepId[] = ['buitenland']; + if (a.buitenlandGewerkt === 'ja') steps.push('buitenlandDetails'); + steps.push('uren'); + if (lageUren(a)) steps.push('scholing'); + steps.push('punten', 'review'); + return steps; +} +``` + +Build the step as a presentational component (Recipe A), composed from atoms/molecules. +Derive "can advance" from a `computed()` over the step's validity; back-navigation keeps +earlier answers for free (immutability). Effects (BRP/DUO/submit) go through `submit-*` +commands in the shell, with results dispatched as `Msg`s (Part 4d). + +**Tests:** `reduce` specs for the step's messages; a story for the step component; a +`visibleSteps`/machine spec as the acceptance check (`intake.machine.spec.ts`). + +_This is the same loop, again — now nested inside the wizard._ + +--- + +## Part 8 — Glossary + +- **Pure function** — output depends only on inputs; no side effects. Trivial to test. +- **Immutability** — never change a value in place; produce a new one (`{ ...s, x }`). +- **Side effect** — anything beyond returning a value: HTTP, timers, `localStorage`, focus. +- **Unidirectional data flow** — data down (inputs), events up (outputs); one loop. +- **Product type** — a record holding several values at once (`interface Draft { ... }`). +- **Sum type / discriminated (tagged) union** — a value that is exactly one of several + labelled shapes; a `tag` field says which, and each shape carries only its own data. +- **Make illegal states unrepresentable** — choose types so nonsense states can't be written. +- **`Model`** — the single value holding all of a feature's state. +- **`Msg`** — a union of every event that can happen to the state. +- **`update` / `reduce`** — the one pure function mapping `(Model, Msg) → next Model`. + (This codebase calls the factory parameter `update`; features export it as `reduce`.) +- **`dispatch`** — send a `Msg`; the store runs `reduce` and updates the signal. +- **Command** — an impure function that does I/O and returns a `Result`, after which the + caller dispatches a `Msg` with the outcome. +- **`Result`** — success-or-error as a value: `{ ok: true, value }` or `{ ok: false, error }`. +- **Value object / `Brand`** — a type whose validity is guaranteed by its parser + (e.g. `Postcode`); a `Brand` is only mintable through `parsePostcode`. +- **`signal`** — Angular's reactive container for a value. +- **`computed`** — a derived signal; recomputes automatically when its inputs change. +- **Presentational (pure) component** — inputs in, events out, `computed()` for display; + no inject, no state, no effects. A view function. +- **Container component** — holds the Model signal, runs `dispatch`/`reduce`, wires + effects. The TEA runtime / imperative shell. +- **Functional core / imperative shell** — all decisions in pure functions; all I/O in a + thin outer layer. +- **TEA (The Elm Architecture)** — Model / Msg / update / view + the one-directional loop. + +--- + +_See also:_ [`ARCHITECTURE.md`](./ARCHITECTURE.md) (reference deep-dive on RemoteData, +the store, parse-don't-validate, and the .NET backend seam) and +[ADR-0001](./architecture/0001-bff-lite-decision-dtos.md) (the BFF-lite + decision-DTO +decision). Live demo: `/concepts` in the running app. diff --git a/documentation.json b/documentation.json index 6707f24..5bd8843 100644 --- a/documentation.json +++ b/documentation.json @@ -93,6 +93,51 @@ "methods": [], "extends": [] }, + { + "name": "AdresValue", + "id": "interface-AdresValue-1e0b1a46198e647fedabf1bd8f892a2cc7444c7741a9a2454ce1ba38bb7d7b7787621fd7c35778bb545b72b34d355ed53f3a644b2eab266a4a981469c6ea70ba", + "file": "src/app/registratie/ui/address-fields/address-fields.component.ts", + "deprecated": false, + "deprecationMessage": "", + "type": "interface", + "sourceCode": "import { Component, input, output } from '@angular/core';\nimport { FormsModule } from '@angular/forms';\nimport { FormFieldComponent } from '@shared/ui/form-field/form-field.component';\nimport { TextInputComponent } from '@shared/ui/text-input/text-input.component';\n\nexport interface AdresValue {\n straat: string;\n postcode: string;\n woonplaats: string;\n}\nexport type AdresErrors = Partial>;\n\n/** Organism: the editable address block (straat / postcode / woonplaats), grouped\n in a fieldset/legend. Pure & presentational — values in via `value`, errors in\n via `errors`, every keystroke out via `fieldChange`. No store, no services, no\n internal state; the container owns the Model and decides what a change means\n (registratie-wizard dispatches SetField; change-request-form sets its props).\n Composes the form-field molecule (×3) so labels/error wiring stay consistent. */\n@Component({\n selector: 'app-address-fields',\n imports: [FormsModule, FormFieldComponent, TextInputComponent],\n template: `\n
\n {{ legend() }}\n \n \n \n \n \n \n \n \n \n
\n `,\n})\nexport class AddressFieldsComponent {\n value = input.required();\n errors = input({});\n /** Prefix for field ids/labels — keeps them unique if two blocks ever co-exist. */\n idPrefix = input('adres');\n legend = input('Adres');\n fieldChange = output<{ key: keyof AdresValue; value: string }>();\n}\n", + "properties": [ + { + "name": "postcode", + "deprecated": false, + "deprecationMessage": "", + "type": "string", + "indexKey": "", + "optional": false, + "description": "", + "line": 8 + }, + { + "name": "straat", + "deprecated": false, + "deprecationMessage": "", + "type": "string", + "indexKey": "", + "optional": false, + "description": "", + "line": 7 + }, + { + "name": "woonplaats", + "deprecated": false, + "deprecationMessage": "", + "type": "string", + "indexKey": "", + "optional": false, + "description": "", + "line": 9 + } + ], + "indexSignatures": [], + "kind": 172, + "methods": [], + "extends": [] + }, { "name": "Answers", "id": "interface-Answers-13704a2a04ab3cffa46cbc109ee84699b236928e1c2649f459e18e4daf323047e8b21cdd5a21440c5fd696a605a09feb673b4020394ba7954ae35162f1bd2732", @@ -281,12 +326,12 @@ }, { "name": "ChangeRequest", - "id": "interface-ChangeRequest-8d79b6a9ce75f62f4e04f9d3ed0867e00d005c9c05cf267503536c4eec9813372aff51a3c8686927fd07f46503b60ddb4107edcc5f57fdfa1b6b52fe2a98026c", + "id": "interface-ChangeRequest-38a13207972e7e101ba989ca2fe390d3a6f353165e03bc0434fe9785b321fba0e953ae0b969ca459acee20e21173dcb190fb46590517af703b89243b5dd45aba", "file": "src/app/registratie/ui/change-request-form/change-request-form.component.ts", "deprecated": false, "deprecationMessage": "", "type": "interface", - "sourceCode": "import { Component, output, signal } from '@angular/core';\nimport { FormsModule } from '@angular/forms';\nimport { FormFieldComponent } from '@shared/ui/form-field/form-field.component';\nimport { TextInputComponent } from '@shared/ui/text-input/text-input.component';\nimport { ButtonComponent } from '@shared/ui/button/button.component';\nimport { HeadingComponent } from '@shared/ui/heading/heading.component';\nimport { Postcode, parsePostcode } from '@registratie/domain/value-objects/postcode';\n\n/** A submitted change request carries a *parsed* postcode (branded Postcode),\n not a raw string — downstream code can't receive an unvalidated one. */\nexport interface ChangeRequest {\n street: string;\n zip: Postcode;\n city: string;\n}\n\n/** Organism: change-request (adreswijziging) form. Reuses the same form-field\n molecule + text-input/button atoms as the login form. Field errors come\n straight from the parser's Result — no parallel \"is it valid\" flag to drift. */\n@Component({\n selector: 'app-change-request-form',\n imports: [FormsModule, FormFieldComponent, TextInputComponent, ButtonComponent, HeadingComponent],\n template: `\n Adreswijziging doorgeven\n
\n \n \n \n \n \n \n \n \n \n
\n Wijziging indienen\n
\n
\n `,\n})\nexport class ChangeRequestFormComponent {\n street = '';\n zip = '';\n city = '';\n streetError = signal('');\n zipError = signal('');\n submitted = output();\n\n onSubmit() {\n const street = this.street.trim();\n this.streetError.set(street ? '' : 'Vul straat en huisnummer in.');\n\n const postcode = parsePostcode(this.zip);\n this.zipError.set(postcode.ok ? '' : postcode.error);\n\n if (!street || !postcode.ok) return;\n this.submitted.emit({ street, zip: postcode.value, city: this.city.trim() });\n }\n}\n", + "sourceCode": "import { Component, output, signal } from '@angular/core';\nimport { FormsModule } from '@angular/forms';\nimport { ButtonComponent } from '@shared/ui/button/button.component';\nimport { HeadingComponent } from '@shared/ui/heading/heading.component';\nimport { AddressFieldsComponent, AdresValue } from '@registratie/ui/address-fields/address-fields.component';\nimport { Postcode, parsePostcode } from '@registratie/domain/value-objects/postcode';\n\n/** A submitted change request carries a *parsed* postcode (branded Postcode),\n not a raw string — downstream code can't receive an unvalidated one. */\nexport interface ChangeRequest {\n street: string;\n zip: Postcode;\n city: string;\n}\n\n/** Organism: change-request (adreswijziging) form. Reuses the same form-field\n molecule + text-input/button atoms as the login form. Field errors come\n straight from the parser's Result — no parallel \"is it valid\" flag to drift. */\n@Component({\n selector: 'app-change-request-form',\n imports: [FormsModule, ButtonComponent, HeadingComponent, AddressFieldsComponent],\n template: `\n Adreswijziging doorgeven\n
\n \n
\n Wijziging indienen\n
\n \n `,\n})\nexport class ChangeRequestFormComponent {\n street = '';\n zip = '';\n city = '';\n streetError = signal('');\n zipError = signal('');\n submitted = output();\n\n onAdres(e: { key: keyof AdresValue; value: string }) {\n if (e.key === 'straat') this.street = e.value;\n else if (e.key === 'postcode') this.zip = e.value;\n else this.city = e.value;\n }\n\n onSubmit() {\n const street = this.street.trim();\n this.streetError.set(street ? '' : 'Vul straat en huisnummer in.');\n\n const postcode = parsePostcode(this.zip);\n this.zipError.set(postcode.ok ? '' : postcode.error);\n\n if (!street || !postcode.ok) return;\n this.submitted.emit({ street, zip: postcode.value, city: this.city.trim() });\n }\n}\n", "properties": [ { "name": "city", @@ -296,7 +341,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 14 + "line": 13 }, { "name": "street", @@ -306,7 +351,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 12 + "line": 11 }, { "name": "zip", @@ -316,7 +361,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 13 + "line": 12 } ], "indexSignatures": [], @@ -412,54 +457,7 @@ }, { "name": "Draft", - "id": "interface-Draft-097a742aa05bc7bbfee387dd3ea5b2ac382815db8ca18107e699d11e6da110cdeae2eb377bcea284f415cfd263937c3b0f6e7ed2ee9090d660d898dddefe3a4f", - "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';\n\n/** What the user is typing (raw, possibly invalid). */\nexport interface Draft {\n uren: string;\n jaren: string;\n punten: string;\n}\n\n/** What we have AFTER parsing — branded/typed, guaranteed valid. */\nexport interface Valid {\n uren: Uren;\n jaren: number;\n punten: number;\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; draft: Draft; errors: Partial> }\n | { tag: 'Submitting'; data: Valid }\n | { tag: 'Submitted'; data: Valid }\n | { tag: 'Failed'; data: Valid; error: string };\n\nexport const initial: WizardState = { tag: 'Editing', step: 1, draft: { uren: '', jaren: '', punten: '' }, errors: {} };\n\n/** Parse every field; on success hand back a Valid, else the per-field errors. */\nfunction validate(draft: Draft): Result>, Valid> {\n const uren = parseUren(draft.uren);\n const jaren = parseUren(draft.jaren);\n const punten = parseUren(draft.punten);\n const errors: Partial> = {};\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 (uren.ok && jaren.ok && punten.ok) {\n return { ok: true, value: { uren: uren.value, jaren: jaren.value, punten: punten.value } };\n }\n return { ok: false, error: errors };\n}\n\n/** Step 1 → 2: advance only when BOTH step-1 fields parse. Illegal elsewhere = no-op. */\nexport function next(s: WizardState): WizardState {\n if (s.tag !== 'Editing' || s.step !== 1) return s;\n const uren = parseUren(s.draft.uren);\n const jaren = parseUren(s.draft.jaren);\n const errors: Partial> = {};\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\nexport function back(s: WizardState): WizardState {\n if (s.tag !== 'Editing' || s.step !== 2) return s;\n return { ...s, step: 1, errors: {} };\n}\n\n/** Step 2 submit: parse everything; move to Submitting only with Valid data. */\nexport function submit(s: WizardState): WizardState {\n if (s.tag !== 'Editing' || s.step !== 2) return s;\n const result = validate(s.draft);\n return result.ok ? { tag: 'Submitting', data: result.value } : { ...s, errors: result.error };\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 ? { tag: 'Submitted', data: s.data } : { 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: 'Submit' }\n | { tag: 'Retry' }\n | { tag: 'SubmitConfirmed' }\n | { tag: 'SubmitFailed'; error: string }\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 '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 '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": 7 - }, - { - "name": "punten", - "deprecated": false, - "deprecationMessage": "", - "type": "string", - "indexKey": "", - "optional": false, - "description": "", - "line": 8 - }, - { - "name": "uren", - "deprecated": false, - "deprecationMessage": "", - "type": "string", - "indexKey": "", - "optional": false, - "description": "", - "line": 6 - } - ], - "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-6ed41f6a28b6c00dc907f54219be9c1d7955aa11bc22f128620d2206b1ea12e6730533ac33f5f5df7b8d7c8d7f977584b3a0a3e98a6c2c3b91d1afe6411278e2-1", + "id": "interface-Draft-6ed41f6a28b6c00dc907f54219be9c1d7955aa11bc22f128620d2206b1ea12e6730533ac33f5f5df7b8d7c8d7f977584b3a0a3e98a6c2c3b91d1afe6411278e2", "file": "src/app/registratie/domain/registratie-wizard.machine.ts", "deprecated": false, "deprecationMessage": "", @@ -582,6 +580,53 @@ "description": "

One record carried across every step (and persisted). All optional: the user\nfills it in gradually. Adres fields are kept flat so one SetField message\nserves them all (mirrors the intake machine).

\n", "rawdescription": "\nOne record carried across every step (and persisted). All optional: the user\nfills it in gradually. Adres fields are kept flat so one `SetField` message\nserves them all (mirrors the intake machine).", "methods": [], + "extends": [] + }, + { + "name": "Draft", + "id": "interface-Draft-097a742aa05bc7bbfee387dd3ea5b2ac382815db8ca18107e699d11e6da110cdeae2eb377bcea284f415cfd263937c3b0f6e7ed2ee9090d660d898dddefe3a4f-1", + "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';\n\n/** What the user is typing (raw, possibly invalid). */\nexport interface Draft {\n uren: string;\n jaren: string;\n punten: string;\n}\n\n/** What we have AFTER parsing — branded/typed, guaranteed valid. */\nexport interface Valid {\n uren: Uren;\n jaren: number;\n punten: number;\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; draft: Draft; errors: Partial> }\n | { tag: 'Submitting'; data: Valid }\n | { tag: 'Submitted'; data: Valid }\n | { tag: 'Failed'; data: Valid; error: string };\n\nexport const initial: WizardState = { tag: 'Editing', step: 1, draft: { uren: '', jaren: '', punten: '' }, errors: {} };\n\n/** Parse every field; on success hand back a Valid, else the per-field errors. */\nfunction validate(draft: Draft): Result>, Valid> {\n const uren = parseUren(draft.uren);\n const jaren = parseUren(draft.jaren);\n const punten = parseUren(draft.punten);\n const errors: Partial> = {};\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 (uren.ok && jaren.ok && punten.ok) {\n return { ok: true, value: { uren: uren.value, jaren: jaren.value, punten: punten.value } };\n }\n return { ok: false, error: errors };\n}\n\n/** Step 1 → 2: advance only when BOTH step-1 fields parse. Illegal elsewhere = no-op. */\nexport function next(s: WizardState): WizardState {\n if (s.tag !== 'Editing' || s.step !== 1) return s;\n const uren = parseUren(s.draft.uren);\n const jaren = parseUren(s.draft.jaren);\n const errors: Partial> = {};\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\nexport function back(s: WizardState): WizardState {\n if (s.tag !== 'Editing' || s.step !== 2) return s;\n return { ...s, step: 1, errors: {} };\n}\n\n/** Step 2 submit: parse everything; move to Submitting only with Valid data. */\nexport function submit(s: WizardState): WizardState {\n if (s.tag !== 'Editing' || s.step !== 2) return s;\n const result = validate(s.draft);\n return result.ok ? { tag: 'Submitting', data: result.value } : { ...s, errors: result.error };\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 ? { tag: 'Submitted', data: s.data } : { 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: 'Submit' }\n | { tag: 'Retry' }\n | { tag: 'SubmitConfirmed' }\n | { tag: 'SubmitFailed'; error: string }\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 '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 '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": 7 + }, + { + "name": "punten", + "deprecated": false, + "deprecationMessage": "", + "type": "string", + "indexKey": "", + "optional": false, + "description": "", + "line": 8 + }, + { + "name": "uren", + "deprecated": false, + "deprecationMessage": "", + "type": "string", + "indexKey": "", + "optional": false, + "description": "", + "line": 6 + } + ], + "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": 1, @@ -2157,6 +2202,113 @@ } ], "components": [ + { + "name": "AddressFieldsComponent", + "id": "component-AddressFieldsComponent-1e0b1a46198e647fedabf1bd8f892a2cc7444c7741a9a2454ce1ba38bb7d7b7787621fd7c35778bb545b72b34d355ed53f3a644b2eab266a4a981469c6ea70ba", + "file": "src/app/registratie/ui/address-fields/address-fields.component.ts", + "encapsulation": [], + "entryComponents": [], + "inputs": [], + "outputs": [], + "providers": [], + "selector": "app-address-fields", + "styleUrls": [], + "styles": [], + "template": "
\n {{ legend() }}\n \n \n \n \n \n \n \n \n \n
\n", + "templateUrl": [], + "viewProviders": [], + "hostDirectives": [], + "inputsClass": [ + { + "name": "errors", + "defaultValue": "{}", + "deprecated": false, + "deprecationMessage": "", + "type": "AdresErrors", + "indexKey": "", + "optional": false, + "description": "", + "line": 45, + "required": false + }, + { + "name": "idPrefix", + "defaultValue": "'adres'", + "deprecated": false, + "deprecationMessage": "", + "indexKey": "", + "optional": false, + "description": "

Prefix for field ids/labels — keeps them unique if two blocks ever co-exist.

\n", + "line": 47, + "rawdescription": "\nPrefix for field ids/labels — keeps them unique if two blocks ever co-exist.", + "required": false + }, + { + "name": "legend", + "defaultValue": "'Adres'", + "deprecated": false, + "deprecationMessage": "", + "indexKey": "", + "optional": false, + "description": "", + "line": 48, + "required": false + }, + { + "name": "value", + "deprecated": false, + "deprecationMessage": "", + "type": "AdresValue", + "indexKey": "", + "optional": false, + "description": "", + "line": 44, + "required": true + } + ], + "outputsClass": [ + { + "name": "fieldChange", + "deprecated": false, + "deprecationMessage": "", + "type": "{ key: keyof AdresValue; value: string }", + "indexKey": "", + "optional": false, + "description": "", + "line": 49, + "required": false + } + ], + "propertiesClass": [], + "methodsClass": [], + "deprecated": false, + "deprecationMessage": "", + "hostBindings": [], + "hostListeners": [], + "standalone": false, + "imports": [ + { + "name": "FormsModule", + "type": "module" + }, + { + "name": "FormFieldComponent", + "type": "component" + }, + { + "name": "TextInputComponent", + "type": "component" + } + ], + "description": "

Organism: the editable address block (straat / postcode / woonplaats), grouped\nin a fieldset/legend. Pure & presentational — values in via value, errors in\nvia errors, every keystroke out via fieldChange. No store, no services, no\ninternal state; the container owns the Model and decides what a change means\n(registratie-wizard dispatches SetField; change-request-form sets its props).\nComposes the form-field molecule (×3) so labels/error wiring stay consistent.

\n", + "rawdescription": "\nOrganism: the editable address block (straat / postcode / woonplaats), grouped\nin a fieldset/legend. Pure & presentational — values in via `value`, errors in\nvia `errors`, every keystroke out via `fieldChange`. No store, no services, no\ninternal state; the container owns the Model and decides what a change means\n(registratie-wizard dispatches SetField; change-request-form sets its props).\nComposes the form-field molecule (×3) so labels/error wiring stay consistent.", + "type": "component", + "sourceCode": "import { Component, input, output } from '@angular/core';\nimport { FormsModule } from '@angular/forms';\nimport { FormFieldComponent } from '@shared/ui/form-field/form-field.component';\nimport { TextInputComponent } from '@shared/ui/text-input/text-input.component';\n\nexport interface AdresValue {\n straat: string;\n postcode: string;\n woonplaats: string;\n}\nexport type AdresErrors = Partial>;\n\n/** Organism: the editable address block (straat / postcode / woonplaats), grouped\n in a fieldset/legend. Pure & presentational — values in via `value`, errors in\n via `errors`, every keystroke out via `fieldChange`. No store, no services, no\n internal state; the container owns the Model and decides what a change means\n (registratie-wizard dispatches SetField; change-request-form sets its props).\n Composes the form-field molecule (×3) so labels/error wiring stay consistent. */\n@Component({\n selector: 'app-address-fields',\n imports: [FormsModule, FormFieldComponent, TextInputComponent],\n template: `\n
\n {{ legend() }}\n \n \n \n \n \n \n \n \n \n
\n `,\n})\nexport class AddressFieldsComponent {\n value = input.required();\n errors = input({});\n /** Prefix for field ids/labels — keeps them unique if two blocks ever co-exist. */\n idPrefix = input('adres');\n legend = input('Adres');\n fieldChange = output<{ key: keyof AdresValue; value: string }>();\n}\n", + "assetsDirs": [], + "styleUrlsData": "", + "stylesData": "", + "extends": [] + }, { "name": "AlertComponent", "id": "component-AlertComponent-7d03ee2c0179c2f8d44795112c823951406810027571b5471bc765873f1aeaee550dcb89c6ebd8514ee36f2d96c8c6ba118293bf44a4a1316f61e91fa3c06f4e", @@ -2554,7 +2706,7 @@ }, { "name": "ChangeRequestFormComponent", - "id": "component-ChangeRequestFormComponent-8d79b6a9ce75f62f4e04f9d3ed0867e00d005c9c05cf267503536c4eec9813372aff51a3c8686927fd07f46503b60ddb4107edcc5f57fdfa1b6b52fe2a98026c", + "id": "component-ChangeRequestFormComponent-38a13207972e7e101ba989ca2fe390d3a6f353165e03bc0434fe9785b321fba0e953ae0b969ca459acee20e21173dcb190fb46590517af703b89243b5dd45aba", "file": "src/app/registratie/ui/change-request-form/change-request-form.component.ts", "encapsulation": [], "entryComponents": [], @@ -2564,7 +2716,7 @@ "selector": "app-change-request-form", "styleUrls": [], "styles": [], - "template": "Adreswijziging doorgeven\n
\n \n \n \n \n \n \n \n \n \n
\n Wijziging indienen\n
\n
\n", + "template": "Adreswijziging doorgeven\n
\n \n
\n Wijziging indienen\n
\n\n", "templateUrl": [], "viewProviders": [], "hostDirectives": [], @@ -2578,7 +2730,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 47, + "line": 42, "required": false } ], @@ -2592,7 +2744,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 44 + "line": 39 }, { "name": "street", @@ -2603,7 +2755,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 42 + "line": 37 }, { "name": "streetError", @@ -2614,7 +2766,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 45 + "line": 40 }, { "name": "zip", @@ -2625,7 +2777,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 43 + "line": 38 }, { "name": "zipError", @@ -2636,17 +2788,49 @@ "indexKey": "", "optional": false, "description": "", - "line": 46 + "line": 41 } ], "methodsClass": [ + { + "name": "onAdres", + "args": [ + { + "name": "e", + "type": "literal type", + "optional": false, + "dotDotDotToken": false, + "deprecated": false, + "deprecationMessage": "" + } + ], + "optional": false, + "returnType": "void", + "typeParameters": [], + "line": 44, + "deprecated": false, + "deprecationMessage": "", + "jsdoctags": [ + { + "name": "e", + "type": "literal type", + "optional": false, + "dotDotDotToken": false, + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + } + ] + }, { "name": "onSubmit", "args": [], "optional": false, "returnType": "void", "typeParameters": [], - "line": 49, + "line": 50, "deprecated": false, "deprecationMessage": "" } @@ -2661,14 +2845,6 @@ "name": "FormsModule", "type": "module" }, - { - "name": "FormFieldComponent", - "type": "component" - }, - { - "name": "TextInputComponent", - "type": "component" - }, { "name": "ButtonComponent", "type": "component" @@ -2676,12 +2852,16 @@ { "name": "HeadingComponent", "type": "component" + }, + { + "name": "AddressFieldsComponent", + "type": "component" } ], "description": "

Organism: change-request (adreswijziging) form. Reuses the same form-field\nmolecule + text-input/button atoms as the login form. Field errors come\nstraight from the parser's Result — no parallel "is it valid" flag to drift.

\n", "rawdescription": "\nOrganism: change-request (adreswijziging) form. Reuses the same form-field\nmolecule + text-input/button atoms as the login form. Field errors come\nstraight from the parser's Result — no parallel \"is it valid\" flag to drift.", "type": "component", - "sourceCode": "import { Component, output, signal } from '@angular/core';\nimport { FormsModule } from '@angular/forms';\nimport { FormFieldComponent } from '@shared/ui/form-field/form-field.component';\nimport { TextInputComponent } from '@shared/ui/text-input/text-input.component';\nimport { ButtonComponent } from '@shared/ui/button/button.component';\nimport { HeadingComponent } from '@shared/ui/heading/heading.component';\nimport { Postcode, parsePostcode } from '@registratie/domain/value-objects/postcode';\n\n/** A submitted change request carries a *parsed* postcode (branded Postcode),\n not a raw string — downstream code can't receive an unvalidated one. */\nexport interface ChangeRequest {\n street: string;\n zip: Postcode;\n city: string;\n}\n\n/** Organism: change-request (adreswijziging) form. Reuses the same form-field\n molecule + text-input/button atoms as the login form. Field errors come\n straight from the parser's Result — no parallel \"is it valid\" flag to drift. */\n@Component({\n selector: 'app-change-request-form',\n imports: [FormsModule, FormFieldComponent, TextInputComponent, ButtonComponent, HeadingComponent],\n template: `\n Adreswijziging doorgeven\n
\n \n \n \n \n \n \n \n \n \n
\n Wijziging indienen\n
\n
\n `,\n})\nexport class ChangeRequestFormComponent {\n street = '';\n zip = '';\n city = '';\n streetError = signal('');\n zipError = signal('');\n submitted = output();\n\n onSubmit() {\n const street = this.street.trim();\n this.streetError.set(street ? '' : 'Vul straat en huisnummer in.');\n\n const postcode = parsePostcode(this.zip);\n this.zipError.set(postcode.ok ? '' : postcode.error);\n\n if (!street || !postcode.ok) return;\n this.submitted.emit({ street, zip: postcode.value, city: this.city.trim() });\n }\n}\n", + "sourceCode": "import { Component, output, signal } from '@angular/core';\nimport { FormsModule } from '@angular/forms';\nimport { ButtonComponent } from '@shared/ui/button/button.component';\nimport { HeadingComponent } from '@shared/ui/heading/heading.component';\nimport { AddressFieldsComponent, AdresValue } from '@registratie/ui/address-fields/address-fields.component';\nimport { Postcode, parsePostcode } from '@registratie/domain/value-objects/postcode';\n\n/** A submitted change request carries a *parsed* postcode (branded Postcode),\n not a raw string — downstream code can't receive an unvalidated one. */\nexport interface ChangeRequest {\n street: string;\n zip: Postcode;\n city: string;\n}\n\n/** Organism: change-request (adreswijziging) form. Reuses the same form-field\n molecule + text-input/button atoms as the login form. Field errors come\n straight from the parser's Result — no parallel \"is it valid\" flag to drift. */\n@Component({\n selector: 'app-change-request-form',\n imports: [FormsModule, ButtonComponent, HeadingComponent, AddressFieldsComponent],\n template: `\n Adreswijziging doorgeven\n
\n \n
\n Wijziging indienen\n
\n \n `,\n})\nexport class ChangeRequestFormComponent {\n street = '';\n zip = '';\n city = '';\n streetError = signal('');\n zipError = signal('');\n submitted = output();\n\n onAdres(e: { key: keyof AdresValue; value: string }) {\n if (e.key === 'straat') this.street = e.value;\n else if (e.key === 'postcode') this.zip = e.value;\n else this.city = e.value;\n }\n\n onSubmit() {\n const street = this.street.trim();\n this.streetError.set(street ? '' : 'Vul straat en huisnummer in.');\n\n const postcode = parsePostcode(this.zip);\n this.zipError.set(postcode.ok ? '' : postcode.error);\n\n if (!street || !postcode.ok) return;\n this.submitted.emit({ street, zip: postcode.value, city: this.city.trim() });\n }\n}\n", "assetsDirs": [], "styleUrlsData": "", "stylesData": "", @@ -4685,7 +4865,7 @@ }, { "name": "RegistratieWizardComponent", - "id": "component-RegistratieWizardComponent-74dcf70dfe72a9b598de5861b8799d6a30f502bae7c2369ccee5759609bef6f4e6aaea03494efb1764da0a1d6437045516c78fb653c132564b7d49126f16fca5", + "id": "component-RegistratieWizardComponent-db420c0c655aba555101de3bab37ec4eb12df2290c2b5fb98d047e0bca827b66c3209cb4dd53e5d472e7d841444da01d5340ea53ecf4f35707386565975f65cb", "file": "src/app/registratie/ui/registratie-wizard/registratie-wizard.component.ts", "encapsulation": [], "entryComponents": [], @@ -4695,7 +4875,7 @@ "selector": "app-registratie-wizard", "styleUrls": [], "styles": [], - "template": "@switch (state().tag) {\n @case ('Invullen') {\n \n

{{ stepTitle() }}

\n
\n @switch (step()) {\n @case ('adres') {\n @if (adresStatus() === 'laden') {\n \n } @else {\n @switch (adresStatus()) {\n @case ('gevonden') {\n Vooraf ingevuld op basis van de BRP. Controleer en pas zo nodig aan.\n }\n @case ('geen') {\n We vonden geen adres in de BRP. Vul uw adres hieronder handmatig in.\n }\n @case ('fout') {\n We konden de BRP nu niet bereiken. Vul uw adres hieronder handmatig in.\n }\n }\n \n \n \n \n \n \n \n \n \n \n \n \n @if (draft().correspondentie === 'email') {\n \n \n \n }\n }\n }\n @case ('beroep') {\n \n \n \n \n \n\n @if (handmatigActief()) {\n Een handmatig ingevoerd diploma kan niet automatisch worden geverifieerd. Kies uw beroep en beantwoord de aanvullende vragen; uw aanvraag wordt daarna handmatig beoordeeld.\n \n \n \n } @else if (draft().beroep) {\n
\n \n
\n }\n\n @for (q of actieveVragen($any(data)); track q.id) {\n \n @if (q.type === 'ja-nee') {\n \n } @else {\n \n }\n \n }\n
\n \n \n \n
\n }\n @case ('controle') {\n Controleer uw gegevens en dien de registratie in.\n
\n \n \n \n @if (draft().correspondentie === 'email') {\n \n }\n \n \n @for (item of samenvattingVragen(); track item.vraag) {\n \n }\n
\n
\n Adres wijzigen\n Diploma wijzigen\n
\n }\n }\n
\n @if (cursor() > 0) {\n Vorige\n }\n {{ step() === 'controle' ? 'Registratie indienen' : 'Volgende' }}\n Annuleren\n
\n \n }\n @case ('Indienen') {\n Uw registratie wordt verwerkt…\n }\n @case ('Ingediend') {\n Uw registratie is ontvangen. Uw referentienummer is {{ referentie() }}. Bewaar dit nummer voor uw administratie.\n
\n Nieuwe registratie starten\n
\n }\n @case ('Mislukt') {\n Het indienen is niet gelukt: {{ failedError() }}\n
\n Opnieuw proberen\n
\n }\n}\n", + "template": "@switch (state().tag) {\n @case ('Invullen') {\n \n

{{ stepTitle() }}

\n
\n @switch (step()) {\n @case ('adres') {\n @if (adresStatus() === 'laden') {\n \n } @else {\n @switch (adresStatus()) {\n @case ('gevonden') {\n Vooraf ingevuld op basis van de BRP. Controleer en pas zo nodig aan.\n }\n @case ('geen') {\n We vonden geen adres in de BRP. Vul uw adres hieronder handmatig in.\n }\n @case ('fout') {\n We konden de BRP nu niet bereiken. Vul uw adres hieronder handmatig in.\n }\n }\n \n \n \n \n @if (draft().correspondentie === 'email') {\n \n \n \n }\n }\n }\n @case ('beroep') {\n \n \n \n \n \n\n @if (handmatigActief()) {\n Een handmatig ingevoerd diploma kan niet automatisch worden geverifieerd. Kies uw beroep en beantwoord de aanvullende vragen; uw aanvraag wordt daarna handmatig beoordeeld.\n \n \n \n } @else if (draft().beroep) {\n
\n \n
\n }\n\n @for (q of actieveVragen($any(data)); track q.id) {\n \n @if (q.type === 'ja-nee') {\n \n } @else {\n \n }\n \n }\n
\n \n \n \n
\n }\n @case ('controle') {\n Controleer uw gegevens en dien de registratie in.\n
\n \n \n \n @if (draft().correspondentie === 'email') {\n \n }\n \n \n @for (item of samenvattingVragen(); track item.vraag) {\n \n }\n
\n
\n Adres wijzigen\n Diploma wijzigen\n
\n }\n }\n
\n @if (cursor() > 0) {\n Vorige\n }\n {{ step() === 'controle' ? 'Registratie indienen' : 'Volgende' }}\n Annuleren\n
\n \n }\n @case ('Indienen') {\n Uw registratie wordt verwerkt…\n }\n @case ('Ingediend') {\n Uw registratie is ontvangen. Uw referentienummer is {{ referentie() }}. Bewaar dit nummer voor uw administratie.\n
\n Nieuwe registratie starten\n
\n }\n @case ('Mislukt') {\n Het indienen is niet gelukt: {{ failedError() }}\n
\n Opnieuw proberen\n
\n }\n}\n", "templateUrl": [], "viewProviders": [], "hostDirectives": [], @@ -4709,7 +4889,7 @@ "indexKey": "", "optional": false, "description": "

Optional seed so Storybook / tests can mount any state directly.

\n", - "line": 184, + "line": 181, "rawdescription": "\nOptional seed so Storybook / tests can mount any state directly.", "required": false } @@ -4725,7 +4905,7 @@ "indexKey": "", "optional": false, "description": "

The policy questions that apply to the current choice (server-decided).

\n", - "line": 258, + "line": 255, "rawdescription": "\nThe policy questions that apply to the current choice (server-decided).", "modifierKind": [ 124 @@ -4740,7 +4920,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 207, + "line": 204, "modifierKind": [ 124 ] @@ -4754,7 +4934,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 180, + "line": 177, "modifierKind": [ 124 ] @@ -4768,7 +4948,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 202, + "line": 199, "modifierKind": [ 124 ] @@ -4782,7 +4962,7 @@ "indexKey": "", "optional": false, "description": "

BRP lookup outcome. A failure or "geen adres" never blocks the wizard — both\nfall back to manual entry (PRD §7). 'geen' covers both no-address-found and a\nmalformed response; 'fout' is an unreachable BRP.

\n", - "line": 214, + "line": 211, "rawdescription": "\nBRP lookup outcome. A failure or \"geen adres\" never blocks the wizard — both\nfall back to manual entry (PRD §7). 'geen' covers both no-address-found and a\nmalformed response; 'fout' is an unreachable BRP.", "modifierKind": [ 124 @@ -4797,7 +4977,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 255, + "line": 252, "modifierKind": [ 124 ] @@ -4811,7 +4991,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 176, + "line": 173, "modifierKind": [ 123 ] @@ -4825,7 +5005,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 208, + "line": 205, "modifierKind": [ 124 ] @@ -4839,7 +5019,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 196, + "line": 193, "modifierKind": [ 124 ] @@ -4853,7 +5033,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 209, + "line": 206, "modifierKind": [ 124 ] @@ -4867,7 +5047,7 @@ "indexKey": "", "optional": false, "description": "

The radio selection: a diploma id, or the "not listed" sentinel in manual mode.

\n", - "line": 248, + "line": 245, "rawdescription": "\nThe radio selection: a diploma id, or the \"not listed\" sentinel in manual mode.", "modifierKind": [ 124 @@ -4882,7 +5062,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 250, + "line": 247, "modifierKind": [ 124 ] @@ -4896,7 +5076,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 181, + "line": 178, "modifierKind": [ 123 ] @@ -4910,7 +5090,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 190, + "line": 187, "modifierKind": [ 148 ] @@ -4924,7 +5104,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 197, + "line": 194, "modifierKind": [ 124 ] @@ -4938,7 +5118,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 177, + "line": 174, "modifierKind": [ 123 ] @@ -4952,7 +5132,7 @@ "indexKey": "", "optional": false, "description": "

Parsed lookup as a plain value (or null) — used outside the beroep step (the\ncontrole summary) where the template variable isn't in scope.

\n", - "line": 233, + "line": 230, "rawdescription": "\nParsed lookup as a plain value (or null) — used outside the beroep step (the\ncontrole summary) where the template variable isn't in scope.", "modifierKind": [ 123 @@ -4967,7 +5147,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 240, + "line": 237, "modifierKind": [ 124 ] @@ -4981,7 +5161,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 201, + "line": 198, "modifierKind": [ 124 ] @@ -4995,7 +5175,7 @@ "indexKey": "", "optional": false, "description": "

True while the user is entering a diploma manually (not in the DUO list).

\n", - "line": 246, + "line": 243, "rawdescription": "\nTrue while the user is entering a diploma manually (not in the DUO list).", "modifierKind": [ 124 @@ -5010,7 +5190,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 195, + "line": 192, "modifierKind": [ 123 ] @@ -5024,7 +5204,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 238, + "line": 235, "modifierKind": [ 148 ] @@ -5038,7 +5218,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 186, + "line": 183, "modifierKind": [ 148 ] @@ -5052,7 +5232,7 @@ "indexKey": "", "optional": false, "description": "

The DUO lookup (diplomas + manual fallback), validated at the trust boundary.

\n", - "line": 224, + "line": 221, "rawdescription": "\nThe DUO lookup (diplomas + manual fallback), validated at the trust boundary.", "modifierKind": [ 124 @@ -5067,7 +5247,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 200, + "line": 197, "modifierKind": [ 124 ] @@ -5081,7 +5261,7 @@ "indexKey": "", "optional": false, "description": "

Answered policy questions for the controle summary (question text + answer).

\n", - "line": 264, + "line": 261, "rawdescription": "\nAnswered policy questions for the controle summary (question text + answer).", "modifierKind": [ 124 @@ -5096,7 +5276,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 242, + "line": 239, "modifierKind": [ 124 ] @@ -5110,7 +5290,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 243, + "line": 240, "modifierKind": [ 124 ] @@ -5124,7 +5304,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 189, + "line": 186, "modifierKind": [ 148 ] @@ -5138,7 +5318,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 198, + "line": 195, "modifierKind": [ 124 ] @@ -5152,7 +5332,7 @@ "indexKey": "", "optional": false, "description": "

Focus target: the step heading receives focus on step change (a11y).

\n", - "line": 193, + "line": 190, "rawdescription": "\nFocus target: the step heading receives focus on step change (a11y).", "modifierKind": [ 123 @@ -5167,7 +5347,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 187, + "line": 184, "modifierKind": [ 148 ] @@ -5181,7 +5361,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 199, + "line": 196, "modifierKind": [ 124 ] @@ -5195,7 +5375,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 188, + "line": 185, "modifierKind": [ 123 ] @@ -5209,7 +5389,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 178, + "line": 175, "modifierKind": [ 123 ] @@ -5223,7 +5403,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 241, + "line": 238, "modifierKind": [ 124 ] @@ -5253,7 +5433,7 @@ "optional": false, "returnType": "void", "typeParameters": [], - "line": 272, + "line": 269, "deprecated": false, "deprecationMessage": "", "modifierKind": [ @@ -5290,7 +5470,7 @@ "optional": false, "returnType": "void", "typeParameters": [], - "line": 332, + "line": 329, "deprecated": false, "deprecationMessage": "" }, @@ -5300,7 +5480,7 @@ "optional": false, "returnType": "void", "typeParameters": [], - "line": 339, + "line": 336, "deprecated": false, "deprecationMessage": "" }, @@ -5310,7 +5490,7 @@ "optional": false, "returnType": "void", "typeParameters": [], - "line": 346, + "line": 343, "deprecated": false, "deprecationMessage": "", "rawdescription": "\nReset the wizard to a fresh start. Reload the BRP lookup so the address\nre-prefills, keeping the form and the \"vooraf ingevuld\" note consistent.", @@ -5322,7 +5502,7 @@ "optional": false, "returnType": "RegistratieState | null", "typeParameters": [], - "line": 322, + "line": 319, "deprecated": false, "deprecationMessage": "", "modifierKind": [ @@ -5335,7 +5515,7 @@ "optional": false, "returnType": "any", "typeParameters": [], - "line": 353, + "line": 350, "deprecated": false, "deprecationMessage": "", "rawdescription": "\nThe effect: when we enter Indienen, call the backend, then dispatch the\noutcome (success carries the confirmation reference).", @@ -5392,6 +5572,10 @@ "name": "StepperComponent", "type": "component" }, + { + "name": "AddressFieldsComponent", + "type": "component" + }, { "name": "ASYNC" } @@ -5399,7 +5583,7 @@ "description": "

Organism: the BIG-registration wizard. All state lives in one signal driven by\nthe pure reduce (registratie-wizard.machine.ts). The BRP address prefills the\ndraft via an effect; the DUO diploma list renders through ; choosing\na diploma reveals its server-derived beroep. The draft is persisted to\nlocalStorage so a reload keeps the user's progress. Built entirely from existing\natoms/molecules.

\n", "rawdescription": "\nOrganism: the BIG-registration wizard. All state lives in one signal driven by\nthe pure `reduce` (registratie-wizard.machine.ts). The BRP address prefills the\ndraft via an effect; the DUO diploma list renders through ; choosing\na diploma reveals its server-derived beroep. The draft is persisted to\nlocalStorage so a reload keeps the user's progress. Built entirely from existing\natoms/molecules.", "type": "component", - "sourceCode": "import { Component, ElementRef, computed, effect, inject, input, untracked, viewChild } from '@angular/core';\nimport { FormsModule } from '@angular/forms';\nimport { FormFieldComponent } from '@shared/ui/form-field/form-field.component';\nimport { TextInputComponent } from '@shared/ui/text-input/text-input.component';\nimport { RadioGroupComponent } from '@shared/ui/radio-group/radio-group.component';\nimport { ButtonComponent } from '@shared/ui/button/button.component';\nimport { AlertComponent } from '@shared/ui/alert/alert.component';\nimport { SpinnerComponent } from '@shared/ui/spinner/spinner.component';\nimport { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';\nimport { DataRowComponent } from '@shared/ui/data-row/data-row.component';\nimport { StepperComponent } from '@shared/ui/stepper/stepper.component';\nimport { ASYNC } from '@shared/ui/async/async.component';\nimport { createStore } from '@shared/application/store';\nimport { RemoteData, fromResource } from '@shared/application/remote-data';\nimport { BrpAdapter, parseBrpAddress } from '@registratie/infrastructure/brp.adapter';\nimport { DuoAdapter, parseDuoLookup } from '@registratie/infrastructure/duo.adapter';\nimport { DuoLookupDto, PolicyQuestionDto } from '@registratie/contracts/duo-diplomas.dto';\nimport {\n RegistratieState,\n RegistratieMsg,\n Draft,\n DraftField,\n Correspondentie,\n StepId,\n initial,\n reduce,\n STEPS,\n} from '@registratie/domain/registratie-wizard.machine';\nimport { submitRegistratie } from '@registratie/application/submit-registratie';\n\nconst STORAGE_KEY = 'registratie-v1'; // ponytail: bump the suffix if the persisted shape changes; no migration.\nconst KANALEN = [{ value: 'email', label: 'E-mail' }, { value: 'post', label: 'Post' }];\nconst JA_NEE = [{ value: 'ja', label: 'Ja' }, { value: 'nee', label: 'Nee' }];\nconst HANDMATIG = '__handmatig__'; // sentinel option: \"my diploma isn't listed\"\n\n/** Organism: the BIG-registration wizard. All state lives in one signal driven by\n the pure `reduce` (registratie-wizard.machine.ts). The BRP address prefills the\n draft via an effect; the DUO diploma list renders through ; choosing\n a diploma reveals its server-derived beroep. The draft is persisted to\n localStorage so a reload keeps the user's progress. Built entirely from existing\n atoms/molecules. */\n@Component({\n selector: 'app-registratie-wizard',\n imports: [\n FormsModule, FormFieldComponent, TextInputComponent, RadioGroupComponent, ButtonComponent,\n AlertComponent, SpinnerComponent, SkeletonComponent, DataRowComponent, StepperComponent, ...ASYNC,\n ],\n template: `\n @switch (state().tag) {\n @case ('Invullen') {\n \n

{{ stepTitle() }}

\n
\n @switch (step()) {\n @case ('adres') {\n @if (adresStatus() === 'laden') {\n \n } @else {\n @switch (adresStatus()) {\n @case ('gevonden') {\n Vooraf ingevuld op basis van de BRP. Controleer en pas zo nodig aan.\n }\n @case ('geen') {\n We vonden geen adres in de BRP. Vul uw adres hieronder handmatig in.\n }\n @case ('fout') {\n We konden de BRP nu niet bereiken. Vul uw adres hieronder handmatig in.\n }\n }\n \n \n \n \n \n \n \n \n \n \n \n \n @if (draft().correspondentie === 'email') {\n \n \n \n }\n }\n }\n @case ('beroep') {\n \n \n \n \n \n\n @if (handmatigActief()) {\n Een handmatig ingevoerd diploma kan niet automatisch worden geverifieerd. Kies uw beroep en beantwoord de aanvullende vragen; uw aanvraag wordt daarna handmatig beoordeeld.\n \n \n \n } @else if (draft().beroep) {\n
\n \n
\n }\n\n @for (q of actieveVragen($any(data)); track q.id) {\n \n @if (q.type === 'ja-nee') {\n \n } @else {\n \n }\n \n }\n
\n \n \n \n
\n }\n @case ('controle') {\n Controleer uw gegevens en dien de registratie in.\n
\n \n \n \n @if (draft().correspondentie === 'email') {\n \n }\n \n \n @for (item of samenvattingVragen(); track item.vraag) {\n \n }\n
\n
\n Adres wijzigen\n Diploma wijzigen\n
\n }\n }\n
\n @if (cursor() > 0) {\n Vorige\n }\n {{ step() === 'controle' ? 'Registratie indienen' : 'Volgende' }}\n Annuleren\n
\n \n }\n @case ('Indienen') {\n Uw registratie wordt verwerkt…\n }\n @case ('Ingediend') {\n Uw registratie is ontvangen. Uw referentienummer is {{ referentie() }}. Bewaar dit nummer voor uw administratie.\n
\n Nieuwe registratie starten\n
\n }\n @case ('Mislukt') {\n Het indienen is niet gelukt: {{ failedError() }}\n
\n Opnieuw proberen\n
\n }\n }\n `,\n})\nexport class RegistratieWizardComponent {\n private brp = inject(BrpAdapter);\n private duo = inject(DuoAdapter);\n private store = createStore(initial, reduce);\n\n protected adresRes = this.brp.adresResource();\n private diplomasRes = this.duo.diplomasResource();\n\n /** Optional seed so Storybook / tests can mount any state directly. */\n seed = input(initial);\n\n readonly kanalen = KANALEN;\n readonly stepLabels = ['Adres', 'Beroep', 'Controle']; // short labels for the stepper\n private stepTitles = ['Adres en correspondentievoorkeur', 'Beroep op basis van uw diploma', 'Controleren en indienen'];\n readonly state = this.store.model;\n readonly dispatch = this.store.dispatch;\n\n /** Focus target: the step heading receives focus on step change (a11y). */\n private stepHeading = viewChild>('stepHeading');\n\n private invullen = computed(() => (this.state().tag === 'Invullen' ? (this.state() as Extract) : null));\n protected cursor = computed(() => this.invullen()?.cursor ?? 0);\n protected draft = computed(() => this.invullen()?.draft ?? { antwoorden: {} });\n protected step = computed(() => STEPS[Math.min(this.cursor(), STEPS.length - 1)]);\n protected stepTitle = computed(() => this.stepTitles[Math.min(this.cursor(), this.stepTitles.length - 1)]);\n protected referentie = computed(() => (this.state().tag === 'Ingediend' ? (this.state() as Extract).referentie : ''));\n protected failedError = computed(() => (this.state().tag === 'Mislukt' ? (this.state() as Extract).error : ''));\n protected adresSamenvatting = computed(() => {\n const d = this.draft();\n return [d.straat, [d.postcode, d.woonplaats].filter(Boolean).join(' ')].filter(Boolean).join(', ');\n });\n // Readable labels for the controle summary (instead of raw enum values).\n protected adresHerkomstLabel = computed(() => ({ brp: 'Automatisch uit de BRP', handmatig: 'Handmatig ingevoerd' }[this.draft().adresHerkomst ?? 'handmatig']));\n protected correspondentieLabel = computed(() => ({ email: 'Per e-mail', post: 'Per post' }[this.draft().correspondentie ?? 'post']));\n protected diplomaHerkomstLabel = computed(() => ({ duo: 'Geverifieerd via DUO', handmatig: 'Handmatig ingevoerd (wordt beoordeeld)' }[this.draft().diplomaHerkomst ?? 'handmatig']));\n\n /** BRP lookup outcome. A failure or \"geen adres\" never blocks the wizard — both\n fall back to manual entry (PRD §7). 'geen' covers both no-address-found and a\n malformed response; 'fout' is an unreachable BRP. */\n protected adresStatus = computed<'laden' | 'gevonden' | 'geen' | 'fout'>(() => {\n const st = this.adresRes.status();\n if (st === 'loading' || st === 'reloading') return 'laden';\n if (st === 'error') return 'fout';\n const json = this.adresRes.value();\n const parsed = json !== undefined ? parseBrpAddress(json) : null;\n return parsed && parsed.ok && parsed.value.gevonden ? 'gevonden' : 'geen';\n });\n\n /** The DUO lookup (diplomas + manual fallback), validated at the trust boundary. */\n protected lookupRd = computed>(() => {\n const rd = fromResource(this.diplomasRes);\n if (rd.tag !== 'Success') return rd;\n const parsed = parseDuoLookup(rd.value);\n return parsed.ok ? { tag: 'Success', value: parsed.value } : { tag: 'Failure', error: new Error(parsed.error) };\n });\n\n /** Parsed lookup as a plain value (or null) — used outside the beroep step (the\n controle summary) where the template variable isn't in scope. */\n private duoData = computed(() => {\n const rd = this.lookupRd();\n return rd.tag === 'Success' ? rd.value : null;\n });\n\n readonly jaNee = JA_NEE;\n\n protected err = (k: DraftField | 'correspondentie' | 'diploma') => this.invullen()?.errors[k] ?? '';\n protected vraagErr = (id: string) => this.invullen()?.errors.antwoorden?.[id] ?? '';\n protected set = (key: DraftField, value: string) => this.dispatch({ tag: 'SetField', key, value });\n protected setKanaal = (value: string) => this.dispatch({ tag: 'SetCorrespondentie', value: value as Correspondentie });\n\n /** True while the user is entering a diploma manually (not in the DUO list). */\n protected handmatigActief = computed(() => this.draft().diplomaHerkomst === 'handmatig');\n /** The radio selection: a diploma id, or the \"not listed\" sentinel in manual mode. */\n protected diplomaKeuze = computed(() => (this.handmatigActief() ? HANDMATIG : this.draft().diplomaId ?? ''));\n\n protected diplomaOptions = (data: DuoLookupDto) => [\n ...data.diplomas.map((d) => ({ value: d.id, label: `${d.naam} — ${d.instelling} (${d.jaar})` })),\n { value: HANDMATIG, label: 'Mijn diploma staat er niet bij' },\n ];\n\n protected beroepOptions = (data: DuoLookupDto) => data.handmatig.beroepen.map((b) => ({ value: b, label: b }));\n\n /** The policy questions that apply to the current choice (server-decided). */\n protected actieveVragen = (data: DuoLookupDto): PolicyQuestionDto[] => {\n if (this.handmatigActief()) return data.handmatig.policyQuestions;\n return data.diplomas.find((d) => d.id === this.draft().diplomaId)?.policyQuestions ?? [];\n };\n\n /** Answered policy questions for the controle summary (question text + answer). */\n protected samenvattingVragen = computed(() => {\n const data = this.duoData();\n const d = this.draft();\n if (!data) return [] as { vraag: string; antwoord: string }[];\n const alle = [...data.diplomas.flatMap((x) => x.policyQuestions), ...data.handmatig.policyQuestions];\n return (d.vraagIds ?? []).map((id) => ({ vraag: alle.find((q) => q.id === id)?.vraag ?? id, antwoord: d.antwoorden[id] ?? '' }));\n });\n\n protected onDiplomaKeuze(data: DuoLookupDto, id: string) {\n if (id === HANDMATIG) {\n this.dispatch({ tag: 'KiesHandmatig', vraagIds: data.handmatig.policyQuestions.map((q) => q.id) });\n return;\n }\n const d = data.diplomas.find((x) => x.id === id);\n if (d) this.dispatch({ tag: 'KiesDiploma', diplomaId: d.id, beroep: d.beroep, vraagIds: d.policyQuestions.map((q) => q.id) });\n }\n\n constructor() {\n // An explicit seed (stories) wins; otherwise resume from localStorage.\n const seeded = this.seed();\n queueMicrotask(() => this.dispatch({ tag: 'Seed', state: seeded !== initial ? seeded : (this.restore() ?? initial) }));\n // Persist only while filling in; clear once the flow is done.\n effect(() => {\n const s = this.state();\n if (s.tag === 'Invullen') localStorage.setItem(STORAGE_KEY, JSON.stringify(s));\n else localStorage.removeItem(STORAGE_KEY);\n });\n // Prefill the address from the BRP lookup as it arrives. Track only the resource\n // value; untrack the dispatch (it reads the state signal, which would otherwise\n // make this effect loop on its own write). Don't clobber edits/restored data.\n effect(() => {\n const json = this.adresRes.value();\n if (!json) return;\n const parsed = parseBrpAddress(json);\n untracked(() => {\n const s = this.state();\n if (s.tag !== 'Invullen' || s.draft.straat) return;\n // ponytail: slice 2 handles gevonden === false -> manual entry stays handmatig.\n if (parsed.ok && parsed.value.gevonden && parsed.value.adres) {\n const a = parsed.value.adres;\n this.dispatch({ tag: 'PrefillAdres', straat: a.straat, postcode: a.postcode, woonplaats: a.woonplaats });\n }\n });\n });\n // A11y: move focus to the step heading when the STEP changes (depends only on\n // cursor(), which is value-stable across keystrokes — so typing never steals\n // focus). Skip the first run so we don't grab focus on initial load.\n let firstStep = true;\n effect(() => {\n this.cursor();\n if (firstStep) { firstStep = false; return; }\n untracked(() => {\n if (this.state().tag !== 'Invullen') return;\n queueMicrotask(() => this.stepHeading()?.nativeElement.focus());\n });\n });\n }\n\n private restore(): RegistratieState | null {\n const raw = localStorage.getItem(STORAGE_KEY);\n if (!raw) return null;\n try {\n return JSON.parse(raw) as RegistratieState;\n } catch {\n return null; // ponytail: corrupt entry -> start fresh, no migration.\n }\n }\n\n onPrimary() {\n const s = this.state();\n if (s.tag !== 'Invullen') return;\n this.dispatch(this.step() === 'controle' ? { tag: 'Submit' } : { tag: 'Next' });\n this.runIfIndienen();\n }\n\n onRetry() {\n this.dispatch({ tag: 'Retry' });\n this.runIfIndienen();\n }\n\n /** Reset the wizard to a fresh start. Reload the BRP lookup so the address\n re-prefills, keeping the form and the \"vooraf ingevuld\" note consistent. */\n restart() {\n this.dispatch({ tag: 'Seed', state: initial });\n this.adresRes.reload();\n }\n\n /** The effect: when we enter Indienen, call the backend, then dispatch the\n outcome (success carries the confirmation reference). */\n private async runIfIndienen() {\n const s = this.state();\n if (s.tag !== 'Indienen') return;\n const r = await submitRegistratie(s.data);\n if (r.ok) this.dispatch({ tag: 'SubmitConfirmed', referentie: r.value });\n else this.dispatch({ tag: 'SubmitFailed', error: r.error });\n }\n}\n", + "sourceCode": "import { Component, ElementRef, computed, effect, inject, input, untracked, viewChild } from '@angular/core';\nimport { FormsModule } from '@angular/forms';\nimport { FormFieldComponent } from '@shared/ui/form-field/form-field.component';\nimport { TextInputComponent } from '@shared/ui/text-input/text-input.component';\nimport { RadioGroupComponent } from '@shared/ui/radio-group/radio-group.component';\nimport { ButtonComponent } from '@shared/ui/button/button.component';\nimport { AlertComponent } from '@shared/ui/alert/alert.component';\nimport { SpinnerComponent } from '@shared/ui/spinner/spinner.component';\nimport { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';\nimport { DataRowComponent } from '@shared/ui/data-row/data-row.component';\nimport { StepperComponent } from '@shared/ui/stepper/stepper.component';\nimport { ASYNC } from '@shared/ui/async/async.component';\nimport { AddressFieldsComponent } from '@registratie/ui/address-fields/address-fields.component';\nimport { createStore } from '@shared/application/store';\nimport { RemoteData, fromResource } from '@shared/application/remote-data';\nimport { BrpAdapter, parseBrpAddress } from '@registratie/infrastructure/brp.adapter';\nimport { DuoAdapter, parseDuoLookup } from '@registratie/infrastructure/duo.adapter';\nimport { DuoLookupDto, PolicyQuestionDto } from '@registratie/contracts/duo-diplomas.dto';\nimport {\n RegistratieState,\n RegistratieMsg,\n Draft,\n DraftField,\n Correspondentie,\n StepId,\n initial,\n reduce,\n STEPS,\n} from '@registratie/domain/registratie-wizard.machine';\nimport { submitRegistratie } from '@registratie/application/submit-registratie';\n\nconst STORAGE_KEY = 'registratie-v1'; // ponytail: bump the suffix if the persisted shape changes; no migration.\nconst KANALEN = [{ value: 'email', label: 'E-mail' }, { value: 'post', label: 'Post' }];\nconst JA_NEE = [{ value: 'ja', label: 'Ja' }, { value: 'nee', label: 'Nee' }];\nconst HANDMATIG = '__handmatig__'; // sentinel option: \"my diploma isn't listed\"\n\n/** Organism: the BIG-registration wizard. All state lives in one signal driven by\n the pure `reduce` (registratie-wizard.machine.ts). The BRP address prefills the\n draft via an effect; the DUO diploma list renders through ; choosing\n a diploma reveals its server-derived beroep. The draft is persisted to\n localStorage so a reload keeps the user's progress. Built entirely from existing\n atoms/molecules. */\n@Component({\n selector: 'app-registratie-wizard',\n imports: [\n FormsModule, FormFieldComponent, TextInputComponent, RadioGroupComponent, ButtonComponent,\n AlertComponent, SpinnerComponent, SkeletonComponent, DataRowComponent, StepperComponent,\n AddressFieldsComponent, ...ASYNC,\n ],\n template: `\n @switch (state().tag) {\n @case ('Invullen') {\n \n

{{ stepTitle() }}

\n
\n @switch (step()) {\n @case ('adres') {\n @if (adresStatus() === 'laden') {\n \n } @else {\n @switch (adresStatus()) {\n @case ('gevonden') {\n Vooraf ingevuld op basis van de BRP. Controleer en pas zo nodig aan.\n }\n @case ('geen') {\n We vonden geen adres in de BRP. Vul uw adres hieronder handmatig in.\n }\n @case ('fout') {\n We konden de BRP nu niet bereiken. Vul uw adres hieronder handmatig in.\n }\n }\n \n \n \n \n @if (draft().correspondentie === 'email') {\n \n \n \n }\n }\n }\n @case ('beroep') {\n \n \n \n \n \n\n @if (handmatigActief()) {\n Een handmatig ingevoerd diploma kan niet automatisch worden geverifieerd. Kies uw beroep en beantwoord de aanvullende vragen; uw aanvraag wordt daarna handmatig beoordeeld.\n \n \n \n } @else if (draft().beroep) {\n
\n \n
\n }\n\n @for (q of actieveVragen($any(data)); track q.id) {\n \n @if (q.type === 'ja-nee') {\n \n } @else {\n \n }\n \n }\n
\n \n \n \n
\n }\n @case ('controle') {\n Controleer uw gegevens en dien de registratie in.\n
\n \n \n \n @if (draft().correspondentie === 'email') {\n \n }\n \n \n @for (item of samenvattingVragen(); track item.vraag) {\n \n }\n
\n
\n Adres wijzigen\n Diploma wijzigen\n
\n }\n }\n
\n @if (cursor() > 0) {\n Vorige\n }\n {{ step() === 'controle' ? 'Registratie indienen' : 'Volgende' }}\n Annuleren\n
\n \n }\n @case ('Indienen') {\n Uw registratie wordt verwerkt…\n }\n @case ('Ingediend') {\n Uw registratie is ontvangen. Uw referentienummer is {{ referentie() }}. Bewaar dit nummer voor uw administratie.\n
\n Nieuwe registratie starten\n
\n }\n @case ('Mislukt') {\n Het indienen is niet gelukt: {{ failedError() }}\n
\n Opnieuw proberen\n
\n }\n }\n `,\n})\nexport class RegistratieWizardComponent {\n private brp = inject(BrpAdapter);\n private duo = inject(DuoAdapter);\n private store = createStore(initial, reduce);\n\n protected adresRes = this.brp.adresResource();\n private diplomasRes = this.duo.diplomasResource();\n\n /** Optional seed so Storybook / tests can mount any state directly. */\n seed = input(initial);\n\n readonly kanalen = KANALEN;\n readonly stepLabels = ['Adres', 'Beroep', 'Controle']; // short labels for the stepper\n private stepTitles = ['Adres en correspondentievoorkeur', 'Beroep op basis van uw diploma', 'Controleren en indienen'];\n readonly state = this.store.model;\n readonly dispatch = this.store.dispatch;\n\n /** Focus target: the step heading receives focus on step change (a11y). */\n private stepHeading = viewChild>('stepHeading');\n\n private invullen = computed(() => (this.state().tag === 'Invullen' ? (this.state() as Extract) : null));\n protected cursor = computed(() => this.invullen()?.cursor ?? 0);\n protected draft = computed(() => this.invullen()?.draft ?? { antwoorden: {} });\n protected step = computed(() => STEPS[Math.min(this.cursor(), STEPS.length - 1)]);\n protected stepTitle = computed(() => this.stepTitles[Math.min(this.cursor(), this.stepTitles.length - 1)]);\n protected referentie = computed(() => (this.state().tag === 'Ingediend' ? (this.state() as Extract).referentie : ''));\n protected failedError = computed(() => (this.state().tag === 'Mislukt' ? (this.state() as Extract).error : ''));\n protected adresSamenvatting = computed(() => {\n const d = this.draft();\n return [d.straat, [d.postcode, d.woonplaats].filter(Boolean).join(' ')].filter(Boolean).join(', ');\n });\n // Readable labels for the controle summary (instead of raw enum values).\n protected adresHerkomstLabel = computed(() => ({ brp: 'Automatisch uit de BRP', handmatig: 'Handmatig ingevoerd' }[this.draft().adresHerkomst ?? 'handmatig']));\n protected correspondentieLabel = computed(() => ({ email: 'Per e-mail', post: 'Per post' }[this.draft().correspondentie ?? 'post']));\n protected diplomaHerkomstLabel = computed(() => ({ duo: 'Geverifieerd via DUO', handmatig: 'Handmatig ingevoerd (wordt beoordeeld)' }[this.draft().diplomaHerkomst ?? 'handmatig']));\n\n /** BRP lookup outcome. A failure or \"geen adres\" never blocks the wizard — both\n fall back to manual entry (PRD §7). 'geen' covers both no-address-found and a\n malformed response; 'fout' is an unreachable BRP. */\n protected adresStatus = computed<'laden' | 'gevonden' | 'geen' | 'fout'>(() => {\n const st = this.adresRes.status();\n if (st === 'loading' || st === 'reloading') return 'laden';\n if (st === 'error') return 'fout';\n const json = this.adresRes.value();\n const parsed = json !== undefined ? parseBrpAddress(json) : null;\n return parsed && parsed.ok && parsed.value.gevonden ? 'gevonden' : 'geen';\n });\n\n /** The DUO lookup (diplomas + manual fallback), validated at the trust boundary. */\n protected lookupRd = computed>(() => {\n const rd = fromResource(this.diplomasRes);\n if (rd.tag !== 'Success') return rd;\n const parsed = parseDuoLookup(rd.value);\n return parsed.ok ? { tag: 'Success', value: parsed.value } : { tag: 'Failure', error: new Error(parsed.error) };\n });\n\n /** Parsed lookup as a plain value (or null) — used outside the beroep step (the\n controle summary) where the template variable isn't in scope. */\n private duoData = computed(() => {\n const rd = this.lookupRd();\n return rd.tag === 'Success' ? rd.value : null;\n });\n\n readonly jaNee = JA_NEE;\n\n protected err = (k: DraftField | 'correspondentie' | 'diploma') => this.invullen()?.errors[k] ?? '';\n protected vraagErr = (id: string) => this.invullen()?.errors.antwoorden?.[id] ?? '';\n protected set = (key: DraftField, value: string) => this.dispatch({ tag: 'SetField', key, value });\n protected setKanaal = (value: string) => this.dispatch({ tag: 'SetCorrespondentie', value: value as Correspondentie });\n\n /** True while the user is entering a diploma manually (not in the DUO list). */\n protected handmatigActief = computed(() => this.draft().diplomaHerkomst === 'handmatig');\n /** The radio selection: a diploma id, or the \"not listed\" sentinel in manual mode. */\n protected diplomaKeuze = computed(() => (this.handmatigActief() ? HANDMATIG : this.draft().diplomaId ?? ''));\n\n protected diplomaOptions = (data: DuoLookupDto) => [\n ...data.diplomas.map((d) => ({ value: d.id, label: `${d.naam} — ${d.instelling} (${d.jaar})` })),\n { value: HANDMATIG, label: 'Mijn diploma staat er niet bij' },\n ];\n\n protected beroepOptions = (data: DuoLookupDto) => data.handmatig.beroepen.map((b) => ({ value: b, label: b }));\n\n /** The policy questions that apply to the current choice (server-decided). */\n protected actieveVragen = (data: DuoLookupDto): PolicyQuestionDto[] => {\n if (this.handmatigActief()) return data.handmatig.policyQuestions;\n return data.diplomas.find((d) => d.id === this.draft().diplomaId)?.policyQuestions ?? [];\n };\n\n /** Answered policy questions for the controle summary (question text + answer). */\n protected samenvattingVragen = computed(() => {\n const data = this.duoData();\n const d = this.draft();\n if (!data) return [] as { vraag: string; antwoord: string }[];\n const alle = [...data.diplomas.flatMap((x) => x.policyQuestions), ...data.handmatig.policyQuestions];\n return (d.vraagIds ?? []).map((id) => ({ vraag: alle.find((q) => q.id === id)?.vraag ?? id, antwoord: d.antwoorden[id] ?? '' }));\n });\n\n protected onDiplomaKeuze(data: DuoLookupDto, id: string) {\n if (id === HANDMATIG) {\n this.dispatch({ tag: 'KiesHandmatig', vraagIds: data.handmatig.policyQuestions.map((q) => q.id) });\n return;\n }\n const d = data.diplomas.find((x) => x.id === id);\n if (d) this.dispatch({ tag: 'KiesDiploma', diplomaId: d.id, beroep: d.beroep, vraagIds: d.policyQuestions.map((q) => q.id) });\n }\n\n constructor() {\n // An explicit seed (stories) wins; otherwise resume from localStorage.\n const seeded = this.seed();\n queueMicrotask(() => this.dispatch({ tag: 'Seed', state: seeded !== initial ? seeded : (this.restore() ?? initial) }));\n // Persist only while filling in; clear once the flow is done.\n effect(() => {\n const s = this.state();\n if (s.tag === 'Invullen') localStorage.setItem(STORAGE_KEY, JSON.stringify(s));\n else localStorage.removeItem(STORAGE_KEY);\n });\n // Prefill the address from the BRP lookup as it arrives. Track only the resource\n // value; untrack the dispatch (it reads the state signal, which would otherwise\n // make this effect loop on its own write). Don't clobber edits/restored data.\n effect(() => {\n const json = this.adresRes.value();\n if (!json) return;\n const parsed = parseBrpAddress(json);\n untracked(() => {\n const s = this.state();\n if (s.tag !== 'Invullen' || s.draft.straat) return;\n // ponytail: slice 2 handles gevonden === false -> manual entry stays handmatig.\n if (parsed.ok && parsed.value.gevonden && parsed.value.adres) {\n const a = parsed.value.adres;\n this.dispatch({ tag: 'PrefillAdres', straat: a.straat, postcode: a.postcode, woonplaats: a.woonplaats });\n }\n });\n });\n // A11y: move focus to the step heading when the STEP changes (depends only on\n // cursor(), which is value-stable across keystrokes — so typing never steals\n // focus). Skip the first run so we don't grab focus on initial load.\n let firstStep = true;\n effect(() => {\n this.cursor();\n if (firstStep) { firstStep = false; return; }\n untracked(() => {\n if (this.state().tag !== 'Invullen') return;\n queueMicrotask(() => this.stepHeading()?.nativeElement.focus());\n });\n });\n }\n\n private restore(): RegistratieState | null {\n const raw = localStorage.getItem(STORAGE_KEY);\n if (!raw) return null;\n try {\n return JSON.parse(raw) as RegistratieState;\n } catch {\n return null; // ponytail: corrupt entry -> start fresh, no migration.\n }\n }\n\n onPrimary() {\n const s = this.state();\n if (s.tag !== 'Invullen') return;\n this.dispatch(this.step() === 'controle' ? { tag: 'Submit' } : { tag: 'Next' });\n this.runIfIndienen();\n }\n\n onRetry() {\n this.dispatch({ tag: 'Retry' });\n this.runIfIndienen();\n }\n\n /** Reset the wizard to a fresh start. Reload the BRP lookup so the address\n re-prefills, keeping the form and the \"vooraf ingevuld\" note consistent. */\n restart() {\n this.dispatch({ tag: 'Seed', state: initial });\n this.adresRes.reload();\n }\n\n /** The effect: when we enter Indienen, call the backend, then dispatch the\n outcome (success carries the confirmation reference). */\n private async runIfIndienen() {\n const s = this.state();\n if (s.tag !== 'Indienen') return;\n const r = await submitRegistratie(s.data);\n if (r.ok) this.dispatch({ tag: 'SubmitConfirmed', referentie: r.value });\n else this.dispatch({ tag: 'SubmitFailed', error: r.error });\n }\n}\n", "assetsDirs": [], "styleUrlsData": "", "stylesData": "", @@ -5409,7 +5593,7 @@ "deprecated": false, "deprecationMessage": "", "args": [], - "line": 279 + "line": 276 }, "extends": [] }, @@ -6543,6 +6727,16 @@ "type": "string", "defaultValue": "'__handmatig__'" }, + { + "name": "initial", + "ctype": "miscellaneous", + "subtype": "variable", + "file": "src/app/registratie/domain/registratie-wizard.machine.ts", + "deprecated": false, + "deprecationMessage": "", + "type": "RegistratieState", + "defaultValue": "{ tag: 'Invullen', draft: emptyDraft, cursor: 0, errors: {} }" + }, { "name": "initial", "ctype": "miscellaneous", @@ -6563,21 +6757,11 @@ "type": "IntakeState", "defaultValue": "{ tag: 'Answering', answers: {}, cursor: 0, errors: {}, scholingThreshold: SCHOLING_THRESHOLD_DEFAULT }" }, - { - "name": "initial", - "ctype": "miscellaneous", - "subtype": "variable", - "file": "src/app/registratie/domain/registratie-wizard.machine.ts", - "deprecated": false, - "deprecationMessage": "", - "type": "RegistratieState", - "defaultValue": "{ tag: 'Invullen', draft: emptyDraft, cursor: 0, errors: {} }" - }, { "name": "JA_NEE", "ctype": "miscellaneous", "subtype": "variable", - "file": "src/app/herregistratie/ui/intake-wizard/intake-wizard.component.ts", + "file": "src/app/registratie/ui/registratie-wizard/registratie-wizard.component.ts", "deprecated": false, "deprecationMessage": "", "type": "[]", @@ -6587,7 +6771,7 @@ "name": "JA_NEE", "ctype": "miscellaneous", "subtype": "variable", - "file": "src/app/registratie/ui/registratie-wizard/registratie-wizard.component.ts", + "file": "src/app/herregistratie/ui/intake-wizard/intake-wizard.component.ts", "deprecated": false, "deprecationMessage": "", "type": "[]", @@ -6651,11 +6835,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" }, @@ -6663,11 +6847,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" }, @@ -6681,16 +6865,6 @@ "type": "string", "defaultValue": "'session-v1'" }, - { - "name": "STORAGE_KEY", - "ctype": "miscellaneous", - "subtype": "variable", - "file": "src/app/herregistratie/ui/intake-wizard/intake-wizard.component.ts", - "deprecated": false, - "deprecationMessage": "", - "type": "string", - "defaultValue": "'intake-v3'" - }, { "name": "STORAGE_KEY", "ctype": "miscellaneous", @@ -6701,6 +6875,16 @@ "type": "string", "defaultValue": "'registratie-v1'" }, + { + "name": "STORAGE_KEY", + "ctype": "miscellaneous", + "subtype": "variable", + "file": "src/app/herregistratie/ui/intake-wizard/intake-wizard.component.ts", + "deprecated": false, + "deprecationMessage": "", + "type": "string", + "defaultValue": "'intake-v3'" + }, { "name": "VALID", "ctype": "miscellaneous", @@ -6784,6 +6968,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", @@ -6842,35 +7055,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": "createStore", "file": "src/app/shared/application/store.ts", @@ -6926,7 +7110,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, @@ -6955,7 +7139,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, @@ -7696,6 +7880,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", @@ -7754,35 +7967,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": "parseBigNummer", "file": "src/app/registratie/domain/value-objects/big-nummer.ts", @@ -8081,6 +8265,50 @@ } ] }, + { + "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": "reduce", "file": "src/app/herregistratie/domain/herregistratie.machine.ts", @@ -8170,7 +8398,7 @@ ] }, { - "name": "reduce", + "name": "resolve", "file": "src/app/registratie/domain/registratie-wizard.machine.ts", "ctype": "miscellaneous", "subtype": "function", @@ -8185,8 +8413,8 @@ "deprecationMessage": "" }, { - "name": "m", - "type": "RegistratieMsg", + "name": "r", + "type": "Result", "deprecated": false, "deprecationMessage": "" } @@ -8203,8 +8431,8 @@ } }, { - "name": "m", - "type": "RegistratieMsg", + "name": "r", + "type": "Result", "deprecated": false, "deprecationMessage": "", "tagName": { @@ -8301,50 +8529,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", @@ -8516,63 +8700,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", @@ -8632,6 +8759,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", @@ -8734,6 +8918,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", @@ -8792,35 +9005,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": "submitHerregistratie", "file": "src/app/herregistratie/application/submit-herregistratie.ts", @@ -8937,6 +9121,35 @@ } ] }, + { + "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": "" + } + ], + "returnType": "Result", + "jsdoctags": [ + { + "name": "d", + "type": "Draft", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + } + ] + }, { "name": "validateAll", "file": "src/app/herregistratie/domain/intake.machine.ts", @@ -8982,14 +9195,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", @@ -8997,8 +9216,17 @@ "deprecationMessage": "" } ], - "returnType": "Result", + "returnType": "Result", "jsdoctags": [ + { + "name": "step", + "type": "StepId", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + }, { "name": "d", "type": "Draft", @@ -9068,50 +9296,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": "" - } - ], - "returnType": "Result", - "jsdoctags": [ - { - "name": "step", - "type": "StepId", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - }, - { - "name": "d", - "type": "Draft", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - } - ] } ], "typealiases": [ @@ -9126,6 +9310,17 @@ "description": "

A note is either a recognised specialism or a plain annotation — a closed set,\nnot an open string, so a typo can't slip through.

\n", "kind": 193 }, + { + "name": "AdresErrors", + "ctype": "miscellaneous", + "subtype": "typealias", + "rawtype": "Partial>", + "file": "src/app/registratie/ui/address-fields/address-fields.component.ts", + "deprecated": false, + "deprecationMessage": "", + "description": "", + "kind": 184 + }, { "name": "AdresHerkomst", "ctype": "miscellaneous", @@ -9361,22 +9556,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 }, { @@ -10030,6 +10225,723 @@ ] } ], + "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": "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": "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": "" + } + ], + "returnType": "Result", + "jsdoctags": [ + { + "name": "d", + "type": "Draft", + "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": "" + } + ], + "returnType": "Result", + "jsdoctags": [ + { + "name": "step", + "type": "StepId", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + }, + { + "name": "d", + "type": "Draft", + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + } + ] + } + ], "src/app/herregistratie/domain/herregistratie.machine.ts": [ { "name": "back", @@ -10749,723 +11661,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": "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": "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": "" - } - ], - "returnType": "Result", - "jsdoctags": [ - { - "name": "d", - "type": "Draft", - "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": "" - } - ], - "returnType": "Result", - "jsdoctags": [ - { - "name": "step", - "type": "StepId", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - }, - { - "name": "d", - "type": "Draft", - "deprecated": false, - "deprecationMessage": "", - "tagName": { - "text": "param" - } - } - ] - } - ], "src/app/shared/application/store.ts": [ { "name": "createStore", @@ -12178,6 +12373,19 @@ "kind": 200 } ], + "src/app/registratie/ui/address-fields/address-fields.component.ts": [ + { + "name": "AdresErrors", + "ctype": "miscellaneous", + "subtype": "typealias", + "rawtype": "Partial>", + "file": "src/app/registratie/ui/address-fields/address-fields.component.ts", + "deprecated": false, + "deprecationMessage": "", + "description": "", + "kind": 184 + } + ], "src/app/registratie/domain/registratie-wizard.machine.ts": [ { "name": "AdresHerkomst", @@ -12543,7 +12751,7 @@ ] }, "coverage": { - "count": 45, + "count": 44, "status": "medium", "files": [ { @@ -13750,13 +13958,41 @@ "coverageCount": "0/1", "status": "low" }, + { + "filePath": "src/app/registratie/ui/address-fields/address-fields.component.ts", + "type": "component", + "linktype": "component", + "name": "AddressFieldsComponent", + "coveragePercent": 33, + "coverageCount": "2/6", + "status": "medium" + }, + { + "filePath": "src/app/registratie/ui/address-fields/address-fields.component.ts", + "type": "interface", + "linktype": "interface", + "name": "AdresValue", + "coveragePercent": 0, + "coverageCount": "0/4", + "status": "low" + }, + { + "filePath": "src/app/registratie/ui/address-fields/address-fields.component.ts", + "type": "type alias", + "linktype": "miscellaneous", + "linksubtype": "typealias", + "name": "AdresErrors", + "coveragePercent": 0, + "coverageCount": "0/1", + "status": "low" + }, { "filePath": "src/app/registratie/ui/change-request-form/change-request-form.component.ts", "type": "component", "linktype": "component", "name": "ChangeRequestFormComponent", - "coveragePercent": 12, - "coverageCount": "1/8", + "coveragePercent": 11, + "coverageCount": "1/9", "status": "low" }, {