Files
atomic-design-poc/docs/project/backlog/WP-65-behandelportal-beoordeling.md
T
ehoandClaude Sonnet 5 4133b30e5d
CI / changes (pull_request) Successful in 17s
CI / lint (pull_request) Failing after 54s
CI / frontend (pull_request) Successful in 2m38s
CI / storybook-a11y (pull_request) Failing after 3m28s
CI / backend (pull_request) Successful in 2m1s
CI / semgrep (pull_request) Successful in 1m9s
CI / e2e (pull_request) Successful in 2m55s
CI / api-client-drift (pull_request) Successful in 2m1s
feat(behandelportal): WP-65a beoordeling detail (read) + fix unreachable medewerker login
New GET /beoordeling/{id} shows one aanvraag's status, linked documents, and a
canBesluiten decision flag, gated by the same CanBeoordelen capability as the
werkvoorraad list. Reads through IZaakSource.ListCases rather than a new seam
method (WP-66 needs one anyway for the real write); owner BSN is masked.

Fixes a real gap found while wiring this up: the behandelportal's login was still
WP-61's copied citizen/BSN DigiD flow, so nothing ever sent X-Medewerker and the
werkvoorraad screen (WP-64) always denied in a real browser. A dev-only
medewerkerInterceptor (mirrors the existing ?role= stand-in as ?rollen=) fixes that.

WP-65's own Risks note authorized splitting read from write across sessions given
its size; this is the read half. The decision-recording mutation is next (65b).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 09:01:09 +02:00

7.6 KiB
Raw Blame History

WP-65 — Behandelportal: zaak detail + beoordeling (decision) screen

Status: in progress (65a — detail read — done; 65b — decision write — not started) Phase: 11 — Behandelportal

Why

The core case-treatment write path — a medewerker opens one aanvraag's detail (including its documents) and records a decision (goedkeuren/afwijzen/meer info opvragen), advancing the status lifecycle WP-63 published. This is the first genuinely new write capability in the system beyond what the citizen SSP already does to itself.

Read first

  • mutation-command skill
  • form-machine skill (the decision action is a state-changing form, same idiom as everywhere else in this house)
  • WP-63 (the status field being advanced)

