Add print view, board info card and document logo upload

- Seitenansicht (/projects/:projectId/print): tabular cover page, board
  overview with supply structure and power sums, one plan per board with a
  repeating title block; PDF via the browser print dialog with named @page
  rules (A4 portrait cover, A4/A3 landscape plans, page x of y).
- Distribution-board info card (board number, location, fed-from, remark)
  in the board edit dialog, persisted through the additively extended
  distribution-board.update command (migration 0008, snapshot schema 6,
  legacy subtree commands normalized).
- Global document logo upload (PNG/JPEG/SVG, max 2 MB) outside project
  revisions and snapshots.
- fed-from stays free text; the overview hierarchy and cumulated sums are
  preliminary until the feed link is modeled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Grovy311 2026-09-25 19:31:04 +02:00
parent 2e3e85a8e5
commit 4779e4a1a7
40 changed files with 5297 additions and 33 deletions

View file

@ -54,6 +54,12 @@ import {
type DistributionBoardSupplyType,
} from "../../../shared/constants/distribution-board";
import { ProjectVersionHistory } from "../../../frontend/components/project-version-history";
import {
DistributionBoardInfoCard,
fromDistributionBoardInfoDraft,
toDistributionBoardInfoDraft,
type DistributionBoardInfoDraft,
} from "../../../frontend/components/distribution-board-info-card";
import {
ProjectSettingsModal,
type ProjectSettingsInput,
@ -101,6 +107,15 @@ export default function ProjectDetailPage() {
setEditingBoardSimultaneityFactor,
] = useState("1");
const [editingBoardCopyName, setEditingBoardCopyName] = useState("");
const [editingBoardInfo, setEditingBoardInfo] =
useState<DistributionBoardInfoDraft>(
toDistributionBoardInfoDraft({
boardNumber: null,
location: null,
fedFrom: null,
remark: null,
})
);
const [floorName, setFloorName] = useState("");
const [editingFloor, setEditingFloor] = useState<FloorDto | null>(null);
const [editingRoom, setEditingRoom] = useState<RoomDto | null>(null);
@ -424,6 +439,7 @@ export default function ProjectDetailPage() {
String(board.simultaneityFactor)
);
setEditingBoardCopyName(`${board.name} Kopie`);
setEditingBoardInfo(toDistributionBoardInfoDraft(board));
}
function openBoardCreator() {
@ -459,6 +475,7 @@ export default function ProjectDetailPage() {
floorId: editingBoardFloorId || null,
supplyType: editingBoardSupplyType,
simultaneityFactor,
...fromDistributionBoardInfoDraft(editingBoardInfo),
},
project.currentRevision
);
@ -849,6 +866,13 @@ export default function ProjectDetailPage() {
</p>
</div>
<div className="d-flex gap-2">
<Link
className="btn btn-primary"
href={`/projects/${projectId}/print`}
target="_blank"
>
Seitenansicht
</Link>
<button
className="btn btn-outline-primary"
disabled={!project}
@ -922,7 +946,9 @@ export default function ProjectDetailPage() {
<table className="table table-hover align-middle mb-0">
<thead>
<tr>
<th>Nr.</th>
<th>Verteilung</th>
<th>Örtlichkeit</th>
<th>Etage</th>
<th>Netzart</th>
<th>GZF</th>
@ -934,7 +960,11 @@ export default function ProjectDetailPage() {
const circuitList = circuitListByBoardId.get(board.id);
return (
<tr key={board.id}>
<td className="fw-semibold">
{board.boardNumber ?? "–"}
</td>
<td>{board.name}</td>
<td>{board.location ?? "–"}</td>
<td>
{board.floorId
? floorById.get(board.floorId)?.name ?? "Unbekannt"
@ -985,7 +1015,7 @@ export default function ProjectDetailPage() {
})}
{!boards.length ? (
<tr>
<td colSpan={5} className="text-center text-secondary py-4">
<td colSpan={7} className="text-center text-secondary py-4">
Noch keine Verteilungen vorhanden.
</td>
</tr>
@ -1464,6 +1494,12 @@ export default function ProjectDetailPage() {
</div>
</div>
</div>
<div className="mt-4">
<DistributionBoardInfoCard
draft={editingBoardInfo}
onChange={setEditingBoardInfo}
/>
</div>
<hr className="my-4" />
<section aria-labelledby="copy-distribution-board-heading">
<h3