diff --git a/apps/ssp/src/app/brief/domain/sample-letter.ts b/apps/ssp/src/app/brief/domain/sample-letter.ts new file mode 100644 index 0000000..0b2c1c7 --- /dev/null +++ b/apps/ssp/src/app/brief/domain/sample-letter.ts @@ -0,0 +1,51 @@ +import { Brief } from './brief'; + +/** A minimal read-only sample letter, so the admin sees the org identity in context + while editing (content itself is not the admin's to change). Production content — + the letter the org-template editor previews — not a test fixture, so it lives here + rather than in `brief.testing.ts` (dependency-cruiser's no-testing-in-production + rule forbids production code from reaching any `*.testing.ts`). */ +export const SAMPLE_LETTER_BRIEF: Brief = { + briefId: 'VOORBEELD-0001', + beroep: 'arts', + templateId: 'sample', + drafterId: 'sample', + status: { tag: 'draft' }, + placeholders: [ + { key: 'naam_zorgverlener', label: 'Naam zorgverlener', autoResolvable: true }, + { key: 'datum', label: 'Datum', autoResolvable: true }, + ], + sections: [ + { + sectionKey: 'body', + title: 'Voorbeeldinhoud', + required: true, + locked: true, + blocks: [ + { + type: 'freeText', + blockId: 'sample-1', + content: { + paragraphs: [ + { + nodes: [ + { type: 'text', text: 'Geachte ' }, + { type: 'placeholder', key: 'naam_zorgverlener' }, + { type: 'text', text: ',' }, + ], + }, + { + nodes: [ + { + type: 'text', + text: 'Dit is voorbeeldinhoud. Alleen de huisstijl-onderdelen (logo, afzender, ondertekening en voettekst) zijn hier bewerkbaar.', + }, + ], + }, + ], + }, + }, + ], + }, + ], +}; diff --git a/apps/ssp/src/app/brief/ui/org-template-editor/logo-upload.component.ts b/apps/ssp/src/app/brief/ui/org-template-editor/logo-upload.component.ts new file mode 100644 index 0000000..2914eee --- /dev/null +++ b/apps/ssp/src/app/brief/ui/org-template-editor/logo-upload.component.ts @@ -0,0 +1,77 @@ +import { Component, computed, input, output } from '@angular/core'; +import { HeadingComponent } from '@shared/ui/heading/heading.component'; +import { AlertComponent } from '@shared/ui/alert/alert.component'; +import { FileInputComponent } from '@shared/ui/upload/file-input/file-input.component'; +import { SingleUploadComponent } from '@shared/ui/upload/single-upload/single-upload.component'; +import { UploadState } from '@shared/domain/upload.machine'; + +const LOGO_CATEGORY = 'org-logo'; + +/** + * Organism: the org-template editor's logo-upload block, split out of + * `org-template-editor.component.ts` (RD-25) — one of its two self-contained + * mutation clusters. Presentational: every mutation is an output the parent + * re-emits unchanged. + */ +@Component({ + selector: 'app-logo-upload', + imports: [HeadingComponent, AlertComponent, FileInputComponent, SingleUploadComponent], + styles: [ + ` + :host { + display: block; + } + .section { + margin-block-start: var(--rhc-space-max-xl); + } + `, + ], + template: ` +
+ Logo + @if (logoCategory()) { + + } + @if (logoRejection()) { + {{ logoRejection() }} + } + @if (logoUploads().length) { + + } +
+ `, +}) +export class LogoUploadComponent { + logoUrl = input(null); + uploadState = input.required(); + previewUrlFor = input<(documentId: string) => string | undefined>(); + + logoSelected = output(); + logoRemoved = output(); + logoRetry = output(); + + protected logoCategory = computed(() => + this.uploadState().categories.find((c) => c.categoryId === LOGO_CATEGORY), + ); + protected logoUploads = computed(() => + this.uploadState().uploads.filter((u) => u.categoryId === LOGO_CATEGORY), + ); + protected logoRejection = computed(() => this.uploadState().rejections[LOGO_CATEGORY]); +} diff --git a/apps/ssp/src/app/brief/ui/org-template-editor/org-template-editor.component.ts b/apps/ssp/src/app/brief/ui/org-template-editor/org-template-editor.component.ts index 59c303c..2239ad6 100644 --- a/apps/ssp/src/app/brief/ui/org-template-editor/org-template-editor.component.ts +++ b/apps/ssp/src/app/brief/ui/org-template-editor/org-template-editor.component.ts @@ -1,13 +1,9 @@ -/* eslint-disable max-lines */ // sample letter + labels + editor in one file — removed by RD-25 import { Component, computed, input, output } from '@angular/core'; -import { DatePipe } from '@angular/common'; -import { HeadingComponent } from '@shared/ui/heading/heading.component'; import { ButtonComponent } from '@shared/ui/button/button.component'; import { AlertComponent } from '@shared/ui/alert/alert.component'; -import { FileInputComponent } from '@shared/ui/upload/file-input/file-input.component'; -import { SingleUploadComponent } from '@shared/ui/upload/single-upload/single-upload.component'; import { UploadState } from '@shared/domain/upload.machine'; import { Brief } from '@brief/domain/brief'; +import { SAMPLE_LETTER_BRIEF } from '@brief/domain/sample-letter'; import { MARGIN_MAX_MM, MARGIN_MIN_MM, @@ -18,74 +14,29 @@ import { } from '@brief/domain/org-template'; import { OrgTemplateTextField } from '@brief/domain/org-template.machine'; import { LetterCanvasComponent } from '@brief/ui/letter-canvas/letter-canvas.component'; +import { LogoUploadComponent } from './logo-upload.component'; +import { VersionHistoryComponent } from './version-history.component'; -const LOGO_CATEGORY = 'org-logo'; const EDGES: readonly (keyof Margins)[] = ['topMm', 'rightMm', 'bottomMm', 'leftMm']; -/** A minimal read-only sample letter, so the admin sees the org identity in context - while editing (content itself is not the admin's to change). */ -export const SAMPLE_LETTER_BRIEF: Brief = { - briefId: 'VOORBEELD-0001', - beroep: 'arts', - templateId: 'sample', - drafterId: 'sample', - status: { tag: 'draft' }, - placeholders: [ - { key: 'naam_zorgverlener', label: 'Naam zorgverlener', autoResolvable: true }, - { key: 'datum', label: 'Datum', autoResolvable: true }, - ], - sections: [ - { - sectionKey: 'body', - title: 'Voorbeeldinhoud', - required: true, - locked: true, - blocks: [ - { - type: 'freeText', - blockId: 'sample-1', - content: { - paragraphs: [ - { - nodes: [ - { type: 'text', text: 'Geachte ' }, - { type: 'placeholder', key: 'naam_zorgverlener' }, - { type: 'text', text: ',' }, - ], - }, - { - nodes: [ - { - type: 'text', - text: 'Dit is voorbeeldinhoud. Alleen de huisstijl-onderdelen (logo, afzender, ondertekening en voettekst) zijn hier bewerkbaar.', - }, - ], - }, - ], - }, - }, - ], - }, - ], -}; - /** * Organism: the admin org-template editor. The mirror of the drafter's * composer — the letter canvas runs in `editableRegions='template'` so the * letterhead/signature/footer are edited in place, while the content is a read-only - * sample. Margins, logo upload, version history and the publish bar sit around it. - * Presentational: every mutation is an output the store turns into a command. + * sample. Margins and the publish bar sit around it; the logo uploader and version + * history are their own children (`app-logo-upload`, `app-version-history`, RD-25) — + * each a self-contained mutation cluster. Presentational: every mutation is an + * output the store turns into a command, whether sourced here or re-emitted from + * a child. */ @Component({ selector: 'app-org-template-editor', imports: [ - DatePipe, - HeadingComponent, ButtonComponent, AlertComponent, - FileInputComponent, - SingleUploadComponent, LetterCanvasComponent, + LogoUploadComponent, + VersionHistoryComponent, ], styles: [ ` @@ -123,22 +74,6 @@ export const SAMPLE_LETTER_BRIEF: Brief = { .margins input { width: 6rem; } - .history-list { - list-style: none; - margin: 0; - padding: 0; - display: flex; - flex-direction: column; - gap: var(--rhc-space-max-sm); - } - .history-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: var(--rhc-space-max-md); - border-block-end: var(--rhc-border-width-sm) solid var(--rhc-color-border-default); - padding-block-end: var(--rhc-space-max-sm); - } .bar { display: flex; flex-wrap: wrap; @@ -154,7 +89,7 @@ export const SAMPLE_LETTER_BRIEF: Brief = { template: `