Expose protected tree structure
This commit is contained in:
@@ -332,6 +332,10 @@ snapshot contains the group, optional component protections, complete circuits,
|
|||||||
circuit protections and all device-row link/override metadata. Delete
|
circuit protections and all device-row link/override metadata. Delete
|
||||||
re-captures and compares the subtree before cascading; restore preserves every
|
re-captures and compares the subtree before cascading; restore preserves every
|
||||||
UUID in foreign-key-safe order.
|
UUID in foreign-key-safe order.
|
||||||
|
The circuit-tree read model exposes fixed `headerComponents`, group-owned
|
||||||
|
`components`, `footerComponents`, optional group category/number and optional
|
||||||
|
one-to-one protection DTOs for circuits and group components. These are
|
||||||
|
additive to the retained flat circuit protection fields during the transition.
|
||||||
Distribution-board floor assignment and a project-enabled supply type use
|
Distribution-board floor assignment and a project-enabled supply type use
|
||||||
`distribution-board.update`; both values are snapshot/export fields and one
|
`distribution-board.update`; both values are snapshot/export fields and one
|
||||||
persistent undo step.
|
persistent undo step.
|
||||||
|
|||||||
@@ -230,7 +230,11 @@ returns HTTP `409` with `PROJECT_HISTORY_OPERATION_UNAVAILABLE`.
|
|||||||
### Tree Endpoint
|
### Tree Endpoint
|
||||||
|
|
||||||
- `GET /projects/:projectId/circuit-lists/:circuitListId/tree`
|
- `GET /projects/:projectId/circuit-lists/:circuitListId/tree`
|
||||||
- Purpose: returns section/circuit/device-row tree with calculated row and circuit totals.
|
- Purpose: returns fixed header components, grouped sections with optional
|
||||||
|
protection components, complete circuit/device-row blocks and auxiliary
|
||||||
|
footer components. Circuit and component protection devices use their
|
||||||
|
separate one-to-one DTOs. Calculated row, circuit, section and board totals
|
||||||
|
remain included.
|
||||||
|
|
||||||
Response sketch:
|
Response sketch:
|
||||||
|
|
||||||
@@ -238,16 +242,47 @@ Response sketch:
|
|||||||
{
|
{
|
||||||
"circuitListId": "cl_1",
|
"circuitListId": "cl_1",
|
||||||
"currentRevision": 12,
|
"currentRevision": 12,
|
||||||
|
"headerComponents": [
|
||||||
|
{
|
||||||
|
"id": "cmp_main",
|
||||||
|
"equipmentIdentifier": "-Q0",
|
||||||
|
"name": "Hauptschalter",
|
||||||
|
"role": "main_switch",
|
||||||
|
"placement": "header"
|
||||||
|
}
|
||||||
|
],
|
||||||
"sections": [
|
"sections": [
|
||||||
{
|
{
|
||||||
"id": "sec_1",
|
"id": "sec_1",
|
||||||
"key": "lighting",
|
"key": "lighting",
|
||||||
"prefix": "-1F",
|
"prefix": "-1F1.",
|
||||||
|
"category": "lighting",
|
||||||
|
"groupNumber": 1,
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"id": "cmp_rcd",
|
||||||
|
"equipmentIdentifier": "-1Q1.0",
|
||||||
|
"name": "Gruppen-FI",
|
||||||
|
"role": "group_residual_current_protection",
|
||||||
|
"placement": "group",
|
||||||
|
"protectionDevice": {
|
||||||
|
"type": "FI",
|
||||||
|
"ratedCurrentA": 40,
|
||||||
|
"rcdType": "A",
|
||||||
|
"ratedResidualCurrentMa": 30
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
"circuits": [
|
"circuits": [
|
||||||
{
|
{
|
||||||
"id": "cir_1",
|
"id": "cir_1",
|
||||||
"equipmentIdentifier": "-1F1",
|
"equipmentIdentifier": "-1F1.1",
|
||||||
"circuitTotalPower": 4.2,
|
"circuitTotalPower": 4.2,
|
||||||
|
"protectionDevice": {
|
||||||
|
"type": "LS",
|
||||||
|
"ratedCurrentA": 10,
|
||||||
|
"tripCharacteristic": "B"
|
||||||
|
},
|
||||||
"deviceRows": [
|
"deviceRows": [
|
||||||
{
|
{
|
||||||
"id": "row_1",
|
"id": "row_1",
|
||||||
@@ -258,6 +293,15 @@ Response sketch:
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"footerComponents": [
|
||||||
|
{
|
||||||
|
"id": "cmp_actor",
|
||||||
|
"equipmentIdentifier": "-K1",
|
||||||
|
"name": "KNX Schaltaktor",
|
||||||
|
"role": "auxiliary",
|
||||||
|
"placement": "footer"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -343,6 +343,13 @@ Snapshot. `circuit-group.restore-subtree` schreibt ihn in FK-sicherer Reihenfolg
|
|||||||
mit denselben UUIDs zurück. Beide Richtungen laufen als vollständige inverse
|
mit denselben UUIDs zurück. Beide Richtungen laufen als vollständige inverse
|
||||||
Projekt-Commands; späte Fehler hinterlassen weder Teillöschungen noch
|
Projekt-Commands; späte Fehler hinterlassen weder Teillöschungen noch
|
||||||
Teilwiederherstellungen.
|
Teilwiederherstellungen.
|
||||||
|
Der Circuit-Tree liefert zusätzlich `headerComponents`,
|
||||||
|
gruppenbezogene `components` und `footerComponents`. Abschnitte enthalten
|
||||||
|
optionale Gruppenkategorie und -nummer; Stromkreise und Gruppenkomponenten
|
||||||
|
enthalten ihre optionale getrennte Schutzgerätekonfiguration. Die bisherigen
|
||||||
|
flachen Stromkreis-Schutzfelder bleiben während der Übergangsphase additiv
|
||||||
|
erhalten. Separate aktive Read-Repositories lesen Komponenten- und beide
|
||||||
|
Schutzgerätetabellen; Schreibzugriffe bleiben ausschließlich in Commands.
|
||||||
`distribution-board.update` versioniert Etage, Netzart und den
|
`distribution-board.update` versioniert Etage, Netzart und den
|
||||||
verteilerweiten Gleichzeitigkeitsfaktor gemeinsam und stellt alle Werte über
|
verteilerweiten Gleichzeitigkeitsfaktor gemeinsam und stellt alle Werte über
|
||||||
dauerhaftes Undo/Redo wieder her. Der Faktor liegt zwischen `0` und `1` und
|
dauerhaftes Undo/Redo wieder her. Der Faktor liegt zwischen `0` und `1` und
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ requirements and intended sequencing, not proof of implementation.
|
|||||||
- [x] Phase D2b: persistent same-category circuit-move command.
|
- [x] Phase D2b: persistent same-category circuit-move command.
|
||||||
- [x] Phase D3a: complete validated group-subtree snapshot and warning summary.
|
- [x] Phase D3a: complete validated group-subtree snapshot and warning summary.
|
||||||
- [x] Phase D3b: persistent populated-group delete/restore command.
|
- [x] Phase D3b: persistent populated-group delete/restore command.
|
||||||
|
- [x] Phase E1: tree read model for components, groups and protection devices.
|
||||||
|
- [ ] Phase E2: header/group/footer editor projection.
|
||||||
- [ ] Phase E: editor projection and editing.
|
- [ ] Phase E: editor projection and editing.
|
||||||
- [ ] Phase F: documentation and full GUI verification.
|
- [ ] Phase F: documentation and full GUI verification.
|
||||||
- [ ] Keep full electrical sizing and cable-dimensioning rules separate until
|
- [ ] Keep full electrical sizing and cable-dimensioning rules separate until
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,6 +44,7 @@ export interface CircuitTreeCircuit {
|
|||||||
remark?: string;
|
remark?: string;
|
||||||
circuitTotalPower: number;
|
circuitTotalPower: number;
|
||||||
deviceRows: CircuitTreeDeviceRow[];
|
deviceRows: CircuitTreeDeviceRow[];
|
||||||
|
protectionDevice?: CircuitTreeProtectionDevice;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CircuitTreeSectionBlock {
|
export interface CircuitTreeSectionBlock {
|
||||||
@@ -52,7 +53,10 @@ export interface CircuitTreeSectionBlock {
|
|||||||
displayName: string;
|
displayName: string;
|
||||||
prefix: string;
|
prefix: string;
|
||||||
sortOrder: number;
|
sortOrder: number;
|
||||||
|
category?: CircuitGroupCategory;
|
||||||
|
groupNumber?: number;
|
||||||
sectionTotalPower: number;
|
sectionTotalPower: number;
|
||||||
|
components: CircuitTreeComponent[];
|
||||||
circuits: CircuitTreeCircuit[];
|
circuits: CircuitTreeCircuit[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +68,9 @@ export interface CircuitTreeResponse {
|
|||||||
distributionBoardSimultaneityFactor: number;
|
distributionBoardSimultaneityFactor: number;
|
||||||
distributionBoardTotalPower: number;
|
distributionBoardTotalPower: number;
|
||||||
distributionBoardTotalPowerWithSimultaneityFactor: number;
|
distributionBoardTotalPowerWithSimultaneityFactor: number;
|
||||||
|
headerComponents: CircuitTreeComponent[];
|
||||||
sections: CircuitTreeSectionBlock[];
|
sections: CircuitTreeSectionBlock[];
|
||||||
|
footerComponents: CircuitTreeComponent[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LegacyMigrationReport {
|
export interface LegacyMigrationReport {
|
||||||
@@ -78,3 +84,35 @@ export interface LegacyMigrationReport {
|
|||||||
warnings: string[];
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,15 @@
|
|||||||
import type { DistributionBoardSupplyType } from "../shared/constants/distribution-board";
|
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 {
|
export interface ProjectDto {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -267,6 +278,27 @@ export interface CircuitTreeDeviceRowDto {
|
|||||||
rowTotalPower: number;
|
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 {
|
export interface CircuitTreeCircuitDto {
|
||||||
id: string;
|
id: string;
|
||||||
circuitListId: string;
|
circuitListId: string;
|
||||||
@@ -289,6 +321,7 @@ export interface CircuitTreeCircuitDto {
|
|||||||
remark?: string;
|
remark?: string;
|
||||||
circuitTotalPower: number;
|
circuitTotalPower: number;
|
||||||
deviceRows: CircuitTreeDeviceRowDto[];
|
deviceRows: CircuitTreeDeviceRowDto[];
|
||||||
|
protectionDevice?: CircuitTreeProtectionDeviceDto;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CircuitTreeSectionDto {
|
export interface CircuitTreeSectionDto {
|
||||||
@@ -297,7 +330,10 @@ export interface CircuitTreeSectionDto {
|
|||||||
displayName: string;
|
displayName: string;
|
||||||
prefix: string;
|
prefix: string;
|
||||||
sortOrder: number;
|
sortOrder: number;
|
||||||
|
category?: CircuitGroupCategory;
|
||||||
|
groupNumber?: number;
|
||||||
sectionTotalPower: number;
|
sectionTotalPower: number;
|
||||||
|
components: CircuitTreeComponentDto[];
|
||||||
circuits: CircuitTreeCircuitDto[];
|
circuits: CircuitTreeCircuitDto[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,7 +356,9 @@ export interface CircuitTreeResponseDto {
|
|||||||
distributionBoardSimultaneityFactor: number;
|
distributionBoardSimultaneityFactor: number;
|
||||||
distributionBoardTotalPower: number;
|
distributionBoardTotalPower: number;
|
||||||
distributionBoardTotalPowerWithSimultaneityFactor: number;
|
distributionBoardTotalPowerWithSimultaneityFactor: number;
|
||||||
|
headerComponents: CircuitTreeComponentDto[];
|
||||||
sections: CircuitTreeSectionDto[];
|
sections: CircuitTreeSectionDto[];
|
||||||
|
footerComponents: CircuitTreeComponentDto[];
|
||||||
migrationReport?: CircuitTreeMigrationReportDto;
|
migrationReport?: CircuitTreeMigrationReportDto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { CircuitDeviceRowRepository } from "../../db/repositories/circuit-device
|
|||||||
import { CircuitListRepository } from "../../db/repositories/circuit-list.repository.js";
|
import { CircuitListRepository } from "../../db/repositories/circuit-list.repository.js";
|
||||||
import { CircuitSectionRepository } from "../../db/repositories/circuit-section.repository.js";
|
import { CircuitSectionRepository } from "../../db/repositories/circuit-section.repository.js";
|
||||||
import { CircuitRepository } from "../../db/repositories/circuit.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 { DistributionBoardRepository } from "../../db/repositories/distribution-board.repository.js";
|
||||||
import { FloorRepository } from "../../db/repositories/floor.repository.js";
|
import { FloorRepository } from "../../db/repositories/floor.repository.js";
|
||||||
import { GlobalDeviceRepository } from "../../db/repositories/global-device.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 circuitListRepository = new CircuitListRepository(db);
|
||||||
export const circuitSectionRepository = new CircuitSectionRepository(db);
|
export const circuitSectionRepository = new CircuitSectionRepository(db);
|
||||||
export const circuitRepository = new CircuitRepository(db);
|
export const circuitRepository = new CircuitRepository(db);
|
||||||
|
export const circuitProtectionDeviceRepository =
|
||||||
|
new CircuitProtectionDeviceRepository(db);
|
||||||
|
export const distributionBoardComponentRepository =
|
||||||
|
new DistributionBoardComponentRepository(db);
|
||||||
export const distributionBoardRepository =
|
export const distributionBoardRepository =
|
||||||
new DistributionBoardRepository(db);
|
new DistributionBoardRepository(db);
|
||||||
export const floorRepository = new FloorRepository(db);
|
export const floorRepository = new FloorRepository(db);
|
||||||
|
|||||||
@@ -7,11 +7,14 @@ import {
|
|||||||
calculateSectionTotalPower,
|
calculateSectionTotalPower,
|
||||||
} from "../../domain/calculations/circuit-power-calculation.js";
|
} from "../../domain/calculations/circuit-power-calculation.js";
|
||||||
import type { CircuitTreeResponse } from "../../domain/models/circuit-tree.model.js";
|
import type { CircuitTreeResponse } from "../../domain/models/circuit-tree.model.js";
|
||||||
|
import type { CircuitTreeProtectionDevice } from "../../domain/models/circuit-tree.model.js";
|
||||||
import {
|
import {
|
||||||
circuitDeviceRowRepository,
|
circuitDeviceRowRepository,
|
||||||
circuitListRepository,
|
circuitListRepository,
|
||||||
|
circuitProtectionDeviceRepository,
|
||||||
circuitRepository,
|
circuitRepository,
|
||||||
circuitSectionRepository,
|
circuitSectionRepository,
|
||||||
|
distributionBoardComponentRepository,
|
||||||
distributionBoardRepository,
|
distributionBoardRepository,
|
||||||
projectRepository,
|
projectRepository,
|
||||||
} from "../composition/application-repositories.js";
|
} from "../composition/application-repositories.js";
|
||||||
@@ -23,7 +26,9 @@ export function isMissingCircuitTreeSchemaError(error: unknown): boolean {
|
|||||||
return (
|
return (
|
||||||
error.message.includes("no such table: circuit_sections") ||
|
error.message.includes("no such table: circuit_sections") ||
|
||||||
error.message.includes("no such table: circuits") ||
|
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 sections = await circuitSectionRepository.listByCircuitList(circuitListId);
|
||||||
const circuits = await circuitRepository.listByCircuitList(circuitListId);
|
const circuits = await circuitRepository.listByCircuitList(circuitListId);
|
||||||
const rows = await circuitDeviceRowRepository.listByCircuitList(circuits.map((entry) => entry.id));
|
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 sectionById = new Map(sections.map((section) => [section.id, section]));
|
||||||
const rowsByCircuitId = new Map<string, typeof rows>();
|
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) {
|
for (const row of rows) {
|
||||||
if (!rowsByCircuitId.has(row.circuitId)) {
|
if (!rowsByCircuitId.has(row.circuitId)) {
|
||||||
rowsByCircuitId.set(row.circuitId, []);
|
rowsByCircuitId.set(row.circuitId, []);
|
||||||
@@ -75,17 +99,46 @@ export async function getCircuitTree(req: Request, res: Response) {
|
|||||||
distributionBoard.simultaneityFactor,
|
distributionBoard.simultaneityFactor,
|
||||||
distributionBoardTotalPower: 0,
|
distributionBoardTotalPower: 0,
|
||||||
distributionBoardTotalPowerWithSimultaneityFactor: 0,
|
distributionBoardTotalPowerWithSimultaneityFactor: 0,
|
||||||
|
headerComponents: [],
|
||||||
sections: sections.map((section) => ({
|
sections: sections.map((section) => ({
|
||||||
id: section.id,
|
id: section.id,
|
||||||
key: section.key,
|
key: section.key,
|
||||||
displayName: section.displayName,
|
displayName: section.displayName,
|
||||||
prefix: section.prefix,
|
prefix: section.prefix,
|
||||||
sortOrder: section.sortOrder,
|
sortOrder: section.sortOrder,
|
||||||
|
category: section.category ?? undefined,
|
||||||
|
groupNumber: section.groupNumber ?? undefined,
|
||||||
sectionTotalPower: 0,
|
sectionTotalPower: 0,
|
||||||
|
components: [],
|
||||||
circuits: [],
|
circuits: [],
|
||||||
})),
|
})),
|
||||||
|
footerComponents: [],
|
||||||
};
|
};
|
||||||
const sectionBlocks = new Map(tree.sections.map((section) => [section.id, section]));
|
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) {
|
for (const circuit of circuits) {
|
||||||
const section = sectionById.get(circuit.sectionId);
|
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),
|
rowTotalPower: calculateRowTotalPower(row.quantity, row.powerPerUnit, row.simultaneityFactor),
|
||||||
}));
|
}));
|
||||||
const circuitTotalPower = calculateCircuitTotalPower(deviceRows);
|
const circuitTotalPower = calculateCircuitTotalPower(deviceRows);
|
||||||
|
const protection = circuitProtectionById.get(circuit.id);
|
||||||
|
|
||||||
sectionBlocks.get(section.id)?.circuits.push({
|
sectionBlocks.get(section.id)?.circuits.push({
|
||||||
id: circuit.id,
|
id: circuit.id,
|
||||||
@@ -139,6 +193,9 @@ export async function getCircuitTree(req: Request, res: Response) {
|
|||||||
remark: circuit.remark ?? undefined,
|
remark: circuit.remark ?? undefined,
|
||||||
circuitTotalPower,
|
circuitTotalPower,
|
||||||
deviceRows,
|
deviceRows,
|
||||||
|
protectionDevice: protection
|
||||||
|
? toProtectionDeviceDto(protection)
|
||||||
|
: undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,7 +224,9 @@ export async function getCircuitTree(req: Request, res: Response) {
|
|||||||
distributionBoard.simultaneityFactor,
|
distributionBoard.simultaneityFactor,
|
||||||
distributionBoardTotalPower: 0,
|
distributionBoardTotalPower: 0,
|
||||||
distributionBoardTotalPowerWithSimultaneityFactor: 0,
|
distributionBoardTotalPowerWithSimultaneityFactor: 0,
|
||||||
|
headerComponents: [],
|
||||||
sections: [],
|
sections: [],
|
||||||
|
footerComponents: [],
|
||||||
warning:
|
warning:
|
||||||
"Circuit-first tables are not available yet. Run database migrations (including 0008_circuit_first_model).",
|
"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;
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,19 @@
|
|||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import { describe, it } from "node:test";
|
import { describe, it } from "node:test";
|
||||||
import { isMissingCircuitTreeSchemaError } from "../src/server/controllers/circuit-tree.controller.js";
|
import {
|
||||||
|
getCircuitTree,
|
||||||
|
isMissingCircuitTreeSchemaError,
|
||||||
|
} from "../src/server/controllers/circuit-tree.controller.js";
|
||||||
|
import {
|
||||||
|
circuitDeviceRowRepository,
|
||||||
|
circuitListRepository,
|
||||||
|
circuitProtectionDeviceRepository,
|
||||||
|
circuitRepository,
|
||||||
|
circuitSectionRepository,
|
||||||
|
distributionBoardComponentRepository,
|
||||||
|
distributionBoardRepository,
|
||||||
|
projectRepository,
|
||||||
|
} from "../src/server/composition/application-repositories.js";
|
||||||
|
|
||||||
describe("circuit tree controller", () => {
|
describe("circuit tree controller", () => {
|
||||||
it("detects missing circuit-first schema errors", () => {
|
it("detects missing circuit-first schema errors", () => {
|
||||||
@@ -13,7 +26,192 @@ describe("circuit tree controller", () => {
|
|||||||
isMissingCircuitTreeSchemaError(new Error("SqliteError: no such table: circuit_device_rows")),
|
isMissingCircuitTreeSchemaError(new Error("SqliteError: no such table: circuit_device_rows")),
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
assert.equal(
|
||||||
|
isMissingCircuitTreeSchemaError(
|
||||||
|
new Error("SqliteError: no such table: distribution_board_components")
|
||||||
|
),
|
||||||
|
true
|
||||||
|
);
|
||||||
assert.equal(isMissingCircuitTreeSchemaError(new Error("Some other error")), false);
|
assert.equal(isMissingCircuitTreeSchemaError(new Error("Some other error")), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("returns header, grouped and footer components with both protection layers", async () => {
|
||||||
|
const originals = {
|
||||||
|
findList: circuitListRepository.findById,
|
||||||
|
findProject: projectRepository.findById,
|
||||||
|
findBoard: distributionBoardRepository.findById,
|
||||||
|
listSections: circuitSectionRepository.listByCircuitList,
|
||||||
|
listCircuits: circuitRepository.listByCircuitList,
|
||||||
|
listRows: circuitDeviceRowRepository.listByCircuitList,
|
||||||
|
listComponents:
|
||||||
|
distributionBoardComponentRepository.listByCircuitList,
|
||||||
|
listComponentProtection:
|
||||||
|
distributionBoardComponentRepository.listProtectionByComponentIds,
|
||||||
|
listCircuitProtection:
|
||||||
|
circuitProtectionDeviceRepository.listByCircuitIds,
|
||||||
|
};
|
||||||
|
circuitListRepository.findById = async () =>
|
||||||
|
({
|
||||||
|
id: "list-1",
|
||||||
|
projectId: "project-1",
|
||||||
|
distributionBoardId: "board-1",
|
||||||
|
name: "UV-01",
|
||||||
|
}) as never;
|
||||||
|
projectRepository.findById = async () =>
|
||||||
|
({
|
||||||
|
id: "project-1",
|
||||||
|
name: "Projekt",
|
||||||
|
currentRevision: 4,
|
||||||
|
singlePhaseVoltageV: 230,
|
||||||
|
threePhaseVoltageV: 400,
|
||||||
|
}) as never;
|
||||||
|
distributionBoardRepository.findById = async () =>
|
||||||
|
({ id: "board-1", simultaneityFactor: 0.8 }) as never;
|
||||||
|
circuitSectionRepository.listByCircuitList = async () =>
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: "group-1",
|
||||||
|
circuitListId: "list-1",
|
||||||
|
key: "lighting",
|
||||||
|
displayName: "Beleuchtung 1",
|
||||||
|
prefix: "-1F1.",
|
||||||
|
sortOrder: 10,
|
||||||
|
category: "lighting",
|
||||||
|
groupNumber: 1,
|
||||||
|
},
|
||||||
|
] as never;
|
||||||
|
circuitRepository.listByCircuitList = async () =>
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: "circuit-1",
|
||||||
|
circuitListId: "list-1",
|
||||||
|
sectionId: "group-1",
|
||||||
|
equipmentIdentifier: "-1F1.1",
|
||||||
|
displayName: "Licht",
|
||||||
|
sortOrder: 10,
|
||||||
|
isReserve: 0,
|
||||||
|
},
|
||||||
|
] as never;
|
||||||
|
circuitDeviceRowRepository.listByCircuitList = async () => [];
|
||||||
|
distributionBoardComponentRepository.listByCircuitList =
|
||||||
|
async () =>
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: "main",
|
||||||
|
circuitListId: "list-1",
|
||||||
|
sectionId: null,
|
||||||
|
equipmentIdentifier: "-Q0",
|
||||||
|
name: "Hauptschalter",
|
||||||
|
role: "main_switch",
|
||||||
|
placement: "header",
|
||||||
|
sortOrder: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "rcd",
|
||||||
|
circuitListId: "list-1",
|
||||||
|
sectionId: "group-1",
|
||||||
|
equipmentIdentifier: "-1Q1.0",
|
||||||
|
name: "Gruppen-FI",
|
||||||
|
role: "group_residual_current_protection",
|
||||||
|
placement: "group",
|
||||||
|
sortOrder: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "actor",
|
||||||
|
circuitListId: "list-1",
|
||||||
|
sectionId: null,
|
||||||
|
equipmentIdentifier: "-K1",
|
||||||
|
name: "KNX Aktor",
|
||||||
|
role: "auxiliary",
|
||||||
|
placement: "footer",
|
||||||
|
sortOrder: 10,
|
||||||
|
},
|
||||||
|
] as never;
|
||||||
|
distributionBoardComponentRepository.listProtectionByComponentIds =
|
||||||
|
async () =>
|
||||||
|
[
|
||||||
|
{
|
||||||
|
componentId: "rcd",
|
||||||
|
type: "FI",
|
||||||
|
ratedCurrentA: 40,
|
||||||
|
fuseUtilizationCategory: null,
|
||||||
|
tripCharacteristic: null,
|
||||||
|
rcdType: "A",
|
||||||
|
ratedResidualCurrentMa: 30,
|
||||||
|
},
|
||||||
|
] as never;
|
||||||
|
circuitProtectionDeviceRepository.listByCircuitIds = async () =>
|
||||||
|
[
|
||||||
|
{
|
||||||
|
circuitId: "circuit-1",
|
||||||
|
type: "LS",
|
||||||
|
ratedCurrentA: 10,
|
||||||
|
fuseUtilizationCategory: null,
|
||||||
|
tripCharacteristic: "B",
|
||||||
|
rcdType: null,
|
||||||
|
ratedResidualCurrentMa: null,
|
||||||
|
},
|
||||||
|
] as never;
|
||||||
|
|
||||||
|
let responseBody: unknown;
|
||||||
|
const response = {
|
||||||
|
status() {
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
json(value: unknown) {
|
||||||
|
responseBody = value;
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await getCircuitTree(
|
||||||
|
{ params: { projectId: "project-1", circuitListId: "list-1" } } as never,
|
||||||
|
response as never
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
circuitListRepository.findById = originals.findList;
|
||||||
|
projectRepository.findById = originals.findProject;
|
||||||
|
distributionBoardRepository.findById = originals.findBoard;
|
||||||
|
circuitSectionRepository.listByCircuitList =
|
||||||
|
originals.listSections;
|
||||||
|
circuitRepository.listByCircuitList = originals.listCircuits;
|
||||||
|
circuitDeviceRowRepository.listByCircuitList = originals.listRows;
|
||||||
|
distributionBoardComponentRepository.listByCircuitList =
|
||||||
|
originals.listComponents;
|
||||||
|
distributionBoardComponentRepository.listProtectionByComponentIds =
|
||||||
|
originals.listComponentProtection;
|
||||||
|
circuitProtectionDeviceRepository.listByCircuitIds =
|
||||||
|
originals.listCircuitProtection;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tree = responseBody as {
|
||||||
|
headerComponents: Array<{ equipmentIdentifier: string }>;
|
||||||
|
footerComponents: Array<{ equipmentIdentifier: string }>;
|
||||||
|
sections: Array<{
|
||||||
|
category: string;
|
||||||
|
groupNumber: number;
|
||||||
|
components: Array<{
|
||||||
|
protectionDevice: { ratedResidualCurrentMa: number };
|
||||||
|
}>;
|
||||||
|
circuits: Array<{
|
||||||
|
protectionDevice: { tripCharacteristic: string };
|
||||||
|
}>;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
assert.equal(tree.headerComponents[0].equipmentIdentifier, "-Q0");
|
||||||
|
assert.equal(tree.footerComponents[0].equipmentIdentifier, "-K1");
|
||||||
|
assert.equal(tree.sections[0].category, "lighting");
|
||||||
|
assert.equal(tree.sections[0].groupNumber, 1);
|
||||||
|
assert.equal(
|
||||||
|
tree.sections[0].components[0].protectionDevice
|
||||||
|
.ratedResidualCurrentMa,
|
||||||
|
30
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
tree.sections[0].circuits[0].protectionDevice
|
||||||
|
.tripCharacteristic,
|
||||||
|
"B"
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user