Compare commits
19
Commits
7a8eab917b
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1866947438 | ||
|
|
6330773fd5 | ||
|
|
856463b738 | ||
|
|
44dcc69811 | ||
|
|
d9aef9541f | ||
|
|
682db41344 | ||
|
|
241fe1b57b | ||
|
|
d31e054504 | ||
|
|
12f17d9d73 | ||
|
|
097e8468e0 | ||
|
|
2aa343f255 | ||
|
|
f3e5745145 | ||
|
|
e6bc19c790 | ||
|
|
fc2a3c348b | ||
|
|
551cabce5e | ||
|
|
b6bd7eea5a | ||
|
|
d4c5a9450a | ||
|
|
57fc72f254 | ||
|
|
43dc3210cd |
@@ -20,11 +20,11 @@ The repo splits docs by kind (`docs/README.md`). Pick one — don't write all th
|
||||
_decision/pattern_ (a tradeoff, a "we chose X over Y"). One format, no drift:
|
||||
`# ADR-NNNN — Title`, then `Status: Accepted · Date: YYYY-MM-DD`, then
|
||||
Problem / Options / Decision / Consequences.
|
||||
- **WP** — `docs/project/backlog/WP-NN-<slug>.md`. Tracked work; use the template at the
|
||||
bottom of `docs/project/backlog/README.md`.
|
||||
- **WP** — `docs/project/archive/backlog/WP-NN-<slug>.md`. Tracked work; use the template at the
|
||||
bottom of `docs/project/archive/backlog/README.md`.
|
||||
|
||||
Then **add the index row**: `docs/README.md` table for a reference doc/ADR,
|
||||
`docs/project/backlog/README.md` for a WP. A doc with no index row is invisible.
|
||||
`docs/project/archive/backlog/README.md` for a WP. A doc with no index row is invisible.
|
||||
|
||||
## Adjusted feature → update what already describes it
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ Work through that checklist, keeping the GREEN gate below passing at every step.
|
||||
re-target it at an arbitrary new context, so it's deleted alongside `registratie`, along
|
||||
with its three wiring lines in `shell.component.ts` (import, `imports:` entry, template tag).
|
||||
- The `dashboard` route is **not** deleted even though it currently imports
|
||||
`@registratie/ui/dashboard.page` — too much else hardcodes `/dashboard` (login's post-auth
|
||||
`@overzicht/ui/overzicht.page` — too much else hardcodes `/dashboard` (login's post-auth
|
||||
redirect, `authGuard`'s fallback, header nav/logo, breadcrumb trail, several stories/specs).
|
||||
The script rewrites its `loadComponent` to point at the freshly scaffolded `--context` page
|
||||
instead (a `TODO(create-frontend)` stopgap landing page, not a real overview).
|
||||
@@ -85,8 +85,8 @@ Work through that checklist, keeping the GREEN gate below passing at every step.
|
||||
placeholder path and creates an empty `public/<name>-huisstijl/` — it cannot generate a
|
||||
real house style. Vendor your CSS there, then re-point the `--rhc-*` bridge in
|
||||
`src/styles.scss` (ADR-0003 pattern: bridge, don't rewrite tokens), then `npm run check:tokens`.
|
||||
- `docs/project/backlog/` WPs, PRDs, and memory-specific docs — new portal, new backlog
|
||||
(keep `docs/project/backlog/README.md`'s WP process/template if you like the workflow).
|
||||
- `docs/project/archive/backlog/` WPs, PRDs, and memory-specific docs — new portal, new backlog
|
||||
(keep `docs/project/archive/backlog/README.md`'s WP process/template if you like the workflow).
|
||||
`docs/reference/scaffolding.md` also names `BigRegister.Api` in prose — update by hand.
|
||||
- `e2e/*.spec.ts` (`smoke.spec.ts`, `brief-v2.spec.ts`, `error-state.spec.ts`): full
|
||||
BIG-register user-flow tests (BSN login → registration wizard → submission assertions).
|
||||
|
||||
@@ -5,13 +5,13 @@ description: Add a shared UI building block (atom, molecule, organism) with its
|
||||
|
||||
# UI component (atom / molecule / organism)
|
||||
|
||||
First: check `shared/ui/` and `shared/layout/` — a new page should be composition of
|
||||
existing blocks. Only add a block when nothing fits.
|
||||
First: check `libs/shared/src/ui/` and `libs/shared/src/layout/` — a new page should be
|
||||
composition of existing blocks. Only add a block when nothing fits.
|
||||
|
||||
## Rules
|
||||
|
||||
- **Folder = atomic layer**: `shared/ui/` atoms → molecules → organisms;
|
||||
`shared/layout/` templates. Each level only uses levels below.
|
||||
- **Folder = atomic layer**: `libs/shared/src/ui/` atoms → molecules → organisms;
|
||||
`libs/shared/src/layout/` templates. Each level only uses levels below.
|
||||
- Standalone component, **English name** (shared = language-agnostic), signal
|
||||
`input()`s only, `inject()` over constructor DI.
|
||||
- **Atoms are thin wrappers over CIBG Huisstijl (Bootstrap 5.2) classes** (`btn`,
|
||||
@@ -21,7 +21,8 @@ existing blocks. Only add a block when nothing fits.
|
||||
- **Tokens only** — `var(--rhc-*)` / `var(--app-*)`, never hardcoded colors
|
||||
(`npm run check:tokens` fails the build; escape hatch: `token-ok` marker + reason).
|
||||
- **No hardcoded Dutch** in shared components — expose copy as `input()`s with
|
||||
`$localize` defaults; the domain caller supplies the text (see `shared/ui/async`).
|
||||
`$localize` defaults; the domain caller supplies the text (see
|
||||
`libs/shared/src/ui/molecules/async`).
|
||||
- Components with content-projected slots export a spread constant so callers import
|
||||
one thing: `export const ASYNC = [AsyncComponent, AsyncLoadedDirective, …] as const;`
|
||||
|
||||
@@ -41,9 +42,10 @@ UI test surface.
|
||||
|
||||
## Worked examples
|
||||
|
||||
- Atom: `src/app/shared/ui/button/` — typed variant API over `btn` classes.
|
||||
- Molecule: `src/app/shared/ui/async/` — slot directives, localizable input defaults, spread constant.
|
||||
- Template: `src/app/shared/layout/wizard-shell/` — the canonical wizard outline.
|
||||
- Atom: `libs/shared/src/ui/atoms/button/` — typed variant API over `btn` classes.
|
||||
- Molecule: `libs/shared/src/ui/molecules/async/` — slot directives, localizable input
|
||||
defaults, spread constant.
|
||||
- Template: `libs/shared/src/layout/wizard-shell/` — the canonical wizard outline.
|
||||
|
||||
## Verify
|
||||
|
||||
|
||||
@@ -109,6 +109,29 @@ module.exports = function buildConfig(contextAllowed, appName, tsConfigFileName)
|
||||
},
|
||||
},
|
||||
|
||||
// --- Atomic ladder within libs/shared/src/ui (folder = layer, CLAUDE.md decision 2) ---
|
||||
{
|
||||
name: 'atoms-compose-nothing-above',
|
||||
comment: 'An atom composes nothing above it — no molecule or organism. See CLAUDE.md §2.',
|
||||
severity: 'error',
|
||||
from: { path: '^libs/shared/src/ui/atoms/' },
|
||||
to: { path: '^libs/shared/src/ui/(molecules|organisms)/' },
|
||||
},
|
||||
{
|
||||
name: 'molecules-below-organisms',
|
||||
comment: 'A molecule composes nothing above it — no organism. See CLAUDE.md §2.',
|
||||
severity: 'error',
|
||||
from: { path: '^libs/shared/src/ui/molecules/' },
|
||||
to: { path: '^libs/shared/src/ui/organisms/' },
|
||||
},
|
||||
{
|
||||
name: 'design-system-not-layout',
|
||||
comment: 'The design system (ui/) does not depend on layout/ templates. See CLAUDE.md §2.',
|
||||
severity: 'error',
|
||||
from: { path: '^libs/shared/src/ui/' },
|
||||
to: { path: '^libs/shared/src/layout/' },
|
||||
},
|
||||
|
||||
{
|
||||
name: 'no-testing-in-production',
|
||||
comment:
|
||||
|
||||
@@ -124,8 +124,15 @@ jobs:
|
||||
# app's messages.en.xlf is missing a unit its source (WP-20) or libs/shared gains.
|
||||
- run: npx ng build ssp --localize && npx ng build behandelportal --localize
|
||||
if: needs.changes.outputs.frontend == 'true'
|
||||
# The shipped bundle must stay clean; dev-only advisories are excluded.
|
||||
- run: npm audit --omit=dev
|
||||
# The shipped bundle must stay clean; dev-only advisories are excluded. The gate is
|
||||
# `high`, not the default `low`, because two moderate Angular advisories stay open
|
||||
# while we hold at 22.0.5: GHSA-p297-fm68-3q8c and GHSA-hh8m-fm6v-7cvg. Neither is
|
||||
# reachable — the app calls no `withRequestsMadeViaParent` and no
|
||||
# `provideClientHydration`, and binds no untrusted value into a directive host
|
||||
# binding. The fix is Angular 22.1.x, which emits `var(--%NS%name)` and breaks every
|
||||
# `--rhc-*` token. Restore `low` after an Angular release substitutes the
|
||||
# placeholder; verify with `grep -rl '%NS%' dist/` after `npm run build`.
|
||||
- run: npm audit --omit=dev --audit-level=high
|
||||
if: needs.changes.outputs.frontend == 'true'
|
||||
|
||||
storybook-a11y:
|
||||
|
||||
+2
-2
@@ -28,5 +28,5 @@ plop-templates/
|
||||
backend/
|
||||
|
||||
# Agent prompts — their exact wording is the input, reflowing markdown edits the prompt
|
||||
docs/project/refactor-backlog-setup/agents/
|
||||
docs/project/refactor-backlog-setup/refactor-backlog/final-prompts/
|
||||
docs/project/archive/refactor-backlog-setup/agents/
|
||||
docs/project/archive/refactor-backlog-setup/refactor-backlog/final-prompts/
|
||||
|
||||
@@ -12,7 +12,7 @@ signals. Auth is faked; **data and business rules are served by a minimal ASP.NE
|
||||
Core backend** (`backend/`, see its README) and consumed through an NSwag-generated
|
||||
typed client. The FE renders the backend's decisions. Reference data mimicking
|
||||
BRP/DUO (`Data/SeedData.cs`) is in-memory; applications, documents and the brief
|
||||
persist to a SQLite file via EF Core (WP-22) — `docs/project/backlog/WP-22-durable-persistence.md`.
|
||||
persist to a SQLite file via EF Core (WP-22) — `docs/project/archive/backlog/WP-22-durable-persistence.md`.
|
||||
|
||||
**Monorepo (WP-67):** two Angular projects share one backend + one shared library —
|
||||
`apps/ssp` (Zorgverlener self-service, this doc's main subject) and `apps/behandelportal`
|
||||
@@ -34,6 +34,7 @@ npm run gen:api # regenerate the ONE typed client (libs/shared) f
|
||||
npm run ci # run the CI gate locally BEFORE pushing (mirrors ci.yml); `npm run ci --full` adds storybook-a11y
|
||||
docker compose up # run both FE apps + backend together (Swagger at :5000/swagger)
|
||||
cd backend && dotnet test # backend rule + endpoint tests
|
||||
task # list every task (a thin facade over the commands above)
|
||||
```
|
||||
|
||||
**Two Storybook instances, not one:** `apps/ssp` and `apps/behandelportal` each have their own
|
||||
@@ -57,7 +58,10 @@ catches a miss before CI does.
|
||||
|
||||
`.npmrc` sets `legacy-peer-deps=true` (Storybook's peer range lags Angular 22).
|
||||
Do not run `npm audit fix --force` — it downgrades Angular 22→21. Dev-only
|
||||
advisories are pinned via `package.json` `overrides`; the shipped bundle audits clean.
|
||||
advisories are pinned via `package.json` `overrides`. Angular is pinned to the exact
|
||||
version 22.0.5: 22.1.x emits `var(--%NS%name)` and breaks every `--rhc-*` token, so the
|
||||
two moderate advisories it fixes stay open. Neither is reachable, so the audit gate runs
|
||||
at `--audit-level=high` (see the comment in `ci.yml`).
|
||||
|
||||
## Model routing for agent delegation
|
||||
|
||||
@@ -72,7 +76,7 @@ so this doesn't depend on a human remembering to run `/model` at the right momen
|
||||
`git status`/`grep`, verifying a file exists. No Edit/Write access.
|
||||
|
||||
Delegate to the matching agent only when the _current_ session isn't already on that
|
||||
model — don't add indirection for its own sake. `docs/project/backlog/README.md`'s
|
||||
model — don't add indirection for its own sake. `docs/project/archive/backlog/README.md`'s
|
||||
session protocol is the worked example of this in practice.
|
||||
|
||||
## The decisions (non-negotiable working agreements)
|
||||
@@ -81,10 +85,11 @@ session protocol is the worked example of this in practice.
|
||||
|
||||
`apps/<app>/src/app/<context>/<layer>/` for an app-local context; `libs/<lib>/src/<layer>/`
|
||||
for a cross-app library (WP-67). Two apps today: `apps/ssp` (Zorgverlener self-service —
|
||||
contexts `auth`, `registratie`, `herregistratie`, `brief` (letter-composition teaching
|
||||
slice), `showcase` (teaching page, not a feature; **sanctioned** to read every context in
|
||||
its own app — nothing imports it)) and `apps/behandelportal` (Behandelaar backoffice,
|
||||
ADR-0002 — contexts `auth`, `behandeling`). Two cross-app libraries: `libs/shared` (the
|
||||
contexts `auth`, `overzicht` (the portal home; composes `registratie`'s dashboard sections
|
||||
plus its own cross-context nav sections), `registratie`, `herregistratie`, `brief`
|
||||
(letter-composition teaching slice), `showcase` (teaching page, not a feature; **sanctioned**
|
||||
to read every context in its own app — nothing imports it)) and `apps/behandelportal`
|
||||
(Behandelaar backoffice, ADR-0002 — contexts `auth`, `behandeling`). Two cross-app libraries: `libs/shared` (the
|
||||
design system + kernel + generated API client — no business logic) and `libs/beheer` (the
|
||||
admin/stamdata context, identical for both apps today — WP-67 folded a silently-diverging
|
||||
duplicate copy back into one). `auth` is deliberately **not** shared even though today it's
|
||||
@@ -106,10 +111,11 @@ depend on `libs/beheer` either — it stays the base). `ui`/`layout` never impor
|
||||
lint-enforced (per app, since each app is cruised against its own tsconfig — WP-67's
|
||||
`.dependency-cruiser.base.js` + one thin `.dependency-cruiser.<app>.js` per app). An app
|
||||
may not import the other app's source directly. Cross-context only
|
||||
`herregistratie → registratie → libs/shared|beheer`, `auth → libs/shared|beheer`,
|
||||
`brief → libs/shared|beheer` (ssp); `behandeling → libs/shared|beheer`, `auth →
|
||||
libs/shared|beheer` (behandelportal). Imports use aliases as direction statements:
|
||||
`@shared/* @beheer/* @auth/* @registratie/* @herregistratie/* @brief/*` (ssp) —
|
||||
`overzicht → registratie → libs/shared|beheer`, `herregistratie → registratie →
|
||||
libs/shared|beheer`, `auth → libs/shared|beheer`, `brief → libs/shared|beheer` (ssp);
|
||||
`behandeling → libs/shared|beheer`, `auth → libs/shared|beheer` (behandelportal). Imports use
|
||||
aliases as direction statements:
|
||||
`@shared/* @beheer/* @auth/* @overzicht/* @registratie/* @herregistratie/* @brief/*` (ssp) —
|
||||
`@shared/* @beheer/* @auth/* @behandeling/*` (behandelportal); each app's own
|
||||
`tsconfig.json` declares its full map (the root `tsconfig.json` intentionally has no
|
||||
`paths` — see its comment). `domain/` imports nothing from Angular.
|
||||
@@ -129,6 +135,18 @@ the design system does the visuals. (Where CIBG lacks a class — e.g. `skeleton
|
||||
`// CIBG-GAP EXTENSION:` marker; see ADR-0003. `alert` is **not** such a case: it wraps the
|
||||
vendored `.feedback feedback-*` classes.)
|
||||
|
||||
**The step-component contract.** A wizard step follows the same rule as
|
||||
`address-fields.component.ts`: values in, events out, no internal state. Three clauses:
|
||||
|
||||
1. **Inputs down.** A step reads its data only from `input()`s the container passes it.
|
||||
2. **One narrow output up.** A step emits one specific event, not the container's whole
|
||||
`dispatch`.
|
||||
3. **`dispatch` is never passed down.** The container owns the Model and decides what a
|
||||
step's event means; a step never calls `dispatch` itself.
|
||||
|
||||
Corollary: a step gets **no** story of its own. The wizard's own story already mounts every
|
||||
step, because it seeds the machine.
|
||||
|
||||
### 3. State: make illegal states unrepresentable
|
||||
|
||||
Default reflex — **if you're about to add a second/third boolean to track state,
|
||||
@@ -136,7 +154,7 @@ model a discriminated union instead.** Three tools, all in `libs/shared/src/appl
|
||||
|
||||
- **`RemoteData<E,T>`** (`remote-data.ts`) — `Loading | Empty | Failure{error} | Success{value}`.
|
||||
Combine sources with `map`/`map2`/`andThen` (Failure > Loading > Success).
|
||||
Render it via the `<app-async>` molecule (`libs/shared/src/ui/async`) — one of four
|
||||
Render it via the `<app-async>` molecule (`libs/shared/src/ui/molecules/async`) — one of four
|
||||
templates, mutually exclusive by construction. Default loading spinner/skeleton
|
||||
is delay-gated (~250ms) so fast connections don't flash.
|
||||
- **Elm-style store** (`store.ts` → `createStore(initial, reduce)`) — all state in
|
||||
@@ -210,10 +228,10 @@ each app has its **own Storybook instance** (`.storybook-ssp/`, `.storybook-beha
|
||||
WP-67 — a single merged tsconfig can't resolve both apps' `@auth/*` at once), each globbing
|
||||
its own app's stories plus both shared libraries'. **Story titles mirror the sidebar's
|
||||
Design System/Domein split** (see `libs/shared/docs/layers.mdx`): a `libs/shared/ui|layout`
|
||||
or `libs/beheer/ui` component is titled `Design System/<Atoms|Molecules|Organisms|Templates|Devtools>/<Name>`;
|
||||
a component in an app context's `ui/` is titled `Domein/<Context>/<Name>` — full stop,
|
||||
regardless of which atomic layer it is (a context organism doesn't get its own
|
||||
`Organisms/` bucket).
|
||||
component is titled `Design System/<Atoms|Molecules|Organisms|Templates|Devtools>/<Name>`;
|
||||
a component in an app context's `ui/`, or in `libs/beheer/ui`, is titled
|
||||
`Domein/<Context>/<Name>` — full stop, regardless of which atomic layer it is (a context
|
||||
organism doesn't get its own `Organisms/` bucket).
|
||||
|
||||
## Conventions
|
||||
|
||||
@@ -242,7 +260,7 @@ regardless of which atomic layer it is (a context organism doesn't get its own
|
||||
(`` $localize`:@@context.key:Tekst` ``). Source locale is `nl`; a second locale is a
|
||||
translation file, not a code change (the seam). Shared/English components must **not**
|
||||
hardcode Dutch — expose copy as `input()`s with localizable defaults; the domain caller
|
||||
supplies the text (see `libs/shared/src/ui/async`). Format-validation messages in
|
||||
supplies the text (see `libs/shared/src/ui/molecules/async`). Format-validation messages in
|
||||
`domain/value-objects/` stay co-located but are still `$localize`-wrapped.
|
||||
- **Forms = one idiom.** Any form with validation or submission uses a `*.machine.ts`
|
||||
(Model/Msg/reduce) + value objects + a `submit-*` command returning `Result` — the
|
||||
@@ -272,7 +290,10 @@ regardless of which atomic layer it is (a context organism doesn't get its own
|
||||
- Prettier; `.editorconfig`. tsconfig: `noImplicitReturns`,
|
||||
`noPropertyAccessFromIndexSignature`, `noFallthroughCasesInSwitch`, `isolatedModules`.
|
||||
- **Enforced, not just hoped-for:** `npm run lint` (`eslint.config.mjs`, scoped to
|
||||
`{apps,libs}/**`) fails the build on `any`; `npm run dep:check`
|
||||
`{apps,libs}/**`) fails the build on `any`; the same config's `max-lines` rule caps every
|
||||
`{apps,libs}/**/*.{page,component,section,step}.ts` file at 250 lines
|
||||
(`skipBlankLines: true`, `skipComments: true`). 250 is reachable, not a style-guide
|
||||
default — the dashboard page lands at 42 lines. `npm run dep:check`
|
||||
(`.dependency-cruiser.base.js` + one `.dependency-cruiser.<app>.js` per app, WP-67) fails
|
||||
on illegal imports — `domain/` importing Angular, a context importing "upward" (the
|
||||
`herregistratie → registratie → shared`, `auth → shared` direction), an app importing the
|
||||
|
||||
@@ -66,7 +66,7 @@ Admin pages (`/beheer/*`, `/brief/huisstijl`) need the `admin` role — see
|
||||
| know _why_ a decision was made | [the ADRs](docs/reference/architecture/) — BFF-lite, contexts, huisstijl, stamdata, ZGW, test data |
|
||||
| work on the backend / BFF | [backend/README.md](backend/README.md) |
|
||||
| run OpenZaak locally | [backend/openzaak/README.md](backend/openzaak/README.md) |
|
||||
| see what shipped, or pick up work | [docs/project/backlog/README.md](docs/project/backlog/README.md) |
|
||||
| see what shipped, or pick up work | [docs/project/archive/backlog/README.md](docs/project/archive/backlog/README.md) |
|
||||
| build a feature the house way | [`.claude/skills/`](.claude/skills/) — invocable recipes (`new-feature`, `form-machine`, …) |
|
||||
| know the import rules | [dependencies.md](docs/reference/architecture/dependencies.md) — enforced by `dep:check` |
|
||||
| work on this repo as an AI agent | [CLAUDE.md](CLAUDE.md) |
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
# ponytail: a thin façade over the npm scripts + dotnet + docker, NOT a second copy of
|
||||
# package.json. `task` (no args) lists everything runnable; the logic stays where it is —
|
||||
# `task ci` shells to scripts/ci-local.sh, which remains the gate. npm keeps working
|
||||
# exactly as before, and CI does not depend on `task` being installed.
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
default:
|
||||
silent: true
|
||||
cmd: task --list
|
||||
|
||||
deps:
|
||||
internal: true
|
||||
status:
|
||||
- test -d node_modules
|
||||
cmd: npm ci
|
||||
|
||||
dev:
|
||||
desc: Serve the SSP on :4200 (proxies /api to the backend)
|
||||
deps: [deps]
|
||||
cmd: npm start
|
||||
|
||||
dev:bp:
|
||||
desc: Serve the behandelportal on :4201
|
||||
deps: [deps]
|
||||
cmd: npm run start:behandelportal
|
||||
|
||||
api:
|
||||
desc: Run the backend on :5000 (Swagger at /swagger)
|
||||
dir: backend
|
||||
cmd: dotnet run --project src/BigRegister.Api
|
||||
|
||||
test:
|
||||
desc: Run the frontend tests (both apps + both shared libraries)
|
||||
deps: [deps]
|
||||
cmd: npm test
|
||||
|
||||
test:api:
|
||||
desc: Run the backend rule + endpoint tests (skips the OpenZaak integration tests)
|
||||
dir: backend
|
||||
# Same filter as scripts/ci-local.sh: the Integration tests need a running OpenZaak
|
||||
# container (scripts/openzaak-ui-up.sh), so they are not part of the default gate.
|
||||
cmd: dotnet test BigRegister.slnx --filter "Category!=Integration"
|
||||
|
||||
lint:
|
||||
desc: Lint the frontend (no `any`, import/layer boundaries)
|
||||
deps: [deps]
|
||||
cmd: npm run lint
|
||||
|
||||
fmt:
|
||||
desc: Format the frontend with prettier and the backend with dotnet format
|
||||
deps: [deps]
|
||||
cmds:
|
||||
- npm run format
|
||||
- dotnet format backend/BigRegister.slnx
|
||||
|
||||
build:
|
||||
desc: Build both Angular apps
|
||||
deps: [deps]
|
||||
cmd: npm run build
|
||||
|
||||
ci:
|
||||
desc: Run the CI gate locally before pushing (scripts/ci-local.sh)
|
||||
deps: [deps]
|
||||
cmd: npm run ci
|
||||
|
||||
e2e:
|
||||
desc: Run the Playwright end-to-end tests (starts its own servers)
|
||||
deps: [deps]
|
||||
cmd: npm run e2e
|
||||
|
||||
sb:
|
||||
desc: Run the SSP Storybook instance
|
||||
deps: [deps]
|
||||
cmd: npm run storybook
|
||||
|
||||
sb:bp:
|
||||
desc: Run the behandelportal Storybook instance
|
||||
deps: [deps]
|
||||
cmd: npm run storybook:behandelportal
|
||||
|
||||
gen:api:
|
||||
desc: Regenerate the typed API client from the backend OpenAPI document
|
||||
deps: [deps]
|
||||
cmd: npm run gen:api
|
||||
|
||||
up:
|
||||
desc: Start both apps + the backend with docker compose
|
||||
cmd: docker compose up
|
||||
|
||||
down:
|
||||
desc: Stop the docker compose stack
|
||||
cmd: docker compose down
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, output } from '@angular/core';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { ButtonComponent } from '@shared/ui/atoms/button/button.component';
|
||||
|
||||
/**
|
||||
* Organism: employee-SSO-style mock login (ADR-C-004). No real auth — and,
|
||||
|
||||
@@ -2,12 +2,12 @@ import { Component, computed, inject } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { successOf } from '@shared/application/remote-data';
|
||||
import { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';
|
||||
import { DataBlockComponent } from '@shared/ui/data-block/data-block.component';
|
||||
import { DataRowComponent } from '@shared/ui/data-row/data-row.component';
|
||||
import { ASYNC } from '@shared/ui/async/async.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { ButtonComponent } from '@shared/ui/atoms/button/button.component';
|
||||
import { SkeletonComponent } from '@shared/ui/atoms/skeleton/skeleton.component';
|
||||
import { DataBlockComponent } from '@shared/ui/molecules/data-block/data-block.component';
|
||||
import { DataRowComponent } from '@shared/ui/molecules/data-row/data-row.component';
|
||||
import { ASYNC } from '@shared/ui/molecules/async/async.component';
|
||||
import { BeoordelingStore } from '@behandeling/application/beoordeling.store';
|
||||
import { detailRows } from '@behandeling/domain/beoordeling-view';
|
||||
import { BeoordelingDocumentenComponent } from '@behandeling/ui/beoordeling-documenten/beoordeling-documenten.component';
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { Component, computed, input, output } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { FormFieldComponent } from '@shared/ui/form-field/form-field.component';
|
||||
import { TextInputComponent } from '@shared/ui/text-input/text-input.component';
|
||||
import { RadioGroupComponent, RadioOption } from '@shared/ui/radio-group/radio-group.component';
|
||||
import { DataBlockComponent } from '@shared/ui/data-block/data-block.component';
|
||||
import { DataRowComponent } from '@shared/ui/data-row/data-row.component';
|
||||
import { ButtonComponent } from '@shared/ui/atoms/button/button.component';
|
||||
import { HeadingComponent } from '@shared/ui/atoms/heading/heading.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { FormFieldComponent } from '@shared/ui/molecules/form-field/form-field.component';
|
||||
import { TextInputComponent } from '@shared/ui/atoms/text-input/text-input.component';
|
||||
import {
|
||||
RadioGroupComponent,
|
||||
RadioOption,
|
||||
} from '@shared/ui/atoms/radio-group/radio-group.component';
|
||||
import { DataBlockComponent } from '@shared/ui/molecules/data-block/data-block.component';
|
||||
import { DataRowComponent } from '@shared/ui/molecules/data-row/data-row.component';
|
||||
import { createStore } from '@shared/application/store';
|
||||
import { whenTag } from '@shared/kernel/fp';
|
||||
import { BesluitState, BesluitMsg, initial, reduce } from '@behandeling/domain/besluit.machine';
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import { Component, input } from '@angular/core';
|
||||
import { ApplicationListComponent } from '@shared/ui/application-list/application-list.component';
|
||||
import { ApplicationLinkComponent } from '@shared/ui/application-link/application-link.component';
|
||||
import { ApplicationListComponent } from '@shared/ui/molecules/application-list/application-list.component';
|
||||
import { ApplicationLinkComponent } from '@shared/ui/molecules/application-link/application-link.component';
|
||||
import { WerkvoorraadItem } from '@behandeling/domain/werkvoorraad-item';
|
||||
import { werkvoorraadRow } from '@behandeling/domain/werkvoorraad-item-view';
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Component, computed, effect, inject } from '@angular/core';
|
||||
import { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';
|
||||
import { ASYNC } from '@shared/ui/async/async.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { ButtonComponent } from '@shared/ui/atoms/button/button.component';
|
||||
import { SkeletonComponent } from '@shared/ui/atoms/skeleton/skeleton.component';
|
||||
import { ASYNC } from '@shared/ui/molecules/async/async.component';
|
||||
import { AccessStore } from '@shared/application/access.store';
|
||||
import { successOr } from '@shared/application/remote-data';
|
||||
import { WerkvoorraadStore } from '@behandeling/application/werkvoorraad.store';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Component, output } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { FormFieldComponent } from '@shared/ui/form-field/form-field.component';
|
||||
import { TextInputComponent } from '@shared/ui/text-input/text-input.component';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { FormFieldComponent } from '@shared/ui/molecules/form-field/form-field.component';
|
||||
import { TextInputComponent } from '@shared/ui/atoms/text-input/text-input.component';
|
||||
import { ButtonComponent } from '@shared/ui/atoms/button/button.component';
|
||||
|
||||
/** Organism: DigiD-style mock login. No real auth — just composes atoms/molecules. */
|
||||
@Component({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, inject, signal } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { LoginFormComponent } from '@auth/ui/login-form/login-form.component';
|
||||
import { SessionStore } from '@auth/application/session.store';
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Component, ElementRef, computed, input, output, viewChild } from '@angular/core';
|
||||
import { PlaceholderOption } from '@shared/ui/rich-text-editor/rich-text-editor.component';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { MaskedValueComponent } from '@shared/ui/masked-value/masked-value.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { StepperComponent } from '@shared/ui/stepper/stepper.component';
|
||||
import { PlaceholderOption } from '@shared/ui/molecules/rich-text-editor/rich-text-editor.component';
|
||||
import { ButtonComponent } from '@shared/ui/atoms/button/button.component';
|
||||
import { HeadingComponent } from '@shared/ui/atoms/heading/heading.component';
|
||||
import { MaskedValueComponent } from '@shared/ui/atoms/masked-value/masked-value.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { StepperComponent } from '@shared/ui/molecules/stepper/stepper.component';
|
||||
import { Besluit, Brief, CaseContext, LibraryPassage } from '@brief/domain/brief';
|
||||
import { besluitGuidance, inferSelection } from '@brief/domain/besluit';
|
||||
import { Diagnostic } from '@brief/domain/placeholders';
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { Component, computed, input, linkedSignal, output } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { CheckboxComponent } from '@shared/ui/checkbox/checkbox.component';
|
||||
import { RadioGroupComponent, RadioOption } from '@shared/ui/radio-group/radio-group.component';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { CheckboxComponent } from '@shared/ui/atoms/checkbox/checkbox.component';
|
||||
import {
|
||||
RadioGroupComponent,
|
||||
RadioOption,
|
||||
} from '@shared/ui/atoms/radio-group/radio-group.component';
|
||||
import { HeadingComponent } from '@shared/ui/atoms/heading/heading.component';
|
||||
import { Besluit, LibraryPassage } from '@brief/domain/brief';
|
||||
import { redenenFor } from '@brief/domain/besluit';
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Component, computed, inject } from '@angular/core';
|
||||
import { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { ASYNC } from '@shared/ui/async/async.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { ButtonComponent } from '@shared/ui/atoms/button/button.component';
|
||||
import { ASYNC } from '@shared/ui/molecules/async/async.component';
|
||||
import { BriefStore } from '@brief/application/brief.store';
|
||||
import { LetterComposerComponent } from '@brief/ui/letter-composer/letter-composer.component';
|
||||
import { BehandelSchermComponent } from '@brief/ui/behandel-scherm/behandel-scherm.component';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, computed, input, output } from '@angular/core';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { Diagnostic } from '@brief/domain/placeholders';
|
||||
|
||||
/** Molecule: lists all letter diagnostics grouped by severity. Errors block
|
||||
|
||||
@@ -3,8 +3,8 @@ import { RichTextBlock } from '@shared/kernel/rich-text';
|
||||
import {
|
||||
RichTextEditorComponent,
|
||||
PlaceholderOption,
|
||||
} from '@shared/ui/rich-text-editor/rich-text-editor.component';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
} from '@shared/ui/molecules/rich-text-editor/rich-text-editor.component';
|
||||
import { ButtonComponent } from '@shared/ui/atoms/button/button.component';
|
||||
import { LetterBlock } from '@brief/domain/brief';
|
||||
|
||||
/** Molecule: one block in a section — its editor plus provenance + block controls.
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
signal,
|
||||
viewChild,
|
||||
} from '@angular/core';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { ButtonComponent } from '@shared/ui/atoms/button/button.component';
|
||||
import { formatDatumNl } from '@shared/kernel/datum';
|
||||
import { Paragraph } from '@shared/kernel/rich-text';
|
||||
import { Brief, LetterBlock } from '@brief/domain/brief';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, computed, input } from '@angular/core';
|
||||
import { PlaceholderChipComponent } from '@shared/ui/placeholder-chip/placeholder-chip.component';
|
||||
import { PlaceholderChipComponent } from '@shared/ui/atoms/placeholder-chip/placeholder-chip.component';
|
||||
import { RichTextNode } from '@shared/kernel/rich-text';
|
||||
import { Diagnostic, PlaceholderDef } from '@brief/domain/placeholders';
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Component, computed, input, output, signal } from '@angular/core';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { StatusBadgeComponent } from '@shared/ui/status-badge/status-badge.component';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { HeadingComponent } from '@shared/ui/atoms/heading/heading.component';
|
||||
import { StatusBadgeComponent } from '@shared/ui/atoms/status-badge/status-badge.component';
|
||||
import { ButtonComponent } from '@shared/ui/atoms/button/button.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { Brief } from '@brief/domain/brief';
|
||||
import { OrgTemplate } from '@brief/domain/org-template';
|
||||
import { Diagnostic } from '@brief/domain/placeholders';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, computed, input, output } from '@angular/core';
|
||||
import { PlaceholderOption } from '@shared/ui/rich-text-editor/rich-text-editor.component';
|
||||
import { PlaceholderOption } from '@shared/ui/molecules/rich-text-editor/rich-text-editor.component';
|
||||
import { Brief } from '@brief/domain/brief';
|
||||
import { BriefMsg } from '@brief/domain/brief.machine';
|
||||
import { LetterSectionComponent } from '@brief/ui/letter-section/letter-section.component';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Component, input, output } from '@angular/core';
|
||||
import { RichTextBlock } from '@shared/kernel/rich-text';
|
||||
import { PlaceholderOption } from '@shared/ui/rich-text-editor/rich-text-editor.component';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { PlaceholderOption } from '@shared/ui/molecules/rich-text-editor/rich-text-editor.component';
|
||||
import { ButtonComponent } from '@shared/ui/atoms/button/button.component';
|
||||
import { HeadingComponent } from '@shared/ui/atoms/heading/heading.component';
|
||||
import { LetterSection } from '@brief/domain/brief';
|
||||
import { BriefMsg } from '@brief/domain/brief.machine';
|
||||
import { LetterBlockComponent } from '@brief/ui/letter-block/letter-block.component';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Component, computed, input, output } from '@angular/core';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { FileInputComponent } from '@shared/ui/upload/file-input/file-input.component';
|
||||
import { SingleUploadComponent } from '@shared/ui/upload/single-upload/single-upload.component';
|
||||
import { HeadingComponent } from '@shared/ui/atoms/heading/heading.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { FileInputComponent } from '@shared/ui/atoms/upload/file-input/file-input.component';
|
||||
import { SingleUploadComponent } from '@shared/ui/molecules/upload/single-upload/single-upload.component';
|
||||
import { UploadState } from '@shared/domain/upload.machine';
|
||||
|
||||
const LOGO_CATEGORY = 'org-logo';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component, computed, input, output } from '@angular/core';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { ButtonComponent } from '@shared/ui/atoms/button/button.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { UploadState } from '@shared/domain/upload.machine';
|
||||
import { Brief } from '@brief/domain/brief';
|
||||
import { SAMPLE_LETTER_BRIEF } from '@brief/domain/sample-letter';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, input, output } from '@angular/core';
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { HeadingComponent } from '@shared/ui/atoms/heading/heading.component';
|
||||
import { ButtonComponent } from '@shared/ui/atoms/button/button.component';
|
||||
import { OrgTemplateVersion } from '@brief/domain/org-template';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Component, computed, effect, inject } from '@angular/core';
|
||||
import { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { ASYNC } from '@shared/ui/async/async.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { ButtonComponent } from '@shared/ui/atoms/button/button.component';
|
||||
import { ASYNC } from '@shared/ui/molecules/async/async.component';
|
||||
import { AccessStore } from '@shared/application/access.store';
|
||||
import { OrgTemplateStore } from '@brief/application/org-template.store';
|
||||
import { OrgTemplateEditorComponent } from '@brief/ui/org-template-editor/org-template-editor.component';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Component, computed, input, output, signal } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { CheckboxComponent } from '@shared/ui/checkbox/checkbox.component';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { TextInputComponent } from '@shared/ui/text-input/text-input.component';
|
||||
import { CheckboxComponent } from '@shared/ui/atoms/checkbox/checkbox.component';
|
||||
import { ButtonComponent } from '@shared/ui/atoms/button/button.component';
|
||||
import { TextInputComponent } from '@shared/ui/atoms/text-input/text-input.component';
|
||||
import { textOf } from '@shared/kernel/rich-text';
|
||||
import { LibraryPassage } from '@brief/domain/brief';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, input, output, signal } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { ButtonComponent } from '@shared/ui/atoms/button/button.component';
|
||||
|
||||
/** Molecule: shows the rejection comments (drafter view) or collects them from the
|
||||
approver. The approver rejects WITH comments; they never edit the letter. */
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
reduceUpload,
|
||||
requiredCategoriesSatisfied,
|
||||
deliveryRefs,
|
||||
digitalDocumentIds,
|
||||
} from '@shared/domain/upload.machine';
|
||||
|
||||
/** What the user is typing (raw, possibly invalid). */
|
||||
@@ -53,7 +54,7 @@ export function hasProgress(s: Extract<WizardState, { tag: 'Editing' }>): boolea
|
||||
!!s.draft.uren ||
|
||||
!!s.draft.jaren ||
|
||||
!!s.draft.punten ||
|
||||
deliveryRefs(s.upload).some((r) => r.channel === 'digital' && !!r.documentId)
|
||||
digitalDocumentIds(s.upload).length > 0
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+53
-44
@@ -1,8 +1,8 @@
|
||||
import { Component, computed, inject, input } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { FormFieldComponent } from '@shared/ui/form-field/form-field.component';
|
||||
import { TextInputComponent } from '@shared/ui/text-input/text-input.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { FormFieldComponent } from '@shared/ui/molecules/form-field/form-field.component';
|
||||
import { TextInputComponent } from '@shared/ui/atoms/text-input/text-input.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import {
|
||||
WizardShellComponent,
|
||||
WizardError,
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
naarStapLabel,
|
||||
} from '@shared/layout/wizard-shell/wizard-shell.component';
|
||||
import { toWizardErrors } from '@shared/layout/wizard-shell/wizard-errors';
|
||||
import { ConfirmationComponent } from '@shared/ui/confirmation/confirmation.component';
|
||||
import { ConfirmationComponent } from '@shared/ui/molecules/confirmation/confirmation.component';
|
||||
import { createStore } from '@shared/application/store';
|
||||
import { whenTag } from '@shared/kernel/fp';
|
||||
import { BigProfileStore } from '@registratie/application/big-profile.store';
|
||||
@@ -23,9 +23,9 @@ import {
|
||||
hasProgress,
|
||||
} from '@herregistratie/domain/herregistratie.machine';
|
||||
import { createDraftSync } from '@registratie/application/draft-sync';
|
||||
import { DocumentUploadComponent } from '@shared/ui/upload/document-upload/document-upload.component';
|
||||
import { DocumentUploadComponent } from '@shared/ui/organisms/upload/document-upload/document-upload.component';
|
||||
import { createUploadController } from '@shared/application/upload-controller';
|
||||
import { UploadState, initialUpload, deliveryRefs } from '@shared/domain/upload.machine';
|
||||
import { UploadState, initialUpload, digitalDocumentIds } from '@shared/domain/upload.machine';
|
||||
|
||||
/** Organism: multi-step herregistratie wizard. ALL state lives in one signal
|
||||
driven by the pure `reduce` function (see herregistratie.machine.ts) via an
|
||||
@@ -148,8 +148,14 @@ import { UploadState, initialUpload, deliveryRefs } from '@shared/domain/upload.
|
||||
})
|
||||
export class HerregistratieWizardComponent {
|
||||
private profile = inject(BigProfileStore);
|
||||
// Effect fires once, on Editing -> Submitting (RD-05's tag-transition rule; `Seed` is
|
||||
// exempt, so a story mounting straight into `Submitting` does not call the network).
|
||||
|
||||
/** Optional seed so Storybook / the showcase can mount any state directly. */
|
||||
seed = input<WizardState>(initial);
|
||||
|
||||
// --- The store: all state in one signal, changed only by a pure reduce -----
|
||||
// The effect fires once, on the `Editing -> Submitting` transition. `Seed` is exempt,
|
||||
// so a story that mounts straight into `Submitting` does not call the network.
|
||||
// `draftSync` is declared below (both callbacks are deferred, so the cycle is safe).
|
||||
private store = createStore<WizardState, WizardMsg>(initial, reduce, {
|
||||
Submitting: async (s, store) => {
|
||||
this.profile.beginHerregistratie();
|
||||
@@ -163,36 +169,10 @@ export class HerregistratieWizardComponent {
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/** Preview/download link for a completed upload; delegates to the upload
|
||||
controller (application layer), which knows the dev-simulation `demo-*` ids
|
||||
have no stored bytes and returns no link for them. */
|
||||
protected previewUrlFor = (documentId: string): string | undefined =>
|
||||
this.uploadCtl.previewUrlFor(documentId);
|
||||
|
||||
/** Optional seed so Storybook / the showcase can mount any state directly. */
|
||||
seed = input<WizardState>(initial);
|
||||
|
||||
readonly state = this.store.model; // public so the showcase can highlight the live state
|
||||
protected dispatch = this.store.dispatch;
|
||||
|
||||
// Backend draft-sync (new persistence for this wizard): create a Concept on first
|
||||
// progress, debounced-sync the snapshot, resume by `?aanvraag=<id>`.
|
||||
private draftSync = createDraftSync({
|
||||
type: 'herregistratie',
|
||||
snapshot: () => {
|
||||
const s = this.state();
|
||||
if (s.tag !== 'Editing' || !hasProgress(s)) return null;
|
||||
const documentIds = deliveryRefs(s.upload)
|
||||
.filter((r) => r.channel === 'digital' && r.documentId)
|
||||
.map((r) => r.documentId!);
|
||||
return { draft: s, stepIndex: s.step - 1, stepCount: this.stepLabels.length, documentIds };
|
||||
},
|
||||
onResume: (draft) => this.dispatch({ tag: 'Seed', state: draft as WizardState }),
|
||||
enabled: () => this.seed() === initial,
|
||||
});
|
||||
|
||||
// Stepper labels + per-step heading titles (presentational only).
|
||||
// --- Static copy: stepper labels and per-step headings ---------------------
|
||||
readonly stepLabels = [
|
||||
$localize`:@@herregWizard.step.werkervaring:Werkervaring`,
|
||||
$localize`:@@herregWizard.step.nascholing:Nascholing`,
|
||||
@@ -204,6 +184,7 @@ export class HerregistratieWizardComponent {
|
||||
$localize`:@@herregWizard.title.documenten:Documenten aanleveren`,
|
||||
];
|
||||
|
||||
// --- State projections: one narrow, then read-only views of it -------------
|
||||
private editing = computed(() => whenTag(this.state(), 'Editing'));
|
||||
protected step = computed(() => this.editing()?.step ?? 1);
|
||||
protected draft = computed<Draft>(
|
||||
@@ -214,11 +195,35 @@ export class HerregistratieWizardComponent {
|
||||
protected errJaren = computed(() => this.editing()?.errors.jaren ?? '');
|
||||
protected errPunten = computed(() => this.editing()?.errors.punten ?? '');
|
||||
protected errDocumenten = computed(() => this.editing()?.errors.documenten ?? '');
|
||||
|
||||
// --- Controllers: persistence and uploads ----------------------------------
|
||||
// Create a Concept on first progress, then debounced-sync the snapshot.
|
||||
// `?aanvraag=<id>` resumes it.
|
||||
private draftSync = createDraftSync({
|
||||
type: 'herregistratie',
|
||||
snapshot: () => {
|
||||
const s = this.state();
|
||||
if (s.tag !== 'Editing' || !hasProgress(s)) return null;
|
||||
return {
|
||||
draft: s,
|
||||
stepIndex: s.step - 1,
|
||||
stepCount: this.stepLabels.length,
|
||||
documentIds: digitalDocumentIds(s.upload),
|
||||
};
|
||||
},
|
||||
onResume: (draft) => this.dispatch({ tag: 'Seed', state: draft as WizardState }),
|
||||
enabled: () => this.seed() === initial,
|
||||
});
|
||||
protected uploadCtl = createUploadController({
|
||||
wizardId: 'herregistratie',
|
||||
getUpload: () => this.upload(),
|
||||
dispatch: (msg) => this.dispatch({ tag: 'Upload', msg }),
|
||||
});
|
||||
/** Preview/download link for a completed upload; delegates to the upload
|
||||
controller (application layer), which knows the dev-simulation `demo-*` ids
|
||||
have no stored bytes and returns no link for them. */
|
||||
protected previewUrlFor = (documentId: string): string | undefined =>
|
||||
this.uploadCtl.previewUrlFor(documentId);
|
||||
|
||||
// --- Presentational wiring for the shared wizard shell ---------------------
|
||||
protected stepTitle = computed(() => this.stepTitles[this.step() - 1]);
|
||||
@@ -228,11 +233,6 @@ export class HerregistratieWizardComponent {
|
||||
? naarStapLabel(step + 1, this.stepLabels[step])
|
||||
: $localize`:@@herregWizard.indienen:Herregistratie aanvragen`;
|
||||
});
|
||||
|
||||
/** Stepper emits a 0-based index for an earlier (visited) step. */
|
||||
protected goToStep(index: number) {
|
||||
this.dispatch({ tag: 'GaNaarStap', step: (index + 1) as 1 | 2 | 3 });
|
||||
}
|
||||
/** Maps this machine's own tags onto the shell's `WizardPhase` vocabulary,
|
||||
composing the localized failure prefix so the `Failed` message arrives intact. */
|
||||
protected phase = computed<WizardPhase>(() => {
|
||||
@@ -254,13 +254,22 @@ export class HerregistratieWizardComponent {
|
||||
/** Current step's field errors, flattened for the shell's error summary. */
|
||||
protected errorList = computed<WizardError[]>(() => toWizardErrors(this.editing()?.errors ?? {}));
|
||||
|
||||
// --- Event handlers: narrow a child event into a message -------------------
|
||||
/** Stepper emits a 0-based index for an earlier (visited) step. */
|
||||
protected goToStep(index: number) {
|
||||
this.dispatch({ tag: 'GaNaarStap', step: (index + 1) as 1 | 2 | 3 });
|
||||
}
|
||||
|
||||
constructor() {
|
||||
// An explicit seed (stories/tests) wins; otherwise resume the backend draft
|
||||
// (`?aanvraag=<id>`) or start fresh. Persistence is the draftSync controller's job.
|
||||
const seeded = this.seed();
|
||||
queueMicrotask(() =>
|
||||
seeded !== initial ? this.dispatch({ tag: 'Seed', state: seeded }) : this.draftSync.resume(),
|
||||
);
|
||||
// Read `seed()` INSIDE the microtask: Angular binds inputs after the constructor
|
||||
// runs, so an eager read here always returns the `initial` default.
|
||||
queueMicrotask(() => {
|
||||
const seeded = this.seed();
|
||||
if (seeded !== initial) this.dispatch({ tag: 'Seed', state: seeded });
|
||||
else void this.draftSync.resume();
|
||||
});
|
||||
}
|
||||
|
||||
/** Reset the wizard to a fresh, empty start. */
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, computed, inject } from '@angular/core';
|
||||
import { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { ASYNC } from '@shared/ui/async/async.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { ASYNC } from '@shared/ui/molecules/async/async.component';
|
||||
import { map } from '@shared/application/remote-data';
|
||||
import { BigProfileStore } from '@registratie/application/big-profile.store';
|
||||
import { HerregistratieWizardComponent } from '@herregistratie/ui/herregistratie-wizard/herregistratie-wizard.component';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Component, input, output } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { FormFieldComponent } from '@shared/ui/form-field/form-field.component';
|
||||
import { TextInputComponent } from '@shared/ui/text-input/text-input.component';
|
||||
import { RadioGroupComponent, JA_NEE } from '@shared/ui/radio-group/radio-group.component';
|
||||
import { FormFieldComponent } from '@shared/ui/molecules/form-field/form-field.component';
|
||||
import { TextInputComponent } from '@shared/ui/atoms/text-input/text-input.component';
|
||||
import { RadioGroupComponent, JA_NEE } from '@shared/ui/atoms/radio-group/radio-group.component';
|
||||
import { Answers, Errors } from '@herregistratie/domain/intake.machine';
|
||||
|
||||
/** Step: the intake wizard's first screen (foreign work in the last 5 years).
|
||||
|
||||
@@ -26,14 +26,30 @@ const buitenlandJa: IntakeState = {
|
||||
scholingThreshold: 1000,
|
||||
};
|
||||
|
||||
/** Mount with a seed and let the constructor's microtask apply it. */
|
||||
async function mountSeeded(state: IntakeState) {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideHttpClient(), provideApiClient()],
|
||||
});
|
||||
const fixture = TestBed.createComponent(IntakeWizardComponent);
|
||||
fixture.componentRef.setInput('seed', state);
|
||||
await Promise.resolve(); // the seed is applied in a queueMicrotask
|
||||
fixture.detectChanges();
|
||||
return fixture;
|
||||
}
|
||||
|
||||
describe('IntakeWizardComponent', () => {
|
||||
it('renders each field group as its own grey <fieldset>', () => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideHttpClient(), provideApiClient()],
|
||||
});
|
||||
const fixture = TestBed.createComponent(IntakeWizardComponent);
|
||||
fixture.componentInstance.dispatch({ tag: 'Seed', state: buitenlandJa });
|
||||
fixture.detectChanges();
|
||||
// Regression: the constructor must read `seed()` INSIDE its microtask. Angular binds
|
||||
// inputs after the constructor runs, so an eager read silently yields the `initial`
|
||||
// default and every seeded story renders step 1 instead of the state it asked for.
|
||||
it('honours the seed input', async () => {
|
||||
const fixture = await mountSeeded(buitenlandJa);
|
||||
|
||||
expect(fixture.componentInstance.state()).toEqual(buitenlandJa);
|
||||
});
|
||||
|
||||
it('renders each field group as its own grey <fieldset>', async () => {
|
||||
const fixture = await mountSeeded(buitenlandJa);
|
||||
|
||||
const fieldsets: HTMLElement[] = Array.from(
|
||||
fixture.nativeElement.querySelectorAll('form.form-horizontal fieldset'),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component, computed, effect, inject, input, untracked } from '@angular/core';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { ConfirmationComponent } from '@shared/ui/confirmation/confirmation.component';
|
||||
import { ButtonComponent } from '@shared/ui/atoms/button/button.component';
|
||||
import { ConfirmationComponent } from '@shared/ui/molecules/confirmation/confirmation.component';
|
||||
import {
|
||||
WizardShellComponent,
|
||||
WizardError,
|
||||
@@ -32,8 +32,9 @@ import { ReviewStep } from './review.step';
|
||||
/** Organism: a BRANCHING intake questionnaire. All state lives in one signal
|
||||
driven by the pure `reduce` (intake.machine.ts). Which step renders is derived
|
||||
from the answers via `visibleSteps`, never stored — so editing an earlier
|
||||
answer immediately changes the remaining steps. Answers are persisted to
|
||||
sessionStorage so a page reload keeps the user's progress (cleared on tab close). */
|
||||
answer immediately changes the remaining steps. The draft persists to the
|
||||
backend as a Concept aanvraag (createDraftSync), so a reload — or a "Verder
|
||||
gaan" from the dashboard via `?aanvraag=<id>` — resumes progress. */
|
||||
@Component({
|
||||
selector: 'app-intake-wizard',
|
||||
imports: [
|
||||
@@ -106,8 +107,14 @@ export class IntakeWizardComponent {
|
||||
// Server-owned policy (scholing threshold): fetched from the backend via the
|
||||
// application facade, not hardcoded. The backend stays the authority on submit.
|
||||
private policyStore = inject(IntakePolicyStore);
|
||||
// Effect fires once, on Answering -> Submitting (RD-05's tag-transition rule; `Seed` is
|
||||
// exempt, so a story mounting straight into `Submitting` does not call the network).
|
||||
|
||||
/** Optional seed so Storybook / the showcase can mount any state directly. */
|
||||
seed = input<IntakeState>(initial);
|
||||
|
||||
// --- The store: all state in one signal, changed only by a pure reduce -----
|
||||
// The effect fires once, on the `Answering -> Submitting` transition. `Seed` is exempt,
|
||||
// so a story that mounts straight into `Submitting` does not call the network.
|
||||
// `draftSync` is declared below (both callbacks are deferred, so the cycle is safe).
|
||||
private store = createStore<IntakeState, IntakeMsg>(initial, reduce, {
|
||||
Submitting: async (s, store) => {
|
||||
this.profile.beginHerregistratie();
|
||||
@@ -129,24 +136,22 @@ export class IntakeWizardComponent {
|
||||
},
|
||||
});
|
||||
|
||||
/** Optional seed so Storybook / the showcase can mount any state directly. */
|
||||
seed = input<IntakeState>(initial);
|
||||
|
||||
readonly state = this.store.model;
|
||||
readonly dispatch = this.store.dispatch;
|
||||
|
||||
// Backend draft-sync (replaces sessionStorage); the intake has no uploads.
|
||||
private draftSync = createDraftSync({
|
||||
type: 'intake',
|
||||
snapshot: () => {
|
||||
const s = this.state();
|
||||
if (s.tag !== 'Answering' || !hasProgress(s)) return null;
|
||||
return { draft: s, stepIndex: s.cursor, stepCount: STEPS.length, documentIds: [] };
|
||||
},
|
||||
onResume: (draft) => this.dispatch({ tag: 'Seed', state: draft as IntakeState }),
|
||||
enabled: () => this.seed() === initial,
|
||||
});
|
||||
// --- Static copy: stepper labels and per-step headings ---------------------
|
||||
readonly stepLabels = [
|
||||
$localize`:@@intake.step.buitenland:Buitenland`,
|
||||
$localize`:@@intake.step.werk:Werk`,
|
||||
$localize`:@@intake.step.controle:Controle`,
|
||||
];
|
||||
private stepTitles: Record<StepId, string> = {
|
||||
buitenland: $localize`:@@intake.title.buitenland:Werken in het buitenland`,
|
||||
werk: $localize`:@@intake.title.werk:Werkervaring in Nederland`,
|
||||
review: $localize`:@@intake.title.review:Controleren en indienen`,
|
||||
};
|
||||
|
||||
// --- State projections: one narrow, then read-only views of it -------------
|
||||
private answering = computed(() => whenTag(this.state(), 'Answering'));
|
||||
/** Public so the showcase can render the (fixed) step list next to the wizard. */
|
||||
readonly steps = STEPS;
|
||||
@@ -159,17 +164,21 @@ export class IntakeWizardComponent {
|
||||
);
|
||||
protected errors = computed<Errors>(() => this.answering()?.errors ?? {});
|
||||
|
||||
// --- Controllers: persistence and uploads ----------------------------------
|
||||
// Create a Concept on first progress, then debounced-sync the snapshot.
|
||||
// `?aanvraag=<id>` resumes it. The intake has no uploads.
|
||||
private draftSync = createDraftSync({
|
||||
type: 'intake',
|
||||
snapshot: () => {
|
||||
const s = this.state();
|
||||
if (s.tag !== 'Answering' || !hasProgress(s)) return null;
|
||||
return { draft: s, stepIndex: s.cursor, stepCount: STEPS.length, documentIds: [] };
|
||||
},
|
||||
onResume: (draft) => this.dispatch({ tag: 'Seed', state: draft as IntakeState }),
|
||||
enabled: () => this.seed() === initial,
|
||||
});
|
||||
|
||||
// --- Presentational wiring for the shared wizard shell ---------------------
|
||||
readonly stepLabels = [
|
||||
$localize`:@@intake.step.buitenland:Buitenland`,
|
||||
$localize`:@@intake.step.werk:Werk`,
|
||||
$localize`:@@intake.step.controle:Controle`,
|
||||
];
|
||||
private stepTitles: Record<StepId, string> = {
|
||||
buitenland: $localize`:@@intake.title.buitenland:Werken in het buitenland`,
|
||||
werk: $localize`:@@intake.title.werk:Werkervaring in Nederland`,
|
||||
review: $localize`:@@intake.title.review:Controleren en indienen`,
|
||||
};
|
||||
protected stepTitle = computed(() => this.stepTitles[this.step()]);
|
||||
protected primaryLabel = computed(() => {
|
||||
if (this.step() === 'review') return $localize`:@@intake.indienen:Aanvraag indienen`;
|
||||
@@ -203,10 +212,13 @@ export class IntakeWizardComponent {
|
||||
constructor() {
|
||||
// An explicit seed (stories/tests) wins; otherwise resume the backend draft
|
||||
// (`?aanvraag=<id>`) or start fresh. Persistence is the draftSync controller's job.
|
||||
const seeded = this.seed();
|
||||
queueMicrotask(() =>
|
||||
seeded !== initial ? this.dispatch({ tag: 'Seed', state: seeded }) : this.draftSync.resume(),
|
||||
);
|
||||
// Read `seed()` INSIDE the microtask: Angular binds inputs after the constructor
|
||||
// runs, so an eager read here always returns the `initial` default.
|
||||
queueMicrotask(() => {
|
||||
const seeded = this.seed();
|
||||
if (seeded !== initial) this.dispatch({ tag: 'Seed', state: seeded });
|
||||
else void this.draftSync.resume();
|
||||
});
|
||||
// Apply the server-owned threshold into machine state as it arrives. Track
|
||||
// only the policy value; untrack the dispatch (it reads the state signal
|
||||
// internally, which would otherwise make this effect loop on its own write).
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, computed, input, output } from '@angular/core';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { DataRowComponent } from '@shared/ui/data-row/data-row.component';
|
||||
import { ReviewSectionComponent } from '@shared/ui/review-section/review-section.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { DataRowComponent } from '@shared/ui/molecules/data-row/data-row.component';
|
||||
import { ReviewSectionComponent } from '@shared/ui/molecules/review-section/review-section.component';
|
||||
import { Answers, lageUren } from '@herregistratie/domain/intake.machine';
|
||||
|
||||
/** Step: the intake wizard's review screen. Pure & presentational — values in via
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Component, computed, input, output } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { FormFieldComponent } from '@shared/ui/form-field/form-field.component';
|
||||
import { TextInputComponent } from '@shared/ui/text-input/text-input.component';
|
||||
import { RadioGroupComponent, JA_NEE } from '@shared/ui/radio-group/radio-group.component';
|
||||
import { FormFieldComponent } from '@shared/ui/molecules/form-field/form-field.component';
|
||||
import { TextInputComponent } from '@shared/ui/atoms/text-input/text-input.component';
|
||||
import { RadioGroupComponent, JA_NEE } from '@shared/ui/atoms/radio-group/radio-group.component';
|
||||
import { Answers, Errors, lageUren } from '@herregistratie/domain/intake.machine';
|
||||
|
||||
/** Step: the intake wizard's second screen (work experience in the Netherlands,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { IntakeWizardComponent } from '@herregistratie/ui/intake-wizard/intake-wizard.component';
|
||||
|
||||
/** Page: the branching intake questionnaire. Built entirely from existing
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, computed, inject } from '@angular/core';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { ApplicationListComponent } from '@shared/ui/application-list/application-list.component';
|
||||
import { ApplicationLinkComponent } from '@shared/ui/application-link/application-link.component';
|
||||
import { HeadingComponent } from '@shared/ui/atoms/heading/heading.component';
|
||||
import { ApplicationListComponent } from '@shared/ui/molecules/application-list/application-list.component';
|
||||
import { ApplicationLinkComponent } from '@shared/ui/molecules/application-link/application-link.component';
|
||||
import { AccessStore } from '@shared/application/access.store';
|
||||
import { HEADER_ADMIN_LINKS } from '@shared/layout/site-header/nav-config';
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';
|
||||
import { MijnAanvragenSection } from '@registratie/ui/dashboard/mijn-aanvragen.section';
|
||||
import { WatMoetIkRegelenSection } from '@registratie/ui/dashboard/wat-moet-ik-regelen.section';
|
||||
import { MijnRegistratieSection } from '@registratie/ui/dashboard/mijn-registratie.section';
|
||||
import { SpecialismenSection } from '@registratie/ui/dashboard/specialismen.section';
|
||||
import { MijnAanvragenSection } from '@registratie/ui/overzicht-secties/mijn-aanvragen.section';
|
||||
import { WatMoetIkRegelenSection } from '@registratie/ui/overzicht-secties/wat-moet-ik-regelen.section';
|
||||
import { MijnRegistratieSection } from '@registratie/ui/overzicht-secties/mijn-registratie.section';
|
||||
import { SpecialismenSection } from '@registratie/ui/overzicht-secties/specialismen.section';
|
||||
import { WatWiltUDoenSection } from './wat-wilt-u-doen.section';
|
||||
import { BeheerLinksSection } from './beheer-links.section';
|
||||
|
||||
/** Page: "Mijn overzicht" — the portal home, following the NL Design System "Mijn
|
||||
omgeving" pattern. Composition only: each section below answers its own data
|
||||
question (own store, own async state) — four sections stay in `registratie/ui/dashboard/`
|
||||
question (own store, own async state) — four sections stay in `registratie/ui/overzicht-secties/`
|
||||
(they render registratie data), two live here (cross-context navigation). */
|
||||
@Component({
|
||||
selector: 'app-overzicht-page',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, computed, inject } from '@angular/core';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { ApplicationListComponent } from '@shared/ui/application-list/application-list.component';
|
||||
import { ApplicationLinkComponent } from '@shared/ui/application-link/application-link.component';
|
||||
import { HeadingComponent } from '@shared/ui/atoms/heading/heading.component';
|
||||
import { ApplicationListComponent } from '@shared/ui/molecules/application-list/application-list.component';
|
||||
import { ApplicationLinkComponent } from '@shared/ui/molecules/application-link/application-link.component';
|
||||
import { FeatureFlagStore } from '@shared/application/feature-flags.store';
|
||||
import { FLAG_INSCHRIJVING_OPEN } from '@shared/domain/feature-flag';
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
reduceUpload,
|
||||
requiredCategoriesSatisfied,
|
||||
deliveryRefs,
|
||||
digitalDocumentIds,
|
||||
} from '@shared/domain/upload.machine';
|
||||
|
||||
/**
|
||||
@@ -109,7 +110,7 @@ export function hasProgress(s: Extract<RegistratieState, { tag: 'Invullen' }>):
|
||||
!!d.email ||
|
||||
!!d.diplomaId ||
|
||||
!!d.beroep ||
|
||||
deliveryRefs(s.upload).some((r) => r.channel === 'digital' && !!r.documentId)
|
||||
digitalDocumentIds(s.upload).length > 0
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, computed, input, output } from '@angular/core';
|
||||
import { formatDatumNl } from '@shared/kernel/datum';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { ButtonComponent } from '@shared/ui/atoms/button/button.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { Aanvraag } from '@registratie/domain/aanvraag';
|
||||
import { TYPE_LABELS } from '@registratie/domain/aanvraag-view';
|
||||
import { blockActions } from '@registratie/domain/block-actions';
|
||||
|
||||
@@ -17,57 +17,17 @@ const meta: Meta<AanvraagBlockComponent> = {
|
||||
title: 'Domein/Registratie/Aanvraag Block',
|
||||
component: AanvraagBlockComponent,
|
||||
decorators: [applicationConfig({ providers: [provideRouter([])] })],
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
// A row is an <li> — the keuzelijst styling needs the real list context.
|
||||
template: `<ul class="keuzelijst__list"><app-aanvraag-block [aanvraag]="aanvraag" /></ul>`,
|
||||
}),
|
||||
parameters: {
|
||||
// Structural: app-aanvraag-block's host sits between the keuzelijst <ul> and its <li>
|
||||
// — axe's list/listitem rule needs them adjacent regardless of `display:contents`.
|
||||
// WP-11 (CIBG markup fidelity) reworks this markup; see docs/project/backlog/WP-11-markup-fidelity.md.
|
||||
a11y: { disable: true },
|
||||
},
|
||||
// A Concept renders as a CIBG melding (block element), not a keuzelijst <li> — no <ul>
|
||||
// wrapper. Production agrees: mijn-aanvragen.section.ts renders this block for concepten
|
||||
// only, outside any list.
|
||||
render: (args) => ({ props: args, template: `<app-aanvraag-block [aanvraag]="aanvraag" />` }),
|
||||
};
|
||||
export default meta;
|
||||
type Story = StoryObj<AanvraagBlockComponent>;
|
||||
|
||||
// One story per status variant; the block renders its own body + actions.
|
||||
// A Concept renders as a CIBG melding (block element), not a keuzelijst <li> — no <ul> wrapper.
|
||||
// The whole template sits inside `@if (aanvraag().status.tag === 'Concept')`, so this is
|
||||
// the only status that renders anything. Submitted/resolved aanvragen render through
|
||||
// application-link, which has its own stories.
|
||||
export const Concept: Story = {
|
||||
args: { aanvraag: { ...base, status: { tag: 'Concept', stepIndex: 1, stepCount: 3 } } },
|
||||
render: (args) => ({ props: args, template: `<app-aanvraag-block [aanvraag]="aanvraag" />` }),
|
||||
};
|
||||
export const InBehandelingAuto: Story = {
|
||||
args: {
|
||||
aanvraag: {
|
||||
...base,
|
||||
status: { tag: 'InBehandeling', referentie: 'BIG-2026-456789', manual: false },
|
||||
},
|
||||
},
|
||||
};
|
||||
export const InBehandelingManual: Story = {
|
||||
args: {
|
||||
aanvraag: {
|
||||
...base,
|
||||
type: 'registratie',
|
||||
status: { tag: 'InBehandeling', referentie: 'BIG-2026-456789', manual: true },
|
||||
},
|
||||
},
|
||||
};
|
||||
export const Goedgekeurd: Story = {
|
||||
args: { aanvraag: { ...base, status: { tag: 'Goedgekeurd', referentie: 'BIG-2026-456789' } } },
|
||||
};
|
||||
export const Afgewezen: Story = {
|
||||
args: {
|
||||
aanvraag: {
|
||||
...base,
|
||||
type: 'herregistratie',
|
||||
status: {
|
||||
tag: 'Afgewezen',
|
||||
referentie: 'BIG-2026-456789',
|
||||
reden: 'Aanvraag afgewezen: geen gewerkte uren geregistreerd.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -2,11 +2,11 @@ import { Component, computed, inject } from '@angular/core';
|
||||
import { successOf } from '@shared/application/remote-data';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';
|
||||
import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { DataBlockComponent } from '@shared/ui/data-block/data-block.component';
|
||||
import { DataRowComponent } from '@shared/ui/data-row/data-row.component';
|
||||
import { ASYNC } from '@shared/ui/async/async.component';
|
||||
import { SkeletonComponent } from '@shared/ui/atoms/skeleton/skeleton.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { DataBlockComponent } from '@shared/ui/molecules/data-block/data-block.component';
|
||||
import { DataRowComponent } from '@shared/ui/molecules/data-row/data-row.component';
|
||||
import { ASYNC } from '@shared/ui/molecules/async/async.component';
|
||||
import { AanvragenStore } from '@registratie/application/aanvragen.store';
|
||||
import { Aanvraag } from '@registratie/domain/aanvraag';
|
||||
import { detailRows } from '@registratie/domain/aanvraag-view';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, input, output } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { FormFieldComponent } from '@shared/ui/form-field/form-field.component';
|
||||
import { TextInputComponent } from '@shared/ui/text-input/text-input.component';
|
||||
import { FormFieldComponent } from '@shared/ui/molecules/form-field/form-field.component';
|
||||
import { TextInputComponent } from '@shared/ui/atoms/text-input/text-input.component';
|
||||
|
||||
export interface AdresValue {
|
||||
straat: string;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Component, computed, effect, inject } from '@angular/core';
|
||||
import { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { DataBlockComponent } from '@shared/ui/data-block/data-block.component';
|
||||
import { DataRowComponent } from '@shared/ui/data-row/data-row.component';
|
||||
import { ASYNC } from '@shared/ui/async/async.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { ButtonComponent } from '@shared/ui/atoms/button/button.component';
|
||||
import { DataBlockComponent } from '@shared/ui/molecules/data-block/data-block.component';
|
||||
import { DataRowComponent } from '@shared/ui/molecules/data-row/data-row.component';
|
||||
import { ASYNC } from '@shared/ui/molecules/async/async.component';
|
||||
import { AccessStore } from '@shared/application/access.store';
|
||||
import { successOr } from '@shared/application/remote-data';
|
||||
import { formatDatumNl } from '@shared/kernel/datum';
|
||||
|
||||
+7
-7
@@ -1,12 +1,12 @@
|
||||
import { Component, computed, input } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { FormFieldComponent } from '@shared/ui/form-field/form-field.component';
|
||||
import { TextInputComponent } from '@shared/ui/text-input/text-input.component';
|
||||
import { DataBlockComponent } from '@shared/ui/data-block/data-block.component';
|
||||
import { DataRowComponent } from '@shared/ui/data-row/data-row.component';
|
||||
import { ButtonComponent } from '@shared/ui/atoms/button/button.component';
|
||||
import { HeadingComponent } from '@shared/ui/atoms/heading/heading.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { FormFieldComponent } from '@shared/ui/molecules/form-field/form-field.component';
|
||||
import { TextInputComponent } from '@shared/ui/atoms/text-input/text-input.component';
|
||||
import { DataBlockComponent } from '@shared/ui/molecules/data-block/data-block.component';
|
||||
import { DataRowComponent } from '@shared/ui/molecules/data-row/data-row.component';
|
||||
import { Adres } from '@registratie/domain/person';
|
||||
import { createStore } from '@shared/application/store';
|
||||
import { whenTag } from '@shared/kernel/fp';
|
||||
|
||||
+6
-6
@@ -1,11 +1,11 @@
|
||||
import { Component, computed, inject } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { ApplicationListComponent } from '@shared/ui/application-list/application-list.component';
|
||||
import { ApplicationLinkComponent } from '@shared/ui/application-link/application-link.component';
|
||||
import { ASYNC } from '@shared/ui/async/async.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { SkeletonComponent } from '@shared/ui/atoms/skeleton/skeleton.component';
|
||||
import { HeadingComponent } from '@shared/ui/atoms/heading/heading.component';
|
||||
import { ApplicationListComponent } from '@shared/ui/molecules/application-list/application-list.component';
|
||||
import { ApplicationLinkComponent } from '@shared/ui/molecules/application-link/application-link.component';
|
||||
import { ASYNC } from '@shared/ui/molecules/async/async.component';
|
||||
import { successOr } from '@shared/application/remote-data';
|
||||
import { AanvragenStore } from '@registratie/application/aanvragen.store';
|
||||
import { Aanvraag, AanvraagType } from '@registratie/domain/aanvraag';
|
||||
+5
-5
@@ -1,10 +1,10 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { successOf } from '@shared/application/remote-data';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';
|
||||
import { DataBlockComponent } from '@shared/ui/data-block/data-block.component';
|
||||
import { DataRowComponent } from '@shared/ui/data-row/data-row.component';
|
||||
import { ASYNC } from '@shared/ui/async/async.component';
|
||||
import { HeadingComponent } from '@shared/ui/atoms/heading/heading.component';
|
||||
import { SkeletonComponent } from '@shared/ui/atoms/skeleton/skeleton.component';
|
||||
import { DataBlockComponent } from '@shared/ui/molecules/data-block/data-block.component';
|
||||
import { DataRowComponent } from '@shared/ui/molecules/data-row/data-row.component';
|
||||
import { ASYNC } from '@shared/ui/molecules/async/async.component';
|
||||
import { BigProfileStore } from '@registratie/application/big-profile.store';
|
||||
import { RegistrationSummaryComponent } from '@registratie/ui/registration-summary/registration-summary.component';
|
||||
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { successOf } from '@shared/application/remote-data';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';
|
||||
import { ASYNC } from '@shared/ui/async/async.component';
|
||||
import { HeadingComponent } from '@shared/ui/atoms/heading/heading.component';
|
||||
import { SkeletonComponent } from '@shared/ui/atoms/skeleton/skeleton.component';
|
||||
import { ASYNC } from '@shared/ui/molecules/async/async.component';
|
||||
import { BigProfileStore } from '@registratie/application/big-profile.store';
|
||||
import { RegistrationTableComponent } from '@registratie/ui/registration-table/registration-table.component';
|
||||
|
||||
-8
@@ -66,14 +66,6 @@ export const MetTaken: Story = {
|
||||
],
|
||||
}),
|
||||
],
|
||||
parameters: {
|
||||
// Structural: app-choice-link's host sits between the keuzelijst <ul> and its <li>
|
||||
// — axe's list/listitem rule needs them adjacent regardless of `display:contents`.
|
||||
// Same pre-existing gap as task-list.stories.ts and choice-list.stories.ts. WP-11
|
||||
// (CIBG markup fidelity) reworks this markup; see
|
||||
// docs/project/backlog/WP-11-markup-fidelity.md.
|
||||
a11y: { disable: true },
|
||||
},
|
||||
};
|
||||
export const NietsOpenstaand: Story = {
|
||||
decorators: [
|
||||
+5
-5
@@ -1,10 +1,10 @@
|
||||
import { Component, computed, inject } from '@angular/core';
|
||||
import { successOf } from '@shared/application/remote-data';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';
|
||||
import { TaskListComponent } from '@shared/ui/task-list/task-list.component';
|
||||
import { ASYNC } from '@shared/ui/async/async.component';
|
||||
import { HeadingComponent } from '@shared/ui/atoms/heading/heading.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { SkeletonComponent } from '@shared/ui/atoms/skeleton/skeleton.component';
|
||||
import { TaskListComponent } from '@shared/ui/molecules/task-list/task-list.component';
|
||||
import { ASYNC } from '@shared/ui/molecules/async/async.component';
|
||||
import { BigProfileStore } from '@registratie/application/big-profile.store';
|
||||
import { tasksFromProfile } from '@registratie/domain/tasks';
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Component, inject, input, output } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { FormFieldComponent } from '@shared/ui/form-field/form-field.component';
|
||||
import { TextInputComponent } from '@shared/ui/text-input/text-input.component';
|
||||
import { RadioGroupComponent } from '@shared/ui/radio-group/radio-group.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';
|
||||
import { FormFieldComponent } from '@shared/ui/molecules/form-field/form-field.component';
|
||||
import { TextInputComponent } from '@shared/ui/atoms/text-input/text-input.component';
|
||||
import { RadioGroupComponent } from '@shared/ui/atoms/radio-group/radio-group.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { SkeletonComponent } from '@shared/ui/atoms/skeleton/skeleton.component';
|
||||
import { AddressFieldsComponent } from '@registratie/ui/address-fields/address-fields.component';
|
||||
import { RegistratieLookupStore } from '@registratie/application/registratie-lookup.store';
|
||||
import { Draft, DraftField, Errors } from '@registratie/domain/registratie-wizard.machine';
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { Component, computed, inject, input, output } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { FormFieldComponent } from '@shared/ui/form-field/form-field.component';
|
||||
import { TextInputComponent } from '@shared/ui/text-input/text-input.component';
|
||||
import { RadioGroupComponent, JA_NEE } from '@shared/ui/radio-group/radio-group.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';
|
||||
import { DataRowComponent } from '@shared/ui/data-row/data-row.component';
|
||||
import { DataBlockComponent } from '@shared/ui/data-block/data-block.component';
|
||||
import { ASYNC } from '@shared/ui/async/async.component';
|
||||
import { DocumentUploadComponent } from '@shared/ui/upload/document-upload/document-upload.component';
|
||||
import { FormFieldComponent } from '@shared/ui/molecules/form-field/form-field.component';
|
||||
import { TextInputComponent } from '@shared/ui/atoms/text-input/text-input.component';
|
||||
import { RadioGroupComponent, JA_NEE } from '@shared/ui/atoms/radio-group/radio-group.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { SkeletonComponent } from '@shared/ui/atoms/skeleton/skeleton.component';
|
||||
import { DataRowComponent } from '@shared/ui/molecules/data-row/data-row.component';
|
||||
import { DataBlockComponent } from '@shared/ui/molecules/data-block/data-block.component';
|
||||
import { ASYNC } from '@shared/ui/molecules/async/async.component';
|
||||
import { DocumentUploadComponent } from '@shared/ui/organisms/upload/document-upload/document-upload.component';
|
||||
import { createUploadController } from '@shared/application/upload-controller';
|
||||
import { UploadMsg, UploadState } from '@shared/domain/upload.machine';
|
||||
import { RemoteData, successOr } from '@shared/application/remote-data';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, computed, inject, input, output } from '@angular/core';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { DataRowComponent } from '@shared/ui/data-row/data-row.component';
|
||||
import { ReviewSectionComponent } from '@shared/ui/review-section/review-section.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { DataRowComponent } from '@shared/ui/molecules/data-row/data-row.component';
|
||||
import { ReviewSectionComponent } from '@shared/ui/molecules/review-section/review-section.component';
|
||||
import { successOr } from '@shared/application/remote-data';
|
||||
import { RegistratieLookupStore } from '@registratie/application/registratie-lookup.store';
|
||||
import { DuoLookupDto } from '@registratie/contracts/duo-diplomas.dto';
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { DuoLookupDto } from '@registratie/contracts/duo-diplomas.dto';
|
||||
import { diplomaMsg } from './diploma-msg';
|
||||
import { HANDMATIG } from './beroep.step';
|
||||
|
||||
const data: DuoLookupDto = {
|
||||
diplomas: [
|
||||
{
|
||||
id: 'd1',
|
||||
naam: 'Verpleegkunde',
|
||||
instelling: 'Hogeschool Utrecht',
|
||||
jaar: 2019,
|
||||
beroep: 'Verpleegkundige',
|
||||
policyQuestions: [
|
||||
{ id: 'q1', vraag: 'Vraag 1', type: 'ja-nee' },
|
||||
{ id: 'q2', vraag: 'Vraag 2', type: 'tekst' },
|
||||
],
|
||||
},
|
||||
],
|
||||
handmatig: {
|
||||
beroepen: ['Verpleegkundige', 'Arts'],
|
||||
policyQuestions: [{ id: 'm1', vraag: 'Handmatige vraag', type: 'ja-nee' }],
|
||||
},
|
||||
};
|
||||
|
||||
describe('diplomaMsg', () => {
|
||||
it('resolves a known diploma into KiesDiploma with the server-derived beroep', () => {
|
||||
expect(diplomaMsg(data, 'd1')).toEqual({
|
||||
tag: 'KiesDiploma',
|
||||
diplomaId: 'd1',
|
||||
beroep: 'Verpleegkundige',
|
||||
vraagIds: ['q1', 'q2'],
|
||||
});
|
||||
});
|
||||
|
||||
it('resolves the manual sentinel into KiesHandmatig with the maximal question set', () => {
|
||||
expect(diplomaMsg(data, HANDMATIG)).toEqual({ tag: 'KiesHandmatig', vraagIds: ['m1'] });
|
||||
});
|
||||
|
||||
it('returns null for an unknown diploma id', () => {
|
||||
expect(diplomaMsg(data, 'onbekend')).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import { DuoLookupDto } from '@registratie/contracts/duo-diplomas.dto';
|
||||
import { RegistratieMsg } from '@registratie/domain/registratie-wizard.machine';
|
||||
import { HANDMATIG } from './beroep.step';
|
||||
|
||||
/**
|
||||
* Resolve the diploma that the user picked into the machine message it implies.
|
||||
*
|
||||
* The DUO payload maps a diploma id onto a server-derived beroep and the policy
|
||||
* questions that apply to it. Reading that map is message construction, so it
|
||||
* belongs beside the container, not in the beroep step. The backend stays the
|
||||
* authority on both values (ADR-0001) — this function only selects them.
|
||||
*
|
||||
* Returns null when the id matches no known diploma. The caller then dispatches
|
||||
* nothing and the wizard keeps its current state.
|
||||
*/
|
||||
export function diplomaMsg(data: DuoLookupDto, id: string): RegistratieMsg | null {
|
||||
if (id === HANDMATIG) {
|
||||
return { tag: 'KiesHandmatig', vraagIds: data.handmatig.policyQuestions.map((q) => q.id) };
|
||||
}
|
||||
const diploma = data.diplomas.find((d) => d.id === id);
|
||||
if (!diploma) return null;
|
||||
return {
|
||||
tag: 'KiesDiploma',
|
||||
diplomaId: diploma.id,
|
||||
beroep: diploma.beroep,
|
||||
vraagIds: diploma.policyQuestions.map((q) => q.id),
|
||||
};
|
||||
}
|
||||
+49
-53
@@ -1,6 +1,6 @@
|
||||
import { Component, computed, effect, inject, input, untracked } from '@angular/core';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { ConfirmationComponent } from '@shared/ui/confirmation/confirmation.component';
|
||||
import { ButtonComponent } from '@shared/ui/atoms/button/button.component';
|
||||
import { ConfirmationComponent } from '@shared/ui/molecules/confirmation/confirmation.component';
|
||||
import {
|
||||
WizardShellComponent,
|
||||
WizardError,
|
||||
@@ -26,17 +26,18 @@ import {
|
||||
STEPS,
|
||||
} from '@registratie/domain/registratie-wizard.machine';
|
||||
import { createDraftSync } from '@registratie/application/draft-sync';
|
||||
import { UploadState, initialUpload, deliveryRefs } from '@shared/domain/upload.machine';
|
||||
import { UploadState, initialUpload, digitalDocumentIds } from '@shared/domain/upload.machine';
|
||||
import { AdresStep } from './adres.step';
|
||||
import { BeroepStep, HANDMATIG } from './beroep.step';
|
||||
import { BeroepStep } from './beroep.step';
|
||||
import { ControleStep } from './controle.step';
|
||||
import { diplomaMsg } from './diploma-msg';
|
||||
|
||||
/** Organism: the BIG-registration wizard. All state lives in one signal driven by
|
||||
the pure `reduce` (registratie-wizard.machine.ts). The BRP address prefills the
|
||||
draft via an effect; the DUO diploma list renders through <app-async>; choosing
|
||||
a diploma reveals its server-derived beroep. The draft is persisted to the
|
||||
backend as a Concept aanvraag (createDraftSync) so a reload — or a"Verder gaan"
|
||||
from the dashboard via `?aanvraag=<id>` — resumes progress. Built from existing
|
||||
a diploma reveals its server-derived beroep. The draft persists to the backend
|
||||
as a Concept aanvraag (createDraftSync), so a reload — or a "Verder gaan" from
|
||||
the dashboard via `?aanvraag=<id>` — resumes progress. Built from existing
|
||||
atoms/molecules. */
|
||||
@Component({
|
||||
selector: 'app-registratie-wizard',
|
||||
@@ -117,8 +118,14 @@ import { ControleStep } from './controle.step';
|
||||
})
|
||||
export class RegistratieWizardComponent {
|
||||
private lookup = inject(RegistratieLookupStore);
|
||||
// Effect fires once, on Invullen -> Indienen (RD-05's tag-transition rule; `Seed` is
|
||||
// exempt, so a story mounting straight into `Indienen` does not call the network).
|
||||
|
||||
/** Optional seed so Storybook / tests can mount any state directly. */
|
||||
seed = input<RegistratieState>(initial);
|
||||
|
||||
// --- The store: all state in one signal, changed only by a pure reduce -----
|
||||
// The effect fires once, on the `Invullen -> Indienen` transition. `Seed` is exempt,
|
||||
// so a story that mounts straight into `Indienen` does not call the network.
|
||||
// `draftSync` is declared below (both callbacks are deferred, so the cycle is safe).
|
||||
private store = createStore<RegistratieState, RegistratieMsg>(initial, reduce, {
|
||||
Indienen: async (s, store) => {
|
||||
const r = await this.draftSync.submit({
|
||||
@@ -129,50 +136,56 @@ export class RegistratieWizardComponent {
|
||||
else store.dispatch({ tag: 'SubmitFailed', error: r.error });
|
||||
},
|
||||
});
|
||||
readonly state = this.store.model;
|
||||
readonly dispatch = this.store.dispatch;
|
||||
|
||||
/** Optional seed so Storybook / tests can mount any state directly. */
|
||||
seed = input<RegistratieState>(initial);
|
||||
|
||||
// --- Static copy: stepper labels and per-step headings ---------------------
|
||||
readonly stepLabels = [
|
||||
$localize`:@@regWizard.step.adres:Adres`,
|
||||
$localize`:@@regWizard.step.beroep:Beroep`,
|
||||
$localize`:@@regWizard.step.controle:Controle`,
|
||||
]; // short labels for the stepper
|
||||
];
|
||||
private stepTitles = [
|
||||
$localize`:@@regWizard.title.adres:Adres en correspondentievoorkeur`,
|
||||
$localize`:@@regWizard.title.beroep:Beroep op basis van uw diploma`,
|
||||
$localize`:@@regWizard.title.controle:Controleren en indienen`,
|
||||
];
|
||||
readonly state = this.store.model;
|
||||
readonly dispatch = this.store.dispatch;
|
||||
|
||||
// --- State projections: one narrow, then read-only views of it -------------
|
||||
private invullen = computed(() => whenTag(this.state(), 'Invullen'));
|
||||
protected cursor = computed(() => this.invullen()?.cursor ?? 0);
|
||||
protected draft = computed<Draft>(() => this.invullen()?.draft ?? { antwoorden: {} });
|
||||
protected errors = computed<Errors>(() => this.invullen()?.errors ?? {});
|
||||
protected upload = computed<UploadState>(() => this.invullen()?.upload ?? initialUpload);
|
||||
// Backend draft-sync (replaces sessionStorage): create a Concept once the user has
|
||||
// made progress, then debounced-sync the whole machine snapshot; resume by `?aanvraag`.
|
||||
protected step = computed<StepId>(() => STEPS[Math.min(this.cursor(), STEPS.length - 1)]);
|
||||
protected referentie = computed(() => whenTag(this.state(), 'Ingediend')?.referentie ?? '');
|
||||
/** From the lookup store, not the machine: the beroep step renders it, and
|
||||
`onDiplomaKeuze` reads it to resolve the picked id into a message. */
|
||||
protected duoData = computed<DuoLookupDto | null>(() => successOr(this.lookup.duoLookup(), null));
|
||||
|
||||
// --- Controllers: persistence and uploads ----------------------------------
|
||||
// Create a Concept once the user has made progress, then debounced-sync the whole
|
||||
// machine snapshot. `?aanvraag=<id>` resumes it.
|
||||
private draftSync = createDraftSync({
|
||||
type: 'registratie',
|
||||
snapshot: () => {
|
||||
const s = this.state();
|
||||
if (s.tag !== 'Invullen' || !hasProgress(s)) return null;
|
||||
const documentIds = deliveryRefs(s.upload)
|
||||
.filter((r) => r.channel === 'digital' && r.documentId)
|
||||
.map((r) => r.documentId!);
|
||||
return { draft: s, stepIndex: s.cursor, stepCount: STEPS.length, documentIds };
|
||||
return {
|
||||
draft: s,
|
||||
stepIndex: s.cursor,
|
||||
stepCount: STEPS.length,
|
||||
documentIds: digitalDocumentIds(s.upload),
|
||||
};
|
||||
},
|
||||
onResume: (draft) => this.dispatch({ tag: 'Seed', state: draft as RegistratieState }),
|
||||
enabled: () => this.seed() === initial,
|
||||
});
|
||||
protected step = computed<StepId>(() => STEPS[Math.min(this.cursor(), STEPS.length - 1)]);
|
||||
|
||||
// --- Presentational wiring for the shared wizard shell ---------------------
|
||||
protected stepTitle = computed(
|
||||
() => this.stepTitles[Math.min(this.cursor(), this.stepTitles.length - 1)],
|
||||
);
|
||||
protected referentie = computed(() => whenTag(this.state(), 'Ingediend')?.referentie ?? '');
|
||||
|
||||
// --- Presentational wiring for the shared wizard shell ---------------------
|
||||
protected primaryLabel = computed(() => {
|
||||
if (this.step() === 'controle') return $localize`:@@regWizard.indienen:Registratie indienen`;
|
||||
const next = this.cursor() + 1;
|
||||
@@ -203,30 +216,12 @@ export class RegistratieWizardComponent {
|
||||
return [...toWizardErrors(e), ...toWizardErrors(e.antwoorden ?? {}, 'vraag-')];
|
||||
});
|
||||
|
||||
/** Parsed lookup as a plain value (or null) — needed here only to resolve
|
||||
`onDiplomaKeuze`'s message from an id (the DUO payload maps an id to a
|
||||
beroep and its question ids; that is machine-message construction, and it
|
||||
belongs in the container, not the beroep step). */
|
||||
protected duoData = computed<DuoLookupDto | null>(() => successOr(this.lookup.duoLookup(), null));
|
||||
|
||||
// --- Event handlers: narrow a child event into a message -------------------
|
||||
protected onDiplomaKeuze(id: string) {
|
||||
const data = this.duoData();
|
||||
if (!data) return;
|
||||
if (id === HANDMATIG) {
|
||||
this.dispatch({
|
||||
tag: 'KiesHandmatig',
|
||||
vraagIds: data.handmatig.policyQuestions.map((q) => q.id),
|
||||
});
|
||||
return;
|
||||
}
|
||||
const d = data.diplomas.find((x) => x.id === id);
|
||||
if (d)
|
||||
this.dispatch({
|
||||
tag: 'KiesDiploma',
|
||||
diplomaId: d.id,
|
||||
beroep: d.beroep,
|
||||
vraagIds: d.policyQuestions.map((q) => q.id),
|
||||
});
|
||||
const msg = diplomaMsg(data, id);
|
||||
if (msg) this.dispatch(msg);
|
||||
}
|
||||
|
||||
/** Narrows the beroep step's plain-string `kanaalChange` into the machine's
|
||||
@@ -238,10 +233,13 @@ export class RegistratieWizardComponent {
|
||||
constructor() {
|
||||
// An explicit seed (stories/tests) wins; otherwise resume from the backend draft
|
||||
// (`?aanvraag=<id>`), or start fresh. Persistence is the draftSync controller's job.
|
||||
const seeded = this.seed();
|
||||
queueMicrotask(() =>
|
||||
seeded !== initial ? this.dispatch({ tag: 'Seed', state: seeded }) : this.draftSync.resume(),
|
||||
);
|
||||
// Read `seed()` INSIDE the microtask: Angular binds inputs after the constructor
|
||||
// runs, so an eager read here always returns the `initial` default.
|
||||
queueMicrotask(() => {
|
||||
const seeded = this.seed();
|
||||
if (seeded !== initial) this.dispatch({ tag: 'Seed', state: seeded });
|
||||
else void this.draftSync.resume();
|
||||
});
|
||||
// Prefill the address from the BRP lookup as it arrives. Track only the facade's
|
||||
// parsed prefill signal; untrack the dispatch (it reads the state signal, which
|
||||
// would otherwise make this effect loop on its own write). Don't clobber
|
||||
@@ -260,12 +258,10 @@ export class RegistratieWizardComponent {
|
||||
});
|
||||
});
|
||||
});
|
||||
// A11y: focus management (step heading on step change, error summary on a
|
||||
// failed submit) now lives in the shared WizardShellComponent.
|
||||
}
|
||||
|
||||
/** Reset the wizard to a fresh start. Reload the BRP lookup so the address
|
||||
re-prefills, keeping the form and the"vooraf ingevuld" note consistent. */
|
||||
re-prefills, keeping the form and the "vooraf ingevuld" note consistent. */
|
||||
restart() {
|
||||
this.draftSync.reset(); // discard the current Concept; a fresh one starts on next progress
|
||||
this.dispatch({ tag: 'Seed', state: initial });
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { AlertComponent } from '@shared/ui/atoms/alert/alert.component';
|
||||
import { RegistratieWizardComponent } from '@registratie/ui/registratie-wizard/registratie-wizard.component';
|
||||
|
||||
/** Page: register in the BIG-register. Built entirely from existing building
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Component, computed, inject } from '@angular/core';
|
||||
import { successOf } from '@shared/application/remote-data';
|
||||
import { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';
|
||||
import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';
|
||||
import { ASYNC } from '@shared/ui/async/async.component';
|
||||
import { SkeletonComponent } from '@shared/ui/atoms/skeleton/skeleton.component';
|
||||
import { ASYNC } from '@shared/ui/molecules/async/async.component';
|
||||
import { RegistrationSummaryComponent } from '@registratie/ui/registration-summary/registration-summary.component';
|
||||
import { ChangeRequestFormComponent } from '@registratie/ui/change-request-form/change-request-form.component';
|
||||
import { BigProfileStore } from '@registratie/application/big-profile.store';
|
||||
|
||||
+3
-3
@@ -2,9 +2,9 @@ import { Component, input } from '@angular/core';
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { Registration } from '@registratie/domain/registration';
|
||||
import { statusColor, statusLabel } from '@registratie/domain/registration.policy';
|
||||
import { DataRowComponent } from '@shared/ui/data-row/data-row.component';
|
||||
import { StatusBadgeComponent } from '@shared/ui/status-badge/status-badge.component';
|
||||
import { DataBlockComponent } from '@shared/ui/data-block/data-block.component';
|
||||
import { DataRowComponent } from '@shared/ui/molecules/data-row/data-row.component';
|
||||
import { StatusBadgeComponent } from '@shared/ui/atoms/status-badge/status-badge.component';
|
||||
import { DataBlockComponent } from '@shared/ui/molecules/data-block/data-block.component';
|
||||
|
||||
/** Organism: registration summary in a CIBG Datablock. Composes data-row rows +
|
||||
status-badge atom (no card wrapper — the datablock carries its own surface). */
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { HeadingComponent } from '@shared/ui/atoms/heading/heading.component';
|
||||
import { HerregistratieWizardComponent } from '@herregistratie/ui/herregistratie-wizard/herregistratie-wizard.component';
|
||||
import { ConceptCardComponent } from './concept-card.component';
|
||||
import { SNIPPETS } from './snippets.generated';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, computed, signal } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { TextInputComponent } from '@shared/ui/text-input/text-input.component';
|
||||
import { HeadingComponent } from '@shared/ui/atoms/heading/heading.component';
|
||||
import { TextInputComponent } from '@shared/ui/atoms/text-input/text-input.component';
|
||||
import { parsePostcode } from '@registratie/domain/value-objects/postcode';
|
||||
import { ConceptCardComponent } from './concept-card.component';
|
||||
import { SNIPPETS } from './snippets.generated';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Component, computed, signal } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { TextInputComponent } from '@shared/ui/text-input/text-input.component';
|
||||
import { MaskedValueComponent } from '@shared/ui/masked-value/masked-value.component';
|
||||
import { HeadingComponent } from '@shared/ui/atoms/heading/heading.component';
|
||||
import { TextInputComponent } from '@shared/ui/atoms/text-input/text-input.component';
|
||||
import { MaskedValueComponent } from '@shared/ui/atoms/masked-value/masked-value.component';
|
||||
import { parseBsn } from '@shared/kernel/bsn';
|
||||
import { maskBsn } from '@shared/kernel/pii';
|
||||
import { ConceptCardComponent } from './concept-card.component';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Component } from '@angular/core';
|
||||
import type { Resource } from '@angular/core';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { ASYNC } from '@shared/ui/async/async.component';
|
||||
import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';
|
||||
import { HeadingComponent } from '@shared/ui/atoms/heading/heading.component';
|
||||
import { ASYNC } from '@shared/ui/molecules/async/async.component';
|
||||
import { SkeletonComponent } from '@shared/ui/atoms/skeleton/skeleton.component';
|
||||
import { ConceptCardComponent } from './concept-card.component';
|
||||
import { SNIPPETS } from './snippets.generated';
|
||||
import { highlightTs } from './highlight-ts';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { HeadingComponent } from '@shared/ui/atoms/heading/heading.component';
|
||||
import { RegistrationSummaryComponent } from '@registratie/ui/registration-summary/registration-summary.component';
|
||||
import { Registration } from '@registratie/domain/registration';
|
||||
import { ConceptCardComponent } from './concept-card.component';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { HeadingComponent } from '@shared/ui/atoms/heading/heading.component';
|
||||
import { IntakeWizardComponent } from '@herregistratie/ui/intake-wizard/intake-wizard.component';
|
||||
import { ConceptCardComponent } from './concept-card.component';
|
||||
import { SNIPPETS } from './snippets.generated';
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ status codes and error envelope are production-shaped.
|
||||
covers it, see `docker-compose.yml`) does **not** lose data. Delete the file to
|
||||
reset demo data back to empty, the same state a fresh clone starts from. This is
|
||||
a deliberate, right-sized choice for a POC (SQLite, no external DB service) — see
|
||||
`docs/project/backlog/WP-22-durable-persistence.md`.
|
||||
`docs/project/archive/backlog/WP-22-durable-persistence.md`.
|
||||
|
||||
## Run
|
||||
|
||||
|
||||
@@ -152,7 +152,7 @@ public static class LetterHtml
|
||||
|
||||
// Walks up from the running assembly's own directory (NOT the process cwd, which
|
||||
// varies by how `dotnet run`/docker/tests invoke it — see
|
||||
// docs/project/backlog/WP-25-letter-preview-html.md) until it finds `public/letter.css`. docker-compose.yml bind-mounts `./public` under the
|
||||
// docs/project/archive/backlog/WP-25-letter-preview-html.md) until it finds `public/letter.css`. docker-compose.yml bind-mounts `./public` under the
|
||||
// api container's `/src` for exactly this walk to resolve there too.
|
||||
private static string FindLetterCss()
|
||||
{
|
||||
|
||||
@@ -12,6 +12,8 @@ services:
|
||||
command: dotnet run --project src/BigRegister.Api --urls http://+:5000
|
||||
environment:
|
||||
- ASPNETCORE_ENVIRONMENT=Development
|
||||
# ponytail: Server GC makes one heap per CPU (22 here); workstation GC makes one.
|
||||
- DOTNET_gcServer=0
|
||||
volumes:
|
||||
# ':z' relabels for SELinux (Fedora/RHEL); harmless on other hosts.
|
||||
# WP-22: no separate volume needed for the SQLite file — `dotnet run` sets
|
||||
@@ -41,6 +43,8 @@ services:
|
||||
# switcher actually switches. ponytail: `--no-fund --loglevel=error` silences npm 11 noise.
|
||||
command: sh -c "npm ci --no-fund --loglevel=error && npx ng build ssp --configuration development --localize && node scripts/serve-i18n.mjs"
|
||||
environment:
|
||||
# ponytail: without a cgroup limit Node sizes its heap from host RAM (16 GB).
|
||||
- NODE_OPTIONS=--max-old-space-size=2048
|
||||
- PORT=4200
|
||||
- API_PROXY_TARGET=http://api:5000
|
||||
- APP_DIST_ROOT=dist/ssp/browser
|
||||
@@ -57,6 +61,8 @@ services:
|
||||
working_dir: /app
|
||||
command: sh -c "npm ci --no-fund --loglevel=error && npx ng build behandelportal --configuration development --localize && node scripts/serve-i18n.mjs"
|
||||
environment:
|
||||
# ponytail: without a cgroup limit Node sizes its heap from host RAM (16 GB).
|
||||
- NODE_OPTIONS=--max-old-space-size=2048
|
||||
- PORT=4201
|
||||
- API_PROXY_TARGET=http://api:5000
|
||||
- APP_DIST_ROOT=dist/behandelportal/browser
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# Archive — finished backlogs
|
||||
|
||||
The trees in this directory are historical. Every ticket in `backlog/` and
|
||||
`refactor-backlog-setup/` carries `Status: done`. `SHOWCASE-ROADMAP.md` was superseded before
|
||||
either arc started.
|
||||
|
||||
Git holds the rest — `git log --follow` on any file here shows its full history, including the
|
||||
work that produced it.
|
||||
|
||||
The live plan is `docs/project/readable-codebase/`. Product reference material is
|
||||
`docs/project/prd/`.
|
||||
@@ -1,6 +1,6 @@
|
||||
# Showcase roadmap — superseded
|
||||
|
||||
**This roadmap is superseded by [`docs/project/backlog/`](backlog/README.md)** (2026-07-02).
|
||||
**This roadmap is superseded by [`docs/project/archive/backlog/`](backlog/README.md)** (2026-07-02).
|
||||
|
||||
The backlog absorbs and corrects this document: its Storybook-as-curriculum track became
|
||||
WP-14/15 (+ per-invariant MDX pages in WP-05/07/08/13/17), its enforcement track became
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user