Expose protected tree structure

This commit is contained in:
2026-07-30 20:14:45 +02:00
parent 99bdf6491b
commit 7a9530a914
11 changed files with 474 additions and 5 deletions
@@ -0,0 +1,17 @@
import { inArray } from "drizzle-orm";
import type { AppDatabase } from "../database-context.js";
import { circuitProtectionDevices } from "../schema/circuit-protection-devices.js";
export class CircuitProtectionDeviceRepository {
constructor(private readonly database: AppDatabase) {}
async listByCircuitIds(circuitIds: string[]) {
if (circuitIds.length === 0) {
return [];
}
return this.database
.select()
.from(circuitProtectionDevices)
.where(inArray(circuitProtectionDevices.circuitId, circuitIds));
}
}
@@ -0,0 +1,36 @@
import { asc, eq, inArray } from "drizzle-orm";
import type { AppDatabase } from "../database-context.js";
import { distributionBoardComponentProtectionDevices } from "../schema/distribution-board-component-protection-devices.js";
import { distributionBoardComponents } from "../schema/distribution-board-components.js";
export class DistributionBoardComponentRepository {
constructor(private readonly database: AppDatabase) {}
async listByCircuitList(circuitListId: string) {
return this.database
.select()
.from(distributionBoardComponents)
.where(
eq(distributionBoardComponents.circuitListId, circuitListId)
)
.orderBy(
asc(distributionBoardComponents.sortOrder),
asc(distributionBoardComponents.id)
);
}
async listProtectionByComponentIds(componentIds: string[]) {
if (componentIds.length === 0) {
return [];
}
return this.database
.select()
.from(distributionBoardComponentProtectionDevices)
.where(
inArray(
distributionBoardComponentProtectionDevices.componentId,
componentIds
)
);
}
}
+38
View File
@@ -44,6 +44,7 @@ export interface CircuitTreeCircuit {
remark?: string;
circuitTotalPower: number;
deviceRows: CircuitTreeDeviceRow[];
protectionDevice?: CircuitTreeProtectionDevice;
}
export interface CircuitTreeSectionBlock {
@@ -52,7 +53,10 @@ export interface CircuitTreeSectionBlock {
displayName: string;
prefix: string;
sortOrder: number;
category?: CircuitGroupCategory;
groupNumber?: number;
sectionTotalPower: number;
components: CircuitTreeComponent[];
circuits: CircuitTreeCircuit[];
}
@@ -64,7 +68,9 @@ export interface CircuitTreeResponse {
distributionBoardSimultaneityFactor: number;
distributionBoardTotalPower: number;
distributionBoardTotalPowerWithSimultaneityFactor: number;
headerComponents: CircuitTreeComponent[];
sections: CircuitTreeSectionBlock[];
footerComponents: CircuitTreeComponent[];
}
export interface LegacyMigrationReport {
@@ -78,3 +84,35 @@ export interface LegacyMigrationReport {
warnings: string[];
}
import type { CircuitGroupCategory } from "../../shared/constants/circuit-group.js";
import type {
DistributionBoardComponentPlacement,
DistributionBoardComponentRole,
} from "../../shared/constants/distribution-board-component.js";
import type {
BreakerTripCharacteristic,
FuseUtilizationCategory,
ProtectionDeviceType,
RcdType,
} from "../../shared/constants/protection-device.js";
export interface CircuitTreeProtectionDevice {
type: ProtectionDeviceType;
ratedCurrentA: number;
fuseUtilizationCategory?: FuseUtilizationCategory;
tripCharacteristic?: BreakerTripCharacteristic;
rcdType?: RcdType;
ratedResidualCurrentMa?: number;
}
export interface CircuitTreeComponent {
id: string;
circuitListId: string;
sectionId?: string;
equipmentIdentifier: string;
name: string;
role: DistributionBoardComponentRole;
placement: DistributionBoardComponentPlacement;
sortOrder: number;
protectionDevice?: CircuitTreeProtectionDevice;
}
+38
View File
@@ -1,4 +1,15 @@
import type { DistributionBoardSupplyType } from "../shared/constants/distribution-board";
import type { CircuitGroupCategory } from "../shared/constants/circuit-group";
import type {
DistributionBoardComponentPlacement,
DistributionBoardComponentRole,
} from "../shared/constants/distribution-board-component";
import type {
BreakerTripCharacteristic,
FuseUtilizationCategory,
ProtectionDeviceType,
RcdType,
} from "../shared/constants/protection-device";
export interface ProjectDto {
id: string;
@@ -267,6 +278,27 @@ export interface CircuitTreeDeviceRowDto {
rowTotalPower: number;
}
export interface CircuitTreeProtectionDeviceDto {
type: ProtectionDeviceType;
ratedCurrentA: number;
fuseUtilizationCategory?: FuseUtilizationCategory;
tripCharacteristic?: BreakerTripCharacteristic;
rcdType?: RcdType;
ratedResidualCurrentMa?: number;
}
export interface CircuitTreeComponentDto {
id: string;
circuitListId: string;
sectionId?: string;
equipmentIdentifier: string;
name: string;
role: DistributionBoardComponentRole;
placement: DistributionBoardComponentPlacement;
sortOrder: number;
protectionDevice?: CircuitTreeProtectionDeviceDto;
}
export interface CircuitTreeCircuitDto {
id: string;
circuitListId: string;
@@ -289,6 +321,7 @@ export interface CircuitTreeCircuitDto {
remark?: string;
circuitTotalPower: number;
deviceRows: CircuitTreeDeviceRowDto[];
protectionDevice?: CircuitTreeProtectionDeviceDto;
}
export interface CircuitTreeSectionDto {
@@ -297,7 +330,10 @@ export interface CircuitTreeSectionDto {
displayName: string;
prefix: string;
sortOrder: number;
category?: CircuitGroupCategory;
groupNumber?: number;
sectionTotalPower: number;
components: CircuitTreeComponentDto[];
circuits: CircuitTreeCircuitDto[];
}
@@ -320,7 +356,9 @@ export interface CircuitTreeResponseDto {
distributionBoardSimultaneityFactor: number;
distributionBoardTotalPower: number;
distributionBoardTotalPowerWithSimultaneityFactor: number;
headerComponents: CircuitTreeComponentDto[];
sections: CircuitTreeSectionDto[];
footerComponents: CircuitTreeComponentDto[];
migrationReport?: CircuitTreeMigrationReportDto;
}
@@ -3,6 +3,8 @@ import { CircuitDeviceRowRepository } from "../../db/repositories/circuit-device
import { CircuitListRepository } from "../../db/repositories/circuit-list.repository.js";
import { CircuitSectionRepository } from "../../db/repositories/circuit-section.repository.js";
import { CircuitRepository } from "../../db/repositories/circuit.repository.js";
import { CircuitProtectionDeviceRepository } from "../../db/repositories/circuit-protection-device.repository.js";
import { DistributionBoardComponentRepository } from "../../db/repositories/distribution-board-component.repository.js";
import { DistributionBoardRepository } from "../../db/repositories/distribution-board.repository.js";
import { FloorRepository } from "../../db/repositories/floor.repository.js";
import { GlobalDeviceRepository } from "../../db/repositories/global-device.repository.js";
@@ -15,6 +17,10 @@ export const circuitDeviceRowRepository =
export const circuitListRepository = new CircuitListRepository(db);
export const circuitSectionRepository = new CircuitSectionRepository(db);
export const circuitRepository = new CircuitRepository(db);
export const circuitProtectionDeviceRepository =
new CircuitProtectionDeviceRepository(db);
export const distributionBoardComponentRepository =
new DistributionBoardComponentRepository(db);
export const distributionBoardRepository =
new DistributionBoardRepository(db);
export const floorRepository = new FloorRepository(db);
@@ -7,11 +7,14 @@ import {
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";
@@ -23,7 +26,9 @@ export function isMissingCircuitTreeSchemaError(error: unknown): boolean {
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: circuit_device_rows") ||
error.message.includes("no such table: distribution_board_components") ||
error.message.includes("no such table: circuit_protection_devices")
);
}
@@ -56,9 +61,28 @@ export async function getCircuitTree(req: Request, res: Response) {
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<string, typeof rows>();
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, []);
@@ -75,17 +99,46 @@ export async function getCircuitTree(req: Request, res: Response) {
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);
@@ -116,6 +169,7 @@ export async function getCircuitTree(req: Request, res: Response) {
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,
@@ -139,6 +193,9 @@ export async function getCircuitTree(req: Request, res: Response) {
remark: circuit.remark ?? undefined,
circuitTotalPower,
deviceRows,
protectionDevice: protection
? toProtectionDeviceDto(protection)
: undefined,
});
}
@@ -167,7 +224,9 @@ export async function getCircuitTree(req: Request, res: Response) {
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).",
});
@@ -175,3 +234,23 @@ export async function getCircuitTree(req: Request, res: Response) {
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,
};
}