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>
194 lines
7.6 KiB
TypeScript
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();
|
|
}
|
|
}
|