Added 1B, 2 and added bootstrap again for site
This commit is contained in:
@@ -10,3 +10,32 @@ body {
|
||||
.table td select.form-select-sm {
|
||||
min-width: 8rem;
|
||||
}
|
||||
|
||||
.circuit-tree-table .section-row td {
|
||||
background: #e9eef8;
|
||||
border-top: 2px solid #c6d3ea;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.circuit-tree-table .summary-row td {
|
||||
background: #f5f8fd;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.circuit-tree-table .device-row td {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.circuit-tree-table .reserve-row td {
|
||||
background: #fff8e8;
|
||||
}
|
||||
|
||||
.circuit-tree-table .placeholder-row td {
|
||||
background: #f7f7f7;
|
||||
color: #6c757d;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.circuit-tree-table .indented-cell {
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
import { CircuitTreePreview } from "../../../../../../frontend/components/circuit-tree-preview";
|
||||
|
||||
export default function CircuitTreePreviewPage() {
|
||||
const params = useParams<{ projectId: string; circuitListId: string }>();
|
||||
const projectId = params.projectId;
|
||||
const circuitListId = params.circuitListId;
|
||||
|
||||
return (
|
||||
<main className="container py-4">
|
||||
<div className="d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
<h1 className="h4 mb-1">Circuit Tree Preview</h1>
|
||||
<p className="text-secondary mb-0">
|
||||
Read-only preview of section blocks, circuits and device rows.
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href={`/projects/${projectId}/circuit-lists`}
|
||||
className="btn btn-outline-secondary btn-sm"
|
||||
>
|
||||
Back to legacy editor
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<CircuitTreePreview projectId={projectId} circuitListId={circuitListId} />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -57,6 +57,13 @@
|
||||
"when": 1777680000000,
|
||||
"tag": "0007_consumer_device_link",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 8,
|
||||
"version": "6",
|
||||
"when": 1777800000000,
|
||||
"tag": "0008_circuit_first_model",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
import crypto from "node:crypto";
|
||||
import { asc, inArray } from "drizzle-orm";
|
||||
import { asc, eq, inArray } from "drizzle-orm";
|
||||
import { db } from "../client.js";
|
||||
import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
|
||||
|
||||
export class CircuitDeviceRowRepository {
|
||||
async findById(rowId: string) {
|
||||
const [row] = await db.select().from(circuitDeviceRows).where(eq(circuitDeviceRows.id, rowId)).limit(1);
|
||||
return row ?? null;
|
||||
}
|
||||
|
||||
async countByCircuit(circuitId: string) {
|
||||
const rows = await db.select({ id: circuitDeviceRows.id }).from(circuitDeviceRows).where(eq(circuitDeviceRows.circuitId, circuitId));
|
||||
return rows.length;
|
||||
}
|
||||
|
||||
async listByCircuitList(circuitIds: string[]) {
|
||||
if (!circuitIds.length) {
|
||||
return [];
|
||||
@@ -26,6 +36,7 @@ export class CircuitDeviceRowRepository {
|
||||
connectionKind?: string;
|
||||
costGroup?: string;
|
||||
category?: string;
|
||||
level?: string;
|
||||
roomId?: string;
|
||||
roomNumberSnapshot?: string;
|
||||
roomNameSnapshot?: string;
|
||||
@@ -34,9 +45,11 @@ export class CircuitDeviceRowRepository {
|
||||
simultaneityFactor: number;
|
||||
cosPhi?: number;
|
||||
remark?: string;
|
||||
overriddenFields?: string;
|
||||
}) {
|
||||
const id = crypto.randomUUID();
|
||||
await db.insert(circuitDeviceRows).values({
|
||||
id: crypto.randomUUID(),
|
||||
id,
|
||||
circuitId: input.circuitId,
|
||||
linkedProjectDeviceId: input.linkedProjectDeviceId ?? null,
|
||||
legacyConsumerId: input.legacyConsumerId ?? null,
|
||||
@@ -47,6 +60,7 @@ export class CircuitDeviceRowRepository {
|
||||
connectionKind: input.connectionKind ?? null,
|
||||
costGroup: input.costGroup ?? null,
|
||||
category: input.category ?? null,
|
||||
level: input.level ?? null,
|
||||
roomId: input.roomId ?? null,
|
||||
roomNumberSnapshot: input.roomNumberSnapshot ?? null,
|
||||
roomNameSnapshot: input.roomNameSnapshot ?? null,
|
||||
@@ -55,7 +69,58 @@ export class CircuitDeviceRowRepository {
|
||||
simultaneityFactor: input.simultaneityFactor,
|
||||
cosPhi: input.cosPhi ?? null,
|
||||
remark: input.remark ?? null,
|
||||
overriddenFields: null,
|
||||
overriddenFields: input.overriddenFields ?? null,
|
||||
});
|
||||
return id;
|
||||
}
|
||||
|
||||
async update(
|
||||
rowId: string,
|
||||
input: {
|
||||
linkedProjectDeviceId?: string;
|
||||
name: string;
|
||||
displayName: string;
|
||||
phaseType?: string;
|
||||
connectionKind?: string;
|
||||
costGroup?: string;
|
||||
category?: string;
|
||||
level?: string;
|
||||
roomId?: string;
|
||||
roomNumberSnapshot?: string;
|
||||
roomNameSnapshot?: string;
|
||||
quantity: number;
|
||||
powerPerUnit: number;
|
||||
simultaneityFactor: number;
|
||||
cosPhi?: number;
|
||||
remark?: string;
|
||||
overriddenFields?: string;
|
||||
}
|
||||
) {
|
||||
await db
|
||||
.update(circuitDeviceRows)
|
||||
.set({
|
||||
linkedProjectDeviceId: input.linkedProjectDeviceId ?? null,
|
||||
name: input.name,
|
||||
displayName: input.displayName,
|
||||
phaseType: input.phaseType ?? null,
|
||||
connectionKind: input.connectionKind ?? null,
|
||||
costGroup: input.costGroup ?? null,
|
||||
category: input.category ?? null,
|
||||
level: input.level ?? null,
|
||||
roomId: input.roomId ?? null,
|
||||
roomNumberSnapshot: input.roomNumberSnapshot ?? null,
|
||||
roomNameSnapshot: input.roomNameSnapshot ?? null,
|
||||
quantity: input.quantity,
|
||||
powerPerUnit: input.powerPerUnit,
|
||||
simultaneityFactor: input.simultaneityFactor,
|
||||
cosPhi: input.cosPhi ?? null,
|
||||
remark: input.remark ?? null,
|
||||
overriddenFields: input.overriddenFields ?? null,
|
||||
})
|
||||
.where(eq(circuitDeviceRows.id, rowId));
|
||||
}
|
||||
|
||||
async delete(rowId: string) {
|
||||
await db.delete(circuitDeviceRows).where(eq(circuitDeviceRows.id, rowId));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,4 +53,13 @@ export class CircuitListRepository {
|
||||
.limit(1);
|
||||
return row ?? null;
|
||||
}
|
||||
|
||||
async findByIdByListIdOnly(circuitListId: string) {
|
||||
const [row] = await db
|
||||
.select()
|
||||
.from(circuitLists)
|
||||
.where(eq(circuitLists.id, circuitListId))
|
||||
.limit(1);
|
||||
return row ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,11 @@ import { db } from "../client.js";
|
||||
import { circuitSections } from "../schema/circuit-sections.js";
|
||||
|
||||
export class CircuitSectionRepository {
|
||||
async findById(sectionId: string) {
|
||||
const [row] = await db.select().from(circuitSections).where(eq(circuitSections.id, sectionId)).limit(1);
|
||||
return row ?? null;
|
||||
}
|
||||
|
||||
async listByCircuitList(circuitListId: string) {
|
||||
return db
|
||||
.select()
|
||||
@@ -39,4 +44,3 @@ export class CircuitSectionRepository {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import crypto from "node:crypto";
|
||||
import { asc, eq } from "drizzle-orm";
|
||||
import { and, asc, eq, ne } from "drizzle-orm";
|
||||
import { db } from "../client.js";
|
||||
import { circuits } from "../schema/circuits.js";
|
||||
|
||||
export class CircuitRepository {
|
||||
async findById(circuitId: string) {
|
||||
const [row] = await db.select().from(circuits).where(eq(circuits.id, circuitId)).limit(1);
|
||||
return row ?? null;
|
||||
}
|
||||
|
||||
async listByCircuitList(circuitListId: string) {
|
||||
return db
|
||||
.select()
|
||||
@@ -26,6 +31,10 @@ export class CircuitRepository {
|
||||
cableLength?: number;
|
||||
voltage?: number;
|
||||
remark?: string;
|
||||
rcdAssignment?: string;
|
||||
terminalDesignation?: string;
|
||||
status?: string;
|
||||
isReserve?: boolean;
|
||||
}) {
|
||||
const id = crypto.randomUUID();
|
||||
await db.insert(circuits).values({
|
||||
@@ -41,10 +50,86 @@ export class CircuitRepository {
|
||||
cableType: input.cableType ?? null,
|
||||
cableCrossSection: input.cableCrossSection ?? null,
|
||||
cableLength: input.cableLength ?? null,
|
||||
rcdAssignment: input.rcdAssignment ?? null,
|
||||
terminalDesignation: input.terminalDesignation ?? null,
|
||||
voltage: input.voltage ?? null,
|
||||
status: input.status ?? null,
|
||||
isReserve: input.isReserve ? 1 : 0,
|
||||
remark: input.remark ?? null,
|
||||
});
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
async update(
|
||||
circuitId: string,
|
||||
input: {
|
||||
sectionId: string;
|
||||
equipmentIdentifier: string;
|
||||
displayName?: string;
|
||||
sortOrder: number;
|
||||
protectionType?: string;
|
||||
protectionRatedCurrent?: number;
|
||||
protectionCharacteristic?: string;
|
||||
cableType?: string;
|
||||
cableCrossSection?: string;
|
||||
cableLength?: number;
|
||||
rcdAssignment?: string;
|
||||
terminalDesignation?: string;
|
||||
voltage?: number;
|
||||
status?: string;
|
||||
isReserve: boolean;
|
||||
remark?: string;
|
||||
}
|
||||
) {
|
||||
await db
|
||||
.update(circuits)
|
||||
.set({
|
||||
sectionId: input.sectionId,
|
||||
equipmentIdentifier: input.equipmentIdentifier,
|
||||
displayName: input.displayName ?? null,
|
||||
sortOrder: input.sortOrder,
|
||||
protectionType: input.protectionType ?? null,
|
||||
protectionRatedCurrent: input.protectionRatedCurrent ?? null,
|
||||
protectionCharacteristic: input.protectionCharacteristic ?? null,
|
||||
cableType: input.cableType ?? null,
|
||||
cableCrossSection: input.cableCrossSection ?? null,
|
||||
cableLength: input.cableLength ?? null,
|
||||
rcdAssignment: input.rcdAssignment ?? null,
|
||||
terminalDesignation: input.terminalDesignation ?? null,
|
||||
voltage: input.voltage ?? null,
|
||||
status: input.status ?? null,
|
||||
isReserve: input.isReserve ? 1 : 0,
|
||||
remark: input.remark ?? null,
|
||||
})
|
||||
.where(eq(circuits.id, circuitId));
|
||||
}
|
||||
|
||||
async delete(circuitId: string) {
|
||||
await db.delete(circuits).where(eq(circuits.id, circuitId));
|
||||
}
|
||||
|
||||
async existsByEquipmentIdentifier(circuitListId: string, equipmentIdentifier: string, excludeCircuitId?: string) {
|
||||
const rows = await db
|
||||
.select({ id: circuits.id })
|
||||
.from(circuits)
|
||||
.where(
|
||||
excludeCircuitId
|
||||
? and(
|
||||
eq(circuits.circuitListId, circuitListId),
|
||||
eq(circuits.equipmentIdentifier, equipmentIdentifier),
|
||||
ne(circuits.id, excludeCircuitId)
|
||||
)
|
||||
: and(eq(circuits.circuitListId, circuitListId), eq(circuits.equipmentIdentifier, equipmentIdentifier))
|
||||
)
|
||||
.limit(1);
|
||||
return Boolean(rows.length);
|
||||
}
|
||||
|
||||
async listBySection(sectionId: string) {
|
||||
return db
|
||||
.select()
|
||||
.from(circuits)
|
||||
.where(eq(circuits.sectionId, sectionId))
|
||||
.orderBy(asc(circuits.sortOrder), asc(circuits.equipmentIdentifier));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
export function calculateRowTotalPower(
|
||||
quantity: number,
|
||||
powerPerUnit: number,
|
||||
simultaneityFactor: number
|
||||
): number {
|
||||
return quantity * powerPerUnit * simultaneityFactor;
|
||||
}
|
||||
|
||||
export function calculateCircuitTotalPower(
|
||||
rows: Array<{ quantity: number; powerPerUnit: number; simultaneityFactor: number }>
|
||||
): number {
|
||||
return rows.reduce(
|
||||
(sum, row) => sum + calculateRowTotalPower(row.quantity, row.powerPerUnit, row.simultaneityFactor),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { CircuitRepository } from "../../db/repositories/circuit.repository.js";
|
||||
import { CircuitSectionRepository } from "../../db/repositories/circuit-section.repository.js";
|
||||
|
||||
function parseSuffix(equipmentIdentifier: string, prefix: string): number | null {
|
||||
if (!equipmentIdentifier.startsWith(prefix)) {
|
||||
return null;
|
||||
}
|
||||
const suffix = equipmentIdentifier.slice(prefix.length);
|
||||
if (!/^\d+$/.test(suffix)) {
|
||||
return null;
|
||||
}
|
||||
return Number(suffix);
|
||||
}
|
||||
|
||||
export class CircuitNumberingService {
|
||||
private readonly sectionRepository: Pick<CircuitSectionRepository, "findById">;
|
||||
private readonly circuitRepository: Pick<CircuitRepository, "listBySection">;
|
||||
|
||||
constructor(deps?: {
|
||||
sectionRepository?: Pick<CircuitSectionRepository, "findById">;
|
||||
circuitRepository?: Pick<CircuitRepository, "listBySection">;
|
||||
}) {
|
||||
this.sectionRepository = deps?.sectionRepository ?? new CircuitSectionRepository();
|
||||
this.circuitRepository = deps?.circuitRepository ?? new CircuitRepository();
|
||||
}
|
||||
|
||||
async getNextIdentifier(sectionId: string) {
|
||||
const section = await this.sectionRepository.findById(sectionId);
|
||||
if (!section) {
|
||||
throw new Error("Section not found");
|
||||
}
|
||||
|
||||
const circuits = await this.circuitRepository.listBySection(sectionId);
|
||||
let maxSuffix = 0;
|
||||
for (const circuit of circuits) {
|
||||
const suffix = parseSuffix(circuit.equipmentIdentifier, section.prefix);
|
||||
if (suffix !== null && suffix > maxSuffix) {
|
||||
maxSuffix = suffix;
|
||||
}
|
||||
}
|
||||
|
||||
return `${section.prefix}${maxSuffix + 1}`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
import { CircuitDeviceRowRepository } from "../../db/repositories/circuit-device-row.repository.js";
|
||||
import { CircuitListRepository } from "../../db/repositories/circuit-list.repository.js";
|
||||
import { CircuitRepository } from "../../db/repositories/circuit.repository.js";
|
||||
import { CircuitSectionRepository } from "../../db/repositories/circuit-section.repository.js";
|
||||
import { ProjectDeviceRepository } from "../../db/repositories/project-device.repository.js";
|
||||
import type {
|
||||
CreateCircuitDeviceRowInput,
|
||||
CreateCircuitInput,
|
||||
UpdateCircuitDeviceRowInput,
|
||||
UpdateCircuitInput,
|
||||
} from "../../shared/validation/circuit.schemas.js";
|
||||
import { CircuitNumberingService } from "./circuit-numbering.service.js";
|
||||
|
||||
export class CircuitWriteService {
|
||||
private readonly circuitRepository: CircuitRepository;
|
||||
private readonly circuitSectionRepository: CircuitSectionRepository;
|
||||
private readonly circuitListRepository: CircuitListRepository;
|
||||
private readonly deviceRowRepository: CircuitDeviceRowRepository;
|
||||
private readonly projectDeviceRepository: ProjectDeviceRepository;
|
||||
private readonly numberingService: CircuitNumberingService;
|
||||
|
||||
constructor(deps?: {
|
||||
circuitRepository?: CircuitRepository;
|
||||
circuitSectionRepository?: CircuitSectionRepository;
|
||||
circuitListRepository?: CircuitListRepository;
|
||||
deviceRowRepository?: CircuitDeviceRowRepository;
|
||||
projectDeviceRepository?: ProjectDeviceRepository;
|
||||
numberingService?: CircuitNumberingService;
|
||||
}) {
|
||||
this.circuitRepository = deps?.circuitRepository ?? new CircuitRepository();
|
||||
this.circuitSectionRepository = deps?.circuitSectionRepository ?? new CircuitSectionRepository();
|
||||
this.circuitListRepository = deps?.circuitListRepository ?? new CircuitListRepository();
|
||||
this.deviceRowRepository = deps?.deviceRowRepository ?? new CircuitDeviceRowRepository();
|
||||
this.projectDeviceRepository = deps?.projectDeviceRepository ?? new ProjectDeviceRepository();
|
||||
this.numberingService = deps?.numberingService ?? new CircuitNumberingService();
|
||||
}
|
||||
|
||||
private async assertSectionInList(sectionId: string, circuitListId: string) {
|
||||
const section = await this.circuitSectionRepository.findById(sectionId);
|
||||
if (!section) {
|
||||
throw new Error("Invalid section id.");
|
||||
}
|
||||
if (section.circuitListId !== circuitListId) {
|
||||
throw new Error("Section does not belong to circuit list.");
|
||||
}
|
||||
return section;
|
||||
}
|
||||
|
||||
private async assertUniqueEquipmentIdentifier(
|
||||
circuitListId: string,
|
||||
equipmentIdentifier: string,
|
||||
excludeCircuitId?: string
|
||||
) {
|
||||
const exists = await this.circuitRepository.existsByEquipmentIdentifier(
|
||||
circuitListId,
|
||||
equipmentIdentifier,
|
||||
excludeCircuitId
|
||||
);
|
||||
if (exists) {
|
||||
throw new Error("Duplicate equipmentIdentifier in circuit list.");
|
||||
}
|
||||
}
|
||||
|
||||
private async assertValidLinkedProjectDevice(circuitId: string, linkedProjectDeviceId?: string) {
|
||||
if (!linkedProjectDeviceId) {
|
||||
return;
|
||||
}
|
||||
const circuit = await this.circuitRepository.findById(circuitId);
|
||||
if (!circuit) {
|
||||
throw new Error("Invalid circuit id.");
|
||||
}
|
||||
const list = await this.circuitListRepository.findByIdByListIdOnly(circuit.circuitListId);
|
||||
if (!list) {
|
||||
throw new Error("Circuit list not found.");
|
||||
}
|
||||
const device = await this.projectDeviceRepository.findById(list.projectId, linkedProjectDeviceId);
|
||||
if (!device) {
|
||||
throw new Error("Invalid linked project device id.");
|
||||
}
|
||||
}
|
||||
|
||||
async createCircuit(projectId: string, circuitListId: string, input: CreateCircuitInput) {
|
||||
const list = await this.circuitListRepository.findById(projectId, circuitListId);
|
||||
if (!list) {
|
||||
throw new Error("Circuit list not found in project.");
|
||||
}
|
||||
await this.assertSectionInList(input.sectionId, circuitListId);
|
||||
await this.assertUniqueEquipmentIdentifier(circuitListId, input.equipmentIdentifier);
|
||||
const id = await this.circuitRepository.create({
|
||||
circuitListId,
|
||||
sectionId: input.sectionId,
|
||||
equipmentIdentifier: input.equipmentIdentifier,
|
||||
displayName: input.displayName,
|
||||
sortOrder: input.sortOrder,
|
||||
protectionType: input.protectionType,
|
||||
protectionRatedCurrent: input.protectionRatedCurrent,
|
||||
protectionCharacteristic: input.protectionCharacteristic,
|
||||
cableType: input.cableType,
|
||||
cableCrossSection: input.cableCrossSection,
|
||||
cableLength: input.cableLength,
|
||||
rcdAssignment: input.rcdAssignment,
|
||||
terminalDesignation: input.terminalDesignation,
|
||||
voltage: input.voltage,
|
||||
status: input.status,
|
||||
isReserve: input.isReserve ?? true,
|
||||
remark: input.remark,
|
||||
});
|
||||
return this.circuitRepository.findById(id);
|
||||
}
|
||||
|
||||
async updateCircuit(circuitId: string, input: UpdateCircuitInput) {
|
||||
const current = await this.circuitRepository.findById(circuitId);
|
||||
if (!current) {
|
||||
throw new Error("Invalid circuit id.");
|
||||
}
|
||||
|
||||
const sectionId = input.sectionId ?? current.sectionId;
|
||||
const equipmentIdentifier = input.equipmentIdentifier ?? current.equipmentIdentifier;
|
||||
const sortOrder = input.sortOrder ?? current.sortOrder;
|
||||
await this.assertSectionInList(sectionId, current.circuitListId);
|
||||
await this.assertUniqueEquipmentIdentifier(current.circuitListId, equipmentIdentifier, circuitId);
|
||||
|
||||
await this.circuitRepository.update(circuitId, {
|
||||
sectionId,
|
||||
equipmentIdentifier,
|
||||
displayName: input.displayName ?? current.displayName ?? undefined,
|
||||
sortOrder,
|
||||
protectionType: input.protectionType ?? current.protectionType ?? undefined,
|
||||
protectionRatedCurrent: input.protectionRatedCurrent ?? current.protectionRatedCurrent ?? undefined,
|
||||
protectionCharacteristic:
|
||||
input.protectionCharacteristic ?? current.protectionCharacteristic ?? undefined,
|
||||
cableType: input.cableType ?? current.cableType ?? undefined,
|
||||
cableCrossSection: input.cableCrossSection ?? current.cableCrossSection ?? undefined,
|
||||
cableLength: input.cableLength ?? current.cableLength ?? undefined,
|
||||
rcdAssignment: input.rcdAssignment ?? current.rcdAssignment ?? undefined,
|
||||
terminalDesignation: input.terminalDesignation ?? current.terminalDesignation ?? undefined,
|
||||
voltage: input.voltage ?? current.voltage ?? undefined,
|
||||
status: input.status ?? current.status ?? undefined,
|
||||
isReserve: input.isReserve ?? Boolean(current.isReserve),
|
||||
remark: input.remark ?? current.remark ?? undefined,
|
||||
});
|
||||
return this.circuitRepository.findById(circuitId);
|
||||
}
|
||||
|
||||
async deleteCircuit(circuitId: string) {
|
||||
const current = await this.circuitRepository.findById(circuitId);
|
||||
if (!current) {
|
||||
throw new Error("Invalid circuit id.");
|
||||
}
|
||||
await this.circuitRepository.delete(circuitId);
|
||||
}
|
||||
|
||||
async createDeviceRow(circuitId: string, input: CreateCircuitDeviceRowInput) {
|
||||
const circuit = await this.circuitRepository.findById(circuitId);
|
||||
if (!circuit) {
|
||||
throw new Error("Invalid circuit id.");
|
||||
}
|
||||
await this.assertValidLinkedProjectDevice(circuitId, input.linkedProjectDeviceId);
|
||||
|
||||
const existingRows = await this.deviceRowRepository.countByCircuit(circuitId);
|
||||
const rowId = await this.deviceRowRepository.create({
|
||||
circuitId,
|
||||
linkedProjectDeviceId: input.linkedProjectDeviceId,
|
||||
sortOrder: input.sortOrder ?? (existingRows + 1) * 10,
|
||||
name: input.name,
|
||||
displayName: input.displayName,
|
||||
phaseType: input.phaseType,
|
||||
connectionKind: input.connectionKind,
|
||||
costGroup: input.costGroup,
|
||||
category: input.category,
|
||||
level: input.level,
|
||||
roomId: input.roomId,
|
||||
roomNumberSnapshot: input.roomNumberSnapshot,
|
||||
roomNameSnapshot: input.roomNameSnapshot,
|
||||
quantity: input.quantity,
|
||||
powerPerUnit: input.powerPerUnit,
|
||||
simultaneityFactor: input.simultaneityFactor,
|
||||
cosPhi: input.cosPhi,
|
||||
remark: input.remark,
|
||||
overriddenFields: input.overriddenFields,
|
||||
});
|
||||
|
||||
if (Boolean(circuit.isReserve)) {
|
||||
await this.circuitRepository.update(circuit.id, {
|
||||
sectionId: circuit.sectionId,
|
||||
equipmentIdentifier: circuit.equipmentIdentifier,
|
||||
displayName: circuit.displayName ?? undefined,
|
||||
sortOrder: circuit.sortOrder,
|
||||
protectionType: circuit.protectionType ?? undefined,
|
||||
protectionRatedCurrent: circuit.protectionRatedCurrent ?? undefined,
|
||||
protectionCharacteristic: circuit.protectionCharacteristic ?? undefined,
|
||||
cableType: circuit.cableType ?? undefined,
|
||||
cableCrossSection: circuit.cableCrossSection ?? undefined,
|
||||
cableLength: circuit.cableLength ?? undefined,
|
||||
rcdAssignment: circuit.rcdAssignment ?? undefined,
|
||||
terminalDesignation: circuit.terminalDesignation ?? undefined,
|
||||
voltage: circuit.voltage ?? undefined,
|
||||
status: circuit.status ?? undefined,
|
||||
isReserve: false,
|
||||
remark: circuit.remark ?? undefined,
|
||||
});
|
||||
}
|
||||
|
||||
return this.deviceRowRepository.findById(rowId);
|
||||
}
|
||||
|
||||
async updateDeviceRow(rowId: string, input: UpdateCircuitDeviceRowInput) {
|
||||
const current = await this.deviceRowRepository.findById(rowId);
|
||||
if (!current) {
|
||||
throw new Error("Invalid device row id.");
|
||||
}
|
||||
await this.assertValidLinkedProjectDevice(current.circuitId, input.linkedProjectDeviceId);
|
||||
await this.deviceRowRepository.update(rowId, {
|
||||
linkedProjectDeviceId: input.linkedProjectDeviceId ?? current.linkedProjectDeviceId ?? undefined,
|
||||
name: input.name ?? current.name,
|
||||
displayName: input.displayName ?? current.displayName,
|
||||
phaseType: input.phaseType ?? current.phaseType ?? undefined,
|
||||
connectionKind: input.connectionKind ?? current.connectionKind ?? undefined,
|
||||
costGroup: input.costGroup ?? current.costGroup ?? undefined,
|
||||
category: input.category ?? current.category ?? undefined,
|
||||
level: input.level ?? current.level ?? undefined,
|
||||
roomId: input.roomId ?? current.roomId ?? undefined,
|
||||
roomNumberSnapshot: input.roomNumberSnapshot ?? current.roomNumberSnapshot ?? undefined,
|
||||
roomNameSnapshot: input.roomNameSnapshot ?? current.roomNameSnapshot ?? undefined,
|
||||
quantity: input.quantity ?? current.quantity,
|
||||
powerPerUnit: input.powerPerUnit ?? current.powerPerUnit,
|
||||
simultaneityFactor: input.simultaneityFactor ?? current.simultaneityFactor,
|
||||
cosPhi: input.cosPhi ?? current.cosPhi ?? undefined,
|
||||
remark: input.remark ?? current.remark ?? undefined,
|
||||
overriddenFields: input.overriddenFields ?? current.overriddenFields ?? undefined,
|
||||
});
|
||||
return this.deviceRowRepository.findById(rowId);
|
||||
}
|
||||
|
||||
async deleteDeviceRow(rowId: string) {
|
||||
const current = await this.deviceRowRepository.findById(rowId);
|
||||
if (!current) {
|
||||
throw new Error("Invalid device row id.");
|
||||
}
|
||||
const circuit = await this.circuitRepository.findById(current.circuitId);
|
||||
if (!circuit) {
|
||||
throw new Error("Invalid circuit id.");
|
||||
}
|
||||
await this.deviceRowRepository.delete(rowId);
|
||||
const remaining = await this.deviceRowRepository.countByCircuit(current.circuitId);
|
||||
if (remaining === 0) {
|
||||
await this.circuitRepository.update(circuit.id, {
|
||||
sectionId: circuit.sectionId,
|
||||
equipmentIdentifier: circuit.equipmentIdentifier,
|
||||
displayName: circuit.displayName ?? undefined,
|
||||
sortOrder: circuit.sortOrder,
|
||||
protectionType: circuit.protectionType ?? undefined,
|
||||
protectionRatedCurrent: circuit.protectionRatedCurrent ?? undefined,
|
||||
protectionCharacteristic: circuit.protectionCharacteristic ?? undefined,
|
||||
cableType: circuit.cableType ?? undefined,
|
||||
cableCrossSection: circuit.cableCrossSection ?? undefined,
|
||||
cableLength: circuit.cableLength ?? undefined,
|
||||
rcdAssignment: circuit.rcdAssignment ?? undefined,
|
||||
terminalDesignation: circuit.terminalDesignation ?? undefined,
|
||||
voltage: circuit.voltage ?? undefined,
|
||||
status: circuit.status ?? undefined,
|
||||
isReserve: true,
|
||||
remark: circuit.remark ?? undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async getNextIdentifier(sectionId: string) {
|
||||
return this.numberingService.getNextIdentifier(sectionId);
|
||||
}
|
||||
|
||||
async renumberSection(sectionId: string) {
|
||||
const section = await this.circuitSectionRepository.findById(sectionId);
|
||||
if (!section) {
|
||||
throw new Error("Invalid section id.");
|
||||
}
|
||||
const sectionCircuits = await this.circuitRepository.listBySection(sectionId);
|
||||
const otherCircuits = (await this.circuitRepository.listByCircuitList(section.circuitListId)).filter(
|
||||
(circuit) => circuit.sectionId !== sectionId
|
||||
);
|
||||
const otherIdentifiers = new Set(otherCircuits.map((circuit) => circuit.equipmentIdentifier));
|
||||
|
||||
let index = 1;
|
||||
for (const circuit of sectionCircuits) {
|
||||
let candidate = `${section.prefix}${index}`;
|
||||
while (otherIdentifiers.has(candidate)) {
|
||||
index += 1;
|
||||
candidate = `${section.prefix}${index}`;
|
||||
}
|
||||
await this.circuitRepository.update(circuit.id, {
|
||||
sectionId: circuit.sectionId,
|
||||
equipmentIdentifier: candidate,
|
||||
displayName: circuit.displayName ?? undefined,
|
||||
sortOrder: circuit.sortOrder,
|
||||
protectionType: circuit.protectionType ?? undefined,
|
||||
protectionRatedCurrent: circuit.protectionRatedCurrent ?? undefined,
|
||||
protectionCharacteristic: circuit.protectionCharacteristic ?? undefined,
|
||||
cableType: circuit.cableType ?? undefined,
|
||||
cableCrossSection: circuit.cableCrossSection ?? undefined,
|
||||
cableLength: circuit.cableLength ?? undefined,
|
||||
rcdAssignment: circuit.rcdAssignment ?? undefined,
|
||||
terminalDesignation: circuit.terminalDesignation ?? undefined,
|
||||
voltage: circuit.voltage ?? undefined,
|
||||
status: circuit.status ?? undefined,
|
||||
isReserve: Boolean(circuit.isReserve),
|
||||
remark: circuit.remark ?? undefined,
|
||||
});
|
||||
index += 1;
|
||||
}
|
||||
|
||||
return this.circuitRepository.listBySection(sectionId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Fragment } from "react";
|
||||
import { getCircuitTree } from "../utils/api";
|
||||
import type { CircuitTreeCircuitDto, CircuitTreeResponseDto } from "../types";
|
||||
|
||||
function formatNumber(value: number | undefined, digits = 2) {
|
||||
if (value === undefined || Number.isNaN(value)) {
|
||||
return "-";
|
||||
}
|
||||
return new Intl.NumberFormat("de-DE", {
|
||||
minimumFractionDigits: digits,
|
||||
maximumFractionDigits: digits,
|
||||
}).format(value);
|
||||
}
|
||||
|
||||
function renderCircuitSummaryLabel(circuit: CircuitTreeCircuitDto) {
|
||||
if (circuit.displayName?.trim()) {
|
||||
return circuit.displayName;
|
||||
}
|
||||
if (circuit.deviceRows.length > 1) {
|
||||
return `${circuit.deviceRows.length} devices`;
|
||||
}
|
||||
return "Reserve";
|
||||
}
|
||||
|
||||
export function CircuitTreePreview(props: { projectId: string; circuitListId: string }) {
|
||||
const { projectId, circuitListId } = props;
|
||||
const [data, setData] = useState<CircuitTreeResponseDto | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
getCircuitTree(projectId, circuitListId)
|
||||
.then(setData)
|
||||
.catch((err: unknown) =>
|
||||
setError(err instanceof Error ? err.message : "Circuit tree could not be loaded.")
|
||||
)
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [projectId, circuitListId]);
|
||||
|
||||
if (isLoading) {
|
||||
return <div className="alert alert-info">Loading circuit tree...</div>;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <div className="alert alert-danger">{error}</div>;
|
||||
}
|
||||
|
||||
if (!data || !data.sections.length) {
|
||||
return <div className="alert alert-secondary">No sections or circuits available.</div>;
|
||||
}
|
||||
|
||||
const hasAnyCircuits = data.sections.some((section) => section.circuits.length > 0);
|
||||
if (!hasAnyCircuits) {
|
||||
return <div className="alert alert-secondary">Sections exist, but no circuits were found yet.</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="card shadow-sm">
|
||||
<div className="card-body">
|
||||
<div className="table-responsive">
|
||||
<table className="table table-sm align-middle circuit-tree-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Equipment identifier</th>
|
||||
<th>Display name</th>
|
||||
<th>Phase type</th>
|
||||
<th>Connection kind</th>
|
||||
<th>Cost group</th>
|
||||
<th>Category</th>
|
||||
<th>Level</th>
|
||||
<th>Room number</th>
|
||||
<th>Room name</th>
|
||||
<th className="text-end">Quantity</th>
|
||||
<th className="text-end">Power / unit</th>
|
||||
<th className="text-end">Simultaneity</th>
|
||||
<th className="text-end">cosPhi</th>
|
||||
<th className="text-end">Row total</th>
|
||||
<th className="text-end">Circuit total</th>
|
||||
<th>Protection type</th>
|
||||
<th className="text-end">Protection current</th>
|
||||
<th>Protection characteristic</th>
|
||||
<th>Cable type</th>
|
||||
<th>Cable cross-section</th>
|
||||
<th className="text-end">Cable length</th>
|
||||
<th>Remark</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.sections.map((section) => (
|
||||
<SectionRows key={section.id} section={section} />
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SectionRows(props: { section: CircuitTreeResponseDto["sections"][number] }) {
|
||||
const { section } = props;
|
||||
return (
|
||||
<>
|
||||
<tr className="section-row">
|
||||
<td colSpan={22}>
|
||||
<strong>{section.displayName}</strong>
|
||||
</td>
|
||||
</tr>
|
||||
{section.circuits.map((circuit) => {
|
||||
if (circuit.deviceRows.length === 0) {
|
||||
return (
|
||||
<tr key={circuit.id} className="reserve-row">
|
||||
<td>{circuit.equipmentIdentifier}</td>
|
||||
<td>{circuit.displayName?.trim() || "Reserve"}</td>
|
||||
<td colSpan={12}>-</td>
|
||||
<td className="text-end">{formatNumber(circuit.circuitTotalPower)}</td>
|
||||
<td>{circuit.protectionType ?? "-"}</td>
|
||||
<td className="text-end">{formatNumber(circuit.protectionRatedCurrent)}</td>
|
||||
<td>{circuit.protectionCharacteristic ?? "-"}</td>
|
||||
<td>{circuit.cableType ?? "-"}</td>
|
||||
<td>{circuit.cableCrossSection ?? "-"}</td>
|
||||
<td className="text-end">{formatNumber(circuit.cableLength)}</td>
|
||||
<td>{circuit.remark ?? "-"}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
if (circuit.deviceRows.length === 1) {
|
||||
const row = circuit.deviceRows[0];
|
||||
return (
|
||||
<tr key={circuit.id} className={circuit.isReserve ? "reserve-row" : ""}>
|
||||
<td>{circuit.equipmentIdentifier}</td>
|
||||
<td>{row.displayName || row.name}</td>
|
||||
<td>{row.phaseType ?? "-"}</td>
|
||||
<td>{row.connectionKind ?? "-"}</td>
|
||||
<td>{row.costGroup ?? "-"}</td>
|
||||
<td>{row.category ?? "-"}</td>
|
||||
<td>{row.level ?? "-"}</td>
|
||||
<td>{row.roomNumberSnapshot ?? "-"}</td>
|
||||
<td>{row.roomNameSnapshot ?? "-"}</td>
|
||||
<td className="text-end">{formatNumber(row.quantity, 0)}</td>
|
||||
<td className="text-end">{formatNumber(row.powerPerUnit)}</td>
|
||||
<td className="text-end">{formatNumber(row.simultaneityFactor)}</td>
|
||||
<td className="text-end">{formatNumber(row.cosPhi)}</td>
|
||||
<td className="text-end">{formatNumber(row.rowTotalPower)}</td>
|
||||
<td className="text-end">{formatNumber(circuit.circuitTotalPower)}</td>
|
||||
<td>{circuit.protectionType ?? "-"}</td>
|
||||
<td className="text-end">{formatNumber(circuit.protectionRatedCurrent)}</td>
|
||||
<td>{circuit.protectionCharacteristic ?? "-"}</td>
|
||||
<td>{circuit.cableType ?? "-"}</td>
|
||||
<td>{circuit.cableCrossSection ?? "-"}</td>
|
||||
<td className="text-end">{formatNumber(circuit.cableLength)}</td>
|
||||
<td>{row.remark ?? circuit.remark ?? "-"}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment key={circuit.id}>
|
||||
<tr key={`${circuit.id}-summary`} className="summary-row">
|
||||
<td>{circuit.equipmentIdentifier}</td>
|
||||
<td>{renderCircuitSummaryLabel(circuit)}</td>
|
||||
<td colSpan={12}>-</td>
|
||||
<td className="text-end">{formatNumber(circuit.circuitTotalPower)}</td>
|
||||
<td>{circuit.protectionType ?? "-"}</td>
|
||||
<td className="text-end">{formatNumber(circuit.protectionRatedCurrent)}</td>
|
||||
<td>{circuit.protectionCharacteristic ?? "-"}</td>
|
||||
<td>{circuit.cableType ?? "-"}</td>
|
||||
<td>{circuit.cableCrossSection ?? "-"}</td>
|
||||
<td className="text-end">{formatNumber(circuit.cableLength)}</td>
|
||||
<td>{circuit.remark ?? "-"}</td>
|
||||
</tr>
|
||||
{circuit.deviceRows.map((row) => (
|
||||
<tr key={row.id} className="device-row">
|
||||
<td className="text-muted"> </td>
|
||||
<td className="indented-cell">{row.displayName || row.name}</td>
|
||||
<td>{row.phaseType ?? "-"}</td>
|
||||
<td>{row.connectionKind ?? "-"}</td>
|
||||
<td>{row.costGroup ?? "-"}</td>
|
||||
<td>{row.category ?? "-"}</td>
|
||||
<td>{row.level ?? "-"}</td>
|
||||
<td>{row.roomNumberSnapshot ?? "-"}</td>
|
||||
<td>{row.roomNameSnapshot ?? "-"}</td>
|
||||
<td className="text-end">{formatNumber(row.quantity, 0)}</td>
|
||||
<td className="text-end">{formatNumber(row.powerPerUnit)}</td>
|
||||
<td className="text-end">{formatNumber(row.simultaneityFactor)}</td>
|
||||
<td className="text-end">{formatNumber(row.cosPhi)}</td>
|
||||
<td className="text-end">{formatNumber(row.rowTotalPower)}</td>
|
||||
<td className="text-end">-</td>
|
||||
<td>-</td>
|
||||
<td className="text-end">-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td className="text-end">-</td>
|
||||
<td>{row.remark ?? "-"}</td>
|
||||
</tr>
|
||||
))}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
<tr className="placeholder-row">
|
||||
<td>-frei-</td>
|
||||
<td colSpan={21}>free placeholder</td>
|
||||
</tr>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -168,3 +168,76 @@ export interface CreateProjectDeviceInput {
|
||||
powerFactor?: number;
|
||||
note?: string;
|
||||
}
|
||||
|
||||
export interface CircuitTreeDeviceRowDto {
|
||||
id: string;
|
||||
linkedProjectDeviceId?: string;
|
||||
legacyConsumerId?: string;
|
||||
sortOrder: number;
|
||||
name: string;
|
||||
displayName: string;
|
||||
phaseType?: string;
|
||||
connectionKind?: string;
|
||||
costGroup?: string;
|
||||
category?: string;
|
||||
level?: string;
|
||||
roomId?: string;
|
||||
roomNumberSnapshot?: string;
|
||||
roomNameSnapshot?: string;
|
||||
quantity: number;
|
||||
powerPerUnit: number;
|
||||
simultaneityFactor: number;
|
||||
cosPhi?: number;
|
||||
remark?: string;
|
||||
overriddenFields?: string;
|
||||
rowTotalPower: number;
|
||||
}
|
||||
|
||||
export interface CircuitTreeCircuitDto {
|
||||
id: string;
|
||||
circuitListId: string;
|
||||
sectionId: string;
|
||||
equipmentIdentifier: string;
|
||||
displayName?: string;
|
||||
sortOrder: number;
|
||||
protectionType?: string;
|
||||
protectionRatedCurrent?: number;
|
||||
protectionCharacteristic?: string;
|
||||
cableType?: string;
|
||||
cableCrossSection?: string;
|
||||
cableLength?: number;
|
||||
rcdAssignment?: string;
|
||||
terminalDesignation?: string;
|
||||
voltage?: number;
|
||||
status?: string;
|
||||
isReserve: boolean;
|
||||
remark?: string;
|
||||
circuitTotalPower: number;
|
||||
deviceRows: CircuitTreeDeviceRowDto[];
|
||||
}
|
||||
|
||||
export interface CircuitTreeSectionDto {
|
||||
id: string;
|
||||
key: string;
|
||||
displayName: string;
|
||||
prefix: string;
|
||||
sortOrder: number;
|
||||
circuits: CircuitTreeCircuitDto[];
|
||||
}
|
||||
|
||||
export interface CircuitTreeMigrationReportDto {
|
||||
circuitListId: string;
|
||||
legacyConsumerCount: number;
|
||||
createdCircuitCount: number;
|
||||
createdDeviceRowCount: number;
|
||||
groupedDuplicateCircuitNumbers: Array<{ normalizedCircuitNumber: string; count: number }>;
|
||||
generatedIdentifiers: string[];
|
||||
unassignedRows: Array<{ consumerId: string; reason: string }>;
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
export interface CircuitTreeResponseDto {
|
||||
circuitListId: string;
|
||||
sections: CircuitTreeSectionDto[];
|
||||
migrationReport?: CircuitTreeMigrationReportDto;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
ProjectDto,
|
||||
RoomDto,
|
||||
UpdateConsumerInput,
|
||||
CircuitTreeResponseDto,
|
||||
} from "../types";
|
||||
|
||||
async function request<T>(url: string, init?: RequestInit): Promise<T> {
|
||||
@@ -77,6 +78,10 @@ export function listCircuitLists(projectId: string) {
|
||||
return request<CircuitListDto[]>(`/api/projects/${projectId}/circuit-lists`);
|
||||
}
|
||||
|
||||
export function getCircuitTree(projectId: string, circuitListId: string) {
|
||||
return request<CircuitTreeResponseDto>(`/api/projects/${projectId}/circuit-lists/${circuitListId}/tree`);
|
||||
}
|
||||
|
||||
export function listFloors(projectId: string) {
|
||||
return request<FloorDto[]>(`/api/projects/${projectId}/floors`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { Request, Response } from "express";
|
||||
import { CircuitWriteService } from "../../domain/services/circuit-write.service.js";
|
||||
import {
|
||||
createCircuitDeviceRowSchema,
|
||||
updateCircuitDeviceRowSchema,
|
||||
} from "../../shared/validation/circuit.schemas.js";
|
||||
|
||||
const circuitWriteService = new CircuitWriteService();
|
||||
|
||||
export async function createCircuitDeviceRow(req: Request, res: Response) {
|
||||
const { circuitId } = req.params;
|
||||
if (typeof circuitId !== "string") {
|
||||
return res.status(400).json({ error: "Invalid circuitId" });
|
||||
}
|
||||
|
||||
const parsed = createCircuitDeviceRowSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
return res.status(400).json({ error: parsed.error.flatten() });
|
||||
}
|
||||
|
||||
try {
|
||||
const created = await circuitWriteService.createDeviceRow(circuitId, parsed.data);
|
||||
return res.status(201).json(created);
|
||||
} catch (error) {
|
||||
return res.status(400).json({ error: error instanceof Error ? error.message : "Failed to create device row." });
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateCircuitDeviceRow(req: Request, res: Response) {
|
||||
const { rowId } = req.params;
|
||||
if (typeof rowId !== "string") {
|
||||
return res.status(400).json({ error: "Invalid rowId" });
|
||||
}
|
||||
|
||||
const parsed = updateCircuitDeviceRowSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
return res.status(400).json({ error: parsed.error.flatten() });
|
||||
}
|
||||
|
||||
try {
|
||||
const updated = await circuitWriteService.updateDeviceRow(rowId, parsed.data);
|
||||
return res.json(updated);
|
||||
} catch (error) {
|
||||
return res.status(400).json({ error: error instanceof Error ? error.message : "Failed to update device row." });
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteCircuitDeviceRow(req: Request, res: Response) {
|
||||
const { rowId } = req.params;
|
||||
if (typeof rowId !== "string") {
|
||||
return res.status(400).json({ error: "Invalid rowId" });
|
||||
}
|
||||
|
||||
try {
|
||||
await circuitWriteService.deleteDeviceRow(rowId);
|
||||
return res.status(204).send();
|
||||
} catch (error) {
|
||||
return res.status(400).json({ error: error instanceof Error ? error.message : "Failed to delete device row." });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { Request, Response } from "express";
|
||||
import { CircuitWriteService } from "../../domain/services/circuit-write.service.js";
|
||||
|
||||
const circuitWriteService = new CircuitWriteService();
|
||||
|
||||
export async function renumberCircuitSection(req: Request, res: Response) {
|
||||
const { sectionId } = req.params;
|
||||
if (typeof sectionId !== "string") {
|
||||
return res.status(400).json({ error: "Invalid sectionId" });
|
||||
}
|
||||
|
||||
try {
|
||||
const updatedCircuits = await circuitWriteService.renumberSection(sectionId);
|
||||
return res.json({ sectionId, circuits: updatedCircuits });
|
||||
} catch (error) {
|
||||
return res.status(400).json({ error: error instanceof Error ? error.message : "Failed to renumber section." });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,17 +3,26 @@ import { CircuitRepository } from "../../db/repositories/circuit.repository.js";
|
||||
import { CircuitDeviceRowRepository } from "../../db/repositories/circuit-device-row.repository.js";
|
||||
import { CircuitListRepository } from "../../db/repositories/circuit-list.repository.js";
|
||||
import { CircuitSectionRepository } from "../../db/repositories/circuit-section.repository.js";
|
||||
import {
|
||||
calculateCircuitTotalPower,
|
||||
calculateRowTotalPower,
|
||||
} from "../../domain/calculations/circuit-power-calculation.js";
|
||||
import type { CircuitTreeResponse } from "../../domain/models/circuit-tree.model.js";
|
||||
import { LegacyConsumerMigrationService } from "../../domain/services/legacy-consumer-migration.service.js";
|
||||
|
||||
const circuitListRepository = new CircuitListRepository();
|
||||
const circuitSectionRepository = new CircuitSectionRepository();
|
||||
const circuitRepository = new CircuitRepository();
|
||||
const circuitDeviceRowRepository = new CircuitDeviceRowRepository();
|
||||
const legacyConsumerMigrationService = new LegacyConsumerMigrationService();
|
||||
|
||||
function rowTotalPower(quantity: number, powerPerUnit: number, simultaneityFactor: number): number {
|
||||
return quantity * powerPerUnit * simultaneityFactor;
|
||||
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")
|
||||
);
|
||||
}
|
||||
|
||||
export async function getCircuitTree(req: Request, res: Response) {
|
||||
@@ -27,87 +36,97 @@ export async function getCircuitTree(req: Request, res: Response) {
|
||||
return res.status(404).json({ error: "Circuit list not found" });
|
||||
}
|
||||
|
||||
const migrationReport = await legacyConsumerMigrationService.migrateCircuitList(projectId, circuitListId);
|
||||
const sections = await circuitSectionRepository.listByCircuitList(circuitListId);
|
||||
const circuits = await circuitRepository.listByCircuitList(circuitListId);
|
||||
const rows = await circuitDeviceRowRepository.listByCircuitList(circuits.map((entry) => entry.id));
|
||||
try {
|
||||
const sections = await circuitSectionRepository.listByCircuitList(circuitListId);
|
||||
const circuits = await circuitRepository.listByCircuitList(circuitListId);
|
||||
const rows = await circuitDeviceRowRepository.listByCircuitList(circuits.map((entry) => entry.id));
|
||||
|
||||
const sectionById = new Map(sections.map((section) => [section.id, section]));
|
||||
const rowsByCircuitId = new Map<string, typeof rows>();
|
||||
for (const row of rows) {
|
||||
if (!rowsByCircuitId.has(row.circuitId)) {
|
||||
rowsByCircuitId.set(row.circuitId, []);
|
||||
const sectionById = new Map(sections.map((section) => [section.id, section]));
|
||||
const rowsByCircuitId = new Map<string, typeof rows>();
|
||||
for (const row of rows) {
|
||||
if (!rowsByCircuitId.has(row.circuitId)) {
|
||||
rowsByCircuitId.set(row.circuitId, []);
|
||||
}
|
||||
rowsByCircuitId.get(row.circuitId)!.push(row);
|
||||
}
|
||||
rowsByCircuitId.get(row.circuitId)!.push(row);
|
||||
}
|
||||
|
||||
const tree: CircuitTreeResponse = {
|
||||
circuitListId,
|
||||
sections: sections.map((section) => ({
|
||||
id: section.id,
|
||||
key: section.key,
|
||||
displayName: section.displayName,
|
||||
prefix: section.prefix,
|
||||
sortOrder: section.sortOrder,
|
||||
circuits: [],
|
||||
})),
|
||||
};
|
||||
const sectionBlocks = new Map(tree.sections.map((section) => [section.id, section]));
|
||||
const tree: CircuitTreeResponse = {
|
||||
circuitListId,
|
||||
sections: sections.map((section) => ({
|
||||
id: section.id,
|
||||
key: section.key,
|
||||
displayName: section.displayName,
|
||||
prefix: section.prefix,
|
||||
sortOrder: section.sortOrder,
|
||||
circuits: [],
|
||||
})),
|
||||
};
|
||||
const sectionBlocks = new Map(tree.sections.map((section) => [section.id, section]));
|
||||
|
||||
for (const circuit of circuits) {
|
||||
const section = sectionById.get(circuit.sectionId);
|
||||
if (!section || section.circuitListId !== circuit.circuitListId) {
|
||||
continue;
|
||||
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,
|
||||
legacyConsumerId: row.legacyConsumerId ?? 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,
|
||||
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);
|
||||
|
||||
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,
|
||||
protectionType: circuit.protectionType ?? undefined,
|
||||
protectionRatedCurrent: circuit.protectionRatedCurrent ?? undefined,
|
||||
protectionCharacteristic: circuit.protectionCharacteristic ?? undefined,
|
||||
cableType: circuit.cableType ?? undefined,
|
||||
cableCrossSection: circuit.cableCrossSection ?? undefined,
|
||||
cableLength: circuit.cableLength ?? undefined,
|
||||
rcdAssignment: circuit.rcdAssignment ?? undefined,
|
||||
terminalDesignation: circuit.terminalDesignation ?? undefined,
|
||||
voltage: circuit.voltage ?? undefined,
|
||||
status: circuit.status ?? undefined,
|
||||
isReserve: Boolean(circuit.isReserve),
|
||||
remark: circuit.remark ?? undefined,
|
||||
circuitTotalPower,
|
||||
deviceRows,
|
||||
});
|
||||
}
|
||||
const deviceRows = (rowsByCircuitId.get(circuit.id) ?? []).map((row) => ({
|
||||
id: row.id,
|
||||
linkedProjectDeviceId: row.linkedProjectDeviceId ?? undefined,
|
||||
legacyConsumerId: row.legacyConsumerId ?? 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,
|
||||
powerPerUnit: row.powerPerUnit,
|
||||
simultaneityFactor: row.simultaneityFactor,
|
||||
cosPhi: row.cosPhi ?? undefined,
|
||||
remark: row.remark ?? undefined,
|
||||
overriddenFields: row.overriddenFields ?? undefined,
|
||||
rowTotalPower: rowTotalPower(row.quantity, row.powerPerUnit, row.simultaneityFactor),
|
||||
}));
|
||||
const circuitTotalPower = deviceRows.reduce((sum, row) => sum + row.rowTotalPower, 0);
|
||||
|
||||
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,
|
||||
protectionType: circuit.protectionType ?? undefined,
|
||||
protectionRatedCurrent: circuit.protectionRatedCurrent ?? undefined,
|
||||
protectionCharacteristic: circuit.protectionCharacteristic ?? undefined,
|
||||
cableType: circuit.cableType ?? undefined,
|
||||
cableCrossSection: circuit.cableCrossSection ?? undefined,
|
||||
cableLength: circuit.cableLength ?? undefined,
|
||||
rcdAssignment: circuit.rcdAssignment ?? undefined,
|
||||
terminalDesignation: circuit.terminalDesignation ?? undefined,
|
||||
voltage: circuit.voltage ?? undefined,
|
||||
status: circuit.status ?? undefined,
|
||||
isReserve: Boolean(circuit.isReserve),
|
||||
remark: circuit.remark ?? undefined,
|
||||
circuitTotalPower,
|
||||
deviceRows,
|
||||
});
|
||||
return res.json(tree);
|
||||
} catch (error) {
|
||||
if (isMissingCircuitTreeSchemaError(error)) {
|
||||
return res.json({
|
||||
circuitListId,
|
||||
sections: [],
|
||||
warning:
|
||||
"Circuit-first tables are not available yet. Run database migrations (including 0008_circuit_first_model).",
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res.json({ ...tree, migrationReport });
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import type { Request, Response } from "express";
|
||||
import { CircuitWriteService } from "../../domain/services/circuit-write.service.js";
|
||||
import { createCircuitSchema, updateCircuitSchema } from "../../shared/validation/circuit.schemas.js";
|
||||
|
||||
const circuitWriteService = new CircuitWriteService();
|
||||
|
||||
export async function createCircuit(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 parsed = createCircuitSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
return res.status(400).json({ error: parsed.error.flatten() });
|
||||
}
|
||||
|
||||
try {
|
||||
const created = await circuitWriteService.createCircuit(projectId, circuitListId, parsed.data);
|
||||
return res.status(201).json(created);
|
||||
} catch (error) {
|
||||
return res.status(400).json({ error: error instanceof Error ? error.message : "Failed to create circuit." });
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateCircuit(req: Request, res: Response) {
|
||||
const { circuitId } = req.params;
|
||||
if (typeof circuitId !== "string") {
|
||||
return res.status(400).json({ error: "Invalid circuitId" });
|
||||
}
|
||||
|
||||
const parsed = updateCircuitSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
return res.status(400).json({ error: parsed.error.flatten() });
|
||||
}
|
||||
|
||||
try {
|
||||
const updated = await circuitWriteService.updateCircuit(circuitId, parsed.data);
|
||||
return res.json(updated);
|
||||
} catch (error) {
|
||||
return res.status(400).json({ error: error instanceof Error ? error.message : "Failed to update circuit." });
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteCircuit(req: Request, res: Response) {
|
||||
const { circuitId } = req.params;
|
||||
if (typeof circuitId !== "string") {
|
||||
return res.status(400).json({ error: "Invalid circuitId" });
|
||||
}
|
||||
|
||||
try {
|
||||
await circuitWriteService.deleteCircuit(circuitId);
|
||||
return res.status(204).send();
|
||||
} catch (error) {
|
||||
return res.status(400).json({ error: error instanceof Error ? error.message : "Failed to delete circuit." });
|
||||
}
|
||||
}
|
||||
|
||||
export async function getNextCircuitIdentifier(req: Request, res: Response) {
|
||||
const { sectionId } = req.params;
|
||||
if (typeof sectionId !== "string") {
|
||||
return res.status(400).json({ error: "Invalid sectionId" });
|
||||
}
|
||||
try {
|
||||
const nextIdentifier = await circuitWriteService.getNextIdentifier(sectionId);
|
||||
return res.json({ sectionId, nextIdentifier });
|
||||
} catch (error) {
|
||||
return res.status(400).json({ error: error instanceof Error ? error.message : "Failed to get identifier." });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import express from "express";
|
||||
import { circuitDeviceRowRouter } from "./routes/circuit-device-row.routes.js";
|
||||
import { circuitRouter } from "./routes/circuit.routes.js";
|
||||
import { circuitSectionRouter } from "./routes/circuit-section.routes.js";
|
||||
import { consumerRouter } from "./routes/consumer.routes.js";
|
||||
import { globalDeviceRouter } from "./routes/global-device.routes.js";
|
||||
import { projectDeviceRouter } from "./routes/project-device.routes.js";
|
||||
@@ -15,6 +18,9 @@ app.get("/health", (_req, res) => {
|
||||
});
|
||||
|
||||
app.use("/api/projects", projectRouter);
|
||||
app.use("/api", circuitRouter);
|
||||
app.use("/api", circuitDeviceRowRouter);
|
||||
app.use("/api", circuitSectionRouter);
|
||||
app.use("/api/consumers", consumerRouter);
|
||||
app.use("/api/global-devices", globalDeviceRouter);
|
||||
app.use("/api/project-devices", projectDeviceRouter);
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Router } from "express";
|
||||
import {
|
||||
deleteCircuitDeviceRow,
|
||||
updateCircuitDeviceRow,
|
||||
} from "../controllers/circuit-device-row.controller.js";
|
||||
|
||||
export const circuitDeviceRowRouter = Router();
|
||||
|
||||
circuitDeviceRowRouter.patch("/circuit-device-rows/:rowId", updateCircuitDeviceRow);
|
||||
circuitDeviceRowRouter.delete("/circuit-device-rows/:rowId", deleteCircuitDeviceRow);
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Router } from "express";
|
||||
import { renumberCircuitSection } from "../controllers/circuit-section.controller.js";
|
||||
|
||||
export const circuitSectionRouter = Router();
|
||||
|
||||
circuitSectionRouter.post("/circuit-sections/:sectionId/renumber", renumberCircuitSection);
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Router } from "express";
|
||||
import {
|
||||
createCircuit,
|
||||
deleteCircuit,
|
||||
getNextCircuitIdentifier,
|
||||
updateCircuit,
|
||||
} from "../controllers/circuit.controller.js";
|
||||
import { createCircuitDeviceRow } from "../controllers/circuit-device-row.controller.js";
|
||||
|
||||
export const circuitRouter = Router();
|
||||
|
||||
circuitRouter.post("/projects/:projectId/circuit-lists/:circuitListId/circuits", createCircuit);
|
||||
circuitRouter.patch("/circuits/:circuitId", updateCircuit);
|
||||
circuitRouter.delete("/circuits/:circuitId", deleteCircuit);
|
||||
circuitRouter.get("/circuit-sections/:sectionId/next-identifier", getNextCircuitIdentifier);
|
||||
circuitRouter.post("/circuits/:circuitId/device-rows", createCircuitDeviceRow);
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const createCircuitSchema = z.object({
|
||||
sectionId: z.string().min(1),
|
||||
equipmentIdentifier: z.string().min(1),
|
||||
displayName: z.string().optional(),
|
||||
sortOrder: z.number(),
|
||||
protectionType: z.string().optional(),
|
||||
protectionRatedCurrent: z.number().min(0).optional(),
|
||||
protectionCharacteristic: z.string().optional(),
|
||||
cableType: z.string().optional(),
|
||||
cableCrossSection: z.string().optional(),
|
||||
cableLength: z.number().min(0).optional(),
|
||||
rcdAssignment: z.string().optional(),
|
||||
terminalDesignation: z.string().optional(),
|
||||
voltage: z.number().positive().optional(),
|
||||
status: z.string().optional(),
|
||||
isReserve: z.boolean().optional(),
|
||||
remark: z.string().optional(),
|
||||
});
|
||||
|
||||
export const updateCircuitSchema = createCircuitSchema.partial().extend({
|
||||
sectionId: z.string().min(1).optional(),
|
||||
equipmentIdentifier: z.string().min(1).optional(),
|
||||
sortOrder: z.number().optional(),
|
||||
isReserve: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const createCircuitDeviceRowSchema = z.object({
|
||||
linkedProjectDeviceId: z.string().min(1).optional(),
|
||||
name: z.string().min(1),
|
||||
displayName: z.string().min(1),
|
||||
phaseType: z.string().optional(),
|
||||
connectionKind: z.string().optional(),
|
||||
costGroup: z.string().optional(),
|
||||
category: z.string().optional(),
|
||||
level: z.string().optional(),
|
||||
roomId: z.string().min(1).optional(),
|
||||
roomNumberSnapshot: z.string().optional(),
|
||||
roomNameSnapshot: z.string().optional(),
|
||||
quantity: z.number().min(0),
|
||||
powerPerUnit: z.number().min(0),
|
||||
simultaneityFactor: z.number().min(0),
|
||||
cosPhi: z.number().positive().optional(),
|
||||
remark: z.string().optional(),
|
||||
overriddenFields: z.string().optional(),
|
||||
sortOrder: z.number().optional(),
|
||||
});
|
||||
|
||||
export const updateCircuitDeviceRowSchema = createCircuitDeviceRowSchema.partial();
|
||||
|
||||
export type CreateCircuitInput = z.infer<typeof createCircuitSchema>;
|
||||
export type UpdateCircuitInput = z.infer<typeof updateCircuitSchema>;
|
||||
export type CreateCircuitDeviceRowInput = z.infer<typeof createCircuitDeviceRowSchema>;
|
||||
export type UpdateCircuitDeviceRowInput = z.infer<typeof updateCircuitDeviceRowSchema>;
|
||||
|
||||
Reference in New Issue
Block a user