Commit Graph

22 Commits

Author SHA1 Message Date
180c08d4ee fix(registratie): silence NG8102 by moving ?? '' guard into a method
`draft().antwoorden[q.id]` types as `string` (Record index, no noUncheckedIndexedAccess),
so the template `?? ''` tripped NG8102's "redundant nullish coalescing" diagnostic — but a
missing key IS `undefined` at runtime, so the guard is real. Moved it into an `antwoord(id)`
component method: same runtime safety, no template-only diagnostic. Build now warning-clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 12:32:13 +02:00
053160c5c9 feat(brief): letter composition + two-person approval (teaching slice)
New `brief` context — a letter-composition feature with a drafter/approver
approval workflow, built as a teaching vertical slice on the repo's existing
FP + Elm + atomic-design patterns (see plan in ~/.claude/plans).

Domain (pure):
- Rich text as a serialisable value tree (placeholders are first-class nodes),
  moved to @shared/kernel/rich-text.ts so the shared editor can use it.
- lintPlaceholders: a pure, total content -> Diagnostic[] linter, derived never stored.
- brief.machine.ts: status sum-type with guarded transitions; frozen-snapshot =
  deep value copy; derived diagnostics/editability. Full specs.

Backend (.NET stub):
- BriefStore + seed, GET/PUT /brief and submit/approve/reject/send endpoints,
  role via X-Role header (mirrors X-Admin), transition + approver!=drafter guards,
  audit logging. Regenerated typed client via gen:api. +6 backend tests.

Seam:
- brief.adapter.ts maps flat wire unions <-> domain discriminated unions at the
  parse boundary (+ spec).

UI (atomic):
- shared atoms: checkbox, placeholder-chip; molecule: rich-text-editor (no-dep
  contenteditable, DOM<->RichTextBlock round-trip tested).
- brief/ui: letter-block, passage-picker, diagnostics-panel, rejection-comments,
  letter-section, letter-composer, letter-preview, brief.page + /brief route.
- Dev-only ?role=drafter|approver toggle + roleInterceptor; dashboard nav link.

