feat(fp): brief v3 — besluit-driven guided drafting
Compose the herregistratie letter from the besluit instead of a library hunt: the behandelaar picks positief/negatief (+ reden-checkboxes for a negatief) and the kern's standaardteksten follow the selection live. Front-end (this increment): - Kern is recomposed reactively from the besluit selection (new BesluitSelected machine msg + composeKern); the "Genereer conceptbrief" button is gone. The drafter's free text is preserved across a selection change. - The editor shows only the editable sections; the locked aanhef/slot render in the preview, not the authoring surface. Slot is a case-type template section (per templateId), documented as such. - The panel re-seeds from the letter via inferSelection() — the besluit + redenen are read back off the kern's passage blocks, so the selection survives reload with no new wire fields (derive, don't store). - letter-section drops the now-redundant per-section passage picker (besluit owns standaardteksten); keeps free-text + block edit/move/remove. Fix: app-checkbox now falls back to a unique per-instance id when checkboxId is omitted. The CIBG styled checkbox routes clicks through the label, so the shared id="undefined" made every reason label toggle the first input — the second checkbox could never be checked. Verified live (Playwright): each reason toggles independently. Backend/seam (brief v3 WIP): besluit/reason passage tags on the wire + seed, carried through the adapter parse boundary. Specs updated (besluit, brief.machine) and the affected stories re-pointed at the new API. FE lint + build + 253 vitest specs green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
import { Component, computed, input, output } from '@angular/core';
|
||||
import { PlaceholderOption } from '@shared/ui/rich-text-editor/rich-text-editor.component';
|
||||
import { Brief } from '@brief/domain/brief';
|
||||
import { BriefMsg } from '@brief/domain/brief.machine';
|
||||
import { LetterSectionComponent } from '@brief/ui/letter-section/letter-section.component';
|
||||
|
||||
/** Organism: the lean authoring surface — just the editable letter sections and their
|
||||
add/edit controls, no letterhead/signature/footer/zoom. The full rendered letter
|
||||
(including the locked aanhef/slot) lives in the preview modal (see behandel-scherm),
|
||||
so the drafter stays focused on composing the body they actually own. */
|
||||
@Component({
|
||||
selector: 'app-letter-editor',
|
||||
imports: [LetterSectionComponent],
|
||||
styles: [
|
||||
`
|
||||
:host {
|
||||
display: grid;
|
||||
gap: var(--rhc-space-max-xl);
|
||||
}
|
||||
`,
|
||||
],
|
||||
template: `
|
||||
@for (section of editableSections(); track section.sectionKey) {
|
||||
<app-letter-section
|
||||
[section]="section"
|
||||
[placeholders]="placeholders()"
|
||||
[editable]="true"
|
||||
(edit)="edit.emit($event)"
|
||||
/>
|
||||
}
|
||||
`,
|
||||
})
|
||||
export class LetterEditorComponent {
|
||||
brief = input.required<Brief>();
|
||||
placeholders = input<readonly PlaceholderOption[]>([]);
|
||||
edit = output<BriefMsg>();
|
||||
|
||||
/** Only unlocked sections are authored here; locked aanhef/slot appear in the preview. */
|
||||
protected editableSections = computed(() => this.brief().sections.filter((s) => !s.locked));
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { Meta, StoryObj } from '@storybook/angular';
|
||||
import { Brief } from '@brief/domain/brief';
|
||||
import { LetterEditorComponent } from './letter-editor.component';
|
||||
|
||||
const brief: Brief = {
|
||||
briefId: 'b1',
|
||||
beroep: 'arts',
|
||||
templateId: 't1',
|
||||
drafterId: 'demo-drafter',
|
||||
status: { tag: 'draft' },
|
||||
placeholders: [{ key: 'reden_besluit', label: 'Reden besluit', autoResolvable: false }],
|
||||
sections: [
|
||||
{
|
||||
sectionKey: 'aanhef',
|
||||
title: 'Aanhef',
|
||||
required: true,
|
||||
locked: true,
|
||||
blocks: [
|
||||
{
|
||||
type: 'freeText',
|
||||
blockId: 'aanhef-1',
|
||||
content: { paragraphs: [{ nodes: [{ type: 'text', text: 'Geachte heer/mevrouw,' }] }] },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
sectionKey: 'kern',
|
||||
title: 'Kern van het besluit',
|
||||
required: true,
|
||||
locked: false,
|
||||
blocks: [
|
||||
{
|
||||
type: 'freeText',
|
||||
blockId: 'kern-1',
|
||||
content: { paragraphs: [{ nodes: [{ type: 'text', text: 'Wij hebben besloten...' }] }] },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const meta: Meta<LetterEditorComponent> = {
|
||||
title: 'Domein/Brief/Letter Editor',
|
||||
component: LetterEditorComponent,
|
||||
args: { brief, placeholders: [] },
|
||||
};
|
||||
export default meta;
|
||||
type Story = StoryObj<LetterEditorComponent>;
|
||||
|
||||
/** The lean authoring surface: only the editable sections (the kern); the locked
|
||||
aanhef/slot are hidden here and appear only in the preview. */
|
||||
export const Default: Story = {};
|
||||
|
||||
/** Empty kern: shows the empty-state hint plus the free-text action, no starter content. */
|
||||
export const EmptyKern: Story = {
|
||||
args: {
|
||||
brief: {
|
||||
...brief,
|
||||
sections: brief.sections.map((s) => (s.sectionKey === 'kern' ? { ...s, blocks: [] } : s)),
|
||||
},
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user