import type { Request, Response } from "express"; import { applyDistributionBoardSimultaneityFactor, calculateCircuitTotalPower, calculateDistributionBoardTotalPower, calculateRowTotalPower, calculateSectionTotalPower, } from "../../domain/calculations/circuit-power-calculation.js"; import type { CircuitTreeResponse } from "../../domain/models/circuit-tree.model.js"; import type { CircuitTreeProtectionDevice } from "../../domain/models/circuit-tree.model.js"; import { circuitDeviceRowRepository, circuitListRepository, circuitProtectionDeviceRepository, circuitRepository, circuitSectionRepository, distributionBoardComponentRepository, distributionBoardRepository, 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" }); } const project = await projectRepository.findById(projectId); if (!project) { return res.status(404).json({ error: "Project not found" }); } const distributionBoard = await distributionBoardRepository.findById( projectId, list.distributionBoardId ); if (!distributionBoard) { return res.status(404).json({ error: "Distribution board not found", }); } try { const sections = await circuitSectionRepository.listByCircuitList(circuitListId); const circuits = await circuitRepository.listByCircuitList(circuitListId); const rows = await circuitDeviceRowRepository.listByCircuitList(circuits.map((entry) => entry.id)); const components = await distributionBoardComponentRepository.listByCircuitList( circuitListId ); const [circuitProtections, componentProtections] = await Promise.all([ circuitProtectionDeviceRepository.listByCircuitIds( circuits.map((entry) => entry.id) ), distributionBoardComponentRepository.listProtectionByComponentIds( components.map((entry) => entry.id) ), ]); const sectionById = new Map(sections.map((section) => [section.id, section])); const rowsByCircuitId = new Map(); const circuitProtectionById = new Map( circuitProtections.map((entry) => [entry.circuitId, entry]) ); const componentProtectionById = new Map( componentProtections.map((entry) => [entry.componentId, entry]) ); for (const row of rows) { if (!rowsByCircuitId.has(row.circuitId)) { rowsByCircuitId.set(row.circuitId, []); } rowsByCircuitId.get(row.circuitId)!.push(row); } const tree: CircuitTreeResponse = { circuitListId, currentRevision: project.currentRevision, singlePhaseVoltageV: project.singlePhaseVoltageV, threePhaseVoltageV: project.threePhaseVoltageV, distributionBoardSimultaneityFactor: distributionBoard.simultaneityFactor, distributionBoardTotalPower: 0, distributionBoardTotalPowerWithSimultaneityFactor: 0, headerComponents: [], sections: sections.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 components) { 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 circuits) { const section = sectionById.get(circuit.sectionId); if (!section || section.circuitListId !== circuit.circuitListId) { 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), })); const circuitTotalPower = calculateCircuitTotalPower(deviceRows); const protection = circuitProtectionById.get(circuit.id); sectionBlocks.get(section.id)?.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), remark: circuit.remark ?? undefined, circuitTotalPower, deviceRows, protectionDevice: protection ? toProtectionDeviceDto(protection) : undefined, }); } for (const section of tree.sections) { section.sectionTotalPower = calculateSectionTotalPower( section.circuits ); } tree.distributionBoardTotalPower = calculateDistributionBoardTotalPower(tree.sections); tree.distributionBoardTotalPowerWithSimultaneityFactor = applyDistributionBoardSimultaneityFactor( tree.distributionBoardTotalPower, tree.distributionBoardSimultaneityFactor ); return res.json(tree); } catch (error) { if (isMissingCircuitTreeSchemaError(error)) { return res.json({ circuitListId, currentRevision: project.currentRevision, singlePhaseVoltageV: project.singlePhaseVoltageV, threePhaseVoltageV: project.threePhaseVoltageV, distributionBoardSimultaneityFactor: distributionBoard.simultaneityFactor, distributionBoardTotalPower: 0, distributionBoardTotalPowerWithSimultaneityFactor: 0, headerComponents: [], sections: [], footerComponents: [], warning: "Circuit-first tables are not available yet. Run database migrations (including 0008_circuit_first_model).", }); } throw error; } } 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, }; }