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() {
+