style: format the repo with prettier (green format:check)

`npm run format:check` (a CI gate) had drifted red across 44 files — pre-existing
files plus recently-added ones committed without formatting. Ran `prettier --write .`;
no logic changes. Also regenerates documentation.json (compodoc reflects the reformatted
component sources).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
eho
2026-07-21 17:20:42 +02:00
co-authored by Claude Opus 4.8
parent 7dfbd4501f
commit 5761b13dd2
46 changed files with 762 additions and 543 deletions
+1 -2
View File
@@ -64,8 +64,7 @@ export const routes: Routes = [
// via the OrgAdmin gate — the guard just avoids loading a page that would 403.
canActivate: [capabilityGuard('orgtemplate:edit')],
canDeactivate: [flushPendingGuard],
loadComponent: () =>
import('@brief/ui/org-template.page').then((m) => m.OrgTemplatePage),
loadComponent: () => import('@brief/ui/org-template.page').then((m) => m.OrgTemplatePage),
},
{
path: 'beheer/stamdata',
@@ -47,11 +47,17 @@ describe('stamdata-editor reduce', () => {
it('edit messages are ignored unless loaded', () => {
expect(reduce(initial, { tag: 'RowAdded' })).toBe(initial);
expect(reduce({ tag: 'failed', reason: 'x' }, { tag: 'CellEdited', row: 0, column: 'a', value: 'b' }).tag).toBe('failed');
expect(
reduce({ tag: 'failed', reason: 'x' }, { tag: 'CellEdited', row: 0, column: 'a', value: 'b' })
.tag,
).toBe('failed');
});
it('LoadFailed and Loading transition regardless of prior state', () => {
expect(reduce(seedLoaded(), { tag: 'LoadFailed', reason: 'boom' })).toEqual({ tag: 'failed', reason: 'boom' });
expect(reduce(seedLoaded(), { tag: 'LoadFailed', reason: 'boom' })).toEqual({
tag: 'failed',
reason: 'boom',
});
expect(reduce(seedLoaded(), { tag: 'Loading' })).toEqual({ tag: 'loading' });
});
});
+14 -4
View File
@@ -28,8 +28,12 @@ describe('activeOn (valid-time, half-open [van, tot))', () => {
expect(activeOn(professions, row('a', 'A', '2000-01-01'), '1999-01-01')).toBe(false);
});
it('excludes on the geldigTot boundary (half-open)', () => {
expect(activeOn(professions, row('a', 'A', '2000-01-01', '2020-01-01'), '2020-01-01')).toBe(false);
expect(activeOn(professions, row('a', 'A', '2000-01-01', '2020-01-01'), '2019-12-31')).toBe(true);
expect(activeOn(professions, row('a', 'A', '2000-01-01', '2020-01-01'), '2020-01-01')).toBe(
false,
);
expect(activeOn(professions, row('a', 'A', '2000-01-01', '2020-01-01'), '2019-12-31')).toBe(
true,
);
});
});
@@ -55,7 +59,11 @@ describe('changeCounts (diff against the loaded snapshot, by key)', () => {
expect(changeCounts(professions, original, draft)).toEqual({ added: 1, removed: 0, edited: 0 });
});
it('counts a removed key', () => {
expect(changeCounts(professions, original, [original[0]])).toEqual({ added: 0, removed: 1, edited: 0 });
expect(changeCounts(professions, original, [original[0]])).toEqual({
added: 0,
removed: 1,
edited: 0,
});
});
it('counts an edited cell', () => {
const draft = [row('a', 'CHANGED', '2000-01-01'), original[1]];
@@ -81,6 +89,8 @@ describe('toJson (draft → file shape)', () => {
{ name: 'jaar', type: 'number', isKey: false, options: [] },
],
};
expect(JSON.parse(toJson(table, [{ code: 'x', jaar: '2020' }]))).toEqual([{ code: 'x', jaar: 2020 }]);
expect(JSON.parse(toJson(table, [{ code: 'x', jaar: '2020' }]))).toEqual([
{ code: 'x', jaar: 2020 },
]);
});
});
+7 -3
View File
@@ -116,9 +116,13 @@ export function toJson(table: StamTable, rows: readonly StamRow[]): string {
for (const c of table.columns) {
const cell = (row[c.name] ?? '').trim();
out[c.name] =
cell === '' ? (c.type === 'text' || c.type === 'enum' ? '' : null)
: c.type === 'number' ? Number(cell)
: cell;
cell === ''
? c.type === 'text' || c.type === 'enum'
? ''
: null
: c.type === 'number'
? Number(cell)
: cell;
}
return out;
});
@@ -30,7 +30,11 @@ describe('parseStamdataTable', () => {
});
it('falls back to text for an unknown column type', () => {
const r = parseStamdataTable({ ...wire, columns: [{ name: 'x', type: 'weird', isKey: true }], rows: [] });
const r = parseStamdataTable({
...wire,
columns: [{ name: 'x', type: 'weird', isKey: true }],
rows: [],
});
if (!r.ok) return;
expect(r.value.table.columns[0].type).toBe('text');
});
@@ -67,7 +67,12 @@ function parseColumn(dto: StamdataColumnDto): Result<string, StamColumn> {
return ok({ name: dto.name, type, isKey: dto.isKey === true, options: dto.options ?? [] });
}
function parseTable(dto: { id?: string; label?: string; columns?: StamdataColumnDto[]; temporal?: boolean }): Result<string, StamTable> {
function parseTable(dto: {
id?: string;
label?: string;
columns?: StamdataColumnDto[];
temporal?: boolean;
}): Result<string, StamTable> {
if (typeof dto.id !== 'string' || !Array.isArray(dto.columns))
return err('stamdata table: bad shape');
const columns: StamColumn[] = [];
@@ -1,12 +1,6 @@
import { Component, computed, input, output } from '@angular/core';
import { ButtonComponent } from '@shared/ui/button/button.component';
import {
ChangeCounts,
StamColumn,
StamRow,
StamTable,
activeOn,
} from '@beheer/domain/stamdata';
import { ChangeCounts, StamColumn, StamRow, StamTable, activeOn } from '@beheer/domain/stamdata';
interface DisplayRow {
row: StamRow;
@@ -94,7 +88,9 @@ interface DisplayRow {
/>
</div>
@if (previewing()) {
<app-button variant="subtle" (click)="previewDateChanged.emit('')">{{ showAll }}</app-button>
<app-button variant="subtle" (click)="previewDateChanged.emit('')">{{
showAll
}}</app-button>
}
}
</div>
@@ -123,7 +119,9 @@ interface DisplayRow {
[value]="item.row[col.name]"
[disabled]="previewing()"
[attr.aria-label]="cellLabel(col, item.index)"
(change)="cellEdited.emit({ row: item.index, column: col.name, value: asValue($event) })"
(change)="
cellEdited.emit({ row: item.index, column: col.name, value: asValue($event) })
"
>
<option value=""></option>
@for (opt of col.options; track opt) {
@@ -138,7 +136,9 @@ interface DisplayRow {
[class.is-invalid]="!!errors()[item.index]"
[disabled]="previewing()"
[attr.aria-label]="cellLabel(col, item.index)"
(input)="cellEdited.emit({ row: item.index, column: col.name, value: asValue($event) })"
(input)="
cellEdited.emit({ row: item.index, column: col.name, value: asValue($event) })
"
/>
}
</td>
@@ -17,7 +17,12 @@ const table: StamTable = {
const rows: StamRow[] = [
{ program: 'geneeskunde', beroep: 'Arts', geldigVan: '2000-01-01', geldigTot: '' },
{ program: 'verpleegkunde', beroep: 'Verpleegkundige', geldigVan: '2000-01-01', geldigTot: '' },
{ program: 'fysiotherapie', beroep: 'Fysiotherapeut', geldigVan: '2000-01-01', geldigTot: '2020-01-01' },
{
program: 'fysiotherapie',
beroep: 'Fysiotherapeut',
geldigVan: '2000-01-01',
geldigTot: '2020-01-01',
},
];
const meta: Meta<StamdataTableEditorComponent> = {
@@ -48,11 +53,14 @@ export const Dirty: Story = {
export const Invalid: Story = {
args: {
rows: [{ program: '', beroep: 'Arts', geldigVan: '2000-01-01', geldigTot: '' }, ...rows.slice(1)],
errors: rowErrors(
table,
[{ program: '', beroep: 'Arts', geldigVan: '2000-01-01', geldigTot: '' }, ...rows.slice(1)],
),
rows: [
{ program: '', beroep: 'Arts', geldigVan: '2000-01-01', geldigTot: '' },
...rows.slice(1),
],
errors: rowErrors(table, [
{ program: '', beroep: 'Arts', geldigVan: '2000-01-01', geldigTot: '' },
...rows.slice(1),
]),
},
};
+7 -1
View File
@@ -14,7 +14,13 @@ import { StamdataTableEditorComponent } from '@beheer/ui/stamdata-table-editor/s
*/
@Component({
selector: 'app-stamdata-page',
imports: [PageShellComponent, AlertComponent, ButtonComponent, ...ASYNC, StamdataTableEditorComponent],
imports: [
PageShellComponent,
AlertComponent,
ButtonComponent,
...ASYNC,
StamdataTableEditorComponent,
],
template: `
<app-page-shell [heading]="heading" [intro]="intro" backLink="/dashboard">
@if (!access.ready()) {
+17 -5
View File
@@ -133,7 +133,11 @@ describe('BriefStore action state (Idle | Busy | Failed)', () => {
// --- WP-27: undo/redo history + rejection diff ---
function block(id: string, text: string): LetterBlock {
return { type: 'freeText', blockId: id, content: { paragraphs: [{ nodes: [{ type: 'text', text }] }] } };
return {
type: 'freeText',
blockId: id,
content: { paragraphs: [{ nodes: [{ type: 'text', text }] }] },
};
}
const kern = (blocks: LetterBlock[]) => ({
sectionKey: 'kern',
@@ -152,7 +156,8 @@ function loadedBrief(store: BriefStore): Brief {
}
async function loadedStore(over: Partial<BriefAdapter> = {}): Promise<BriefStore> {
const ok = (v: BriefView): Promise<Result<string, BriefView>> => Promise.resolve({ ok: true, value: v });
const ok = (v: BriefView): Promise<Result<string, BriefView>> =>
Promise.resolve({ ok: true, value: v });
const store = setup({ load: () => ok(filledView), save: () => ok(filledView), ...over });
await store.load();
return store;
@@ -203,12 +208,16 @@ describe('BriefStore undo/redo history', () => {
describe('BriefStore rejection diff', () => {
it('captures the rejected letter and diffs a subsequent edit against it', async () => {
const submitted: Brief = { ...filledBrief, status: { tag: 'submitted', submittedBy: 'u', submittedAt: 't' } };
const submitted: Brief = {
...filledBrief,
status: { tag: 'submitted', submittedBy: 'u', submittedAt: 't' },
};
const rejected: Brief = {
...filledBrief,
status: { tag: 'rejected', rejectedBy: 'u2', rejectedAt: 't', comments: 'nee' },
};
const ok = (v: BriefView): Promise<Result<string, BriefView>> => Promise.resolve({ ok: true, value: v });
const ok = (v: BriefView): Promise<Result<string, BriefView>> =>
Promise.resolve({ ok: true, value: v });
const store = setup({
load: () => ok({ ...filledView, brief: submitted }),
save: () => ok(filledView),
@@ -272,7 +281,10 @@ describe('BriefStore.revealBigNummer (PRD-0002 §5c)', () => {
afterEach(() => vi.restoreAllMocks());
// Loaded with a MASKED BIG-nummer, as the server ships it by default.
const maskedView: BriefView = { ...view, caseContext: { ...caseContext, bigNummer: '********601' } };
const maskedView: BriefView = {
...view,
caseContext: { ...caseContext, bigNummer: '********601' },
};
it('swaps the masked value for the revealed one on success', async () => {
const store = setup({ load: () => Promise.resolve({ ok: true, value: maskedView }) });
@@ -88,9 +88,12 @@ export class OrgTemplateStore implements PendingSave {
readonly draftValid = computed(() => {
const d = this.draft();
if (!d) return false;
const marginsOk = [d.margins.topMm, d.margins.rightMm, d.margins.bottomMm, d.margins.leftMm].every(
(v) => v >= MARGIN_MIN_MM && v <= MARGIN_MAX_MM,
);
const marginsOk = [
d.margins.topMm,
d.margins.rightMm,
d.margins.bottomMm,
d.margins.leftMm,
].every((v) => v >= MARGIN_MIN_MM && v <= MARGIN_MAX_MM);
return d.orgName.trim().length > 0 && d.signatureName.trim().length > 0 && marginsOk;
});
@@ -107,7 +110,10 @@ export class OrgTemplateStore implements PendingSave {
if (s.tag !== 'loaded' || s.upload.categories.length > 0) return;
const status = this.categoriesRes.status();
if (status === 'resolved' || status === 'local')
this.dispatchUpload({ type: 'CategoriesLoaded', categories: this.categoriesRes.value() ?? [] });
this.dispatchUpload({
type: 'CategoriesLoaded',
categories: this.categoriesRes.value() ?? [],
});
});
// Flush a pending debounced edit before navigation/unload (see pending-saves.ts).
registerPendingSave(this);
@@ -259,8 +265,9 @@ export class OrgTemplateStore implements PendingSave {
fileName: file.name,
fileSizeMb: file.size / 1e6,
});
this.shell.upload({ localId, categoryId: cat.categoryId, wizardId: 'org-template', file }, (m) =>
this.onUploadMsg(m),
this.shell.upload(
{ localId, categoryId: cat.categoryId, wizardId: 'org-template', file },
(m) => this.onUploadMsg(m),
);
}
+25 -6
View File
@@ -2,7 +2,9 @@ import { describe, it, expect } from 'vitest';
import { Besluit, LetterBlock, LibraryPassage } from './brief';
import { inferSelection, passagesForBesluit, redenenFor } from './besluit';
const block = (t: string): LibraryPassage['content'] => ({ paragraphs: [{ nodes: [{ type: 'text', text: t }] }] });
const block = (t: string): LibraryPassage['content'] => ({
paragraphs: [{ nodes: [{ type: 'text', text: t }] }],
});
const p = (over: Partial<LibraryPassage>): LibraryPassage => ({
passageId: over.passageId ?? 'x',
@@ -18,8 +20,18 @@ const lib: LibraryPassage[] = [
p({ passageId: 'intro', besluit: undefined }), // shared, any besluit
p({ passageId: 'pos', besluit: 'positief' }),
p({ passageId: 'neg', besluit: 'negatief' }),
p({ passageId: 'neg-scholing', besluit: 'negatief', reason: 'onvoldoende_scholing', label: 'Onvoldoende scholing' }),
p({ passageId: 'neg-gegevens', besluit: 'negatief', reason: 'onjuiste_gegevens', label: 'Onjuiste gegevens' }),
p({
passageId: 'neg-scholing',
besluit: 'negatief',
reason: 'onvoldoende_scholing',
label: 'Onvoldoende scholing',
}),
p({
passageId: 'neg-gegevens',
besluit: 'negatief',
reason: 'onjuiste_gegevens',
label: 'Onjuiste gegevens',
}),
p({ passageId: 'slot-x', sectionKey: 'slot', besluit: undefined }), // not kern → never offered
];
@@ -35,17 +47,24 @@ describe('passagesForBesluit', () => {
});
it('negatief with a reden ticked includes that reason-specific passage only', () => {
const ids = passagesForBesluit(lib, 'negatief', ['onvoldoende_scholing']).map((x) => x.passageId);
const ids = passagesForBesluit(lib, 'negatief', ['onvoldoende_scholing']).map(
(x) => x.passageId,
);
expect(ids).toEqual(['intro', 'neg', 'neg-scholing']);
});
it('preserves library order (= reading order)', () => {
const ids = passagesForBesluit(lib, 'negatief', ['onjuiste_gegevens', 'onvoldoende_scholing']).map((x) => x.passageId);
const ids = passagesForBesluit(lib, 'negatief', [
'onjuiste_gegevens',
'onvoldoende_scholing',
]).map((x) => x.passageId);
expect(ids).toEqual(['intro', 'neg', 'neg-scholing', 'neg-gegevens']);
});
it('never offers non-kern passages', () => {
expect(passagesForBesluit(lib, 'positief', []).some((x) => x.sectionKey !== 'kern')).toBe(false);
expect(passagesForBesluit(lib, 'positief', []).some((x) => x.sectionKey !== 'kern')).toBe(
false,
);
});
});
+2 -1
View File
@@ -66,7 +66,8 @@ export function inferSelection(
const source = byId.get(b.sourcePassageId);
if (!source) continue;
if (source.besluit !== undefined) besluit = source.besluit;
if (source.reason !== undefined && !reasons.includes(source.reason)) reasons.push(source.reason);
if (source.reason !== undefined && !reasons.includes(source.reason))
reasons.push(source.reason);
}
return { besluit, reasons };
}
+5 -1
View File
@@ -3,7 +3,11 @@ import { Brief, LetterBlock } from './brief';
import { diffBlocks, changedBlocks } from './brief-diff';
function block(id: string, text: string): LetterBlock {
return { type: 'freeText', blockId: id, content: { paragraphs: [{ nodes: [{ type: 'text', text }] }] } };
return {
type: 'freeText',
blockId: id,
content: { paragraphs: [{ nodes: [{ type: 'text', text }] }] },
};
}
function brief(blocks: LetterBlock[]): Brief {
@@ -24,7 +24,8 @@ const view = (over: Partial<OrgTemplateAdminView> = {}): OrgTemplateAdminView =>
...over,
});
const loaded = (): OrgTemplateState => reduce({ tag: 'loading' }, { tag: 'DraftLoaded', view: view() });
const loaded = (): OrgTemplateState =>
reduce({ tag: 'loading' }, { tag: 'DraftLoaded', view: view() });
const logoCategory: DocumentCategory = {
categoryId: 'org-logo',
@@ -84,7 +85,9 @@ describe('org-template.machine', () => {
});
it('edits are no-ops in non-loaded states', () => {
expect(reduce({ tag: 'loading' }, { tag: 'FieldEdited', field: 'orgName', value: 'x' })).toEqual({
expect(
reduce({ tag: 'loading' }, { tag: 'FieldEdited', field: 'orgName', value: 'x' }),
).toEqual({
tag: 'loading',
});
});
@@ -96,7 +99,13 @@ describe('org-template.machine', () => {
});
const selected = reduce(withCat, {
tag: 'Upload',
msg: { type: 'FileSelected', categoryId: 'org-logo', localId: 'a', fileName: 'l.png', fileSizeMb: 0.1 },
msg: {
type: 'FileSelected',
categoryId: 'org-logo',
localId: 'a',
fileName: 'l.png',
fileSizeMb: 0.1,
},
});
const done = reduce(selected, {
tag: 'Upload',
+7 -5
View File
@@ -50,10 +50,7 @@ export type OrgTemplateMsg =
| { tag: 'Upload'; msg: UploadMsg };
/** Edit the loaded draft; a no-op in any non-loaded state (illegal by construction). */
function editDraft(
s: OrgTemplateState,
f: (draft: OrgTemplate) => OrgTemplate,
): OrgTemplateState {
function editDraft(s: OrgTemplateState, f: (draft: OrgTemplate) => OrgTemplate): OrgTemplateState {
return s.tag === 'loaded' ? { ...s, draft: f(s.draft), dirty: true } : s;
}
@@ -87,7 +84,12 @@ export function reduce(s: OrgTemplateState, m: OrgTemplateMsg): OrgTemplateState
const upload = reduceUpload(s.upload, m.msg);
// A completed/removed logo upload also updates the draft's logoDocumentId.
if (m.msg.type === 'UploadComplete')
return { ...s, upload, draft: { ...s.draft, logoDocumentId: m.msg.documentId }, dirty: true };
return {
...s,
upload,
draft: { ...s.draft, logoDocumentId: m.msg.documentId },
dirty: true,
};
if (m.msg.type === 'UploadRemoved') {
const { logoDocumentId: _dropped, ...rest } = s.draft;
return { ...s, upload, draft: rest, dirty: true };
@@ -62,7 +62,13 @@ const view: BriefViewDto = {
reason: 'onvoldoende_scholing',
},
],
decisions: { canEdit: false, canApprove: true, canReject: true, canSend: false, canRevealBigNummer: false },
decisions: {
canEdit: false,
canApprove: true,
canReject: true,
canSend: false,
canRevealBigNummer: false,
},
orgTemplate: {
subOrgId: 'cibg-registers',
orgName: 'CIBG — Registers',
@@ -126,8 +132,10 @@ describe('brief.adapter parse boundary', () => {
it('rejects a view whose case context is missing or malformed', () => {
expect(parseBriefView({ ...view, caseContext: undefined }).ok).toBe(false);
expect(
parseBriefView({ ...view, caseContext: { ...view.caseContext!, bigNummer: undefined as never } })
.ok,
parseBriefView({
...view,
caseContext: { ...view.caseContext!, bigNummer: undefined as never },
}).ok,
).toBe(false);
});
@@ -47,7 +47,9 @@ describe('parseOrgTemplateAdminView', () => {
it('rejects a malformed history entry', () => {
const r = parseOrgTemplateAdminView({
...view,
history: [{ version: 2, publishedAt: '2026-06-01', template: { ...draft, orgName: undefined } }],
history: [
{ version: 2, publishedAt: '2026-06-01', template: { ...draft, orgName: undefined } },
],
});
expect(r.ok).toBe(false);
});
@@ -65,10 +65,7 @@ export class OrgTemplateAdapter {
return r.ok ? parsePublish(r.value) : r;
}
async rollback(
subOrgId: string,
version: number,
): Promise<Result<string, OrgTemplateAdminView>> {
async rollback(subOrgId: string, version: number): Promise<Result<string, OrgTemplateAdminView>> {
const r = await runSubmit(() => this.client.orgTemplateRollback(subOrgId, version), FAILED);
return r.ok ? parseAdminView(r.value) : r;
}
@@ -31,7 +31,11 @@ export class RevealBigNummerAdapter {
if (!res.ok) return err(await errorMessage(res));
const body: unknown = await res.json().catch(() => null);
// Trust boundary: validate the shape before handing back a plain string.
if (typeof body === 'object' && body !== null && typeof (body as { bigNummer?: unknown }).bigNummer === 'string') {
if (
typeof body === 'object' &&
body !== null &&
typeof (body as { bigNummer?: unknown }).bigNummer === 'string'
) {
return ok((body as { bigNummer: string }).bigNummer);
}
return err(REVEAL_FAILED);
@@ -126,12 +126,9 @@ import { BesluitPanelComponent } from '@brief/ui/besluit-panel/besluit-panel.com
<div class="bar">
<app-button variant="subtle" (click)="openPreview()">{{ previewLabel() }}</app-button>
<app-button
variant="primary"
[disabled]="!canSubmit() || busy()"
(click)="submit.emit()"
>{{ submitLabel() }}</app-button
>
<app-button variant="primary" [disabled]="!canSubmit() || busy()" (click)="submit.emit()">{{
submitLabel()
}}</app-button>
@if (!canSubmit()) {
<span class="app-text-subtle">{{ submitHint() }}</span>
}
@@ -149,7 +146,9 @@ import { BesluitPanelComponent } from '@brief/ui/besluit-panel/besluit-panel.com
/>
</div>
<div class="modal-bar">
<app-button variant="secondary" (click)="preview.emit()">{{ openDocumentLabel() }}</app-button>
<app-button variant="secondary" (click)="preview.emit()">{{
openDocumentLabel()
}}</app-button>
<app-button variant="primary" (click)="closePreview()">{{ closeLabel() }}</app-button>
</div>
</dialog>
@@ -1,9 +1,17 @@
import type { Meta, StoryObj } from '@storybook/angular';
import { Brief, BriefStatus, CaseContext, LibraryPassage, allDiagnostics } from '@brief/domain/brief';
import {
Brief,
BriefStatus,
CaseContext,
LibraryPassage,
allDiagnostics,
} from '@brief/domain/brief';
import { OrgTemplate } from '@brief/domain/org-template';
import { BehandelSchermComponent } from './behandel-scherm.component';
const text = (t: string): LibraryPassage['content'] => ({ paragraphs: [{ nodes: [{ type: 'text', text: t }] }] });
const text = (t: string): LibraryPassage['content'] => ({
paragraphs: [{ nodes: [{ type: 'text', text: t }] }],
});
const orgTemplate: OrgTemplate = {
subOrgId: 'cibg-registers',
@@ -26,9 +34,34 @@ const caseContext: CaseContext = {
};
const passages: LibraryPassage[] = [
{ passageId: 'p-kern-positief', scope: 'global', sectionKey: 'kern', label: 'Toewijzing', version: 1, besluit: 'positief', content: text('Uw aanvraag is toegewezen.') },
{ passageId: 'p-kern-negatief', scope: 'global', sectionKey: 'kern', label: 'Afwijzing', version: 1, besluit: 'negatief', content: text('Uw aanvraag is afgewezen.') },
{ passageId: 'p-kern-scholing', scope: 'global', sectionKey: 'kern', label: 'Onvoldoende scholing', version: 1, besluit: 'negatief', reason: 'onvoldoende_scholing', content: text('Onvoldoende scholing.') },
{
passageId: 'p-kern-positief',
scope: 'global',
sectionKey: 'kern',
label: 'Toewijzing',
version: 1,
besluit: 'positief',
content: text('Uw aanvraag is toegewezen.'),
},
{
passageId: 'p-kern-negatief',
scope: 'global',
sectionKey: 'kern',
label: 'Afwijzing',
version: 1,
besluit: 'negatief',
content: text('Uw aanvraag is afgewezen.'),
},
{
passageId: 'p-kern-scholing',
scope: 'global',
sectionKey: 'kern',
label: 'Onvoldoende scholing',
version: 1,
besluit: 'negatief',
reason: 'onvoldoende_scholing',
content: text('Onvoldoende scholing.'),
},
];
function brief(status: BriefStatus, kernBlocks: Brief['sections'][number]['blocks'] = []): Brief {
@@ -43,9 +76,27 @@ function brief(status: BriefStatus, kernBlocks: Brief['sections'][number]['block
{ key: 'reden_besluit', label: 'Reden besluit', autoResolvable: false },
],
sections: [
{ sectionKey: 'aanhef', title: 'Aanhef', required: true, locked: true, blocks: [{ type: 'freeText', blockId: 'aanhef-1', content: text('Geachte heer/mevrouw,') }] },
{ sectionKey: 'kern', title: 'Kern van het besluit', required: true, locked: false, blocks: kernBlocks },
{ sectionKey: 'slot', title: 'Slot', required: false, locked: true, blocks: [{ type: 'freeText', blockId: 'slot-1', content: text('Met vriendelijke groet,') }] },
{
sectionKey: 'aanhef',
title: 'Aanhef',
required: true,
locked: true,
blocks: [{ type: 'freeText', blockId: 'aanhef-1', content: text('Geachte heer/mevrouw,') }],
},
{
sectionKey: 'kern',
title: 'Kern van het besluit',
required: true,
locked: false,
blocks: kernBlocks,
},
{
sectionKey: 'slot',
title: 'Slot',
required: false,
locked: true,
blocks: [{ type: 'freeText', blockId: 'slot-1', content: text('Met vriendelijke groet,') }],
},
],
};
}
@@ -66,8 +117,22 @@ export const EmptyKern: Story = {
// A besluit-sourced kern block (carries provenance), so the panel re-seeds itself from it.
const negatiefScholingKern: Brief['sections'][number]['blocks'] = [
{ type: 'passage', blockId: 'local-1', sourcePassageId: 'p-kern-negatief', sourceVersion: 1, edited: false, content: text('Uw aanvraag is afgewezen.') },
{ type: 'passage', blockId: 'local-2', sourcePassageId: 'p-kern-scholing', sourceVersion: 1, edited: false, content: text('Onvoldoende scholing.') },
{
type: 'passage',
blockId: 'local-1',
sourcePassageId: 'p-kern-negatief',
sourceVersion: 1,
edited: false,
content: text('Uw aanvraag is afgewezen.'),
},
{
type: 'passage',
blockId: 'local-2',
sourcePassageId: 'p-kern-scholing',
sourceVersion: 1,
edited: false,
content: text('Onvoldoende scholing.'),
},
];
/** Draft with a negatief besluit: the kern is filled from the selection and the besluit
@@ -96,7 +161,12 @@ export const MaskedBigNummer: Story = {
export const Rejected: Story = {
render: (args) => {
const b = brief(
{ tag: 'rejected', rejectedBy: 'demo-approver', rejectedAt: '2026-07-01', comments: 'Graag de reden concreter.' },
{
tag: 'rejected',
rejectedBy: 'demo-approver',
rejectedAt: '2026-07-01',
comments: 'Graag de reden concreter.',
},
negatiefScholingKern,
);
return { props: { ...args, brief: b, diagnostics: allDiagnostics(b), canSubmit: true } };
@@ -2,13 +2,49 @@ import type { Meta, StoryObj } from '@storybook/angular';
import { LibraryPassage } from '@brief/domain/brief';
import { BesluitPanelComponent } from './besluit-panel.component';
const text = (t: string): LibraryPassage['content'] => ({ paragraphs: [{ nodes: [{ type: 'text', text: t }] }] });
const text = (t: string): LibraryPassage['content'] => ({
paragraphs: [{ nodes: [{ type: 'text', text: t }] }],
});
const passages: LibraryPassage[] = [
{ passageId: 'p-kern-positief', scope: 'global', sectionKey: 'kern', label: 'Toewijzing', version: 1, besluit: 'positief', content: text('Toegewezen.') },
{ passageId: 'p-kern-negatief', scope: 'global', sectionKey: 'kern', label: 'Afwijzing', version: 1, besluit: 'negatief', content: text('Afgewezen.') },
{ passageId: 'p-kern-scholing', scope: 'global', sectionKey: 'kern', label: 'Onvoldoende scholing', version: 1, besluit: 'negatief', reason: 'onvoldoende_scholing', content: text('Onvoldoende scholing.') },
{ passageId: 'p-kern-gegevens', scope: 'global', sectionKey: 'kern', label: 'Onjuiste gegevens', version: 1, besluit: 'negatief', reason: 'onjuiste_gegevens', content: text('Onjuiste gegevens.') },
{
passageId: 'p-kern-positief',
scope: 'global',
sectionKey: 'kern',
label: 'Toewijzing',
version: 1,
besluit: 'positief',
content: text('Toegewezen.'),
},
{
passageId: 'p-kern-negatief',
scope: 'global',
sectionKey: 'kern',
label: 'Afwijzing',
version: 1,
besluit: 'negatief',
content: text('Afgewezen.'),
},
{
passageId: 'p-kern-scholing',
scope: 'global',
sectionKey: 'kern',
label: 'Onvoldoende scholing',
version: 1,
besluit: 'negatief',
reason: 'onvoldoende_scholing',
content: text('Onvoldoende scholing.'),
},
{
passageId: 'p-kern-gegevens',
scope: 'global',
sectionKey: 'kern',
label: 'Onjuiste gegevens',
version: 1,
besluit: 'negatief',
reason: 'onjuiste_gegevens',
content: text('Onjuiste gegevens.'),
},
];
const meta: Meta<BesluitPanelComponent> = {
@@ -178,7 +178,9 @@ const A4_HEIGHT_PX = (297 * 96) / 25.4;
(click)="zoomBy(0.1)"
>+</app-button
>
<app-button variant="subtle" (click)="zoomLevel.set(1)">{{ zoomResetLabel() }}</app-button>
<app-button variant="subtle" (click)="zoomLevel.set(1)">{{
zoomResetLabel()
}}</app-button>
</div>
@if (editableRegions() === 'none') {
<app-button
@@ -399,7 +401,10 @@ export class LetterCanvasComponent {
protected editing = computed(() => this.editableRegions() === 'template');
protected emitEdit(field: OrgTemplateTextField, event: Event) {
this.templateEdit.emit({ field, value: (event.target as HTMLInputElement | HTMLTextAreaElement).value });
this.templateEdit.emit({
field,
value: (event.target as HTMLInputElement | HTMLTextAreaElement).value,
});
}
protected marginStyle = computed(() => {
@@ -227,7 +227,9 @@ export const SAMPLE_LETTER_BRIEF: Brief = {
<ul class="history-list">
@for (v of history(); track v.version) {
<li class="history-row">
<span>{{ versionLabel() }} {{ v.version }} · {{ v.publishedAt | date: 'longDate' }}</span>
<span
>{{ versionLabel() }} {{ v.version }} · {{ v.publishedAt | date: 'longDate' }}</span
>
<app-button variant="subtle" [disabled]="busy()" (click)="rollback.emit(v.version)">
{{ rollbackLabel() }}
</app-button>
@@ -326,12 +328,15 @@ export class OrgTemplateEditorComponent {
}
}
protected impactText = computed(() =>
$localize`:@@orgTemplate.publish.impact:Dit raakt ${this.unsentBriefs()}:count: nog niet verzonden brieven. Publiceren?`,
protected impactText = computed(
() =>
$localize`:@@orgTemplate.publish.impact:Dit raakt ${this.unsentBriefs()}:count: nog niet verzonden brieven. Publiceren?`,
);
protected subOrgLabel = input($localize`:@@orgTemplate.subOrg:Organisatieonderdeel`);
protected marginsLegend = input($localize`:@@orgTemplate.margins:Marges (mm, tussen ${MARGIN_MIN_MM}:min: en ${MARGIN_MAX_MM}:max:)`);
protected marginsLegend = input(
$localize`:@@orgTemplate.margins:Marges (mm, tussen ${MARGIN_MIN_MM}:min: en ${MARGIN_MAX_MM}:max:)`,
);
protected logoHeading = input($localize`:@@orgTemplate.logo:Logo`);
protected historyHeading = input($localize`:@@orgTemplate.history:Versiegeschiedenis`);
protected noHistory = input($localize`:@@orgTemplate.history.none:Nog niets gepubliceerd.`);
+6 -2
View File
@@ -61,8 +61,12 @@ import { OrgTemplateEditorComponent } from '@brief/ui/org-template-editor/org-te
[saveText]="saveText()"
[previewUrlFor]="previewUrlFor"
(selectSubOrg)="store.selectSubOrg($event)"
(templateEdit)="store.edit({ tag: 'FieldEdited', field: $event.field, value: $event.value })"
(marginEdit)="store.edit({ tag: 'MarginEdited', edge: $event.edge, value: $event.value })"
(templateEdit)="
store.edit({ tag: 'FieldEdited', field: $event.field, value: $event.value })
"
(marginEdit)="
store.edit({ tag: 'MarginEdited', edge: $event.edge, value: $event.value })
"
(logoSelected)="store.onLogoSelected($event)"
(logoRemoved)="store.onLogoRemoved($event)"
(logoRetry)="store.onLogoRetry($event)"
+1 -6
View File
@@ -1,9 +1,4 @@
import {
DestroyRef,
ENVIRONMENT_INITIALIZER,
Injectable,
inject,
} from '@angular/core';
import { DestroyRef, ENVIRONMENT_INITIALIZER, Injectable, inject } from '@angular/core';
import { CanDeactivateFn } from '@angular/router';
/**
+1 -5
View File
@@ -3,8 +3,4 @@
* Server-resolved and opaque to the FE — never derived from a role client-side.
*/
export type Capability =
| 'brief:approve'
| 'brief:reject'
| 'brief:send'
| 'orgtemplate:edit'
| 'stamdata:edit';
'brief:approve' | 'brief:reject' | 'brief:send' | 'orgtemplate:edit' | 'stamdata:edit';
+3 -1
View File
@@ -53,7 +53,9 @@ export function deepCopyBlock(block: RichTextBlock): RichTextBlock {
passages. Placeholders contribute their key so "naam" matches a `naam_zorgverlener` chip. */
export function textOf(block: RichTextBlock): string {
return block.paragraphs
.flatMap((p) => p.nodes.map((n) => (n.type === 'text' ? n.text : n.type === 'placeholder' ? n.key : '')))
.flatMap((p) =>
p.nodes.map((n) => (n.type === 'text' ? n.text : n.type === 'placeholder' ? n.key : '')),
)
.join(' ')
.toLowerCase();
}