`npm run format:check` (a CI gate) had drifted red across 44 files — pre-existing files plus recently-added ones committed without formatting. Ran `prettier --write .`; no logic changes. Also regenerates documentation.json (compodoc reflects the reformatted component sources). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
247 lines
9.5 KiB
TypeScript
247 lines
9.5 KiB
TypeScript
import { Component, ElementRef, computed, input, output, viewChild } from '@angular/core';
|
|
import { PlaceholderOption } from '@shared/ui/rich-text-editor/rich-text-editor.component';
|
|
import { ButtonComponent } from '@shared/ui/button/button.component';
|
|
import { HeadingComponent } from '@shared/ui/heading/heading.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 { Diagnostic } from '@brief/domain/placeholders';
|
|
import { OrgTemplate } from '@brief/domain/org-template';
|
|
import { BriefMsg } from '@brief/domain/brief.machine';
|
|
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';
|
|
import { LetterEditorComponent } from '@brief/ui/letter-editor/letter-editor.component';
|
|
import { BesluitPanelComponent } from '@brief/ui/besluit-panel/besluit-panel.component';
|
|
|
|
/** Organism: the behandelaar's drafting step. Frames "Brief opstellen" as one step in
|
|
the case workflow — a case-context header + stepper (Beoordelen → Brief opstellen →
|
|
Indienen, neighbours stubbed) — with the besluit-driven guidance, the lean letter
|
|
editor, and an on-demand full-letter preview in a modal. Only ever renders for an
|
|
editable brief (draft/rejected); the approver's read-only flow stays in
|
|
letter-composer. Presentational: emits edit/submit/preview intents. */
|
|
@Component({
|
|
selector: 'app-behandel-scherm',
|
|
imports: [
|
|
ButtonComponent,
|
|
HeadingComponent,
|
|
StepperComponent,
|
|
LetterCanvasComponent,
|
|
DiagnosticsPanelComponent,
|
|
RejectionCommentsComponent,
|
|
LetterEditorComponent,
|
|
BesluitPanelComponent,
|
|
],
|
|
styles: [
|
|
`
|
|
:host {
|
|
display: block;
|
|
}
|
|
.case-head {
|
|
margin-block-end: var(--rhc-space-max-lg);
|
|
padding: var(--rhc-space-max-md) var(--rhc-space-max-lg);
|
|
border-inline-start: 4px solid var(--rhc-color-primary, var(--rhc-color-border-strong));
|
|
background: var(--rhc-color-background-subtle, transparent);
|
|
}
|
|
.case-meta {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: var(--rhc-space-max-md);
|
|
color: var(--rhc-color-foreground-subtle);
|
|
}
|
|
.step-body {
|
|
display: grid;
|
|
gap: var(--rhc-space-max-xl);
|
|
margin-block-start: var(--rhc-space-max-lg);
|
|
}
|
|
.bar {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: var(--rhc-space-max-md);
|
|
align-items: center;
|
|
margin-block-start: var(--rhc-space-max-xl);
|
|
}
|
|
dialog {
|
|
border: none;
|
|
border-radius: var(--rhc-radius-md, 4px);
|
|
padding: 0;
|
|
max-width: min(900px, 95vw);
|
|
width: 100%;
|
|
}
|
|
dialog::backdrop {
|
|
background: rgb(0 0 0 / 45%); /* token-ok: modal scrim, not a palette colour */
|
|
}
|
|
.modal-body {
|
|
padding: var(--rhc-space-max-lg);
|
|
max-height: 80vh;
|
|
overflow: auto;
|
|
}
|
|
.modal-bar {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
gap: var(--rhc-space-max-md);
|
|
padding: var(--rhc-space-max-md) var(--rhc-space-max-lg);
|
|
border-block-start: 1px solid var(--rhc-color-border);
|
|
}
|
|
`,
|
|
],
|
|
template: `
|
|
<div class="case-head">
|
|
<app-heading [level]="2">{{ caseHeading() }}</app-heading>
|
|
<div class="case-meta">
|
|
<span>{{ caseContext().aanvraagReferentie }}</span>
|
|
<span>{{ caseContext().zorgverlenerNaam }}</span>
|
|
<span>
|
|
{{ bigLabel() }} {{ caseContext().bigNummer }}
|
|
@if (canRevealBigNummer() && isMasked()) {
|
|
<app-button variant="subtle" (click)="onReveal()">{{ revealLabel() }}</app-button>
|
|
}
|
|
</span>
|
|
<span>{{ caseContext().beroep }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<app-stepper
|
|
[steps]="steps()"
|
|
[current]="1"
|
|
[processName]="processName()"
|
|
[stepTitle]="stepTitle()"
|
|
/>
|
|
|
|
<div class="step-body">
|
|
@if (status() === 'rejected') {
|
|
<app-rejection-comments mode="show" [comments]="rejectComments()" />
|
|
}
|
|
|
|
<app-besluit-panel
|
|
[passages]="availablePassages()"
|
|
[besluit]="selection().besluit"
|
|
[initialRedenen]="selection().reasons"
|
|
(selectionChange)="onSelection($event)"
|
|
/>
|
|
|
|
<app-letter-editor [brief]="brief()" [placeholders]="menu()" (edit)="edit.emit($event)" />
|
|
|
|
<app-diagnostics-panel [diagnostics]="diagnostics()" (locate)="locate.emit($event)" />
|
|
|
|
<div class="bar">
|
|
<app-button variant="subtle" (click)="openPreview()">{{ previewLabel() }}</app-button>
|
|
<app-button variant="primary" [disabled]="!canSubmit() || busy()" (click)="submit.emit()">{{
|
|
submitLabel()
|
|
}}</app-button>
|
|
@if (!canSubmit()) {
|
|
<span class="app-text-subtle">{{ submitHint() }}</span>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
<dialog #previewDialog>
|
|
<div class="modal-body">
|
|
<app-letter-canvas
|
|
[brief]="brief()"
|
|
[orgTemplate]="orgTemplate()"
|
|
[logoUrl]="logoUrl()"
|
|
[editableRegions]="'none'"
|
|
[diagnostics]="diagnostics()"
|
|
/>
|
|
</div>
|
|
<div class="modal-bar">
|
|
<app-button variant="secondary" (click)="preview.emit()">{{
|
|
openDocumentLabel()
|
|
}}</app-button>
|
|
<app-button variant="primary" (click)="closePreview()">{{ closeLabel() }}</app-button>
|
|
</div>
|
|
</dialog>
|
|
`,
|
|
})
|
|
export class BehandelSchermComponent {
|
|
brief = input.required<Brief>();
|
|
orgTemplate = input.required<OrgTemplate>();
|
|
logoUrl = input<string | null>(null);
|
|
availablePassages = input<readonly LibraryPassage[]>([]);
|
|
diagnostics = input<readonly Diagnostic[]>([]);
|
|
caseContext = input.required<CaseContext>();
|
|
canSubmit = input(false);
|
|
busy = input(false);
|
|
/** Server decision (PRD-0002 §5c): may this actor unmask the case BIG-nummer? */
|
|
canRevealBigNummer = input(false);
|
|
|
|
edit = output<BriefMsg>();
|
|
submit = output<void>();
|
|
preview = output<void>();
|
|
locate = output<Diagnostic>();
|
|
revealBigNummer = output<void>();
|
|
|
|
/** The BIG-nummer arrives masked (contains `*`); once revealed the swapped value has
|
|
no `*`, so the reveal action hides itself — no separate "revealed" flag needed. */
|
|
protected isMasked = computed(() => this.caseContext().bigNummer.includes('*'));
|
|
|
|
/** Step-up (PRD-0002 §5d) stubbed as a native confirm — the extra verification gesture
|
|
before an audited PII reveal. ponytail: real systems prompt MFA / recent re-auth. */
|
|
protected onReveal() {
|
|
if (confirm(this.stepUpPrompt())) this.revealBigNummer.emit();
|
|
}
|
|
|
|
private previewDialog = viewChild<ElementRef<HTMLDialogElement>>('previewDialog');
|
|
|
|
protected status = computed(() => this.brief().status.tag);
|
|
protected rejectComments = computed(() => {
|
|
const s = this.brief().status;
|
|
return s.tag === 'rejected' ? s.comments : '';
|
|
});
|
|
|
|
/** The besluit + redenen the letter currently reflects, read back off the kern's
|
|
passages — this seeds the panel so it survives reload/undo (no separate storage). */
|
|
protected selection = computed(() => {
|
|
const kern = this.brief().sections.find((s) => s.sectionKey === 'kern');
|
|
return inferSelection(kern?.blocks ?? [], this.availablePassages());
|
|
});
|
|
|
|
// Same insert menu as the composer: only valid, fillable, non-deprecated fields.
|
|
protected menu = computed<PlaceholderOption[]>(() =>
|
|
this.brief()
|
|
.placeholders.filter((p) => p.fillable !== false && !p.deprecated)
|
|
.map((p) => ({ key: p.key, label: p.label, autoResolvable: p.autoResolvable })),
|
|
);
|
|
|
|
/** Besluit/redenen changed → recompose the kern as one edit (= one undo step). */
|
|
protected onSelection(sel: { besluit: Besluit | null; reasons: string[] }) {
|
|
this.edit.emit({ tag: 'BesluitSelected', besluit: sel.besluit, reasons: sel.reasons });
|
|
}
|
|
|
|
protected openPreview() {
|
|
this.previewDialog()?.nativeElement.showModal();
|
|
}
|
|
protected closePreview() {
|
|
this.previewDialog()?.nativeElement.close();
|
|
}
|
|
|
|
protected submitLabel = computed(() =>
|
|
this.status() === 'rejected'
|
|
? $localize`:@@brief.resubmit:Opnieuw indienen`
|
|
: $localize`:@@brief.submit:Indienen ter beoordeling`,
|
|
);
|
|
|
|
protected steps = input<string[]>([
|
|
$localize`:@@brief.step.beoordelen:Beoordelen`,
|
|
$localize`:@@brief.step.opstellen:Brief opstellen`,
|
|
$localize`:@@brief.step.indienen:Indienen`,
|
|
]);
|
|
protected processName = input($localize`:@@brief.process:Herregistratie behandelen`);
|
|
protected stepTitle = input($localize`:@@brief.step.opstellen:Brief opstellen`);
|
|
protected caseHeading = input($localize`:@@brief.case.heading:Aanvraag herregistratie`);
|
|
protected bigLabel = input($localize`:@@brief.case.big:BIG-nummer`);
|
|
protected revealLabel = input($localize`:@@brief.case.reveal:Toon BIG-nummer`);
|
|
protected stepUpPrompt = input(
|
|
$localize`:@@brief.case.revealConfirm:Extra verificatie vereist. Het tonen van het BIG-nummer wordt vastgelegd. Doorgaan?`,
|
|
);
|
|
protected previewLabel = input($localize`:@@brief.preview.open:Voorbeeld`);
|
|
protected openDocumentLabel = input(
|
|
$localize`:@@brief.preview.openDocument:Openen als document (PDF)`,
|
|
);
|
|
protected closeLabel = input($localize`:@@common.close:Sluiten`);
|
|
protected submitHint = input(
|
|
$localize`:@@brief.submitHint:Vul eerst alle verplichte secties en los fouten op.`,
|
|
);
|
|
}
|