Files
atomic-design-poc/apps/ssp/src/app/brief/ui/brief.page.ts
T
ehoandClaude Sonnet 5 dd11eafe50 refactor: strip WP-/RB- ticket refs from apps and libs (RD-18)
204 WP-NN/RB-NN comments named a closed ticket instead of the code they
sit next to. git blame already records history and stays correct when
code moves; the comment does not. This sweep removes the reference and
keeps the sentence, across 95 files in apps/ and libs/ plus the
behaviour-spec generator's header text.

Eleven references stay: five story files justify an a11y disable per
the README's rule, and one line in a11y.mdx documents that convention.
Two sentences needed a rewrite, not a deletion, so the reference's
meaning survives its removal. behaviour-spec.mdx is regenerated, not
hand-edited.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-04 21:23:07 +02:00

194 lines
7.6 KiB
TypeScript

import { Component, computed, inject } from '@angular/core';
import { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';
import { AlertComponent } from '@shared/ui/alert/alert.component';
import { ButtonComponent } from '@shared/ui/button/button.component';
import { ASYNC } from '@shared/ui/async/async.component';
import { BriefStore } from '@brief/application/brief.store';
import { LetterComposerComponent } from '@brief/ui/letter-composer/letter-composer.component';
import { BehandelSchermComponent } from '@brief/ui/behandel-scherm/behandel-scherm.component';
/** Page: thin container. Injects the root store, kicks off the load, and passes its
derived read-model to the composer. Business/UI logic lives below in pure pieces;
this just wires signals to the organism and events back to store commands. */
@Component({
selector: 'app-brief-page',
imports: [
PageShellComponent,
AlertComponent,
ButtonComponent,
...ASYNC,
LetterComposerComponent,
BehandelSchermComponent,
],
host: { '(document:keydown)': 'onKey($event)' },
styles: [
`
.brief-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--rhc-space-max-md);
margin-block-end: var(--rhc-space-max-lg);
}
.toolbar-start {
display: flex;
align-items: center;
gap: var(--rhc-space-max-md);
}
.save {
color: var(--rhc-color-foreground-subtle);
font-size: 0.9em;
}
`,
],
template: `
<app-page-shell [heading]="heading" [intro]="intro" backLink="/dashboard">
@if (lastError(); as err) {
<app-alert type="error">{{ err }}</app-alert>
}
<app-async [data]="store.remoteData()">
<ng-template appAsyncError>
<app-alert type="error">{{ failedText }}</app-alert>
<app-button variant="secondary" (click)="reload()">{{ retryText }}</app-button>
</ng-template>
<ng-template appAsyncLoaded>
@if (loaded(); as s) {
@if (store.orgTemplate(); as orgTemplate) {
<div class="brief-toolbar">
<div class="toolbar-start">
@if (store.canEdit()) {
<app-button
variant="subtle"
[disabled]="!store.canUndo()"
[attr.aria-label]="undoLabel"
(click)="store.undo()"
>{{ undoLabel }}</app-button
>
<app-button
variant="subtle"
[disabled]="!store.canRedo()"
[attr.aria-label]="redoLabel"
(click)="store.redo()"
>{{ redoLabel }}</app-button
>
}
<span class="save" role="status" aria-live="polite">{{ saveText() }}</span>
@if (store.saveState().tag === 'Error') {
<app-button variant="secondary" (click)="store.retrySave()">{{
retrySaveLabel
}}</app-button>
}
</div>
<app-button variant="subtle" [disabled]="store.busy()" (click)="resetDemo()">{{
resetLabel
}}</app-button>
</div>
@if (store.canEdit() && store.caseContext(); as caseContext) {
<!-- Drafter (behandelaar): guided drafting step in the case workflow. -->
<app-behandel-scherm
[brief]="s.brief"
[orgTemplate]="orgTemplate"
[logoUrl]="store.logoUrl()"
[availablePassages]="s.availablePassages"
[diagnostics]="store.diagnostics()"
[caseContext]="caseContext"
[canSubmit]="store.canSubmit()"
[busy]="store.busy()"
[canRevealBigNummer]="store.canRevealBigNummer()"
(edit)="store.edit($event)"
(submit)="store.submit()"
(preview)="store.previewLetter()"
(revealBigNummer)="store.revealBigNummer()"
/>
} @else {
<!-- Approver / read-only: review + approve/reject/send. -->
<app-letter-composer
[brief]="s.brief"
[orgTemplate]="orgTemplate"
[logoUrl]="store.logoUrl()"
[diagnostics]="store.diagnostics()"
[blockDiffs]="store.blockDiffs()"
[removedCount]="store.removedSinceReject()"
[canApprove]="store.canApprove()"
[canReject]="store.canReject()"
[canSend]="store.canSend()"
[busy]="store.busy()"
(approve)="store.approve()"
(reject)="store.reject($event)"
(send)="store.send()"
(preview)="store.previewLetter()"
/>
}
}
}
</ng-template>
</app-async>
</app-page-shell>
`,
})
export class BriefPage {
protected store = inject(BriefStore);
protected model = this.store.model;
protected lastError = this.store.lastError;
protected heading = $localize`:@@brief.page.heading:Brief opstellen`;
protected intro = $localize`:@@brief.page.intro:Stel de brief aan de zorgverlener samen uit standaardteksten en vrije tekst.`;
protected failedText = $localize`:@@brief.page.failed:De brief kon niet worden geladen.`;
protected retryText = $localize`:@@brief.page.retry:Opnieuw proberen`;
protected resetLabel = $localize`:@@brief.page.reset:Opnieuw beginnen (demo)`;
protected undoLabel = $localize`:@@brief.page.undo:Ongedaan maken`;
protected redoLabel = $localize`:@@brief.page.redo:Opnieuw uitvoeren`;
protected retrySaveLabel = $localize`:@@brief.page.retrySave:Opnieuw proberen`;
private savingText = $localize`:@@brief.page.saving:Concept opslaan…`;
private savedText = $localize`:@@brief.page.saved:Concept opgeslagen`;
private saveErrorText = $localize`:@@brief.page.saveError:Niet opgeslagen — opnieuw proberen`;
/** Debounced-save state, surfaced in a polite live region. */
protected saveText = computed(() => {
switch (this.store.saveState().tag) {
case 'Saving':
return this.savingText;
case 'Saved':
return this.savedText;
case 'Error':
return this.saveErrorText;
default:
return '';
}
});
constructor() {
void this.store.load();
}
protected resetDemo() {
void this.store.resetDemo();
}
/** Typed narrowing for the `<app-async>` loaded slot: a structural
directive's context can't inherit a generic from a sibling host input, so the
Success value is unwrapped here instead of through `let-`. */
protected readonly loaded = computed(() => {
const s = this.model();
return s.tag === 'Loaded' ? s : undefined;
});
protected reload() {
void this.store.load();
}
/** Ctrl/Cmd+Z = undo, Ctrl/Cmd+Shift+Z = redo. Ignored while focus is in the
rich-text editor or a form control, so the browser's own text undo keeps working
there — our shell-level undo is for structural edits (add/remove/reorder blocks). */
protected onKey(e: KeyboardEvent) {
if (!(e.ctrlKey || e.metaKey) || e.key.toLowerCase() !== 'z' || !this.store.canEdit()) return;
const t = e.target as HTMLElement | null;
if (t && (t.isContentEditable || t.tagName === 'INPUT' || t.tagName === 'TEXTAREA')) return;
e.preventDefault();
if (e.shiftKey) this.store.redo();
else this.store.undo();
}
}