Decisions (pre-made, don't relitigate)

  • The decision action is modeled as a *.machine.ts (Model/Msg/reduce) + a submit-* command returning Result, per this house's one-idiom-for-forms rule — not a hand-rolled mutable field.
  • The mutation endpoint is a new BFF-lite write (per mutation-command recipe) that transitions the status field from WP-63; it validates the transition is legal server-side (e.g. can't approve an already-approved case) — the backend remains the authority.
  • Runs against LocalZaakSource for this slice; wiring the decision into real OpenZaak is explicitly WP-66, not bundled here — keeps this slice's surface to app-level behavior only.

Files

New mutation endpoint + command in BigRegister.Api; behandeling/ui detail page + behandeling/application decision machine in the behandelportal app.

Steps

  1. Backend: mutation endpoint advancing aanvraag status (goedkeuren/afwijzen/meer-info- opvragen), validating the transition.
  2. FE: zaak-detail page (documents + current status) + a decision form machine + submit command.
  3. Wire the werkvoorraad list (WP-64) to link into this detail page.
  4. Storybook stories + a11y for the new detail/decision UI.

Progress notes (65a — done)

This WP's own Risks note offered an explicit escape hatch ("split detail-view (read) from decision-recording (write) into two sessions if it feels too big"). Taken: 65a below is done and committed; 65b (the mutation + decision machine + form) is the remaining half — see its own separate session notes once started. Don't relitigate the split; do relitigate nothing else pre-made here.

Blocking gap found and fixed, not in this WP's original scope: the behandelportal's login was still WP-61's copy-pasted citizen/BSN DigiD flow — nothing sent X-Medewerker, so StubIdentityProvider always resolved a zorgverlener, GET /me never contained aanvraag:beoordelen, and WP-64's WerkvoorraadPage always rendered its denial alert in a real browser. WP-64's own Verification line ("manual: log in as a stub medewerker, see the queue populated") could not have passed as written — CI stayed green because none of its tests exercise the browser gate. Fixed with a dev-only medewerkerInterceptor (apps/behandelportal/src/app/auth/infrastructure/medewerker.interceptor.ts + medewerker.ts), mirroring @shared/infrastructure/role.ts's ?role= idiom but app-local (?rollen=, sticky per tab, default behandelaar) — real employee-SSO login stays out of scope (ADR-0002 §3: the two apps' login flows are expected to diverge). Documented in docs/reference/roles-and-access.md.

Backend (GET /beoordeling/{id}): reads through IZaakSource.ListCases (no new seam method — one would force an OpenZaakZaakSource get-by-id + mapper, which is WP-66's surface), filters to the requested id, 404s a Concept or unknown id. Documents come from DocumentStore.ByIds (new method, mirrors ByLocalIds) — DocumentStore directly, not IDocumentSource, since that seam has no read method and its own header comment already says DocumentStore stays the record of truth for preview/download/audit regardless of config. The owner BSN is masked (MaskTail, same helper /brief's BIG-nummer masking uses) — WP-64's queue row leaks the full BSN via the same ApplicationSummaryDto.Owner field; flagging as a follow-up, not fixed here or ssp's /admin/cases page moves too. The gate that was Werkvoorraad(ctx, action) is now Beoordelen(ctx, resource, action) — one gate for every behandelaar endpoint, resource feeding the audit row; the one existing /werkvoorraad call site was updated to pass "werkvoorraad".

Decision-readiness (BeoordelingDecisionsDto.canBesluiten) ships now, not deferred to 65b: BeoordelingRules.CanDecide(AanvraagStatusTag) only inspects the aanvraag's current computed status tag (Ingediend/InBehandeling/MeerInfoGevraagd → decidable; Goedgekeurd/Afgewezen → not) — no persisted "was a decision recorded" field exists yet, so this pure rule needed nothing from 65b's eventual migration to be correct today. 65b adds the mutation, the Besluit enum, and the transition-legality check that reuses this same function.

FE: BeoordelingStatus (domain/beoordeling.ts) is the five-tag union (all of ssp's AanvraagStatus minus Concept, which 404s) — wider than WP-64's WerkvoorraadStatus (two tags). TYPE_LABELS/statusLabel moved from werkvoorraad-item-view.ts into a new domain/beoordeling-view.ts (the file owning the wider union) and are re-exported from the old location so no consumer or existing spec needed to change. Detail page composes <app-data-block>/<app-data-row> (mirrors ssp's aanvraag-detail.page.ts) plus one new organism, beoordeling-documenten (plain links to the existing, pre-existing-unauthenticated /uploads/{id}/content endpoint — not ApplicationLinkComponent, whose to is a routerLink, not an external href). The werkvoorraad list's rows now link to /aanvraag/:id, gated by the same aanvraag:beoordelen capability. No MaskedValueComponent reveal affordance — this screen never reveals the owner, so pulling in that atom's unused reveal machinery would be speculative.

Deferred to 65b, deliberately: the mutation endpoint, the Aanvraag.BesluitStatus column + EF migration, Mappers.ToStatusDto's "a recorded decision wins" branch, the besluit.machine.ts + besluit-form UI, and re-running the werkvoorraad/ssp-dashboard end-to-end smoke this WP's acceptance criteria actually asks for (a decision advancing status, illegal transitions rejected). None of WP-65's acceptance criteria are checked off yet — 65a is infrastructure the decision screen needs, not a slice of the AC itself.

npm run ci green (lint, dep:check ×2, both apps' localized builds, both Storybook builds, test, backend test — 197/197 including this WP's 9 new tests). Only the api-client-drift step shows the expected pre-commit diff (this WP's own uncommitted endpoint).

Acceptance criteria

  • A medewerker can view one aanvraag's detail and record a decision that advances its status.
  • Illegal transitions are rejected server-side (tested).
  • End-to-end smoke: werkvoorraad → detail → decision → status change reflected back in the queue.
  • npm run ci (behandelportal app) + dotnet test green.

Verification

Manual/automated smoke test of the full werkvoorraad → beoordeling → besluit flow against LocalZaakSource; npm run ci; cd backend && dotnet test.

Out of scope

Writing the decision to real OpenZaak (WP-66).

Risks

This is the largest FE slice in Phase 11 — if it feels too big at kickoff, split detail-view (read) from decision-recording (write) into two sessions; the WP as scoped already keeps them in one slice because a detail view with no decision action isn't independently useful for a caseworker.

Depends on: WP-64.