Enforcement: @brief/* alias + eslint layer boundary (brief depends only on shared).

Also included (same session):
- Value-object specs (postcode/uren/big-nummer) — closes the "domain must have a spec" gap.
- src/docs/ Storybook MDX foundation pages (atomic design, tokens, FP-in-UI).
- .storybook/tsconfig.json: add @angular/localize to types (Storybook was fully
  broken — $localize unresolved — dev + build).

Verified: 168 FE tests, 68 backend tests, lint/build/check:tokens green,
Storybook boots, end-to-end HTTP smoke (self-approve 403, approver 200, full flow).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 21:32:22 +02:00
6a61c179cd Registratie: answer-driven required document uploads
Categories stay server-owned (ADR-0001); the FE sends its answers to
/uploads/categories and re-fetches reactively when they change:
- Diplomabewijs required only for a handmatig diploma (DUO is verified digitally;
  nothing required before a diploma is chosen).
- Bewijs Nederlandse taalvaardigheid required only when the applicant answers "ja"
  to the nl-taalvaardigheid (B2) policy question.
CategoriesFor(wizardId, diplomaHerkomst, taalvaardigheid) decides; Find uses the
maximal set so uploads still validate. CategoriesLoaded drops orphaned uploads
when a category disappears. Also: show the foreground-only upload banner only when
there is at least one category.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 18:35:37 +02:00
5027f099cf Mijn aanvragen: list enter/leave animations + 204 null-body fix
- animate.enter/leave on aanvraag cards and upload rows (native Angular, no @angular/animations)
- reduced-motion: skip animation → instant removal
- api-client: null-body statuses (204/205/304) must pass null to Response()

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 17:14:29 +02:00
0f14239f68 Fix Mijn aanvragen: instant cancel + one Concept per type (resume)
Two dashboard bugs from the just-built feature.

1. Cancel didn't reflect until a browser refresh. ApplicationsStore now OWNS the
   list in a writable RemoteData signal instead of projecting a resource() through
   fromResource; cancel removes the row synchronously (guaranteed disappear, no
   dependence on CD timing / HTTP cache / the reloading gap), then confirms the
   DELETE (rollback on failure, no resync). Adapter gains list(); applicationsResource()
   removed. Shared fromResource/remote-data.ts deliberately untouched.

2. Duplicate / inconsistent Concepts per type. createDraftSync.resume() now: a
   ?aanvraag link wins; else it resumes THIS type's existing Concept (loads its
   draft); else fresh. ensureId is gated behind resume so a fast typist can't create
   a duplicate before the lookup lands. restart()/reset() deletes the current Concept
   (submitted → 409, kept) so there's at most one active Concept per type. A non-Concept
   id can't reopen as an editable draft. Backend unchanged.

Gates green: lint, vitest 128, build, check:tokens, backend dotnet 56.
Wiring is not unit-covered — needs live verification (see plan).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 16:34:01 +02:00
168cf9786c Mijn aanvragen (F2): document preview/download on completed uploads
Re-opening a Concept wizard now lets the user preview/download what they already
uploaded (PRD 0001 goal 2).

- document-chip: optional previewUrl input → a "Voorbeeld / Download" link (opens
  the stored bytes; server serves inline for pdf/image, attachment otherwise).
- previewUrlFor callback threaded document-upload → document-category → single-upload
  (which builds the URL from a completed upload's documentId). Keeps URL-building out
  of the presentational atoms.
- registratie + herregistratie wizards supply previewUrlFor via UploadAdapter.contentUrl,
  returning undefined for dev-simulation `demo-*` ids (no stored bytes → no link).
- Story: DocumentChip/WithPreview.

Gates green: vitest 128, lint, ng build, check:tokens; backend dotnet 56.
(build-storybook has a pre-existing Compodoc/$localize issue in untouched files.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 14:50:40 +02:00
0bef08e5b3 Mijn aanvragen (F1): ApplicationsStore + dashboard blocks
The payoff — A–E become visible. The dashboard now shows a "Mijn aanvragen"
section at the top with a block per application.

- ApplicationsStore (registratie/application, root): the list as a RemoteData
  signal, parsed at the trust boundary; reload() (dashboard revisit reflects
  server-computed auto-approval); optimistic cancel (hide → reload / un-hide on fail).
- aanvraag-block (organism): badge (tag → colour/label) + per-status body
  ("Stap X van Y" / referentie + ingediend-datum / manual note / reden) + actions
  from the pure blockActions. Composes card + status-badge + button. Stories per status.
- dashboard: "Mijn aanvragen" section (hidden when empty), sorted Concept → In
  behandeling → resolved; Verder gaan deep-links the wizard (?aanvraag=<id>),
  Annuleren cancels via the store.

Deferred to F2: document-chip preview/download affordance.
Gates green: vitest 128, lint, build, check:tokens; backend dotnet 56.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 14:43:36 +02:00
9f217abe19 Mijn aanvragen (E): two-flow submit through the aanvraag + all-wizard persistence
All three wizards now submit through the backend aanvraag lifecycle, so a
submitted Concept actually transitions (dashboard shows it correctly in F).

- blockActions(status) (domain + spec): the pure per-status action decision
  (Concept → resume/cancel; In behandeling → viewDocuments; resolved → none).
- createDraftSync.submit(): ensure the Concept exists, then
  POST /applications/{id}/submit; folded into a Result like the old commands.
- registratie: submit via draftSync (duo → auto, handmatig → manual pending — the
  old 422 path is gone from the wizard).
- intake + herregistratie: adopt createDraftSync (persistence + resume-by-link);
  intake retires sessionStorage `intake-v3`; herregistratie gains persistence.
  Both submit through the aanvraag too. hasProgress added to each machine (+spec).
- Delete now-dead submit-registratie/submit-intake/submit-herregistratie commands.

Deferred: the old /registrations, /intakes, /herregistraties backend endpoints +
RejectRegistratie are now unused by the FE but still present (+ tested) — retiring
them cascades into backend test rewrites, so it's a focused follow-up cleanup.
Gates green: vitest 128, lint, build; backend unchanged (dotnet 56).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 14:25:02 +02:00
6db7f1e673 Mijn aanvragen (D): backend draft-sync + resume-by-link (registratie slice)
Replaces the registratie wizard's sessionStorage draft with a backend-owned
Concept aanvraag (PRD 0001, phase D — the registratie vertical slice).

- createDraftSync (registratie/application): reusable controller (field-initializer
  idiom, like createUploadController). Creates the Concept lazily on first progress,
  stamps `?aanvraag=<id>` into the URL, debounced-syncs the machine snapshot per
  change, and resumes from `?aanvraag` on load. Inert without a Router or when an
  explicit seed is present (Storybook/tests) — no network there.
- hasProgress (machine, pure + spec): "worth persisting?" — excludes the automatic
  BRP address prefill so a bare page visit creates nothing. Accepted regression:
  a step-0-only address edit isn't persisted until the user advances/chooses.
- Wizard: dropped STORAGE_KEY/restore + the sessionStorage effect; restart() detaches
  the Concept and drops the link.

Deferred (noted): ApplicationsStore -> phase F (dashboard is its only consumer);
intake-v3 + herregistratie persistence -> phase E (copy this pattern).
Gates green: vitest 125, lint, build; backend unchanged (dotnet 56).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 12:23:30 +02:00
bfd957a6d4 Upload feature (e): wire inline upload (registratie beroep) + documenten step (herregistratie)
- Fold UploadState into both wizard machines; route via { tag: 'Upload', msg }
- Gate step validation on requiredCategoriesSatisfied; include deliveryRefs in submit
- Shared createUploadController (effectful glue: categories, transport, focus-poll, File map)
- rejectReason pure format validator + specs; bump registratie storage key to v2

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 08:38:37 +02:00
a079d3259e Add showcase link (Functionele patronen) back to the dashboard sidebar
Restores the /concepts entry the restyle dropped, as a side-nav item.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 20:01:52 +02:00
4ee4f95c92 Fix: restore dashboard entry point to branching intake flow
The Rijkshuisstijl restyle (7a582ae) trimmed the dashboard action-card grid and
dropped the card linking to /intake, orphaning the branching herregistratie intake
questionnaire (reachable only by typing the URL; the breadcrumb still expected it).
Restore the action card (localized). The route, page and branching wizard were
intact — only the entry point was missing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 19:51:57 +02:00
9c2a80451f Step 3 (production-readiness): PII storage, validated reads, seams
Implement-now:
- G1: keep PII out of persistent storage — never persist BSN (only `naam`);
  move both wizard drafts (address/email, work data) localStorage → sessionStorage
  so they clear on tab close.
- G2: validate storage reads before trusting the cast — shape/tag guard in every
  restore() (mirrors the parse* HTTP boundary); corrupt/foreign shape → start fresh.
- G3: already satisfied (debug-state redacts via mask.ts).

Show-the-seam (hook + doc, not fully built):
- G4: problemFieldErrors() maps a server validation envelope (ASP.NET
  ValidationProblemDetails `errors`) to the field-keyed map the wizards already
  render; returns {} until the backend sends it. +spec.
- G5: documented the retry/backoff seam at the adapter GET loader; reads may
  retry, mutating submits never do.

Out of scope (named): unsaved-changes warning (persistence prevents data loss),
real auth/tokens, axe-core in CI.

Gate green: lint, check:tokens, build, test 79/79.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 14:07:10 +02:00
474c040410 Step 2 (i18n): $localize sweep + JA_NEE dedup (M3, M4)
Wrap every user-facing Dutch string in Angular's first-party i18n — `i18n`/
`i18n-<attr>` in templates, `$localize` in TS (value-objects, machines, commands,
label constants, shared-component defaults). Source locale stays nl; a second
locale is now a translation file, not a code change.

- M3: ~145 strings localized with stable @@ ids across registratie,
  herregistratie, auth, shared/ui, shared/layout. Skipped: showcase, debug-state,
  scenario interceptor, generated client, specs/stories, raw status enum tags,
  internal parse* diagnostics.
- M4: single shared JA_NEE (localized labels) in radio-group; both wizard copies
  removed.

Gate green: lint, check:tokens, build, test 77/77.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 14:00:10 +02:00
1c65025fef Step 2 (code quality): dedup + stop FE recomputing a server rule
- H1: tasksFromProfile takes the server's eligibleForHerregistratie decision
  instead of recomputing isHerregistratieEligible — the FE renders the rule,
  doesn't own it (ADR-0001). Policy reference impl kept for tests.
- M1: one shared runSubmit(fn, fallback) wrapper; the 4 submit-* commands keep
  only their payload mapping. +spec.
- M2: whenTag() kernel helper removes 10 repeated `as Extract<U,{tag}>` casts
  across the wizard/form components.

M4 (shared JA_NEE) folded into the upcoming i18n pass (clean dedup needs
$localize labels to sit in shared without breaking the English-shared-UI rule).
L1 already resolved by the restyle commit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 13:48:35 +02:00
7a582ae2fa Rijkshuisstijl restyle + wizard fixes
Chrome: two-tier Rijksoverheid header (white brand bar + lint-blue
breadcrumb bar, route-driven), dark multi-column footer, white page
surface. Session shown via a shared SESSION_PORT token (keeps shared/
free of the auth context).

Overview ("Mijn overzicht") rebuilt to the NL Design System #392 pattern:
side-nav + "Wat moet ik regelen" task list (derived) + "Mijn registratie"
cards. New shared components: card, task-list, side-nav; pure
tasksFromProfile (+spec).

Wizards: grey form panel, connected numbered stepper, form-field
"(verplicht)" markers + styled description/error, full-width inputs.
Propagated to login, detail, change-request, address-fields.

Bug fixes:
- wizard-shell: add FormsModule so NgForm intercepts submit (wizards now
  advance; no native GET leaking choices into the URL).
- wizard-shell: error-summary links focus the field instead of navigating
  (a fragment href resolved against <base href="/"> reloaded to "/" and
  bounced to login).
- wizard-shell: error-summary focus only on the rising edge, so typing
  while errors are shown no longer scrolls the page up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 13:21:54 +02:00
d08f3877f7 Architect-review remediation: enforce conventions, prod-safe tooling, one form idiom, resilience seams
Acts on the showcase review. Four workstreams; all tests green
(npm run lint, 70 FE tests, ng build, 33 backend tests).

Enforcement + CI:
- eslint.config.mjs bans `any` and enforces layer/context boundaries
  (domain ≠ Angular; herregistratie → registratie → shared, auth → shared);
  `npm run lint` added; ajv 6 scoped to ESLint via nested override.
- .github/workflows/ci.yml: FE lint+check:tokens+test+build, backend dotnet test,
  and an API-client drift check.

One form idiom (the headline finding):
- change-request-form converged onto the wizard pattern — change-request.machine.ts
  (Model/Msg/reduce + value objects) + submit-change-request.ts (Result) + a real
  POST /api/v1/change-requests (server re-validates). Spec + story added; the detail
  page no longer holds an ad-hoc success signal.

Resilience/observability seam:
- api-client.provider.ts: request timeout, X-Correlation-Id, Idempotency-Key for
  writes; comments naming the retry/auth seams.
- Backend logs correlation id + a no-PII submit-audit line; /api/v1 prefix +
  backward-compat note; client regenerated.

Quick wins:
- Dev tooling excluded from prod: scenario.interceptor wired only under isDevMode()
  (?scenario= inert in prod); debug panel @if(isDev) (tree-shaken out).
- src/environments + apiBaseUrl into provideApiClient (angular.json fileReplacements).
- Backend /health + /health/ready.
- Debug view PII-minimised (redactProfile: name/address/DOB redacted, BIG masked).
- IntakePolicyAdapter (removes inline resource in the intake wizard).
- README de-staled; CLAUDE.md gains EN/NL + forms-one-idiom + lint/CI notes.
- Stories: text-input, link, data-row, site-header, site-footer, change-request-form.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 08:25:51 +02:00
cf570a8132 Add ASP.NET Core backend hosting business rules; FE consumes via typed client
Move the authoritative business rules off the frontend into a real backend,
realising the BFF-lite + decision-DTO design (ADR-0001) that until now lived
only in static mock JSON.

Backend (backend/):
- ASP.NET Core (.NET 10) minimal API, contract-first, Swagger UI at /swagger.
- DDD Domain/ rules layer: profession derivation + applicable policy questions
  (DiplomaRules), herregistratie eligibility + reason (HerregistratieRule),
  scholing threshold (IntakePolicy), submit rejections + reference generation
  (SubmissionRules). In-memory seeded data, ProblemDetails (RFC 7807) errors.
- 27 xUnit tests: rule units + endpoint integration incl. BRP no-address and
  DUO not-found fallbacks and 422 submit paths.

Frontend (only infrastructure/ + contracts/ change, as the architecture promised):
- NSwag-generated typed client (api-client.ts), routed through Angular HttpClient
  via a small fetch adapter so the ?scenario= interceptor still applies.
- GET adapters use resource({ loader: client.x }); submit commands call the client
  and map ProblemDetails -> err. The hardcoded uren==0 / manual-diploma rules are
  deleted (now server-side). Domain, stores, UI and format validators unchanged.
- Deleted the now-dead public/mock/*.json.

Tooling/docs:
- npm start proxies /api -> backend; npm run gen:api regenerates the client;
  docker compose up runs both (bind mounts use :z for SELinux/Fedora).
- backend/README.md walkthrough: adding a policy question is a one-file backend
  change, no FE change, no client regen. Updated CLAUDE.md + ARCHITECTURE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 20:05:53 +02:00
770d454a32 Extract reusable address-fields organism, adopt in both registratie call-sites
The editable address block (straat/postcode/woonplaats) was hand-built inline in
two places — the registratie wizard and the change-request form. Factor it into one
pure presentational organism (values in, errors in, per-field change out) grouped in
a fieldset/legend, and adopt it in both. Behaviour, validation and state flow are
unchanged: the wizard still dispatches SetField (flipping adresHerkomst on edit) and
the change-request form still parses the postcode on submit. Storybook entry added;
other field clusters left inline by design (single-use or genuinely divergent).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 17:25:39 +02:00
64385999eb Add registratie wizard, BFF dashboard-view, contracts/value-objects, and architecture docs
Checkpoint of in-progress work: the registration wizard (address prefill,
DUO diploma lookup, policy questions), decision-DTO contracts, parse-don't-
validate value objects, infrastructure adapters, plus CLAUDE.md and the
architecture/ADR docs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 17:23:52 +02:00
7463efdc2d Add branching intake wizard (derived steps + radio-group atom)
A second wizard demonstrating a BRANCHING flow: the visible steps are derived
from the answers by a pure `visibleSteps` function rather than stored, so
answering "buiten Nederland gewerkt? -> ja" or reporting few hours adds steps
and the progress denominator changes live. Same Elm-style store + RemoteData
patterns as the fixed wizard; answers persist to localStorage.

- intake.machine.ts: IntakeState union + Answers + visibleSteps + pure reduce (+spec)
- intake-wizard organism, intake.page, submit-intake command
- new radio-group atom (ControlValueAccessor) in shared/ui
- /intake route + dashboard link + concepts showcase section
- tighten Aantekening.type to a 'Specialisme' | 'Aantekening' union
- README + ARCHITECTURE updated

Verified live end-to-end (branches add steps 4->5->6, review, submit) with no
console errors; build, unit tests, and Storybook all green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 09:38:26 +02:00
2114514ad7 Restructure into DDD bounded contexts + functional state management
Reorganise from atomic-design-only folders into bounded contexts
(auth / registratie / herregistratie) over a shared kernel, each split into
domain / application / infrastructure / ui layers. Dependencies point inward;
the domain layer is framework-free. Path aliases (@shared/@auth/@registratie/
@herregistratie) make import direction explicit.

State management (Elm-style, native TS, no new deps):
- shared/application/store.ts — createStore(init, update): pure reducer + signal
- shared/application/remote-data.ts — add map/map2/map3/andThen combinators so
  several services fold into one RemoteData; <app-async> gains an [rd] input
- registratie/application/big-profile.store.ts — root singleton combining the
  BIG-register and BRP services via map2 into one state; holds the optimistic
  herregistratie flag shared with the dashboard
- herregistratie: machine gains a WizardMsg union + pure reduce; submit is a
  command that calls infra and dispatches the result, with optimistic update +
  rollback against the shared store
- auth: SessionStore + DigiD adapter + functional route guard; login establishes
  the session, protected routes use canActivate

Rich domain: registration.policy.ts (statusColor/label, herregistratie
eligibility, invariants); BigNummer/Postcode/Uren value objects with smart
constructors. status-badge is now domain-free (colour/label inputs).

Specs for the reducer, RemoteData combinators, and eligibility policy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 07:20:13 +02:00