forked from jappel/leistungsbilanz-ts
388 lines
16 KiB
TypeScript
388 lines
16 KiB
TypeScript
import type { Request, Response } from "express";
|
|
import { calculateDistributionBoardLoads } from "../../domain/calculations/distribution-board-load-calculation.js";
|
|
import { calculateRowTotalPower } from "../../domain/calculations/circuit-power-calculation.js";
|
|
import type {
|
|
CircuitTreeProtectionDevice,
|
|
CircuitTreeResponse,
|
|
ProjectCircuitTreesResponse,
|
|
} from "../../domain/models/circuit-tree.model.js";
|
|
import {
|
|
circuitDeviceRowRepository,
|
|
circuitListRepository,
|
|
circuitProtectionDeviceRepository,
|
|
circuitRepository,
|
|
circuitSectionRepository,
|
|
distributionBoardComponentRepository,
|
|
distributionBoardRepository,
|
|
externalModelStateRepository,
|
|
projectRepository,
|
|
} from "../composition/application-repositories.js";
|
|
|
|
export function isMissingCircuitTreeSchemaError(error: unknown): boolean {
|
|
if (!(error instanceof Error)) return false;
|
|
return (
|
|
error.message.includes("no such table: circuit_sections") ||
|
|
error.message.includes("no such table: circuits") ||
|
|
error.message.includes("no such table: circuit_device_rows") ||
|
|
error.message.includes("no such table: distribution_board_components") ||
|
|
error.message.includes("no such table: circuit_protection_devices")
|
|
);
|
|
}
|
|
|
|
export async function getCircuitTree(req: Request, res: Response) {
|
|
const { projectId, circuitListId } = req.params;
|
|
if (typeof projectId !== "string" || typeof circuitListId !== "string") {
|
|
return res.status(400).json({ error: "Invalid parameters" });
|
|
}
|
|
const list = await circuitListRepository.findById(projectId, circuitListId);
|
|
if (!list) return res.status(404).json({ error: "Circuit list not found" });
|
|
|
|
try {
|
|
const result = await buildProjectCircuitTrees(projectId);
|
|
if (!result) return res.status(404).json({ error: "Project not found" });
|
|
const tree = result.trees.find((entry) => entry.circuitListId === circuitListId);
|
|
if (!tree) {
|
|
return res.status(404).json({ error: "Distribution board not found" });
|
|
}
|
|
return res.json(tree);
|
|
} catch (error) {
|
|
if (isMissingCircuitTreeSchemaError(error)) {
|
|
const project = await projectRepository.findById(projectId);
|
|
const board = await distributionBoardRepository.findById(
|
|
projectId,
|
|
list.distributionBoardId
|
|
);
|
|
if (!project || !board) {
|
|
return res.status(404).json({ error: "Project structure not found" });
|
|
}
|
|
return res.json(emptyCircuitTree(project, board, circuitListId));
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function getProjectCircuitTrees(req: Request, res: Response) {
|
|
const { projectId } = req.params;
|
|
if (typeof projectId !== "string") {
|
|
return res.status(400).json({ error: "Invalid parameters" });
|
|
}
|
|
try {
|
|
const result = await buildProjectCircuitTrees(projectId);
|
|
if (!result) return res.status(404).json({ error: "Project not found" });
|
|
return res.json(result);
|
|
} catch (error) {
|
|
if (isMissingCircuitTreeSchemaError(error)) {
|
|
const project = await projectRepository.findById(projectId);
|
|
if (!project) return res.status(404).json({ error: "Project not found" });
|
|
return res.json({ currentRevision: project.currentRevision, trees: [] });
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function buildProjectCircuitTrees(
|
|
projectId: string
|
|
): Promise<ProjectCircuitTreesResponse | null> {
|
|
const project = await projectRepository.findById(projectId);
|
|
if (!project) return null;
|
|
const [lists, boards] = await Promise.all([
|
|
circuitListRepository.listByProject(projectId),
|
|
distributionBoardRepository.listByProject(projectId),
|
|
]);
|
|
const externalState = externalModelStateRepository.getByProject(projectId).state;
|
|
const externalCountByRowId = new Map<string, number>();
|
|
for (const object of externalState.objects) {
|
|
if (object.circuitDeviceRowId) {
|
|
externalCountByRowId.set(object.circuitDeviceRowId, (externalCountByRowId.get(object.circuitDeviceRowId) ?? 0) + 1);
|
|
}
|
|
}
|
|
const listIds = lists.map((entry) => entry.id);
|
|
const [sections, circuits, components] = await Promise.all([
|
|
circuitSectionRepository.listByCircuitLists(listIds),
|
|
circuitRepository.listByCircuitLists(listIds),
|
|
distributionBoardComponentRepository.listByCircuitLists(listIds),
|
|
]);
|
|
const [rows, circuitProtections, componentProtections] = await Promise.all([
|
|
circuitDeviceRowRepository.listByCircuitList(circuits.map((entry) => entry.id)),
|
|
circuitProtectionDeviceRepository.listByCircuitIds(circuits.map((entry) => entry.id)),
|
|
distributionBoardComponentRepository.listProtectionByComponentIds(
|
|
components.map((entry) => entry.id)
|
|
),
|
|
]);
|
|
|
|
const boardById = new Map(boards.map((board) => [board.id, board]));
|
|
const listById = new Map(lists.map((list) => [list.id, list]));
|
|
const listByBoardId = new Map(
|
|
lists.map((list) => [list.distributionBoardId, list])
|
|
);
|
|
const sectionById = new Map(sections.map((section) => [section.id, section]));
|
|
const circuitById = new Map(circuits.map((circuit) => [circuit.id, circuit]));
|
|
const rowsByCircuitId = groupBy(rows, (row) => row.circuitId);
|
|
const sectionsByListId = groupBy(sections, (section) => section.circuitListId);
|
|
const circuitsByListId = groupBy(circuits, (circuit) => circuit.circuitListId);
|
|
const componentsByListId = groupBy(components, (component) => component.circuitListId);
|
|
const circuitProtectionById = new Map(
|
|
circuitProtections.map((entry) => [entry.circuitId, entry])
|
|
);
|
|
const componentProtectionById = new Map(
|
|
componentProtections.map((entry) => [entry.componentId, entry])
|
|
);
|
|
|
|
const loads = calculateDistributionBoardLoads({
|
|
boards: boards.map((board) => ({
|
|
id: board.id,
|
|
name: board.name,
|
|
boardNumber: board.boardNumber,
|
|
simultaneityFactor: board.simultaneityFactor,
|
|
feedingDistributionBoardId: board.feedingDistributionBoardId,
|
|
feedingCircuitId: board.feedingCircuitId,
|
|
})),
|
|
circuits: circuits.map((circuit) => {
|
|
const list = listById.get(circuit.circuitListId);
|
|
if (!list) throw new Error("Circuit list is missing from project tree.");
|
|
return {
|
|
id: circuit.id,
|
|
distributionBoardId: list.distributionBoardId,
|
|
rows: (rowsByCircuitId.get(circuit.id) ?? []).map((row) => ({
|
|
activePowerKw: calculateRowTotalPower(
|
|
row.quantity,
|
|
row.powerPerUnit,
|
|
row.simultaneityFactor
|
|
),
|
|
cosPhi: row.cosPhi,
|
|
})),
|
|
};
|
|
}),
|
|
});
|
|
|
|
const trees = lists.map((list): CircuitTreeResponse => {
|
|
const board = boardById.get(list.distributionBoardId);
|
|
if (!board) throw new Error("Distribution board is missing from project tree.");
|
|
const boardLoad = loads.boards.get(board.id);
|
|
if (!boardLoad) throw new Error("Distribution-board load is missing.");
|
|
const tree: CircuitTreeResponse = {
|
|
circuitListId: list.id,
|
|
distributionBoardId: board.id,
|
|
distributionBoardName: board.name,
|
|
distributionBoardNumber: board.boardNumber ?? undefined,
|
|
distributionBoardSupplyType: board.supplyType ?? undefined,
|
|
feedingReference: resolveFeedingReference(
|
|
board,
|
|
boardById,
|
|
listByBoardId,
|
|
circuitById,
|
|
sectionById
|
|
),
|
|
currentRevision: project.currentRevision,
|
|
singlePhaseVoltageV: project.singlePhaseVoltageV,
|
|
threePhaseVoltageV: project.threePhaseVoltageV,
|
|
distributionBoardSimultaneityFactor: board.simultaneityFactor,
|
|
distributionBoardTotalPower: boardLoad.activePowerBeforeSimultaneityKw,
|
|
distributionBoardTotalPowerWithSimultaneityFactor:
|
|
boardLoad.demandActivePowerKw,
|
|
headerComponents: [],
|
|
sections: (sectionsByListId.get(list.id) ?? []).map((section) => ({
|
|
id: section.id,
|
|
key: section.key,
|
|
displayName: section.displayName,
|
|
prefix: section.prefix,
|
|
sortOrder: section.sortOrder,
|
|
category: section.category ?? undefined,
|
|
groupNumber: section.groupNumber ?? undefined,
|
|
sectionTotalPower: 0,
|
|
components: [],
|
|
circuits: [],
|
|
})),
|
|
footerComponents: [],
|
|
};
|
|
const sectionBlocks = new Map(tree.sections.map((section) => [section.id, section]));
|
|
for (const component of componentsByListId.get(list.id) ?? []) {
|
|
const protection = componentProtectionById.get(component.id);
|
|
const dto = {
|
|
id: component.id,
|
|
circuitListId: component.circuitListId,
|
|
sectionId: component.sectionId ?? undefined,
|
|
equipmentIdentifier: component.equipmentIdentifier,
|
|
name: component.name,
|
|
role: component.role,
|
|
placement: component.placement,
|
|
sortOrder: component.sortOrder,
|
|
protectionDevice: protection ? toProtectionDeviceDto(protection) : undefined,
|
|
};
|
|
if (component.placement === "header") tree.headerComponents.push(dto);
|
|
else if (component.placement === "footer") tree.footerComponents.push(dto);
|
|
else if (component.sectionId !== null) {
|
|
sectionBlocks.get(component.sectionId)?.components.push(dto);
|
|
}
|
|
}
|
|
|
|
for (const circuit of circuitsByListId.get(list.id) ?? []) {
|
|
const block = sectionBlocks.get(circuit.sectionId);
|
|
if (!block) continue;
|
|
const deviceRows = (rowsByCircuitId.get(circuit.id) ?? []).map((row) => ({
|
|
id: row.id,
|
|
linkedProjectDeviceId: row.linkedProjectDeviceId ?? undefined,
|
|
sortOrder: row.sortOrder,
|
|
name: row.name,
|
|
displayName: row.displayName,
|
|
phaseType: row.phaseType ?? undefined,
|
|
connectionKind: row.connectionKind ?? undefined,
|
|
costGroup: row.costGroup ?? undefined,
|
|
category: row.category ?? undefined,
|
|
level: row.level ?? undefined,
|
|
roomId: row.roomId ?? undefined,
|
|
roomNumberSnapshot: row.roomNumberSnapshot ?? undefined,
|
|
roomNameSnapshot: row.roomNameSnapshot ?? undefined,
|
|
quantity: row.quantity,
|
|
manualQuantity: row.manualQuantity,
|
|
powerPerUnit: row.powerPerUnit,
|
|
simultaneityFactor: row.simultaneityFactor,
|
|
cosPhi: row.cosPhi ?? undefined,
|
|
remark: row.remark ?? undefined,
|
|
overriddenFields: row.overriddenFields ?? undefined,
|
|
rowTotalPower: calculateRowTotalPower(
|
|
row.quantity,
|
|
row.powerPerUnit,
|
|
row.simultaneityFactor
|
|
),
|
|
externalObjectCount: externalCountByRowId.get(row.id) ?? 0,
|
|
}));
|
|
const circuitLoad = loads.circuits.get(circuit.id);
|
|
if (!circuitLoad) throw new Error("Circuit load is missing.");
|
|
const protection = circuitProtectionById.get(circuit.id);
|
|
block.circuits.push({
|
|
id: circuit.id,
|
|
circuitListId: circuit.circuitListId,
|
|
sectionId: circuit.sectionId,
|
|
equipmentIdentifier: circuit.equipmentIdentifier,
|
|
displayName: circuit.displayName ?? undefined,
|
|
sortOrder: circuit.sortOrder,
|
|
cableType: circuit.cableType ?? undefined,
|
|
cableCrossSection: circuit.cableCrossSection ?? undefined,
|
|
cableLength: circuit.cableLength ?? undefined,
|
|
rcdAssignment: circuit.rcdAssignment ?? undefined,
|
|
terminalDesignation: circuit.terminalDesignation ?? undefined,
|
|
voltage: circuit.voltage ?? undefined,
|
|
controlRequirement: circuit.controlRequirement ?? undefined,
|
|
status: circuit.status ?? undefined,
|
|
isReserve:
|
|
Boolean(circuit.isReserve) &&
|
|
circuitLoad.downstreamBoards.length === 0,
|
|
remark: circuit.remark ?? undefined,
|
|
circuitTotalPower: circuitLoad.activePowerKw,
|
|
circuitApparentPower: circuitLoad.apparentPowerKva,
|
|
circuitCosPhi: circuitLoad.cosPhi,
|
|
deviceRows,
|
|
downstreamBoardLoads: circuitLoad.downstreamBoards
|
|
.map((load) => ({
|
|
distributionBoardId: load.distributionBoardId,
|
|
name: load.name,
|
|
boardNumber: load.boardNumber ?? undefined,
|
|
supplyType: boardById.get(load.distributionBoardId)?.supplyType ?? undefined,
|
|
activePowerKw: load.activePowerKw,
|
|
apparentPowerKva: load.apparentPowerKva,
|
|
cosPhi: load.cosPhi,
|
|
}))
|
|
.sort((left, right) =>
|
|
(left.boardNumber ?? left.name).localeCompare(
|
|
right.boardNumber ?? right.name,
|
|
"de"
|
|
)
|
|
),
|
|
protectionDevice: protection ? toProtectionDeviceDto(protection) : undefined,
|
|
});
|
|
}
|
|
for (const section of tree.sections) {
|
|
section.sectionTotalPower = section.circuits.reduce(
|
|
(sum, circuit) => sum + circuit.circuitTotalPower,
|
|
0
|
|
);
|
|
}
|
|
return tree;
|
|
});
|
|
|
|
return { currentRevision: project.currentRevision, trees };
|
|
}
|
|
|
|
function resolveFeedingReference(
|
|
board: Awaited<ReturnType<typeof distributionBoardRepository.listByProject>>[number],
|
|
boardById: Map<string, Awaited<ReturnType<typeof distributionBoardRepository.listByProject>>[number]>,
|
|
listByBoardId: Map<string, Awaited<ReturnType<typeof circuitListRepository.listByProject>>[number]>,
|
|
circuitById: Map<string, Awaited<ReturnType<typeof circuitRepository.listByCircuitLists>>[number]>,
|
|
sectionById: Map<string, Awaited<ReturnType<typeof circuitSectionRepository.listByCircuitLists>>[number]>
|
|
) {
|
|
if (board.feedingDistributionBoardId === null) return undefined;
|
|
const feedingBoard = boardById.get(board.feedingDistributionBoardId);
|
|
if (!feedingBoard) throw new Error("Feeding distribution board is missing.");
|
|
const feedingList = listByBoardId.get(feedingBoard.id);
|
|
if (!feedingList) throw new Error("Feeding circuit list is missing.");
|
|
const feedingCircuit = board.feedingCircuitId === null
|
|
? undefined
|
|
: circuitById.get(board.feedingCircuitId);
|
|
return {
|
|
distributionBoardId: feedingBoard.id,
|
|
circuitListId: feedingList.id,
|
|
distributionBoardName: feedingBoard.name,
|
|
distributionBoardNumber: feedingBoard.boardNumber ?? undefined,
|
|
circuitId: feedingCircuit?.id,
|
|
circuitEquipmentIdentifier: feedingCircuit?.equipmentIdentifier,
|
|
circuitDisplayName: feedingCircuit?.displayName ?? undefined,
|
|
circuitCategory: feedingCircuit
|
|
? sectionById.get(feedingCircuit.sectionId)?.category ?? undefined
|
|
: undefined,
|
|
};
|
|
}
|
|
|
|
function emptyCircuitTree(
|
|
project: NonNullable<Awaited<ReturnType<typeof projectRepository.findById>>>,
|
|
board: NonNullable<Awaited<ReturnType<typeof distributionBoardRepository.findById>>>,
|
|
circuitListId: string
|
|
): CircuitTreeResponse & { warning: string } {
|
|
return {
|
|
circuitListId,
|
|
distributionBoardId: board.id,
|
|
distributionBoardName: board.name,
|
|
distributionBoardNumber: board.boardNumber ?? undefined,
|
|
distributionBoardSupplyType: board.supplyType ?? undefined,
|
|
currentRevision: project.currentRevision,
|
|
singlePhaseVoltageV: project.singlePhaseVoltageV,
|
|
threePhaseVoltageV: project.threePhaseVoltageV,
|
|
distributionBoardSimultaneityFactor: board.simultaneityFactor,
|
|
distributionBoardTotalPower: 0,
|
|
distributionBoardTotalPowerWithSimultaneityFactor: 0,
|
|
headerComponents: [],
|
|
sections: [],
|
|
footerComponents: [],
|
|
warning:
|
|
"Circuit-first tables are not available yet. Run database migrations (including 0008_circuit_first_model).",
|
|
};
|
|
}
|
|
|
|
function toProtectionDeviceDto(value: {
|
|
type: CircuitTreeProtectionDevice["type"];
|
|
ratedCurrentA: number;
|
|
fuseUtilizationCategory: CircuitTreeProtectionDevice["fuseUtilizationCategory"] | null;
|
|
tripCharacteristic: CircuitTreeProtectionDevice["tripCharacteristic"] | null;
|
|
rcdType: CircuitTreeProtectionDevice["rcdType"] | null;
|
|
ratedResidualCurrentMa: number | null;
|
|
}): CircuitTreeProtectionDevice {
|
|
return {
|
|
type: value.type,
|
|
ratedCurrentA: value.ratedCurrentA,
|
|
fuseUtilizationCategory: value.fuseUtilizationCategory ?? undefined,
|
|
tripCharacteristic: value.tripCharacteristic ?? undefined,
|
|
rcdType: value.rcdType ?? undefined,
|
|
ratedResidualCurrentMa: value.ratedResidualCurrentMa ?? undefined,
|
|
};
|
|
}
|
|
|
|
function groupBy<T>(values: T[], keyOf: (value: T) => string) {
|
|
const groups = new Map<string, T[]>();
|
|
for (const value of values) {
|
|
const key = keyOf(value);
|
|
const group = groups.get(key) ?? [];
|
|
group.push(value);
|
|
groups.set(key, group);
|
|
}
|
|
return groups;
|
|
}
|