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>
This commit is contained in:
2026-06-26 07:20:13 +02:00
parent 6bd6e854c7
commit 2114514ad7
74 changed files with 841 additions and 347 deletions

View File

@@ -0,0 +1,31 @@
import { Component, input } from '@angular/core';
import { HeadingComponent } from '@shared/ui/heading/heading.component';
import { LinkComponent } from '@shared/ui/link/link.component';
/** Template: standard page body — optional back-link, a heading, optional intro,
and projected content. Rendered inside the persistent ShellComponent via the
router outlet, so it owns only the content (not the header/footer chrome). */
@Component({
selector: 'app-page-shell',
imports: [HeadingComponent, LinkComponent],
styles: [':host{display:block}'],
template: `
<div [style.max-width]="width() === 'narrow' ? '32rem' : null">
@if (backLink()) {
<p><app-link [to]="backLink()!">← {{ backLabel() }}</app-link></p>
}
<app-heading [level]="1">{{ heading() }}</app-heading>
@if (intro()) {
<p class="rhc-paragraph" style="margin-bottom:1.5rem">{{ intro() }}</p>
}
<ng-content />
</div>
`,
})
export class PageShellComponent {
heading = input.required<string>();
intro = input<string>();
backLink = input<string>();
backLabel = input('Terug naar overzicht');
width = input<'default' | 'narrow'>('default');
}

View File

@@ -0,0 +1,34 @@
import type { Meta, StoryObj } from '@storybook/angular';
import { applicationConfig, moduleMetadata } from '@storybook/angular';
import { provideRouter } from '@angular/router';
import { PageShellComponent } from './page-shell.component';
import { ButtonComponent } from '@shared/ui/button/button.component';
const meta: Meta<PageShellComponent> = {
title: 'Templates/PageShell',
component: PageShellComponent,
decorators: [
applicationConfig({ providers: [provideRouter([])] }),
moduleMetadata({ imports: [ButtonComponent] }),
],
render: (args) => ({
props: args,
template: `
<app-page-shell [heading]="heading" [intro]="intro" [backLink]="backLink" [width]="width">
<p class="rhc-paragraph">Pagina-inhoud wordt hier geprojecteerd.</p>
<app-button variant="primary">Een actie</app-button>
</app-page-shell>`,
}),
};
export default meta;
type Story = StoryObj<PageShellComponent>;
export const Default: Story = {
args: { heading: 'Mijn BIG-registratie', intro: 'Overzicht van uw registratie.' },
};
export const WithBackLink: Story = {
args: { heading: 'Mijn gegevens', backLink: '/dashboard' },
};
export const Narrow: Story = {
args: { heading: 'Inloggen', width: 'narrow', intro: 'Log in op uw omgeving.' },
};

View File

@@ -0,0 +1,25 @@
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { SiteHeaderComponent } from '@shared/layout/site-header/site-header.component';
import { SiteFooterComponent } from '@shared/layout/site-footer/site-footer.component';
/** Template: persistent app chrome. Header + footer mount once; only the routed
content inside <router-outlet> changes (and cross-fades — see styles.scss). */
@Component({
selector: 'app-shell',
imports: [RouterOutlet, SiteHeaderComponent, SiteFooterComponent],
styles: [':host{display:block}'],
template: `
<a href="#main" class="rhc-skip-link" style="position:absolute;left:-999px">Naar de inhoud</a>
<div class="utrecht-page-layout utrecht-page-layout--stretch" style="--app-content-max:64rem;min-height:100vh;align-items:stretch">
<app-site-header />
<main id="main" class="utrecht-page-content" style="flex:1;width:100%;box-sizing:border-box">
<div style="max-width:var(--app-content-max);margin:0 auto;padding:2rem 1.5rem;box-sizing:border-box">
<router-outlet />
</div>
</main>
<app-site-footer />
</div>
`,
})
export class ShellComponent {}

View File

@@ -0,0 +1,17 @@
import { Component } from '@angular/core';
/** Organism: site footer. */
@Component({
selector: 'app-site-footer',
styles: [':host{display:block}'],
template: `
<footer class="utrecht-page-footer" style="background:var(--rhc-color-lintblauw-900,#01689b);color:#fff;margin-top:3rem;width:100%">
<div style="max-width:var(--app-content-max);margin:0 auto;padding:1.5rem;display:flex;gap:2rem;flex-wrap:wrap;box-sizing:border-box">
<span>BIG-register</span>
<span>CIBG — Ministerie van Volksgezondheid, Welzijn en Sport</span>
<span style="margin-left:auto;opacity:0.85">Demo / POC — geen echte gegevens</span>
</div>
</footer>
`,
})
export class SiteFooterComponent {}

View File

@@ -0,0 +1,28 @@
import { Component, input } from '@angular/core';
import { RouterLink } from '@angular/router';
/** Organism: site header with Rijksoverheid-style wordmark + title.
ponytail: text wordmark instead of the licensed Rijksoverheid logo. */
@Component({
selector: 'app-site-header',
imports: [RouterLink],
// :host display:block so the full-bleed bar fills the flex column (custom
// elements default to display:inline, which collapsed the bar to its content).
styles: [':host{display:block}'],
template: `
<header class="utrecht-page-header" style="background:var(--rhc-color-lintblauw-700,#154273);color:#fff;width:100%">
<div style="display:flex;align-items:center;gap:1rem;max-width:var(--app-content-max);margin:0 auto;padding:1rem 1.5rem;box-sizing:border-box">
<a routerLink="/dashboard" style="display:flex;align-items:center;gap:0.75rem;color:inherit;text-decoration:none">
<span aria-hidden="true" style="display:inline-block;width:0.5rem;height:2.25rem;background:#fff"></span>
<span style="font-weight:700;line-height:1.1">
Rijksoverheid<br><span style="font-weight:400;font-size:0.9rem">BIG-register</span>
</span>
</a>
<span style="margin-left:auto;font-weight:500">{{ subtitle() }}</span>
</div>
</header>
`,
})
export class SiteHeaderComponent {
subtitle = input('Mijn omgeving');
}