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:
@@ -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' });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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),
|
||||
]),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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.`);
|
||||
|
||||
@@ -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,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';
|
||||
|
||||
/**
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user