From b9fd41128385b16c4829a154a03ec03fd6a24966 Mon Sep 17 00:00:00 2001 From: Edwin van den Houdt Date: Fri, 4 Sep 2026 16:26:11 +0200 Subject: [PATCH] refactor: move the portal home into its own overzicht context (RD-03) The dashboard page lived inside registratie, a context with no permitted outbound edges. Two of its sections do cross-context navigation: action links to herregistratie/intake/brief/concepts, and an admin links section that imported ADMIN_LINKS directly from the app shell. Move the page and these two navigation sections into a new overzicht context. Declare overzicht: ['registratie'] as the one sanctioned edge, so the four data sections that stay in registratie/ui/dashboard/ remain reachable. The route stays at /dashboard: it is a user-visible URL and four e2e specs assert it, so only the import path and class name change. beheer-links.section.ts now injects HEADER_ADMIN_LINKS, the same token the site header uses, instead of importing the app shell's nav config directly. Used npm run gen:context and corrected its three known-wrong outputs: the overzicht: [] edge, the scaffolded overzicht.page.ts (the real page is the moved dashboard.page.ts), and the scaffolded path: 'overzicht' route. Verified the boundary is enforced, not decorative: a temporary import of a herregistratie file into overzicht.page.ts makes dep:check fail, as expected, then reverted. Co-Authored-By: Claude Sonnet 5 --- .dependency-cruiser.ssp.js | 1 + apps/ssp/src/app/app.routes.ts | 4 +- .../ui}/beheer-links.section.ts | 5 +- .../ui/overzicht.page.ts} | 19 +- .../ui}/wat-wilt-u-doen.section.ts | 0 apps/ssp/tsconfig.json | 1 + .../RD-03-overzicht-context.md | 168 ++++++++++++++++++ docs/project/readable-codebase/README.md | 2 +- 8 files changed, 187 insertions(+), 13 deletions(-) rename apps/ssp/src/app/{registratie/ui/dashboard => overzicht/ui}/beheer-links.section.ts (85%) rename apps/ssp/src/app/{registratie/ui/dashboard.page.ts => overzicht/ui/overzicht.page.ts} (59%) rename apps/ssp/src/app/{registratie/ui/dashboard => overzicht/ui}/wat-wilt-u-doen.section.ts (100%) create mode 100644 docs/project/readable-codebase/RD-03-overzicht-context.md diff --git a/.dependency-cruiser.ssp.js b/.dependency-cruiser.ssp.js index 52df921..c5815c9 100644 --- a/.dependency-cruiser.ssp.js +++ b/.dependency-cruiser.ssp.js @@ -2,6 +2,7 @@ // scaffolding one (see `gen:context`, WP-44). module.exports = require('./.dependency-cruiser.base.js')( { + overzicht: ['registratie'], auth: [], registratie: [], herregistratie: ['registratie'], // the one sanctioned cross-feature edge diff --git a/apps/ssp/src/app/app.routes.ts b/apps/ssp/src/app/app.routes.ts index 67618a5..610ca24 100644 --- a/apps/ssp/src/app/app.routes.ts +++ b/apps/ssp/src/app/app.routes.ts @@ -14,9 +14,11 @@ export const routes: Routes = [ loadComponent: () => import('@auth/ui/login.page').then((m) => m.LoginPage), }, { + // Path stays 'dashboard' on purpose: it is a user-visible URL and four e2e + // specs assert it. The context is `overzicht`; only the path string differs. path: 'dashboard', canActivate: [authGuard], - loadComponent: () => import('@registratie/ui/dashboard.page').then((m) => m.DashboardPage), + loadComponent: () => import('@overzicht/ui/overzicht.page').then((m) => m.OverzichtPage), }, { path: 'registratie', diff --git a/apps/ssp/src/app/registratie/ui/dashboard/beheer-links.section.ts b/apps/ssp/src/app/overzicht/ui/beheer-links.section.ts similarity index 85% rename from apps/ssp/src/app/registratie/ui/dashboard/beheer-links.section.ts rename to apps/ssp/src/app/overzicht/ui/beheer-links.section.ts index cbfacfc..3bb4e36 100644 --- a/apps/ssp/src/app/registratie/ui/dashboard/beheer-links.section.ts +++ b/apps/ssp/src/app/overzicht/ui/beheer-links.section.ts @@ -3,7 +3,7 @@ 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 { AccessStore } from '@shared/application/access.store'; -import { ADMIN_LINKS } from '../../../shell/nav.config'; +import { HEADER_ADMIN_LINKS } from '@shared/layout/site-header/nav-config'; /** Section: "Beheer" — the admin pages the current principal may reach, capability- gated (never role-derived), the same source + filter the site header uses. @@ -31,5 +31,6 @@ import { ADMIN_LINKS } from '../../../shell/nav.config'; }) export class BeheerLinksSection { private access = inject(AccessStore); - protected adminLinks = computed(() => ADMIN_LINKS.filter((l) => this.access.can(l.cap))); + private rawAdminLinks = inject(HEADER_ADMIN_LINKS); + protected adminLinks = computed(() => this.rawAdminLinks.filter((l) => this.access.can(l.cap))); } diff --git a/apps/ssp/src/app/registratie/ui/dashboard.page.ts b/apps/ssp/src/app/overzicht/ui/overzicht.page.ts similarity index 59% rename from apps/ssp/src/app/registratie/ui/dashboard.page.ts rename to apps/ssp/src/app/overzicht/ui/overzicht.page.ts index 0d4f177..ad66f00 100644 --- a/apps/ssp/src/app/registratie/ui/dashboard.page.ts +++ b/apps/ssp/src/app/overzicht/ui/overzicht.page.ts @@ -1,17 +1,18 @@ import { Component } from '@angular/core'; import { PageShellComponent } from '@shared/layout/page-shell/page-shell.component'; -import { MijnAanvragenSection } from './dashboard/mijn-aanvragen.section'; -import { WatMoetIkRegelenSection } from './dashboard/wat-moet-ik-regelen.section'; -import { MijnRegistratieSection } from './dashboard/mijn-registratie.section'; -import { SpecialismenSection } from './dashboard/specialismen.section'; -import { WatWiltUDoenSection } from './dashboard/wat-wilt-u-doen.section'; -import { BeheerLinksSection } from './dashboard/beheer-links.section'; +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 { 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) — see `ui/dashboard/*.section.ts`. */ + question (own store, own async state) — four sections stay in `registratie/ui/dashboard/` + (they render registratie data), two live here (cross-context navigation). */ @Component({ - selector: 'app-dashboard-page', + selector: 'app-overzicht-page', imports: [ PageShellComponent, MijnAanvragenSection, @@ -39,4 +40,4 @@ import { BeheerLinksSection } from './dashboard/beheer-links.section'; `, }) -export class DashboardPage {} +export class OverzichtPage {} diff --git a/apps/ssp/src/app/registratie/ui/dashboard/wat-wilt-u-doen.section.ts b/apps/ssp/src/app/overzicht/ui/wat-wilt-u-doen.section.ts similarity index 100% rename from apps/ssp/src/app/registratie/ui/dashboard/wat-wilt-u-doen.section.ts rename to apps/ssp/src/app/overzicht/ui/wat-wilt-u-doen.section.ts diff --git a/apps/ssp/tsconfig.json b/apps/ssp/tsconfig.json index 34dd533..b76b871 100644 --- a/apps/ssp/tsconfig.json +++ b/apps/ssp/tsconfig.json @@ -6,6 +6,7 @@ "compilerOptions": { "baseUrl": ".", "paths": { + "@overzicht/*": ["src/app/overzicht/*"], "@shared/*": ["../../libs/shared/src/*"], "@beheer/*": ["../../libs/beheer/src/*"], "@auth/*": ["src/app/auth/*"], diff --git a/docs/project/readable-codebase/RD-03-overzicht-context.md b/docs/project/readable-codebase/RD-03-overzicht-context.md new file mode 100644 index 0000000..834c2a8 --- /dev/null +++ b/docs/project/readable-codebase/RD-03-overzicht-context.md @@ -0,0 +1,168 @@ +# RD-03 — Move the portal home into its own `overzicht` context + +Status: done +Source: PLAN.md Phase 0 + +## Why + +The dashboard is the portal home, but it lives inside `registratie` — a context that +`.dependency-cruiser.ssp.js` declares as `registratie: []`, permitted to import no other +context. Three concrete symptoms: + +- Its six sections span four concerns: registratie data (3), aanvragen (1), cross-context + action links to `/herregistratie` `/intake` `/brief` `/concepts` (1), and admin links to + `/beheer/*` (1). +- The cross-context coupling is **invisible to the linter**, because + `wat-wilt-u-doen.section.ts` links by route _string_. `npm run dep:check` passes and gives + false assurance on exactly this file. +- `beheer-links.section.ts:6` imports `ADMIN_LINKS` from `../../../shell/nav.config` — a + context reaching into the app frame. + +Splitting the dashboard into sections is what makes this fixable at all. That is the +refactor's first real payoff. + +## Read first + +- `apps/ssp/src/app/registratie/ui/dashboard.page.ts` (42 lines, pure composition) +- `apps/ssp/src/app/registratie/ui/dashboard/` — the six sections +- `.dependency-cruiser.ssp.js` — the `CONTEXT_ALLOWED` map (5 lines) +- `libs/shared/src/layout/site-header/nav-config.ts` — the `HEADER_ADMIN_LINKS` token +- `libs/shared/src/layout/site-header/site-header.component.ts:99` — how to inject it +- `apps/ssp/src/app/app.config.ts:72` — where `ADMIN_LINKS` is already provided to the token +- `plopfile.mjs`, the `context` generator +- CLAUDE.md decision 1 (DDD contexts and layers) + +## Decisions (pre-made, don't relitigate) + +1. **New context name: `overzicht`.** Dutch, per CLAUDE.md ("domain contexts are Dutch"), and + it matches the page's own title, "Mijn overzicht". + +2. **Only the page and the two _navigation_ sections move.** These three: + - `dashboard.page.ts` → `overzicht/ui/overzicht.page.ts` (class `OverzichtPage`, selector + `app-overzicht-page`) + - `dashboard/wat-wilt-u-doen.section.ts` → `overzicht/ui/wat-wilt-u-doen.section.ts` + - `dashboard/beheer-links.section.ts` → `overzicht/ui/beheer-links.section.ts` + +3. **The four data sections STAY in `registratie/ui/dashboard/`** — `mijn-aanvragen`, + `wat-moet-ik-regelen`, `mijn-registratie`, `specialismen`. They render registratie data and + belong beside `BigProfileStore`/`AanvragenStore`. Do not move them, and do not move their + three story files. + +4. **Declare `overzicht: ['registratie']`** in `.dependency-cruiser.ssp.js` — the second + sanctioned cross-feature edge, mirroring `herregistratie: ['registratie']`. The generator + inserts `overzicht: []`; you must change it. + +5. **`beheer-links.section.ts` injects `HEADER_ADMIN_LINKS`**, exactly as + `site-header.component.ts:99` does. Drop the `shell/nav.config` import entirely. + `app.config.ts:72` already provides `ADMIN_LINKS` to that token, and the `AdminLink` doc + comment already says it is "Consumed by the site header's admin nav AND (per app) a + dashboard's own Beheer section". The direct import was always the anomaly. + +6. **KEEP the route at `/dashboard`.** Four e2e specs assert it (`e2e/smoke.spec.ts:18`, + `e2e/brief-v2.spec.ts:33`, `e2e/error-state.spec.ts:16,18`), and it is a user-visible URL. + Only the `loadComponent` import path and the class name change. Add a one-line comment in + `app.routes.ts` saying the path stays `dashboard` deliberately while the context is + `overzicht`, so the mismatch reads as a decision and not an oversight. + +7. **Use `npm run gen:context`, then correct three things.** The generator gets the folders, + the `@overzicht/*` tsconfig alias, and the dep-cruiser entry right. It gets three things + wrong for this case: + - it writes `overzicht: []` → change to `['registratie']` (decision 4) + - it scaffolds a **new** `overzicht.page.ts` from a template → delete it, the real page is + the moved `dashboard.page.ts` + - it adds a **new** `path: 'overzicht'` route → delete that block, and instead repoint the + existing `dashboard` route (decision 6) + +8. **Delete the empty layer folders the generator creates.** `overzicht` has no `domain/`, + `application/`, `infrastructure/` or `contracts/` — it is composition only. Remove those + four `.gitkeep` directories rather than keeping scaffolding for work that does not exist. + +9. **The page keeps its 8 imports.** CLAUDE.md has no import-count rule; one `import` per + rendered section is exactly right. Do not introduce an `OVERZICHT_SECTIONS` const to get + the number down — that trades a self-documenting array for an indirection, and this repo + deliberately has no barrels. + +## Files + +Move (use `git mv` so the rename is visible in review): + +| From | To | +| ---------------------------------------------------------------------- | ---------------------------------------------------------- | +| `apps/ssp/src/app/registratie/ui/dashboard.page.ts` | `apps/ssp/src/app/overzicht/ui/overzicht.page.ts` | +| `apps/ssp/src/app/registratie/ui/dashboard/wat-wilt-u-doen.section.ts` | `apps/ssp/src/app/overzicht/ui/wat-wilt-u-doen.section.ts` | +| `apps/ssp/src/app/registratie/ui/dashboard/beheer-links.section.ts` | `apps/ssp/src/app/overzicht/ui/beheer-links.section.ts` | + +Edit: `apps/ssp/tsconfig.json` (alias), `.dependency-cruiser.ssp.js` (edge), +`apps/ssp/src/app/app.routes.ts` (import path + class + comment), +`apps/ssp/src/app/overzicht/ui/beheer-links.section.ts` (token), +`apps/ssp/src/app/overzicht/ui/overzicht.page.ts` (class, selector, section import paths). + +## Steps + +1. Run `npm run gen:context` and answer `overzicht`. +2. Delete the generated `overzicht/ui/overzicht.page.ts` and the four empty layer folders. +3. Delete the generated `path: 'overzicht'` route block from `app.routes.ts`. +4. Change the generated `overzicht: []` to `overzicht: ['registratie']`. +5. `git mv` the three files per the table above. +6. Rename the page's class to `OverzichtPage` and its selector to `app-overzicht-page`; fix + its six section import paths (four now via `@registratie/ui/dashboard/…`, two local). +7. Repoint the existing `dashboard` route to `@overzicht/ui/overzicht.page` / `OverzichtPage` + and add the comment from decision 6. +8. Rewrite `beheer-links.section.ts` to inject `HEADER_ADMIN_LINKS` per decision 5. +9. Update this ticket's `Status:` to `done` and the README's RD-03 row to `done`. +10. Commit all of it together. + +## Acceptance criteria + +```bash +npm run dep:check # exits 0 — proves overzicht: ['registratie'] is accepted +npm run ci # exits 0 +``` + +Then prove the boundary is real, not decorative: + +```bash +# Add a temporary import of a herregistratie file to overzicht/ui/overzicht.page.ts. +# `npm run dep:check` MUST fail (overzicht may reach registratie only). Then revert. +``` + +And prove the section still renders for an admin: + +```bash +grep -n "HEADER_ADMIN_LINKS" apps/ssp/src/app/overzicht/ui/beheer-links.section.ts # present +grep -rn "shell/nav.config" apps/ssp/src/app/overzicht/ # no match +``` + +## Verification + +`npm run ci`. No story or `.mdx` file moves in this ticket — the three section stories stay +in `registratie` — so `--full` is not strictly required. Run it once anyway, because this is +a structural move and Storybook globs the app's stories by path. + +Manual check, since e2e is not in the local GREEN one-liner: `npm start`, open +`http://localhost:4200/dashboard`, confirm all six sections render, then `?role=admin` and +confirm the Beheer section appears (and is absent without it). + +## Out of scope + +- Renaming the route to `/overzicht`. It needs a redirect and touches four e2e specs. + Recorded in PLAN.md as deliberately out of scope. +- Moving the four data sections (decision 3). +- Story titles — that is RD-04. +- Turning `wat-wilt-u-doen`'s action list into a token beside `NAV_ITEMS`. PLAN.md Phase 0 + step 6 lists it as a "consider"; leave it for now, since route strings on a landing page + are legitimate once the page lives in a context whose job is cross-context linking. + +## Risks + +- **The generator's three wrong outputs** (decision 7). If you skip the corrections, + `dep:check` still passes with the boundary silently missing — the exact failure mode + `plopfile.mjs`'s own comment warns about for `create-frontend.mjs`. +- **`messages.xlf` / `messages.en.xlf` carry `` paths** + that these moves make stale. Nothing checks them and `$localize` ids are the identity, so + `ng build --localize` stays green. Do **not** hand-edit the xlf files to chase paths. +- **Do not change any `$localize` id.** Every id in the three moved files stays byte + identical; only the file's location changes. A changed id needs a new English `` or + the localize gate fails. +- **`git mv`, not delete-and-create**, or the review loses the rename and the diff looks like + 300 new lines. diff --git a/docs/project/readable-codebase/README.md b/docs/project/readable-codebase/README.md index ac91dc6..0a24fcb 100644 --- a/docs/project/readable-codebase/README.md +++ b/docs/project/readable-codebase/README.md @@ -97,7 +97,7 @@ two. Note that RD-15 exists because 22 abandoned agent worktrees are still on di | ----- | ---------------------------------------------------------------------------- | ---------- | --------- | ------ | | RD-01 | Scaffold this backlog: README, PLAN, ticket template | — | | done | | RD-02 | `max-lines` rule + `reportUnusedDisableDirectives` + 7 disables | 01 | | done | -| RD-03 | `overzicht` context: page + 2 nav sections, boundary edge, admin-links token | 02 | yes | todo | +| RD-03 | `overzicht` context: page + 2 nav sections, boundary edge, admin-links token | 02 | yes | done | | RD-04 | Story titles to `Domein//`; add the missing stories | 03 | yes | todo | | RD-05 | `createStore` gains the effect map + specs | 02 | | todo | | RD-06 | **Bug fix:** 2 single-step forms to the effect map + retry affordance | 05 | yes | todo |