feat(fp): WP-23 — org-template backend + admin role
Second template axis (org identity: letterhead, footer, signature, margins) server-side: OrgTemplateStore with JSON version history, publish/rollback, sent-brief version pinning, admin role + capability, 5 admin endpoints, org-logo upload category. FE seam widened only (Role/Capability unions, interceptor); WP-24/26 consume it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -30,8 +30,9 @@ From WP-01 onward, additionally:
|
||||
npm run test-storybook:ci
|
||||
```
|
||||
|
||||
Backend stays untouched throughout (frontend-only backlog); `cd backend && dotnet test`
|
||||
only needs re-running if a WP unexpectedly touches `backend/`.
|
||||
Phases 0–5 were frontend-only; **phase 6 (Brief v2) touches `backend/`** — for those
|
||||
WPs `cd backend && dotnet test` is part of GREEN, and any wire change ends with
|
||||
`npm run gen:api` leaving no drift.
|
||||
|
||||
From WP-19 onward, `npm run e2e` is part of CI (its own job) but NOT part of the local
|
||||
GREEN one-liner above — it needs the real backend + `npm start` already running (see
|
||||
@@ -63,9 +64,15 @@ for its existing violations, so every WP ends green.
|
||||
| [WP-17](WP-17-app-a11y.md) | App-level a11y: route focus, template lint, WCAG checklist | 4 · a11y | done |
|
||||
| [WP-18](WP-18-abac-capability-spine.md) | ABAC capability spine (Principal + capabilities, phase P1) | 5 · productie-volwassenheid | done |
|
||||
| [WP-19](WP-19-e2e-smoke.md) | Playwright e2e smoke | 5 · productie-volwassenheid | done |
|
||||
| [WP-20](WP-20-second-locale.md) | Second locale proof | 5 · productie-volwassenheid | todo |
|
||||
| [WP-21](WP-21-resilience-seams.md) | Resilience seams (correlation-id, idempotency, retry) | 5 · productie-volwassenheid | todo |
|
||||
| [WP-22](WP-22-durable-persistence.md) | Durable persistence (optional tier) | 5 · productie-volwassenheid | todo |
|
||||
| [WP-20](WP-20-second-locale.md) | Second locale proof | 5 · productie-volwassenheid | done |
|
||||
| [WP-21](WP-21-resilience-seams.md) | Resilience seams (correlation-id, idempotency, retry) | 5 · productie-volwassenheid | done |
|
||||
| [WP-22](WP-22-durable-persistence.md) | Durable persistence (optional tier) | 5 · productie-volwassenheid | done |
|
||||
| [WP-23](WP-23-org-template-backend.md) | Org-template backend + admin role | 6 · Brief v2 | in-progress |
|
||||
| [WP-24](WP-24-letter-canvas.md) | Letter canvas (edit on the letter) | 6 · Brief v2 | todo |
|
||||
| [WP-25](WP-25-letter-preview-html.md) | Server-rendered letter preview (HTML; PDF deferred) | 6 · Brief v2 | todo |
|
||||
| [WP-26](WP-26-org-template-editor.md) | Admin org-template editor | 6 · Brief v2 | todo |
|
||||
| [WP-27](WP-27-brief-ux-layer.md) | Brief UX layer (undo/redo, standaardbrief, diff) | 6 · Brief v2 | todo |
|
||||
| [WP-28](WP-28-brief-v2-demo-polish.md) | Brief v2 demo polish (scenarios, e2e, docs) | 6 · Brief v2 | todo |
|
||||
|
||||
Sequencing dependencies (stated in the WPs too): 01 before 10–15 (axe covers story churn);
|
||||
03/04 before 05–09 (boundaries stop new violations during refactors); 06 before 07 (typed
|
||||
@@ -75,6 +82,9 @@ are independent of each other and of phases 1–4 — pick any order; **18 is th
|
||||
first pick** (it's the headline gap: no authorization spine exists yet, and it closes the
|
||||
FE-computed-authz anti-pattern in `brief.store.ts`). 22 is explicitly lower priority — the
|
||||
current in-memory persistence is a documented, defensible POC choice, not a bug.
|
||||
Phase 6 (Brief v2, the "Brief opstellen v2" PRD) is strictly ordered
|
||||
23 → 24 → 25 → 26 → 27 → 28: 24 needs 23's `orgTemplate` on the wire, 25 needs 24's
|
||||
`letter.css` contract, 26 needs 23's endpoints + 24's canvas, 27/28 polish on top.
|
||||
|
||||
## WP template
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# WP-20 — Second locale proof
|
||||
|
||||
Status: done (pending commit)
|
||||
Status: done (e276629)
|
||||
Phase: 5 — productie-volwassenheid
|
||||
|
||||
## Why
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# WP-21 — Resilience seams (correlation-id, idempotency, retry)
|
||||
|
||||
Status: done (pending commit)
|
||||
Status: done (40dbcb2)
|
||||
Phase: 5 — productie-volwassenheid
|
||||
|
||||
## Why
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# WP-22 — Durable persistence (optional tier)
|
||||
|
||||
Status: done (pending commit)
|
||||
Status: done (556f2f4)
|
||||
Phase: 5 — productie-volwassenheid
|
||||
|
||||
## Why
|
||||
|
||||
114
docs/backlog/WP-23-org-template-backend.md
Normal file
114
docs/backlog/WP-23-org-template-backend.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# WP-23 — Org-template backend + admin role
|
||||
|
||||
Status: done
|
||||
Phase: 6 — Brief v2 (edit-on-the-letter, org templates, server-rendered preview)
|
||||
|
||||
## Why
|
||||
|
||||
PRD "Brief opstellen v2" splits a rendered letter over two orthogonal template axes:
|
||||
the **case-type template** (section structure + placeholders — exists, unchanged) and
|
||||
a new **organization template** (appearance/identity per sub-organization: letterhead,
|
||||
footer, signature, margins). This WP builds the second axis server-side plus the
|
||||
`admin` role that will edit it (WP-26). Everything downstream (canvas WP-24, preview
|
||||
WP-25, editor WP-26) reads what this WP serves.
|
||||
|
||||
## Read first
|
||||
|
||||
- `docs/prd` — the Brief v2 PRD §2a/§3 (two axes, OrgTemplate model, invariants)
|
||||
- `backend/src/BigRegister.Api/Data/BriefStore.cs` (store idiom + `BriefSeed`)
|
||||
- `backend/src/BigRegister.Api/Domain/Authorization/Authz.cs` (emit+enforce single source)
|
||||
- `backend/src/BigRegister.Api/Data/AppDbContext.cs` (JSON-column precedent, WP-22)
|
||||
- `docs/backlog/WP-18-abac-capability-spine.md` (how the capability spine works)
|
||||
|
||||
## Decisions (pre-made, don't relitigate)
|
||||
|
||||
- **No case model, no sub-org auth scoping.** `GET /brief` stays; the brief just
|
||||
gains a `SubOrgId`. Two seeded sub-orgs (`cibg-registers`, `cibg-vakbekwaamheid`)
|
||||
exist purely so the admin editor can demo isolation.
|
||||
- **One row per sub-org**, versions as a JSON history column
|
||||
(`OrgTemplateEntity { SubOrgId PK, Draft json, PublishedVersion, History json }`) —
|
||||
the WP-22 JSON-column precedent; no extra tables. Publish = append draft snapshot
|
||||
to history + version++. Rollback = copy `History[v]` into `Draft` (history stays
|
||||
append-only; admin republishes).
|
||||
- **Sent letters are immutable**: `Send` pins `SentOrgTemplateVersion`; a sent
|
||||
brief's `BriefViewDto.orgTemplate` resolves from history, never from the current
|
||||
published version. Unsent briefs always follow the current published version —
|
||||
that is the point of the admin editor.
|
||||
- **Admin = third `X-Role` value** (`PrincipalRole.Admin`), capability
|
||||
`orgtemplate:edit` via `GET /me`. Approve/reject gain an explicit
|
||||
`Role == Approver` condition so the new role cannot slip through the SoD-only
|
||||
check. The existing `X-Admin` document-deletion seam stays untouched.
|
||||
- **Trimmed model** (PRD §3 minus): no signature image asset, no structured address
|
||||
objects, no per-template fonts. Return address / footer contact are multiline
|
||||
strings. Margins are 4 bounded ints (mm, 10–50) — server-validated.
|
||||
- **Logo = existing upload machinery**: seed an `org-logo` category (png/jpeg, 1 MB)
|
||||
under a `org-template` wizardId; the template stores only `logoDocumentId`.
|
||||
- Seed values come from the sample artifact `voorbeeldbrief-inschrijving.pdf`
|
||||
(A. de Vries / Hoofd Registratie / Postbus 00000 / info@voorbeeld.example — all fictitious).
|
||||
|
||||
## Files
|
||||
|
||||
- `backend/src/BigRegister.Api/Contracts/Dtos.cs` — `MarginsDto`, `OrgTemplateDto`,
|
||||
`OrgTemplateVersionDto`, `OrgTemplateAdminViewDto`, `SaveOrgTemplateRequest`,
|
||||
`PublishOrgTemplateResponse`, `SubOrgSummaryDto`; `BriefViewDto` + `OrgTemplate`.
|
||||
- `backend/src/BigRegister.Api/Data/OrgTemplateStore.cs` (new) — entity + store + seed.
|
||||
- `backend/src/BigRegister.Api/Data/AppDbContext.cs` — OrgTemplates DbSet + JSON converters.
|
||||
- `backend/src/BigRegister.Api/Data/Migrations/*` — new migration.
|
||||
- `backend/src/BigRegister.Api/Data/BriefStore.cs` — `SubOrgId`, `SentOrgTemplateVersion`, pin at `Send`.
|
||||
- `backend/src/BigRegister.Api/Domain/Authorization/Authz.cs` — `Admin` role, capability, gate.
|
||||
- `backend/src/BigRegister.Api/Domain/Documents/DocumentCategory.cs` — `org-logo` category.
|
||||
- `backend/src/BigRegister.Api/Program.cs` — 5 admin endpoints, `ToView` orgTemplate resolution.
|
||||
- `backend/tests/BigRegister.Tests/OrgTemplateEndpointTests.cs` (new).
|
||||
- Regenerated: `backend/swagger.json`, `src/app/shared/infrastructure/api-client.ts`.
|
||||
- FE seam only: `src/app/shared/domain/role.ts`, `shared/domain/capability.ts`,
|
||||
`shared/infrastructure/role.ts`, `shared/infrastructure/role.interceptor.ts`.
|
||||
|
||||
## Steps
|
||||
|
||||
1. DTOs (above).
|
||||
2. `OrgTemplateEntity` + `OrgTemplateStore` (list/get/saveDraft/publish/rollback/
|
||||
published/versionPayload; margin validation; seed-on-first-access, 2 sub-orgs,
|
||||
draft == published v1) + AppDbContext mapping + migration.
|
||||
3. `PrincipalRole.Admin`; `ResolvePrincipal` reads `admin`; `RoleCapabilities(Admin)`
|
||||
→ `orgtemplate:edit`; approve/reject checks require `Approver` explicitly.
|
||||
4. Endpoints: `GET /admin/org-templates`, `GET|PUT /admin/org-template/{subOrgId}`,
|
||||
`POST …/publish` (returns impact count = unsent briefs of that sub-org),
|
||||
`POST …/rollback/{version}`. All 403 for non-admin via `Authz`.
|
||||
5. `BriefEntity.SubOrgId` (seed `cibg-registers`) + `SentOrgTemplateVersion`; `Send`
|
||||
pins; `ToView` resolves published-vs-pinned into `BriefViewDto.orgTemplate`.
|
||||
6. Seed `org-logo` upload category.
|
||||
7. `npm run gen:api`.
|
||||
8. FE: widen `Role`/`Capability` unions, `currentRole()`, interceptor URL filter
|
||||
(nothing consumes them yet — WP-24/26 do).
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [x] Publish increments `publishedVersion` and appends to history; rollback copies an
|
||||
old version into the draft without rewriting history.
|
||||
- [x] Every admin endpoint returns 403 for drafter/approver, 200 for `X-Role: admin`.
|
||||
- [x] A sent brief keeps its pinned org-template version after a republish; an unsent
|
||||
brief follows the new published version (both asserted in one test).
|
||||
- [x] Publish impact count = number of unsent briefs of that sub-org.
|
||||
- [x] `PUT` with out-of-bounds margins → 400.
|
||||
- [x] `GET /brief` carries `orgTemplate`; existing brief tests stay green.
|
||||
- [x] `GET /me` with `X-Role: admin` → `["orgtemplate:edit"]`.
|
||||
- [x] Full GREEN (FE untouched functionally, but lint/test/build/storybook all pass).
|
||||
|
||||
## Verification
|
||||
|
||||
`cd backend && dotnet test`; GREEN one-liner; curl smoke: admin list/save/publish
|
||||
(200) vs drafter (403); sent-brief pin walk-through per acceptance.
|
||||
|
||||
## Out of scope
|
||||
|
||||
The canvas (WP-24), HTML preview endpoints + archive-at-send (WP-25), the admin UI
|
||||
(WP-26). Template approval chains (draft→publish is enough for the POC; flagged as
|
||||
an open question in the PRD). Sub-org-scoped brief authorization.
|
||||
|
||||
## Risks
|
||||
|
||||
`BriefViewDto` gains a field — additive, but the FE `parseBriefView` boundary and
|
||||
generated client must be regenerated in the same WP to keep the drift check green.
|
||||
Adding `PrincipalRole.Admin` touches the approve/reject SoD path: the explicit
|
||||
`Role == Approver` condition must preserve today's Forbidden-before-Conflict order
|
||||
(existing tests prove it).
|
||||
91
docs/backlog/WP-24-letter-canvas.md
Normal file
91
docs/backlog/WP-24-letter-canvas.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# WP-24 — Letter canvas (edit on the letter)
|
||||
|
||||
Status: todo
|
||||
Phase: 6 — Brief v2 (edit-on-the-letter, org templates, server-rendered preview)
|
||||
|
||||
## Why
|
||||
|
||||
The drafter should compose **on the letter** — letterhead above, footer/signature
|
||||
below, content blocks edited in place — instead of in an abstract form next to a
|
||||
separate preview. PRD Brief v2 §4. This is a **presentation rebuild only**: the
|
||||
domain model, `brief.machine.ts`, and every `BriefMsg` stay byte-identical.
|
||||
|
||||
## Read first
|
||||
|
||||
- PRD Brief v2 §2b (fidelity note), §4, §10; the sample `voorbeeldbrief-inschrijving.pdf`
|
||||
- `src/app/brief/ui/letter-composer/letter-composer.component.ts` (the `canEdit` pivot)
|
||||
- `src/app/brief/ui/letter-preview/letter-preview.component.ts` (rendering that migrates in)
|
||||
- `docs/backlog/WP-23-org-template-backend.md` (the `orgTemplate` on `BriefViewDto`)
|
||||
|
||||
## Decisions (pre-made, don't relitigate)
|
||||
|
||||
- **One stylesheet is the FE⇄BE rendering contract**: `public/letter.css` — class
|
||||
vocabulary `.letter`, `.letter__letterhead`, `.letter__body`, `.letter__signature`,
|
||||
`.letter__footer`, `.letter__page-break`; margins as `--letter-margin-*` custom
|
||||
props; `@page`/print rules. Loaded via `<link>` in `index.html` (app + Storybook).
|
||||
WP-25's backend renderer inlines the same file; its parity test is the fence.
|
||||
- **`LetterCanvasComponent`** (new organism, `Domein/Brief/Letter Canvas`) with
|
||||
`editableRegions: 'content' | 'template' | 'none'` — one component serves drafter
|
||||
composing, approver review (and in WP-26, the admin editor). Letter typography on
|
||||
the canvas is the letter's, not the portal UI's — but still token-bridged.
|
||||
- `'content'` mode hosts the **existing** `letter-section`/`letter-block` components
|
||||
unchanged; letterhead/footer/signature render read-only with a subtle tint and a
|
||||
first-use caption ("komt uit de huisstijl van de organisatie").
|
||||
- `'none'` mode absorbs `letter-preview`'s rendering (paragraph grouping, placeholder
|
||||
chips, sample-values toggle); **`ui/letter-preview/` is then deleted** — the PRD
|
||||
explicitly supersedes it; no third rendering is maintained.
|
||||
- **Page-break indicator is approximate by design**: a dashed line per A4-content
|
||||
interval with the caption "±pagina-einde — afdrukvoorbeeld is leidend" (PRD §2b
|
||||
honesty requirement).
|
||||
- `brief.machine.ts` and `BriefMsg` are untouched — `git diff` must prove it.
|
||||
|
||||
## Files
|
||||
|
||||
- `public/letter.css` (new), `src/index.html` (link)
|
||||
- `src/app/brief/domain/org-template.ts` (new, pure) — `OrgTemplate` type
|
||||
- `src/app/brief/infrastructure/brief.adapter.ts` (+spec) — parse `orgTemplate`
|
||||
- `src/app/brief/ui/letter-canvas/*` (new: component + stories)
|
||||
- `src/app/brief/ui/letter-composer/letter-composer.component.ts` (pivot swap) + stories
|
||||
- `src/app/brief/ui/brief.page.ts` (pass orgTemplate through)
|
||||
- delete `src/app/brief/ui/letter-preview/*`
|
||||
|
||||
## Steps
|
||||
|
||||
1. `letter.css` from the sample PDF's geometry (A4 proportions, letterhead, address
|
||||
window + reference block, footer rule, signature).
|
||||
2. `OrgTemplate` domain type + `parseOrgTemplate` boundary in the adapter (+spec).
|
||||
3. Canvas organism: three modes, zoom input, page-break indicator.
|
||||
4. Migrate `letter-preview` rendering into `'none'` mode; delete the component,
|
||||
fold its stories into the canvas stories.
|
||||
5. Swap the composer's `@if (canEdit())` pivot for
|
||||
`<app-letter-canvas [editableRegions]="canEdit() ? 'content' : 'none'">`.
|
||||
6. Stories: three modes + zoom + page-break, all axe-gated.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] Drafter edits blocks in place on the letter surface; passage picker and
|
||||
diagnostics click-to-locate still work on the canvas.
|
||||
- [ ] Approver sees the identical surface read-only with the action bar.
|
||||
- [ ] Letterhead/footer/signature come from `orgTemplate` and are visibly non-editable.
|
||||
- [ ] `git diff` shows zero changes in `brief.machine.ts` / `brief.ts` Msg surface.
|
||||
- [ ] `letter-preview` is gone; no story regression (`test-storybook:ci` green).
|
||||
- [ ] Full GREEN + e2e smoke.
|
||||
|
||||
## Verification
|
||||
|
||||
GREEN one-liner; `npm run e2e` (brief flow); manual: `?role=drafter` compose on
|
||||
canvas, `?role=approver` review; `?scenario=slow|error` still degrade gracefully
|
||||
through `<app-async>`.
|
||||
|
||||
## Out of scope
|
||||
|
||||
Server-rendered preview + `[NOG IN TE VULLEN]` resolution (WP-25); admin `'template'`
|
||||
mode wiring beyond the input existing (WP-26 gives it a consumer); zoom controls
|
||||
polish + standaardbrief + diff badges (WP-27).
|
||||
|
||||
## Risks
|
||||
|
||||
The canvas duplicates the letter markup that WP-25's backend renderer will emit —
|
||||
acceptable *only* because `letter.css` is shared and WP-25 adds the class-parity
|
||||
test; until WP-25 lands, the canvas is the sole consumer, so no drift is possible.
|
||||
Deleting `letter-preview` breaks any deep import of it — repo-wide grep before delete.
|
||||
90
docs/backlog/WP-25-letter-preview-html.md
Normal file
90
docs/backlog/WP-25-letter-preview-html.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# WP-25 — Server-rendered letter preview (HTML; PDF seam deferred)
|
||||
|
||||
Status: todo
|
||||
Phase: 6 — Brief v2 (edit-on-the-letter, org templates, server-rendered preview)
|
||||
|
||||
## Why
|
||||
|
||||
"What you compose is what is sent" needs a server-side rendering of the letter —
|
||||
placeholders resolved, org template applied — from the same CSS contract the canvas
|
||||
uses (PRD §2b: one rendering, used twice). The preview is the artifact: at send, the
|
||||
same composition is archived with the brief, making sent letters immutable.
|
||||
|
||||
## Read first
|
||||
|
||||
- PRD Brief v2 §2b, §8; `docs/backlog/WP-24-letter-canvas.md` (the `letter.css` contract)
|
||||
- `backend/src/BigRegister.Api/Program.cs` — upload `content` endpoint (binary house
|
||||
pattern: `.ExcludeFromDescription()` + hand-written FE fetch)
|
||||
- `src/app/shared/upload/upload.adapter.ts` (hand-written transport precedent)
|
||||
|
||||
## Decisions (pre-made, don't relitigate)
|
||||
|
||||
- **HTML, not PDF** (user decision at plan review): no Microsoft.Playwright/Chromium
|
||||
dependency in the POC. `GET /api/v1/brief/preview` returns `text/html` — the fully
|
||||
composed, print-ready letter (`@page` CSS; browser print-to-PDF is the manual
|
||||
affordance). The endpoint is the seam where a headless-Chromium PDF render slots
|
||||
in later; mark it `// ponytail: HTML today, Chromium PDF behind this same route if
|
||||
the POC ever needs real PDF bytes`.
|
||||
- **`LetterHtml.Render(brief, orgTemplate)`** is a pure static composer: mirrors the
|
||||
canvas class vocabulary exactly, inlines `public/letter.css` from disk, inlines the
|
||||
logo bytes as a data-URI. Placeholders: auto-resolvable keys resolve from
|
||||
seed/case data; unresolved manual keys render as `[NOG IN TE VULLEN: label]`
|
||||
(PRD §8) — preview is allowed with errors, only send blocks on them.
|
||||
- **Parity is tested, not hoped for**: a golden-file test snapshots the composed
|
||||
HTML; a second test asserts every `letter`-prefixed class in the golden HTML
|
||||
exists in `letter.css`. `dotnet test` never launches a browser.
|
||||
- **Archive at send**: `Send` stores the composed HTML in `BriefEntity.ArchivedHtml`
|
||||
(SQLite text column) alongside the WP-23 version pin; the preview endpoint serves
|
||||
the archive when status is `sent`, so a republish never changes a sent letter.
|
||||
- **Two endpoints, both excluded from OpenAPI** (JSON-only generated client stays
|
||||
clean): `GET /brief/preview` and `GET /admin/org-template/{subOrgId}/preview`
|
||||
(proefbrief: draft template + a fixture brief). FE consumes them via a small
|
||||
hand-written fetch (needs the `X-Role` header) → blob → object URL in a new tab.
|
||||
- Watermark: previews of unsent letters carry a `VOORBEELD` watermark (CSS), the
|
||||
archived/sent rendering never does — the PRD's open question resolved the simple way.
|
||||
|
||||
## Files
|
||||
|
||||
- `backend/src/BigRegister.Api/Domain/Letters/LetterHtml.cs` (new)
|
||||
- `backend/src/BigRegister.Api/Data/BriefStore.cs` — `ArchivedHtml` + archive at send (+migration)
|
||||
- `backend/src/BigRegister.Api/Program.cs` — 2 preview endpoints
|
||||
- `backend/tests/BigRegister.Tests/LetterHtmlTests.cs` (new) + `LetterHtml.golden.html`
|
||||
- `src/app/brief/infrastructure/letter-preview.adapter.ts` (new, fetch → `Result<string, Blob>`)
|
||||
- `src/app/brief/application/brief.store.ts` — `previewLetter()` command
|
||||
- `src/app/brief/ui/letter-composer/*` — "Voorbeeld" button
|
||||
|
||||
## Steps
|
||||
|
||||
1. `LetterHtml.Render` + placeholder resolution + data-URI logo + watermark flag.
|
||||
2. Golden-file + class-parity tests.
|
||||
3. Endpoints (serve archive when sent; proefbrief renders the draft template).
|
||||
4. Archive-at-send in `BriefStore.Send` (+ migration for `ArchivedHtml`).
|
||||
5. FE adapter + store command + button (explicit action — no live re-render; PRD §8).
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] Preview opens the composed print-ready letter in a new tab; browser print
|
||||
shows correct margins via `@page`.
|
||||
- [ ] Unresolved manual placeholders render `[NOG IN TE VULLEN: …]`; preview works
|
||||
despite lint errors (only send blocks).
|
||||
- [ ] A sent brief serves its archived HTML unchanged after an org-template republish.
|
||||
- [ ] Golden + parity tests green without any browser installed.
|
||||
- [ ] `swagger.json` unchanged by the two endpoints (drift check green).
|
||||
|
||||
## Verification
|
||||
|
||||
`cd backend && dotnet test`; GREEN one-liner; manual: compose → preview → print
|
||||
dialog; send → republish template → preview still the archived rendering.
|
||||
|
||||
## Out of scope
|
||||
|
||||
Real PDF bytes / headless Chromium (the deliberate deferral — the endpoint is the
|
||||
seam). Pixel-parity testing (the shared CSS + class-parity test is the fence).
|
||||
Pagination fidelity beyond the browser's own print engine.
|
||||
|
||||
## Risks
|
||||
|
||||
`LetterHtml` reads `public/letter.css` from disk — path must resolve for `dotnet run`,
|
||||
tests, and docker (bind mount/copy); fail loudly with a clear error if missing.
|
||||
The golden file will churn whenever the letter structure changes — that is its job;
|
||||
update it deliberately, never blindly.
|
||||
90
docs/backlog/WP-26-org-template-editor.md
Normal file
90
docs/backlog/WP-26-org-template-editor.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# WP-26 — Admin org-template editor
|
||||
|
||||
Status: todo
|
||||
Phase: 6 — Brief v2 (edit-on-the-letter, org templates, server-rendered preview)
|
||||
|
||||
## Why
|
||||
|
||||
The org template (WP-23) needs its editor: an admin edits, per sub-organization, the
|
||||
letter's appearance **in place on the same canvas** the drafter composes on — the
|
||||
mirror image (`editableRegions='template'`: letterhead/footer/signature editable,
|
||||
content a read-only sample). PRD Brief v2 §5, §7h.
|
||||
|
||||
## Read first
|
||||
|
||||
- PRD Brief v2 §5, §7h; `docs/backlog/WP-23/24/25` (endpoints, canvas, proefbrief)
|
||||
- `src/app/shared/application/access.store.ts` (`can('orgtemplate:edit')`)
|
||||
- `.claude/skills/form-machine` — the house form idiom this editor follows
|
||||
- `src/app/shared/ui/upload/single-upload` (logo upload reuse)
|
||||
|
||||
## Decisions (pre-made, don't relitigate)
|
||||
|
||||
- **Lives in the `brief` context** (route `/brief/huisstijl`, lazy) — same bounded
|
||||
capability, no new context. Gated by `AccessStore.can('orgtemplate:edit')` with a
|
||||
denial alert (deny-by-default); no new route guard.
|
||||
- **House form-machine idiom**: `org-template.machine.ts`
|
||||
(`OrgTemplateState`/`OrgTemplateMsg`, pure `reduce` + spec) — draft fields are form
|
||||
state, not brief state. Store (`org-template.store.ts`, root singleton) does
|
||||
debounced draft save (mirror `BriefStore.scheduleSave`), publish, rollback.
|
||||
- **Publish shows impact first**: confirmation displays the WP-23 impact count
|
||||
("Dit raakt N nog niet verzonden brieven") before the POST.
|
||||
- **Version history is a list, rollback copies into draft** (WP-23 semantics) —
|
||||
no side-by-side rendered diff (deferred; field-level history list is enough here).
|
||||
- **Logo upload reuses `single-upload`** against the `org-logo` category; the canvas
|
||||
shows `<img src="/api/v1/uploads/{id}/content">`.
|
||||
- **Proefbrief** = the WP-25 admin preview endpoint; just a button.
|
||||
- Margins are bounded number inputs (server re-validates, WP-23).
|
||||
|
||||
## Files
|
||||
|
||||
- `src/app/brief/domain/org-template.machine.ts` (+spec)
|
||||
- `src/app/brief/application/org-template.store.ts`
|
||||
- `src/app/brief/infrastructure/org-template.adapter.ts` (+spec, `parseOrgTemplateAdminView`)
|
||||
- `src/app/brief/ui/org-template-editor/*` (organism + stories)
|
||||
- `src/app/brief/ui/org-template.page.ts`
|
||||
- `src/app/app.routes.ts` (route `brief/huisstijl`)
|
||||
|
||||
## Steps
|
||||
|
||||
1. Machine (fields, `FieldEdited`/`MarginEdited`/`LogoSet`/`DraftLoaded`/save-publish
|
||||
outcome Msgs) + spec.
|
||||
2. Adapter (generated client CRUD + parse boundary) + spec.
|
||||
3. Store: load (sub-org list + selected), debounced save, publish (impact confirm),
|
||||
rollback.
|
||||
4. Editor UI: sub-org switcher, canvas in `'template'` mode with inline-editable
|
||||
regions, margins inputs, logo upload, version history + rollback, proefbrief
|
||||
button, publish bar showing live version + published-at.
|
||||
5. Route + capability gate + stories (axe).
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] `?role=admin` can switch sub-orgs, edit all template fields in place on the
|
||||
canvas, and see the canvas update live.
|
||||
- [ ] Draft saves are debounced; publish asks for confirmation showing the impact
|
||||
count; after publish the drafter's canvas (WP-24) reflects it on reload.
|
||||
- [ ] Version history lists published versions (who is faked, when is real);
|
||||
rollback copies an old version into the draft.
|
||||
- [ ] Non-admin on `/brief/huisstijl` sees the denial alert; API would 403 anyway.
|
||||
- [ ] Logo upload validates type/size client-side (existing `rejectReason`) and
|
||||
renders on the canvas after upload.
|
||||
- [ ] Machine spec covers field edits, dirty tracking, publish/rollback outcomes.
|
||||
- [ ] Full GREEN.
|
||||
|
||||
## Verification
|
||||
|
||||
GREEN one-liner; manual walk: admin edits footer + margin → canvas live-updates →
|
||||
proefbrief shows draft → publish (impact count) → `?role=drafter` reload shows new
|
||||
appearance; second sub-org unaffected.
|
||||
|
||||
## Out of scope
|
||||
|
||||
Template approval chain (four-eyes on templates — PRD open question, out for POC).
|
||||
Rendered side-by-side version diff. Soft locks. New shared overlay/modal component —
|
||||
the publish confirmation uses the existing inline confirmation pattern, not a dialog.
|
||||
|
||||
## Risks
|
||||
|
||||
The editor is the first consumer of `editableRegions='template'` — WP-24 built the
|
||||
input but nothing exercised it; budget for canvas fixes here. Debounced draft save +
|
||||
publish can race — flush the draft save before publishing (same
|
||||
`clearTimeout`+`flushSave` discipline as `BriefStore.transition`).
|
||||
93
docs/backlog/WP-27-brief-ux-layer.md
Normal file
93
docs/backlog/WP-27-brief-ux-layer.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# WP-27 — Brief UX layer (undo/redo, standaardbrief, search, diff badges)
|
||||
|
||||
Status: todo
|
||||
Phase: 6 — Brief v2 (edit-on-the-letter, org templates, server-rendered preview)
|
||||
|
||||
## Why
|
||||
|
||||
PRD Brief v2 §7: the working-day features that make the composer pleasant daily.
|
||||
Several are nearly free **because** state is one immutable value — that's the
|
||||
teaching payload: undo/redo is a shell-side snapshot list, the rejection diff is a
|
||||
pure function over two values. Say so in code comments and stories.
|
||||
|
||||
## Read first
|
||||
|
||||
- PRD Brief v2 §7 (and the plan-review trim recorded below)
|
||||
- `src/app/brief/application/brief.store.ts` (autosave + `SaveState` already exist)
|
||||
- `src/app/brief/domain/brief.machine.ts` — the `Seed` Msg (undo/redo's restore path)
|
||||
|
||||
## Decisions (pre-made, don't relitigate)
|
||||
|
||||
- **Trim agreed at plan review.** IN: undo/redo, autosave retry affordance,
|
||||
standaardbrief, passage search, canvas zoom controls, Ctrl+Z/Ctrl+Shift+Z,
|
||||
block-level rejection-diff badges. OUT (deferred, one line each in Out of scope):
|
||||
soft lock/takeover, case-context panel, 401 autosave grace, per-user usage counts,
|
||||
shortcut-overlay dialog, inline character-level text diff.
|
||||
- **Undo/redo is shell state, not machine state**: `past`/`future: Brief[]` in
|
||||
`BriefStore` (cap 50; push on `edit()`; clear `future` on a new edit); restore
|
||||
dispatches the **existing `Seed` Msg** — zero machine changes — then `scheduleSave()`.
|
||||
- **Standaardbrief**: backend seeds `IsDefault` on 2–3 kern passages
|
||||
(`LibraryPassageDto` gains the flag); one button, visible only while the kern
|
||||
section is empty, dispatches the existing `PassagesInserted` with the default set —
|
||||
one Msg, one undo step.
|
||||
- **Passage search is a client-side filter** in the picker (label + content match) —
|
||||
the library is small; no server search, no usage tracking.
|
||||
- **Rejection diff**: pure `diffBlocks(before, after): BlockDiff[]` in
|
||||
`domain/brief-diff.ts` (added/removed/changed by `blockId`); the "before" snapshot
|
||||
is captured shell-side when the `Rejected` dispatch happens (POC limit: lost on
|
||||
reload — comment it). Rendered as "gewijzigd sinds afwijzing" badges on the canvas;
|
||||
the approver gets a "Toon wijzigingen" toggle on resubmission.
|
||||
- **Autosave retry**: `SaveState.Error` already exists; add the "Opnieuw proberen"
|
||||
button that calls the existing flush path. No new state.
|
||||
|
||||
## Files
|
||||
|
||||
- `src/app/brief/application/brief.store.ts` (+spec: history bounds, clear-on-edit,
|
||||
redo, rejection snapshot)
|
||||
- `src/app/brief/domain/brief-diff.ts` (new, +spec)
|
||||
- `backend/src/BigRegister.Api/Data/BriefStore.cs` (`IsDefault` seed) +
|
||||
`Contracts/Dtos.cs` (`LibraryPassageDto`) + gen:api + adapter parse
|
||||
- `src/app/brief/ui/passage-picker/*` (search input)
|
||||
- `src/app/brief/ui/letter-canvas/*` (diff badges, standaardbrief button, zoom controls)
|
||||
- `src/app/brief/ui/brief.page.ts` (undo/redo buttons + keydown listener, retry button)
|
||||
|
||||
## Steps
|
||||
|
||||
1. `diffBlocks` + spec (added/removed/changed/unchanged; changed = same blockId,
|
||||
different content).
|
||||
2. Store: history + undo/redo + rejection snapshot (+spec).
|
||||
3. Backend `IsDefault` + gen:api + parse.
|
||||
4. UI: standaardbrief button, search, zoom, badges, keyboard, retry.
|
||||
5. Stories for the new states (axe).
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] Remove a block → Ctrl+Z restores it → Ctrl+Shift+Z re-removes; buttons mirror;
|
||||
history capped at 50; a new edit clears redo; restore re-triggers autosave.
|
||||
- [ ] Empty kern + "Standaardbrief invoegen" → default passages inserted as one undo
|
||||
step; button gone once kern is non-empty.
|
||||
- [ ] Search filters passages by label and content.
|
||||
- [ ] Reject → edit → resubmit: approver toggles "Toon wijzigingen", changed/added/
|
||||
removed blocks are badged (block granularity).
|
||||
- [ ] Autosave failure shows "Niet opgeslagen — opnieuw proberen"; retry works;
|
||||
content never lost locally.
|
||||
- [ ] Full GREEN.
|
||||
|
||||
## Verification
|
||||
|
||||
GREEN one-liner; store + diff specs; manual reject→edit→diff walk with two roles.
|
||||
|
||||
## Out of scope (deferred, per plan review)
|
||||
|
||||
Soft lock/heartbeat/takeover (real session infra, no FP teaching value here).
|
||||
Case-context panel (no case data exists). 401 autosave grace (auth is faked).
|
||||
Per-user passage usage counts (bookkeeping, demos nothing). Shortcut overlay dialog
|
||||
(no modal component exists; not worth building one). Inline character-level diff
|
||||
(block granularity carries the teaching point).
|
||||
|
||||
## Risks
|
||||
|
||||
Undo history holds `Brief` snapshots — deep-frozen immutable values, so sharing is
|
||||
safe, but never push non-content dispatches (status transitions, `Seed` itself) into
|
||||
history or undo will replay workflow state. The rejection snapshot lives in memory
|
||||
only — document it where it's captured.
|
||||
66
docs/backlog/WP-28-brief-v2-demo-polish.md
Normal file
66
docs/backlog/WP-28-brief-v2-demo-polish.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# WP-28 — Brief v2 demo polish (scenarios, e2e, docs)
|
||||
|
||||
Status: todo
|
||||
Phase: 6 — Brief v2 (edit-on-the-letter, org templates, server-rendered preview)
|
||||
|
||||
## Why
|
||||
|
||||
Phase 6 ships across five WPs; this one makes it demonstrable and closes the loop:
|
||||
a demo script that maps every kept PRD §12 scenario to a URL + click path, an e2e
|
||||
spec covering the new flows end-to-end, story gap-fill, and the docs/README updates
|
||||
that keep CLAUDE.md and the backlog truthful.
|
||||
|
||||
## Read first
|
||||
|
||||
- PRD Brief v2 §6 (demo choreography), §12 (scenario list); WP-23..27 as built
|
||||
- `e2e/` (WP-19 conventions); `src/app/shared/infrastructure/scenario.ts`
|
||||
|
||||
## Decisions (pre-made, don't relitigate)
|
||||
|
||||
- **No preset registry.** The PRD's 18 scenarios collapse onto the existing toggles:
|
||||
`?role=drafter|approver|admin`, `?scenario=slow|loading|error` (the interceptor
|
||||
already covers all `/api/` calls, the new endpoints included), and
|
||||
`POST /brief/reset`. The demo script documents the mapping; no new interceptor
|
||||
cases, no scenario code.
|
||||
- Demo script lives at `docs/prd/0003-brief-v2-demo-script.md` and follows the §6
|
||||
choreography (compose → preview → switch sub-org seed → "two axes, one render").
|
||||
- One e2e spec, not a suite: drafter composes on canvas → submit → approve → send
|
||||
pins the org-template version; admin publishes → drafter canvas reflects it.
|
||||
Preview assertion is content-type-level (text/html), not pixel.
|
||||
- CLAUDE.md gets the new role value + route only — keep it rules, not narrative.
|
||||
|
||||
## Files
|
||||
|
||||
- `docs/prd/0003-brief-v2-demo-script.md` (new)
|
||||
- `e2e/brief-v2.spec.ts` (new)
|
||||
- story gap-fill where WP-24..27 left holes
|
||||
- `docs/backlog/README.md` (statuses), `CLAUDE.md` (roles/routes touch-up)
|
||||
|
||||
## Steps
|
||||
|
||||
1. Demo script: table scenario → URL + clicks, covering every kept §12 entry.
|
||||
2. e2e spec (backend + FE running, WP-19 pattern).
|
||||
3. Story sweep for the new components/states.
|
||||
4. Docs updates.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] Every kept PRD §12 scenario has a working URL + click path in the script
|
||||
(walked manually once).
|
||||
- [ ] `npm run e2e` green, including the new spec.
|
||||
- [ ] Full GREEN; backlog README statuses correct; CLAUDE.md mentions
|
||||
`?role=admin` and `/brief/huisstijl`.
|
||||
|
||||
## Verification
|
||||
|
||||
Walk the demo script top to bottom against `docker compose up`; GREEN one-liner;
|
||||
`npm run e2e`.
|
||||
|
||||
## Out of scope
|
||||
|
||||
New scenario interceptor cases; a scenario-switcher UI; screenshots/video.
|
||||
|
||||
## Risks
|
||||
|
||||
The demo script rots when flows change — it lists URLs + clicks only (no prose
|
||||
walkthroughs), so churn stays cheap.
|
||||
Reference in New Issue
Block a user