import { Besluit, LetterBlock, LibraryPassage } from './brief'; /** * Guided drafting: given the behandelaar's besluit + chosen redenen, which library * passages belong in the kern. This is the "don't make them a detective" logic — * pure, so it's unit-tested directly and the UI just renders the result. * * A passage is offered when: * - it has no besluit tag (a shared intro/toelichting, relevant to any besluit), OR * - its besluit matches AND either it isn't reason-specific, or its reason is ticked. * * Kept in library order (server order = reading order), so an inserted set already * flows as a letter. */ export function passagesForBesluit( passages: readonly LibraryPassage[], besluit: Besluit, reasons: readonly string[], ): LibraryPassage[] { return passages.filter((p) => { if (p.sectionKey !== 'kern') return false; if (p.besluit === undefined) return true; // shared, any besluit if (p.besluit !== besluit) return false; if (p.reason === undefined) return true; // besluit-level, not reason-specific return reasons.includes(p.reason); }); } /** A selectable reden for a besluit, derived from the reason-specific passages — no separate catalog. `code` drives `passagesForBesluit`; `label` is the checkbox text. ponytail: assumes one passage per reason (true for the seed); dedupes on code if not. */ export interface Reden { readonly code: string; readonly label: string; } /** Visible assistance for the behandelaar on top of the silent auto-insert: how many kern standaardteksten the current besluit+redenen produced, and whether a reden still needs choosing (the besluit has reason-specific motivering passages but none is ticked). Pure DATA — the component maps it to localized copy. */ export interface BesluitGuidance { readonly insertedCount: number; readonly needsReason: boolean; } export function besluitGuidance( passages: readonly LibraryPassage[], besluit: Besluit, reasons: readonly string[], ): BesluitGuidance { return { insertedCount: passagesForBesluit(passages, besluit, reasons).length, needsReason: redenenFor(passages, besluit).length > 0 && reasons.length === 0, }; } export function redenenFor(passages: readonly LibraryPassage[], besluit: Besluit): Reden[] { const seen = new Set(); const out: Reden[] = []; for (const p of passages) { if (p.sectionKey !== 'kern' || p.besluit !== besluit || p.reason === undefined) continue; if (seen.has(p.reason)) continue; seen.add(p.reason); out.push({ code: p.reason, label: p.label }); } return out; } /** * The inverse of `passagesForBesluit`: read the current besluit + redenen back off the * kern's passage blocks (each carries its `sourcePassageId`), so the panel can re-seed * itself on reload/undo without persisting the selection separately. Kern passage blocks * are besluit-derived by construction (the only way passages enter the kern), so this * round-trips: `inferSelection(kern(passagesForBesluit(lib, b, r)), lib) === { b, r }`. * Free-text blocks carry no provenance and are ignored. */ export function inferSelection( kernBlocks: readonly LetterBlock[], passages: readonly LibraryPassage[], ): { besluit: Besluit | null; reasons: string[] } { const byId = new Map(passages.map((p) => [p.passageId, p])); let besluit: Besluit | null = null; const reasons: string[] = []; for (const b of kernBlocks) { if (b.type !== 'passage') continue; const source = byId.get(b.sourcePassageId); if (!source) continue; if (source.besluit !== undefined) besluit = source.besluit; if (source.reason !== undefined && !reasons.includes(source.reason)) reasons.push(source.reason); } return { besluit, reasons }; }