feat(WP-67): merge behandelportal into this repo as a monorepo

Restructures into apps/ssp + apps/behandelportal (two Angular projects)
plus libs/shared + libs/beheer (cross-app libraries), replacing WP-61's
separate sibling repo. That split had already produced real drift: a
hand-vendored copy of the backend's OpenAPI doc, a shared/ui+layout tree
forked and silently diverging (7 files), and beheer + the styles.scss
token bridge duplicated byte-for-byte across both repos.

- git mv the SSP's src/app/* into apps/ssp/; fold shared/, beheer/,
  environments/, the Storybook docs/*.mdx, and styles.scss into
  libs/shared + libs/beheer (all confirmed identical between the two
  repos before merging). auth stays deliberately duplicated per
  ADR-0002 (actor-specific, expected to diverge) - amended there.
- One generated API client (libs/shared), no more vendored swagger.json.
- .dependency-cruiser split into a base factory + one config per app,
  and Storybook into .storybook-ssp/.storybook-behandelportal - both
  forced by the @auth/* alias resolving to different directories per app.
- SiteHeaderComponent/ShellComponent gained HEADER_NAV_ITEMS/
  HEADER_ADMIN_LINKS/DEBUG_PANEL injection tokens so each app supplies
  its own nav/admin-links/dev-panel instead of one being hardcoded.
- CLAUDE.md, ARCHITECTURE.md, dependencies.md, and ADR-0002 updated;
  WP-67 backlog entry documents the full decision trail.

npm run ci green (lint, dep:check x2, 360 tests across ssp/
behandelportal/shared/beheer, both localized builds, backend tests,
snippet + api-client drift); both dev servers, both Storybook
instances, and docker compose verified working.

The old sibling repo (/home/eho/repos/behandelportal) is left
untouched, not deleted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
eho
2026-08-02 21:01:57 +02:00
co-authored by Claude Sonnet 5
parent d3f3b13345
commit e7156c5132
403 changed files with 7103 additions and 60917 deletions
@@ -0,0 +1,97 @@
import { Component, input, output } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { FormFieldComponent } from '@shared/ui/form-field/form-field.component';
import { TextInputComponent } from '@shared/ui/text-input/text-input.component';
export interface AdresValue {
straat: string;
postcode: string;
woonplaats: string;
}
export type AdresErrors = Partial<Record<keyof AdresValue, string>>;
/** Organism: the editable address block (straat / postcode / woonplaats), grouped
in a fieldset/legend. Pure & presentational — values in via `value`, errors in
via `errors`, every keystroke out via `fieldChange`. No store, no services, no
internal state; the container owns the Model and decides what a change means
(registratie-wizard dispatches SetField; change-request-form sets its props).
Composes the form-field molecule (×3) so labels/error wiring stay consistent. */
@Component({
selector: 'app-address-fields',
imports: [FormsModule, FormFieldComponent, TextInputComponent],
// No scoped `fieldset` reset here: the fieldset must keep CIBG's
// `.form-horizontal fieldset` grey-box padding/margin. A local `fieldset{padding:0}`
// would tie on specificity and (injected later) win, flattening the padding.
styles: [
`
legend {
padding: 0;
font-weight: var(--rhc-text-font-weight-semi-bold);
margin-block-end: var(--rhc-space-max-md);
}
`,
],
template: `
<fieldset>
<legend>{{ legend() }}</legend>
<app-form-field
i18n-label="@@address.straat"
label="Straat en huisnummer"
[fieldId]="idPrefix() + '-straat'"
required
[error]="errors().straat"
>
<app-text-input
[inputId]="idPrefix() + '-straat'"
[invalid]="!!errors().straat"
[ngModel]="value().straat"
(ngModelChange)="fieldChange.emit({ key: 'straat', value: $event })"
name="straat"
[ngModelOptions]="{ standalone: true }"
/>
</app-form-field>
<app-form-field
i18n-label="@@address.postcode"
label="Postcode"
[fieldId]="idPrefix() + '-postcode'"
required
[error]="errors().postcode"
>
<app-text-input
[inputId]="idPrefix() + '-postcode'"
[invalid]="!!errors().postcode"
[ngModel]="value().postcode"
(ngModelChange)="fieldChange.emit({ key: 'postcode', value: $event })"
name="postcode"
i18n-placeholder="@@address.postcodePlaceholder"
placeholder="1234 AB"
[ngModelOptions]="{ standalone: true }"
/>
</app-form-field>
<app-form-field
i18n-label="@@address.woonplaats"
label="Woonplaats"
[fieldId]="idPrefix() + '-woonplaats'"
required
[error]="errors().woonplaats"
>
<app-text-input
[inputId]="idPrefix() + '-woonplaats'"
[invalid]="!!errors().woonplaats"
[ngModel]="value().woonplaats"
(ngModelChange)="fieldChange.emit({ key: 'woonplaats', value: $event })"
name="woonplaats"
[ngModelOptions]="{ standalone: true }"
/>
</app-form-field>
</fieldset>
`,
})
export class AddressFieldsComponent {
value = input.required<AdresValue>();
errors = input<AdresErrors>({});
/** Prefix for field ids/labels — keeps them unique if two blocks ever co-exist. */
idPrefix = input('adres');
legend = input($localize`:@@address.legend:Adres`);
fieldChange = output<{ key: keyof AdresValue; value: string }>();
}