`s — the letter surface
+must stay letter.css-only. The passage picker's checkboxes now get unique
+`checkboxId`s (all previously resolved to `id="undefined"`, so labels toggled only
+the first box — multi-select was broken) and an opaque background; `.letter__body`
+stacks above the page-break marks so the dashed line never draws through content,
+while the "±pagina-einde" caption floats above everything for legibility.
## Verification
diff --git a/documentation.json b/documentation.json
index 4bf33f4..a8f9b2b 100644
--- a/documentation.json
+++ b/documentation.json
@@ -1263,12 +1263,12 @@
},
{
"name": "BriefView",
- "id": "interface-BriefView-e4c43eb0bd3adf6aa76a863f468dd620485bf6919f2114e80247f770ecb2a5e3ca8953dfb62397c149b19a9abaa2963ecba95348cc1371dd18cebc87a1bdfbe9",
+ "id": "interface-BriefView-f88a06e1726891a07b99252e904d8f5891cf2943f08448262a5f1fa0eea6524119535f800f15c73350abf85aaffbeaf2b91849d7ac4c27cef011ba4a9f4b087f",
"file": "src/app/brief/infrastructure/brief.adapter.ts",
"deprecated": false,
"deprecationMessage": "",
"type": "interface",
- "sourceCode": "import { Injectable, inject } from '@angular/core';\nimport { Result, ok, err } from '@shared/kernel/fp';\nimport { runSubmit } from '@shared/application/submit';\nimport {\n ApiClient,\n BriefDecisionsDto,\n BriefDto,\n BriefStatusDto,\n BriefViewDto,\n LetterBlockDto,\n LetterSectionDto,\n LibraryPassageDto,\n PlaceholderDefDto,\n RichTextBlockDto,\n RichTextNodeDto,\n} from '@shared/infrastructure/api-client';\nimport {\n Brief,\n BriefDecisions,\n BriefStatus,\n LetterBlock,\n LetterSection,\n LibraryPassage,\n} from '@brief/domain/brief';\nimport { PlaceholderDef } from '@brief/domain/placeholders';\nimport { Mark, Paragraph, RichTextBlock, RichTextNode } from '@shared/kernel/rich-text';\n\n/**\n * The only place brief HTTP lives (ADR-0001 anti-corruption boundary). The wire\n * uses FLAT unions (a `type`/`tag` string + nullable fields, the repo convention);\n * the `parse*` boundary narrows them into the domain's proper discriminated unions\n * and rejects malformed shapes. Mutations go through `runSubmit` (ProblemDetails →\n * error string), then parse the returned brief.\n */\n\nexport interface BriefView {\n readonly brief: Brief;\n readonly availablePassages: LibraryPassage[];\n readonly decisions: BriefDecisions;\n}\n\nexport const BRIEF_LOAD_FAILED = $localize`:@@brief.load.failed:De brief kon niet worden geladen.`;\nexport const BRIEF_ACTION_FAILED = $localize`:@@brief.action.failed:De actie is niet gelukt. Probeer het later opnieuw.`;\n\n@Injectable({ providedIn: 'root' })\nexport class BriefAdapter {\n private client = inject(ApiClient);\n\n async load(): Promise
> {\n const r = await runSubmit(() => this.client.briefGET(), BRIEF_LOAD_FAILED);\n return r.ok ? parseBriefView(r.value) : r;\n }\n\n async save(sections: readonly LetterSection[]): Promise> {\n const r = await runSubmit(\n () => this.client.briefPUT({ sections: sections.map(sectionToDto) }),\n BRIEF_ACTION_FAILED,\n );\n return r.ok ? parseBriefView(r.value) : r;\n }\n\n async submit(): Promise> {\n const r = await runSubmit(() => this.client.briefSubmit(), BRIEF_ACTION_FAILED);\n return r.ok ? parseBriefView(r.value) : r;\n }\n\n async approve(): Promise> {\n const r = await runSubmit(() => this.client.approve(), BRIEF_ACTION_FAILED);\n return r.ok ? parseBriefView(r.value) : r;\n }\n\n async reject(comments: string): Promise> {\n const r = await runSubmit(() => this.client.reject({ comments }), BRIEF_ACTION_FAILED);\n return r.ok ? parseBriefView(r.value) : r;\n }\n\n async send(): Promise> {\n const r = await runSubmit(() => this.client.send(), BRIEF_ACTION_FAILED);\n return r.ok ? parseBriefView(r.value) : r;\n }\n\n /** Demo \"start over\" — recreate a fresh brief server-side and return the new view. */\n async reset(): Promise> {\n const r = await runSubmit(() => this.client.briefReset(), BRIEF_ACTION_FAILED);\n return r.ok ? parseBriefView(r.value) : r;\n }\n}\n\n// --- parse: wire (flat) → domain (discriminated unions), validating at the boundary ---\n\nconst MARKS: readonly string[] = ['bold', 'italic', 'underline'];\n\nexport function parseNode(dto: RichTextNodeDto): Result {\n switch (dto.type) {\n case 'text': {\n if (typeof dto.text !== 'string') return err('node: text missing text');\n const marks = dto.marks?.filter((m): m is Mark => MARKS.includes(m));\n return ok(\n marks && marks.length\n ? { type: 'text', text: dto.text, marks }\n : { type: 'text', text: dto.text },\n );\n }\n case 'placeholder':\n return typeof dto.key === 'string'\n ? ok({ type: 'placeholder', key: dto.key })\n : err('node: placeholder missing key');\n case 'lineBreak':\n return ok({ type: 'lineBreak' });\n default:\n return err(`node: unknown type ${dto.type}`);\n }\n}\n\nexport function parseBlockContent(\n dto: RichTextBlockDto | undefined,\n): Result {\n if (!dto || !Array.isArray(dto.paragraphs)) return err('content: paragraphs not an array');\n const paragraphs: Paragraph[] = [];\n for (const p of dto.paragraphs) {\n const nodes: RichTextNode[] = [];\n for (const n of p.nodes ?? []) {\n const parsed = parseNode(n);\n if (!parsed.ok) return parsed;\n nodes.push(parsed.value);\n }\n const list = p.list === 'bullet' || p.list === 'number' ? p.list : undefined;\n paragraphs.push(list ? { nodes, list } : { nodes });\n }\n return ok({ paragraphs });\n}\n\nfunction parseBlock(dto: LetterBlockDto): Result {\n if (typeof dto.blockId !== 'string') return err('block: missing blockId');\n const content = parseBlockContent(dto.content);\n if (!content.ok) return content;\n switch (dto.type) {\n case 'passage':\n if (typeof dto.sourcePassageId !== 'string' || typeof dto.sourceVersion !== 'number')\n return err('block: bad passage provenance');\n return ok({\n type: 'passage',\n blockId: dto.blockId,\n sourcePassageId: dto.sourcePassageId,\n sourceVersion: dto.sourceVersion,\n content: content.value,\n edited: dto.edited ?? false,\n });\n case 'freeText':\n return ok({ type: 'freeText', blockId: dto.blockId, content: content.value });\n default:\n return err(`block: unknown type ${dto.type}`);\n }\n}\n\nfunction parseSection(dto: LetterSectionDto): Result {\n if (\n typeof dto.sectionKey !== 'string' ||\n typeof dto.title !== 'string' ||\n typeof dto.required !== 'boolean'\n ) {\n return err('section: bad shape');\n }\n const blocks: LetterBlock[] = [];\n for (const b of dto.blocks ?? []) {\n const parsed = parseBlock(b);\n if (!parsed.ok) return parsed;\n blocks.push(parsed.value);\n }\n return ok({\n sectionKey: dto.sectionKey,\n title: dto.title,\n required: dto.required,\n locked: dto.locked ?? false,\n blocks,\n });\n}\n\nfunction parsePlaceholderDef(dto: PlaceholderDefDto): Result {\n if (\n typeof dto.key !== 'string' ||\n typeof dto.label !== 'string' ||\n typeof dto.autoResolvable !== 'boolean'\n ) {\n return err('placeholder: bad shape');\n }\n return ok({\n key: dto.key,\n label: dto.label,\n autoResolvable: dto.autoResolvable,\n ...(dto.fillable != null ? { fillable: dto.fillable } : {}),\n ...(dto.deprecated != null ? { deprecated: dto.deprecated } : {}),\n });\n}\n\nexport function parseStatus(dto: BriefStatusDto | undefined): Result {\n switch (dto?.tag) {\n case 'draft':\n return ok({ tag: 'draft' });\n case 'submitted':\n if (typeof dto.submittedBy !== 'string' || typeof dto.submittedAt !== 'string')\n return err('status: bad submitted');\n return ok({ tag: 'submitted', submittedBy: dto.submittedBy, submittedAt: dto.submittedAt });\n case 'approved':\n if (typeof dto.approvedBy !== 'string' || typeof dto.approvedAt !== 'string')\n return err('status: bad approved');\n return ok({ tag: 'approved', approvedBy: dto.approvedBy, approvedAt: dto.approvedAt });\n case 'rejected':\n if (\n typeof dto.rejectedBy !== 'string' ||\n typeof dto.rejectedAt !== 'string' ||\n typeof dto.comments !== 'string'\n )\n return err('status: bad rejected');\n return ok({\n tag: 'rejected',\n rejectedBy: dto.rejectedBy,\n rejectedAt: dto.rejectedAt,\n comments: dto.comments,\n });\n case 'sent':\n if (typeof dto.sentAt !== 'string') return err('status: bad sent');\n return ok({ tag: 'sent', sentAt: dto.sentAt });\n default:\n return err(`status: unknown tag ${dto?.tag}`);\n }\n}\n\nfunction parsePassage(dto: LibraryPassageDto): Result {\n if (typeof dto.passageId !== 'string') return err('passage: bad shape');\n if (dto.scope !== 'global' && dto.scope !== 'beroep')\n return err(`passage: unknown scope ${dto.scope}`);\n if (\n typeof dto.sectionKey !== 'string' ||\n typeof dto.label !== 'string' ||\n typeof dto.version !== 'number'\n )\n return err('passage: bad shape');\n const content = parseBlockContent(dto.content);\n if (!content.ok) return content;\n return ok({\n passageId: dto.passageId,\n scope: dto.scope,\n sectionKey: dto.sectionKey,\n label: dto.label,\n content: content.value,\n version: dto.version,\n ...(dto.beroep != null ? { beroep: dto.beroep } : {}),\n });\n}\n\nexport function parseBrief(dto: BriefDto): Result {\n if (\n typeof dto.briefId !== 'string' ||\n typeof dto.drafterId !== 'string' ||\n typeof dto.beroep !== 'string' ||\n typeof dto.templateId !== 'string'\n ) {\n return err('brief: missing ids');\n }\n const status = parseStatus(dto.status);\n if (!status.ok) return status;\n\n const placeholders: PlaceholderDef[] = [];\n for (const p of dto.placeholders ?? []) {\n const parsed = parsePlaceholderDef(p);\n if (!parsed.ok) return parsed;\n placeholders.push(parsed.value);\n }\n const sections: LetterSection[] = [];\n for (const s of dto.sections ?? []) {\n const parsed = parseSection(s);\n if (!parsed.ok) return parsed;\n sections.push(parsed.value);\n }\n return ok({\n briefId: dto.briefId,\n beroep: dto.beroep,\n templateId: dto.templateId,\n placeholders,\n sections,\n status: status.value,\n drafterId: dto.drafterId,\n });\n}\n\nfunction parseDecisions(dto: BriefDecisionsDto | undefined): Result {\n if (\n typeof dto?.canEdit !== 'boolean' ||\n typeof dto.canApprove !== 'boolean' ||\n typeof dto.canReject !== 'boolean' ||\n typeof dto.canSend !== 'boolean'\n ) {\n return err('brief-view: missing/invalid decisions');\n }\n return ok({\n canEdit: dto.canEdit,\n canApprove: dto.canApprove,\n canReject: dto.canReject,\n canSend: dto.canSend,\n });\n}\n\nexport function parseBriefView(dto: BriefViewDto): Result {\n if (!dto.brief) return err('brief-view: missing brief');\n const brief = parseBrief(dto.brief);\n if (!brief.ok) return brief;\n const decisions = parseDecisions(dto.decisions);\n if (!decisions.ok) return decisions;\n const availablePassages: LibraryPassage[] = [];\n for (const p of dto.availablePassages ?? []) {\n const parsed = parsePassage(p);\n if (!parsed.ok) return parsed;\n availablePassages.push(parsed.value);\n }\n return ok({ brief: brief.value, availablePassages, decisions: decisions.value });\n}\n\n// --- toDto: domain → wire, for save (collapses the union to the flat shape) ---\n\nfunction nodeToDto(n: RichTextNode): RichTextNodeDto {\n switch (n.type) {\n case 'text':\n return { type: 'text', text: n.text, ...(n.marks ? { marks: [...n.marks] } : {}) };\n case 'placeholder':\n return { type: 'placeholder', key: n.key };\n case 'lineBreak':\n return { type: 'lineBreak' };\n }\n}\n\nfunction contentToDto(content: RichTextBlock): RichTextBlockDto {\n return {\n paragraphs: content.paragraphs.map((p) => ({\n nodes: p.nodes.map(nodeToDto),\n ...(p.list ? { list: p.list } : {}),\n })),\n };\n}\n\nfunction blockToDto(b: LetterBlock): LetterBlockDto {\n return b.type === 'passage'\n ? {\n type: 'passage',\n blockId: b.blockId,\n content: contentToDto(b.content),\n sourcePassageId: b.sourcePassageId,\n sourceVersion: b.sourceVersion,\n edited: b.edited,\n }\n : { type: 'freeText', blockId: b.blockId, content: contentToDto(b.content) };\n}\n\nfunction sectionToDto(s: LetterSection): LetterSectionDto {\n return {\n sectionKey: s.sectionKey,\n title: s.title,\n required: s.required,\n locked: s.locked,\n blocks: s.blocks.map(blockToDto),\n };\n}\n",
+ "sourceCode": "import { Injectable, inject } from '@angular/core';\nimport { Result, ok, err } from '@shared/kernel/fp';\nimport { runSubmit } from '@shared/application/submit';\nimport {\n ApiClient,\n BriefDecisionsDto,\n BriefDto,\n BriefStatusDto,\n BriefViewDto,\n LetterBlockDto,\n LetterSectionDto,\n LibraryPassageDto,\n OrgTemplateDto,\n PlaceholderDefDto,\n RichTextBlockDto,\n RichTextNodeDto,\n} from '@shared/infrastructure/api-client';\nimport {\n Brief,\n BriefDecisions,\n BriefStatus,\n LetterBlock,\n LetterSection,\n LibraryPassage,\n} from '@brief/domain/brief';\nimport { OrgTemplate } from '@brief/domain/org-template';\nimport { PlaceholderDef } from '@brief/domain/placeholders';\nimport { Mark, Paragraph, RichTextBlock, RichTextNode } from '@shared/kernel/rich-text';\n\n/**\n * The only place brief HTTP lives (ADR-0001 anti-corruption boundary). The wire\n * uses FLAT unions (a `type`/`tag` string + nullable fields, the repo convention);\n * the `parse*` boundary narrows them into the domain's proper discriminated unions\n * and rejects malformed shapes. Mutations go through `runSubmit` (ProblemDetails →\n * error string), then parse the returned brief.\n */\n\nexport interface BriefView {\n readonly brief: Brief;\n readonly availablePassages: LibraryPassage[];\n readonly decisions: BriefDecisions;\n readonly orgTemplate: OrgTemplate;\n}\n\nexport const BRIEF_LOAD_FAILED = $localize`:@@brief.load.failed:De brief kon niet worden geladen.`;\nexport const BRIEF_ACTION_FAILED = $localize`:@@brief.action.failed:De actie is niet gelukt. Probeer het later opnieuw.`;\n\n@Injectable({ providedIn: 'root' })\nexport class BriefAdapter {\n private client = inject(ApiClient);\n\n async load(): Promise> {\n const r = await runSubmit(() => this.client.briefGET(), BRIEF_LOAD_FAILED);\n return r.ok ? parseBriefView(r.value) : r;\n }\n\n async save(sections: readonly LetterSection[]): Promise> {\n const r = await runSubmit(\n () => this.client.briefPUT({ sections: sections.map(sectionToDto) }),\n BRIEF_ACTION_FAILED,\n );\n return r.ok ? parseBriefView(r.value) : r;\n }\n\n async submit(): Promise> {\n const r = await runSubmit(() => this.client.briefSubmit(), BRIEF_ACTION_FAILED);\n return r.ok ? parseBriefView(r.value) : r;\n }\n\n async approve(): Promise> {\n const r = await runSubmit(() => this.client.approve(), BRIEF_ACTION_FAILED);\n return r.ok ? parseBriefView(r.value) : r;\n }\n\n async reject(comments: string): Promise> {\n const r = await runSubmit(() => this.client.reject({ comments }), BRIEF_ACTION_FAILED);\n return r.ok ? parseBriefView(r.value) : r;\n }\n\n async send(): Promise> {\n const r = await runSubmit(() => this.client.send(), BRIEF_ACTION_FAILED);\n return r.ok ? parseBriefView(r.value) : r;\n }\n\n /** Demo \"start over\" — recreate a fresh brief server-side and return the new view. */\n async reset(): Promise> {\n const r = await runSubmit(() => this.client.briefReset(), BRIEF_ACTION_FAILED);\n return r.ok ? parseBriefView(r.value) : r;\n }\n}\n\n// --- parse: wire (flat) → domain (discriminated unions), validating at the boundary ---\n\nconst MARKS: readonly string[] = ['bold', 'italic', 'underline'];\n\nexport function parseNode(dto: RichTextNodeDto): Result {\n switch (dto.type) {\n case 'text': {\n if (typeof dto.text !== 'string') return err('node: text missing text');\n const marks = dto.marks?.filter((m): m is Mark => MARKS.includes(m));\n return ok(\n marks && marks.length\n ? { type: 'text', text: dto.text, marks }\n : { type: 'text', text: dto.text },\n );\n }\n case 'placeholder':\n return typeof dto.key === 'string'\n ? ok({ type: 'placeholder', key: dto.key })\n : err('node: placeholder missing key');\n case 'lineBreak':\n return ok({ type: 'lineBreak' });\n default:\n return err(`node: unknown type ${dto.type}`);\n }\n}\n\nexport function parseBlockContent(\n dto: RichTextBlockDto | undefined,\n): Result {\n if (!dto || !Array.isArray(dto.paragraphs)) return err('content: paragraphs not an array');\n const paragraphs: Paragraph[] = [];\n for (const p of dto.paragraphs) {\n const nodes: RichTextNode[] = [];\n for (const n of p.nodes ?? []) {\n const parsed = parseNode(n);\n if (!parsed.ok) return parsed;\n nodes.push(parsed.value);\n }\n const list = p.list === 'bullet' || p.list === 'number' ? p.list : undefined;\n paragraphs.push(list ? { nodes, list } : { nodes });\n }\n return ok({ paragraphs });\n}\n\nfunction parseBlock(dto: LetterBlockDto): Result {\n if (typeof dto.blockId !== 'string') return err('block: missing blockId');\n const content = parseBlockContent(dto.content);\n if (!content.ok) return content;\n switch (dto.type) {\n case 'passage':\n if (typeof dto.sourcePassageId !== 'string' || typeof dto.sourceVersion !== 'number')\n return err('block: bad passage provenance');\n return ok({\n type: 'passage',\n blockId: dto.blockId,\n sourcePassageId: dto.sourcePassageId,\n sourceVersion: dto.sourceVersion,\n content: content.value,\n edited: dto.edited ?? false,\n });\n case 'freeText':\n return ok({ type: 'freeText', blockId: dto.blockId, content: content.value });\n default:\n return err(`block: unknown type ${dto.type}`);\n }\n}\n\nfunction parseSection(dto: LetterSectionDto): Result {\n if (\n typeof dto.sectionKey !== 'string' ||\n typeof dto.title !== 'string' ||\n typeof dto.required !== 'boolean'\n ) {\n return err('section: bad shape');\n }\n const blocks: LetterBlock[] = [];\n for (const b of dto.blocks ?? []) {\n const parsed = parseBlock(b);\n if (!parsed.ok) return parsed;\n blocks.push(parsed.value);\n }\n return ok({\n sectionKey: dto.sectionKey,\n title: dto.title,\n required: dto.required,\n locked: dto.locked ?? false,\n blocks,\n });\n}\n\nfunction parsePlaceholderDef(dto: PlaceholderDefDto): Result {\n if (\n typeof dto.key !== 'string' ||\n typeof dto.label !== 'string' ||\n typeof dto.autoResolvable !== 'boolean'\n ) {\n return err('placeholder: bad shape');\n }\n return ok({\n key: dto.key,\n label: dto.label,\n autoResolvable: dto.autoResolvable,\n ...(dto.fillable != null ? { fillable: dto.fillable } : {}),\n ...(dto.deprecated != null ? { deprecated: dto.deprecated } : {}),\n });\n}\n\nexport function parseStatus(dto: BriefStatusDto | undefined): Result {\n switch (dto?.tag) {\n case 'draft':\n return ok({ tag: 'draft' });\n case 'submitted':\n if (typeof dto.submittedBy !== 'string' || typeof dto.submittedAt !== 'string')\n return err('status: bad submitted');\n return ok({ tag: 'submitted', submittedBy: dto.submittedBy, submittedAt: dto.submittedAt });\n case 'approved':\n if (typeof dto.approvedBy !== 'string' || typeof dto.approvedAt !== 'string')\n return err('status: bad approved');\n return ok({ tag: 'approved', approvedBy: dto.approvedBy, approvedAt: dto.approvedAt });\n case 'rejected':\n if (\n typeof dto.rejectedBy !== 'string' ||\n typeof dto.rejectedAt !== 'string' ||\n typeof dto.comments !== 'string'\n )\n return err('status: bad rejected');\n return ok({\n tag: 'rejected',\n rejectedBy: dto.rejectedBy,\n rejectedAt: dto.rejectedAt,\n comments: dto.comments,\n });\n case 'sent':\n if (typeof dto.sentAt !== 'string') return err('status: bad sent');\n return ok({ tag: 'sent', sentAt: dto.sentAt });\n default:\n return err(`status: unknown tag ${dto?.tag}`);\n }\n}\n\nfunction parsePassage(dto: LibraryPassageDto): Result {\n if (typeof dto.passageId !== 'string') return err('passage: bad shape');\n if (dto.scope !== 'global' && dto.scope !== 'beroep')\n return err(`passage: unknown scope ${dto.scope}`);\n if (\n typeof dto.sectionKey !== 'string' ||\n typeof dto.label !== 'string' ||\n typeof dto.version !== 'number'\n )\n return err('passage: bad shape');\n const content = parseBlockContent(dto.content);\n if (!content.ok) return content;\n return ok({\n passageId: dto.passageId,\n scope: dto.scope,\n sectionKey: dto.sectionKey,\n label: dto.label,\n content: content.value,\n version: dto.version,\n ...(dto.beroep != null ? { beroep: dto.beroep } : {}),\n });\n}\n\nexport function parseBrief(dto: BriefDto): Result {\n if (\n typeof dto.briefId !== 'string' ||\n typeof dto.drafterId !== 'string' ||\n typeof dto.beroep !== 'string' ||\n typeof dto.templateId !== 'string'\n ) {\n return err('brief: missing ids');\n }\n const status = parseStatus(dto.status);\n if (!status.ok) return status;\n\n const placeholders: PlaceholderDef[] = [];\n for (const p of dto.placeholders ?? []) {\n const parsed = parsePlaceholderDef(p);\n if (!parsed.ok) return parsed;\n placeholders.push(parsed.value);\n }\n const sections: LetterSection[] = [];\n for (const s of dto.sections ?? []) {\n const parsed = parseSection(s);\n if (!parsed.ok) return parsed;\n sections.push(parsed.value);\n }\n return ok({\n briefId: dto.briefId,\n beroep: dto.beroep,\n templateId: dto.templateId,\n placeholders,\n sections,\n status: status.value,\n drafterId: dto.drafterId,\n });\n}\n\nfunction parseDecisions(dto: BriefDecisionsDto | undefined): Result {\n if (\n typeof dto?.canEdit !== 'boolean' ||\n typeof dto.canApprove !== 'boolean' ||\n typeof dto.canReject !== 'boolean' ||\n typeof dto.canSend !== 'boolean'\n ) {\n return err('brief-view: missing/invalid decisions');\n }\n return ok({\n canEdit: dto.canEdit,\n canApprove: dto.canApprove,\n canReject: dto.canReject,\n canSend: dto.canSend,\n });\n}\n\nexport function parseOrgTemplate(dto: OrgTemplateDto | undefined): Result {\n if (\n typeof dto?.subOrgId !== 'string' ||\n typeof dto.orgName !== 'string' ||\n typeof dto.returnAddress !== 'string' ||\n typeof dto.footerContact !== 'string' ||\n typeof dto.footerLegal !== 'string' ||\n typeof dto.signatureName !== 'string' ||\n typeof dto.signatureRole !== 'string' ||\n typeof dto.signatureClosing !== 'string' ||\n typeof dto.version !== 'number'\n ) {\n return err('org-template: bad shape');\n }\n const m = dto.margins;\n if (\n typeof m?.topMm !== 'number' ||\n typeof m.rightMm !== 'number' ||\n typeof m.bottomMm !== 'number' ||\n typeof m.leftMm !== 'number'\n ) {\n return err('org-template: bad margins');\n }\n return ok({\n subOrgId: dto.subOrgId,\n orgName: dto.orgName,\n returnAddress: dto.returnAddress,\n ...(dto.logoDocumentId != null ? { logoDocumentId: dto.logoDocumentId } : {}),\n footerContact: dto.footerContact,\n footerLegal: dto.footerLegal,\n signatureName: dto.signatureName,\n signatureRole: dto.signatureRole,\n signatureClosing: dto.signatureClosing,\n margins: { topMm: m.topMm, rightMm: m.rightMm, bottomMm: m.bottomMm, leftMm: m.leftMm },\n version: dto.version,\n });\n}\n\nexport function parseBriefView(dto: BriefViewDto): Result {\n if (!dto.brief) return err('brief-view: missing brief');\n const brief = parseBrief(dto.brief);\n if (!brief.ok) return brief;\n const decisions = parseDecisions(dto.decisions);\n if (!decisions.ok) return decisions;\n const orgTemplate = parseOrgTemplate(dto.orgTemplate);\n if (!orgTemplate.ok) return orgTemplate;\n const availablePassages: LibraryPassage[] = [];\n for (const p of dto.availablePassages ?? []) {\n const parsed = parsePassage(p);\n if (!parsed.ok) return parsed;\n availablePassages.push(parsed.value);\n }\n return ok({\n brief: brief.value,\n availablePassages,\n decisions: decisions.value,\n orgTemplate: orgTemplate.value,\n });\n}\n\n// --- toDto: domain → wire, for save (collapses the union to the flat shape) ---\n\nfunction nodeToDto(n: RichTextNode): RichTextNodeDto {\n switch (n.type) {\n case 'text':\n return { type: 'text', text: n.text, ...(n.marks ? { marks: [...n.marks] } : {}) };\n case 'placeholder':\n return { type: 'placeholder', key: n.key };\n case 'lineBreak':\n return { type: 'lineBreak' };\n }\n}\n\nfunction contentToDto(content: RichTextBlock): RichTextBlockDto {\n return {\n paragraphs: content.paragraphs.map((p) => ({\n nodes: p.nodes.map(nodeToDto),\n ...(p.list ? { list: p.list } : {}),\n })),\n };\n}\n\nfunction blockToDto(b: LetterBlock): LetterBlockDto {\n return b.type === 'passage'\n ? {\n type: 'passage',\n blockId: b.blockId,\n content: contentToDto(b.content),\n sourcePassageId: b.sourcePassageId,\n sourceVersion: b.sourceVersion,\n edited: b.edited,\n }\n : { type: 'freeText', blockId: b.blockId, content: contentToDto(b.content) };\n}\n\nfunction sectionToDto(s: LetterSection): LetterSectionDto {\n return {\n sectionKey: s.sectionKey,\n title: s.title,\n required: s.required,\n locked: s.locked,\n blocks: s.blocks.map(blockToDto),\n };\n}\n",
"properties": [
{
"name": "availablePassages",
@@ -1278,7 +1278,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 38,
+ "line": 40,
"modifierKind": [
148
]
@@ -1291,7 +1291,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 37,
+ "line": 39,
"modifierKind": [
148
]
@@ -1304,7 +1304,20 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 39,
+ "line": 41,
+ "modifierKind": [
+ 148
+ ]
+ },
+ {
+ "name": "orgTemplate",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "OrgTemplate",
+ "indexKey": "",
+ "optional": false,
+ "description": "",
+ "line": 42,
"modifierKind": [
148
]
@@ -2092,7 +2105,54 @@
},
{
"name": "Draft",
- "id": "interface-Draft-51c29a3fca3c5bd3eba53c9bc57714c79817ae8c86b1ed9c1cd76652d31b45ee7c635d35f7eb0a3a7e5bf9ea7c5da5c066cee3908c8fe4b9fe053586f834b133",
+ "id": "interface-Draft-3f75a0ff51d12ccab50bfa13789cace778d5acff9f4feaa1607cdf1acb4a5ab4fcc687512424cb2252ec07568f6aea07a0b4f04469cbeed733c28654443ffd9e",
+ "file": "src/app/herregistratie/domain/herregistratie.machine.ts",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "interface",
+ "sourceCode": "import { Result, assertNever } from '@shared/kernel/fp';\nimport { Uren, parseUren } from '@registratie/domain/value-objects/uren';\nimport {\n UploadState,\n UploadMsg,\n initialUpload,\n reduceUpload,\n requiredCategoriesSatisfied,\n deliveryRefs,\n} from '@shared/upload/upload.machine';\n\n/** What the user is typing (raw, possibly invalid). */\nexport interface Draft {\n uren: string;\n jaren: string;\n punten: string;\n}\n\nexport type StepErrors = Partial>;\n\n/** What we have AFTER parsing — branded/typed, guaranteed valid. */\nexport interface Valid {\n uren: Uren;\n jaren: number;\n punten: number;\n documents: Array<{ categoryId: string; channel: 'digital' | 'post'; documentId?: string }>;\n}\n\n/**\n * The whole wizard as one tagged union. `step` and `errors` exist ONLY while\n * Editing; Submitting/Submitted/Failed carry a `Valid` payload and nothing else.\n * So \"submitting while a field is invalid\" or \"showing the success screen with\n * errors set\" are unrepresentable — the bug class is gone by construction.\n */\nexport type WizardState =\n | { tag: 'Editing'; step: 1 | 2 | 3; draft: Draft; errors: StepErrors; upload: UploadState }\n | { tag: 'Submitting'; data: Valid }\n | { tag: 'Submitted'; data: Valid }\n | { tag: 'Failed'; data: Valid; error: string };\n\nexport const initial: WizardState = {\n tag: 'Editing',\n step: 1,\n draft: { uren: '', jaren: '', punten: '' },\n errors: {},\n upload: initialUpload,\n};\n\n/** Has the user meaningfully started, so it's worth persisting as a Concept? */\nexport function hasProgress(s: Extract): boolean {\n return (\n s.step > 1 ||\n !!s.draft.uren ||\n !!s.draft.jaren ||\n !!s.draft.punten ||\n deliveryRefs(s.upload).some((r) => r.channel === 'digital' && !!r.documentId)\n );\n}\n\n/** Parse every field; on success hand back a Valid, else the per-field errors. */\nfunction validate(draft: Draft, upload: UploadState): Result {\n const uren = parseUren(draft.uren);\n const jaren = parseUren(draft.jaren);\n const punten = parseUren(draft.punten);\n const errors: StepErrors = {};\n if (!uren.ok) errors.uren = uren.error;\n if (!jaren.ok) errors.jaren = jaren.error;\n if (!punten.ok) errors.punten = punten.error;\n if (!requiredCategoriesSatisfied(upload)) {\n errors.documenten = $localize`:@@validation.documenten:Lever de verplichte documenten aan (upload of kies \"per post nasturen\").`;\n }\n if (uren.ok && jaren.ok && punten.ok && !errors.documenten) {\n return {\n ok: true,\n value: {\n uren: uren.value,\n jaren: jaren.value,\n punten: punten.value,\n documents: deliveryRefs(upload),\n },\n };\n }\n return { ok: false, error: errors };\n}\n\n/** Advance one step, gating on that step's fields. Illegal elsewhere = no-op. */\nexport function next(s: WizardState): WizardState {\n if (s.tag !== 'Editing') return s;\n const errors: StepErrors = {};\n if (s.step === 1) {\n const uren = parseUren(s.draft.uren);\n const jaren = parseUren(s.draft.jaren);\n if (!uren.ok) errors.uren = uren.error;\n if (!jaren.ok) errors.jaren = jaren.error;\n return Object.keys(errors).length === 0 ? { ...s, step: 2, errors: {} } : { ...s, errors };\n }\n if (s.step === 2) {\n const punten = parseUren(s.draft.punten);\n if (!punten.ok) errors.punten = punten.error;\n return punten.ok ? { ...s, step: 3, errors: {} } : { ...s, errors };\n }\n return s;\n}\n\nexport function back(s: WizardState): WizardState {\n if (s.tag !== 'Editing' || s.step === 1) return s;\n return { ...s, step: (s.step - 1) as 1 | 2, errors: {} };\n}\n\n/** Jump back to an earlier step to correct data (controle → step N). Forward\n jumps are not allowed (would skip validation). */\nexport function gaNaarStap(s: WizardState, step: 1 | 2 | 3): WizardState {\n if (s.tag !== 'Editing' || step >= s.step) return s;\n return { ...s, step, errors: {} };\n}\n\n/** Step 3 submit: parse everything + require documents; Submitting only with Valid. */\nexport function submit(s: WizardState): WizardState {\n if (s.tag !== 'Editing' || s.step !== 3) return s;\n const result = validate(s.draft, s.upload);\n return result.ok ? { tag: 'Submitting', data: result.value } : { ...s, errors: result.error };\n}\n\n/** Route an upload sub-message through the pure upload reducer (Editing only). */\nexport function upload(s: WizardState, msg: UploadMsg): WizardState {\n if (s.tag !== 'Editing') return s;\n return { ...s, upload: reduceUpload(s.upload, msg) };\n}\n\n/** Resolve the async submit. Only meaningful while Submitting. */\nexport function resolve(s: WizardState, r: Result): WizardState {\n if (s.tag !== 'Submitting') return s;\n return r.ok\n ? { tag: 'Submitted', data: s.data }\n : { tag: 'Failed', data: s.data, error: r.error };\n}\n\n/** Update one draft field while editing; ignored in any other state. */\nexport function setField(s: WizardState, key: keyof Draft, value: string): WizardState {\n if (s.tag !== 'Editing') return s;\n return { ...s, draft: { ...s.draft, [key]: value } };\n}\n\n/**\n * Every event that can happen to the wizard, as one message type. The component\n * sends a WizardMsg; `reduce` decides the next state. This is the Elm\n * Model+Msg+update pattern: ONE pure function describes all state changes.\n */\nexport type WizardMsg =\n | { tag: 'SetField'; key: keyof Draft; value: string }\n | { tag: 'Next' }\n | { tag: 'Back' }\n | { tag: 'GaNaarStap'; step: 1 | 2 | 3 }\n | { tag: 'Submit' }\n | { tag: 'Retry' }\n | { tag: 'SubmitConfirmed' }\n | { tag: 'SubmitFailed'; error: string }\n | { tag: 'Upload'; msg: UploadMsg }\n | { tag: 'Seed'; state: WizardState }; // mount a specific state (stories/showcase)\n\nexport function reduce(s: WizardState, m: WizardMsg): WizardState {\n switch (m.tag) {\n case 'SetField':\n return setField(s, m.key, m.value);\n case 'Next':\n return next(s);\n case 'Back':\n return back(s);\n case 'GaNaarStap':\n return gaNaarStap(s, m.step);\n case 'Submit':\n return submit(s);\n case 'Retry':\n return s.tag === 'Failed' ? { tag: 'Submitting', data: s.data } : s;\n case 'SubmitConfirmed':\n return s.tag === 'Submitting' ? { tag: 'Submitted', data: s.data } : s;\n case 'SubmitFailed':\n return s.tag === 'Submitting' ? { tag: 'Failed', data: s.data, error: m.error } : s;\n case 'Upload':\n return upload(s, m.msg);\n case 'Seed':\n return m.state;\n default:\n return assertNever(m);\n }\n}\n",
+ "properties": [
+ {
+ "name": "jaren",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "string",
+ "indexKey": "",
+ "optional": false,
+ "description": "",
+ "line": 15
+ },
+ {
+ "name": "punten",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "string",
+ "indexKey": "",
+ "optional": false,
+ "description": "",
+ "line": 16
+ },
+ {
+ "name": "uren",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "string",
+ "indexKey": "",
+ "optional": false,
+ "description": "",
+ "line": 14
+ }
+ ],
+ "indexSignatures": [],
+ "kind": 172,
+ "description": "What the user is typing (raw, possibly invalid).
\n",
+ "rawdescription": "\nWhat the user is typing (raw, possibly invalid).",
+ "methods": [],
+ "extends": []
+ },
+ {
+ "name": "Draft",
+ "id": "interface-Draft-51c29a3fca3c5bd3eba53c9bc57714c79817ae8c86b1ed9c1cd76652d31b45ee7c635d35f7eb0a3a7e5bf9ea7c5da5c066cee3908c8fe4b9fe053586f834b133-1",
"file": "src/app/registratie/domain/change-request.machine.ts",
"deprecated": false,
"deprecationMessage": "",
@@ -2135,11 +2195,14 @@
"description": "What the user is typing (raw, possibly invalid).
\n",
"rawdescription": "\nWhat the user is typing (raw, possibly invalid).",
"methods": [],
- "extends": []
+ "extends": [],
+ "isDuplicate": true,
+ "duplicateId": 1,
+ "duplicateName": "Draft-1"
},
{
"name": "Draft",
- "id": "interface-Draft-9669bb22f8d4cc591a3bd64d9fc83275b70ad9e7275d59b4d1d394db7252e8820908b20c593f0b3a16c415c40ad1232ed7d04ba684d0dee9d72bc1ae820d06b6-1",
+ "id": "interface-Draft-9669bb22f8d4cc591a3bd64d9fc83275b70ad9e7275d59b4d1d394db7252e8820908b20c593f0b3a16c415c40ad1232ed7d04ba684d0dee9d72bc1ae820d06b6-2",
"file": "src/app/registratie/domain/registratie-wizard.machine.ts",
"deprecated": false,
"deprecationMessage": "",
@@ -2264,56 +2327,6 @@
"methods": [],
"extends": [],
"isDuplicate": true,
- "duplicateId": 1,
- "duplicateName": "Draft-1"
- },
- {
- "name": "Draft",
- "id": "interface-Draft-3f75a0ff51d12ccab50bfa13789cace778d5acff9f4feaa1607cdf1acb4a5ab4fcc687512424cb2252ec07568f6aea07a0b4f04469cbeed733c28654443ffd9e-2",
- "file": "src/app/herregistratie/domain/herregistratie.machine.ts",
- "deprecated": false,
- "deprecationMessage": "",
- "type": "interface",
- "sourceCode": "import { Result, assertNever } from '@shared/kernel/fp';\nimport { Uren, parseUren } from '@registratie/domain/value-objects/uren';\nimport {\n UploadState,\n UploadMsg,\n initialUpload,\n reduceUpload,\n requiredCategoriesSatisfied,\n deliveryRefs,\n} from '@shared/upload/upload.machine';\n\n/** What the user is typing (raw, possibly invalid). */\nexport interface Draft {\n uren: string;\n jaren: string;\n punten: string;\n}\n\nexport type StepErrors = Partial>;\n\n/** What we have AFTER parsing — branded/typed, guaranteed valid. */\nexport interface Valid {\n uren: Uren;\n jaren: number;\n punten: number;\n documents: Array<{ categoryId: string; channel: 'digital' | 'post'; documentId?: string }>;\n}\n\n/**\n * The whole wizard as one tagged union. `step` and `errors` exist ONLY while\n * Editing; Submitting/Submitted/Failed carry a `Valid` payload and nothing else.\n * So \"submitting while a field is invalid\" or \"showing the success screen with\n * errors set\" are unrepresentable — the bug class is gone by construction.\n */\nexport type WizardState =\n | { tag: 'Editing'; step: 1 | 2 | 3; draft: Draft; errors: StepErrors; upload: UploadState }\n | { tag: 'Submitting'; data: Valid }\n | { tag: 'Submitted'; data: Valid }\n | { tag: 'Failed'; data: Valid; error: string };\n\nexport const initial: WizardState = {\n tag: 'Editing',\n step: 1,\n draft: { uren: '', jaren: '', punten: '' },\n errors: {},\n upload: initialUpload,\n};\n\n/** Has the user meaningfully started, so it's worth persisting as a Concept? */\nexport function hasProgress(s: Extract): boolean {\n return (\n s.step > 1 ||\n !!s.draft.uren ||\n !!s.draft.jaren ||\n !!s.draft.punten ||\n deliveryRefs(s.upload).some((r) => r.channel === 'digital' && !!r.documentId)\n );\n}\n\n/** Parse every field; on success hand back a Valid, else the per-field errors. */\nfunction validate(draft: Draft, upload: UploadState): Result {\n const uren = parseUren(draft.uren);\n const jaren = parseUren(draft.jaren);\n const punten = parseUren(draft.punten);\n const errors: StepErrors = {};\n if (!uren.ok) errors.uren = uren.error;\n if (!jaren.ok) errors.jaren = jaren.error;\n if (!punten.ok) errors.punten = punten.error;\n if (!requiredCategoriesSatisfied(upload)) {\n errors.documenten = $localize`:@@validation.documenten:Lever de verplichte documenten aan (upload of kies \"per post nasturen\").`;\n }\n if (uren.ok && jaren.ok && punten.ok && !errors.documenten) {\n return {\n ok: true,\n value: {\n uren: uren.value,\n jaren: jaren.value,\n punten: punten.value,\n documents: deliveryRefs(upload),\n },\n };\n }\n return { ok: false, error: errors };\n}\n\n/** Advance one step, gating on that step's fields. Illegal elsewhere = no-op. */\nexport function next(s: WizardState): WizardState {\n if (s.tag !== 'Editing') return s;\n const errors: StepErrors = {};\n if (s.step === 1) {\n const uren = parseUren(s.draft.uren);\n const jaren = parseUren(s.draft.jaren);\n if (!uren.ok) errors.uren = uren.error;\n if (!jaren.ok) errors.jaren = jaren.error;\n return Object.keys(errors).length === 0 ? { ...s, step: 2, errors: {} } : { ...s, errors };\n }\n if (s.step === 2) {\n const punten = parseUren(s.draft.punten);\n if (!punten.ok) errors.punten = punten.error;\n return punten.ok ? { ...s, step: 3, errors: {} } : { ...s, errors };\n }\n return s;\n}\n\nexport function back(s: WizardState): WizardState {\n if (s.tag !== 'Editing' || s.step === 1) return s;\n return { ...s, step: (s.step - 1) as 1 | 2, errors: {} };\n}\n\n/** Jump back to an earlier step to correct data (controle → step N). Forward\n jumps are not allowed (would skip validation). */\nexport function gaNaarStap(s: WizardState, step: 1 | 2 | 3): WizardState {\n if (s.tag !== 'Editing' || step >= s.step) return s;\n return { ...s, step, errors: {} };\n}\n\n/** Step 3 submit: parse everything + require documents; Submitting only with Valid. */\nexport function submit(s: WizardState): WizardState {\n if (s.tag !== 'Editing' || s.step !== 3) return s;\n const result = validate(s.draft, s.upload);\n return result.ok ? { tag: 'Submitting', data: result.value } : { ...s, errors: result.error };\n}\n\n/** Route an upload sub-message through the pure upload reducer (Editing only). */\nexport function upload(s: WizardState, msg: UploadMsg): WizardState {\n if (s.tag !== 'Editing') return s;\n return { ...s, upload: reduceUpload(s.upload, msg) };\n}\n\n/** Resolve the async submit. Only meaningful while Submitting. */\nexport function resolve(s: WizardState, r: Result): WizardState {\n if (s.tag !== 'Submitting') return s;\n return r.ok\n ? { tag: 'Submitted', data: s.data }\n : { tag: 'Failed', data: s.data, error: r.error };\n}\n\n/** Update one draft field while editing; ignored in any other state. */\nexport function setField(s: WizardState, key: keyof Draft, value: string): WizardState {\n if (s.tag !== 'Editing') return s;\n return { ...s, draft: { ...s.draft, [key]: value } };\n}\n\n/**\n * Every event that can happen to the wizard, as one message type. The component\n * sends a WizardMsg; `reduce` decides the next state. This is the Elm\n * Model+Msg+update pattern: ONE pure function describes all state changes.\n */\nexport type WizardMsg =\n | { tag: 'SetField'; key: keyof Draft; value: string }\n | { tag: 'Next' }\n | { tag: 'Back' }\n | { tag: 'GaNaarStap'; step: 1 | 2 | 3 }\n | { tag: 'Submit' }\n | { tag: 'Retry' }\n | { tag: 'SubmitConfirmed' }\n | { tag: 'SubmitFailed'; error: string }\n | { tag: 'Upload'; msg: UploadMsg }\n | { tag: 'Seed'; state: WizardState }; // mount a specific state (stories/showcase)\n\nexport function reduce(s: WizardState, m: WizardMsg): WizardState {\n switch (m.tag) {\n case 'SetField':\n return setField(s, m.key, m.value);\n case 'Next':\n return next(s);\n case 'Back':\n return back(s);\n case 'GaNaarStap':\n return gaNaarStap(s, m.step);\n case 'Submit':\n return submit(s);\n case 'Retry':\n return s.tag === 'Failed' ? { tag: 'Submitting', data: s.data } : s;\n case 'SubmitConfirmed':\n return s.tag === 'Submitting' ? { tag: 'Submitted', data: s.data } : s;\n case 'SubmitFailed':\n return s.tag === 'Submitting' ? { tag: 'Failed', data: s.data, error: m.error } : s;\n case 'Upload':\n return upload(s, m.msg);\n case 'Seed':\n return m.state;\n default:\n return assertNever(m);\n }\n}\n",
- "properties": [
- {
- "name": "jaren",
- "deprecated": false,
- "deprecationMessage": "",
- "type": "string",
- "indexKey": "",
- "optional": false,
- "description": "",
- "line": 15
- },
- {
- "name": "punten",
- "deprecated": false,
- "deprecationMessage": "",
- "type": "string",
- "indexKey": "",
- "optional": false,
- "description": "",
- "line": 16
- },
- {
- "name": "uren",
- "deprecated": false,
- "deprecationMessage": "",
- "type": "string",
- "indexKey": "",
- "optional": false,
- "description": "",
- "line": 14
- }
- ],
- "indexSignatures": [],
- "kind": 172,
- "description": "What the user is typing (raw, possibly invalid).
\n",
- "rawdescription": "\nWhat the user is typing (raw, possibly invalid).",
- "methods": [],
- "extends": [],
- "isDuplicate": true,
"duplicateId": 2,
"duplicateName": "Draft-2"
},
@@ -3571,6 +3584,75 @@
"duplicateId": 1,
"duplicateName": "ManualDiplomaPolicyDto-1"
},
+ {
+ "name": "Margins",
+ "id": "interface-Margins-e82c95e71a7bea05025c07216feb15b3d3fb7cfb6bbf1bce9e449b8a511c7cf987d29bd59655cf1cc21895701aa60877fa0e5f10f6bcaca79671843d228c1e3d",
+ "file": "src/app/brief/domain/org-template.ts",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "interface",
+ "sourceCode": "export interface Margins {\n readonly topMm: number;\n readonly rightMm: number;\n readonly bottomMm: number;\n readonly leftMm: number;\n}\n\nexport interface OrgTemplate {\n readonly subOrgId: string;\n readonly orgName: string;\n /** Multiline; rendered above the envelope window. */\n readonly returnAddress: string;\n readonly logoDocumentId?: string;\n /** Multiline contact block in the footer. */\n readonly footerContact: string;\n readonly footerLegal: string;\n readonly signatureName: string;\n readonly signatureRole: string;\n readonly signatureClosing: string;\n readonly margins: Margins;\n /** 0 = draft; n>0 = the published snapshot this letter renders with. */\n readonly version: number;\n}\n",
+ "properties": [
+ {
+ "name": "bottomMm",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "number",
+ "indexKey": "",
+ "optional": false,
+ "description": "",
+ "line": 12,
+ "modifierKind": [
+ 148
+ ]
+ },
+ {
+ "name": "leftMm",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "number",
+ "indexKey": "",
+ "optional": false,
+ "description": "",
+ "line": 13,
+ "modifierKind": [
+ 148
+ ]
+ },
+ {
+ "name": "rightMm",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "number",
+ "indexKey": "",
+ "optional": false,
+ "description": "",
+ "line": 11,
+ "modifierKind": [
+ 148
+ ]
+ },
+ {
+ "name": "topMm",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "number",
+ "indexKey": "",
+ "optional": false,
+ "description": "",
+ "line": 10,
+ "modifierKind": [
+ 148
+ ]
+ }
+ ],
+ "indexSignatures": [],
+ "kind": 172,
+ "description": "The organization template (Brief v2 PRD §3, WP-23/24): the SECOND template axis —\nappearance/identity per sub-organization (letterhead, footer, signature, margins).\nOrthogonal to the case-type template (sections + placeholders); the two only meet\nat render time, on the letter canvas. Server-owned: the FE renders it verbatim,\nnever edits it here (the admin editor is WP-26).
\n",
+ "rawdescription": "\n\nThe organization template (Brief v2 PRD §3, WP-23/24): the SECOND template axis —\nappearance/identity per sub-organization (letterhead, footer, signature, margins).\nOrthogonal to the case-type template (sections + placeholders); the two only meet\nat render time, on the letter canvas. Server-owned: the FE renders it verbatim,\nnever edits it here (the admin editor is WP-26).\n",
+ "methods": [],
+ "extends": []
+ },
{
"name": "MarginsDto",
"id": "interface-MarginsDto-6a5ea57f33619caac97c0bf864e0e725aae634df76c8709ab43bd09fab437342598ef2f1475c8705f4fa3aef49aafa9ddc63f365d890c5b4bf3547aa65e28986",
@@ -3651,6 +3733,167 @@
"methods": [],
"extends": []
},
+ {
+ "name": "OrgTemplate",
+ "id": "interface-OrgTemplate-e82c95e71a7bea05025c07216feb15b3d3fb7cfb6bbf1bce9e449b8a511c7cf987d29bd59655cf1cc21895701aa60877fa0e5f10f6bcaca79671843d228c1e3d",
+ "file": "src/app/brief/domain/org-template.ts",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "interface",
+ "sourceCode": "export interface Margins {\n readonly topMm: number;\n readonly rightMm: number;\n readonly bottomMm: number;\n readonly leftMm: number;\n}\n\nexport interface OrgTemplate {\n readonly subOrgId: string;\n readonly orgName: string;\n /** Multiline; rendered above the envelope window. */\n readonly returnAddress: string;\n readonly logoDocumentId?: string;\n /** Multiline contact block in the footer. */\n readonly footerContact: string;\n readonly footerLegal: string;\n readonly signatureName: string;\n readonly signatureRole: string;\n readonly signatureClosing: string;\n readonly margins: Margins;\n /** 0 = draft; n>0 = the published snapshot this letter renders with. */\n readonly version: number;\n}\n",
+ "properties": [
+ {
+ "name": "footerContact",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "string",
+ "indexKey": "",
+ "optional": false,
+ "description": "Multiline contact block in the footer.
\n",
+ "line": 23,
+ "rawdescription": "\nMultiline contact block in the footer.",
+ "modifierKind": [
+ 148
+ ]
+ },
+ {
+ "name": "footerLegal",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "string",
+ "indexKey": "",
+ "optional": false,
+ "description": "",
+ "line": 24,
+ "modifierKind": [
+ 148
+ ]
+ },
+ {
+ "name": "logoDocumentId",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "string",
+ "indexKey": "",
+ "optional": true,
+ "description": "",
+ "line": 21,
+ "modifierKind": [
+ 148
+ ]
+ },
+ {
+ "name": "margins",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "Margins",
+ "indexKey": "",
+ "optional": false,
+ "description": "",
+ "line": 28,
+ "modifierKind": [
+ 148
+ ]
+ },
+ {
+ "name": "orgName",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "string",
+ "indexKey": "",
+ "optional": false,
+ "description": "",
+ "line": 18,
+ "modifierKind": [
+ 148
+ ]
+ },
+ {
+ "name": "returnAddress",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "string",
+ "indexKey": "",
+ "optional": false,
+ "description": "Multiline; rendered above the envelope window.
\n",
+ "line": 20,
+ "rawdescription": "\nMultiline; rendered above the envelope window.",
+ "modifierKind": [
+ 148
+ ]
+ },
+ {
+ "name": "signatureClosing",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "string",
+ "indexKey": "",
+ "optional": false,
+ "description": "",
+ "line": 27,
+ "modifierKind": [
+ 148
+ ]
+ },
+ {
+ "name": "signatureName",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "string",
+ "indexKey": "",
+ "optional": false,
+ "description": "",
+ "line": 25,
+ "modifierKind": [
+ 148
+ ]
+ },
+ {
+ "name": "signatureRole",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "string",
+ "indexKey": "",
+ "optional": false,
+ "description": "",
+ "line": 26,
+ "modifierKind": [
+ 148
+ ]
+ },
+ {
+ "name": "subOrgId",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "string",
+ "indexKey": "",
+ "optional": false,
+ "description": "",
+ "line": 17,
+ "modifierKind": [
+ 148
+ ]
+ },
+ {
+ "name": "version",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "number",
+ "indexKey": "",
+ "optional": false,
+ "description": "0 = draft; n>0 = the published snapshot this letter renders with.
\n",
+ "line": 30,
+ "rawdescription": "\n0 = draft; n>0 = the published snapshot this letter renders with.",
+ "modifierKind": [
+ 148
+ ]
+ }
+ ],
+ "indexSignatures": [],
+ "kind": 172,
+ "methods": [],
+ "extends": []
+ },
{
"name": "OrgTemplateAdminViewDto",
"id": "interface-OrgTemplateAdminViewDto-6a5ea57f33619caac97c0bf864e0e725aae634df76c8709ab43bd09fab437342598ef2f1475c8705f4fa3aef49aafa9ddc63f365d890c5b4bf3547aa65e28986",
@@ -5928,54 +6171,7 @@
},
{
"name": "Valid",
- "id": "interface-Valid-51c29a3fca3c5bd3eba53c9bc57714c79817ae8c86b1ed9c1cd76652d31b45ee7c635d35f7eb0a3a7e5bf9ea7c5da5c066cee3908c8fe4b9fe053586f834b133",
- "file": "src/app/registratie/domain/change-request.machine.ts",
- "deprecated": false,
- "deprecationMessage": "",
- "type": "interface",
- "sourceCode": "import { Result, assertNever } from '@shared/kernel/fp';\nimport { Postcode, parsePostcode } from '@registratie/domain/value-objects/postcode';\n\n/** What the user is typing (raw, possibly invalid). */\nexport interface Draft {\n straat: string;\n postcode: string;\n woonplaats: string;\n}\n\n/** After parsing — postcode is the branded type, so downstream can't get a raw one. */\nexport interface Valid {\n straat: string;\n postcode: Postcode;\n woonplaats: string;\n}\n\nexport type Errors = Partial>;\n\n/**\n * The change-request (adreswijziging) form as one tagged union — the SAME idiom\n * as the wizards, just single-step. `draft`/`errors` exist only while Editing;\n * Submitting/Submitted/Failed carry the parsed `Valid`. Illegal states (submitting\n * an invalid draft, a success screen with errors) are unrepresentable.\n */\nexport type ChangeRequestState =\n | { tag: 'Editing'; draft: Draft; errors: Errors }\n | { tag: 'Submitting'; data: Valid }\n | { tag: 'Submitted'; data: Valid; referentie: string }\n | { tag: 'Failed'; data: Valid; error: string };\n\nexport const initial: ChangeRequestState = {\n tag: 'Editing',\n draft: { straat: '', postcode: '', woonplaats: '' },\n errors: {},\n};\n\n/** Parse via the value objects; on success hand back a Valid, else per-field errors. */\nfunction validate(draft: Draft): Result {\n const straat = draft.straat.trim();\n const postcode = parsePostcode(draft.postcode);\n const errors: Errors = {};\n if (!straat) errors.straat = $localize`:@@validation.straat:Vul straat en huisnummer in.`;\n if (!postcode.ok) errors.postcode = postcode.error;\n if (straat && postcode.ok) {\n return {\n ok: true,\n value: { straat, postcode: postcode.value, woonplaats: draft.woonplaats.trim() },\n };\n }\n return { ok: false, error: errors };\n}\n\nexport type ChangeRequestMsg =\n | { tag: 'SetField'; key: keyof Draft; value: string }\n | { tag: 'Submit' }\n | { tag: 'Retry' }\n | { tag: 'SubmitConfirmed'; referentie: string }\n | { tag: 'SubmitFailed'; error: string }\n | { tag: 'Reset' }\n | { tag: 'Seed'; state: ChangeRequestState }; // mount a specific state (stories/tests)\n\nexport function reduce(s: ChangeRequestState, m: ChangeRequestMsg): ChangeRequestState {\n switch (m.tag) {\n case 'SetField':\n return s.tag === 'Editing' ? { ...s, draft: { ...s.draft, [m.key]: m.value } } : s;\n case 'Submit': {\n if (s.tag !== 'Editing') return s;\n const r = validate(s.draft);\n return r.ok ? { tag: 'Submitting', data: r.value } : { ...s, errors: r.error };\n }\n case 'Retry':\n return s.tag === 'Failed' ? { tag: 'Submitting', data: s.data } : s;\n case 'SubmitConfirmed':\n return s.tag === 'Submitting'\n ? { tag: 'Submitted', data: s.data, referentie: m.referentie }\n : s;\n case 'SubmitFailed':\n return s.tag === 'Submitting' ? { tag: 'Failed', data: s.data, error: m.error } : s;\n case 'Reset':\n return initial;\n case 'Seed':\n return m.state;\n default:\n return assertNever(m);\n }\n}\n",
- "properties": [
- {
- "name": "postcode",
- "deprecated": false,
- "deprecationMessage": "",
- "type": "Postcode",
- "indexKey": "",
- "optional": false,
- "description": "",
- "line": 14
- },
- {
- "name": "straat",
- "deprecated": false,
- "deprecationMessage": "",
- "type": "string",
- "indexKey": "",
- "optional": false,
- "description": "",
- "line": 13
- },
- {
- "name": "woonplaats",
- "deprecated": false,
- "deprecationMessage": "",
- "type": "string",
- "indexKey": "",
- "optional": false,
- "description": "",
- "line": 15
- }
- ],
- "indexSignatures": [],
- "kind": 172,
- "description": "After parsing — postcode is the branded type, so downstream can't get a raw one.
\n",
- "rawdescription": "\nAfter parsing — postcode is the branded type, so downstream can't get a raw one.",
- "methods": [],
- "extends": []
- },
- {
- "name": "Valid",
- "id": "interface-Valid-3f75a0ff51d12ccab50bfa13789cace778d5acff9f4feaa1607cdf1acb4a5ab4fcc687512424cb2252ec07568f6aea07a0b4f04469cbeed733c28654443ffd9e-1",
+ "id": "interface-Valid-3f75a0ff51d12ccab50bfa13789cace778d5acff9f4feaa1607cdf1acb4a5ab4fcc687512424cb2252ec07568f6aea07a0b4f04469cbeed733c28654443ffd9e",
"file": "src/app/herregistratie/domain/herregistratie.machine.ts",
"deprecated": false,
"deprecationMessage": "",
@@ -6028,6 +6224,53 @@
"description": "What we have AFTER parsing — branded/typed, guaranteed valid.
\n",
"rawdescription": "\nWhat we have AFTER parsing — branded/typed, guaranteed valid.",
"methods": [],
+ "extends": []
+ },
+ {
+ "name": "Valid",
+ "id": "interface-Valid-51c29a3fca3c5bd3eba53c9bc57714c79817ae8c86b1ed9c1cd76652d31b45ee7c635d35f7eb0a3a7e5bf9ea7c5da5c066cee3908c8fe4b9fe053586f834b133-1",
+ "file": "src/app/registratie/domain/change-request.machine.ts",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "interface",
+ "sourceCode": "import { Result, assertNever } from '@shared/kernel/fp';\nimport { Postcode, parsePostcode } from '@registratie/domain/value-objects/postcode';\n\n/** What the user is typing (raw, possibly invalid). */\nexport interface Draft {\n straat: string;\n postcode: string;\n woonplaats: string;\n}\n\n/** After parsing — postcode is the branded type, so downstream can't get a raw one. */\nexport interface Valid {\n straat: string;\n postcode: Postcode;\n woonplaats: string;\n}\n\nexport type Errors = Partial>;\n\n/**\n * The change-request (adreswijziging) form as one tagged union — the SAME idiom\n * as the wizards, just single-step. `draft`/`errors` exist only while Editing;\n * Submitting/Submitted/Failed carry the parsed `Valid`. Illegal states (submitting\n * an invalid draft, a success screen with errors) are unrepresentable.\n */\nexport type ChangeRequestState =\n | { tag: 'Editing'; draft: Draft; errors: Errors }\n | { tag: 'Submitting'; data: Valid }\n | { tag: 'Submitted'; data: Valid; referentie: string }\n | { tag: 'Failed'; data: Valid; error: string };\n\nexport const initial: ChangeRequestState = {\n tag: 'Editing',\n draft: { straat: '', postcode: '', woonplaats: '' },\n errors: {},\n};\n\n/** Parse via the value objects; on success hand back a Valid, else per-field errors. */\nfunction validate(draft: Draft): Result {\n const straat = draft.straat.trim();\n const postcode = parsePostcode(draft.postcode);\n const errors: Errors = {};\n if (!straat) errors.straat = $localize`:@@validation.straat:Vul straat en huisnummer in.`;\n if (!postcode.ok) errors.postcode = postcode.error;\n if (straat && postcode.ok) {\n return {\n ok: true,\n value: { straat, postcode: postcode.value, woonplaats: draft.woonplaats.trim() },\n };\n }\n return { ok: false, error: errors };\n}\n\nexport type ChangeRequestMsg =\n | { tag: 'SetField'; key: keyof Draft; value: string }\n | { tag: 'Submit' }\n | { tag: 'Retry' }\n | { tag: 'SubmitConfirmed'; referentie: string }\n | { tag: 'SubmitFailed'; error: string }\n | { tag: 'Reset' }\n | { tag: 'Seed'; state: ChangeRequestState }; // mount a specific state (stories/tests)\n\nexport function reduce(s: ChangeRequestState, m: ChangeRequestMsg): ChangeRequestState {\n switch (m.tag) {\n case 'SetField':\n return s.tag === 'Editing' ? { ...s, draft: { ...s.draft, [m.key]: m.value } } : s;\n case 'Submit': {\n if (s.tag !== 'Editing') return s;\n const r = validate(s.draft);\n return r.ok ? { tag: 'Submitting', data: r.value } : { ...s, errors: r.error };\n }\n case 'Retry':\n return s.tag === 'Failed' ? { tag: 'Submitting', data: s.data } : s;\n case 'SubmitConfirmed':\n return s.tag === 'Submitting'\n ? { tag: 'Submitted', data: s.data, referentie: m.referentie }\n : s;\n case 'SubmitFailed':\n return s.tag === 'Submitting' ? { tag: 'Failed', data: s.data, error: m.error } : s;\n case 'Reset':\n return initial;\n case 'Seed':\n return m.state;\n default:\n return assertNever(m);\n }\n}\n",
+ "properties": [
+ {
+ "name": "postcode",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "Postcode",
+ "indexKey": "",
+ "optional": false,
+ "description": "",
+ "line": 14
+ },
+ {
+ "name": "straat",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "string",
+ "indexKey": "",
+ "optional": false,
+ "description": "",
+ "line": 13
+ },
+ {
+ "name": "woonplaats",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "string",
+ "indexKey": "",
+ "optional": false,
+ "description": "",
+ "line": 15
+ }
+ ],
+ "indexSignatures": [],
+ "kind": 172,
+ "description": "After parsing — postcode is the branded type, so downstream can't get a raw one.
\n",
+ "rawdescription": "\nAfter parsing — postcode is the branded type, so downstream can't get a raw one.",
+ "methods": [],
"extends": [],
"isDuplicate": true,
"duplicateId": 1,
@@ -7073,7 +7316,7 @@
},
{
"name": "BriefAdapter",
- "id": "injectable-BriefAdapter-e4c43eb0bd3adf6aa76a863f468dd620485bf6919f2114e80247f770ecb2a5e3ca8953dfb62397c149b19a9abaa2963ecba95348cc1371dd18cebc87a1bdfbe9",
+ "id": "injectable-BriefAdapter-f88a06e1726891a07b99252e904d8f5891cf2943f08448262a5f1fa0eea6524119535f800f15c73350abf85aaffbeaf2b91849d7ac4c27cef011ba4a9f4b087f",
"file": "src/app/brief/infrastructure/brief.adapter.ts",
"properties": [
{
@@ -7085,7 +7328,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 47,
+ "line": 50,
"modifierKind": [
123
]
@@ -7098,7 +7341,7 @@
"optional": false,
"returnType": "Promise>",
"typeParameters": [],
- "line": 67,
+ "line": 70,
"deprecated": false,
"deprecationMessage": "",
"modifierKind": [
@@ -7111,7 +7354,7 @@
"optional": false,
"returnType": "Promise>",
"typeParameters": [],
- "line": 49,
+ "line": 52,
"deprecated": false,
"deprecationMessage": "",
"modifierKind": [
@@ -7133,7 +7376,7 @@
"optional": false,
"returnType": "Promise>",
"typeParameters": [],
- "line": 72,
+ "line": 75,
"deprecated": false,
"deprecationMessage": "",
"modifierKind": [
@@ -7159,7 +7402,7 @@
"optional": false,
"returnType": "Promise>",
"typeParameters": [],
- "line": 83,
+ "line": 86,
"deprecated": false,
"deprecationMessage": "",
"rawdescription": "\nDemo \"start over\" — recreate a fresh brief server-side and return the new view.",
@@ -7183,7 +7426,7 @@
"optional": false,
"returnType": "Promise>",
"typeParameters": [],
- "line": 54,
+ "line": 57,
"deprecated": false,
"deprecationMessage": "",
"modifierKind": [
@@ -7209,7 +7452,7 @@
"optional": false,
"returnType": "Promise>",
"typeParameters": [],
- "line": 77,
+ "line": 80,
"deprecated": false,
"deprecationMessage": "",
"modifierKind": [
@@ -7222,7 +7465,7 @@
"optional": false,
"returnType": "Promise>",
"typeParameters": [],
- "line": 62,
+ "line": 65,
"deprecated": false,
"deprecationMessage": "",
"modifierKind": [
@@ -7234,13 +7477,13 @@
"deprecationMessage": "",
"description": "",
"rawdescription": "\n",
- "sourceCode": "import { Injectable, inject } from '@angular/core';\nimport { Result, ok, err } from '@shared/kernel/fp';\nimport { runSubmit } from '@shared/application/submit';\nimport {\n ApiClient,\n BriefDecisionsDto,\n BriefDto,\n BriefStatusDto,\n BriefViewDto,\n LetterBlockDto,\n LetterSectionDto,\n LibraryPassageDto,\n PlaceholderDefDto,\n RichTextBlockDto,\n RichTextNodeDto,\n} from '@shared/infrastructure/api-client';\nimport {\n Brief,\n BriefDecisions,\n BriefStatus,\n LetterBlock,\n LetterSection,\n LibraryPassage,\n} from '@brief/domain/brief';\nimport { PlaceholderDef } from '@brief/domain/placeholders';\nimport { Mark, Paragraph, RichTextBlock, RichTextNode } from '@shared/kernel/rich-text';\n\n/**\n * The only place brief HTTP lives (ADR-0001 anti-corruption boundary). The wire\n * uses FLAT unions (a `type`/`tag` string + nullable fields, the repo convention);\n * the `parse*` boundary narrows them into the domain's proper discriminated unions\n * and rejects malformed shapes. Mutations go through `runSubmit` (ProblemDetails →\n * error string), then parse the returned brief.\n */\n\nexport interface BriefView {\n readonly brief: Brief;\n readonly availablePassages: LibraryPassage[];\n readonly decisions: BriefDecisions;\n}\n\nexport const BRIEF_LOAD_FAILED = $localize`:@@brief.load.failed:De brief kon niet worden geladen.`;\nexport const BRIEF_ACTION_FAILED = $localize`:@@brief.action.failed:De actie is niet gelukt. Probeer het later opnieuw.`;\n\n@Injectable({ providedIn: 'root' })\nexport class BriefAdapter {\n private client = inject(ApiClient);\n\n async load(): Promise> {\n const r = await runSubmit(() => this.client.briefGET(), BRIEF_LOAD_FAILED);\n return r.ok ? parseBriefView(r.value) : r;\n }\n\n async save(sections: readonly LetterSection[]): Promise> {\n const r = await runSubmit(\n () => this.client.briefPUT({ sections: sections.map(sectionToDto) }),\n BRIEF_ACTION_FAILED,\n );\n return r.ok ? parseBriefView(r.value) : r;\n }\n\n async submit(): Promise> {\n const r = await runSubmit(() => this.client.briefSubmit(), BRIEF_ACTION_FAILED);\n return r.ok ? parseBriefView(r.value) : r;\n }\n\n async approve(): Promise> {\n const r = await runSubmit(() => this.client.approve(), BRIEF_ACTION_FAILED);\n return r.ok ? parseBriefView(r.value) : r;\n }\n\n async reject(comments: string): Promise> {\n const r = await runSubmit(() => this.client.reject({ comments }), BRIEF_ACTION_FAILED);\n return r.ok ? parseBriefView(r.value) : r;\n }\n\n async send(): Promise> {\n const r = await runSubmit(() => this.client.send(), BRIEF_ACTION_FAILED);\n return r.ok ? parseBriefView(r.value) : r;\n }\n\n /** Demo \"start over\" — recreate a fresh brief server-side and return the new view. */\n async reset(): Promise> {\n const r = await runSubmit(() => this.client.briefReset(), BRIEF_ACTION_FAILED);\n return r.ok ? parseBriefView(r.value) : r;\n }\n}\n\n// --- parse: wire (flat) → domain (discriminated unions), validating at the boundary ---\n\nconst MARKS: readonly string[] = ['bold', 'italic', 'underline'];\n\nexport function parseNode(dto: RichTextNodeDto): Result {\n switch (dto.type) {\n case 'text': {\n if (typeof dto.text !== 'string') return err('node: text missing text');\n const marks = dto.marks?.filter((m): m is Mark => MARKS.includes(m));\n return ok(\n marks && marks.length\n ? { type: 'text', text: dto.text, marks }\n : { type: 'text', text: dto.text },\n );\n }\n case 'placeholder':\n return typeof dto.key === 'string'\n ? ok({ type: 'placeholder', key: dto.key })\n : err('node: placeholder missing key');\n case 'lineBreak':\n return ok({ type: 'lineBreak' });\n default:\n return err(`node: unknown type ${dto.type}`);\n }\n}\n\nexport function parseBlockContent(\n dto: RichTextBlockDto | undefined,\n): Result {\n if (!dto || !Array.isArray(dto.paragraphs)) return err('content: paragraphs not an array');\n const paragraphs: Paragraph[] = [];\n for (const p of dto.paragraphs) {\n const nodes: RichTextNode[] = [];\n for (const n of p.nodes ?? []) {\n const parsed = parseNode(n);\n if (!parsed.ok) return parsed;\n nodes.push(parsed.value);\n }\n const list = p.list === 'bullet' || p.list === 'number' ? p.list : undefined;\n paragraphs.push(list ? { nodes, list } : { nodes });\n }\n return ok({ paragraphs });\n}\n\nfunction parseBlock(dto: LetterBlockDto): Result {\n if (typeof dto.blockId !== 'string') return err('block: missing blockId');\n const content = parseBlockContent(dto.content);\n if (!content.ok) return content;\n switch (dto.type) {\n case 'passage':\n if (typeof dto.sourcePassageId !== 'string' || typeof dto.sourceVersion !== 'number')\n return err('block: bad passage provenance');\n return ok({\n type: 'passage',\n blockId: dto.blockId,\n sourcePassageId: dto.sourcePassageId,\n sourceVersion: dto.sourceVersion,\n content: content.value,\n edited: dto.edited ?? false,\n });\n case 'freeText':\n return ok({ type: 'freeText', blockId: dto.blockId, content: content.value });\n default:\n return err(`block: unknown type ${dto.type}`);\n }\n}\n\nfunction parseSection(dto: LetterSectionDto): Result {\n if (\n typeof dto.sectionKey !== 'string' ||\n typeof dto.title !== 'string' ||\n typeof dto.required !== 'boolean'\n ) {\n return err('section: bad shape');\n }\n const blocks: LetterBlock[] = [];\n for (const b of dto.blocks ?? []) {\n const parsed = parseBlock(b);\n if (!parsed.ok) return parsed;\n blocks.push(parsed.value);\n }\n return ok({\n sectionKey: dto.sectionKey,\n title: dto.title,\n required: dto.required,\n locked: dto.locked ?? false,\n blocks,\n });\n}\n\nfunction parsePlaceholderDef(dto: PlaceholderDefDto): Result {\n if (\n typeof dto.key !== 'string' ||\n typeof dto.label !== 'string' ||\n typeof dto.autoResolvable !== 'boolean'\n ) {\n return err('placeholder: bad shape');\n }\n return ok({\n key: dto.key,\n label: dto.label,\n autoResolvable: dto.autoResolvable,\n ...(dto.fillable != null ? { fillable: dto.fillable } : {}),\n ...(dto.deprecated != null ? { deprecated: dto.deprecated } : {}),\n });\n}\n\nexport function parseStatus(dto: BriefStatusDto | undefined): Result {\n switch (dto?.tag) {\n case 'draft':\n return ok({ tag: 'draft' });\n case 'submitted':\n if (typeof dto.submittedBy !== 'string' || typeof dto.submittedAt !== 'string')\n return err('status: bad submitted');\n return ok({ tag: 'submitted', submittedBy: dto.submittedBy, submittedAt: dto.submittedAt });\n case 'approved':\n if (typeof dto.approvedBy !== 'string' || typeof dto.approvedAt !== 'string')\n return err('status: bad approved');\n return ok({ tag: 'approved', approvedBy: dto.approvedBy, approvedAt: dto.approvedAt });\n case 'rejected':\n if (\n typeof dto.rejectedBy !== 'string' ||\n typeof dto.rejectedAt !== 'string' ||\n typeof dto.comments !== 'string'\n )\n return err('status: bad rejected');\n return ok({\n tag: 'rejected',\n rejectedBy: dto.rejectedBy,\n rejectedAt: dto.rejectedAt,\n comments: dto.comments,\n });\n case 'sent':\n if (typeof dto.sentAt !== 'string') return err('status: bad sent');\n return ok({ tag: 'sent', sentAt: dto.sentAt });\n default:\n return err(`status: unknown tag ${dto?.tag}`);\n }\n}\n\nfunction parsePassage(dto: LibraryPassageDto): Result {\n if (typeof dto.passageId !== 'string') return err('passage: bad shape');\n if (dto.scope !== 'global' && dto.scope !== 'beroep')\n return err(`passage: unknown scope ${dto.scope}`);\n if (\n typeof dto.sectionKey !== 'string' ||\n typeof dto.label !== 'string' ||\n typeof dto.version !== 'number'\n )\n return err('passage: bad shape');\n const content = parseBlockContent(dto.content);\n if (!content.ok) return content;\n return ok({\n passageId: dto.passageId,\n scope: dto.scope,\n sectionKey: dto.sectionKey,\n label: dto.label,\n content: content.value,\n version: dto.version,\n ...(dto.beroep != null ? { beroep: dto.beroep } : {}),\n });\n}\n\nexport function parseBrief(dto: BriefDto): Result {\n if (\n typeof dto.briefId !== 'string' ||\n typeof dto.drafterId !== 'string' ||\n typeof dto.beroep !== 'string' ||\n typeof dto.templateId !== 'string'\n ) {\n return err('brief: missing ids');\n }\n const status = parseStatus(dto.status);\n if (!status.ok) return status;\n\n const placeholders: PlaceholderDef[] = [];\n for (const p of dto.placeholders ?? []) {\n const parsed = parsePlaceholderDef(p);\n if (!parsed.ok) return parsed;\n placeholders.push(parsed.value);\n }\n const sections: LetterSection[] = [];\n for (const s of dto.sections ?? []) {\n const parsed = parseSection(s);\n if (!parsed.ok) return parsed;\n sections.push(parsed.value);\n }\n return ok({\n briefId: dto.briefId,\n beroep: dto.beroep,\n templateId: dto.templateId,\n placeholders,\n sections,\n status: status.value,\n drafterId: dto.drafterId,\n });\n}\n\nfunction parseDecisions(dto: BriefDecisionsDto | undefined): Result {\n if (\n typeof dto?.canEdit !== 'boolean' ||\n typeof dto.canApprove !== 'boolean' ||\n typeof dto.canReject !== 'boolean' ||\n typeof dto.canSend !== 'boolean'\n ) {\n return err('brief-view: missing/invalid decisions');\n }\n return ok({\n canEdit: dto.canEdit,\n canApprove: dto.canApprove,\n canReject: dto.canReject,\n canSend: dto.canSend,\n });\n}\n\nexport function parseBriefView(dto: BriefViewDto): Result {\n if (!dto.brief) return err('brief-view: missing brief');\n const brief = parseBrief(dto.brief);\n if (!brief.ok) return brief;\n const decisions = parseDecisions(dto.decisions);\n if (!decisions.ok) return decisions;\n const availablePassages: LibraryPassage[] = [];\n for (const p of dto.availablePassages ?? []) {\n const parsed = parsePassage(p);\n if (!parsed.ok) return parsed;\n availablePassages.push(parsed.value);\n }\n return ok({ brief: brief.value, availablePassages, decisions: decisions.value });\n}\n\n// --- toDto: domain → wire, for save (collapses the union to the flat shape) ---\n\nfunction nodeToDto(n: RichTextNode): RichTextNodeDto {\n switch (n.type) {\n case 'text':\n return { type: 'text', text: n.text, ...(n.marks ? { marks: [...n.marks] } : {}) };\n case 'placeholder':\n return { type: 'placeholder', key: n.key };\n case 'lineBreak':\n return { type: 'lineBreak' };\n }\n}\n\nfunction contentToDto(content: RichTextBlock): RichTextBlockDto {\n return {\n paragraphs: content.paragraphs.map((p) => ({\n nodes: p.nodes.map(nodeToDto),\n ...(p.list ? { list: p.list } : {}),\n })),\n };\n}\n\nfunction blockToDto(b: LetterBlock): LetterBlockDto {\n return b.type === 'passage'\n ? {\n type: 'passage',\n blockId: b.blockId,\n content: contentToDto(b.content),\n sourcePassageId: b.sourcePassageId,\n sourceVersion: b.sourceVersion,\n edited: b.edited,\n }\n : { type: 'freeText', blockId: b.blockId, content: contentToDto(b.content) };\n}\n\nfunction sectionToDto(s: LetterSection): LetterSectionDto {\n return {\n sectionKey: s.sectionKey,\n title: s.title,\n required: s.required,\n locked: s.locked,\n blocks: s.blocks.map(blockToDto),\n };\n}\n",
+ "sourceCode": "import { Injectable, inject } from '@angular/core';\nimport { Result, ok, err } from '@shared/kernel/fp';\nimport { runSubmit } from '@shared/application/submit';\nimport {\n ApiClient,\n BriefDecisionsDto,\n BriefDto,\n BriefStatusDto,\n BriefViewDto,\n LetterBlockDto,\n LetterSectionDto,\n LibraryPassageDto,\n OrgTemplateDto,\n PlaceholderDefDto,\n RichTextBlockDto,\n RichTextNodeDto,\n} from '@shared/infrastructure/api-client';\nimport {\n Brief,\n BriefDecisions,\n BriefStatus,\n LetterBlock,\n LetterSection,\n LibraryPassage,\n} from '@brief/domain/brief';\nimport { OrgTemplate } from '@brief/domain/org-template';\nimport { PlaceholderDef } from '@brief/domain/placeholders';\nimport { Mark, Paragraph, RichTextBlock, RichTextNode } from '@shared/kernel/rich-text';\n\n/**\n * The only place brief HTTP lives (ADR-0001 anti-corruption boundary). The wire\n * uses FLAT unions (a `type`/`tag` string + nullable fields, the repo convention);\n * the `parse*` boundary narrows them into the domain's proper discriminated unions\n * and rejects malformed shapes. Mutations go through `runSubmit` (ProblemDetails →\n * error string), then parse the returned brief.\n */\n\nexport interface BriefView {\n readonly brief: Brief;\n readonly availablePassages: LibraryPassage[];\n readonly decisions: BriefDecisions;\n readonly orgTemplate: OrgTemplate;\n}\n\nexport const BRIEF_LOAD_FAILED = $localize`:@@brief.load.failed:De brief kon niet worden geladen.`;\nexport const BRIEF_ACTION_FAILED = $localize`:@@brief.action.failed:De actie is niet gelukt. Probeer het later opnieuw.`;\n\n@Injectable({ providedIn: 'root' })\nexport class BriefAdapter {\n private client = inject(ApiClient);\n\n async load(): Promise> {\n const r = await runSubmit(() => this.client.briefGET(), BRIEF_LOAD_FAILED);\n return r.ok ? parseBriefView(r.value) : r;\n }\n\n async save(sections: readonly LetterSection[]): Promise> {\n const r = await runSubmit(\n () => this.client.briefPUT({ sections: sections.map(sectionToDto) }),\n BRIEF_ACTION_FAILED,\n );\n return r.ok ? parseBriefView(r.value) : r;\n }\n\n async submit(): Promise> {\n const r = await runSubmit(() => this.client.briefSubmit(), BRIEF_ACTION_FAILED);\n return r.ok ? parseBriefView(r.value) : r;\n }\n\n async approve(): Promise> {\n const r = await runSubmit(() => this.client.approve(), BRIEF_ACTION_FAILED);\n return r.ok ? parseBriefView(r.value) : r;\n }\n\n async reject(comments: string): Promise> {\n const r = await runSubmit(() => this.client.reject({ comments }), BRIEF_ACTION_FAILED);\n return r.ok ? parseBriefView(r.value) : r;\n }\n\n async send(): Promise> {\n const r = await runSubmit(() => this.client.send(), BRIEF_ACTION_FAILED);\n return r.ok ? parseBriefView(r.value) : r;\n }\n\n /** Demo \"start over\" — recreate a fresh brief server-side and return the new view. */\n async reset(): Promise> {\n const r = await runSubmit(() => this.client.briefReset(), BRIEF_ACTION_FAILED);\n return r.ok ? parseBriefView(r.value) : r;\n }\n}\n\n// --- parse: wire (flat) → domain (discriminated unions), validating at the boundary ---\n\nconst MARKS: readonly string[] = ['bold', 'italic', 'underline'];\n\nexport function parseNode(dto: RichTextNodeDto): Result {\n switch (dto.type) {\n case 'text': {\n if (typeof dto.text !== 'string') return err('node: text missing text');\n const marks = dto.marks?.filter((m): m is Mark => MARKS.includes(m));\n return ok(\n marks && marks.length\n ? { type: 'text', text: dto.text, marks }\n : { type: 'text', text: dto.text },\n );\n }\n case 'placeholder':\n return typeof dto.key === 'string'\n ? ok({ type: 'placeholder', key: dto.key })\n : err('node: placeholder missing key');\n case 'lineBreak':\n return ok({ type: 'lineBreak' });\n default:\n return err(`node: unknown type ${dto.type}`);\n }\n}\n\nexport function parseBlockContent(\n dto: RichTextBlockDto | undefined,\n): Result {\n if (!dto || !Array.isArray(dto.paragraphs)) return err('content: paragraphs not an array');\n const paragraphs: Paragraph[] = [];\n for (const p of dto.paragraphs) {\n const nodes: RichTextNode[] = [];\n for (const n of p.nodes ?? []) {\n const parsed = parseNode(n);\n if (!parsed.ok) return parsed;\n nodes.push(parsed.value);\n }\n const list = p.list === 'bullet' || p.list === 'number' ? p.list : undefined;\n paragraphs.push(list ? { nodes, list } : { nodes });\n }\n return ok({ paragraphs });\n}\n\nfunction parseBlock(dto: LetterBlockDto): Result {\n if (typeof dto.blockId !== 'string') return err('block: missing blockId');\n const content = parseBlockContent(dto.content);\n if (!content.ok) return content;\n switch (dto.type) {\n case 'passage':\n if (typeof dto.sourcePassageId !== 'string' || typeof dto.sourceVersion !== 'number')\n return err('block: bad passage provenance');\n return ok({\n type: 'passage',\n blockId: dto.blockId,\n sourcePassageId: dto.sourcePassageId,\n sourceVersion: dto.sourceVersion,\n content: content.value,\n edited: dto.edited ?? false,\n });\n case 'freeText':\n return ok({ type: 'freeText', blockId: dto.blockId, content: content.value });\n default:\n return err(`block: unknown type ${dto.type}`);\n }\n}\n\nfunction parseSection(dto: LetterSectionDto): Result {\n if (\n typeof dto.sectionKey !== 'string' ||\n typeof dto.title !== 'string' ||\n typeof dto.required !== 'boolean'\n ) {\n return err('section: bad shape');\n }\n const blocks: LetterBlock[] = [];\n for (const b of dto.blocks ?? []) {\n const parsed = parseBlock(b);\n if (!parsed.ok) return parsed;\n blocks.push(parsed.value);\n }\n return ok({\n sectionKey: dto.sectionKey,\n title: dto.title,\n required: dto.required,\n locked: dto.locked ?? false,\n blocks,\n });\n}\n\nfunction parsePlaceholderDef(dto: PlaceholderDefDto): Result {\n if (\n typeof dto.key !== 'string' ||\n typeof dto.label !== 'string' ||\n typeof dto.autoResolvable !== 'boolean'\n ) {\n return err('placeholder: bad shape');\n }\n return ok({\n key: dto.key,\n label: dto.label,\n autoResolvable: dto.autoResolvable,\n ...(dto.fillable != null ? { fillable: dto.fillable } : {}),\n ...(dto.deprecated != null ? { deprecated: dto.deprecated } : {}),\n });\n}\n\nexport function parseStatus(dto: BriefStatusDto | undefined): Result {\n switch (dto?.tag) {\n case 'draft':\n return ok({ tag: 'draft' });\n case 'submitted':\n if (typeof dto.submittedBy !== 'string' || typeof dto.submittedAt !== 'string')\n return err('status: bad submitted');\n return ok({ tag: 'submitted', submittedBy: dto.submittedBy, submittedAt: dto.submittedAt });\n case 'approved':\n if (typeof dto.approvedBy !== 'string' || typeof dto.approvedAt !== 'string')\n return err('status: bad approved');\n return ok({ tag: 'approved', approvedBy: dto.approvedBy, approvedAt: dto.approvedAt });\n case 'rejected':\n if (\n typeof dto.rejectedBy !== 'string' ||\n typeof dto.rejectedAt !== 'string' ||\n typeof dto.comments !== 'string'\n )\n return err('status: bad rejected');\n return ok({\n tag: 'rejected',\n rejectedBy: dto.rejectedBy,\n rejectedAt: dto.rejectedAt,\n comments: dto.comments,\n });\n case 'sent':\n if (typeof dto.sentAt !== 'string') return err('status: bad sent');\n return ok({ tag: 'sent', sentAt: dto.sentAt });\n default:\n return err(`status: unknown tag ${dto?.tag}`);\n }\n}\n\nfunction parsePassage(dto: LibraryPassageDto): Result {\n if (typeof dto.passageId !== 'string') return err('passage: bad shape');\n if (dto.scope !== 'global' && dto.scope !== 'beroep')\n return err(`passage: unknown scope ${dto.scope}`);\n if (\n typeof dto.sectionKey !== 'string' ||\n typeof dto.label !== 'string' ||\n typeof dto.version !== 'number'\n )\n return err('passage: bad shape');\n const content = parseBlockContent(dto.content);\n if (!content.ok) return content;\n return ok({\n passageId: dto.passageId,\n scope: dto.scope,\n sectionKey: dto.sectionKey,\n label: dto.label,\n content: content.value,\n version: dto.version,\n ...(dto.beroep != null ? { beroep: dto.beroep } : {}),\n });\n}\n\nexport function parseBrief(dto: BriefDto): Result {\n if (\n typeof dto.briefId !== 'string' ||\n typeof dto.drafterId !== 'string' ||\n typeof dto.beroep !== 'string' ||\n typeof dto.templateId !== 'string'\n ) {\n return err('brief: missing ids');\n }\n const status = parseStatus(dto.status);\n if (!status.ok) return status;\n\n const placeholders: PlaceholderDef[] = [];\n for (const p of dto.placeholders ?? []) {\n const parsed = parsePlaceholderDef(p);\n if (!parsed.ok) return parsed;\n placeholders.push(parsed.value);\n }\n const sections: LetterSection[] = [];\n for (const s of dto.sections ?? []) {\n const parsed = parseSection(s);\n if (!parsed.ok) return parsed;\n sections.push(parsed.value);\n }\n return ok({\n briefId: dto.briefId,\n beroep: dto.beroep,\n templateId: dto.templateId,\n placeholders,\n sections,\n status: status.value,\n drafterId: dto.drafterId,\n });\n}\n\nfunction parseDecisions(dto: BriefDecisionsDto | undefined): Result {\n if (\n typeof dto?.canEdit !== 'boolean' ||\n typeof dto.canApprove !== 'boolean' ||\n typeof dto.canReject !== 'boolean' ||\n typeof dto.canSend !== 'boolean'\n ) {\n return err('brief-view: missing/invalid decisions');\n }\n return ok({\n canEdit: dto.canEdit,\n canApprove: dto.canApprove,\n canReject: dto.canReject,\n canSend: dto.canSend,\n });\n}\n\nexport function parseOrgTemplate(dto: OrgTemplateDto | undefined): Result {\n if (\n typeof dto?.subOrgId !== 'string' ||\n typeof dto.orgName !== 'string' ||\n typeof dto.returnAddress !== 'string' ||\n typeof dto.footerContact !== 'string' ||\n typeof dto.footerLegal !== 'string' ||\n typeof dto.signatureName !== 'string' ||\n typeof dto.signatureRole !== 'string' ||\n typeof dto.signatureClosing !== 'string' ||\n typeof dto.version !== 'number'\n ) {\n return err('org-template: bad shape');\n }\n const m = dto.margins;\n if (\n typeof m?.topMm !== 'number' ||\n typeof m.rightMm !== 'number' ||\n typeof m.bottomMm !== 'number' ||\n typeof m.leftMm !== 'number'\n ) {\n return err('org-template: bad margins');\n }\n return ok({\n subOrgId: dto.subOrgId,\n orgName: dto.orgName,\n returnAddress: dto.returnAddress,\n ...(dto.logoDocumentId != null ? { logoDocumentId: dto.logoDocumentId } : {}),\n footerContact: dto.footerContact,\n footerLegal: dto.footerLegal,\n signatureName: dto.signatureName,\n signatureRole: dto.signatureRole,\n signatureClosing: dto.signatureClosing,\n margins: { topMm: m.topMm, rightMm: m.rightMm, bottomMm: m.bottomMm, leftMm: m.leftMm },\n version: dto.version,\n });\n}\n\nexport function parseBriefView(dto: BriefViewDto): Result {\n if (!dto.brief) return err('brief-view: missing brief');\n const brief = parseBrief(dto.brief);\n if (!brief.ok) return brief;\n const decisions = parseDecisions(dto.decisions);\n if (!decisions.ok) return decisions;\n const orgTemplate = parseOrgTemplate(dto.orgTemplate);\n if (!orgTemplate.ok) return orgTemplate;\n const availablePassages: LibraryPassage[] = [];\n for (const p of dto.availablePassages ?? []) {\n const parsed = parsePassage(p);\n if (!parsed.ok) return parsed;\n availablePassages.push(parsed.value);\n }\n return ok({\n brief: brief.value,\n availablePassages,\n decisions: decisions.value,\n orgTemplate: orgTemplate.value,\n });\n}\n\n// --- toDto: domain → wire, for save (collapses the union to the flat shape) ---\n\nfunction nodeToDto(n: RichTextNode): RichTextNodeDto {\n switch (n.type) {\n case 'text':\n return { type: 'text', text: n.text, ...(n.marks ? { marks: [...n.marks] } : {}) };\n case 'placeholder':\n return { type: 'placeholder', key: n.key };\n case 'lineBreak':\n return { type: 'lineBreak' };\n }\n}\n\nfunction contentToDto(content: RichTextBlock): RichTextBlockDto {\n return {\n paragraphs: content.paragraphs.map((p) => ({\n nodes: p.nodes.map(nodeToDto),\n ...(p.list ? { list: p.list } : {}),\n })),\n };\n}\n\nfunction blockToDto(b: LetterBlock): LetterBlockDto {\n return b.type === 'passage'\n ? {\n type: 'passage',\n blockId: b.blockId,\n content: contentToDto(b.content),\n sourcePassageId: b.sourcePassageId,\n sourceVersion: b.sourceVersion,\n edited: b.edited,\n }\n : { type: 'freeText', blockId: b.blockId, content: contentToDto(b.content) };\n}\n\nfunction sectionToDto(s: LetterSection): LetterSectionDto {\n return {\n sectionKey: s.sectionKey,\n title: s.title,\n required: s.required,\n locked: s.locked,\n blocks: s.blocks.map(blockToDto),\n };\n}\n",
"extends": [],
"type": "injectable"
},
{
"name": "BriefStore",
- "id": "injectable-BriefStore-d49743e426155c1b80538a861ad83174e87eea9f939e2102a680afd86f7f05c08d12156d0db866bb34df7b492e3ed43fdb8f54cc449d74fc030ea6c3dc7ebacd",
+ "id": "injectable-BriefStore-4a968ac20e44b5138e882f8a2dc24bc1976b8efbb1a6d3c9be6105e9c622a6e85976c83019c5cbd9d7d33266fa8bce3b36b543e265130a797b3c28ad9f904006",
"file": "src/app/brief/application/brief.store.ts",
"properties": [
{
@@ -7252,7 +7495,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 41,
+ "line": 42,
"modifierKind": [
123
]
@@ -7266,7 +7509,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 36,
+ "line": 37,
"modifierKind": [
123
]
@@ -7280,7 +7523,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 135
+ "line": 146
},
{
"name": "brief",
@@ -7291,7 +7534,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 66,
+ "line": 72,
"modifierKind": [
123
]
@@ -7305,7 +7548,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 42,
+ "line": 43,
"modifierKind": [
148
]
@@ -7319,7 +7562,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 72,
+ "line": 78,
"modifierKind": [
148
]
@@ -7333,7 +7576,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 71,
+ "line": 77,
"modifierKind": [
148
]
@@ -7347,7 +7590,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 73,
+ "line": 79,
"modifierKind": [
148
]
@@ -7361,7 +7604,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 74,
+ "line": 80,
"modifierKind": [
148
]
@@ -7375,7 +7618,7 @@
"indexKey": "",
"optional": false,
"description": "Submit is allowed only when required sections are filled AND no blocking errors.
\n",
- "line": 83,
+ "line": 89,
"rawdescription": "\nSubmit is allowed only when required sections are filled AND no blocking errors.",
"modifierKind": [
148
@@ -7390,7 +7633,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 76,
+ "line": 82,
"modifierKind": [
123
]
@@ -7404,7 +7647,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 80,
+ "line": 86,
"modifierKind": [
148
]
@@ -7418,7 +7661,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 43,
+ "line": 44,
"modifierKind": [
148
]
@@ -7432,7 +7675,22 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 39,
+ "line": 40,
+ "modifierKind": [
+ 148
+ ]
+ },
+ {
+ "name": "orgTemplate",
+ "defaultValue": "signal(null)",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "unknown",
+ "indexKey": "",
+ "optional": false,
+ "description": "The org template the letter renders with (WP-24). Server-owned appearance data,\nnot letter state — held beside the machine, never inside it (brief.machine.ts\nstays untouched by design). Set from every server view that carries it.
\n",
+ "line": 55,
+ "rawdescription": "\nThe org template the letter renders with (WP-24). Server-owned appearance data,\nnot letter state — held beside the machine, never inside it (`brief.machine.ts`\nstays untouched by design). Set from every server view that carries it.",
"modifierKind": [
148
]
@@ -7446,7 +7704,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 136
+ "line": 147
},
{
"name": "remoteData",
@@ -7457,7 +7715,7 @@
"indexKey": "",
"optional": false,
"description": "The load lifecycle as RemoteData, for <app-async> — the machine keeps\nowning the letter's own domain lifecycle (draft/submitted/approved/…); this is\npurely a projection of its loading/failed tags onto the shared async seam.
\n",
- "line": 54,
+ "line": 60,
"rawdescription": "\nThe load lifecycle as `RemoteData`, for `` — the machine keeps\nowning the letter's own domain lifecycle (draft/submitted/approved/…); this is\npurely a projection of its loading/failed tags onto the shared async seam.",
"modifierKind": [
148
@@ -7472,7 +7730,7 @@
"indexKey": "",
"optional": false,
"description": "Surfaced autosave state for the indicator + aria-live region.
\n",
- "line": 49,
+ "line": 50,
"rawdescription": "\nSurfaced autosave state for the indicator + aria-live region.",
"modifierKind": [
148
@@ -7486,7 +7744,7 @@
"indexKey": "",
"optional": true,
"description": "",
- "line": 100,
+ "line": 110,
"modifierKind": [
123
]
@@ -7500,7 +7758,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 137
+ "line": 148
},
{
"name": "store",
@@ -7511,7 +7769,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 37,
+ "line": 38,
"modifierKind": [
123
]
@@ -7525,7 +7783,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 134
+ "line": 145
},
{
"name": "unresolved",
@@ -7536,7 +7794,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 81,
+ "line": 87,
"modifierKind": [
148
]
@@ -7558,7 +7816,7 @@
"optional": false,
"returnType": "void",
"typeParameters": [],
- "line": 154,
+ "line": 165,
"deprecated": false,
"deprecationMessage": "",
"modifierKind": [
@@ -7593,7 +7851,7 @@
"optional": false,
"returnType": "void",
"typeParameters": [],
- "line": 95,
+ "line": 105,
"deprecated": false,
"deprecationMessage": "",
"rawdescription": "\nAn edit: apply it optimistically in the pure reducer, then debounce-save.",
@@ -7618,7 +7876,7 @@
"optional": false,
"returnType": "any",
"typeParameters": [],
- "line": 107,
+ "line": 117,
"deprecated": false,
"deprecationMessage": "",
"modifierKind": [
@@ -7632,7 +7890,7 @@
"optional": false,
"returnType": "any",
"typeParameters": [],
- "line": 88,
+ "line": 94,
"deprecated": false,
"deprecationMessage": "",
"modifierKind": [
@@ -7645,7 +7903,7 @@
"optional": false,
"returnType": "any",
"typeParameters": [],
- "line": 121,
+ "line": 131,
"deprecated": false,
"deprecationMessage": "",
"rawdescription": "\nDemo \"start over\": recreate the brief server-side and load the fresh view.",
@@ -7660,7 +7918,7 @@
"optional": false,
"returnType": "void",
"typeParameters": [],
- "line": 101,
+ "line": 111,
"deprecated": false,
"deprecationMessage": "",
"modifierKind": [
@@ -7683,7 +7941,7 @@
"optional": false,
"returnType": "any",
"typeParameters": [],
- "line": 141,
+ "line": 152,
"deprecated": false,
"deprecationMessage": "",
"modifierKind": [
@@ -7710,7 +7968,7 @@
"deprecationMessage": "",
"description": "Root singleton for the letter: the Elm store (Model + dispatch), the derived\nread-model, and the commands (effects) that call the adapter and dispatch the\noutcome. Mirrors BigProfileStore. All of canEdit/canApprove/canReject/\ncanSend, diagnostics, unresolved, canSubmit are DERIVED here — never\nstored. The permission flags come from the server's decision DTO (PRD-0002 phase\nP1) via BriefState.loaded.decisions — this store never computes them itself.
\n",
"rawdescription": "\n\nRoot singleton for the letter: the Elm store (Model + dispatch), the derived\nread-model, and the commands (effects) that call the adapter and dispatch the\noutcome. Mirrors `BigProfileStore`. All of `canEdit`/`canApprove`/`canReject`/\n`canSend`, `diagnostics`, `unresolved`, `canSubmit` are DERIVED here — never\nstored. The permission flags come from the server's decision DTO (PRD-0002 phase\nP1) via `BriefState.loaded.decisions` — this store never computes them itself.\n",
- "sourceCode": "import { Injectable, computed, inject, signal } from '@angular/core';\nimport { Result } from '@shared/kernel/fp';\nimport { RemoteData } from '@shared/application/remote-data';\nimport { createStore } from '@shared/application/store';\nimport {\n Brief,\n allDiagnostics,\n canSubmit,\n hasBlockingErrors,\n unresolvedPlaceholders,\n} from '@brief/domain/brief';\nimport { BriefMsg, BriefState, initial, reduce } from '@brief/domain/brief.machine';\nimport { BriefAdapter, BriefView } from '@brief/infrastructure/brief.adapter';\n\n/** Transient action state (submit/approve/reject/send/resetDemo) — one tagged union\n instead of a busy boolean + a nullable error sitting side by side. */\ntype ActionState = { tag: 'Idle' } | { tag: 'Busy' } | { tag: 'Failed'; error: string };\n\n/** Debounced-autosave indicator, shown in a small status line near the toolbar —\n a separate concern from ActionState (a stale autosave error doesn't block\n submit/approve/reject), but tag-aligned with it for one consistent idiom. */\ntype SaveState = { tag: 'Idle' } | { tag: 'Saving' } | { tag: 'Saved' } | { tag: 'Error' };\n\ntype LoadedBriefState = Extract;\n\n/**\n * Root singleton for the letter: the Elm store (Model + dispatch), the derived\n * read-model, and the commands (effects) that call the adapter and dispatch the\n * outcome. Mirrors `BigProfileStore`. All of `canEdit`/`canApprove`/`canReject`/\n * `canSend`, `diagnostics`, `unresolved`, `canSubmit` are DERIVED here — never\n * stored. The permission flags come from the server's decision DTO (PRD-0002 phase\n * P1) via `BriefState.loaded.decisions` — this store never computes them itself.\n */\n@Injectable({ providedIn: 'root' })\nexport class BriefStore {\n private adapter = inject(BriefAdapter);\n private store = createStore(initial, reduce);\n\n readonly model = this.store.model;\n\n private actionState = signal({ tag: 'Idle' });\n readonly busy = computed(() => this.actionState().tag === 'Busy');\n readonly lastError = computed(() => {\n const s = this.actionState();\n return s.tag === 'Failed' ? s.error : null;\n });\n\n /** Surfaced autosave state for the indicator + aria-live region. */\n readonly saveState = signal({ tag: 'Idle' });\n\n /** The load lifecycle as `RemoteData`, for `` — the machine keeps\n owning the letter's own domain lifecycle (draft/submitted/approved/…); this is\n purely a projection of its loading/failed tags onto the shared async seam. */\n readonly remoteData = computed>(() => {\n const s = this.model();\n switch (s.tag) {\n case 'loading':\n return { tag: 'Loading' };\n case 'failed':\n return { tag: 'Failure', error: new Error(s.reason) };\n case 'loaded':\n return { tag: 'Success', value: s };\n }\n });\n\n private brief = computed(() => {\n const s = this.model();\n return s.tag === 'loaded' ? s.brief : null;\n });\n\n readonly canEdit = computed(() => this.decisions()?.canEdit ?? false);\n readonly canApprove = computed(() => this.decisions()?.canApprove ?? false);\n readonly canReject = computed(() => this.decisions()?.canReject ?? false);\n readonly canSend = computed(() => this.decisions()?.canSend ?? false);\n\n private decisions = computed(() => {\n const s = this.model();\n return s.tag === 'loaded' ? s.decisions : null;\n });\n readonly diagnostics = computed(() => (this.brief() ? allDiagnostics(this.brief()!) : []));\n readonly unresolved = computed(() => (this.brief() ? unresolvedPlaceholders(this.brief()!) : []));\n /** Submit is allowed only when required sections are filled AND no blocking errors. */\n readonly canSubmit = computed(() => {\n const b = this.brief();\n return !!b && canSubmit(b) && !hasBlockingErrors(this.diagnostics());\n });\n\n async load() {\n const r = await this.adapter.load();\n if (r.ok) this.store.dispatch({ tag: 'BriefLoaded', ...r.value });\n else this.store.dispatch({ tag: 'BriefLoadFailed', reason: r.error });\n }\n\n /** An edit: apply it optimistically in the pure reducer, then debounce-save. */\n edit(msg: BriefMsg) {\n this.store.dispatch(msg);\n this.scheduleSave();\n }\n\n private saveTimer?: ReturnType;\n private scheduleSave() {\n if (!this.canEdit()) return;\n clearTimeout(this.saveTimer);\n // ponytail: 600ms debounce like the wizard draft-sync; the server is the store of record.\n this.saveTimer = setTimeout(() => void this.flushSave(), 600);\n }\n private async flushSave() {\n const b = this.brief();\n if (!b) return;\n this.saveState.set({ tag: 'Saving' });\n const r = await this.adapter.save(b.sections);\n if (r.ok) {\n this.saveState.set({ tag: 'Saved' });\n } else {\n this.actionState.set({ tag: 'Failed', error: r.error });\n this.saveState.set({ tag: 'Error' });\n }\n }\n\n /** Demo \"start over\": recreate the brief server-side and load the fresh view. */\n async resetDemo() {\n this.actionState.set({ tag: 'Busy' });\n clearTimeout(this.saveTimer);\n const r = await this.adapter.reset();\n this.saveState.set({ tag: 'Idle' });\n if (r.ok) {\n this.actionState.set({ tag: 'Idle' });\n this.store.dispatch({ tag: 'BriefLoaded', ...r.value });\n } else {\n this.actionState.set({ tag: 'Failed', error: r.error });\n }\n }\n\n submit = () => this.transition(() => this.adapter.submit());\n approve = () => this.transition(() => this.adapter.approve());\n reject = (comments: string) => this.transition(() => this.adapter.reject(comments));\n send = () => this.transition(() => this.adapter.send());\n\n // A transition: flush any pending save, call the server (authoritative), then mirror\n // the returned status through the pure reducer's guarded transition.\n private async transition(action: () => Promise>) {\n this.actionState.set({ tag: 'Busy' });\n clearTimeout(this.saveTimer);\n await this.flushSave();\n const r = await action();\n if (!r.ok) {\n this.actionState.set({ tag: 'Failed', error: r.error });\n return;\n }\n this.actionState.set({ tag: 'Idle' });\n this.applyServerStatus(r.value);\n }\n\n private applyServerStatus(view: BriefView) {\n const { brief, decisions } = view;\n const s = brief.status;\n switch (s.tag) {\n case 'submitted':\n this.store.dispatch({ tag: 'Submitted', by: s.submittedBy, at: s.submittedAt, decisions });\n break;\n case 'approved':\n this.store.dispatch({ tag: 'Approved', by: s.approvedBy, at: s.approvedAt, decisions });\n break;\n case 'rejected':\n this.store.dispatch({\n tag: 'Rejected',\n by: s.rejectedBy,\n at: s.rejectedAt,\n comments: s.comments,\n decisions,\n });\n break;\n case 'sent':\n this.store.dispatch({ tag: 'Sent', at: s.sentAt, decisions });\n break;\n case 'draft':\n // reopened by a save on a rejected letter — reducer already handled it locally.\n break;\n }\n }\n}\n",
+ "sourceCode": "import { Injectable, computed, inject, signal } from '@angular/core';\nimport { Result } from '@shared/kernel/fp';\nimport { RemoteData } from '@shared/application/remote-data';\nimport { createStore } from '@shared/application/store';\nimport {\n Brief,\n allDiagnostics,\n canSubmit,\n hasBlockingErrors,\n unresolvedPlaceholders,\n} from '@brief/domain/brief';\nimport { BriefMsg, BriefState, initial, reduce } from '@brief/domain/brief.machine';\nimport { OrgTemplate } from '@brief/domain/org-template';\nimport { BriefAdapter, BriefView } from '@brief/infrastructure/brief.adapter';\n\n/** Transient action state (submit/approve/reject/send/resetDemo) — one tagged union\n instead of a busy boolean + a nullable error sitting side by side. */\ntype ActionState = { tag: 'Idle' } | { tag: 'Busy' } | { tag: 'Failed'; error: string };\n\n/** Debounced-autosave indicator, shown in a small status line near the toolbar —\n a separate concern from ActionState (a stale autosave error doesn't block\n submit/approve/reject), but tag-aligned with it for one consistent idiom. */\ntype SaveState = { tag: 'Idle' } | { tag: 'Saving' } | { tag: 'Saved' } | { tag: 'Error' };\n\ntype LoadedBriefState = Extract;\n\n/**\n * Root singleton for the letter: the Elm store (Model + dispatch), the derived\n * read-model, and the commands (effects) that call the adapter and dispatch the\n * outcome. Mirrors `BigProfileStore`. All of `canEdit`/`canApprove`/`canReject`/\n * `canSend`, `diagnostics`, `unresolved`, `canSubmit` are DERIVED here — never\n * stored. The permission flags come from the server's decision DTO (PRD-0002 phase\n * P1) via `BriefState.loaded.decisions` — this store never computes them itself.\n */\n@Injectable({ providedIn: 'root' })\nexport class BriefStore {\n private adapter = inject(BriefAdapter);\n private store = createStore(initial, reduce);\n\n readonly model = this.store.model;\n\n private actionState = signal({ tag: 'Idle' });\n readonly busy = computed(() => this.actionState().tag === 'Busy');\n readonly lastError = computed(() => {\n const s = this.actionState();\n return s.tag === 'Failed' ? s.error : null;\n });\n\n /** Surfaced autosave state for the indicator + aria-live region. */\n readonly saveState = signal({ tag: 'Idle' });\n\n /** The org template the letter renders with (WP-24). Server-owned appearance data,\n not letter state — held beside the machine, never inside it (`brief.machine.ts`\n stays untouched by design). Set from every server view that carries it. */\n readonly orgTemplate = signal(null);\n\n /** The load lifecycle as `RemoteData`, for `` — the machine keeps\n owning the letter's own domain lifecycle (draft/submitted/approved/…); this is\n purely a projection of its loading/failed tags onto the shared async seam. */\n readonly remoteData = computed>(() => {\n const s = this.model();\n switch (s.tag) {\n case 'loading':\n return { tag: 'Loading' };\n case 'failed':\n return { tag: 'Failure', error: new Error(s.reason) };\n case 'loaded':\n return { tag: 'Success', value: s };\n }\n });\n\n private brief = computed(() => {\n const s = this.model();\n return s.tag === 'loaded' ? s.brief : null;\n });\n\n readonly canEdit = computed(() => this.decisions()?.canEdit ?? false);\n readonly canApprove = computed(() => this.decisions()?.canApprove ?? false);\n readonly canReject = computed(() => this.decisions()?.canReject ?? false);\n readonly canSend = computed(() => this.decisions()?.canSend ?? false);\n\n private decisions = computed(() => {\n const s = this.model();\n return s.tag === 'loaded' ? s.decisions : null;\n });\n readonly diagnostics = computed(() => (this.brief() ? allDiagnostics(this.brief()!) : []));\n readonly unresolved = computed(() => (this.brief() ? unresolvedPlaceholders(this.brief()!) : []));\n /** Submit is allowed only when required sections are filled AND no blocking errors. */\n readonly canSubmit = computed(() => {\n const b = this.brief();\n return !!b && canSubmit(b) && !hasBlockingErrors(this.diagnostics());\n });\n\n async load() {\n const r = await this.adapter.load();\n if (r.ok) {\n this.orgTemplate.set(r.value.orgTemplate);\n this.store.dispatch({ tag: 'BriefLoaded', ...r.value });\n } else {\n this.store.dispatch({ tag: 'BriefLoadFailed', reason: r.error });\n }\n }\n\n /** An edit: apply it optimistically in the pure reducer, then debounce-save. */\n edit(msg: BriefMsg) {\n this.store.dispatch(msg);\n this.scheduleSave();\n }\n\n private saveTimer?: ReturnType;\n private scheduleSave() {\n if (!this.canEdit()) return;\n clearTimeout(this.saveTimer);\n // ponytail: 600ms debounce like the wizard draft-sync; the server is the store of record.\n this.saveTimer = setTimeout(() => void this.flushSave(), 600);\n }\n private async flushSave() {\n const b = this.brief();\n if (!b) return;\n this.saveState.set({ tag: 'Saving' });\n const r = await this.adapter.save(b.sections);\n if (r.ok) {\n this.saveState.set({ tag: 'Saved' });\n } else {\n this.actionState.set({ tag: 'Failed', error: r.error });\n this.saveState.set({ tag: 'Error' });\n }\n }\n\n /** Demo \"start over\": recreate the brief server-side and load the fresh view. */\n async resetDemo() {\n this.actionState.set({ tag: 'Busy' });\n clearTimeout(this.saveTimer);\n const r = await this.adapter.reset();\n this.saveState.set({ tag: 'Idle' });\n if (r.ok) {\n this.actionState.set({ tag: 'Idle' });\n this.orgTemplate.set(r.value.orgTemplate);\n this.store.dispatch({ tag: 'BriefLoaded', ...r.value });\n } else {\n this.actionState.set({ tag: 'Failed', error: r.error });\n }\n }\n\n submit = () => this.transition(() => this.adapter.submit());\n approve = () => this.transition(() => this.adapter.approve());\n reject = (comments: string) => this.transition(() => this.adapter.reject(comments));\n send = () => this.transition(() => this.adapter.send());\n\n // A transition: flush any pending save, call the server (authoritative), then mirror\n // the returned status through the pure reducer's guarded transition.\n private async transition(action: () => Promise>) {\n this.actionState.set({ tag: 'Busy' });\n clearTimeout(this.saveTimer);\n await this.flushSave();\n const r = await action();\n if (!r.ok) {\n this.actionState.set({ tag: 'Failed', error: r.error });\n return;\n }\n this.actionState.set({ tag: 'Idle' });\n this.applyServerStatus(r.value);\n }\n\n private applyServerStatus(view: BriefView) {\n // `send` pins the org-template version server-side — mirror whatever came back.\n this.orgTemplate.set(view.orgTemplate);\n const { brief, decisions } = view;\n const s = brief.status;\n switch (s.tag) {\n case 'submitted':\n this.store.dispatch({ tag: 'Submitted', by: s.submittedBy, at: s.submittedAt, decisions });\n break;\n case 'approved':\n this.store.dispatch({ tag: 'Approved', by: s.approvedBy, at: s.approvedAt, decisions });\n break;\n case 'rejected':\n this.store.dispatch({\n tag: 'Rejected',\n by: s.rejectedBy,\n at: s.rejectedAt,\n comments: s.comments,\n decisions,\n });\n break;\n case 'sent':\n this.store.dispatch({ tag: 'Sent', at: s.sentAt, decisions });\n break;\n case 'draft':\n // reopened by a save on a rejected letter — reducer already handled it locally.\n break;\n }\n }\n}\n",
"extends": [],
"type": "injectable"
},
@@ -13326,7 +13584,7 @@
},
{
"name": "BriefPage",
- "id": "component-BriefPage-40bd5b6cae9f4888c48606d832d345c05337eabf98ba52d84475acaa42c7c627ee00857bd2994bd64b9f8d9af71d3dd459bb136edfd748e7e85597ccba2ecb13",
+ "id": "component-BriefPage-831e9d4abed2b19b4791670c05a78d0c64992fa585dcd43f455f38f6f722f516db6d193c14944644a68e474f4df072ad1f3cc2007a43ce9ee50225dc724387d0",
"file": "src/app/brief/ui/brief.page.ts",
"encapsulation": [],
"entryComponents": [],
@@ -13338,7 +13596,7 @@
"styles": [
"\n .brief-toolbar {\n display: flex;\n justify-content: space-between;\n align-items: center;\n gap: var(--rhc-space-max-md);\n margin-block-end: var(--rhc-space-max-lg);\n }\n .save {\n color: var(--rhc-color-foreground-subtle);\n font-size: 0.9em;\n }\n "
],
- "template": "\n @if (lastError(); as err) {\n {{ err }}\n }\n\n \n \n {{ failedText }}\n {{ retryText }}\n \n \n @if (loaded(); as s) {\n \n \n }\n \n \n\n",
+ "template": "\n @if (lastError(); as err) {\n {{ err }}\n }\n\n \n \n {{ failedText }}\n {{ retryText }}\n \n \n @if (loaded(); as s) {\n @if (store.orgTemplate(); as orgTemplate) {\n \n \n }\n }\n \n \n\n",
"templateUrl": [],
"viewProviders": [],
"hostDirectives": [],
@@ -13350,7 +13608,7 @@
"indexKey": "",
"optional": false,
"description": "Typed narrowing for the <app-async> loaded slot — see WP-06: a structural\ndirective's context can't inherit a generic from a sibling host input, so the\nSuccess value is unwrapped here instead of through let-.
\n",
- "line": 111,
+ "line": 114,
"rawdescription": "\nTyped narrowing for the `` loaded slot — see WP-06: a structural\ndirective's context can't inherit a generic from a sibling host input, so the\nSuccess value is unwrapped here instead of through `let-`.",
"modifierKind": [
124,
@@ -13367,7 +13625,7 @@
"indexKey": "",
"optional": false,
"description": "Typed narrowing for the <app-async> loaded slot — see WP-06: a structural\ndirective's context can't inherit a generic from a sibling host input, so the\nSuccess value is unwrapped here instead of through let-.
\n",
- "line": 111,
+ "line": 114,
"rawdescription": "\nTyped narrowing for the `` loaded slot — see WP-06: a structural\ndirective's context can't inherit a generic from a sibling host input, so the\nSuccess value is unwrapped here instead of through `let-`.",
"modifierKind": [
124,
@@ -13386,7 +13644,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 78,
+ "line": 81,
"modifierKind": [
124
]
@@ -13400,7 +13658,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 76,
+ "line": 79,
"modifierKind": [
124
]
@@ -13414,7 +13672,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 77,
+ "line": 80,
"modifierKind": [
124
]
@@ -13428,7 +13686,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 74,
+ "line": 77,
"modifierKind": [
124
]
@@ -13442,7 +13700,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 73,
+ "line": 76,
"modifierKind": [
124
]
@@ -13456,7 +13714,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 80,
+ "line": 83,
"modifierKind": [
124
]
@@ -13470,7 +13728,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 79,
+ "line": 82,
"modifierKind": [
124
]
@@ -13484,7 +13742,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 83,
+ "line": 86,
"modifierKind": [
123
]
@@ -13498,7 +13756,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 84,
+ "line": 87,
"modifierKind": [
123
]
@@ -13512,7 +13770,7 @@
"indexKey": "",
"optional": false,
"description": "Debounced-save state, surfaced in a polite live region.
\n",
- "line": 87,
+ "line": 90,
"rawdescription": "\nDebounced-save state, surfaced in a polite live region.",
"modifierKind": [
124
@@ -13527,7 +13785,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 82,
+ "line": 85,
"modifierKind": [
123
]
@@ -13541,7 +13799,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 72,
+ "line": 75,
"modifierKind": [
124
]
@@ -13554,7 +13812,7 @@
"optional": false,
"returnType": "void",
"typeParameters": [],
- "line": 116,
+ "line": 119,
"deprecated": false,
"deprecationMessage": "",
"modifierKind": [
@@ -13567,7 +13825,7 @@
"optional": false,
"returnType": "void",
"typeParameters": [],
- "line": 104,
+ "line": 107,
"deprecated": false,
"deprecationMessage": "",
"modifierKind": [
@@ -13604,7 +13862,7 @@
"description": "Page: thin container. Injects the root store, kicks off the load, and passes its\nderived read-model to the composer. Business/UI logic lives below in pure pieces;\nthis just wires signals to the organism and events back to store commands.
\n",
"rawdescription": "\nPage: thin container. Injects the root store, kicks off the load, and passes its\nderived read-model to the composer. Business/UI logic lives below in pure pieces;\nthis just wires signals to the organism and events back to store commands.",
"type": "component",
- "sourceCode": "import { Component, computed, inject } from '@angular/core';\nimport { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';\nimport { AlertComponent } from '@shared/ui/alert/alert.component';\nimport { ButtonComponent } from '@shared/ui/button/button.component';\nimport { ASYNC } from '@shared/ui/async/async.component';\nimport { BriefStore } from '@brief/application/brief.store';\nimport { LetterComposerComponent } from '@brief/ui/letter-composer/letter-composer.component';\n\n/** Page: thin container. Injects the root store, kicks off the load, and passes its\n derived read-model to the composer. Business/UI logic lives below in pure pieces;\n this just wires signals to the organism and events back to store commands. */\n@Component({\n selector: 'app-brief-page',\n imports: [PageShellComponent, AlertComponent, ButtonComponent, ...ASYNC, LetterComposerComponent],\n styles: [\n `\n .brief-toolbar {\n display: flex;\n justify-content: space-between;\n align-items: center;\n gap: var(--rhc-space-max-md);\n margin-block-end: var(--rhc-space-max-lg);\n }\n .save {\n color: var(--rhc-color-foreground-subtle);\n font-size: 0.9em;\n }\n `,\n ],\n template: `\n \n @if (lastError(); as err) {\n {{ err }}\n }\n\n \n \n {{ failedText }}\n {{ retryText }}\n \n \n @if (loaded(); as s) {\n \n \n }\n \n \n \n `,\n})\nexport class BriefPage {\n protected store = inject(BriefStore);\n protected model = this.store.model;\n protected lastError = this.store.lastError;\n\n protected heading = $localize`:@@brief.page.heading:Brief opstellen`;\n protected intro = $localize`:@@brief.page.intro:Stel de brief aan de zorgverlener samen uit standaardteksten en vrije tekst.`;\n protected failedText = $localize`:@@brief.page.failed:De brief kon niet worden geladen.`;\n protected retryText = $localize`:@@brief.page.retry:Opnieuw proberen`;\n protected resetLabel = $localize`:@@brief.page.reset:Opnieuw beginnen (demo)`;\n\n private savingText = $localize`:@@brief.page.saving:Concept opslaan…`;\n private savedText = $localize`:@@brief.page.saved:Concept opgeslagen`;\n private saveErrorText = $localize`:@@brief.page.saveError:Opslaan mislukt`;\n\n /** Debounced-save state, surfaced in a polite live region. */\n protected saveText = computed(() => {\n switch (this.store.saveState().tag) {\n case 'Saving':\n return this.savingText;\n case 'Saved':\n return this.savedText;\n case 'Error':\n return this.saveErrorText;\n default:\n return '';\n }\n });\n\n constructor() {\n void this.store.load();\n }\n\n protected resetDemo() {\n void this.store.resetDemo();\n }\n\n /** Typed narrowing for the `` loaded slot — see WP-06: a structural\n directive's context can't inherit a generic from a sibling host input, so the\n Success value is unwrapped here instead of through `let-`. */\n protected readonly loaded = computed(() => {\n const s = this.model();\n return s.tag === 'loaded' ? s : undefined;\n });\n\n protected reload() {\n void this.store.load();\n }\n}\n",
+ "sourceCode": "import { Component, computed, inject } from '@angular/core';\nimport { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';\nimport { AlertComponent } from '@shared/ui/alert/alert.component';\nimport { ButtonComponent } from '@shared/ui/button/button.component';\nimport { ASYNC } from '@shared/ui/async/async.component';\nimport { BriefStore } from '@brief/application/brief.store';\nimport { LetterComposerComponent } from '@brief/ui/letter-composer/letter-composer.component';\n\n/** Page: thin container. Injects the root store, kicks off the load, and passes its\n derived read-model to the composer. Business/UI logic lives below in pure pieces;\n this just wires signals to the organism and events back to store commands. */\n@Component({\n selector: 'app-brief-page',\n imports: [PageShellComponent, AlertComponent, ButtonComponent, ...ASYNC, LetterComposerComponent],\n styles: [\n `\n .brief-toolbar {\n display: flex;\n justify-content: space-between;\n align-items: center;\n gap: var(--rhc-space-max-md);\n margin-block-end: var(--rhc-space-max-lg);\n }\n .save {\n color: var(--rhc-color-foreground-subtle);\n font-size: 0.9em;\n }\n `,\n ],\n template: `\n \n @if (lastError(); as err) {\n {{ err }}\n }\n\n \n \n {{ failedText }}\n {{ retryText }}\n \n \n @if (loaded(); as s) {\n @if (store.orgTemplate(); as orgTemplate) {\n \n \n }\n }\n \n \n \n `,\n})\nexport class BriefPage {\n protected store = inject(BriefStore);\n protected model = this.store.model;\n protected lastError = this.store.lastError;\n\n protected heading = $localize`:@@brief.page.heading:Brief opstellen`;\n protected intro = $localize`:@@brief.page.intro:Stel de brief aan de zorgverlener samen uit standaardteksten en vrije tekst.`;\n protected failedText = $localize`:@@brief.page.failed:De brief kon niet worden geladen.`;\n protected retryText = $localize`:@@brief.page.retry:Opnieuw proberen`;\n protected resetLabel = $localize`:@@brief.page.reset:Opnieuw beginnen (demo)`;\n\n private savingText = $localize`:@@brief.page.saving:Concept opslaan…`;\n private savedText = $localize`:@@brief.page.saved:Concept opgeslagen`;\n private saveErrorText = $localize`:@@brief.page.saveError:Opslaan mislukt`;\n\n /** Debounced-save state, surfaced in a polite live region. */\n protected saveText = computed(() => {\n switch (this.store.saveState().tag) {\n case 'Saving':\n return this.savingText;\n case 'Saved':\n return this.savedText;\n case 'Error':\n return this.saveErrorText;\n default:\n return '';\n }\n });\n\n constructor() {\n void this.store.load();\n }\n\n protected resetDemo() {\n void this.store.resetDemo();\n }\n\n /** Typed narrowing for the `` loaded slot — see WP-06: a structural\n directive's context can't inherit a generic from a sibling host input, so the\n Success value is unwrapped here instead of through `let-`. */\n protected readonly loaded = computed(() => {\n const s = this.model();\n return s.tag === 'loaded' ? s : undefined;\n });\n\n protected reload() {\n void this.store.load();\n }\n}\n",
"assetsDirs": [],
"styleUrlsData": "",
"stylesData": "\n .brief-toolbar {\n display: flex;\n justify-content: space-between;\n align-items: center;\n gap: var(--rhc-space-max-md);\n margin-block-end: var(--rhc-space-max-lg);\n }\n .save {\n color: var(--rhc-color-foreground-subtle);\n font-size: 0.9em;\n }\n \n",
@@ -13614,7 +13872,7 @@
"deprecated": false,
"deprecationMessage": "",
"args": [],
- "line": 98
+ "line": 101
},
"extends": []
},
@@ -17910,9 +18168,426 @@
"stylesData": "\n :host {\n display: block;\n }\n .block {\n border-inline-start: var(--rhc-border-width-lg) solid var(--rhc-color-border-subtle);\n padding-inline-start: var(--rhc-space-max-md);\n }\n .meta {\n display: flex;\n justify-content: space-between;\n align-items: center;\n gap: var(--rhc-space-max-md);\n margin-block-end: var(--rhc-space-max-sm);\n }\n .controls {\n display: flex;\n gap: var(--rhc-space-max-sm);\n }\n \n",
"extends": []
},
+ {
+ "name": "LetterCanvasComponent",
+ "id": "component-LetterCanvasComponent-5b5482d538b44c7279a3af3862826da02c9bd1d6550fdbbfc3d6d674df661325e934578640bbeca376f63094612435c71528f94407bb8bb7afd9b5b79cd522d3",
+ "file": "src/app/brief/ui/letter-canvas/letter-canvas.component.ts",
+ "encapsulation": [],
+ "entryComponents": [],
+ "inputs": [],
+ "outputs": [],
+ "providers": [],
+ "selector": "app-letter-canvas",
+ "styleUrls": [],
+ "styles": [
+ "\n :host {\n display: block;\n }\n .toolbar {\n display: flex;\n justify-content: flex-end;\n margin-block-end: var(--rhc-space-max-sm);\n }\n /* Portal-side chrome around the letter surface (not part of the contract file). */\n .surface {\n background: var(--rhc-color-grijs-100, #f0f0f0);\n border: var(--rhc-border-width-sm) solid var(--rhc-color-border-default);\n border-radius: var(--rhc-border-radius-md);\n padding: var(--rhc-space-max-lg);\n overflow-x: auto;\n }\n .surface .letter {\n box-shadow: 0 1px 4px rgb(0 0 0 / 0.15);\n }\n /* Org-identity regions: visibly not the drafter's to edit. */\n .from-template {\n background: var(--rhc-color-grijs-100, #f5f5f5);\n outline: 2mm solid var(--rhc-color-grijs-100, #f5f5f5);\n }\n .from-template-caption {\n font-size: 7.5pt;\n /* grijs-700, not foreground-subtle: subtle misses AA contrast on the tint. */\n color: var(--rhc-color-grijs-700, #334155);\n margin: 0 0 2mm;\n }\n "
+ ],
+ "template": "\n @for (node of nodes; track $index) {\n @switch (node.type) {\n @case ('text') {\n {{ node.text }}\n }\n @case ('lineBreak') {\n
\n }\n @case ('placeholder') {\n @if (showSample() && autoFor(node.key)) {\n {{ sampleFor(node.key) }}\n } @else {\n \n }\n }\n }\n }\n\n\n@if (editableRegions() === 'none') {\n \n}\n\n\n
\n \n
\n @if (tintTemplate()) {\n
{{ fromTemplateCaption() }}
\n }\n
{{ orgTemplate().orgName }}
\n
{{ orgTemplate().returnAddress }}\n
{{ recipientText() }}\n
\n \n
- {{ referenceLabel() }}
\n - {{ brief().briefId }}
\n \n \n
- {{ dateLabel() }}
\n - {{ letterDate }}
\n \n
\n
\n\n
\n @if (editableRegions() === 'content') {\n @for (section of brief().sections; track section.sectionKey) {\n
\n }\n } @else {\n @for (section of brief().sections; track section.sectionKey) {\n
\n {{ section.title }}
\n @for (block of section.blocks; track block.blockId) {\n @for (seg of segmentsOf(block); track $index) {\n @if (seg.list === 'bullet') {\n \n @for (para of seg.items; track $index) {\n - \n \n
\n }\n
\n } @else if (seg.list === 'number') {\n \n @for (para of seg.items; track $index) {\n - \n \n
\n }\n
\n } @else {\n \n \n
\n }\n }\n }\n \n }\n }\n
\n\n
\n
{{ orgTemplate().signatureClosing }}
\n
{{ orgTemplate().signatureName }}
\n
{{ orgTemplate().signatureRole }}
\n
\n\n \n\n @for (top of pageBreaks(); track $index) {\n
\n {{ pageBreakCaption() }}\n
\n }\n
\n
\n",
+ "templateUrl": [],
+ "viewProviders": [],
+ "hostDirectives": [],
+ "inputsClass": [
+ {
+ "name": "availablePassages",
+ "defaultValue": "[]",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "readonly LibraryPassage[]",
+ "indexKey": "",
+ "optional": false,
+ "description": "",
+ "line": 237,
+ "required": false
+ },
+ {
+ "name": "brief",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "Brief",
+ "indexKey": "",
+ "optional": false,
+ "description": "",
+ "line": 232,
+ "required": true
+ },
+ {
+ "name": "dateLabel",
+ "defaultValue": "$localize`:@@brief.canvas.date:Datum`",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "indexKey": "",
+ "optional": false,
+ "description": "",
+ "line": 255,
+ "required": false
+ },
+ {
+ "name": "diagnostics",
+ "defaultValue": "[]",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "readonly Diagnostic[]",
+ "indexKey": "",
+ "optional": false,
+ "description": "",
+ "line": 239,
+ "required": false
+ },
+ {
+ "name": "editableRegions",
+ "defaultValue": "'none'",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "\"content\" | \"template\" | \"none\"",
+ "indexKey": "",
+ "optional": false,
+ "description": "Who edits what on the surface: drafter ('content'), read-only ('none'),\nadmin editor ('template', consumer arrives in WP-26).
\n",
+ "line": 236,
+ "rawdescription": "\nWho edits what on the surface: drafter ('content'), read-only ('none'),\nadmin editor ('template', consumer arrives in WP-26).",
+ "required": false
+ },
+ {
+ "name": "fromTemplateCaption",
+ "defaultValue": " $localize`:@@brief.canvas.fromTemplate:Komt uit de huisstijl van de organisatie — niet bewerkbaar in de brief.`, ",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "indexKey": "",
+ "optional": false,
+ "description": "",
+ "line": 245,
+ "required": false
+ },
+ {
+ "name": "hideSampleLabel",
+ "defaultValue": "$localize`:@@brief.preview.hideSample:Testwaarden verbergen`",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "indexKey": "",
+ "optional": false,
+ "description": "",
+ "line": 244,
+ "required": false
+ },
+ {
+ "name": "orgTemplate",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "OrgTemplate",
+ "indexKey": "",
+ "optional": false,
+ "description": "",
+ "line": 233,
+ "required": true
+ },
+ {
+ "name": "pageBreakCaption",
+ "defaultValue": " $localize`:@@brief.canvas.pageBreak:±pagina-einde — afdrukvoorbeeld is leidend`, ",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "indexKey": "",
+ "optional": false,
+ "description": "",
+ "line": 248,
+ "required": false
+ },
+ {
+ "name": "placeholders",
+ "defaultValue": "[]",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "readonly PlaceholderOption[]",
+ "indexKey": "",
+ "optional": false,
+ "description": "",
+ "line": 238,
+ "required": false
+ },
+ {
+ "name": "recipientText",
+ "defaultValue": " $localize`:@@brief.canvas.recipient:Adres van de geadresseerde\\n(wordt ingevuld bij verzending)`, ",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "indexKey": "",
+ "optional": false,
+ "description": "",
+ "line": 251,
+ "required": false
+ },
+ {
+ "name": "referenceLabel",
+ "defaultValue": "$localize`:@@brief.canvas.reference:Ons kenmerk`",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "indexKey": "",
+ "optional": false,
+ "description": "",
+ "line": 254,
+ "required": false
+ },
+ {
+ "name": "showSampleLabel",
+ "defaultValue": "$localize`:@@brief.preview.showSample:Voorbeeld met testwaarden`",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "indexKey": "",
+ "optional": false,
+ "description": "",
+ "line": 243,
+ "required": false
+ },
+ {
+ "name": "zoom",
+ "defaultValue": "1",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "indexKey": "",
+ "optional": false,
+ "description": "",
+ "line": 240,
+ "required": false
+ }
+ ],
+ "outputsClass": [
+ {
+ "name": "edit",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "BriefMsg",
+ "indexKey": "",
+ "optional": false,
+ "description": "",
+ "line": 241,
+ "required": false
+ }
+ ],
+ "propertiesClass": [
+ {
+ "name": "autoFor",
+ "defaultValue": "() => {...}",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "unknown",
+ "indexKey": "",
+ "optional": false,
+ "description": "",
+ "line": 289,
+ "modifierKind": [
+ 124
+ ]
+ },
+ {
+ "name": "defs",
+ "defaultValue": "computed(() => new Map(this.brief().placeholders.map((p) => [p.key, p])))",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "unknown",
+ "indexKey": "",
+ "optional": false,
+ "description": "",
+ "line": 276,
+ "modifierKind": [
+ 123
+ ]
+ },
+ {
+ "name": "labelFor",
+ "defaultValue": "() => {...}",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "unknown",
+ "indexKey": "",
+ "optional": false,
+ "description": "",
+ "line": 288,
+ "modifierKind": [
+ 124
+ ]
+ },
+ {
+ "name": "letterDate",
+ "defaultValue": "formatDatumNl(new Date())",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "unknown",
+ "indexKey": "",
+ "optional": false,
+ "description": "",
+ "line": 258,
+ "modifierKind": [
+ 124
+ ]
+ },
+ {
+ "name": "marginStyle",
+ "defaultValue": "computed(() => {\n const m = this.orgTemplate().margins;\n return {\n '--letter-margin-top': `${m.topMm}mm`,\n '--letter-margin-right': `${m.rightMm}mm`,\n '--letter-margin-bottom': `${m.bottomMm}mm`,\n '--letter-margin-left': `${m.leftMm}mm`,\n };\n })",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "unknown",
+ "indexKey": "",
+ "optional": false,
+ "description": "",
+ "line": 264,
+ "modifierKind": [
+ 124
+ ]
+ },
+ {
+ "name": "page",
+ "defaultValue": "viewChild>('page')",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "unknown",
+ "indexKey": "",
+ "optional": false,
+ "description": "",
+ "line": 296,
+ "modifierKind": [
+ 123
+ ]
+ },
+ {
+ "name": "pageBreaks",
+ "defaultValue": "signal([])",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "unknown",
+ "indexKey": "",
+ "optional": false,
+ "description": "",
+ "line": 297,
+ "modifierKind": [
+ 124
+ ]
+ },
+ {
+ "name": "sampleFor",
+ "defaultValue": "() => {...}",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "unknown",
+ "indexKey": "",
+ "optional": false,
+ "description": "",
+ "line": 291,
+ "modifierKind": [
+ 124
+ ]
+ },
+ {
+ "name": "segmentsOf",
+ "defaultValue": "() => {...}",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "unknown",
+ "indexKey": "",
+ "optional": false,
+ "description": "",
+ "line": 287,
+ "modifierKind": [
+ 124
+ ]
+ },
+ {
+ "name": "showSample",
+ "defaultValue": "signal(false)",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "unknown",
+ "indexKey": "",
+ "optional": false,
+ "description": "",
+ "line": 257,
+ "modifierKind": [
+ 124
+ ]
+ },
+ {
+ "name": "stateFor",
+ "defaultValue": "() => {...}",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "unknown",
+ "indexKey": "",
+ "optional": false,
+ "description": "",
+ "line": 290,
+ "modifierKind": [
+ 124
+ ]
+ },
+ {
+ "name": "tintTemplate",
+ "defaultValue": "computed(() => this.editableRegions() === 'content')",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "unknown",
+ "indexKey": "",
+ "optional": false,
+ "description": "The letterhead/signature/footer are tinted "not yours" only while composing —\nin 'none' the whole surface is read-only, in 'template' they ARE the editable focus.
\n",
+ "line": 262,
+ "rawdescription": "\nThe letterhead/signature/footer are tinted \"not yours\" only while composing —\nin 'none' the whole surface is read-only, in 'template' they ARE the editable focus.",
+ "modifierKind": [
+ 124
+ ]
+ },
+ {
+ "name": "worst",
+ "defaultValue": "computed(() => {\n const m = new Map();\n for (const d of this.diagnostics()) {\n if (!d.placeholderKey) continue;\n if (d.severity === 'error') m.set(d.placeholderKey, 'error');\n else if (!m.has(d.placeholderKey)) m.set(d.placeholderKey, 'warning');\n }\n return m;\n })",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "unknown",
+ "indexKey": "",
+ "optional": false,
+ "description": "",
+ "line": 277,
+ "modifierKind": [
+ 123
+ ]
+ }
+ ],
+ "methodsClass": [],
+ "deprecated": false,
+ "deprecationMessage": "",
+ "hostBindings": [],
+ "hostListeners": [],
+ "standalone": false,
+ "imports": [
+ {
+ "name": "NgTemplateOutlet"
+ },
+ {
+ "name": "ButtonComponent",
+ "type": "component"
+ },
+ {
+ "name": "PlaceholderChipComponent",
+ "type": "component"
+ },
+ {
+ "name": "LetterSectionComponent",
+ "type": "component"
+ }
+ ],
+ "description": "Organism: the letter as one surface — the org template's letterhead, signature and\nfooter around the case-type template's sections. editableRegions picks who edits\nwhat: 'content' hosts the editable letter-sections in place (drafter), 'none'\nrenders everything read-only (approver/locked, absorbs the old letter-preview),\n'template' reserves the org-identity regions for the admin editor (WP-26).\nLetter typography/geometry come from the shared public/letter.css contract —\nthe same file the backend preview renderer inlines (WP-25).
\n",
+ "rawdescription": "\nOrganism: the letter as one surface — the org template's letterhead, signature and\nfooter around the case-type template's sections. `editableRegions` picks who edits\nwhat: `'content'` hosts the editable letter-sections in place (drafter), `'none'`\nrenders everything read-only (approver/locked, absorbs the old letter-preview),\n`'template'` reserves the org-identity regions for the admin editor (WP-26).\nLetter typography/geometry come from the shared `public/letter.css` contract —\nthe same file the backend preview renderer inlines (WP-25).",
+ "type": "component",
+ "sourceCode": "import {\n Component,\n DestroyRef,\n ElementRef,\n computed,\n effect,\n inject,\n input,\n output,\n signal,\n viewChild,\n} from '@angular/core';\nimport { NgTemplateOutlet } from '@angular/common';\nimport { ButtonComponent } from '@shared/ui/button/button.component';\nimport { PlaceholderChipComponent } from '@shared/ui/placeholder-chip/placeholder-chip.component';\nimport { PlaceholderOption } from '@shared/ui/rich-text-editor/rich-text-editor.component';\nimport { formatDatumNl } from '@shared/kernel/datum';\nimport { Paragraph } from '@shared/kernel/rich-text';\nimport { Brief, LetterBlock, LibraryPassage } from '@brief/domain/brief';\nimport { OrgTemplate } from '@brief/domain/org-template';\nimport { Diagnostic } from '@brief/domain/placeholders';\nimport { BriefMsg } from '@brief/domain/brief.machine';\nimport { LetterSectionComponent } from '@brief/ui/letter-section/letter-section.component';\n\n/** A run of consecutive lines to render together: a list (bullet/number) or a single plain line. */\ntype PreviewSegment = {\n readonly list: 'bullet' | 'number' | null;\n readonly items: readonly Paragraph[];\n};\n\nfunction groupParagraphs(paras: readonly Paragraph[]): PreviewSegment[] {\n const out: { list: 'bullet' | 'number' | null; items: Paragraph[] }[] = [];\n for (const para of paras) {\n const kind = para.list ?? null;\n const last = out[out.length - 1];\n if (kind && last && last.list === kind) last.items.push(para);\n else out.push({ list: kind, items: [para] });\n }\n return out;\n}\n\n// Illustrative values for the \"Voorbeeld\" toggle — what send resolves server-side.\nconst SAMPLE_VALUES: Record = {\n naam_zorgverlener: 'J. Jansen',\n big_nummer: '12345678901',\n};\n\n/** A4 height in CSS px (1in = 96px = 25.4mm) — for the approximate page-break marks. */\nconst A4_HEIGHT_PX = (297 * 96) / 25.4;\n\n/** Organism: the letter as one surface — the org template's letterhead, signature and\n footer around the case-type template's sections. `editableRegions` picks who edits\n what: `'content'` hosts the editable letter-sections in place (drafter), `'none'`\n renders everything read-only (approver/locked, absorbs the old letter-preview),\n `'template'` reserves the org-identity regions for the admin editor (WP-26).\n Letter typography/geometry come from the shared `public/letter.css` contract —\n the same file the backend preview renderer inlines (WP-25). */\n@Component({\n selector: 'app-letter-canvas',\n imports: [NgTemplateOutlet, ButtonComponent, PlaceholderChipComponent, LetterSectionComponent],\n styles: [\n `\n :host {\n display: block;\n }\n .toolbar {\n display: flex;\n justify-content: flex-end;\n margin-block-end: var(--rhc-space-max-sm);\n }\n /* Portal-side chrome around the letter surface (not part of the contract file). */\n .surface {\n background: var(--rhc-color-grijs-100, #f0f0f0);\n border: var(--rhc-border-width-sm) solid var(--rhc-color-border-default);\n border-radius: var(--rhc-border-radius-md);\n padding: var(--rhc-space-max-lg);\n overflow-x: auto;\n }\n .surface .letter {\n box-shadow: 0 1px 4px rgb(0 0 0 / 0.15);\n }\n /* Org-identity regions: visibly not the drafter's to edit. */\n .from-template {\n background: var(--rhc-color-grijs-100, #f5f5f5);\n outline: 2mm solid var(--rhc-color-grijs-100, #f5f5f5);\n }\n .from-template-caption {\n font-size: 7.5pt;\n /* grijs-700, not foreground-subtle: subtle misses AA contrast on the tint. */\n color: var(--rhc-color-grijs-700, #334155);\n margin: 0 0 2mm;\n }\n `,\n ],\n template: `\n \n @for (node of nodes; track $index) {\n @switch (node.type) {\n @case ('text') {\n {{ node.text }}\n }\n @case ('lineBreak') {\n
\n }\n @case ('placeholder') {\n @if (showSample() && autoFor(node.key)) {\n {{ sampleFor(node.key) }}\n } @else {\n \n }\n }\n }\n }\n \n\n @if (editableRegions() === 'none') {\n \n }\n\n \n
\n \n
\n @if (tintTemplate()) {\n
{{ fromTemplateCaption() }}
\n }\n
{{ orgTemplate().orgName }}
\n
{{ orgTemplate().returnAddress }}\n
{{ recipientText() }}\n
\n \n
- {{ referenceLabel() }}
\n - {{ brief().briefId }}
\n \n \n
- {{ dateLabel() }}
\n - {{ letterDate }}
\n \n
\n
\n\n
\n @if (editableRegions() === 'content') {\n @for (section of brief().sections; track section.sectionKey) {\n
\n }\n } @else {\n @for (section of brief().sections; track section.sectionKey) {\n
\n {{ section.title }}
\n @for (block of section.blocks; track block.blockId) {\n @for (seg of segmentsOf(block); track $index) {\n @if (seg.list === 'bullet') {\n \n @for (para of seg.items; track $index) {\n - \n \n
\n }\n
\n } @else if (seg.list === 'number') {\n \n @for (para of seg.items; track $index) {\n - \n \n
\n }\n
\n } @else {\n \n \n
\n }\n }\n }\n \n }\n }\n
\n\n
\n
{{ orgTemplate().signatureClosing }}
\n
{{ orgTemplate().signatureName }}
\n
{{ orgTemplate().signatureRole }}
\n
\n\n \n\n @for (top of pageBreaks(); track $index) {\n
\n {{ pageBreakCaption() }}\n
\n }\n
\n
\n `,\n})\nexport class LetterCanvasComponent {\n brief = input.required();\n orgTemplate = input.required();\n /** Who edits what on the surface: drafter ('content'), read-only ('none'),\n admin editor ('template', consumer arrives in WP-26). */\n editableRegions = input<'content' | 'template' | 'none'>('none');\n availablePassages = input([]);\n placeholders = input([]);\n diagnostics = input([]);\n zoom = input(1);\n edit = output();\n\n showSampleLabel = input($localize`:@@brief.preview.showSample:Voorbeeld met testwaarden`);\n hideSampleLabel = input($localize`:@@brief.preview.hideSample:Testwaarden verbergen`);\n fromTemplateCaption = input(\n $localize`:@@brief.canvas.fromTemplate:Komt uit de huisstijl van de organisatie — niet bewerkbaar in de brief.`,\n );\n pageBreakCaption = input(\n $localize`:@@brief.canvas.pageBreak:±pagina-einde — afdrukvoorbeeld is leidend`,\n );\n recipientText = input(\n $localize`:@@brief.canvas.recipient:Adres van de geadresseerde\\n(wordt ingevuld bij verzending)`,\n );\n referenceLabel = input($localize`:@@brief.canvas.reference:Ons kenmerk`);\n dateLabel = input($localize`:@@brief.canvas.date:Datum`);\n\n protected showSample = signal(false);\n protected letterDate = formatDatumNl(new Date());\n\n /** The letterhead/signature/footer are tinted \"not yours\" only while composing —\n in 'none' the whole surface is read-only, in 'template' they ARE the editable focus. */\n protected tintTemplate = computed(() => this.editableRegions() === 'content');\n\n protected marginStyle = computed(() => {\n const m = this.orgTemplate().margins;\n return {\n '--letter-margin-top': `${m.topMm}mm`,\n '--letter-margin-right': `${m.rightMm}mm`,\n '--letter-margin-bottom': `${m.bottomMm}mm`,\n '--letter-margin-left': `${m.leftMm}mm`,\n };\n });\n\n // --- read-only rendering helpers (migrated from the superseded letter-preview) ---\n\n private defs = computed(() => new Map(this.brief().placeholders.map((p) => [p.key, p])));\n private worst = computed(() => {\n const m = new Map();\n for (const d of this.diagnostics()) {\n if (!d.placeholderKey) continue;\n if (d.severity === 'error') m.set(d.placeholderKey, 'error');\n else if (!m.has(d.placeholderKey)) m.set(d.placeholderKey, 'warning');\n }\n return m;\n });\n\n protected segmentsOf = (block: LetterBlock) => groupParagraphs(block.content.paragraphs);\n protected labelFor = (key: string) => this.defs().get(key)?.label ?? key;\n protected autoFor = (key: string) => this.defs().get(key)?.autoResolvable ?? false;\n protected stateFor = (key: string): 'ok' | 'warning' | 'error' => this.worst().get(key) ?? 'ok';\n protected sampleFor = (key: string) =>\n SAMPLE_VALUES[key] ?? (key === 'datum' ? this.letterDate : this.labelFor(key));\n\n // --- approximate page-break marks (PRD §2b: honest \"±\", print preview is leading) ---\n\n private page = viewChild>('page');\n protected pageBreaks = signal([]);\n\n constructor() {\n // ponytail: whole-surface height / A4-interval — ignores that a break never truly\n // falls mid-line; the caption says \"±\" and WP-25's server preview is authoritative.\n const observer = new ResizeObserver(([entry]) => {\n // ~1cm tolerance so a letter ending on a page boundary gets no edge-hugging mark.\n const pages = Math.ceil((entry.target.scrollHeight - 40) / A4_HEIGHT_PX);\n this.pageBreaks.set(\n Array.from({ length: Math.max(0, pages - 1) }, (_, i) => (i + 1) * A4_HEIGHT_PX),\n );\n });\n effect((onCleanup) => {\n const el = this.page()?.nativeElement;\n if (!el) return;\n observer.observe(el);\n onCleanup(() => observer.unobserve(el));\n });\n inject(DestroyRef).onDestroy(() => observer.disconnect());\n }\n}\n",
+ "assetsDirs": [],
+ "styleUrlsData": "",
+ "stylesData": "\n :host {\n display: block;\n }\n .toolbar {\n display: flex;\n justify-content: flex-end;\n margin-block-end: var(--rhc-space-max-sm);\n }\n /* Portal-side chrome around the letter surface (not part of the contract file). */\n .surface {\n background: var(--rhc-color-grijs-100, #f0f0f0);\n border: var(--rhc-border-width-sm) solid var(--rhc-color-border-default);\n border-radius: var(--rhc-border-radius-md);\n padding: var(--rhc-space-max-lg);\n overflow-x: auto;\n }\n .surface .letter {\n box-shadow: 0 1px 4px rgb(0 0 0 / 0.15);\n }\n /* Org-identity regions: visibly not the drafter's to edit. */\n .from-template {\n background: var(--rhc-color-grijs-100, #f5f5f5);\n outline: 2mm solid var(--rhc-color-grijs-100, #f5f5f5);\n }\n .from-template-caption {\n font-size: 7.5pt;\n /* grijs-700, not foreground-subtle: subtle misses AA contrast on the tint. */\n color: var(--rhc-color-grijs-700, #334155);\n margin: 0 0 2mm;\n }\n \n",
+ "constructorObj": {
+ "name": "constructor",
+ "description": "",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "args": [],
+ "line": 297
+ },
+ "extends": []
+ },
{
"name": "LetterComposerComponent",
- "id": "component-LetterComposerComponent-91a7a2e389e108e2c579198ec50c420c381916c3871b5d19ada723b9bed3769c6fe381dc0d52cbf21679e0b9c341b6fb00eed00ed8736f0f4a1d5e7a6bd89515",
+ "id": "component-LetterComposerComponent-77edd5fe085467d0e3885c0fc211e474792ed97568ba59ca9bc88e4c35b4f10b52b54a4e6ea973db3d12630ab4ed00129f1cf1c552677a18b83d56a0e2dbfef8",
"file": "src/app/brief/ui/letter-composer/letter-composer.component.ts",
"encapsulation": [],
"entryComponents": [],
@@ -17922,9 +18597,9 @@
"selector": "app-letter-composer",
"styleUrls": [],
"styles": [
- "\n :host {\n display: block;\n }\n .head {\n display: flex;\n justify-content: space-between;\n align-items: center;\n gap: var(--rhc-space-max-md);\n flex-wrap: wrap;\n margin-block-end: var(--rhc-space-max-lg);\n }\n .sections {\n display: grid;\n gap: var(--rhc-space-max-2xl);\n }\n .panel {\n margin-block: var(--rhc-space-max-xl);\n }\n .bar {\n display: flex;\n flex-wrap: wrap;\n gap: var(--rhc-space-max-md);\n align-items: center;\n margin-block-start: var(--rhc-space-max-xl);\n }\n "
+ "\n :host {\n display: block;\n }\n .head {\n display: flex;\n justify-content: space-between;\n align-items: center;\n gap: var(--rhc-space-max-md);\n flex-wrap: wrap;\n margin-block-end: var(--rhc-space-max-lg);\n }\n .panel {\n margin-block: var(--rhc-space-max-xl);\n }\n .bar {\n display: flex;\n flex-wrap: wrap;\n gap: var(--rhc-space-max-md);\n align-items: center;\n margin-block-start: var(--rhc-space-max-xl);\n }\n "
],
- "template": "\n\n@if (status() === 'rejected') {\n \n}\n\n@if (canEdit()) {\n \n @for (section of brief().sections; track section.sectionKey) {\n
\n }\n
\n} @else {\n \n}\n\n\n\n\n @switch (status()) {\n @case ('draft') {\n @if (canEdit()) {\n
{{ submitLabel() }}\n @if (!canSubmit()) {\n
{{ submitHint() }}\n }\n }\n }\n @case ('rejected') {\n @if (canEdit()) {\n
{{ resubmitLabel() }}\n }\n }\n @case ('submitted') {\n @if (canApprove() || canReject()) {\n
{{\n approveLabel()\n }}\n
\n } @else {\n
{{ awaitingText() }}\n }\n }\n @case ('approved') {\n @if (canSend()) {\n
{{\n sendLabel()\n }}\n }\n }\n @case ('sent') {\n
{{ sentText() }}\n }\n }\n
\n",
+ "template": "\n\n@if (status() === 'rejected') {\n \n}\n\n\n\n\n\n\n @switch (status()) {\n @case ('draft') {\n @if (canEdit()) {\n
{{ submitLabel() }}\n @if (!canSubmit()) {\n
{{ submitHint() }}\n }\n }\n }\n @case ('rejected') {\n @if (canEdit()) {\n
{{ resubmitLabel() }}\n }\n }\n @case ('submitted') {\n @if (canApprove() || canReject()) {\n
{{\n approveLabel()\n }}\n
\n } @else {\n
{{ awaitingText() }}\n }\n }\n @case ('approved') {\n @if (canSend()) {\n
{{\n sendLabel()\n }}\n }\n }\n @case ('sent') {\n
{{ sentText() }}\n }\n }\n
\n",
"templateUrl": [],
"viewProviders": [],
"hostDirectives": [],
@@ -17937,7 +18612,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 164,
+ "line": 154,
"required": false
},
{
@@ -17949,7 +18624,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 142,
+ "line": 132,
"required": false
},
{
@@ -17960,7 +18635,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 166,
+ "line": 156,
"required": false
},
{
@@ -17971,7 +18646,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 141,
+ "line": 130,
"required": true
},
{
@@ -17982,7 +18657,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 149,
+ "line": 139,
"required": false
},
{
@@ -17993,7 +18668,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 145,
+ "line": 135,
"required": false
},
{
@@ -18004,7 +18679,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 144,
+ "line": 134,
"required": false
},
{
@@ -18015,7 +18690,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 146,
+ "line": 136,
"required": false
},
{
@@ -18026,7 +18701,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 147,
+ "line": 137,
"required": false
},
{
@@ -18037,7 +18712,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 148,
+ "line": 138,
"required": false
},
{
@@ -18049,9 +18724,20 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 143,
+ "line": 133,
"required": false
},
+ {
+ "name": "orgTemplate",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "OrgTemplate",
+ "indexKey": "",
+ "optional": false,
+ "description": "",
+ "line": 131,
+ "required": true
+ },
{
"name": "resubmitLabel",
"defaultValue": "$localize`:@@brief.resubmit:Opnieuw indienen`",
@@ -18060,7 +18746,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 160,
+ "line": 150,
"required": false
},
{
@@ -18071,7 +18757,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 165,
+ "line": 155,
"required": false
},
{
@@ -18082,7 +18768,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 169,
+ "line": 159,
"required": false
},
{
@@ -18093,7 +18779,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 161,
+ "line": 151,
"required": false
},
{
@@ -18104,7 +18790,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 159,
+ "line": 149,
"required": false
},
{
@@ -18115,7 +18801,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 158,
+ "line": 148,
"required": false
}
],
@@ -18128,7 +18814,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 153,
+ "line": 143,
"required": false
},
{
@@ -18139,7 +18825,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 151,
+ "line": 141,
"required": false
},
{
@@ -18150,7 +18836,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 156,
+ "line": 146,
"required": false
},
{
@@ -18161,7 +18847,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 154,
+ "line": 144,
"required": false
},
{
@@ -18172,7 +18858,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 155,
+ "line": 145,
"required": false
},
{
@@ -18183,7 +18869,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 152,
+ "line": 142,
"required": false
}
],
@@ -18197,7 +18883,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 179,
+ "line": 169,
"modifierKind": [
124
]
@@ -18211,7 +18897,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 172,
+ "line": 162,
"modifierKind": [
124
]
@@ -18225,7 +18911,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 171,
+ "line": 161,
"modifierKind": [
124
]
@@ -18239,7 +18925,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 200,
+ "line": 190,
"modifierKind": [
124
]
@@ -18253,7 +18939,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 185,
+ "line": 175,
"modifierKind": [
124
]
@@ -18283,11 +18969,7 @@
"type": "component"
},
{
- "name": "LetterSectionComponent",
- "type": "component"
- },
- {
- "name": "LetterPreviewComponent",
+ "name": "LetterCanvasComponent",
"type": "component"
},
{
@@ -18299,239 +18981,13 @@
"type": "component"
}
],
- "description": "Organism: the whole letter — status badge, the editable sections (drafter) or a\nread-only preview (approver / locked), the diagnostics panel, and the action bar\nappropriate to status × permission. Presentational: emits edit + transition\nintents; the canEdit/canApprove/canReject/canSend inputs are server-computed\ndecision flags (PRD-0002 phase P1) — this component never derives them.
\n",
- "rawdescription": "\nOrganism: the whole letter — status badge, the editable sections (drafter) or a\nread-only preview (approver / locked), the diagnostics panel, and the action bar\nappropriate to status × permission. Presentational: emits edit + transition\nintents; the canEdit/canApprove/canReject/canSend inputs are server-computed\ndecision flags (PRD-0002 phase P1) — this component never derives them.",
+ "description": "Organism: the whole letter flow — status badge, the letter canvas (editable in\nplace for the drafter, read-only for approver/locked), the diagnostics panel, and\nthe action bar appropriate to status × permission. Presentational: emits edit +\ntransition intents; the canEdit/canApprove/canReject/canSend inputs are\nserver-computed decision flags (PRD-0002 phase P1) — this component never derives them.
\n",
+ "rawdescription": "\nOrganism: the whole letter flow — status badge, the letter canvas (editable in\nplace for the drafter, read-only for approver/locked), the diagnostics panel, and\nthe action bar appropriate to status × permission. Presentational: emits edit +\ntransition intents; the canEdit/canApprove/canReject/canSend inputs are\nserver-computed decision flags (PRD-0002 phase P1) — this component never derives them.",
"type": "component",
- "sourceCode": "import { Component, computed, input, output } from '@angular/core';\nimport { HeadingComponent } from '@shared/ui/heading/heading.component';\nimport { StatusBadgeComponent } from '@shared/ui/status-badge/status-badge.component';\nimport { ButtonComponent } from '@shared/ui/button/button.component';\nimport { AlertComponent } from '@shared/ui/alert/alert.component';\nimport { PlaceholderOption } from '@shared/ui/rich-text-editor/rich-text-editor.component';\nimport { Brief, LibraryPassage } from '@brief/domain/brief';\nimport { Diagnostic } from '@brief/domain/placeholders';\nimport { BriefMsg } from '@brief/domain/brief.machine';\nimport { LetterSectionComponent } from '@brief/ui/letter-section/letter-section.component';\nimport { LetterPreviewComponent } from '@brief/ui/letter-preview/letter-preview.component';\nimport { DiagnosticsPanelComponent } from '@brief/ui/diagnostics-panel/diagnostics-panel.component';\nimport { RejectionCommentsComponent } from '@brief/ui/rejection-comments/rejection-comments.component';\n\n/** Organism: the whole letter — status badge, the editable sections (drafter) or a\n read-only preview (approver / locked), the diagnostics panel, and the action bar\n appropriate to status × permission. Presentational: emits edit + transition\n intents; the canEdit/canApprove/canReject/canSend inputs are server-computed\n decision flags (PRD-0002 phase P1) — this component never derives them. */\n@Component({\n selector: 'app-letter-composer',\n imports: [\n HeadingComponent,\n StatusBadgeComponent,\n ButtonComponent,\n AlertComponent,\n LetterSectionComponent,\n LetterPreviewComponent,\n DiagnosticsPanelComponent,\n RejectionCommentsComponent,\n ],\n styles: [\n `\n :host {\n display: block;\n }\n .head {\n display: flex;\n justify-content: space-between;\n align-items: center;\n gap: var(--rhc-space-max-md);\n flex-wrap: wrap;\n margin-block-end: var(--rhc-space-max-lg);\n }\n .sections {\n display: grid;\n gap: var(--rhc-space-max-2xl);\n }\n .panel {\n margin-block: var(--rhc-space-max-xl);\n }\n .bar {\n display: flex;\n flex-wrap: wrap;\n gap: var(--rhc-space-max-md);\n align-items: center;\n margin-block-start: var(--rhc-space-max-xl);\n }\n `,\n ],\n template: `\n \n\n @if (status() === 'rejected') {\n \n }\n\n @if (canEdit()) {\n \n @for (section of brief().sections; track section.sectionKey) {\n
\n }\n
\n } @else {\n \n }\n\n \n\n \n @switch (status()) {\n @case ('draft') {\n @if (canEdit()) {\n
{{ submitLabel() }}\n @if (!canSubmit()) {\n
{{ submitHint() }}\n }\n }\n }\n @case ('rejected') {\n @if (canEdit()) {\n
{{ resubmitLabel() }}\n }\n }\n @case ('submitted') {\n @if (canApprove() || canReject()) {\n
{{\n approveLabel()\n }}\n
\n } @else {\n
{{ awaitingText() }}\n }\n }\n @case ('approved') {\n @if (canSend()) {\n
{{\n sendLabel()\n }}\n }\n }\n @case ('sent') {\n
{{ sentText() }}\n }\n }\n
\n `,\n})\nexport class LetterComposerComponent {\n brief = input.required();\n availablePassages = input([]);\n diagnostics = input([]);\n canEdit = input(false);\n canApprove = input(false);\n canReject = input(false);\n canSend = input(false);\n canSubmit = input(false);\n busy = input(false);\n\n edit = output();\n submit = output();\n approve = output();\n reject = output();\n send = output();\n locate = output();\n\n title = input($localize`:@@brief.title:Brief aan de zorgverlener`);\n submitLabel = input($localize`:@@brief.submit:Indienen ter beoordeling`);\n resubmitLabel = input($localize`:@@brief.resubmit:Opnieuw indienen`);\n submitHint = input(\n $localize`:@@brief.submitHint:Vul eerst alle verplichte secties en los fouten op.`,\n );\n approveLabel = input($localize`:@@brief.approve:Goedkeuren`);\n sendLabel = input($localize`:@@brief.send:Versturen`);\n awaitingText = input(\n $localize`:@@brief.awaiting:De brief wacht op beoordeling door een collega.`,\n );\n sentText = input($localize`:@@brief.sent:De brief is verzonden.`);\n\n protected status = computed(() => this.brief().status.tag);\n protected rejectComments = computed(() => {\n const s = this.brief().status;\n return s.tag === 'rejected' ? s.comments : '';\n });\n\n // The insert menu offers only valid, fillable, non-deprecated fields — inserting an\n // unknown/retired key is structurally impossible.\n protected menu = computed(() =>\n this.brief()\n .placeholders.filter((p) => p.fillable !== false && !p.deprecated)\n .map((p) => ({ key: p.key, label: p.label, autoResolvable: p.autoResolvable })),\n );\n\n protected statusLabel = computed(() => {\n switch (this.status()) {\n case 'draft':\n return $localize`:@@brief.status.draft:Concept`;\n case 'submitted':\n return $localize`:@@brief.status.submitted:Ter beoordeling`;\n case 'approved':\n return $localize`:@@brief.status.approved:Goedgekeurd`;\n case 'rejected':\n return $localize`:@@brief.status.rejected:Afgewezen`;\n case 'sent':\n return $localize`:@@brief.status.sent:Verzonden`;\n }\n });\n\n protected statusColor = computed(() => {\n switch (this.status()) {\n case 'draft':\n return 'var(--rhc-color-border-strong)';\n case 'submitted':\n return 'var(--rhc-color-oranje-500)';\n case 'approved':\n case 'sent':\n return 'var(--rhc-color-groen-500)';\n case 'rejected':\n return 'var(--rhc-color-rood-500)';\n }\n });\n}\n",
+ "sourceCode": "import { Component, computed, input, output } from '@angular/core';\nimport { HeadingComponent } from '@shared/ui/heading/heading.component';\nimport { StatusBadgeComponent } from '@shared/ui/status-badge/status-badge.component';\nimport { ButtonComponent } from '@shared/ui/button/button.component';\nimport { AlertComponent } from '@shared/ui/alert/alert.component';\nimport { PlaceholderOption } from '@shared/ui/rich-text-editor/rich-text-editor.component';\nimport { Brief, LibraryPassage } from '@brief/domain/brief';\nimport { OrgTemplate } from '@brief/domain/org-template';\nimport { Diagnostic } from '@brief/domain/placeholders';\nimport { BriefMsg } from '@brief/domain/brief.machine';\nimport { LetterCanvasComponent } from '@brief/ui/letter-canvas/letter-canvas.component';\nimport { DiagnosticsPanelComponent } from '@brief/ui/diagnostics-panel/diagnostics-panel.component';\nimport { RejectionCommentsComponent } from '@brief/ui/rejection-comments/rejection-comments.component';\n\n/** Organism: the whole letter flow — status badge, the letter canvas (editable in\n place for the drafter, read-only for approver/locked), the diagnostics panel, and\n the action bar appropriate to status × permission. Presentational: emits edit +\n transition intents; the canEdit/canApprove/canReject/canSend inputs are\n server-computed decision flags (PRD-0002 phase P1) — this component never derives them. */\n@Component({\n selector: 'app-letter-composer',\n imports: [\n HeadingComponent,\n StatusBadgeComponent,\n ButtonComponent,\n AlertComponent,\n LetterCanvasComponent,\n DiagnosticsPanelComponent,\n RejectionCommentsComponent,\n ],\n styles: [\n `\n :host {\n display: block;\n }\n .head {\n display: flex;\n justify-content: space-between;\n align-items: center;\n gap: var(--rhc-space-max-md);\n flex-wrap: wrap;\n margin-block-end: var(--rhc-space-max-lg);\n }\n .panel {\n margin-block: var(--rhc-space-max-xl);\n }\n .bar {\n display: flex;\n flex-wrap: wrap;\n gap: var(--rhc-space-max-md);\n align-items: center;\n margin-block-start: var(--rhc-space-max-xl);\n }\n `,\n ],\n template: `\n \n\n @if (status() === 'rejected') {\n \n }\n\n \n\n \n\n \n @switch (status()) {\n @case ('draft') {\n @if (canEdit()) {\n
{{ submitLabel() }}\n @if (!canSubmit()) {\n
{{ submitHint() }}\n }\n }\n }\n @case ('rejected') {\n @if (canEdit()) {\n
{{ resubmitLabel() }}\n }\n }\n @case ('submitted') {\n @if (canApprove() || canReject()) {\n
{{\n approveLabel()\n }}\n
\n } @else {\n
{{ awaitingText() }}\n }\n }\n @case ('approved') {\n @if (canSend()) {\n
{{\n sendLabel()\n }}\n }\n }\n @case ('sent') {\n
{{ sentText() }}\n }\n }\n
\n `,\n})\nexport class LetterComposerComponent {\n brief = input.required();\n orgTemplate = input.required();\n availablePassages = input([]);\n diagnostics = input([]);\n canEdit = input(false);\n canApprove = input(false);\n canReject = input(false);\n canSend = input(false);\n canSubmit = input(false);\n busy = input(false);\n\n edit = output();\n submit = output();\n approve = output();\n reject = output();\n send = output();\n locate = output();\n\n title = input($localize`:@@brief.title:Brief aan de zorgverlener`);\n submitLabel = input($localize`:@@brief.submit:Indienen ter beoordeling`);\n resubmitLabel = input($localize`:@@brief.resubmit:Opnieuw indienen`);\n submitHint = input(\n $localize`:@@brief.submitHint:Vul eerst alle verplichte secties en los fouten op.`,\n );\n approveLabel = input($localize`:@@brief.approve:Goedkeuren`);\n sendLabel = input($localize`:@@brief.send:Versturen`);\n awaitingText = input(\n $localize`:@@brief.awaiting:De brief wacht op beoordeling door een collega.`,\n );\n sentText = input($localize`:@@brief.sent:De brief is verzonden.`);\n\n protected status = computed(() => this.brief().status.tag);\n protected rejectComments = computed(() => {\n const s = this.brief().status;\n return s.tag === 'rejected' ? s.comments : '';\n });\n\n // The insert menu offers only valid, fillable, non-deprecated fields — inserting an\n // unknown/retired key is structurally impossible.\n protected menu = computed(() =>\n this.brief()\n .placeholders.filter((p) => p.fillable !== false && !p.deprecated)\n .map((p) => ({ key: p.key, label: p.label, autoResolvable: p.autoResolvable })),\n );\n\n protected statusLabel = computed(() => {\n switch (this.status()) {\n case 'draft':\n return $localize`:@@brief.status.draft:Concept`;\n case 'submitted':\n return $localize`:@@brief.status.submitted:Ter beoordeling`;\n case 'approved':\n return $localize`:@@brief.status.approved:Goedgekeurd`;\n case 'rejected':\n return $localize`:@@brief.status.rejected:Afgewezen`;\n case 'sent':\n return $localize`:@@brief.status.sent:Verzonden`;\n }\n });\n\n protected statusColor = computed(() => {\n switch (this.status()) {\n case 'draft':\n return 'var(--rhc-color-border-strong)';\n case 'submitted':\n return 'var(--rhc-color-oranje-500)';\n case 'approved':\n case 'sent':\n return 'var(--rhc-color-groen-500)';\n case 'rejected':\n return 'var(--rhc-color-rood-500)';\n }\n });\n}\n",
"assetsDirs": [],
"styleUrlsData": "",
- "stylesData": "\n :host {\n display: block;\n }\n .head {\n display: flex;\n justify-content: space-between;\n align-items: center;\n gap: var(--rhc-space-max-md);\n flex-wrap: wrap;\n margin-block-end: var(--rhc-space-max-lg);\n }\n .sections {\n display: grid;\n gap: var(--rhc-space-max-2xl);\n }\n .panel {\n margin-block: var(--rhc-space-max-xl);\n }\n .bar {\n display: flex;\n flex-wrap: wrap;\n gap: var(--rhc-space-max-md);\n align-items: center;\n margin-block-start: var(--rhc-space-max-xl);\n }\n \n",
- "extends": []
- },
- {
- "name": "LetterPreviewComponent",
- "id": "component-LetterPreviewComponent-23860a46c81be5b99588ec2bf5211cffe7bd31c5deefdca348916d0c85837834280c1a028f2efdebdedfbe60607091984e966c02d8a428ce089295dcc194a566",
- "file": "src/app/brief/ui/letter-preview/letter-preview.component.ts",
- "encapsulation": [],
- "entryComponents": [],
- "inputs": [],
- "outputs": [],
- "providers": [],
- "selector": "app-letter-preview",
- "styleUrls": [],
- "styles": [
- "\n :host {\n display: block;\n }\n .toolbar {\n display: flex;\n justify-content: flex-end;\n margin-block-end: var(--rhc-space-max-sm);\n }\n .letter {\n background: var(--rhc-color-wit);\n border: var(--rhc-border-width-sm) solid var(--rhc-color-border-default);\n border-radius: var(--rhc-border-radius-md);\n padding: var(--rhc-space-max-2xl);\n }\n section {\n margin-block-end: var(--rhc-space-max-xl);\n }\n p {\n margin: 0 0 var(--rhc-space-max-sm);\n }\n ul,\n ol {\n margin: 0 0 var(--rhc-space-max-sm);\n padding-inline-start: 1.4em;\n }\n "
- ],
- "template": "\n @for (node of nodes; track $index) {\n @switch (node.type) {\n @case ('text') {\n {{ node.text }}\n }\n @case ('lineBreak') {\n
\n }\n @case ('placeholder') {\n @if (showSample() && autoFor(node.key)) {\n {{ sampleFor(node.key) }}\n } @else {\n \n }\n }\n }\n }\n\n\n\n\n\n @for (section of brief().sections; track section.sectionKey) {\n
\n {{ section.title }}\n @for (block of section.blocks; track block.blockId) {\n @for (seg of segmentsOf(block); track $index) {\n @if (seg.list === 'bullet') {\n \n @for (para of seg.items; track $index) {\n - \n \n
\n }\n
\n } @else if (seg.list === 'number') {\n \n @for (para of seg.items; track $index) {\n - \n \n
\n }\n
\n } @else {\n \n \n
\n }\n }\n }\n \n }\n
\n",
- "templateUrl": [],
- "viewProviders": [],
- "hostDirectives": [],
- "inputsClass": [
- {
- "name": "brief",
- "deprecated": false,
- "deprecationMessage": "",
- "type": "Brief",
- "indexKey": "",
- "optional": false,
- "description": "",
- "line": 149,
- "required": true
- },
- {
- "name": "diagnostics",
- "defaultValue": "[]",
- "deprecated": false,
- "deprecationMessage": "",
- "type": "readonly Diagnostic[]",
- "indexKey": "",
- "optional": false,
- "description": "",
- "line": 150,
- "required": false
- },
- {
- "name": "hideSampleLabel",
- "defaultValue": "$localize`:@@brief.preview.hideSample:Testwaarden verbergen`",
- "deprecated": false,
- "deprecationMessage": "",
- "indexKey": "",
- "optional": false,
- "description": "",
- "line": 153,
- "required": false
- },
- {
- "name": "showSampleLabel",
- "defaultValue": "$localize`:@@brief.preview.showSample:Voorbeeld met testwaarden`",
- "deprecated": false,
- "deprecationMessage": "",
- "indexKey": "",
- "optional": false,
- "description": "",
- "line": 152,
- "required": false
- }
- ],
- "outputsClass": [],
- "propertiesClass": [
- {
- "name": "autoFor",
- "defaultValue": "() => {...}",
- "deprecated": false,
- "deprecationMessage": "",
- "type": "unknown",
- "indexKey": "",
- "optional": false,
- "description": "",
- "line": 171,
- "modifierKind": [
- 124
- ]
- },
- {
- "name": "defs",
- "defaultValue": "computed(() => new Map(this.brief().placeholders.map((p) => [p.key, p])))",
- "deprecated": false,
- "deprecationMessage": "",
- "type": "unknown",
- "indexKey": "",
- "optional": false,
- "description": "",
- "line": 158,
- "modifierKind": [
- 123
- ]
- },
- {
- "name": "labelFor",
- "defaultValue": "() => {...}",
- "deprecated": false,
- "deprecationMessage": "",
- "type": "unknown",
- "indexKey": "",
- "optional": false,
- "description": "",
- "line": 170,
- "modifierKind": [
- 124
- ]
- },
- {
- "name": "sampleFor",
- "defaultValue": "() => {...}",
- "deprecated": false,
- "deprecationMessage": "",
- "type": "unknown",
- "indexKey": "",
- "optional": false,
- "description": "",
- "line": 173,
- "modifierKind": [
- 124
- ]
- },
- {
- "name": "segmentsOf",
- "defaultValue": "() => {...}",
- "deprecated": false,
- "deprecationMessage": "",
- "type": "unknown",
- "indexKey": "",
- "optional": false,
- "description": "",
- "line": 169,
- "modifierKind": [
- 124
- ]
- },
- {
- "name": "showSample",
- "defaultValue": "signal(false)",
- "deprecated": false,
- "deprecationMessage": "",
- "type": "unknown",
- "indexKey": "",
- "optional": false,
- "description": "",
- "line": 155,
- "modifierKind": [
- 124
- ]
- },
- {
- "name": "stateFor",
- "defaultValue": "() => {...}",
- "deprecated": false,
- "deprecationMessage": "",
- "type": "unknown",
- "indexKey": "",
- "optional": false,
- "description": "",
- "line": 172,
- "modifierKind": [
- 124
- ]
- },
- {
- "name": "today",
- "defaultValue": "formatDatumNl(new Date())",
- "deprecated": false,
- "deprecationMessage": "",
- "type": "unknown",
- "indexKey": "",
- "optional": false,
- "description": "",
- "line": 156,
- "modifierKind": [
- 123
- ]
- },
- {
- "name": "worst",
- "defaultValue": "computed(() => {\n const m = new Map();\n for (const d of this.diagnostics()) {\n if (!d.placeholderKey) continue;\n if (d.severity === 'error') m.set(d.placeholderKey, 'error');\n else if (!m.has(d.placeholderKey)) m.set(d.placeholderKey, 'warning');\n }\n return m;\n })",
- "deprecated": false,
- "deprecationMessage": "",
- "type": "unknown",
- "indexKey": "",
- "optional": false,
- "description": "",
- "line": 159,
- "modifierKind": [
- 123
- ]
- }
- ],
- "methodsClass": [],
- "deprecated": false,
- "deprecationMessage": "",
- "hostBindings": [],
- "hostListeners": [],
- "standalone": false,
- "imports": [
- {
- "name": "NgTemplateOutlet"
- },
- {
- "name": "HeadingComponent",
- "type": "component"
- },
- {
- "name": "ButtonComponent",
- "type": "component"
- },
- {
- "name": "PlaceholderChipComponent",
- "type": "component"
- }
- ],
- "description": "Organism: read-only rendering of the letter as the approver/recipient sees it.\nPlaceholders show as labelled chips (values are resolved server-side only at send);\na chip flagged by the linter shows its error/warning state. The "Voorbeeld" toggle\nfills auto-resolvable placeholders with sample values to preview the sent result.
\n",
- "rawdescription": "\nOrganism: read-only rendering of the letter as the approver/recipient sees it.\nPlaceholders show as labelled chips (values are resolved server-side only at send);\na chip flagged by the linter shows its error/warning state. The \"Voorbeeld\" toggle\nfills auto-resolvable placeholders with sample values to preview the sent result.",
- "type": "component",
- "sourceCode": "import { Component, computed, input, signal } from '@angular/core';\nimport { NgTemplateOutlet } from '@angular/common';\nimport { HeadingComponent } from '@shared/ui/heading/heading.component';\nimport { ButtonComponent } from '@shared/ui/button/button.component';\nimport { PlaceholderChipComponent } from '@shared/ui/placeholder-chip/placeholder-chip.component';\nimport { formatDatumNl } from '@shared/kernel/datum';\nimport { Paragraph } from '@shared/kernel/rich-text';\nimport { Brief, LetterBlock } from '@brief/domain/brief';\nimport { Diagnostic } from '@brief/domain/placeholders';\n\n/** A run of consecutive lines to render together: a list (bullet/number) or a single plain line. */\ntype PreviewSegment = {\n readonly list: 'bullet' | 'number' | null;\n readonly items: readonly Paragraph[];\n};\n\nfunction groupParagraphs(paras: readonly Paragraph[]): PreviewSegment[] {\n const out: { list: 'bullet' | 'number' | null; items: Paragraph[] }[] = [];\n for (const para of paras) {\n const kind = para.list ?? null;\n const last = out[out.length - 1];\n if (kind && last && last.list === kind) last.items.push(para);\n else out.push({ list: kind, items: [para] });\n }\n return out;\n}\n\n// Illustrative values for the \"Voorbeeld\" toggle — what send resolves server-side.\nconst SAMPLE_VALUES: Record = {\n naam_zorgverlener: 'J. Jansen',\n big_nummer: '12345678901',\n};\n\n/** Organism: read-only rendering of the letter as the approver/recipient sees it.\n Placeholders show as labelled chips (values are resolved server-side only at send);\n a chip flagged by the linter shows its error/warning state. The \"Voorbeeld\" toggle\n fills auto-resolvable placeholders with sample values to preview the sent result. */\n@Component({\n selector: 'app-letter-preview',\n imports: [NgTemplateOutlet, HeadingComponent, ButtonComponent, PlaceholderChipComponent],\n styles: [\n `\n :host {\n display: block;\n }\n .toolbar {\n display: flex;\n justify-content: flex-end;\n margin-block-end: var(--rhc-space-max-sm);\n }\n .letter {\n background: var(--rhc-color-wit);\n border: var(--rhc-border-width-sm) solid var(--rhc-color-border-default);\n border-radius: var(--rhc-border-radius-md);\n padding: var(--rhc-space-max-2xl);\n }\n section {\n margin-block-end: var(--rhc-space-max-xl);\n }\n p {\n margin: 0 0 var(--rhc-space-max-sm);\n }\n ul,\n ol {\n margin: 0 0 var(--rhc-space-max-sm);\n padding-inline-start: 1.4em;\n }\n `,\n ],\n template: `\n \n @for (node of nodes; track $index) {\n @switch (node.type) {\n @case ('text') {\n {{ node.text }}\n }\n @case ('lineBreak') {\n
\n }\n @case ('placeholder') {\n @if (showSample() && autoFor(node.key)) {\n {{ sampleFor(node.key) }}\n } @else {\n \n }\n }\n }\n }\n \n\n \n\n \n @for (section of brief().sections; track section.sectionKey) {\n
\n {{ section.title }}\n @for (block of section.blocks; track block.blockId) {\n @for (seg of segmentsOf(block); track $index) {\n @if (seg.list === 'bullet') {\n \n @for (para of seg.items; track $index) {\n - \n \n
\n }\n
\n } @else if (seg.list === 'number') {\n \n @for (para of seg.items; track $index) {\n - \n \n
\n }\n
\n } @else {\n \n \n
\n }\n }\n }\n \n }\n
\n `,\n})\nexport class LetterPreviewComponent {\n brief = input.required();\n diagnostics = input([]);\n\n showSampleLabel = input($localize`:@@brief.preview.showSample:Voorbeeld met testwaarden`);\n hideSampleLabel = input($localize`:@@brief.preview.hideSample:Testwaarden verbergen`);\n\n protected showSample = signal(false);\n private today = formatDatumNl(new Date());\n\n private defs = computed(() => new Map(this.brief().placeholders.map((p) => [p.key, p])));\n private worst = computed(() => {\n const m = new Map();\n for (const d of this.diagnostics()) {\n if (!d.placeholderKey) continue;\n if (d.severity === 'error') m.set(d.placeholderKey, 'error');\n else if (!m.has(d.placeholderKey)) m.set(d.placeholderKey, 'warning');\n }\n return m;\n });\n\n protected segmentsOf = (block: LetterBlock) => groupParagraphs(block.content.paragraphs);\n protected labelFor = (key: string) => this.defs().get(key)?.label ?? key;\n protected autoFor = (key: string) => this.defs().get(key)?.autoResolvable ?? false;\n protected stateFor = (key: string): 'ok' | 'warning' | 'error' => this.worst().get(key) ?? 'ok';\n protected sampleFor = (key: string) =>\n SAMPLE_VALUES[key] ?? (key === 'datum' ? this.today : this.labelFor(key));\n}\n",
- "assetsDirs": [],
- "styleUrlsData": "",
- "stylesData": "\n :host {\n display: block;\n }\n .toolbar {\n display: flex;\n justify-content: flex-end;\n margin-block-end: var(--rhc-space-max-sm);\n }\n .letter {\n background: var(--rhc-color-wit);\n border: var(--rhc-border-width-sm) solid var(--rhc-color-border-default);\n border-radius: var(--rhc-border-radius-md);\n padding: var(--rhc-space-max-2xl);\n }\n section {\n margin-block-end: var(--rhc-space-max-xl);\n }\n p {\n margin: 0 0 var(--rhc-space-max-sm);\n }\n ul,\n ol {\n margin: 0 0 var(--rhc-space-max-sm);\n padding-inline-start: 1.4em;\n }\n \n",
+ "stylesData": "\n :host {\n display: block;\n }\n .head {\n display: flex;\n justify-content: space-between;\n align-items: center;\n gap: var(--rhc-space-max-md);\n flex-wrap: wrap;\n margin-block-end: var(--rhc-space-max-lg);\n }\n .panel {\n margin-block: var(--rhc-space-max-xl);\n }\n .bar {\n display: flex;\n flex-wrap: wrap;\n gap: var(--rhc-space-max-md);\n align-items: center;\n margin-block-start: var(--rhc-space-max-xl);\n }\n \n",
"extends": []
},
{
@@ -19231,7 +19687,7 @@
},
{
"name": "PassagePickerComponent",
- "id": "component-PassagePickerComponent-bf0a92f03c5c7ff5c973536344d7f585ee91a5aa94f4eb65ae81c6d4b445bbb83f04787d7b2484baa0838bc89d152e8ff67e1d7238c4dfb84e23c2bb7cb368bb",
+ "id": "component-PassagePickerComponent-61e92246ba5329367274c46fd9bbf6201d22bfb6762d6c1f81b88ad0a1f1f1c4489556c629501a39b08f8cb2a31bef7077a8cea8aa42d18be93e7e3a77efcfa5",
"file": "src/app/brief/ui/passage-picker/passage-picker.component.ts",
"encapsulation": [],
"entryComponents": [],
@@ -19241,9 +19697,9 @@
"selector": "app-passage-picker",
"styleUrls": [],
"styles": [
- "\n :host {\n display: block;\n border: var(--rhc-border-width-sm) solid var(--rhc-color-border-default);\n border-radius: var(--rhc-border-radius-md);\n padding: var(--rhc-space-max-md);\n }\n ul {\n list-style: none;\n margin: 0 0 var(--rhc-space-max-md);\n padding: 0;\n display: grid;\n gap: var(--rhc-space-max-sm);\n }\n .scope {\n color: var(--rhc-color-foreground-subtle);\n }\n "
+ "\n :host {\n display: block;\n background: var(--rhc-color-wit, #fff);\n border: var(--rhc-border-width-sm) solid var(--rhc-color-border-default);\n border-radius: var(--rhc-border-radius-md);\n padding: var(--rhc-space-max-md);\n }\n ul {\n list-style: none;\n margin: 0 0 var(--rhc-space-max-md);\n padding: 0;\n display: grid;\n gap: var(--rhc-space-max-sm);\n }\n .scope {\n color: var(--rhc-color-foreground-subtle);\n }\n "
],
- "template": "\n @for (p of passages(); track p.passageId) {\n - \n \n · {{ p.scope === 'beroep' ? beroepLabel() : globalLabel() }}\n
\n }\n
\n{{ addLabel() }} ({{ count() }})\n",
+ "template": "\n @for (p of passages(); track p.passageId) {\n - \n \n · {{ p.scope === 'beroep' ? beroepLabel() : globalLabel() }}\n
\n }\n
\n{{ addLabel() }} ({{ count() }})\n",
"templateUrl": [],
"viewProviders": [],
"hostDirectives": [],
@@ -19256,7 +19712,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 55,
+ "line": 57,
"required": false
},
{
@@ -19267,7 +19723,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 57,
+ "line": 59,
"required": false
},
{
@@ -19278,7 +19734,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 56,
+ "line": 58,
"required": false
},
{
@@ -19289,7 +19745,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 52,
+ "line": 54,
"required": true
}
],
@@ -19302,7 +19758,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 53,
+ "line": 55,
"required": false
}
],
@@ -19316,7 +19772,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 59,
+ "line": 61,
"modifierKind": [
124
]
@@ -19330,7 +19786,7 @@
"indexKey": "",
"optional": false,
"description": "",
- "line": 60,
+ "line": 62,
"modifierKind": [
124
]
@@ -19343,7 +19799,7 @@
"optional": false,
"returnType": "void",
"typeParameters": [],
- "line": 66,
+ "line": 68,
"deprecated": false,
"deprecationMessage": "",
"modifierKind": [
@@ -19373,7 +19829,7 @@
"optional": false,
"returnType": "void",
"typeParameters": [],
- "line": 62,
+ "line": 64,
"deprecated": false,
"deprecationMessage": "",
"modifierKind": [
@@ -19427,10 +19883,10 @@
"description": "Molecule: multi-select list of the section's library passages. One "Voeg toe"\ninserts ALL checked passages at once (a single message upstream) — there is no\nsingle-insert path. Presentational: emits the chosen passages in list order.
\n",
"rawdescription": "\nMolecule: multi-select list of the section's library passages. One \"Voeg toe\"\ninserts ALL checked passages at once (a single message upstream) — there is no\nsingle-insert path. Presentational: emits the chosen passages in list order.",
"type": "component",
- "sourceCode": "import { Component, input, output, signal } from '@angular/core';\nimport { FormsModule } from '@angular/forms';\nimport { CheckboxComponent } from '@shared/ui/checkbox/checkbox.component';\nimport { ButtonComponent } from '@shared/ui/button/button.component';\nimport { LibraryPassage } from '@brief/domain/brief';\n\n/** Molecule: multi-select list of the section's library passages. One \"Voeg toe\"\n inserts ALL checked passages at once (a single message upstream) — there is no\n single-insert path. Presentational: emits the chosen passages in list order. */\n@Component({\n selector: 'app-passage-picker',\n imports: [FormsModule, CheckboxComponent, ButtonComponent],\n styles: [\n `\n :host {\n display: block;\n border: var(--rhc-border-width-sm) solid var(--rhc-color-border-default);\n border-radius: var(--rhc-border-radius-md);\n padding: var(--rhc-space-max-md);\n }\n ul {\n list-style: none;\n margin: 0 0 var(--rhc-space-max-md);\n padding: 0;\n display: grid;\n gap: var(--rhc-space-max-sm);\n }\n .scope {\n color: var(--rhc-color-foreground-subtle);\n }\n `,\n ],\n template: `\n \n @for (p of passages(); track p.passageId) {\n - \n \n · {{ p.scope === 'beroep' ? beroepLabel() : globalLabel() }}\n
\n }\n
\n {{ addLabel() }} ({{ count() }})\n `,\n})\nexport class PassagePickerComponent {\n passages = input.required();\n insert = output();\n\n addLabel = input($localize`:@@brief.picker.add:Voeg toe`);\n globalLabel = input($localize`:@@brief.picker.global:algemeen`);\n beroepLabel = input($localize`:@@brief.picker.beroep:beroepsspecifiek`);\n\n protected checked = signal>({});\n protected count = () => Object.values(this.checked()).filter(Boolean).length;\n\n protected set(id: string, on: boolean) {\n this.checked.update((c) => ({ ...c, [id]: on }));\n }\n\n protected add() {\n const chosen = this.passages().filter((p) => this.checked()[p.passageId]);\n if (chosen.length) {\n this.insert.emit(chosen);\n this.checked.set({});\n }\n }\n}\n",
+ "sourceCode": "import { Component, input, output, signal } from '@angular/core';\nimport { FormsModule } from '@angular/forms';\nimport { CheckboxComponent } from '@shared/ui/checkbox/checkbox.component';\nimport { ButtonComponent } from '@shared/ui/button/button.component';\nimport { LibraryPassage } from '@brief/domain/brief';\n\n/** Molecule: multi-select list of the section's library passages. One \"Voeg toe\"\n inserts ALL checked passages at once (a single message upstream) — there is no\n single-insert path. Presentational: emits the chosen passages in list order. */\n@Component({\n selector: 'app-passage-picker',\n imports: [FormsModule, CheckboxComponent, ButtonComponent],\n styles: [\n `\n :host {\n display: block;\n background: var(--rhc-color-wit, #fff);\n border: var(--rhc-border-width-sm) solid var(--rhc-color-border-default);\n border-radius: var(--rhc-border-radius-md);\n padding: var(--rhc-space-max-md);\n }\n ul {\n list-style: none;\n margin: 0 0 var(--rhc-space-max-md);\n padding: 0;\n display: grid;\n gap: var(--rhc-space-max-sm);\n }\n .scope {\n color: var(--rhc-color-foreground-subtle);\n }\n `,\n ],\n template: `\n \n @for (p of passages(); track p.passageId) {\n - \n \n · {{ p.scope === 'beroep' ? beroepLabel() : globalLabel() }}\n
\n }\n
\n {{ addLabel() }} ({{ count() }})\n `,\n})\nexport class PassagePickerComponent {\n passages = input.required();\n insert = output();\n\n addLabel = input($localize`:@@brief.picker.add:Voeg toe`);\n globalLabel = input($localize`:@@brief.picker.global:algemeen`);\n beroepLabel = input($localize`:@@brief.picker.beroep:beroepsspecifiek`);\n\n protected checked = signal>({});\n protected count = () => Object.values(this.checked()).filter(Boolean).length;\n\n protected set(id: string, on: boolean) {\n this.checked.update((c) => ({ ...c, [id]: on }));\n }\n\n protected add() {\n const chosen = this.passages().filter((p) => this.checked()[p.passageId]);\n if (chosen.length) {\n this.insert.emit(chosen);\n this.checked.set({});\n }\n }\n}\n",
"assetsDirs": [],
"styleUrlsData": "",
- "stylesData": "\n :host {\n display: block;\n border: var(--rhc-border-width-sm) solid var(--rhc-color-border-default);\n border-radius: var(--rhc-border-radius-md);\n padding: var(--rhc-space-max-md);\n }\n ul {\n list-style: none;\n margin: 0 0 var(--rhc-space-max-md);\n padding: 0;\n display: grid;\n gap: var(--rhc-space-max-sm);\n }\n .scope {\n color: var(--rhc-color-foreground-subtle);\n }\n \n",
+ "stylesData": "\n :host {\n display: block;\n background: var(--rhc-color-wit, #fff);\n border: var(--rhc-border-width-sm) solid var(--rhc-color-border-default);\n border-radius: var(--rhc-border-radius-md);\n padding: var(--rhc-space-max-md);\n }\n ul {\n list-style: none;\n margin: 0 0 var(--rhc-space-max-md);\n padding: 0;\n display: grid;\n gap: var(--rhc-space-max-sm);\n }\n .scope {\n color: var(--rhc-color-foreground-subtle);\n }\n \n",
"extends": []
},
{
@@ -23620,6 +24076,18 @@
"modules": [],
"miscellaneous": {
"variables": [
+ {
+ "name": "A4_HEIGHT_PX",
+ "ctype": "miscellaneous",
+ "subtype": "variable",
+ "file": "src/app/brief/ui/letter-canvas/letter-canvas.component.ts",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "unknown",
+ "defaultValue": "(297 * 96) / 25.4",
+ "rawdescription": "A4 height in CSS px (1in = 96px = 25.4mm) — for the approximate page-break marks.",
+ "description": "A4 height in CSS px (1in = 96px = 25.4mm) — for the approximate page-break marks.
\n"
+ },
{
"name": "AANTEKENING_TYPES",
"ctype": "miscellaneous",
@@ -23830,26 +24298,6 @@
"type": "BriefState",
"defaultValue": "{ tag: 'loading' }"
},
- {
- "name": "initial",
- "ctype": "miscellaneous",
- "subtype": "variable",
- "file": "src/app/registratie/domain/change-request.machine.ts",
- "deprecated": false,
- "deprecationMessage": "",
- "type": "ChangeRequestState",
- "defaultValue": "{\n tag: 'Editing',\n draft: { straat: '', postcode: '', woonplaats: '' },\n errors: {},\n}"
- },
- {
- "name": "initial",
- "ctype": "miscellaneous",
- "subtype": "variable",
- "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
- "deprecated": false,
- "deprecationMessage": "",
- "type": "RegistratieState",
- "defaultValue": "{\n tag: 'Invullen',\n draft: emptyDraft,\n cursor: 0,\n errors: {},\n upload: initialUpload,\n}"
- },
{
"name": "initial",
"ctype": "miscellaneous",
@@ -23870,6 +24318,26 @@
"type": "IntakeState",
"defaultValue": "{\n tag: 'Answering',\n answers: {},\n cursor: 0,\n errors: {},\n scholingThreshold: SCHOLING_THRESHOLD_DEFAULT,\n}"
},
+ {
+ "name": "initial",
+ "ctype": "miscellaneous",
+ "subtype": "variable",
+ "file": "src/app/registratie/domain/change-request.machine.ts",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "ChangeRequestState",
+ "defaultValue": "{\n tag: 'Editing',\n draft: { straat: '', postcode: '', woonplaats: '' },\n errors: {},\n}"
+ },
+ {
+ "name": "initial",
+ "ctype": "miscellaneous",
+ "subtype": "variable",
+ "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "RegistratieState",
+ "defaultValue": "{\n tag: 'Invullen',\n draft: emptyDraft,\n cursor: 0,\n errors: {},\n upload: initialUpload,\n}"
+ },
{
"name": "initialUpload",
"ctype": "miscellaneous",
@@ -24117,7 +24585,7 @@
"name": "SAMPLE_VALUES",
"ctype": "miscellaneous",
"subtype": "variable",
- "file": "src/app/brief/ui/letter-preview/letter-preview.component.ts",
+ "file": "src/app/brief/ui/letter-canvas/letter-canvas.component.ts",
"deprecated": false,
"deprecationMessage": "",
"type": "Record",
@@ -24171,11 +24639,11 @@
"name": "STEPS",
"ctype": "miscellaneous",
"subtype": "variable",
- "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
+ "file": "src/app/herregistratie/domain/intake.machine.ts",
"deprecated": false,
"deprecationMessage": "",
"type": "StepId[]",
- "defaultValue": "['adres', 'beroep', 'controle']",
+ "defaultValue": "['buitenland', 'werk', 'review']",
"rawdescription": "The fixed step list. Number of steps never changes; questions reveal inline.",
"description": "The fixed step list. Number of steps never changes; questions reveal inline.
\n"
},
@@ -24183,11 +24651,11 @@
"name": "STEPS",
"ctype": "miscellaneous",
"subtype": "variable",
- "file": "src/app/herregistratie/domain/intake.machine.ts",
+ "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
"deprecated": false,
"deprecationMessage": "",
"type": "StepId[]",
- "defaultValue": "['buitenland', 'werk', 'review']",
+ "defaultValue": "['adres', 'beroep', 'controle']",
"rawdescription": "The fixed step list. Number of steps never changes; questions reveal inline.",
"description": "The fixed step list. Number of steps never changes; questions reveal inline.
\n"
},
@@ -24499,35 +24967,6 @@
}
]
},
- {
- "name": "back",
- "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
- "ctype": "miscellaneous",
- "subtype": "function",
- "deprecated": false,
- "deprecationMessage": "",
- "description": "",
- "args": [
- {
- "name": "s",
- "type": "RegistratieState",
- "deprecated": false,
- "deprecationMessage": ""
- }
- ],
- "returnType": "RegistratieState",
- "jsdoctags": [
- {
- "name": "s",
- "type": "RegistratieState",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- }
- ]
- },
{
"name": "back",
"file": "src/app/herregistratie/domain/herregistratie.machine.ts",
@@ -24586,6 +25025,35 @@
}
]
},
+ {
+ "name": "back",
+ "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
+ "ctype": "miscellaneous",
+ "subtype": "function",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "description": "",
+ "args": [
+ {
+ "name": "s",
+ "type": "RegistratieState",
+ "deprecated": false,
+ "deprecationMessage": ""
+ }
+ ],
+ "returnType": "RegistratieState",
+ "jsdoctags": [
+ {
+ "name": "s",
+ "type": "RegistratieState",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ }
+ ]
+ },
{
"name": "blockActions",
"file": "src/app/registratie/domain/block-actions.ts",
@@ -25076,7 +25544,7 @@
},
{
"name": "currentStep",
- "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
+ "file": "src/app/herregistratie/domain/intake.machine.ts",
"ctype": "miscellaneous",
"subtype": "function",
"deprecated": false,
@@ -25105,7 +25573,7 @@
},
{
"name": "currentStep",
- "file": "src/app/herregistratie/domain/intake.machine.ts",
+ "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
"ctype": "miscellaneous",
"subtype": "function",
"deprecated": false,
@@ -25657,50 +26125,6 @@
}
]
},
- {
- "name": "gaNaarStap",
- "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
- "ctype": "miscellaneous",
- "subtype": "function",
- "deprecated": false,
- "deprecationMessage": "",
- "description": "Jump back to an earlier step to correct data (controle → step N). Forward\njumps are not allowed (would skip validation). Preserves the draft.
\n",
- "args": [
- {
- "name": "s",
- "type": "RegistratieState",
- "deprecated": false,
- "deprecationMessage": ""
- },
- {
- "name": "cursor",
- "type": "number",
- "deprecated": false,
- "deprecationMessage": ""
- }
- ],
- "returnType": "RegistratieState",
- "jsdoctags": [
- {
- "name": "s",
- "type": "RegistratieState",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- },
- {
- "name": "cursor",
- "type": "number",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- }
- ]
- },
{
"name": "gaNaarStap",
"file": "src/app/herregistratie/domain/herregistratie.machine.ts",
@@ -25787,9 +26211,53 @@
}
]
},
+ {
+ "name": "gaNaarStap",
+ "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
+ "ctype": "miscellaneous",
+ "subtype": "function",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "description": "Jump back to an earlier step to correct data (controle → step N). Forward\njumps are not allowed (would skip validation). Preserves the draft.
\n",
+ "args": [
+ {
+ "name": "s",
+ "type": "RegistratieState",
+ "deprecated": false,
+ "deprecationMessage": ""
+ },
+ {
+ "name": "cursor",
+ "type": "number",
+ "deprecated": false,
+ "deprecationMessage": ""
+ }
+ ],
+ "returnType": "RegistratieState",
+ "jsdoctags": [
+ {
+ "name": "s",
+ "type": "RegistratieState",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ },
+ {
+ "name": "cursor",
+ "type": "number",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ }
+ ]
+ },
{
"name": "groupParagraphs",
- "file": "src/app/brief/ui/letter-preview/letter-preview.component.ts",
+ "file": "src/app/brief/ui/letter-canvas/letter-canvas.component.ts",
"ctype": "miscellaneous",
"subtype": "function",
"deprecated": false,
@@ -25841,35 +26309,6 @@
}
]
},
- {
- "name": "hasProgress",
- "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
- "ctype": "miscellaneous",
- "subtype": "function",
- "deprecated": false,
- "deprecationMessage": "",
- "description": "Has the user meaningfully started, so it's worth persisting as a Concept? Excludes\nthe automatic BRP address prefill on step 0 — a bare page visit creates nothing.\nponytail: an address typed at step 0 without any of these signals is not yet\npersisted (created once they advance/choose); accepted regression vs. sessionStorage.
\n",
- "args": [
- {
- "name": "s",
- "type": "Extract",
- "deprecated": false,
- "deprecationMessage": ""
- }
- ],
- "returnType": "boolean",
- "jsdoctags": [
- {
- "name": "s",
- "type": "Extract",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- }
- ]
- },
{
"name": "hasProgress",
"file": "src/app/herregistratie/domain/herregistratie.machine.ts",
@@ -25928,6 +26367,35 @@
}
]
},
+ {
+ "name": "hasProgress",
+ "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
+ "ctype": "miscellaneous",
+ "subtype": "function",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "description": "Has the user meaningfully started, so it's worth persisting as a Concept? Excludes\nthe automatic BRP address prefill on step 0 — a bare page visit creates nothing.\nponytail: an address typed at step 0 without any of these signals is not yet\npersisted (created once they advance/choose); accepted regression vs. sessionStorage.
\n",
+ "args": [
+ {
+ "name": "s",
+ "type": "Extract",
+ "deprecated": false,
+ "deprecationMessage": ""
+ }
+ ],
+ "returnType": "boolean",
+ "jsdoctags": [
+ {
+ "name": "s",
+ "type": "Extract",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ }
+ ]
+ },
{
"name": "herregistratieDeadline",
"file": "src/app/registratie/domain/registration.policy.ts",
@@ -26990,35 +27458,6 @@
}
]
},
- {
- "name": "next",
- "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
- "ctype": "miscellaneous",
- "subtype": "function",
- "deprecated": false,
- "deprecationMessage": "",
- "description": "",
- "args": [
- {
- "name": "s",
- "type": "RegistratieState",
- "deprecated": false,
- "deprecationMessage": ""
- }
- ],
- "returnType": "RegistratieState",
- "jsdoctags": [
- {
- "name": "s",
- "type": "RegistratieState",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- }
- ]
- },
{
"name": "next",
"file": "src/app/herregistratie/domain/herregistratie.machine.ts",
@@ -27077,6 +27516,35 @@
}
]
},
+ {
+ "name": "next",
+ "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
+ "ctype": "miscellaneous",
+ "subtype": "function",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "description": "",
+ "args": [
+ {
+ "name": "s",
+ "type": "RegistratieState",
+ "deprecated": false,
+ "deprecationMessage": ""
+ }
+ ],
+ "returnType": "RegistratieState",
+ "jsdoctags": [
+ {
+ "name": "s",
+ "type": "RegistratieState",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ }
+ ]
+ },
{
"name": "nextLocalIndex",
"file": "src/app/brief/domain/brief.machine.ts",
@@ -27664,6 +28132,33 @@
}
]
},
+ {
+ "name": "parseOrgTemplate",
+ "file": "src/app/brief/infrastructure/brief.adapter.ts",
+ "ctype": "miscellaneous",
+ "subtype": "function",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "description": "",
+ "args": [
+ {
+ "name": "dto",
+ "deprecated": false,
+ "deprecationMessage": ""
+ }
+ ],
+ "returnType": "Result",
+ "jsdoctags": [
+ {
+ "name": "dto",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ }
+ ]
+ },
{
"name": "parsePassage",
"file": "src/app/brief/infrastructure/brief.adapter.ts",
@@ -28216,94 +28711,6 @@
}
]
},
- {
- "name": "reduce",
- "file": "src/app/registratie/domain/change-request.machine.ts",
- "ctype": "miscellaneous",
- "subtype": "function",
- "deprecated": false,
- "deprecationMessage": "",
- "description": "",
- "args": [
- {
- "name": "s",
- "type": "ChangeRequestState",
- "deprecated": false,
- "deprecationMessage": ""
- },
- {
- "name": "m",
- "type": "ChangeRequestMsg",
- "deprecated": false,
- "deprecationMessage": ""
- }
- ],
- "returnType": "ChangeRequestState",
- "jsdoctags": [
- {
- "name": "s",
- "type": "ChangeRequestState",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- },
- {
- "name": "m",
- "type": "ChangeRequestMsg",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- }
- ]
- },
- {
- "name": "reduce",
- "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
- "ctype": "miscellaneous",
- "subtype": "function",
- "deprecated": false,
- "deprecationMessage": "",
- "description": "",
- "args": [
- {
- "name": "s",
- "type": "RegistratieState",
- "deprecated": false,
- "deprecationMessage": ""
- },
- {
- "name": "m",
- "type": "RegistratieMsg",
- "deprecated": false,
- "deprecationMessage": ""
- }
- ],
- "returnType": "RegistratieState",
- "jsdoctags": [
- {
- "name": "s",
- "type": "RegistratieState",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- },
- {
- "name": "m",
- "type": "RegistratieMsg",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- }
- ]
- },
{
"name": "reduce",
"file": "src/app/herregistratie/domain/herregistratie.machine.ts",
@@ -28392,6 +28799,94 @@
}
]
},
+ {
+ "name": "reduce",
+ "file": "src/app/registratie/domain/change-request.machine.ts",
+ "ctype": "miscellaneous",
+ "subtype": "function",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "description": "",
+ "args": [
+ {
+ "name": "s",
+ "type": "ChangeRequestState",
+ "deprecated": false,
+ "deprecationMessage": ""
+ },
+ {
+ "name": "m",
+ "type": "ChangeRequestMsg",
+ "deprecated": false,
+ "deprecationMessage": ""
+ }
+ ],
+ "returnType": "ChangeRequestState",
+ "jsdoctags": [
+ {
+ "name": "s",
+ "type": "ChangeRequestState",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ },
+ {
+ "name": "m",
+ "type": "ChangeRequestMsg",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ }
+ ]
+ },
+ {
+ "name": "reduce",
+ "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
+ "ctype": "miscellaneous",
+ "subtype": "function",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "description": "",
+ "args": [
+ {
+ "name": "s",
+ "type": "RegistratieState",
+ "deprecated": false,
+ "deprecationMessage": ""
+ },
+ {
+ "name": "m",
+ "type": "RegistratieMsg",
+ "deprecated": false,
+ "deprecationMessage": ""
+ }
+ ],
+ "returnType": "RegistratieState",
+ "jsdoctags": [
+ {
+ "name": "s",
+ "type": "RegistratieState",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ },
+ {
+ "name": "m",
+ "type": "RegistratieMsg",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ }
+ ]
+ },
{
"name": "reduceUpload",
"file": "src/app/shared/upload/upload.machine.ts",
@@ -28680,50 +29175,6 @@
}
]
},
- {
- "name": "resolve",
- "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
- "ctype": "miscellaneous",
- "subtype": "function",
- "deprecated": false,
- "deprecationMessage": "",
- "description": "",
- "args": [
- {
- "name": "s",
- "type": "RegistratieState",
- "deprecated": false,
- "deprecationMessage": ""
- },
- {
- "name": "r",
- "type": "Result",
- "deprecated": false,
- "deprecationMessage": ""
- }
- ],
- "returnType": "RegistratieState",
- "jsdoctags": [
- {
- "name": "s",
- "type": "RegistratieState",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- },
- {
- "name": "r",
- "type": "Result",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- }
- ]
- },
{
"name": "resolve",
"file": "src/app/herregistratie/domain/herregistratie.machine.ts",
@@ -28812,6 +29263,50 @@
}
]
},
+ {
+ "name": "resolve",
+ "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
+ "ctype": "miscellaneous",
+ "subtype": "function",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "description": "",
+ "args": [
+ {
+ "name": "s",
+ "type": "RegistratieState",
+ "deprecated": false,
+ "deprecationMessage": ""
+ },
+ {
+ "name": "r",
+ "type": "Result",
+ "deprecated": false,
+ "deprecationMessage": ""
+ }
+ ],
+ "returnType": "RegistratieState",
+ "jsdoctags": [
+ {
+ "name": "s",
+ "type": "RegistratieState",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ },
+ {
+ "name": "r",
+ "type": "Result",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ }
+ ]
+ },
{
"name": "restore",
"file": "src/app/auth/application/session.store.ts",
@@ -29098,6 +29593,63 @@
}
]
},
+ {
+ "name": "setField",
+ "file": "src/app/herregistratie/domain/herregistratie.machine.ts",
+ "ctype": "miscellaneous",
+ "subtype": "function",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "description": "Update one draft field while editing; ignored in any other state.
\n",
+ "args": [
+ {
+ "name": "s",
+ "type": "WizardState",
+ "deprecated": false,
+ "deprecationMessage": ""
+ },
+ {
+ "name": "key",
+ "deprecated": false,
+ "deprecationMessage": ""
+ },
+ {
+ "name": "value",
+ "type": "string",
+ "deprecated": false,
+ "deprecationMessage": ""
+ }
+ ],
+ "returnType": "WizardState",
+ "jsdoctags": [
+ {
+ "name": "s",
+ "type": "WizardState",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ },
+ {
+ "name": "key",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ },
+ {
+ "name": "value",
+ "type": "string",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ }
+ ]
+ },
{
"name": "setField",
"file": "src/app/registratie/domain/registratie-wizard.machine.ts",
@@ -29157,63 +29709,6 @@
}
]
},
- {
- "name": "setField",
- "file": "src/app/herregistratie/domain/herregistratie.machine.ts",
- "ctype": "miscellaneous",
- "subtype": "function",
- "deprecated": false,
- "deprecationMessage": "",
- "description": "Update one draft field while editing; ignored in any other state.
\n",
- "args": [
- {
- "name": "s",
- "type": "WizardState",
- "deprecated": false,
- "deprecationMessage": ""
- },
- {
- "name": "key",
- "deprecated": false,
- "deprecationMessage": ""
- },
- {
- "name": "value",
- "type": "string",
- "deprecated": false,
- "deprecationMessage": ""
- }
- ],
- "returnType": "WizardState",
- "jsdoctags": [
- {
- "name": "s",
- "type": "WizardState",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- },
- {
- "name": "key",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- },
- {
- "name": "value",
- "type": "string",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- }
- ]
- },
{
"name": "setPolicy",
"file": "src/app/herregistratie/domain/intake.machine.ts",
@@ -29414,35 +29909,6 @@
}
]
},
- {
- "name": "submit",
- "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
- "ctype": "miscellaneous",
- "subtype": "function",
- "deprecated": false,
- "deprecationMessage": "",
- "description": "",
- "args": [
- {
- "name": "s",
- "type": "RegistratieState",
- "deprecated": false,
- "deprecationMessage": ""
- }
- ],
- "returnType": "RegistratieState",
- "jsdoctags": [
- {
- "name": "s",
- "type": "RegistratieState",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- }
- ]
- },
{
"name": "submit",
"file": "src/app/herregistratie/domain/herregistratie.machine.ts",
@@ -29501,6 +29967,35 @@
}
]
},
+ {
+ "name": "submit",
+ "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
+ "ctype": "miscellaneous",
+ "subtype": "function",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "description": "",
+ "args": [
+ {
+ "name": "s",
+ "type": "RegistratieState",
+ "deprecated": false,
+ "deprecationMessage": ""
+ }
+ ],
+ "returnType": "RegistratieState",
+ "jsdoctags": [
+ {
+ "name": "s",
+ "type": "RegistratieState",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ }
+ ]
+ },
{
"name": "submittedRow",
"file": "src/app/registratie/domain/aanvraag-view.ts",
@@ -29864,50 +30359,6 @@
}
]
},
- {
- "name": "upload",
- "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
- "ctype": "miscellaneous",
- "subtype": "function",
- "deprecated": false,
- "deprecationMessage": "",
- "description": "Route an upload sub-message through the pure upload reducer (Invullen only).
\n",
- "args": [
- {
- "name": "s",
- "type": "RegistratieState",
- "deprecated": false,
- "deprecationMessage": ""
- },
- {
- "name": "msg",
- "type": "UploadMsg",
- "deprecated": false,
- "deprecationMessage": ""
- }
- ],
- "returnType": "RegistratieState",
- "jsdoctags": [
- {
- "name": "s",
- "type": "RegistratieState",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- },
- {
- "name": "msg",
- "type": "UploadMsg",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- }
- ]
- },
{
"name": "upload",
"file": "src/app/herregistratie/domain/herregistratie.machine.ts",
@@ -29953,26 +30404,41 @@
]
},
{
- "name": "validate",
- "file": "src/app/registratie/domain/change-request.machine.ts",
+ "name": "upload",
+ "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
"ctype": "miscellaneous",
"subtype": "function",
"deprecated": false,
"deprecationMessage": "",
- "description": "Parse via the value objects; on success hand back a Valid, else per-field errors.
\n",
+ "description": "Route an upload sub-message through the pure upload reducer (Invullen only).
\n",
"args": [
{
- "name": "draft",
- "type": "Draft",
+ "name": "s",
+ "type": "RegistratieState",
+ "deprecated": false,
+ "deprecationMessage": ""
+ },
+ {
+ "name": "msg",
+ "type": "UploadMsg",
"deprecated": false,
"deprecationMessage": ""
}
],
- "returnType": "Result",
+ "returnType": "RegistratieState",
"jsdoctags": [
{
- "name": "draft",
- "type": "Draft",
+ "name": "s",
+ "type": "RegistratieState",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ },
+ {
+ "name": "msg",
+ "type": "UploadMsg",
"deprecated": false,
"deprecationMessage": "",
"tagName": {
@@ -30026,46 +30492,31 @@
]
},
{
- "name": "validateAll",
- "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
+ "name": "validate",
+ "file": "src/app/registratie/domain/change-request.machine.ts",
"ctype": "miscellaneous",
"subtype": "function",
"deprecated": false,
"deprecationMessage": "",
- "description": "Parse the whole wizard into a ValidRegistratie (called on submit).
\n",
+ "description": "Parse via the value objects; on success hand back a Valid, else per-field errors.
\n",
"args": [
{
- "name": "d",
+ "name": "draft",
"type": "Draft",
"deprecated": false,
"deprecationMessage": ""
- },
- {
- "name": "upload",
- "type": "UploadState",
- "deprecated": false,
- "deprecationMessage": ""
}
],
- "returnType": "Result",
+ "returnType": "Result",
"jsdoctags": [
{
- "name": "d",
+ "name": "draft",
"type": "Draft",
"deprecated": false,
"deprecationMessage": "",
"tagName": {
"text": "param"
}
- },
- {
- "name": "upload",
- "type": "UploadState",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
}
]
},
@@ -30114,20 +30565,14 @@
]
},
{
- "name": "validateStep",
+ "name": "validateAll",
"file": "src/app/registratie/domain/registratie-wizard.machine.ts",
"ctype": "miscellaneous",
"subtype": "function",
"deprecated": false,
"deprecationMessage": "",
- "description": "Validate every question currently visible in ONE step. Errors keyed per field.
\n",
+ "description": "Parse the whole wizard into a ValidRegistratie (called on submit).
\n",
"args": [
- {
- "name": "step",
- "type": "StepId",
- "deprecated": false,
- "deprecationMessage": ""
- },
{
"name": "d",
"type": "Draft",
@@ -30141,17 +30586,8 @@
"deprecationMessage": ""
}
],
- "returnType": "Result",
+ "returnType": "Result",
"jsdoctags": [
- {
- "name": "step",
- "type": "StepId",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- },
{
"name": "d",
"type": "Draft",
@@ -30231,6 +30667,65 @@
}
]
},
+ {
+ "name": "validateStep",
+ "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
+ "ctype": "miscellaneous",
+ "subtype": "function",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "description": "Validate every question currently visible in ONE step. Errors keyed per field.
\n",
+ "args": [
+ {
+ "name": "step",
+ "type": "StepId",
+ "deprecated": false,
+ "deprecationMessage": ""
+ },
+ {
+ "name": "d",
+ "type": "Draft",
+ "deprecated": false,
+ "deprecationMessage": ""
+ },
+ {
+ "name": "upload",
+ "type": "UploadState",
+ "deprecated": false,
+ "deprecationMessage": ""
+ }
+ ],
+ "returnType": "Result",
+ "jsdoctags": [
+ {
+ "name": "step",
+ "type": "StepId",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ },
+ {
+ "name": "d",
+ "type": "Draft",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ },
+ {
+ "name": "upload",
+ "type": "UploadState",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ }
+ ]
+ },
{
"name": "whenTag",
"file": "src/app/shared/kernel/fp.ts",
@@ -30673,22 +31168,22 @@
"name": "Errors",
"ctype": "miscellaneous",
"subtype": "typealias",
- "rawtype": "Partial>",
- "file": "src/app/registratie/domain/change-request.machine.ts",
+ "rawtype": "Partial>",
+ "file": "src/app/herregistratie/domain/intake.machine.ts",
"deprecated": false,
"deprecationMessage": "",
- "description": "",
+ "description": "Per-field error map: one message per question, since a step holds several.
\n",
"kind": 184
},
{
"name": "Errors",
"ctype": "miscellaneous",
"subtype": "typealias",
- "rawtype": "Partial>",
- "file": "src/app/herregistratie/domain/intake.machine.ts",
+ "rawtype": "Partial>",
+ "file": "src/app/registratie/domain/change-request.machine.ts",
"deprecated": false,
"deprecationMessage": "",
- "description": "Per-field error map: one message per question, since a step holds several.
\n",
+ "description": "",
"kind": 184
},
{
@@ -30784,7 +31279,7 @@
"ctype": "miscellaneous",
"subtype": "typealias",
"rawtype": "literal type",
- "file": "src/app/brief/ui/letter-preview/letter-preview.component.ts",
+ "file": "src/app/brief/ui/letter-canvas/letter-canvas.component.ts",
"deprecated": false,
"deprecationMessage": "",
"description": "A run of consecutive lines to render together: a list (bullet/number) or a single plain line.
\n",
@@ -30926,22 +31421,22 @@
"name": "StepId",
"ctype": "miscellaneous",
"subtype": "typealias",
- "rawtype": "\"adres\" | \"beroep\" | \"controle\"",
- "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
+ "rawtype": "\"buitenland\" | \"werk\" | \"review\"",
+ "file": "src/app/herregistratie/domain/intake.machine.ts",
"deprecated": false,
"deprecationMessage": "",
- "description": "A FIXED 3-step registration wizard. The steps never change in number (always\nSTEPS): (1) adres + correspondentievoorkeur, (2) beroep o.b.v. diploma,\n(3) controle & indienen. Follow-up questions appear inline within a step\n(e.g. choosing 'email' reveals the e-mail field). "Is this field required\nright now" is a pure function (validateStep), so it is trivial to test and\nimpossible to get out of sync with the data. Invariants live here, not in the\nUI: the wizard reaches Indienen only when a complete ValidRegistratie parses.
\n",
+ "description": "The three fixed steps. Each step groups one or more questions.
\n",
"kind": 193
},
{
"name": "StepId",
"ctype": "miscellaneous",
"subtype": "typealias",
- "rawtype": "\"buitenland\" | \"werk\" | \"review\"",
- "file": "src/app/herregistratie/domain/intake.machine.ts",
+ "rawtype": "\"adres\" | \"beroep\" | \"controle\"",
+ "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
"deprecated": false,
"deprecationMessage": "",
- "description": "The three fixed steps. Each step groups one or more questions.
\n",
+ "description": "A FIXED 3-step registration wizard. The steps never change in number (always\nSTEPS): (1) adres + correspondentievoorkeur, (2) beroep o.b.v. diploma,\n(3) controle & indienen. Follow-up questions appear inline within a step\n(e.g. choosing 'email' reveals the e-mail field). "Is this field required\nright now" is a pure function (validateStep), so it is trivial to test and\nimpossible to get out of sync with the data. Invariants live here, not in the\nUI: the wizard reaches Indienen only when a complete ValidRegistratie parses.
\n",
"kind": 193
},
{
@@ -31024,6 +31519,30 @@
],
"enumerations": [],
"groupedVariables": {
+ "src/app/brief/ui/letter-canvas/letter-canvas.component.ts": [
+ {
+ "name": "A4_HEIGHT_PX",
+ "ctype": "miscellaneous",
+ "subtype": "variable",
+ "file": "src/app/brief/ui/letter-canvas/letter-canvas.component.ts",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "unknown",
+ "defaultValue": "(297 * 96) / 25.4",
+ "rawdescription": "A4 height in CSS px (1in = 96px = 25.4mm) — for the approximate page-break marks.",
+ "description": "A4 height in CSS px (1in = 96px = 25.4mm) — for the approximate page-break marks.
\n"
+ },
+ {
+ "name": "SAMPLE_VALUES",
+ "ctype": "miscellaneous",
+ "subtype": "variable",
+ "file": "src/app/brief/ui/letter-canvas/letter-canvas.component.ts",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "Record",
+ "defaultValue": "{\n naam_zorgverlener: 'J. Jansen',\n big_nummer: '12345678901',\n}"
+ }
+ ],
"src/app/registratie/infrastructure/big-register.adapter.ts": [
{
"name": "AANTEKENING_TYPES",
@@ -31382,18 +31901,6 @@
"defaultValue": "{ tag: 'loading' }"
}
],
- "src/app/registratie/domain/change-request.machine.ts": [
- {
- "name": "initial",
- "ctype": "miscellaneous",
- "subtype": "variable",
- "file": "src/app/registratie/domain/change-request.machine.ts",
- "deprecated": false,
- "deprecationMessage": "",
- "type": "ChangeRequestState",
- "defaultValue": "{\n tag: 'Editing',\n draft: { straat: '', postcode: '', woonplaats: '' },\n errors: {},\n}"
- }
- ],
"src/app/herregistratie/domain/herregistratie.machine.ts": [
{
"name": "initial",
@@ -31442,6 +31949,18 @@
"description": "The fixed step list. Number of steps never changes; questions reveal inline.
\n"
}
],
+ "src/app/registratie/domain/change-request.machine.ts": [
+ {
+ "name": "initial",
+ "ctype": "miscellaneous",
+ "subtype": "variable",
+ "file": "src/app/registratie/domain/change-request.machine.ts",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "type": "ChangeRequestState",
+ "defaultValue": "{\n tag: 'Editing',\n draft: { straat: '', postcode: '', woonplaats: '' },\n errors: {},\n}"
+ }
+ ],
"src/app/shared/ui/radio-group/radio-group.component.ts": [
{
"name": "JA_NEE",
@@ -31651,18 +32170,6 @@
"defaultValue": "{\n '/dashboard': { label: $localize`:@@crumb.dashboard:Mijn overzicht` },\n '/registratie': { label: $localize`:@@crumb.registratie:Mijn gegevens`, parent: '/dashboard' },\n '/registreren': { label: $localize`:@@crumb.registreren:Inschrijven`, parent: '/dashboard' },\n '/herregistratie': {\n label: $localize`:@@crumb.herregistratie:Herregistratie`,\n parent: '/dashboard',\n },\n '/intake': { label: $localize`:@@crumb.intake:Herregistratie-intake`, parent: '/dashboard' },\n '/concepts': { label: $localize`:@@crumb.concepts:Functionele patronen`, parent: '/dashboard' },\n}"
}
],
- "src/app/brief/ui/letter-preview/letter-preview.component.ts": [
- {
- "name": "SAMPLE_VALUES",
- "ctype": "miscellaneous",
- "subtype": "variable",
- "file": "src/app/brief/ui/letter-preview/letter-preview.component.ts",
- "deprecated": false,
- "deprecationMessage": "",
- "type": "Record",
- "defaultValue": "{\n naam_zorgverlener: 'J. Jansen',\n big_nummer: '12345678901',\n}"
- }
- ],
"src/app/shared/infrastructure/scenario.interceptor.ts": [
{
"name": "scenarioInterceptor",
@@ -33397,826 +33904,6 @@
]
}
],
- "src/app/registratie/domain/registratie-wizard.machine.ts": [
- {
- "name": "back",
- "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
- "ctype": "miscellaneous",
- "subtype": "function",
- "deprecated": false,
- "deprecationMessage": "",
- "description": "",
- "args": [
- {
- "name": "s",
- "type": "RegistratieState",
- "deprecated": false,
- "deprecationMessage": ""
- }
- ],
- "returnType": "RegistratieState",
- "jsdoctags": [
- {
- "name": "s",
- "type": "RegistratieState",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- }
- ]
- },
- {
- "name": "currentStep",
- "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
- "ctype": "miscellaneous",
- "subtype": "function",
- "deprecated": false,
- "deprecationMessage": "",
- "description": "Which step the cursor currently points at (clamped to the fixed list).
\n",
- "args": [
- {
- "name": "s",
- "type": "Extract",
- "deprecated": false,
- "deprecationMessage": ""
- }
- ],
- "returnType": "StepId",
- "jsdoctags": [
- {
- "name": "s",
- "type": "Extract",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- }
- ]
- },
- {
- "name": "declareerBeroep",
- "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
- "ctype": "miscellaneous",
- "subtype": "function",
- "deprecated": false,
- "deprecationMessage": "",
- "description": "Declare the beroep for a manually-entered diploma (chosen from a fixed list).
\n",
- "args": [
- {
- "name": "s",
- "type": "RegistratieState",
- "deprecated": false,
- "deprecationMessage": ""
- },
- {
- "name": "beroep",
- "type": "string",
- "deprecated": false,
- "deprecationMessage": ""
- }
- ],
- "returnType": "RegistratieState",
- "jsdoctags": [
- {
- "name": "s",
- "type": "RegistratieState",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- },
- {
- "name": "beroep",
- "type": "string",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- }
- ]
- },
- {
- "name": "gaNaarStap",
- "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
- "ctype": "miscellaneous",
- "subtype": "function",
- "deprecated": false,
- "deprecationMessage": "",
- "description": "Jump back to an earlier step to correct data (controle → step N). Forward\njumps are not allowed (would skip validation). Preserves the draft.
\n",
- "args": [
- {
- "name": "s",
- "type": "RegistratieState",
- "deprecated": false,
- "deprecationMessage": ""
- },
- {
- "name": "cursor",
- "type": "number",
- "deprecated": false,
- "deprecationMessage": ""
- }
- ],
- "returnType": "RegistratieState",
- "jsdoctags": [
- {
- "name": "s",
- "type": "RegistratieState",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- },
- {
- "name": "cursor",
- "type": "number",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- }
- ]
- },
- {
- "name": "hasProgress",
- "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
- "ctype": "miscellaneous",
- "subtype": "function",
- "deprecated": false,
- "deprecationMessage": "",
- "description": "Has the user meaningfully started, so it's worth persisting as a Concept? Excludes\nthe automatic BRP address prefill on step 0 — a bare page visit creates nothing.\nponytail: an address typed at step 0 without any of these signals is not yet\npersisted (created once they advance/choose); accepted regression vs. sessionStorage.
\n",
- "args": [
- {
- "name": "s",
- "type": "Extract",
- "deprecated": false,
- "deprecationMessage": ""
- }
- ],
- "returnType": "boolean",
- "jsdoctags": [
- {
- "name": "s",
- "type": "Extract",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- }
- ]
- },
- {
- "name": "kiesDiploma",
- "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
- "ctype": "miscellaneous",
- "subtype": "function",
- "deprecated": false,
- "deprecationMessage": "",
- "description": "Pick a DUO diploma; the beroep is derived from it and the applicable policy\nquestions (vraagIds) come with it (both server-computed, passed in).
\n",
- "args": [
- {
- "name": "s",
- "type": "RegistratieState",
- "deprecated": false,
- "deprecationMessage": ""
- },
- {
- "name": "diplomaId",
- "type": "string",
- "deprecated": false,
- "deprecationMessage": ""
- },
- {
- "name": "beroep",
- "type": "string",
- "deprecated": false,
- "deprecationMessage": ""
- },
- {
- "name": "vraagIds",
- "deprecated": false,
- "deprecationMessage": ""
- }
- ],
- "returnType": "RegistratieState",
- "jsdoctags": [
- {
- "name": "s",
- "type": "RegistratieState",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- },
- {
- "name": "diplomaId",
- "type": "string",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- },
- {
- "name": "beroep",
- "type": "string",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- },
- {
- "name": "vraagIds",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- }
- ]
- },
- {
- "name": "kiesHandmatig",
- "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
- "ctype": "miscellaneous",
- "subtype": "function",
- "deprecated": false,
- "deprecationMessage": "",
- "description": "Switch to manual diploma entry: the diploma isn't in DUO, so the MAXIMAL\npolicy-question set applies and the entry is flagged handmatig/unverified. The\nberoep is declared separately (declareerBeroep).
\n",
- "args": [
- {
- "name": "s",
- "type": "RegistratieState",
- "deprecated": false,
- "deprecationMessage": ""
- },
- {
- "name": "vraagIds",
- "deprecated": false,
- "deprecationMessage": ""
- }
- ],
- "returnType": "RegistratieState",
- "jsdoctags": [
- {
- "name": "s",
- "type": "RegistratieState",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- },
- {
- "name": "vraagIds",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- }
- ]
- },
- {
- "name": "next",
- "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
- "ctype": "miscellaneous",
- "subtype": "function",
- "deprecated": false,
- "deprecationMessage": "",
- "description": "",
- "args": [
- {
- "name": "s",
- "type": "RegistratieState",
- "deprecated": false,
- "deprecationMessage": ""
- }
- ],
- "returnType": "RegistratieState",
- "jsdoctags": [
- {
- "name": "s",
- "type": "RegistratieState",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- }
- ]
- },
- {
- "name": "prefillAdres",
- "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
- "ctype": "miscellaneous",
- "subtype": "function",
- "deprecated": false,
- "deprecationMessage": "",
- "description": "Prefill the address from a BRP lookup and flag its origin (PRD §7).
\n",
- "args": [
- {
- "name": "s",
- "type": "RegistratieState",
- "deprecated": false,
- "deprecationMessage": ""
- },
- {
- "name": "straat",
- "type": "string",
- "deprecated": false,
- "deprecationMessage": ""
- },
- {
- "name": "postcode",
- "type": "string",
- "deprecated": false,
- "deprecationMessage": ""
- },
- {
- "name": "woonplaats",
- "type": "string",
- "deprecated": false,
- "deprecationMessage": ""
- }
- ],
- "returnType": "RegistratieState",
- "jsdoctags": [
- {
- "name": "s",
- "type": "RegistratieState",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- },
- {
- "name": "straat",
- "type": "string",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- },
- {
- "name": "postcode",
- "type": "string",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- },
- {
- "name": "woonplaats",
- "type": "string",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- }
- ]
- },
- {
- "name": "reduce",
- "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
- "ctype": "miscellaneous",
- "subtype": "function",
- "deprecated": false,
- "deprecationMessage": "",
- "description": "",
- "args": [
- {
- "name": "s",
- "type": "RegistratieState",
- "deprecated": false,
- "deprecationMessage": ""
- },
- {
- "name": "m",
- "type": "RegistratieMsg",
- "deprecated": false,
- "deprecationMessage": ""
- }
- ],
- "returnType": "RegistratieState",
- "jsdoctags": [
- {
- "name": "s",
- "type": "RegistratieState",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- },
- {
- "name": "m",
- "type": "RegistratieMsg",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- }
- ]
- },
- {
- "name": "resolve",
- "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
- "ctype": "miscellaneous",
- "subtype": "function",
- "deprecated": false,
- "deprecationMessage": "",
- "description": "",
- "args": [
- {
- "name": "s",
- "type": "RegistratieState",
- "deprecated": false,
- "deprecationMessage": ""
- },
- {
- "name": "r",
- "type": "Result",
- "deprecated": false,
- "deprecationMessage": ""
- }
- ],
- "returnType": "RegistratieState",
- "jsdoctags": [
- {
- "name": "s",
- "type": "RegistratieState",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- },
- {
- "name": "r",
- "type": "Result",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- }
- ]
- },
- {
- "name": "setAntwoord",
- "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
- "ctype": "miscellaneous",
- "subtype": "function",
- "deprecated": false,
- "deprecationMessage": "",
- "description": "",
- "args": [
- {
- "name": "s",
- "type": "RegistratieState",
- "deprecated": false,
- "deprecationMessage": ""
- },
- {
- "name": "vraagId",
- "type": "string",
- "deprecated": false,
- "deprecationMessage": ""
- },
- {
- "name": "value",
- "type": "string",
- "deprecated": false,
- "deprecationMessage": ""
- }
- ],
- "returnType": "RegistratieState",
- "jsdoctags": [
- {
- "name": "s",
- "type": "RegistratieState",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- },
- {
- "name": "vraagId",
- "type": "string",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- },
- {
- "name": "value",
- "type": "string",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- }
- ]
- },
- {
- "name": "setCorrespondentie",
- "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
- "ctype": "miscellaneous",
- "subtype": "function",
- "deprecated": false,
- "deprecationMessage": "",
- "description": "",
- "args": [
- {
- "name": "s",
- "type": "RegistratieState",
- "deprecated": false,
- "deprecationMessage": ""
- },
- {
- "name": "value",
- "type": "Correspondentie",
- "deprecated": false,
- "deprecationMessage": ""
- }
- ],
- "returnType": "RegistratieState",
- "jsdoctags": [
- {
- "name": "s",
- "type": "RegistratieState",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- },
- {
- "name": "value",
- "type": "Correspondentie",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- }
- ]
- },
- {
- "name": "setField",
- "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
- "ctype": "miscellaneous",
- "subtype": "function",
- "deprecated": false,
- "deprecationMessage": "",
- "description": "",
- "args": [
- {
- "name": "s",
- "type": "RegistratieState",
- "deprecated": false,
- "deprecationMessage": ""
- },
- {
- "name": "key",
- "type": "DraftField",
- "deprecated": false,
- "deprecationMessage": ""
- },
- {
- "name": "value",
- "type": "string",
- "deprecated": false,
- "deprecationMessage": ""
- }
- ],
- "returnType": "RegistratieState",
- "jsdoctags": [
- {
- "name": "s",
- "type": "RegistratieState",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- },
- {
- "name": "key",
- "type": "DraftField",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- },
- {
- "name": "value",
- "type": "string",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- }
- ]
- },
- {
- "name": "submit",
- "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
- "ctype": "miscellaneous",
- "subtype": "function",
- "deprecated": false,
- "deprecationMessage": "",
- "description": "",
- "args": [
- {
- "name": "s",
- "type": "RegistratieState",
- "deprecated": false,
- "deprecationMessage": ""
- }
- ],
- "returnType": "RegistratieState",
- "jsdoctags": [
- {
- "name": "s",
- "type": "RegistratieState",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- }
- ]
- },
- {
- "name": "upload",
- "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
- "ctype": "miscellaneous",
- "subtype": "function",
- "deprecated": false,
- "deprecationMessage": "",
- "description": "Route an upload sub-message through the pure upload reducer (Invullen only).
\n",
- "args": [
- {
- "name": "s",
- "type": "RegistratieState",
- "deprecated": false,
- "deprecationMessage": ""
- },
- {
- "name": "msg",
- "type": "UploadMsg",
- "deprecated": false,
- "deprecationMessage": ""
- }
- ],
- "returnType": "RegistratieState",
- "jsdoctags": [
- {
- "name": "s",
- "type": "RegistratieState",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- },
- {
- "name": "msg",
- "type": "UploadMsg",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- }
- ]
- },
- {
- "name": "validateAll",
- "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
- "ctype": "miscellaneous",
- "subtype": "function",
- "deprecated": false,
- "deprecationMessage": "",
- "description": "Parse the whole wizard into a ValidRegistratie (called on submit).
\n",
- "args": [
- {
- "name": "d",
- "type": "Draft",
- "deprecated": false,
- "deprecationMessage": ""
- },
- {
- "name": "upload",
- "type": "UploadState",
- "deprecated": false,
- "deprecationMessage": ""
- }
- ],
- "returnType": "Result",
- "jsdoctags": [
- {
- "name": "d",
- "type": "Draft",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- },
- {
- "name": "upload",
- "type": "UploadState",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- }
- ]
- },
- {
- "name": "validateStep",
- "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
- "ctype": "miscellaneous",
- "subtype": "function",
- "deprecated": false,
- "deprecationMessage": "",
- "description": "Validate every question currently visible in ONE step. Errors keyed per field.
\n",
- "args": [
- {
- "name": "step",
- "type": "StepId",
- "deprecated": false,
- "deprecationMessage": ""
- },
- {
- "name": "d",
- "type": "Draft",
- "deprecated": false,
- "deprecationMessage": ""
- },
- {
- "name": "upload",
- "type": "UploadState",
- "deprecated": false,
- "deprecationMessage": ""
- }
- ],
- "returnType": "Result",
- "jsdoctags": [
- {
- "name": "step",
- "type": "StepId",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- },
- {
- "name": "d",
- "type": "Draft",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- },
- {
- "name": "upload",
- "type": "UploadState",
- "deprecated": false,
- "deprecationMessage": "",
- "tagName": {
- "text": "param"
- }
- }
- ]
- }
- ],
"src/app/herregistratie/domain/herregistratie.machine.ts": [
{
"name": "back",
@@ -35139,6 +34826,826 @@
]
}
],
+ "src/app/registratie/domain/registratie-wizard.machine.ts": [
+ {
+ "name": "back",
+ "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
+ "ctype": "miscellaneous",
+ "subtype": "function",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "description": "",
+ "args": [
+ {
+ "name": "s",
+ "type": "RegistratieState",
+ "deprecated": false,
+ "deprecationMessage": ""
+ }
+ ],
+ "returnType": "RegistratieState",
+ "jsdoctags": [
+ {
+ "name": "s",
+ "type": "RegistratieState",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ }
+ ]
+ },
+ {
+ "name": "currentStep",
+ "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
+ "ctype": "miscellaneous",
+ "subtype": "function",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "description": "Which step the cursor currently points at (clamped to the fixed list).
\n",
+ "args": [
+ {
+ "name": "s",
+ "type": "Extract",
+ "deprecated": false,
+ "deprecationMessage": ""
+ }
+ ],
+ "returnType": "StepId",
+ "jsdoctags": [
+ {
+ "name": "s",
+ "type": "Extract",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ }
+ ]
+ },
+ {
+ "name": "declareerBeroep",
+ "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
+ "ctype": "miscellaneous",
+ "subtype": "function",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "description": "Declare the beroep for a manually-entered diploma (chosen from a fixed list).
\n",
+ "args": [
+ {
+ "name": "s",
+ "type": "RegistratieState",
+ "deprecated": false,
+ "deprecationMessage": ""
+ },
+ {
+ "name": "beroep",
+ "type": "string",
+ "deprecated": false,
+ "deprecationMessage": ""
+ }
+ ],
+ "returnType": "RegistratieState",
+ "jsdoctags": [
+ {
+ "name": "s",
+ "type": "RegistratieState",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ },
+ {
+ "name": "beroep",
+ "type": "string",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ }
+ ]
+ },
+ {
+ "name": "gaNaarStap",
+ "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
+ "ctype": "miscellaneous",
+ "subtype": "function",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "description": "Jump back to an earlier step to correct data (controle → step N). Forward\njumps are not allowed (would skip validation). Preserves the draft.
\n",
+ "args": [
+ {
+ "name": "s",
+ "type": "RegistratieState",
+ "deprecated": false,
+ "deprecationMessage": ""
+ },
+ {
+ "name": "cursor",
+ "type": "number",
+ "deprecated": false,
+ "deprecationMessage": ""
+ }
+ ],
+ "returnType": "RegistratieState",
+ "jsdoctags": [
+ {
+ "name": "s",
+ "type": "RegistratieState",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ },
+ {
+ "name": "cursor",
+ "type": "number",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ }
+ ]
+ },
+ {
+ "name": "hasProgress",
+ "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
+ "ctype": "miscellaneous",
+ "subtype": "function",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "description": "Has the user meaningfully started, so it's worth persisting as a Concept? Excludes\nthe automatic BRP address prefill on step 0 — a bare page visit creates nothing.\nponytail: an address typed at step 0 without any of these signals is not yet\npersisted (created once they advance/choose); accepted regression vs. sessionStorage.
\n",
+ "args": [
+ {
+ "name": "s",
+ "type": "Extract",
+ "deprecated": false,
+ "deprecationMessage": ""
+ }
+ ],
+ "returnType": "boolean",
+ "jsdoctags": [
+ {
+ "name": "s",
+ "type": "Extract",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ }
+ ]
+ },
+ {
+ "name": "kiesDiploma",
+ "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
+ "ctype": "miscellaneous",
+ "subtype": "function",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "description": "Pick a DUO diploma; the beroep is derived from it and the applicable policy\nquestions (vraagIds) come with it (both server-computed, passed in).
\n",
+ "args": [
+ {
+ "name": "s",
+ "type": "RegistratieState",
+ "deprecated": false,
+ "deprecationMessage": ""
+ },
+ {
+ "name": "diplomaId",
+ "type": "string",
+ "deprecated": false,
+ "deprecationMessage": ""
+ },
+ {
+ "name": "beroep",
+ "type": "string",
+ "deprecated": false,
+ "deprecationMessage": ""
+ },
+ {
+ "name": "vraagIds",
+ "deprecated": false,
+ "deprecationMessage": ""
+ }
+ ],
+ "returnType": "RegistratieState",
+ "jsdoctags": [
+ {
+ "name": "s",
+ "type": "RegistratieState",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ },
+ {
+ "name": "diplomaId",
+ "type": "string",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ },
+ {
+ "name": "beroep",
+ "type": "string",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ },
+ {
+ "name": "vraagIds",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ }
+ ]
+ },
+ {
+ "name": "kiesHandmatig",
+ "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
+ "ctype": "miscellaneous",
+ "subtype": "function",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "description": "Switch to manual diploma entry: the diploma isn't in DUO, so the MAXIMAL\npolicy-question set applies and the entry is flagged handmatig/unverified. The\nberoep is declared separately (declareerBeroep).
\n",
+ "args": [
+ {
+ "name": "s",
+ "type": "RegistratieState",
+ "deprecated": false,
+ "deprecationMessage": ""
+ },
+ {
+ "name": "vraagIds",
+ "deprecated": false,
+ "deprecationMessage": ""
+ }
+ ],
+ "returnType": "RegistratieState",
+ "jsdoctags": [
+ {
+ "name": "s",
+ "type": "RegistratieState",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ },
+ {
+ "name": "vraagIds",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ }
+ ]
+ },
+ {
+ "name": "next",
+ "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
+ "ctype": "miscellaneous",
+ "subtype": "function",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "description": "",
+ "args": [
+ {
+ "name": "s",
+ "type": "RegistratieState",
+ "deprecated": false,
+ "deprecationMessage": ""
+ }
+ ],
+ "returnType": "RegistratieState",
+ "jsdoctags": [
+ {
+ "name": "s",
+ "type": "RegistratieState",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ }
+ ]
+ },
+ {
+ "name": "prefillAdres",
+ "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
+ "ctype": "miscellaneous",
+ "subtype": "function",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "description": "Prefill the address from a BRP lookup and flag its origin (PRD §7).
\n",
+ "args": [
+ {
+ "name": "s",
+ "type": "RegistratieState",
+ "deprecated": false,
+ "deprecationMessage": ""
+ },
+ {
+ "name": "straat",
+ "type": "string",
+ "deprecated": false,
+ "deprecationMessage": ""
+ },
+ {
+ "name": "postcode",
+ "type": "string",
+ "deprecated": false,
+ "deprecationMessage": ""
+ },
+ {
+ "name": "woonplaats",
+ "type": "string",
+ "deprecated": false,
+ "deprecationMessage": ""
+ }
+ ],
+ "returnType": "RegistratieState",
+ "jsdoctags": [
+ {
+ "name": "s",
+ "type": "RegistratieState",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ },
+ {
+ "name": "straat",
+ "type": "string",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ },
+ {
+ "name": "postcode",
+ "type": "string",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ },
+ {
+ "name": "woonplaats",
+ "type": "string",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ }
+ ]
+ },
+ {
+ "name": "reduce",
+ "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
+ "ctype": "miscellaneous",
+ "subtype": "function",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "description": "",
+ "args": [
+ {
+ "name": "s",
+ "type": "RegistratieState",
+ "deprecated": false,
+ "deprecationMessage": ""
+ },
+ {
+ "name": "m",
+ "type": "RegistratieMsg",
+ "deprecated": false,
+ "deprecationMessage": ""
+ }
+ ],
+ "returnType": "RegistratieState",
+ "jsdoctags": [
+ {
+ "name": "s",
+ "type": "RegistratieState",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ },
+ {
+ "name": "m",
+ "type": "RegistratieMsg",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ }
+ ]
+ },
+ {
+ "name": "resolve",
+ "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
+ "ctype": "miscellaneous",
+ "subtype": "function",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "description": "",
+ "args": [
+ {
+ "name": "s",
+ "type": "RegistratieState",
+ "deprecated": false,
+ "deprecationMessage": ""
+ },
+ {
+ "name": "r",
+ "type": "Result",
+ "deprecated": false,
+ "deprecationMessage": ""
+ }
+ ],
+ "returnType": "RegistratieState",
+ "jsdoctags": [
+ {
+ "name": "s",
+ "type": "RegistratieState",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ },
+ {
+ "name": "r",
+ "type": "Result",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ }
+ ]
+ },
+ {
+ "name": "setAntwoord",
+ "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
+ "ctype": "miscellaneous",
+ "subtype": "function",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "description": "",
+ "args": [
+ {
+ "name": "s",
+ "type": "RegistratieState",
+ "deprecated": false,
+ "deprecationMessage": ""
+ },
+ {
+ "name": "vraagId",
+ "type": "string",
+ "deprecated": false,
+ "deprecationMessage": ""
+ },
+ {
+ "name": "value",
+ "type": "string",
+ "deprecated": false,
+ "deprecationMessage": ""
+ }
+ ],
+ "returnType": "RegistratieState",
+ "jsdoctags": [
+ {
+ "name": "s",
+ "type": "RegistratieState",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ },
+ {
+ "name": "vraagId",
+ "type": "string",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ },
+ {
+ "name": "value",
+ "type": "string",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ }
+ ]
+ },
+ {
+ "name": "setCorrespondentie",
+ "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
+ "ctype": "miscellaneous",
+ "subtype": "function",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "description": "",
+ "args": [
+ {
+ "name": "s",
+ "type": "RegistratieState",
+ "deprecated": false,
+ "deprecationMessage": ""
+ },
+ {
+ "name": "value",
+ "type": "Correspondentie",
+ "deprecated": false,
+ "deprecationMessage": ""
+ }
+ ],
+ "returnType": "RegistratieState",
+ "jsdoctags": [
+ {
+ "name": "s",
+ "type": "RegistratieState",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ },
+ {
+ "name": "value",
+ "type": "Correspondentie",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ }
+ ]
+ },
+ {
+ "name": "setField",
+ "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
+ "ctype": "miscellaneous",
+ "subtype": "function",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "description": "",
+ "args": [
+ {
+ "name": "s",
+ "type": "RegistratieState",
+ "deprecated": false,
+ "deprecationMessage": ""
+ },
+ {
+ "name": "key",
+ "type": "DraftField",
+ "deprecated": false,
+ "deprecationMessage": ""
+ },
+ {
+ "name": "value",
+ "type": "string",
+ "deprecated": false,
+ "deprecationMessage": ""
+ }
+ ],
+ "returnType": "RegistratieState",
+ "jsdoctags": [
+ {
+ "name": "s",
+ "type": "RegistratieState",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ },
+ {
+ "name": "key",
+ "type": "DraftField",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ },
+ {
+ "name": "value",
+ "type": "string",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ }
+ ]
+ },
+ {
+ "name": "submit",
+ "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
+ "ctype": "miscellaneous",
+ "subtype": "function",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "description": "",
+ "args": [
+ {
+ "name": "s",
+ "type": "RegistratieState",
+ "deprecated": false,
+ "deprecationMessage": ""
+ }
+ ],
+ "returnType": "RegistratieState",
+ "jsdoctags": [
+ {
+ "name": "s",
+ "type": "RegistratieState",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ }
+ ]
+ },
+ {
+ "name": "upload",
+ "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
+ "ctype": "miscellaneous",
+ "subtype": "function",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "description": "Route an upload sub-message through the pure upload reducer (Invullen only).
\n",
+ "args": [
+ {
+ "name": "s",
+ "type": "RegistratieState",
+ "deprecated": false,
+ "deprecationMessage": ""
+ },
+ {
+ "name": "msg",
+ "type": "UploadMsg",
+ "deprecated": false,
+ "deprecationMessage": ""
+ }
+ ],
+ "returnType": "RegistratieState",
+ "jsdoctags": [
+ {
+ "name": "s",
+ "type": "RegistratieState",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ },
+ {
+ "name": "msg",
+ "type": "UploadMsg",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ }
+ ]
+ },
+ {
+ "name": "validateAll",
+ "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
+ "ctype": "miscellaneous",
+ "subtype": "function",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "description": "Parse the whole wizard into a ValidRegistratie (called on submit).
\n",
+ "args": [
+ {
+ "name": "d",
+ "type": "Draft",
+ "deprecated": false,
+ "deprecationMessage": ""
+ },
+ {
+ "name": "upload",
+ "type": "UploadState",
+ "deprecated": false,
+ "deprecationMessage": ""
+ }
+ ],
+ "returnType": "Result",
+ "jsdoctags": [
+ {
+ "name": "d",
+ "type": "Draft",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ },
+ {
+ "name": "upload",
+ "type": "UploadState",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ }
+ ]
+ },
+ {
+ "name": "validateStep",
+ "file": "src/app/registratie/domain/registratie-wizard.machine.ts",
+ "ctype": "miscellaneous",
+ "subtype": "function",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "description": "Validate every question currently visible in ONE step. Errors keyed per field.
\n",
+ "args": [
+ {
+ "name": "step",
+ "type": "StepId",
+ "deprecated": false,
+ "deprecationMessage": ""
+ },
+ {
+ "name": "d",
+ "type": "Draft",
+ "deprecated": false,
+ "deprecationMessage": ""
+ },
+ {
+ "name": "upload",
+ "type": "UploadState",
+ "deprecated": false,
+ "deprecationMessage": ""
+ }
+ ],
+ "returnType": "Result",
+ "jsdoctags": [
+ {
+ "name": "step",
+ "type": "StepId",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ },
+ {
+ "name": "d",
+ "type": "Draft",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ },
+ {
+ "name": "upload",
+ "type": "UploadState",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ }
+ ]
+ }
+ ],
"src/app/registratie/domain/block-actions.ts": [
{
"name": "blockActions",
@@ -35428,6 +35935,33 @@
}
]
},
+ {
+ "name": "parseOrgTemplate",
+ "file": "src/app/brief/infrastructure/brief.adapter.ts",
+ "ctype": "miscellaneous",
+ "subtype": "function",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "description": "",
+ "args": [
+ {
+ "name": "dto",
+ "deprecated": false,
+ "deprecationMessage": ""
+ }
+ ],
+ "returnType": "Result",
+ "jsdoctags": [
+ {
+ "name": "dto",
+ "deprecated": false,
+ "deprecationMessage": "",
+ "tagName": {
+ "text": "param"
+ }
+ }
+ ]
+ },
{
"name": "parsePassage",
"file": "src/app/brief/infrastructure/brief.adapter.ts",
@@ -36622,10 +37156,10 @@
]
}
],
- "src/app/brief/ui/letter-preview/letter-preview.component.ts": [
+ "src/app/brief/ui/letter-canvas/letter-canvas.component.ts": [
{
"name": "groupParagraphs",
- "file": "src/app/brief/ui/letter-preview/letter-preview.component.ts",
+ "file": "src/app/brief/ui/letter-canvas/letter-canvas.component.ts",
"ctype": "miscellaneous",
"subtype": "function",
"deprecated": false,
@@ -38531,13 +39065,13 @@
"kind": 184
}
],
- "src/app/brief/ui/letter-preview/letter-preview.component.ts": [
+ "src/app/brief/ui/letter-canvas/letter-canvas.component.ts": [
{
"name": "PreviewSegment",
"ctype": "miscellaneous",
"subtype": "typealias",
"rawtype": "literal type",
- "file": "src/app/brief/ui/letter-preview/letter-preview.component.ts",
+ "file": "src/app/brief/ui/letter-canvas/letter-canvas.component.ts",
"deprecated": false,
"deprecationMessage": "",
"description": "A run of consecutive lines to render together: a list (bullet/number) or a single plain line.
\n",
@@ -38876,8 +39410,8 @@
"type": "injectable",
"linktype": "injectable",
"name": "BriefStore",
- "coveragePercent": 20,
- "coverageCount": "6/30",
+ "coveragePercent": 22,
+ "coverageCount": "7/31",
"status": "low"
},
{
@@ -39186,6 +39720,24 @@
"coverageCount": "1/1",
"status": "very-good"
},
+ {
+ "filePath": "src/app/brief/domain/org-template.ts",
+ "type": "interface",
+ "linktype": "interface",
+ "name": "Margins",
+ "coveragePercent": 20,
+ "coverageCount": "1/5",
+ "status": "low"
+ },
+ {
+ "filePath": "src/app/brief/domain/org-template.ts",
+ "type": "interface",
+ "linktype": "interface",
+ "name": "OrgTemplate",
+ "coveragePercent": 25,
+ "coverageCount": "3/12",
+ "status": "low"
+ },
{
"filePath": "src/app/brief/domain/placeholders.ts",
"type": "interface",
@@ -39297,8 +39849,8 @@
"type": "interface",
"linktype": "interface",
"name": "BriefView",
- "coveragePercent": 25,
- "coverageCount": "1/4",
+ "coveragePercent": 20,
+ "coverageCount": "1/5",
"status": "low"
},
{
@@ -39391,6 +39943,16 @@
"coverageCount": "0/1",
"status": "low"
},
+ {
+ "filePath": "src/app/brief/infrastructure/brief.adapter.ts",
+ "type": "function",
+ "linktype": "miscellaneous",
+ "linksubtype": "function",
+ "name": "parseOrgTemplate",
+ "coveragePercent": 0,
+ "coverageCount": "0/1",
+ "status": "low"
+ },
{
"filePath": "src/app/brief/infrastructure/brief.adapter.ts",
"type": "function",
@@ -39499,25 +40061,16 @@
"status": "low"
},
{
- "filePath": "src/app/brief/ui/letter-composer/letter-composer.component.ts",
+ "filePath": "src/app/brief/ui/letter-canvas/letter-canvas.component.ts",
"type": "component",
"linktype": "component",
- "name": "LetterComposerComponent",
- "coveragePercent": 3,
- "coverageCount": "1/29",
+ "name": "LetterCanvasComponent",
+ "coveragePercent": 10,
+ "coverageCount": "3/30",
"status": "low"
},
{
- "filePath": "src/app/brief/ui/letter-preview/letter-preview.component.ts",
- "type": "component",
- "linktype": "component",
- "name": "LetterPreviewComponent",
- "coveragePercent": 7,
- "coverageCount": "1/14",
- "status": "low"
- },
- {
- "filePath": "src/app/brief/ui/letter-preview/letter-preview.component.ts",
+ "filePath": "src/app/brief/ui/letter-canvas/letter-canvas.component.ts",
"type": "function",
"linktype": "miscellaneous",
"linksubtype": "function",
@@ -39527,7 +40080,17 @@
"status": "low"
},
{
- "filePath": "src/app/brief/ui/letter-preview/letter-preview.component.ts",
+ "filePath": "src/app/brief/ui/letter-canvas/letter-canvas.component.ts",
+ "type": "variable",
+ "linktype": "miscellaneous",
+ "linksubtype": "variable",
+ "name": "A4_HEIGHT_PX",
+ "coveragePercent": 100,
+ "coverageCount": "1/1",
+ "status": "very-good"
+ },
+ {
+ "filePath": "src/app/brief/ui/letter-canvas/letter-canvas.component.ts",
"type": "variable",
"linktype": "miscellaneous",
"linksubtype": "variable",
@@ -39537,7 +40100,7 @@
"status": "low"
},
{
- "filePath": "src/app/brief/ui/letter-preview/letter-preview.component.ts",
+ "filePath": "src/app/brief/ui/letter-canvas/letter-canvas.component.ts",
"type": "type alias",
"linktype": "miscellaneous",
"linksubtype": "typealias",
@@ -39546,6 +40109,15 @@
"coverageCount": "1/1",
"status": "very-good"
},
+ {
+ "filePath": "src/app/brief/ui/letter-composer/letter-composer.component.ts",
+ "type": "component",
+ "linktype": "component",
+ "name": "LetterComposerComponent",
+ "coveragePercent": 3,
+ "coverageCount": "1/30",
+ "status": "low"
+ },
{
"filePath": "src/app/brief/ui/letter-section/letter-section.component.ts",
"type": "component",
diff --git a/public/letter.css b/public/letter.css
new file mode 100644
index 0000000..870dca7
--- /dev/null
+++ b/public/letter.css
@@ -0,0 +1,197 @@
+/* letter.css — the FE⇄BE letter-rendering CONTRACT (WP-24/WP-25).
+ *
+ * One stylesheet, two consumers: the FE letter canvas loads it via
+ * (index.html + Storybook preview-head), the backend HTML renderer (WP-25)
+ * inlines this same file. Its class-parity test is the fence against drift.
+ *
+ * Class vocabulary: .letter, .letter__letterhead, .letter__body,
+ * .letter__signature, .letter__footer, .letter__page-break.
+ * Margins arrive as --letter-margin-* custom props (mm, from the OrgTemplate);
+ * the defaults below match the seed template.
+ *
+ * Deliberately self-contained: no --rhc- or --bs- tokens — the backend renderer
+ * has no token bridge. Geometry follows the sample voorbeeldbrief-inschrijving
+ * (A4, return address above the envelope window, reference block, footer rule).
+ */
+
+.letter {
+ --letter-margin-top: 25mm;
+ --letter-margin-right: 25mm;
+ --letter-margin-bottom: 25mm;
+ --letter-margin-left: 25mm;
+
+ position: relative;
+ box-sizing: border-box;
+ width: 210mm; /* A4 */
+ max-width: 100%;
+ min-height: 297mm;
+ margin-inline: auto;
+ padding: var(--letter-margin-top) var(--letter-margin-right) var(--letter-margin-bottom)
+ var(--letter-margin-left);
+ background: #fff;
+ color: #1a1a1a;
+ /* Letter typography is the letter's, not the portal UI's. Licensed RO/Rijks
+ fonts are not shipped; system stack mirrors the app-wide decision (ADR-0003). */
+ font-family:
+ system-ui,
+ -apple-system,
+ 'Segoe UI',
+ Roboto,
+ sans-serif;
+ font-size: 10.5pt;
+ line-height: 1.5;
+ display: flex;
+ flex-direction: column;
+}
+
+.letter p {
+ margin: 0 0 0.75em;
+}
+
+.letter ul,
+.letter ol {
+ margin: 0 0 0.75em;
+ padding-inline-start: 1.4em;
+}
+
+/* --- Letterhead: wordmark, return address above the envelope window, reference block --- */
+
+.letter__letterhead {
+ margin-block-end: 12mm;
+}
+
+.letter__letterhead .org-wordmark {
+ font-size: 13pt;
+ font-weight: 700;
+ margin: 0 0 10mm;
+}
+
+.letter__letterhead .return-address {
+ font-style: normal;
+ font-size: 7.5pt;
+ white-space: pre-line;
+ color: #555;
+ margin-block-end: 2mm;
+}
+
+/* Envelope-window position (~C5 venstercouvert): recipient block. */
+.letter__letterhead .address-window {
+ min-height: 22mm;
+ font-style: normal;
+ white-space: pre-line;
+ margin-block-end: 8mm;
+}
+
+.letter__letterhead .reference {
+ display: flex;
+ gap: 10mm;
+ font-size: 8.5pt;
+ color: #333;
+}
+
+.letter__letterhead .reference dt {
+ font-weight: 700;
+ margin: 0;
+}
+
+.letter__letterhead .reference dd {
+ margin: 0;
+}
+
+/* --- Body: the case-type template's sections --- */
+
+.letter__body {
+ flex: 1;
+ /* Above the absolutely-positioned page-break marks: the dashed line stays visible
+ in the gaps but never draws THROUGH opaque content (editors, pickers). */
+ position: relative;
+ z-index: 1;
+}
+
+.letter__body h3 {
+ font-size: inherit;
+ font-weight: 700;
+ margin: 0 0 0.25em;
+}
+
+.letter__body section {
+ margin-block-end: 1.5em;
+}
+
+/* --- Signature --- */
+
+.letter__signature {
+ margin-block-start: 10mm;
+}
+
+.letter__signature p {
+ margin: 0;
+}
+
+.letter__signature .signature-name {
+ margin-block-start: 3em; /* room for the (not-shipped) handwritten signature */
+ font-weight: 700;
+}
+
+/* --- Footer: contact + legal, above a rule --- */
+
+.letter__footer {
+ margin-block-start: 10mm;
+ padding-block-start: 3mm;
+ border-block-start: 0.5pt solid #999;
+ font-size: 7.5pt;
+ color: #555;
+ display: flex;
+ justify-content: space-between;
+ gap: 10mm;
+}
+
+.letter__footer .footer-contact {
+ white-space: pre-line;
+}
+
+.letter__footer .footer-legal {
+ text-align: end;
+}
+
+/* --- Approximate page-break indicator (screen only; PRD §2b honesty rule) ---
+ Positioned per A4-interval by the canvas; the print preview is authoritative. */
+
+.letter__page-break {
+ position: absolute;
+ inset-inline: 0;
+ border-block-start: 1px dashed #b36200;
+ pointer-events: none;
+}
+
+.letter__page-break > span {
+ position: absolute;
+ inset-inline-end: 2mm;
+ inset-block-start: 0;
+ transform: translateY(-50%);
+ /* Above .letter__body: the honesty caption stays legible even over content. */
+ z-index: 2;
+ font-size: 7pt;
+ color: #b36200;
+ background: #fff;
+ padding-inline: 1mm;
+ white-space: nowrap;
+}
+
+/* --- Print: real pages, no indicator chrome --- */
+
+@page {
+ size: A4;
+ margin: 0;
+}
+
+@media print {
+ .letter {
+ width: auto;
+ min-height: 0;
+ }
+
+ .letter__page-break {
+ display: none;
+ }
+}
diff --git a/src/app/brief/application/brief.store.spec.ts b/src/app/brief/application/brief.store.spec.ts
index 85ddc81..bf316c4 100644
--- a/src/app/brief/application/brief.store.spec.ts
+++ b/src/app/brief/application/brief.store.spec.ts
@@ -2,6 +2,7 @@ import { TestBed } from '@angular/core/testing';
import { describe, it, expect } from 'vitest';
import { Result } from '@shared/kernel/fp';
import { Brief, BriefDecisions } from '@brief/domain/brief';
+import { OrgTemplate } from '@brief/domain/org-template';
import { BriefAdapter, BriefView } from '@brief/infrastructure/brief.adapter';
import { BriefStore } from './brief.store';
@@ -22,7 +23,20 @@ const brief: Brief = {
drafterId: 'u1',
};
-const view: BriefView = { brief, availablePassages: [], decisions };
+const orgTemplate: OrgTemplate = {
+ subOrgId: 'cibg-registers',
+ orgName: 'CIBG — Registers',
+ returnAddress: 'Postbus 00000\n2500 AA Den Haag',
+ footerContact: 'info@voorbeeld.example',
+ footerLegal: 'KvK 00000000',
+ signatureName: 'A. de Vries',
+ signatureRole: 'Hoofd Registratie',
+ signatureClosing: 'Met vriendelijke groet,',
+ margins: { topMm: 25, rightMm: 25, bottomMm: 25, leftMm: 25 },
+ version: 1,
+};
+
+const view: BriefView = { brief, availablePassages: [], decisions, orgTemplate };
function setup(adapter: Partial): BriefStore {
TestBed.configureTestingModule({ providers: [{ provide: BriefAdapter, useValue: adapter }] });
diff --git a/src/app/brief/application/brief.store.ts b/src/app/brief/application/brief.store.ts
index 06d3bdf..b9a3bf2 100644
--- a/src/app/brief/application/brief.store.ts
+++ b/src/app/brief/application/brief.store.ts
@@ -10,6 +10,7 @@ import {
unresolvedPlaceholders,
} from '@brief/domain/brief';
import { BriefMsg, BriefState, initial, reduce } from '@brief/domain/brief.machine';
+import { OrgTemplate } from '@brief/domain/org-template';
import { BriefAdapter, BriefView } from '@brief/infrastructure/brief.adapter';
/** Transient action state (submit/approve/reject/send/resetDemo) — one tagged union
@@ -48,6 +49,11 @@ export class BriefStore {
/** Surfaced autosave state for the indicator + aria-live region. */
readonly saveState = signal({ tag: 'Idle' });
+ /** The org template the letter renders with (WP-24). Server-owned appearance data,
+ not letter state — held beside the machine, never inside it (`brief.machine.ts`
+ stays untouched by design). Set from every server view that carries it. */
+ readonly orgTemplate = signal(null);
+
/** The load lifecycle as `RemoteData`, for `` — the machine keeps
owning the letter's own domain lifecycle (draft/submitted/approved/…); this is
purely a projection of its loading/failed tags onto the shared async seam. */
@@ -87,8 +93,12 @@ export class BriefStore {
async load() {
const r = await this.adapter.load();
- if (r.ok) this.store.dispatch({ tag: 'BriefLoaded', ...r.value });
- else this.store.dispatch({ tag: 'BriefLoadFailed', reason: r.error });
+ if (r.ok) {
+ this.orgTemplate.set(r.value.orgTemplate);
+ this.store.dispatch({ tag: 'BriefLoaded', ...r.value });
+ } else {
+ this.store.dispatch({ tag: 'BriefLoadFailed', reason: r.error });
+ }
}
/** An edit: apply it optimistically in the pure reducer, then debounce-save. */
@@ -125,6 +135,7 @@ export class BriefStore {
this.saveState.set({ tag: 'Idle' });
if (r.ok) {
this.actionState.set({ tag: 'Idle' });
+ this.orgTemplate.set(r.value.orgTemplate);
this.store.dispatch({ tag: 'BriefLoaded', ...r.value });
} else {
this.actionState.set({ tag: 'Failed', error: r.error });
@@ -152,6 +163,8 @@ export class BriefStore {
}
private applyServerStatus(view: BriefView) {
+ // `send` pins the org-template version server-side — mirror whatever came back.
+ this.orgTemplate.set(view.orgTemplate);
const { brief, decisions } = view;
const s = brief.status;
switch (s.tag) {
diff --git a/src/app/brief/domain/org-template.ts b/src/app/brief/domain/org-template.ts
new file mode 100644
index 0000000..d97a05f
--- /dev/null
+++ b/src/app/brief/domain/org-template.ts
@@ -0,0 +1,31 @@
+/**
+ * The organization template (Brief v2 PRD §3, WP-23/24): the SECOND template axis —
+ * appearance/identity per sub-organization (letterhead, footer, signature, margins).
+ * Orthogonal to the case-type template (sections + placeholders); the two only meet
+ * at render time, on the letter canvas. Server-owned: the FE renders it verbatim,
+ * never edits it here (the admin editor is WP-26).
+ */
+
+export interface Margins {
+ readonly topMm: number;
+ readonly rightMm: number;
+ readonly bottomMm: number;
+ readonly leftMm: number;
+}
+
+export interface OrgTemplate {
+ readonly subOrgId: string;
+ readonly orgName: string;
+ /** Multiline; rendered above the envelope window. */
+ readonly returnAddress: string;
+ readonly logoDocumentId?: string;
+ /** Multiline contact block in the footer. */
+ readonly footerContact: string;
+ readonly footerLegal: string;
+ readonly signatureName: string;
+ readonly signatureRole: string;
+ readonly signatureClosing: string;
+ readonly margins: Margins;
+ /** 0 = draft; n>0 = the published snapshot this letter renders with. */
+ readonly version: number;
+}
diff --git a/src/app/brief/infrastructure/brief.adapter.spec.ts b/src/app/brief/infrastructure/brief.adapter.spec.ts
index 6b6b678..5cdeebf 100644
--- a/src/app/brief/infrastructure/brief.adapter.spec.ts
+++ b/src/app/brief/infrastructure/brief.adapter.spec.ts
@@ -1,6 +1,12 @@
import { describe, it, expect } from 'vitest';
import { BriefViewDto } from '@shared/infrastructure/api-client';
-import { parseBrief, parseBriefView, parseNode, parseStatus } from './brief.adapter';
+import {
+ parseBrief,
+ parseBriefView,
+ parseNode,
+ parseOrgTemplate,
+ parseStatus,
+} from './brief.adapter';
const view: BriefViewDto = {
brief: {
@@ -47,6 +53,18 @@ const view: BriefViewDto = {
},
],
decisions: { canEdit: false, canApprove: true, canReject: true, canSend: false },
+ orgTemplate: {
+ subOrgId: 'cibg-registers',
+ orgName: 'CIBG — Registers',
+ returnAddress: 'BIG-register\nPostbus 00000\n2500 AA Den Haag',
+ footerContact: 'www.bigregister.nl\ninfo@voorbeeld.example',
+ footerLegal: 'KvK 00000000',
+ signatureName: 'A. de Vries',
+ signatureRole: 'Hoofd Registratie',
+ signatureClosing: 'Met vriendelijke groet,',
+ margins: { topMm: 25, rightMm: 25, bottomMm: 25, leftMm: 25 },
+ version: 1,
+ },
};
describe('brief.adapter parse boundary', () => {
@@ -76,6 +94,26 @@ describe('brief.adapter parse boundary', () => {
});
});
+ it('parses the org template and drops a null logoDocumentId', () => {
+ const r = parseOrgTemplate(view.orgTemplate);
+ expect(r.ok).toBe(true);
+ if (!r.ok) return;
+ expect(r.value.orgName).toBe('CIBG — Registers');
+ expect(r.value.margins).toEqual({ topMm: 25, rightMm: 25, bottomMm: 25, leftMm: 25 });
+ expect('logoDocumentId' in r.value).toBe(false);
+ });
+
+ it('rejects a view whose org template is missing or malformed', () => {
+ expect(parseBriefView({ ...view, orgTemplate: undefined }).ok).toBe(false);
+ expect(parseOrgTemplate({ ...view.orgTemplate, signatureName: undefined }).ok).toBe(false);
+ expect(
+ parseOrgTemplate({
+ ...view.orgTemplate,
+ margins: { topMm: 25, rightMm: 25, bottomMm: 25 },
+ }).ok,
+ ).toBe(false);
+ });
+
it('rejects a view whose decisions are missing or malformed', () => {
expect(parseBriefView({ ...view, decisions: undefined as never }).ok).toBe(false);
expect(
diff --git a/src/app/brief/infrastructure/brief.adapter.ts b/src/app/brief/infrastructure/brief.adapter.ts
index ad5e95f..b801182 100644
--- a/src/app/brief/infrastructure/brief.adapter.ts
+++ b/src/app/brief/infrastructure/brief.adapter.ts
@@ -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 {
+ 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 {
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) ---
diff --git a/src/app/brief/ui/brief.page.ts b/src/app/brief/ui/brief.page.ts
index 226aaa4..7270afb 100644
--- a/src/app/brief/ui/brief.page.ts
+++ b/src/app/brief/ui/brief.page.ts
@@ -40,28 +40,31 @@ import { LetterComposerComponent } from '@brief/ui/letter-composer/letter-compos
@if (loaded(); as s) {
-
-
+ @if (store.orgTemplate(); as orgTemplate) {
+
+
+ }
}
diff --git a/src/app/brief/ui/letter-canvas/letter-canvas.component.ts b/src/app/brief/ui/letter-canvas/letter-canvas.component.ts
new file mode 100644
index 0000000..9db4121
--- /dev/null
+++ b/src/app/brief/ui/letter-canvas/letter-canvas.component.ts
@@ -0,0 +1,317 @@
+import {
+ Component,
+ DestroyRef,
+ ElementRef,
+ computed,
+ effect,
+ inject,
+ input,
+ output,
+ signal,
+ viewChild,
+} from '@angular/core';
+import { NgTemplateOutlet } from '@angular/common';
+import { ButtonComponent } from '@shared/ui/button/button.component';
+import { PlaceholderChipComponent } from '@shared/ui/placeholder-chip/placeholder-chip.component';
+import { PlaceholderOption } from '@shared/ui/rich-text-editor/rich-text-editor.component';
+import { formatDatumNl } from '@shared/kernel/datum';
+import { Paragraph } from '@shared/kernel/rich-text';
+import { Brief, LetterBlock, LibraryPassage } from '@brief/domain/brief';
+import { OrgTemplate } from '@brief/domain/org-template';
+import { Diagnostic } from '@brief/domain/placeholders';
+import { BriefMsg } from '@brief/domain/brief.machine';
+import { LetterSectionComponent } from '@brief/ui/letter-section/letter-section.component';
+
+/** A run of consecutive lines to render together: a list (bullet/number) or a single plain line. */
+type PreviewSegment = {
+ readonly list: 'bullet' | 'number' | null;
+ readonly items: readonly Paragraph[];
+};
+
+function groupParagraphs(paras: readonly Paragraph[]): PreviewSegment[] {
+ const out: { list: 'bullet' | 'number' | null; items: Paragraph[] }[] = [];
+ for (const para of paras) {
+ const kind = para.list ?? null;
+ const last = out[out.length - 1];
+ if (kind && last && last.list === kind) last.items.push(para);
+ else out.push({ list: kind, items: [para] });
+ }
+ return out;
+}
+
+// Illustrative values for the "Voorbeeld" toggle — what send resolves server-side.
+const SAMPLE_VALUES: Record = {
+ naam_zorgverlener: 'J. Jansen',
+ big_nummer: '12345678901',
+};
+
+/** A4 height in CSS px (1in = 96px = 25.4mm) — for the approximate page-break marks. */
+const A4_HEIGHT_PX = (297 * 96) / 25.4;
+
+/** Organism: the letter as one surface — the org template's letterhead, signature and
+ footer around the case-type template's sections. `editableRegions` picks who edits
+ what: `'content'` hosts the editable letter-sections in place (drafter), `'none'`
+ renders everything read-only (approver/locked, absorbs the old letter-preview),
+ `'template'` reserves the org-identity regions for the admin editor (WP-26).
+ Letter typography/geometry come from the shared `public/letter.css` contract —
+ the same file the backend preview renderer inlines (WP-25). */
+@Component({
+ selector: 'app-letter-canvas',
+ imports: [NgTemplateOutlet, ButtonComponent, PlaceholderChipComponent, LetterSectionComponent],
+ styles: [
+ `
+ :host {
+ display: block;
+ }
+ .toolbar {
+ display: flex;
+ justify-content: flex-end;
+ margin-block-end: var(--rhc-space-max-sm);
+ }
+ /* Portal-side chrome around the letter surface (not part of the contract file). */
+ .surface {
+ background: var(--rhc-color-grijs-100, #f0f0f0);
+ border: var(--rhc-border-width-sm) solid var(--rhc-color-border-default);
+ border-radius: var(--rhc-border-radius-md);
+ padding: var(--rhc-space-max-lg);
+ overflow-x: auto;
+ }
+ .surface .letter {
+ box-shadow: 0 1px 4px rgb(0 0 0 / 0.15);
+ }
+ /* Org-identity regions: visibly not the drafter's to edit. */
+ .from-template {
+ background: var(--rhc-color-grijs-100, #f5f5f5);
+ outline: 2mm solid var(--rhc-color-grijs-100, #f5f5f5);
+ }
+ .from-template-caption {
+ font-size: 7.5pt;
+ /* grijs-700, not foreground-subtle: subtle misses AA contrast on the tint. */
+ color: var(--rhc-color-grijs-700, #334155);
+ margin: 0 0 2mm;
+ }
+ `,
+ ],
+ template: `
+
+ @for (node of nodes; track $index) {
+ @switch (node.type) {
+ @case ('text') {
+ {{ node.text }}
+ }
+ @case ('lineBreak') {
+
+ }
+ @case ('placeholder') {
+ @if (showSample() && autoFor(node.key)) {
+ {{ sampleFor(node.key) }}
+ } @else {
+
+ }
+ }
+ }
+ }
+
+
+ @if (editableRegions() === 'none') {
+
+ }
+
+
+
+
+
+ @if (tintTemplate()) {
+
{{ fromTemplateCaption() }}
+ }
+
{{ orgTemplate().orgName }}
+
{{ orgTemplate().returnAddress }}
+
{{ recipientText() }}
+
+
+
- {{ referenceLabel() }}
+ - {{ brief().briefId }}
+
+
+
- {{ dateLabel() }}
+ - {{ letterDate }}
+
+
+
+
+
+ @if (editableRegions() === 'content') {
+ @for (section of brief().sections; track section.sectionKey) {
+
+ }
+ } @else {
+ @for (section of brief().sections; track section.sectionKey) {
+
+ {{ section.title }}
+ @for (block of section.blocks; track block.blockId) {
+ @for (seg of segmentsOf(block); track $index) {
+ @if (seg.list === 'bullet') {
+
+ @for (para of seg.items; track $index) {
+ -
+
+
+ }
+
+ } @else if (seg.list === 'number') {
+
+ @for (para of seg.items; track $index) {
+ -
+
+
+ }
+
+ } @else {
+
+
+
+ }
+ }
+ }
+
+ }
+ }
+
+
+
+
{{ orgTemplate().signatureClosing }}
+
{{ orgTemplate().signatureName }}
+
{{ orgTemplate().signatureRole }}
+
+
+
+
+ @for (top of pageBreaks(); track $index) {
+
+ {{ pageBreakCaption() }}
+
+ }
+
+
+ `,
+})
+export class LetterCanvasComponent {
+ brief = input.required();
+ orgTemplate = input.required();
+ /** Who edits what on the surface: drafter ('content'), read-only ('none'),
+ admin editor ('template', consumer arrives in WP-26). */
+ editableRegions = input<'content' | 'template' | 'none'>('none');
+ availablePassages = input([]);
+ placeholders = input([]);
+ diagnostics = input([]);
+ zoom = input(1);
+ edit = output();
+
+ showSampleLabel = input($localize`:@@brief.preview.showSample:Voorbeeld met testwaarden`);
+ hideSampleLabel = input($localize`:@@brief.preview.hideSample:Testwaarden verbergen`);
+ fromTemplateCaption = input(
+ $localize`:@@brief.canvas.fromTemplate:Komt uit de huisstijl van de organisatie — niet bewerkbaar in de brief.`,
+ );
+ pageBreakCaption = input(
+ $localize`:@@brief.canvas.pageBreak:±pagina-einde — afdrukvoorbeeld is leidend`,
+ );
+ recipientText = input(
+ $localize`:@@brief.canvas.recipient:Adres van de geadresseerde\n(wordt ingevuld bij verzending)`,
+ );
+ referenceLabel = input($localize`:@@brief.canvas.reference:Ons kenmerk`);
+ dateLabel = input($localize`:@@brief.canvas.date:Datum`);
+
+ protected showSample = signal(false);
+ protected letterDate = formatDatumNl(new Date());
+
+ /** The letterhead/signature/footer are tinted "not yours" only while composing —
+ in 'none' the whole surface is read-only, in 'template' they ARE the editable focus. */
+ protected tintTemplate = computed(() => this.editableRegions() === 'content');
+
+ protected marginStyle = computed(() => {
+ const m = this.orgTemplate().margins;
+ return {
+ '--letter-margin-top': `${m.topMm}mm`,
+ '--letter-margin-right': `${m.rightMm}mm`,
+ '--letter-margin-bottom': `${m.bottomMm}mm`,
+ '--letter-margin-left': `${m.leftMm}mm`,
+ };
+ });
+
+ // --- read-only rendering helpers (migrated from the superseded letter-preview) ---
+
+ private defs = computed(() => new Map(this.brief().placeholders.map((p) => [p.key, p])));
+ private worst = computed(() => {
+ const m = new Map();
+ for (const d of this.diagnostics()) {
+ if (!d.placeholderKey) continue;
+ if (d.severity === 'error') m.set(d.placeholderKey, 'error');
+ else if (!m.has(d.placeholderKey)) m.set(d.placeholderKey, 'warning');
+ }
+ return m;
+ });
+
+ protected segmentsOf = (block: LetterBlock) => groupParagraphs(block.content.paragraphs);
+ protected labelFor = (key: string) => this.defs().get(key)?.label ?? key;
+ protected autoFor = (key: string) => this.defs().get(key)?.autoResolvable ?? false;
+ protected stateFor = (key: string): 'ok' | 'warning' | 'error' => this.worst().get(key) ?? 'ok';
+ protected sampleFor = (key: string) =>
+ SAMPLE_VALUES[key] ?? (key === 'datum' ? this.letterDate : this.labelFor(key));
+
+ // --- approximate page-break marks (PRD §2b: honest "±", print preview is leading) ---
+
+ private page = viewChild>('page');
+ protected pageBreaks = signal([]);
+
+ constructor() {
+ // ponytail: whole-surface height / A4-interval — ignores that a break never truly
+ // falls mid-line; the caption says "±" and WP-25's server preview is authoritative.
+ const observer = new ResizeObserver(([entry]) => {
+ // ~1cm tolerance so a letter ending on a page boundary gets no edge-hugging mark.
+ const pages = Math.ceil((entry.target.scrollHeight - 40) / A4_HEIGHT_PX);
+ this.pageBreaks.set(
+ Array.from({ length: Math.max(0, pages - 1) }, (_, i) => (i + 1) * A4_HEIGHT_PX),
+ );
+ });
+ effect((onCleanup) => {
+ const el = this.page()?.nativeElement;
+ if (!el) return;
+ observer.observe(el);
+ onCleanup(() => observer.unobserve(el));
+ });
+ inject(DestroyRef).onDestroy(() => observer.disconnect());
+ }
+}
diff --git a/src/app/brief/ui/letter-canvas/letter-canvas.stories.ts b/src/app/brief/ui/letter-canvas/letter-canvas.stories.ts
new file mode 100644
index 0000000..6625949
--- /dev/null
+++ b/src/app/brief/ui/letter-canvas/letter-canvas.stories.ts
@@ -0,0 +1,154 @@
+import type { Meta, StoryObj } from '@storybook/angular';
+import { Brief, LibraryPassage, allDiagnostics } from '@brief/domain/brief';
+import { OrgTemplate } from '@brief/domain/org-template';
+import { LetterCanvasComponent } from './letter-canvas.component';
+
+const orgTemplate: OrgTemplate = {
+ subOrgId: 'cibg-registers',
+ orgName: 'CIBG — Registers',
+ returnAddress: 'BIG-register\nPostbus 00000\n2500 AA Den Haag',
+ footerContact: 'www.bigregister.nl\ninfo@voorbeeld.example\n070 000 00 00',
+ footerLegal: 'Ons kenmerk vermelden bij correspondentie.',
+ signatureName: 'A. de Vries',
+ signatureRole: 'Hoofd Registratie',
+ signatureClosing: 'Met vriendelijke groet,',
+ margins: { topMm: 25, rightMm: 25, bottomMm: 25, leftMm: 25 },
+ version: 1,
+};
+
+const passages: LibraryPassage[] = [
+ {
+ passageId: 'p1',
+ scope: 'global',
+ sectionKey: 'kern',
+ label: 'Standaard toelichting',
+ version: 1,
+ content: { paragraphs: [{ nodes: [{ type: 'text', text: 'Standaardtekst.' }] }] },
+ },
+];
+
+const brief: Brief = {
+ briefId: 'b1',
+ beroep: 'arts',
+ templateId: 't1',
+ drafterId: 'demo-drafter',
+ status: { tag: 'draft' },
+ placeholders: [
+ { key: 'naam_zorgverlener', label: 'Naam zorgverlener', autoResolvable: true },
+ { key: 'reden_besluit', label: 'Reden besluit', autoResolvable: false },
+ ],
+ sections: [
+ {
+ sectionKey: 'aanhef',
+ title: 'Aanhef',
+ required: true,
+ locked: true,
+ blocks: [
+ {
+ type: 'passage',
+ blockId: 'local-1',
+ sourcePassageId: 'p1',
+ sourceVersion: 1,
+ edited: false,
+ content: {
+ paragraphs: [
+ {
+ nodes: [
+ { type: 'text', text: 'Geachte heer/mevrouw ' },
+ { type: 'placeholder', key: 'naam_zorgverlener' },
+ { type: 'text', text: ',' },
+ ],
+ },
+ ],
+ },
+ },
+ ],
+ },
+ {
+ sectionKey: 'kern',
+ title: 'Kern van het besluit',
+ required: true,
+ locked: false,
+ blocks: [
+ {
+ type: 'freeText',
+ blockId: 'local-2',
+ content: {
+ paragraphs: [
+ {
+ nodes: [
+ { type: 'text', text: 'Wij hebben besloten om reden ' },
+ { type: 'placeholder', key: 'reden_besluit' },
+ { type: 'text', text: '.' },
+ ],
+ },
+ ],
+ },
+ },
+ ],
+ },
+ ],
+};
+
+/** Enough repeated body text to push the surface past one A4 page. */
+const longBrief: Brief = {
+ ...brief,
+ sections: brief.sections.map((s) =>
+ s.sectionKey === 'kern'
+ ? {
+ ...s,
+ blocks: [
+ {
+ type: 'freeText',
+ blockId: 'local-long',
+ content: {
+ paragraphs: Array.from({ length: 40 }, (_, i) => ({
+ nodes: [
+ {
+ type: 'text' as const,
+ text: `Alinea ${i + 1}: de beoordeling van uw aanvraag is uitgevoerd volgens de geldende regels voor herregistratie in het BIG-register.`,
+ },
+ ],
+ })),
+ },
+ },
+ ],
+ }
+ : s,
+ ),
+};
+
+const meta: Meta = {
+ title: 'Domein/Brief/Letter Canvas',
+ component: LetterCanvasComponent,
+ args: {
+ brief,
+ orgTemplate,
+ availablePassages: passages,
+ placeholders: brief.placeholders,
+ diagnostics: allDiagnostics(brief),
+ },
+};
+export default meta;
+type Story = StoryObj;
+
+/** Drafter: content blocks editable in place; org-identity regions tinted read-only. */
+export const ContentMode: Story = { args: { editableRegions: 'content' } };
+
+/** Approver/locked: the identical surface fully read-only, with the sample-values
+ toggle and diagnostic placeholder chips (absorbs the old Letter Preview). */
+export const ReadOnly: Story = { args: { editableRegions: 'none' } };
+
+export const ReadOnlyZonderBevindingen: Story = {
+ args: { editableRegions: 'none', diagnostics: [] },
+};
+
+/** Admin editor focus (consumer arrives in WP-26): body read-only, no "not yours" tint. */
+export const TemplateMode: Story = { args: { editableRegions: 'template' } };
+
+export const Zoomed: Story = { args: { editableRegions: 'none', zoom: 0.6 } };
+
+/** Long letter: the approximate ±page-break marks appear per A4 interval. */
+export const PageBreak: Story = {
+ args: { editableRegions: 'none', brief: longBrief, diagnostics: [] },
+};
diff --git a/src/app/brief/ui/letter-composer/letter-composer.component.ts b/src/app/brief/ui/letter-composer/letter-composer.component.ts
index b989451..da72cb7 100644
--- a/src/app/brief/ui/letter-composer/letter-composer.component.ts
+++ b/src/app/brief/ui/letter-composer/letter-composer.component.ts
@@ -5,18 +5,18 @@ import { ButtonComponent } from '@shared/ui/button/button.component';
import { AlertComponent } from '@shared/ui/alert/alert.component';
import { PlaceholderOption } from '@shared/ui/rich-text-editor/rich-text-editor.component';
import { Brief, LibraryPassage } from '@brief/domain/brief';
+import { OrgTemplate } from '@brief/domain/org-template';
import { Diagnostic } from '@brief/domain/placeholders';
import { BriefMsg } from '@brief/domain/brief.machine';
-import { LetterSectionComponent } from '@brief/ui/letter-section/letter-section.component';
-import { LetterPreviewComponent } from '@brief/ui/letter-preview/letter-preview.component';
+import { LetterCanvasComponent } from '@brief/ui/letter-canvas/letter-canvas.component';
import { DiagnosticsPanelComponent } from '@brief/ui/diagnostics-panel/diagnostics-panel.component';
import { RejectionCommentsComponent } from '@brief/ui/rejection-comments/rejection-comments.component';
-/** Organism: the whole letter — status badge, the editable sections (drafter) or a
- read-only preview (approver / locked), the diagnostics panel, and the action bar
- appropriate to status × permission. Presentational: emits edit + transition
- intents; the canEdit/canApprove/canReject/canSend inputs are server-computed
- decision flags (PRD-0002 phase P1) — this component never derives them. */
+/** Organism: the whole letter flow — status badge, the letter canvas (editable in
+ place for the drafter, read-only for approver/locked), the diagnostics panel, and
+ the action bar appropriate to status × permission. Presentational: emits edit +
+ transition intents; the canEdit/canApprove/canReject/canSend inputs are
+ server-computed decision flags (PRD-0002 phase P1) — this component never derives them. */
@Component({
selector: 'app-letter-composer',
imports: [
@@ -24,8 +24,7 @@ import { RejectionCommentsComponent } from '@brief/ui/rejection-comments/rejecti
StatusBadgeComponent,
ButtonComponent,
AlertComponent,
- LetterSectionComponent,
- LetterPreviewComponent,
+ LetterCanvasComponent,
DiagnosticsPanelComponent,
RejectionCommentsComponent,
],
@@ -42,10 +41,6 @@ import { RejectionCommentsComponent } from '@brief/ui/rejection-comments/rejecti
flex-wrap: wrap;
margin-block-end: var(--rhc-space-max-lg);
}
- .sections {
- display: grid;
- gap: var(--rhc-space-max-2xl);
- }
.panel {
margin-block: var(--rhc-space-max-xl);
}
@@ -68,21 +63,15 @@ import { RejectionCommentsComponent } from '@brief/ui/rejection-comments/rejecti
}
- @if (canEdit()) {
-
- @for (section of brief().sections; track section.sectionKey) {
-
- }
-
- } @else {
-
- }
+
@@ -139,6 +128,7 @@ import { RejectionCommentsComponent } from '@brief/ui/rejection-comments/rejecti
})
export class LetterComposerComponent {
brief = input.required
();
+ orgTemplate = input.required();
availablePassages = input([]);
diagnostics = input([]);
canEdit = input(false);
diff --git a/src/app/brief/ui/letter-composer/letter-composer.stories.ts b/src/app/brief/ui/letter-composer/letter-composer.stories.ts
index bcdf3ef..7e8c5b3 100644
--- a/src/app/brief/ui/letter-composer/letter-composer.stories.ts
+++ b/src/app/brief/ui/letter-composer/letter-composer.stories.ts
@@ -2,6 +2,20 @@ import type { Meta, StoryObj } from '@storybook/angular';
import { LetterComposerComponent } from './letter-composer.component';
import { Brief, BriefDecisions, BriefStatus, LibraryPassage } from '@brief/domain/brief';
import { allDiagnostics } from '@brief/domain/brief';
+import { OrgTemplate } from '@brief/domain/org-template';
+
+const orgTemplate: OrgTemplate = {
+ subOrgId: 'cibg-registers',
+ orgName: 'CIBG — Registers',
+ returnAddress: 'BIG-register\nPostbus 00000\n2500 AA Den Haag',
+ footerContact: 'www.bigregister.nl\ninfo@voorbeeld.example',
+ footerLegal: 'Ons kenmerk vermelden bij correspondentie.',
+ signatureName: 'A. de Vries',
+ signatureRole: 'Hoofd Registratie',
+ signatureClosing: 'Met vriendelijke groet,',
+ margins: { topMm: 25, rightMm: 25, bottomMm: 25, leftMm: 25 },
+ version: 1,
+};
const passages: LibraryPassage[] = [
{
@@ -106,15 +120,16 @@ function brief(status: BriefStatus): Brief {
const render = (b: Brief, decisions: BriefDecisions) => ({
props: {
brief: b,
+ orgTemplate,
availablePassages: passages,
diagnostics: allDiagnostics(b),
...decisions,
canSubmit: true,
busy: false,
},
- template: ``,
+ template: ``,
});
const meta: Meta = {
diff --git a/src/app/brief/ui/letter-preview/letter-preview.component.ts b/src/app/brief/ui/letter-preview/letter-preview.component.ts
deleted file mode 100644
index 7f1b9ce..0000000
--- a/src/app/brief/ui/letter-preview/letter-preview.component.ts
+++ /dev/null
@@ -1,175 +0,0 @@
-import { Component, computed, input, signal } from '@angular/core';
-import { NgTemplateOutlet } from '@angular/common';
-import { HeadingComponent } from '@shared/ui/heading/heading.component';
-import { ButtonComponent } from '@shared/ui/button/button.component';
-import { PlaceholderChipComponent } from '@shared/ui/placeholder-chip/placeholder-chip.component';
-import { formatDatumNl } from '@shared/kernel/datum';
-import { Paragraph } from '@shared/kernel/rich-text';
-import { Brief, LetterBlock } from '@brief/domain/brief';
-import { Diagnostic } from '@brief/domain/placeholders';
-
-/** A run of consecutive lines to render together: a list (bullet/number) or a single plain line. */
-type PreviewSegment = {
- readonly list: 'bullet' | 'number' | null;
- readonly items: readonly Paragraph[];
-};
-
-function groupParagraphs(paras: readonly Paragraph[]): PreviewSegment[] {
- const out: { list: 'bullet' | 'number' | null; items: Paragraph[] }[] = [];
- for (const para of paras) {
- const kind = para.list ?? null;
- const last = out[out.length - 1];
- if (kind && last && last.list === kind) last.items.push(para);
- else out.push({ list: kind, items: [para] });
- }
- return out;
-}
-
-// Illustrative values for the "Voorbeeld" toggle — what send resolves server-side.
-const SAMPLE_VALUES: Record = {
- naam_zorgverlener: 'J. Jansen',
- big_nummer: '12345678901',
-};
-
-/** Organism: read-only rendering of the letter as the approver/recipient sees it.
- Placeholders show as labelled chips (values are resolved server-side only at send);
- a chip flagged by the linter shows its error/warning state. The "Voorbeeld" toggle
- fills auto-resolvable placeholders with sample values to preview the sent result. */
-@Component({
- selector: 'app-letter-preview',
- imports: [NgTemplateOutlet, HeadingComponent, ButtonComponent, PlaceholderChipComponent],
- styles: [
- `
- :host {
- display: block;
- }
- .toolbar {
- display: flex;
- justify-content: flex-end;
- margin-block-end: var(--rhc-space-max-sm);
- }
- .letter {
- background: var(--rhc-color-wit);
- border: var(--rhc-border-width-sm) solid var(--rhc-color-border-default);
- border-radius: var(--rhc-border-radius-md);
- padding: var(--rhc-space-max-2xl);
- }
- section {
- margin-block-end: var(--rhc-space-max-xl);
- }
- p {
- margin: 0 0 var(--rhc-space-max-sm);
- }
- ul,
- ol {
- margin: 0 0 var(--rhc-space-max-sm);
- padding-inline-start: 1.4em;
- }
- `,
- ],
- template: `
-
- @for (node of nodes; track $index) {
- @switch (node.type) {
- @case ('text') {
- {{ node.text }}
- }
- @case ('lineBreak') {
-
- }
- @case ('placeholder') {
- @if (showSample() && autoFor(node.key)) {
- {{ sampleFor(node.key) }}
- } @else {
-
- }
- }
- }
- }
-
-
-
-
-
- @for (section of brief().sections; track section.sectionKey) {
-
- {{ section.title }}
- @for (block of section.blocks; track block.blockId) {
- @for (seg of segmentsOf(block); track $index) {
- @if (seg.list === 'bullet') {
-
- @for (para of seg.items; track $index) {
- -
-
-
- }
-
- } @else if (seg.list === 'number') {
-
- @for (para of seg.items; track $index) {
- -
-
-
- }
-
- } @else {
-
-
-
- }
- }
- }
-
- }
-
- `,
-})
-export class LetterPreviewComponent {
- brief = input.required();
- diagnostics = input([]);
-
- showSampleLabel = input($localize`:@@brief.preview.showSample:Voorbeeld met testwaarden`);
- hideSampleLabel = input($localize`:@@brief.preview.hideSample:Testwaarden verbergen`);
-
- protected showSample = signal(false);
- private today = formatDatumNl(new Date());
-
- private defs = computed(() => new Map(this.brief().placeholders.map((p) => [p.key, p])));
- private worst = computed(() => {
- const m = new Map();
- for (const d of this.diagnostics()) {
- if (!d.placeholderKey) continue;
- if (d.severity === 'error') m.set(d.placeholderKey, 'error');
- else if (!m.has(d.placeholderKey)) m.set(d.placeholderKey, 'warning');
- }
- return m;
- });
-
- protected segmentsOf = (block: LetterBlock) => groupParagraphs(block.content.paragraphs);
- protected labelFor = (key: string) => this.defs().get(key)?.label ?? key;
- protected autoFor = (key: string) => this.defs().get(key)?.autoResolvable ?? false;
- protected stateFor = (key: string): 'ok' | 'warning' | 'error' => this.worst().get(key) ?? 'ok';
- protected sampleFor = (key: string) =>
- SAMPLE_VALUES[key] ?? (key === 'datum' ? this.today : this.labelFor(key));
-}
diff --git a/src/app/brief/ui/letter-preview/letter-preview.stories.ts b/src/app/brief/ui/letter-preview/letter-preview.stories.ts
deleted file mode 100644
index f3678fe..0000000
--- a/src/app/brief/ui/letter-preview/letter-preview.stories.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-import type { Meta, StoryObj } from '@storybook/angular';
-import { Brief } from '@brief/domain/brief';
-import { Diagnostic } from '@brief/domain/placeholders';
-import { LetterPreviewComponent } from './letter-preview.component';
-
-const brief: Brief = {
- briefId: 'b1',
- beroep: 'arts',
- templateId: 't1',
- drafterId: 'demo-drafter',
- status: { tag: 'draft' },
- placeholders: [
- { key: 'naam_zorgverlener', label: 'Naam zorgverlener', autoResolvable: true },
- { key: 'reden_besluit', label: 'Reden besluit', autoResolvable: false },
- ],
- sections: [
- {
- sectionKey: 'aanhef',
- title: 'Aanhef',
- required: true,
- locked: true,
- blocks: [
- {
- type: 'passage',
- blockId: 'local-1',
- sourcePassageId: 'p1',
- sourceVersion: 1,
- edited: false,
- content: {
- paragraphs: [
- {
- nodes: [
- { type: 'text', text: 'Geachte heer/mevrouw ' },
- { type: 'placeholder', key: 'naam_zorgverlener' },
- { type: 'text', text: ',' },
- ],
- },
- ],
- },
- },
- ],
- },
- {
- sectionKey: 'kern',
- title: 'Kern van het besluit',
- required: true,
- locked: false,
- blocks: [
- {
- 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 diagnostics: Diagnostic[] = [
- {
- severity: 'warning',
- code: 'unresolved-at-send',
- placeholderKey: 'reden_besluit',
- location: { blockId: 'local-2', paragraphIndex: 0, nodeIndex: 1 },
- message: '"Reden besluit" is nog niet ingevuld.',
- },
-];
-
-const meta: Meta = {
- title: 'Domein/Brief/Letter Preview',
- component: LetterPreviewComponent,
- args: { brief, diagnostics },
-};
-export default meta;
-type Story = StoryObj;
-
-export const Default: Story = {};
-export const ZonderBevindingen: Story = { args: { diagnostics: [] } };
diff --git a/src/app/brief/ui/passage-picker/passage-picker.component.ts b/src/app/brief/ui/passage-picker/passage-picker.component.ts
index 59b8378..652a425 100644
--- a/src/app/brief/ui/passage-picker/passage-picker.component.ts
+++ b/src/app/brief/ui/passage-picker/passage-picker.component.ts
@@ -14,6 +14,7 @@ import { LibraryPassage } from '@brief/domain/brief';
`
:host {
display: block;
+ background: var(--rhc-color-wit, #fff);
border: var(--rhc-border-width-sm) solid var(--rhc-color-border-default);
border-radius: var(--rhc-border-radius-md);
padding: var(--rhc-space-max-md);
@@ -35,6 +36,7 @@ import { LibraryPassage } from '@brief/domain/brief';
@for (p of passages(); track p.passageId) {
+
+