- 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>
209 lines
6.7 KiB
TypeScript
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 };
|
|
}
|