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 { 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(); 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>[number], boardById: Map>[number]>, listByBoardId: Map>[number]>, circuitById: Map>[number]>, sectionById: Map>[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>>, board: NonNullable>>, 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(values: T[], keyOf: (value: T) => string) { const groups = new Map(); for (const value of values) { const key = keyOf(value); const group = groups.get(key) ?? []; group.push(value); groups.set(key, group); } return groups; }