leistungsbilanz-ts/src/server/controllers/distribution-board.controller.ts
Grovy311 4779e4a1a7 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>
2026-09-26 10:38:59 +02:00

209 lines
6.7 KiB
TypeScript

import type { Request, Response } from "express";
import { createDistributionBoardUpdateProjectCommand } from "../../domain/models/distribution-board-project-command.model.js";
import {
createDistributionBoardInsertProjectCommand,
createDistributionBoardStructureSnapshot,
} from "../../domain/models/distribution-board-structure-project-command.model.js";
import {
createDistributionBoardDeleteSubtreeProjectCommand,
createDistributionBoardInsertSubtreeProjectCommand,
} from "../../domain/models/distribution-board-subtree-project-command.model.js";
import { cloneDistributionBoardSubtree } from "../../domain/models/distribution-board-subtree-snapshot.model.js";
import { distributionBoardInfoFields } from "../../shared/constants/distribution-board.js";
import {
copyDistributionBoardSchema,
createDistributionBoardSchema,
deleteDistributionBoardSchema,
updateDistributionBoardSchema,
} from "../../shared/validation/project-structure.schemas.js";
import {
distributionBoardSubtreeProjectCommandStore,
projectCommandService,
} from "../composition/project-command-stores.js";
import { distributionBoardRepository } from "../composition/application-repositories.js";
import { respondWithProjectCommandError } from "./project-command.controller.js";
export async function listDistributionBoardsByProject(req: Request, res: Response) {
const { projectId } = req.params;
if (typeof projectId !== "string") {
return res.status(400).json({ error: "Invalid projectId" });
}
const result = await distributionBoardRepository.listByProject(projectId);
return res.json(result);
}
export async function createDistributionBoard(req: Request, res: Response) {
const { projectId } = req.params;
if (typeof projectId !== "string") {
return res.status(400).json({ error: "Invalid projectId" });
}
const parsed = createDistributionBoardSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({ error: parsed.error.flatten() });
}
const structure = createDistributionBoardStructureSnapshot(
projectId,
parsed.data.name,
{
floorId: parsed.data.floorId,
supplyType: parsed.data.supplyType,
}
);
try {
const result = projectCommandService.executeUser({
projectId,
expectedRevision: parsed.data.expectedRevision,
description: "Verteilung anlegen",
command:
createDistributionBoardInsertProjectCommand(structure),
});
return res.status(201).json({
...result,
distributionBoard: {
...structure.distributionBoard,
simultaneityFactor: 1,
boardNumber: null,
location: null,
fedFrom: null,
remark: null,
},
});
} catch (error) {
return respondWithProjectCommandError(error, res);
}
}
export async function updateDistributionBoard(
req: Request,
res: Response
) {
const { projectId, distributionBoardId } = req.params;
if (
typeof projectId !== "string" ||
typeof distributionBoardId !== "string"
) {
return res.status(400).json({ error: "Invalid distribution board id" });
}
const parsed = updateDistributionBoardSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({ error: parsed.error.flatten() });
}
// Info fields are optional in the request; only submitted fields become
// part of the command and therefore of its inverse.
const infoPatch = Object.fromEntries(
distributionBoardInfoFields
.filter((field) => parsed.data[field] !== undefined)
.map((field) => [field, parsed.data[field]])
);
try {
const result = projectCommandService.executeUser({
projectId,
expectedRevision: parsed.data.expectedRevision,
description: "Verteilung bearbeiten",
command: createDistributionBoardUpdateProjectCommand(
distributionBoardId,
{
floorId: parsed.data.floorId,
supplyType: parsed.data.supplyType,
simultaneityFactor: parsed.data.simultaneityFactor,
...infoPatch,
}
),
});
const distributionBoard = (
await distributionBoardRepository.listByProject(projectId)
).find((board) => board.id === distributionBoardId);
if (!distributionBoard) {
return res.status(404).json({ error: "Distribution board not found" });
}
return res.json({ ...result, distributionBoard });
} catch (error) {
return respondWithProjectCommandError(error, res);
}
}
export async function copyDistributionBoard(
req: Request,
res: Response
) {
const ids = getDistributionBoardIds(req, res);
if (!ids) {
return;
}
const parsed = copyDistributionBoardSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({ error: parsed.error.flatten() });
}
try {
const source = distributionBoardSubtreeProjectCommandStore.capture(
ids.projectId,
ids.distributionBoardId
);
const copy = cloneDistributionBoardSubtree(source, parsed.data.name);
const result = projectCommandService.executeUser({
projectId: ids.projectId,
expectedRevision: parsed.data.expectedRevision,
description: "Verteilung kopieren",
command:
createDistributionBoardInsertSubtreeProjectCommand(copy),
});
return res.status(201).json({
...result,
distributionBoard: copy.distributionBoard,
circuitList: copy.circuitList,
});
} catch (error) {
return respondWithProjectCommandError(error, res);
}
}
export async function deleteDistributionBoard(
req: Request,
res: Response
) {
const ids = getDistributionBoardIds(req, res);
if (!ids) {
return;
}
const parsed = deleteDistributionBoardSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({ error: parsed.error.flatten() });
}
try {
const snapshot =
distributionBoardSubtreeProjectCommandStore.capture(
ids.projectId,
ids.distributionBoardId
);
const result = projectCommandService.executeUser({
projectId: ids.projectId,
expectedRevision: parsed.data.expectedRevision,
description: "Verteilung löschen",
command:
createDistributionBoardDeleteSubtreeProjectCommand(snapshot),
});
return res.json({
...result,
distributionBoardId: ids.distributionBoardId,
});
} catch (error) {
return respondWithProjectCommandError(error, res);
}
}
function getDistributionBoardIds(req: Request, res: Response) {
const { projectId, distributionBoardId } = req.params;
if (
typeof projectId !== "string" ||
typeof distributionBoardId !== "string" ||
!projectId.trim() ||
!distributionBoardId.trim()
) {
res.status(400).json({ error: "Invalid distribution board id" });
return null;
}
return { projectId, distributionBoardId };
}