Added 1B, 2 and added bootstrap again for site

This commit is contained in:
2026-05-03 22:04:45 +02:00
parent b8995b3a1b
commit d1ce485572
37 changed files with 1842 additions and 89 deletions
+29
View File
@@ -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>
);
}
+7
View File
@@ -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 {
}
}
}
+87 -2
View File
@@ -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>
</>
);
}
+73
View File
@@ -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;
}
+5
View File
@@ -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." });
}
}
+6
View File
@@ -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);
+17
View File
@@ -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);
+56
View File
@@ -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>;