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,79 @@
import { Component, computed, input, output } from '@angular/core';
import { RichTextBlock } from '@shared/kernel/rich-text';
import {
RichTextEditorComponent,
PlaceholderOption,
} from '@shared/ui/rich-text-editor/rich-text-editor.component';
import { ButtonComponent } from '@shared/ui/button/button.component';
import { LetterBlock } from '@brief/domain/brief';
/** Molecule: one block in a section — its editor plus provenance + block controls.
Presentational: emits content/remove/move events; the section maps them to messages. */
@Component({
selector: 'app-letter-block',
imports: [RichTextEditorComponent, ButtonComponent],
styles: [
`
:host {
display: block;
}
.block {
border-inline-start: var(--rhc-border-width-lg) solid var(--rhc-color-border-subtle);
padding-inline-start: var(--rhc-space-max-md);
}
.meta {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--rhc-space-max-md);
margin-block-end: var(--rhc-space-max-sm);
}
.controls {
display: flex;
gap: var(--rhc-space-max-sm);
}
`,
],
template: `
<div class="block">
<div class="meta">
<span class="app-text-subtle">{{ provenance() }}</span>
@if (editable()) {
<span class="controls">
<app-button variant="subtle" (click)="moved.emit(-1)" i18n="@@brief.block.moveUp"
>Omhoog</app-button
>
<app-button variant="subtle" (click)="moved.emit(1)" i18n="@@brief.block.moveDown"
>Omlaag</app-button
>
<app-button variant="subtle" (click)="removed.emit()" i18n="@@brief.block.remove"
>Verwijderen</app-button
>
</span>
}
</div>
<app-rich-text-editor
[content]="block().content"
[placeholders]="placeholders()"
[editable]="editable()"
(contentChanged)="contentChanged.emit($event)"
/>
</div>
`,
})
export class LetterBlockComponent {
block = input.required<LetterBlock>();
placeholders = input<readonly PlaceholderOption[]>([]);
editable = input(false);
contentChanged = output<RichTextBlock>();
removed = output<void>();
moved = output<-1 | 1>();
protected provenance = computed(() => {
const b = this.block();
if (b.type === 'freeText') return $localize`:@@brief.provenance.free:Vrije tekst`;
return b.edited
? $localize`:@@brief.provenance.edited:Aangepaste standaardtekst`
: $localize`:@@brief.provenance.standard:Standaardtekst`;
});
}
@@ -0,0 +1,47 @@
import type { Meta, StoryObj } from '@storybook/angular';
import { LetterBlock } from '@brief/domain/brief';
import { LetterBlockComponent } from './letter-block.component';
const passageBlock: LetterBlock = {
type: 'passage',
blockId: 'local-1',
sourcePassageId: 'p1',
sourceVersion: 1,
edited: false,
content: { paragraphs: [{ nodes: [{ type: 'text', text: 'Geachte heer/mevrouw,' }] }] },
};
const freeTextBlock: LetterBlock = {
type: 'freeText',
blockId: 'local-2',
content: {
paragraphs: [
{
nodes: [
{ type: 'text', text: 'Wij hebben besloten om reden ' },
{ type: 'placeholder', key: 'reden_besluit' },
{ type: 'text', text: '.' },
],
},
],
},
};
const placeholders = [{ key: 'reden_besluit', label: 'Reden besluit' }];
const meta: Meta<LetterBlockComponent> = {
title: 'Domein/Brief/Letter Block',
component: LetterBlockComponent,
args: {
block: passageBlock,
placeholders,
contentChanged: () => {},
removed: () => {},
moved: () => {},
},
};
export default meta;
type Story = StoryObj<LetterBlockComponent>;
export const ReadOnlyPassage: Story = { args: { editable: false } };
export const EditableFreeText: Story = { args: { block: freeTextBlock, editable: true } };