feat(brief): besluit guidance + read-only notice (role clarity)
Surface the besluit-driven assistance that was previously silent: a pure besluitGuidance() (kern passage count + needs-reason flag) rendered as a hint below the besluit panel in behandel-scherm (warning to pick a reden, else info on how many standaardteksten were inserted). Add a read-only notice on letter-composer for a pure viewer (no edit/approve/reject/send right, e.g. admin) so the read-only letter isn't mistaken for a broken editor — the "reverted to a view of the letter" confusion was a role issue (non-drafter sees the composer). +besluitGuidance spec. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+3993
-1278
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { Besluit, LetterBlock, LibraryPassage } from './brief';
|
||||
import { inferSelection, passagesForBesluit, redenenFor } from './besluit';
|
||||
import { besluitGuidance, inferSelection, passagesForBesluit, redenenFor } from './besluit';
|
||||
|
||||
const block = (t: string): LibraryPassage['content'] => ({
|
||||
paragraphs: [{ nodes: [{ type: 'text', text: t }] }],
|
||||
@@ -117,3 +117,20 @@ describe('inferSelection', () => {
|
||||
expect(inferSelection(blocks, lib)).toEqual({ besluit: 'positief', reasons: [] });
|
||||
});
|
||||
});
|
||||
|
||||
describe('besluitGuidance', () => {
|
||||
it('positief: counts inserted passages, no reden needed (positief has no redenen)', () => {
|
||||
expect(besluitGuidance(lib, 'positief', [])).toEqual({ insertedCount: 2, needsReason: false });
|
||||
});
|
||||
|
||||
it('negatief without a reden: flags that a reden must be chosen', () => {
|
||||
expect(besluitGuidance(lib, 'negatief', [])).toEqual({ insertedCount: 2, needsReason: true });
|
||||
});
|
||||
|
||||
it('negatief with a reden: no longer flags, and the reason passage is counted', () => {
|
||||
expect(besluitGuidance(lib, 'negatief', ['onvoldoende_scholing'])).toEqual({
|
||||
insertedCount: 3,
|
||||
needsReason: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -34,6 +34,26 @@ export interface Reden {
|
||||
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<string>();
|
||||
const out: Reden[] = [];
|
||||
|
||||
@@ -3,9 +3,10 @@ import { PlaceholderOption } from '@shared/ui/rich-text-editor/rich-text-editor.
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { MaskedValueComponent } from '@shared/ui/masked-value/masked-value.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { StepperComponent } from '@shared/ui/stepper/stepper.component';
|
||||
import { Besluit, Brief, CaseContext, LibraryPassage } from '@brief/domain/brief';
|
||||
import { inferSelection } from '@brief/domain/besluit';
|
||||
import { besluitGuidance, inferSelection } from '@brief/domain/besluit';
|
||||
import { Diagnostic } from '@brief/domain/placeholders';
|
||||
import { OrgTemplate } from '@brief/domain/org-template';
|
||||
import { BriefMsg } from '@brief/domain/brief.machine';
|
||||
@@ -27,6 +28,7 @@ import { BesluitPanelComponent } from '@brief/ui/besluit-panel/besluit-panel.com
|
||||
ButtonComponent,
|
||||
HeadingComponent,
|
||||
MaskedValueComponent,
|
||||
AlertComponent,
|
||||
StepperComponent,
|
||||
LetterCanvasComponent,
|
||||
DiagnosticsPanelComponent,
|
||||
@@ -125,6 +127,14 @@ import { BesluitPanelComponent } from '@brief/ui/besluit-panel/besluit-panel.com
|
||||
(selectionChange)="onSelection($event)"
|
||||
/>
|
||||
|
||||
@if (guidance(); as g) {
|
||||
@if (g.needsReason) {
|
||||
<app-alert type="warning">{{ needsReasonHint }}</app-alert>
|
||||
} @else {
|
||||
<app-alert type="info">{{ insertedHint(g.insertedCount) }}</app-alert>
|
||||
}
|
||||
}
|
||||
|
||||
<app-letter-editor [brief]="brief()" [placeholders]="menu()" (edit)="edit.emit($event)" />
|
||||
|
||||
<app-diagnostics-panel [diagnostics]="diagnostics()" (locate)="locate.emit($event)" />
|
||||
@@ -198,6 +208,16 @@ export class BehandelSchermComponent {
|
||||
return inferSelection(kern?.blocks ?? [], this.availablePassages());
|
||||
});
|
||||
|
||||
/** Visible guidance for the current selection — null until a besluit is chosen (the
|
||||
panel's own intro copy prompts that first step). */
|
||||
protected guidance = computed(() => {
|
||||
const s = this.selection();
|
||||
return s.besluit ? besluitGuidance(this.availablePassages(), s.besluit, s.reasons) : null;
|
||||
});
|
||||
protected needsReasonHint = $localize`:@@brief.guidance.needsReason:Kies een reden, zodat de juiste motivering aan de brief wordt toegevoegd.`;
|
||||
protected insertedHint = (n: number) =>
|
||||
$localize`:@@brief.guidance.inserted:${n}:count: standaardtekst(en) toegevoegd op basis van het besluit. Vul aan met vrije tekst waar nodig.`;
|
||||
|
||||
// Same insert menu as the composer: only valid, fillable, non-deprecated fields.
|
||||
protected menu = computed<PlaceholderOption[]>(() =>
|
||||
this.brief()
|
||||
|
||||
@@ -80,6 +80,10 @@ import { RejectionCommentsComponent } from '@brief/ui/rejection-comments/rejecti
|
||||
<app-rejection-comments mode="show" [comments]="rejectComments()" />
|
||||
}
|
||||
|
||||
@if (pureViewer()) {
|
||||
<app-alert type="info">{{ readonlyNotice() }}</app-alert>
|
||||
}
|
||||
|
||||
@if (showDiff() && removedCount() > 0) {
|
||||
<app-alert type="info">{{ removedText() }}</app-alert>
|
||||
}
|
||||
@@ -162,6 +166,14 @@ export class LetterComposerComponent {
|
||||
sentText = input($localize`:@@brief.sent:De brief is verzonden.`);
|
||||
|
||||
protected status = computed(() => this.brief().status.tag);
|
||||
|
||||
/** A pure viewer has no action on this letter (not the behandelaar, not an approver with
|
||||
approve/reject/send) — e.g. an admin. Show a notice so the read-only letter isn't
|
||||
mistaken for a broken editor. */
|
||||
protected pureViewer = computed(() => !this.canApprove() && !this.canReject() && !this.canSend());
|
||||
readonlyNotice = input(
|
||||
$localize`:@@brief.readonlyNotice:Alleen-lezen weergave. De behandelaar stelt de brief op.`,
|
||||
);
|
||||
protected rejectComments = computed(() => {
|
||||
const s = this.brief().status;
|
||||
return s.tag === 'rejected' ? s.comments : '';
|
||||
|
||||
@@ -290,6 +290,18 @@
|
||||
<context context-type="linenumber">158</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="brief.guidance.needsReason" datatype="html">
|
||||
<source>Kies een reden, zodat de juiste motivering aan de brief wordt toegevoegd.</source>
|
||||
<target datatype="html">Choose a reason so the correct justification is added to the letter.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="brief.guidance.inserted" datatype="html">
|
||||
<source><x id="count" equiv-text="n"/> standaardtekst(en) toegevoegd op basis van het besluit. Vul aan met vrije tekst waar nodig.</source>
|
||||
<target datatype="html"><x id="count" equiv-text="n"/> standard passage(s) added based on the decision. Add free text where needed.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="brief.readonlyNotice" datatype="html">
|
||||
<source>Alleen-lezen weergave. De behandelaar stelt de brief op.</source>
|
||||
<target datatype="html">Read-only view. The case handler composes the letter.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="brief.submit" datatype="html">
|
||||
<source>Indienen ter beoordeling</source>
|
||||
<target datatype="html">Submit for review</target>
|
||||
|
||||
+50
-29
@@ -395,110 +395,124 @@
|
||||
<context context-type="linenumber">7</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="brief.guidance.needsReason" datatype="html">
|
||||
<source>Kies een reden, zodat de juiste motivering aan de brief wordt toegevoegd.</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/brief/ui/behandel-scherm/behandel-scherm.component.ts</context>
|
||||
<context context-type="linenumber">217</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="brief.guidance.inserted" datatype="html">
|
||||
<source><x id="count" equiv-text="n"/> standaardtekst(en) toegevoegd op basis van het besluit. Vul aan met vrije tekst waar nodig.</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/brief/ui/behandel-scherm/behandel-scherm.component.ts</context>
|
||||
<context context-type="linenumber">219</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="brief.resubmit" datatype="html">
|
||||
<source>Opnieuw indienen</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/brief/ui/behandel-scherm/behandel-scherm.component.ts</context>
|
||||
<context context-type="linenumber">222</context>
|
||||
<context context-type="linenumber">242</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="brief.submit" datatype="html">
|
||||
<source>Indienen ter beoordeling</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/brief/ui/behandel-scherm/behandel-scherm.component.ts</context>
|
||||
<context context-type="linenumber">223</context>
|
||||
<context context-type="linenumber">243</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="brief.step.beoordelen" datatype="html">
|
||||
<source>Beoordelen</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/brief/ui/behandel-scherm/behandel-scherm.component.ts</context>
|
||||
<context context-type="linenumber">227</context>
|
||||
<context context-type="linenumber">247</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="brief.step.opstellen" datatype="html">
|
||||
<source>Brief opstellen</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/brief/ui/behandel-scherm/behandel-scherm.component.ts</context>
|
||||
<context context-type="linenumber">228</context>
|
||||
<context context-type="linenumber">248</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/brief/ui/behandel-scherm/behandel-scherm.component.ts</context>
|
||||
<context context-type="linenumber">232</context>
|
||||
<context context-type="linenumber">252</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="brief.step.indienen" datatype="html">
|
||||
<source>Indienen</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/brief/ui/behandel-scherm/behandel-scherm.component.ts</context>
|
||||
<context context-type="linenumber">229</context>
|
||||
<context context-type="linenumber">249</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="brief.process" datatype="html">
|
||||
<source>Herregistratie behandelen</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/brief/ui/behandel-scherm/behandel-scherm.component.ts</context>
|
||||
<context context-type="linenumber">231</context>
|
||||
<context context-type="linenumber">251</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="brief.case.heading" datatype="html">
|
||||
<source>Aanvraag herregistratie</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/brief/ui/behandel-scherm/behandel-scherm.component.ts</context>
|
||||
<context context-type="linenumber">233</context>
|
||||
<context context-type="linenumber">253</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="brief.case.big" datatype="html">
|
||||
<source>BIG-nummer</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/brief/ui/behandel-scherm/behandel-scherm.component.ts</context>
|
||||
<context context-type="linenumber">234</context>
|
||||
<context context-type="linenumber">254</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="brief.case.reveal" datatype="html">
|
||||
<source>Toon BIG-nummer</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/brief/ui/behandel-scherm/behandel-scherm.component.ts</context>
|
||||
<context context-type="linenumber">235</context>
|
||||
<context context-type="linenumber">255</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="brief.case.revealConfirm" datatype="html">
|
||||
<source>Extra verificatie vereist. Het tonen van het BIG-nummer wordt vastgelegd. Doorgaan?</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/brief/ui/behandel-scherm/behandel-scherm.component.ts</context>
|
||||
<context context-type="linenumber">237</context>
|
||||
<context context-type="linenumber">257</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="brief.preview.open" datatype="html">
|
||||
<source>Voorbeeld</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/brief/ui/behandel-scherm/behandel-scherm.component.ts</context>
|
||||
<context context-type="linenumber">239</context>
|
||||
<context context-type="linenumber">259</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/brief/ui/letter-composer/letter-composer.component.ts</context>
|
||||
<context context-type="linenumber">150</context>
|
||||
<context context-type="linenumber">154</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="brief.preview.openDocument" datatype="html">
|
||||
<source>Openen als document (PDF)</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/brief/ui/behandel-scherm/behandel-scherm.component.ts</context>
|
||||
<context context-type="linenumber">241</context>
|
||||
<context context-type="linenumber">261</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="common.close" datatype="html">
|
||||
<source>Sluiten</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/brief/ui/behandel-scherm/behandel-scherm.component.ts</context>
|
||||
<context context-type="linenumber">243</context>
|
||||
<context context-type="linenumber">263</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="brief.submitHint" datatype="html">
|
||||
<source>Vul eerst alle verplichte secties en los fouten op.</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/brief/ui/behandel-scherm/behandel-scherm.component.ts</context>
|
||||
<context context-type="linenumber">245</context>
|
||||
<context context-type="linenumber">265</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="brief.besluit.positief" datatype="html">
|
||||
@@ -821,91 +835,98 @@
|
||||
<source>Brief aan de zorgverlener</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/brief/ui/letter-composer/letter-composer.component.ts</context>
|
||||
<context context-type="linenumber">149</context>
|
||||
<context context-type="linenumber">153</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="brief.diff.show" datatype="html">
|
||||
<source>Toon wijzigingen</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/brief/ui/letter-composer/letter-composer.component.ts</context>
|
||||
<context context-type="linenumber">151</context>
|
||||
<context context-type="linenumber">155</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="brief.diff.hide" datatype="html">
|
||||
<source>Verberg wijzigingen</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/brief/ui/letter-composer/letter-composer.component.ts</context>
|
||||
<context context-type="linenumber">152</context>
|
||||
<context context-type="linenumber">156</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="brief.diff.removed" datatype="html">
|
||||
<source><x id="count" equiv-text="this.removedCount()"/> blok(ken) verwijderd sinds afwijzing.</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/brief/ui/letter-composer/letter-composer.component.ts</context>
|
||||
<context context-type="linenumber">155</context>
|
||||
<context context-type="linenumber">159</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="brief.approve" datatype="html">
|
||||
<source>Goedkeuren</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/brief/ui/letter-composer/letter-composer.component.ts</context>
|
||||
<context context-type="linenumber">157</context>
|
||||
<context context-type="linenumber">161</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="brief.send" datatype="html">
|
||||
<source>Versturen</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/brief/ui/letter-composer/letter-composer.component.ts</context>
|
||||
<context context-type="linenumber">158</context>
|
||||
<context context-type="linenumber">162</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="brief.awaiting" datatype="html">
|
||||
<source>De brief wacht op beoordeling door een collega.</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/brief/ui/letter-composer/letter-composer.component.ts</context>
|
||||
<context context-type="linenumber">160</context>
|
||||
<context context-type="linenumber">164</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="brief.sent" datatype="html">
|
||||
<source>De brief is verzonden.</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/brief/ui/letter-composer/letter-composer.component.ts</context>
|
||||
<context context-type="linenumber">162</context>
|
||||
<context context-type="linenumber">166</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="brief.readonlyNotice" datatype="html">
|
||||
<source>Alleen-lezen weergave. De behandelaar stelt de brief op.</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/brief/ui/letter-composer/letter-composer.component.ts</context>
|
||||
<context context-type="linenumber">177</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="brief.status.draft" datatype="html">
|
||||
<source>Concept</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/brief/ui/letter-composer/letter-composer.component.ts</context>
|
||||
<context context-type="linenumber">173</context>
|
||||
<context context-type="linenumber">187</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="brief.status.submitted" datatype="html">
|
||||
<source>Ter beoordeling</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/brief/ui/letter-composer/letter-composer.component.ts</context>
|
||||
<context context-type="linenumber">175</context>
|
||||
<context context-type="linenumber">189</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="brief.status.approved" datatype="html">
|
||||
<source>Goedgekeurd</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/brief/ui/letter-composer/letter-composer.component.ts</context>
|
||||
<context context-type="linenumber">177</context>
|
||||
<context context-type="linenumber">191</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="brief.status.rejected" datatype="html">
|
||||
<source>Afgewezen</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/brief/ui/letter-composer/letter-composer.component.ts</context>
|
||||
<context context-type="linenumber">179</context>
|
||||
<context context-type="linenumber">193</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="brief.status.sent" datatype="html">
|
||||
<source>Verzonden</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/brief/ui/letter-composer/letter-composer.component.ts</context>
|
||||
<context context-type="linenumber">181</context>
|
||||
<context context-type="linenumber">195</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="brief.section.required" datatype="html">
|
||||
|
||||
Reference in New Issue
Block a user