474 lines
20 KiB
TypeScript
474 lines
20 KiB
TypeScript
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,
|
|
MoveCircuitDeviceRowInput,
|
|
ReorderSectionCircuitsInput,
|
|
UpdateSectionEquipmentIdentifiersInput,
|
|
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 moveDeviceRow(rowId: string, input: MoveCircuitDeviceRowInput) {
|
|
const row = await this.deviceRowRepository.findById(rowId);
|
|
if (!row) {
|
|
throw new Error("Invalid device row id.");
|
|
}
|
|
|
|
const sourceCircuit = await this.circuitRepository.findById(row.circuitId);
|
|
if (!sourceCircuit) {
|
|
throw new Error("Invalid circuit id.");
|
|
}
|
|
|
|
let targetCircuit = input.targetCircuitId
|
|
? await this.circuitRepository.findById(input.targetCircuitId)
|
|
: null;
|
|
if (input.targetCircuitId && !targetCircuit) {
|
|
throw new Error("Invalid target circuit id.");
|
|
}
|
|
|
|
if (!targetCircuit) {
|
|
if (!input.targetSectionId || !input.createNewCircuit) {
|
|
throw new Error("Invalid move target.");
|
|
}
|
|
const section = await this.assertSectionInList(input.targetSectionId, sourceCircuit.circuitListId);
|
|
const nextIdentifier = await this.numberingService.getNextIdentifier(section.id);
|
|
const sectionCircuits = await this.circuitRepository.listBySection(section.id);
|
|
const nextSortOrder =
|
|
sectionCircuits.length > 0 ? Math.max(...sectionCircuits.map((circuit) => circuit.sortOrder)) + 10 : 10;
|
|
const createdId = await this.circuitRepository.create({
|
|
circuitListId: sourceCircuit.circuitListId,
|
|
sectionId: section.id,
|
|
equipmentIdentifier: nextIdentifier,
|
|
displayName: "New circuit",
|
|
sortOrder: nextSortOrder,
|
|
isReserve: false,
|
|
});
|
|
targetCircuit = await this.circuitRepository.findById(createdId);
|
|
if (!targetCircuit) {
|
|
throw new Error("Failed to create target circuit.");
|
|
}
|
|
}
|
|
|
|
if (targetCircuit.circuitListId !== sourceCircuit.circuitListId) {
|
|
throw new Error("Target circuit does not belong to same circuit list.");
|
|
}
|
|
if (targetCircuit.id === sourceCircuit.id) {
|
|
return this.deviceRowRepository.findById(rowId);
|
|
}
|
|
|
|
const targetCount = await this.deviceRowRepository.countByCircuit(targetCircuit.id);
|
|
await this.deviceRowRepository.moveToCircuit(rowId, targetCircuit.id, (targetCount + 1) * 10);
|
|
|
|
const sourceRemaining = await this.deviceRowRepository.countByCircuit(sourceCircuit.id);
|
|
if (sourceRemaining === 0) {
|
|
await this.circuitRepository.update(sourceCircuit.id, {
|
|
sectionId: sourceCircuit.sectionId,
|
|
equipmentIdentifier: sourceCircuit.equipmentIdentifier,
|
|
displayName: sourceCircuit.displayName ?? undefined,
|
|
sortOrder: sourceCircuit.sortOrder,
|
|
protectionType: sourceCircuit.protectionType ?? undefined,
|
|
protectionRatedCurrent: sourceCircuit.protectionRatedCurrent ?? undefined,
|
|
protectionCharacteristic: sourceCircuit.protectionCharacteristic ?? undefined,
|
|
cableType: sourceCircuit.cableType ?? undefined,
|
|
cableCrossSection: sourceCircuit.cableCrossSection ?? undefined,
|
|
cableLength: sourceCircuit.cableLength ?? undefined,
|
|
rcdAssignment: sourceCircuit.rcdAssignment ?? undefined,
|
|
terminalDesignation: sourceCircuit.terminalDesignation ?? undefined,
|
|
voltage: sourceCircuit.voltage ?? undefined,
|
|
status: sourceCircuit.status ?? undefined,
|
|
isReserve: true,
|
|
remark: sourceCircuit.remark ?? undefined,
|
|
});
|
|
}
|
|
|
|
if (Boolean(targetCircuit.isReserve)) {
|
|
await this.circuitRepository.update(targetCircuit.id, {
|
|
sectionId: targetCircuit.sectionId,
|
|
equipmentIdentifier: targetCircuit.equipmentIdentifier,
|
|
displayName: targetCircuit.displayName ?? undefined,
|
|
sortOrder: targetCircuit.sortOrder,
|
|
protectionType: targetCircuit.protectionType ?? undefined,
|
|
protectionRatedCurrent: targetCircuit.protectionRatedCurrent ?? undefined,
|
|
protectionCharacteristic: targetCircuit.protectionCharacteristic ?? undefined,
|
|
cableType: targetCircuit.cableType ?? undefined,
|
|
cableCrossSection: targetCircuit.cableCrossSection ?? undefined,
|
|
cableLength: targetCircuit.cableLength ?? undefined,
|
|
rcdAssignment: targetCircuit.rcdAssignment ?? undefined,
|
|
terminalDesignation: targetCircuit.terminalDesignation ?? undefined,
|
|
voltage: targetCircuit.voltage ?? undefined,
|
|
status: targetCircuit.status ?? undefined,
|
|
isReserve: false,
|
|
remark: targetCircuit.remark ?? undefined,
|
|
});
|
|
}
|
|
|
|
return this.deviceRowRepository.findById(rowId);
|
|
}
|
|
|
|
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));
|
|
|
|
const finalAssignments: Array<{ id: string; equipmentIdentifier: string }> = [];
|
|
let index = 1;
|
|
for (const circuit of sectionCircuits) {
|
|
let candidate = `${section.prefix}${index}`;
|
|
while (otherIdentifiers.has(candidate)) {
|
|
index += 1;
|
|
candidate = `${section.prefix}${index}`;
|
|
}
|
|
finalAssignments.push({ id: circuit.id, equipmentIdentifier: candidate });
|
|
index += 1;
|
|
}
|
|
await this.circuitRepository.updateEquipmentIdentifiersSafely(
|
|
section.circuitListId,
|
|
finalAssignments,
|
|
sectionId
|
|
);
|
|
|
|
return this.circuitRepository.listBySection(sectionId);
|
|
}
|
|
|
|
async updateSectionEquipmentIdentifiers(
|
|
sectionId: string,
|
|
input: UpdateSectionEquipmentIdentifiersInput
|
|
) {
|
|
const section = await this.circuitSectionRepository.findById(sectionId);
|
|
if (!section) {
|
|
throw new Error("Invalid section id.");
|
|
}
|
|
const sectionCircuits = await this.circuitRepository.listBySection(sectionId);
|
|
const sectionIds = new Set(sectionCircuits.map((circuit) => circuit.id));
|
|
if (input.identifiers.length !== sectionCircuits.length) {
|
|
throw new Error("identifiers must include all circuits in the section.");
|
|
}
|
|
for (const entry of input.identifiers) {
|
|
if (!sectionIds.has(entry.circuitId)) {
|
|
throw new Error("Circuit id does not belong to section.");
|
|
}
|
|
}
|
|
|
|
await this.circuitRepository.updateEquipmentIdentifiersSafely(
|
|
section.circuitListId,
|
|
input.identifiers.map((entry) => ({ id: entry.circuitId, equipmentIdentifier: entry.equipmentIdentifier })),
|
|
sectionId
|
|
);
|
|
return this.circuitRepository.listBySection(sectionId);
|
|
}
|
|
|
|
async reorderCircuitsInSection(sectionId: string, input: ReorderSectionCircuitsInput) {
|
|
const section = await this.circuitSectionRepository.findById(sectionId);
|
|
if (!section) {
|
|
throw new Error("Invalid section id.");
|
|
}
|
|
const sectionCircuits = await this.circuitRepository.listBySection(sectionId);
|
|
const sectionIds = new Set(sectionCircuits.map((circuit) => circuit.id));
|
|
if (sectionCircuits.length !== input.orderedCircuitIds.length) {
|
|
throw new Error("orderedCircuitIds must include all circuits of the section.");
|
|
}
|
|
for (const circuitId of input.orderedCircuitIds) {
|
|
if (!sectionIds.has(circuitId)) {
|
|
throw new Error("Circuit id does not belong to section.");
|
|
}
|
|
}
|
|
|
|
for (let index = 0; index < input.orderedCircuitIds.length; index += 1) {
|
|
const circuitId = input.orderedCircuitIds[index];
|
|
const circuit = sectionCircuits.find((entry) => entry.id === circuitId);
|
|
if (!circuit) {
|
|
throw new Error("Invalid circuit id.");
|
|
}
|
|
await this.circuitRepository.update(circuit.id, {
|
|
sectionId: circuit.sectionId,
|
|
equipmentIdentifier: circuit.equipmentIdentifier,
|
|
displayName: circuit.displayName ?? undefined,
|
|
sortOrder: (index + 1) * 10,
|
|
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,
|
|
});
|
|
}
|
|
return this.circuitRepository.listBySection(sectionId);
|
|
}
|
|
}
|