From bf58296ba526e71f45e10546303fcbb1b386ad09 Mon Sep 17 00:00:00 2001 From: Grovy311 Date: Fri, 25 Sep 2026 20:42:44 +0200 Subject: [PATCH] Add print cover fields and office profile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Project settings gain siteAddress, workStage, projectLead and projectEditor (project.update-settings schema version 3, migration 0009); versions 2 and 1 stay executable and keep the current cover values. Snapshot schema 6 carries the fields; older versions upgrade with null. - Global office profile (office name/address) next to the document logo: table document_branding_settings, PUT /api/document-branding/office, editable from the print view toolbar. - Cover page reads all fields; empty values show the "[ergänzen]" marker. The Planstände editor column shows the project editor. - Shared logo constants moved to a dependency-free module so the Next.js bundler can import them. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 24 +- docs/circuit-list-editor-api.md | 8 +- docs/circuit-list-editor-known-limitations.md | 5 +- docs/current-architecture.md | 8 +- docs/print-view.md | 40 +- scripts/db-verify-circuit-schema.js | 5 + src/app/projects/[projectId]/print/page.tsx | 100 +- src/db/migrations/0009_amazing_cable.sql | 10 + src/db/migrations/meta/0009_snapshot.json | 2656 +++++++++++++++++ src/db/migrations/meta/_journal.json | 7 + .../document-branding.repository.ts | 36 +- ...ect-settings-project-command.repository.ts | 59 +- ...roject-state-restore-command.repository.ts | 4 + .../project-state-snapshot.persistence.ts | 8 + src/db/repositories/project.repository.ts | 4 + src/db/schema/document-branding-settings.ts | 13 + src/db/schema/projects.ts | 4 + .../project-settings-project-command.model.ts | 65 +- .../models/project-state-snapshot.model.ts | 47 +- .../components/project-settings-modal.tsx | 79 +- src/frontend/print/print-document.css | 3 + src/frontend/print/print-document.tsx | 38 +- src/frontend/types.ts | 9 + src/frontend/utils/api.ts | 17 +- .../document-branding.controller.ts | 18 +- src/server/controllers/project.controller.ts | 21 +- src/server/routes/document-branding.routes.ts | 1 + src/shared/constants/document-branding.ts | 9 + src/shared/constants/project-cover.ts | 45 + .../validation/document-branding.schemas.ts | 35 +- .../validation/project-structure.schemas.ts | 19 + tests/document-branding.repository.test.ts | 77 + tests/project-command.service.test.ts | 4 + ...ettings-project-command.repository.test.ts | 135 + tests/project-state-snapshot.test.ts | 64 +- 35 files changed, 3571 insertions(+), 106 deletions(-) create mode 100644 src/db/migrations/0009_amazing_cable.sql create mode 100644 src/db/migrations/meta/0009_snapshot.json create mode 100644 src/db/schema/document-branding-settings.ts create mode 100644 src/shared/constants/document-branding.ts create mode 100644 src/shared/constants/project-cover.ts create mode 100644 tests/document-branding.repository.test.ts diff --git a/AGENTS.md b/AGENTS.md index 31e70c0..500e9a6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -251,8 +251,9 @@ snapshot verifies its checksum and the current-state hash, replaces supported project data atomically and records a new `restore` revision with a complete inverse command. Restore can therefore be undone and redone after a restart. Current snapshot schema version 6 additionally stores the distribution-board -info fields `boardNumber`, `location`, `fedFrom` and `remark`; all older -supported versions are upgraded with `null` for these fields. Version 5 stores +info fields `boardNumber`, `location`, `fedFrom` and `remark` and the project +cover fields `siteAddress`, `workStage`, `projectLead` and `projectEditor`; +all older supported versions are upgraded with `null` for these fields. Version 5 stores the manual quantity share of every CircuitDeviceRow. Version 4 snapshots are upgraded with `manualQuantity = quantity`; they already contain the complete confirmed external-model source, import batches, room mappings and objects. Version 3 @@ -370,6 +371,10 @@ Project settings use the persistent `project.update-settings` command. Project metadata, the public-building classification, both voltage defaults and the enabled distribution-board supply types change in one revision and Undo/Redo restores them together; the project PUT route requires `expectedRevision`. +Schema version 3 additionally carries the print cover fields `siteAddress`, +`workStage`, `projectLead` and `projectEditor` (migration `0009`); version 2 +(without them) and baseline version 1 stay executable and keep the current +cover values. The PUT route accepts the cover fields optionally. The public-building flag is the future sizing input for requiring halogen-free cables and lines. The system catalog is `AV`, `SV`, `EV`, `USV`, `MSR`, `SiBe`; at least one must be enabled and a type used by a board @@ -474,11 +479,14 @@ Required operations: PRELIMINARY (text match of `fedFrom` against board number/name, plain sum, no upstream simultaneity factor). Replace this once the feed link is modeled. - Colors/fonts are the provisional kbp CD (`--pd-*` variables) until the - official CD specification exists. Cover fields shown as "[ergänzen]" - (address, Leistungsphase, office staff) have no data model yet. -- The document logo is global office branding: table - `document_branding_assets` (migration `0008`), endpoints - `GET|PUT|DELETE /api/document-branding/logo` and `GET /api/document-branding`. + official CD specification exists. Empty cover values are shown as + "[ergänzen]"; the Planstände editor column shows the project's current + `projectEditor` because snapshots do not record an editor. +- The document logo and the office profile (office name/address on the + cover) are global office branding: tables `document_branding_assets` + (migration `0008`) and `document_branding_settings` (migration `0009`), + endpoints `GET|PUT|DELETE /api/document-branding/logo`, + `PUT /api/document-branding/office` and `GET /api/document-branding`. It is intentionally outside project revisions, snapshots and transfer. Uploads are PNG/JPEG/SVG up to 2 MB with a content signature check; the logo is served with `nosniff` and a restrictive CSP. @@ -581,8 +589,6 @@ Users must be able to override sizing suggestions. - logical feed link between distribution boards (`fedFrom` stays free text until the project owner decides whether it references a board, a feeding circuit or both) and the resulting hierarchical power summation -- data model for the remaining print cover fields (address, Leistungsphase, - project lead/editor) and the official kbp CD values Do not implement these while working on an unrelated phase. diff --git a/docs/circuit-list-editor-api.md b/docs/circuit-list-editor-api.md index 5234670..b1d7f07 100644 --- a/docs/circuit-list-editor-api.md +++ b/docs/circuit-list-editor-api.md @@ -74,8 +74,9 @@ Protected-board structure uses persisted inverse of a delete. The dispatcher also supports the documented project-device, project settings, distribution-board and project-location commands. The generic editor commands listed here use schema version `1`. -`project.update-settings` uses version `2` and still accepts supported baseline -version `1` commands without changing the newer building classification. +`project.update-settings` uses version `3` (including the print cover fields) +and still accepts version `2` and supported baseline version `1` commands +without changing the newer cover fields or building classification. Pre-baseline formats remain unsupported. Unsupported types or schema versions are rejected. Insert commands contain the complete entity or circuit block with stable ids; delete commands include the expected parent identity. Circuit @@ -173,6 +174,9 @@ synchronizes linked circuit rows implicitly. - `PUT /projects/:projectId` - request: `{ "expectedRevision": 12, "isPublicBuilding": true, "singlePhaseVoltageV": 230, "threePhaseVoltageV": 400, "enabledDistributionBoardSupplyTypes": ["AV", "MSR", "SiBe"], ...projectMetadata }` + - optional print cover fields `siteAddress` (max. 500), `workStage` (max. + 100), `projectLead` and `projectEditor` (max. 200 each); omitted fields keep + their value, empty strings are stored as `null` - executes `project.update-settings` as one atomic revision - response: `{ "project": { ... }, "revision": { ... }, "history": { ... } }` diff --git a/docs/circuit-list-editor-known-limitations.md b/docs/circuit-list-editor-known-limitations.md index bea8b87..3e70e76 100644 --- a/docs/circuit-list-editor-known-limitations.md +++ b/docs/circuit-list-editor-known-limitations.md @@ -31,7 +31,8 @@ supply structure and "Σ inkl. unterlagert" only from exact text matches with board numbers/names and adds values without an upstream simultaneity factor (see `docs/print-view.md`). -- The print view cover page shows placeholders for address, Leistungsphase and - office staff, and uses a provisional kbp corporate design. On screen, long +- The print view uses a kbp corporate design derived from logo and + presentation material. Snapshots do not record an editor, so the cover's + Planstände list shows the project's current editor for every entry. On screen, long board plans are not split into pages; pagination happens in the browser print dialog. diff --git a/docs/current-architecture.md b/docs/current-architecture.md index 1bf0437..5230b74 100644 --- a/docs/current-architecture.md +++ b/docs/current-architecture.md @@ -232,8 +232,9 @@ und Historienstapel teilen dieselbe Transaktion. Create, Import aus der globalen Gerätebibliothek und Delete laufen über dieselbe Command-Grenze; ihre Antworten liefern Gerät und aktualisierten Historienstand an die Projektseite zurück. `project.update-settings` versioniert Projektname, interne und externe -Projektnummer, Bauherr, Beschreibung, die Kennzeichnung als öffentliches -Gebäude, beide Standardspannungen sowie die im Projekt freigeschalteten +Projektnummer, Bauherr, Beschreibung, die Deckblattangaben (Anschrift, +Leistungsphase, Projektleitung, Bearbeitung; Schema-Version 3, Migration +`0009`), die Kennzeichnung als öffentliches Gebäude, beide Standardspannungen sowie die im Projekt freigeschalteten Verteiler-Netzarten als eine atomare Änderung. Die Gebäudekennzeichnung ist der spätere Eingang für die Auswahl halogenfreier Kabel und Leitungen. Der Systemkatalog besteht aus `AV`, `SV`, `EV`, `USV`, `MSR` und `SiBe`; mindestens @@ -421,7 +422,8 @@ Derselbe Befehl versioniert die Felder der Verteiler-Infokarte Infokarte liegt im Bearbeiten-Dialog der Verteilung; die Werte sind lokale Planungswerte und sollen später über den Revit-Abgleich nur nach Vorschau und ausdrücklicher Bestätigung vorbelegt werden. `fedFrom` ist vorerst Freitext. -Snapshot-Schema 6 enthält diese Felder; ältere Versionen werden mit `null` +Snapshot-Schema 6 enthält diese Felder und die Deckblattangaben des +Projekts; ältere Versionen werden mit `null` hochgestuft. Snapshot-Schema 5 enthält zusätzlich den manuellen Mengenanteil jeder Gerätezeile. Version 4 wird mit `manualQuantity = quantity` hochgestuft und enthält bereits den vollständigen externen Modellzustand diff --git a/docs/print-view.md b/docs/print-view.md index b258d7b..202f172 100644 --- a/docs/print-view.md +++ b/docs/print-view.md @@ -1,7 +1,6 @@ # Seitenansicht und PDF-Export -Stand: erste Ausbaustufe (Beispiel zur Abstimmung). Die Seitenansicht ist -rein lesend und verändert keine Projektdaten. +Die Seitenansicht ist rein lesend und verändert keine Projektdaten. ## Aufruf @@ -12,14 +11,15 @@ rein lesend und verändert keine Projektdaten. wird als Dateiname vorgeschlagen. - **Planformat** schaltet die Verteilerpläne zwischen A4 quer und A3 quer um. Deckblatt bleibt A4 hoch. +- **Büroangaben** pflegt Büroname und Büroanschrift (Planverfasser, für alle + Projekte). - **Logo hinterlegen / ersetzen / entfernen** verwaltet das Büro-Logo. ## Seiten 1. **Deckblatt (A4 hoch)** – tabellarisch: Projekt, Planverfasser, Dokument, - Planstände (aus benannten Sicherungspunkten) und Inhalt. Angaben ohne - Datenmodell erscheinen als orangefarbenes „[ergänzen]“ (Anschrift, - Leistungsphase, Projektleitung, Bearbeitung). + Planstände (aus benannten Sicherungspunkten) und Inhalt. Leere Angaben + erscheinen als orangefarbenes „[ergänzen]“. 2. **Verteilerübersicht (quer)** – Versorgungsstruktur und Tabelle je Verteiler mit Stromkreisanzahl, Pinst (Anzahl × Leistung je Stück), PGeräte (mit Geräte-GZF), Verteiler-GZF, @@ -51,8 +51,34 @@ Seitenaufteilung langer Verteilerpläne entsteht erst beim Drucken. - **Corporate Design:** Farben (`--pd-*` in `src/frontend/print/print-document.css`) und Schrift (Arial) sind aus Logo und kbp-Präsentationsmaterial abgeleitet und werden durch die offizielle - CD-Vorgabe ersetzt. Der Büroname steht vorläufig als Konstante - `printOfficeName` in `src/frontend/print/print-document.tsx`. + CD-Vorgabe ersetzt, falls diese abweicht. +- **Planstände:** Die Spalte „Bearbeitung“ zeigt für jeden Planstand die + aktuelle Bearbeitung aus den Projekteinstellungen, weil Sicherungspunkte + (noch) keinen Bearbeiter speichern. + +## Datenquellen des Deckblatts + +| Feld | Quelle | +|---|---| +| Bauvorhaben, Bauherr, Projektnummern, Beschreibung | Projekteinstellungen | +| Anschrift Bauvorhaben, Leistungsphase, Projektleitung, Bearbeitung | Projekteinstellungen → Abschnitt „Deckblatt“ (`siteAddress`, `workStage`, `projectLead`, `projectEditor`) | +| Büro, Anschrift Büro | Seitenansicht → „Büroangaben“ (global) | +| Stand, Datenstand, Anzahl Verteiler, Inhalt | automatisch | +| Planstände | benannte Sicherungspunkte | + +Die vier Projektfelder laufen über `project.update-settings` (Schema-Version +3, Migration `0009`) und sind Teil von Snapshot-Schema 6 und Projektexport. +Im `PUT /api/projects/:projectId` sind sie optional; fehlende Felder behalten +ihren Wert. + +## Büroangaben + +- Tabelle `document_branding_settings` (Schlüssel/Wert, Migration `0009`), + global wie das Logo. +- `GET /api/document-branding` liefert zusätzlich + `office: { officeName, officeAddress }`. +- `PUT /api/document-branding/office` mit `{ "officeName", "officeAddress" }` + (max. 200 / 500 Zeichen, leerer Text = nicht gesetzt). ## Logo diff --git a/scripts/db-verify-circuit-schema.js b/scripts/db-verify-circuit-schema.js index 558178e..8d27b1d 100644 --- a/scripts/db-verify-circuit-schema.js +++ b/scripts/db-verify-circuit-schema.js @@ -13,6 +13,7 @@ const requiredTables = [ "distribution_board_components", "distribution_board_component_protection_devices", "document_branding_assets", + "document_branding_settings", ]; const tablePlaceholders = requiredTables.map(() => "?").join(", "); @@ -88,6 +89,10 @@ const requiredProjectColumns = [ "description", "is_public_building", "enabled_distribution_board_supply_types", + "site_address", + "work_stage", + "project_lead", + "project_editor", ]; const projectColumns = new Set( db.prepare("PRAGMA table_info(projects)").all().map((column) => column.name) diff --git a/src/app/projects/[projectId]/print/page.tsx b/src/app/projects/[projectId]/print/page.tsx index 91eb433..11e6710 100644 --- a/src/app/projects/[projectId]/print/page.tsx +++ b/src/app/projects/[projectId]/print/page.tsx @@ -2,7 +2,7 @@ import Link from "next/link"; import { useParams } from "next/navigation"; -import { ChangeEvent, useEffect, useMemo, useState } from "react"; +import { ChangeEvent, FormEvent, useEffect, useMemo, useState } from "react"; import "../../../../frontend/print/print-document.css"; import { deleteDocumentLogo, @@ -14,13 +14,20 @@ import { listDistributionBoards, listFloors, listProjectSnapshots, + updateOfficeProfile, uploadDocumentLogo, } from "../../../../frontend/utils/api"; import type { DocumentLogoMetadataDto, + OfficeProfileDto, ProjectDto, ProjectSnapshotMetadataDto, } from "../../../../frontend/types"; +import { FormModal } from "../../../../frontend/components/form-modal"; +import { + officeProfileFieldLabels, + officeProfileFieldMaxLengths, +} from "../../../../shared/constants/project-cover"; import { PrintDocument } from "../../../../frontend/print/print-document"; import { buildPreliminaryBoardHierarchy, @@ -30,7 +37,7 @@ import { import { documentLogoMimeTypes, maxDocumentLogoBytes, -} from "../../../../shared/validation/document-branding.schemas"; +} from "../../../../shared/constants/document-branding"; type PlanFormat = "a4" | "a3"; @@ -61,6 +68,14 @@ export default function ProjectPrintPage() { const projectId = params.projectId; const [data, setData] = useState(null); const [logo, setLogo] = useState(null); + const [office, setOffice] = useState({ + officeName: null, + officeAddress: null, + }); + const [officeDraft, setOfficeDraft] = useState<{ + officeName: string; + officeAddress: string; + } | null>(null); const [format, setFormat] = useState("a4"); const [error, setError] = useState(null); const [message, setMessage] = useState(null); @@ -116,6 +131,7 @@ export default function ProjectPrintPage() { .sort((left, right) => left.createdAtIso.localeCompare(right.createdAtIso)), }); setLogo(branding.logo); + setOffice(branding.office); document.title = `Leistungsbilanz – ${project.name}`; } load().catch((err: unknown) => { @@ -172,6 +188,28 @@ export default function ProjectPrintPage() { } } + async function handleOfficeSave(event: FormEvent) { + event.preventDefault(); + if (!officeDraft) { + return; + } + setIsBusy(true); + setError(null); + try { + const result = await updateOfficeProfile({ + officeName: officeDraft.officeName.trim() || null, + officeAddress: officeDraft.officeAddress.trim() || null, + }); + setOffice(result.office); + setOfficeDraft(null); + setMessage("Büroangaben gespeichert. Sie gelten für alle Projekte."); + } catch (err) { + setError(err instanceof Error ? err.message : "Die Büroangaben konnten nicht gespeichert werden."); + } finally { + setIsBusy(false); + } + } + async function handleLogoDelete() { if (!window.confirm("Hinterlegtes Logo für alle Projekte entfernen?")) { return; @@ -213,6 +251,19 @@ export default function ProjectPrintPage() { +