feat(fp): WP-24 — letter canvas (edit on the letter)
Some checks failed
CI / frontend (push) Failing after 59s
CI / storybook-a11y (push) Successful in 4m22s
CI / backend (push) Successful in 1m18s
CI / codeql (csharp) (push) Failing after 1m47s
CI / codeql (javascript-typescript) (push) Failing after 1m20s
CI / api-client-drift (push) Successful in 1m42s
CI / e2e (push) Failing after 3h8m54s

One letter surface for every role: LetterCanvasComponent renders the
org template's letterhead/signature/footer around the case-type
sections, with editableRegions content|template|none. public/letter.css
is the FE⇄BE rendering contract (WP-25 inlines it verbatim).
letter-preview deleted — its read-only rendering absorbed into 'none'
mode. brief.machine.ts byte-identical; orgTemplate parses at the
adapter boundary and lives beside the machine in BriefStore.

Also fixes passage-picker multi-select (checkboxes all shared
id="undefined", so labels only toggled the first box) and keeps the
±page-break marks from drawing through canvas content.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-05 11:47:51 +02:00
parent 5a610c10f0
commit c07a33ee3e
19 changed files with 3270 additions and 2126 deletions

View File

@@ -10,6 +10,7 @@ import {
LetterBlockDto,
LetterSectionDto,
LibraryPassageDto,
OrgTemplateDto,
PlaceholderDefDto,
RichTextBlockDto,
RichTextNodeDto,
@@ -22,6 +23,7 @@ import {
LetterSection,
LibraryPassage,
} from '@brief/domain/brief';
import { OrgTemplate } from '@brief/domain/org-template';
import { PlaceholderDef } from '@brief/domain/placeholders';
import { Mark, Paragraph, RichTextBlock, RichTextNode } from '@shared/kernel/rich-text';
@@ -37,6 +39,7 @@ export interface BriefView {
readonly brief: Brief;
readonly availablePassages: LibraryPassage[];
readonly decisions: BriefDecisions;
readonly orgTemplate: OrgTemplate;
}
export const BRIEF_LOAD_FAILED = $localize`:@@brief.load.failed:De brief kon niet worden geladen.`;
@@ -301,19 +304,64 @@ function parseDecisions(dto: BriefDecisionsDto | undefined): Result<string, Brie
});
}
export function parseOrgTemplate(dto: OrgTemplateDto | undefined): Result<string, OrgTemplate> {
if (
typeof dto?.subOrgId !== 'string' ||
typeof dto.orgName !== 'string' ||
typeof dto.returnAddress !== 'string' ||
typeof dto.footerContact !== 'string' ||
typeof dto.footerLegal !== 'string' ||
typeof dto.signatureName !== 'string' ||
typeof dto.signatureRole !== 'string' ||
typeof dto.signatureClosing !== 'string' ||
typeof dto.version !== 'number'
) {
return err('org-template: bad shape');
}
const m = dto.margins;
if (
typeof m?.topMm !== 'number' ||
typeof m.rightMm !== 'number' ||
typeof m.bottomMm !== 'number' ||
typeof m.leftMm !== 'number'
) {
return err('org-template: bad margins');
}
return ok({
subOrgId: dto.subOrgId,
orgName: dto.orgName,
returnAddress: dto.returnAddress,
...(dto.logoDocumentId != null ? { logoDocumentId: dto.logoDocumentId } : {}),
footerContact: dto.footerContact,
footerLegal: dto.footerLegal,
signatureName: dto.signatureName,
signatureRole: dto.signatureRole,
signatureClosing: dto.signatureClosing,
margins: { topMm: m.topMm, rightMm: m.rightMm, bottomMm: m.bottomMm, leftMm: m.leftMm },
version: dto.version,
});
}
export function parseBriefView(dto: BriefViewDto): Result<string, BriefView> {
if (!dto.brief) return err('brief-view: missing brief');
const brief = parseBrief(dto.brief);
if (!brief.ok) return brief;
const decisions = parseDecisions(dto.decisions);
if (!decisions.ok) return decisions;
const orgTemplate = parseOrgTemplate(dto.orgTemplate);
if (!orgTemplate.ok) return orgTemplate;
const availablePassages: LibraryPassage[] = [];
for (const p of dto.availablePassages ?? []) {
const parsed = parsePassage(p);
if (!parsed.ok) return parsed;
availablePassages.push(parsed.value);
}
return ok({ brief: brief.value, availablePassages, decisions: decisions.value });
return ok({
brief: brief.value,
availablePassages,
decisions: decisions.value,
orgTemplate: orgTemplate.value,
});
}
// --- toDto: domain → wire, for save (collapses the union to the flat shape) ---