409 lines
14 KiB
TypeScript
409 lines
14 KiB
TypeScript
import crypto from "node:crypto";
|
|
import { and, asc, eq, inArray, isNull } from "drizzle-orm";
|
|
import { db } from "../client.js";
|
|
import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
|
|
import { circuitLists } from "../schema/circuit-lists.js";
|
|
import { circuits } from "../schema/circuits.js";
|
|
import { distributionBoards } from "../schema/distribution-boards.js";
|
|
|
|
export interface CircuitDeviceRowUpdateInput {
|
|
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;
|
|
}
|
|
|
|
export interface CircuitDeviceRowsBulkMoveInput {
|
|
rows: Array<{ id: string; expectedCircuitId: string }>;
|
|
targetCircuitId?: string;
|
|
createTargetCircuit?: {
|
|
circuitListId: string;
|
|
sectionId: string;
|
|
equipmentIdentifier: string;
|
|
displayName: string;
|
|
sortOrder: number;
|
|
};
|
|
}
|
|
|
|
function toUpdateValues(input: CircuitDeviceRowUpdateInput) {
|
|
return {
|
|
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,
|
|
};
|
|
}
|
|
|
|
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 [];
|
|
}
|
|
return db
|
|
.select()
|
|
.from(circuitDeviceRows)
|
|
.where(inArray(circuitDeviceRows.circuitId, circuitIds))
|
|
.orderBy(asc(circuitDeviceRows.sortOrder));
|
|
}
|
|
|
|
async listLinkedByProjectDevice(projectId: string, projectDeviceId: string) {
|
|
return db
|
|
.select({
|
|
id: circuitDeviceRows.id,
|
|
circuitId: circuitDeviceRows.circuitId,
|
|
linkedProjectDeviceId: circuitDeviceRows.linkedProjectDeviceId,
|
|
legacyConsumerId: circuitDeviceRows.legacyConsumerId,
|
|
sortOrder: circuitDeviceRows.sortOrder,
|
|
name: circuitDeviceRows.name,
|
|
displayName: circuitDeviceRows.displayName,
|
|
phaseType: circuitDeviceRows.phaseType,
|
|
connectionKind: circuitDeviceRows.connectionKind,
|
|
costGroup: circuitDeviceRows.costGroup,
|
|
category: circuitDeviceRows.category,
|
|
level: circuitDeviceRows.level,
|
|
roomId: circuitDeviceRows.roomId,
|
|
roomNumberSnapshot: circuitDeviceRows.roomNumberSnapshot,
|
|
roomNameSnapshot: circuitDeviceRows.roomNameSnapshot,
|
|
quantity: circuitDeviceRows.quantity,
|
|
powerPerUnit: circuitDeviceRows.powerPerUnit,
|
|
simultaneityFactor: circuitDeviceRows.simultaneityFactor,
|
|
cosPhi: circuitDeviceRows.cosPhi,
|
|
remark: circuitDeviceRows.remark,
|
|
overriddenFields: circuitDeviceRows.overriddenFields,
|
|
equipmentIdentifier: circuits.equipmentIdentifier,
|
|
circuitDisplayName: circuits.displayName,
|
|
circuitListId: circuitLists.id,
|
|
circuitListName: circuitLists.name,
|
|
distributionBoardId: distributionBoards.id,
|
|
distributionBoardName: distributionBoards.name,
|
|
})
|
|
.from(circuitDeviceRows)
|
|
.innerJoin(circuits, eq(circuitDeviceRows.circuitId, circuits.id))
|
|
.innerJoin(circuitLists, eq(circuits.circuitListId, circuitLists.id))
|
|
.innerJoin(distributionBoards, eq(circuitLists.distributionBoardId, distributionBoards.id))
|
|
.where(
|
|
and(
|
|
eq(circuitDeviceRows.linkedProjectDeviceId, projectDeviceId),
|
|
eq(circuitLists.projectId, projectId)
|
|
)
|
|
)
|
|
.orderBy(asc(distributionBoards.name), asc(circuits.equipmentIdentifier), asc(circuitDeviceRows.sortOrder));
|
|
}
|
|
|
|
async listLinkStatesByIds(projectId: string, rowIds: string[]) {
|
|
if (rowIds.length === 0) {
|
|
return [];
|
|
}
|
|
return db
|
|
.select({
|
|
id: circuitDeviceRows.id,
|
|
linkedProjectDeviceId: circuitDeviceRows.linkedProjectDeviceId,
|
|
})
|
|
.from(circuitDeviceRows)
|
|
.innerJoin(circuits, eq(circuitDeviceRows.circuitId, circuits.id))
|
|
.innerJoin(circuitLists, eq(circuits.circuitListId, circuitLists.id))
|
|
.where(and(eq(circuitLists.projectId, projectId), inArray(circuitDeviceRows.id, rowIds)));
|
|
}
|
|
|
|
async create(input: {
|
|
circuitId: 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;
|
|
}) {
|
|
const id = crypto.randomUUID();
|
|
await db.insert(circuitDeviceRows).values({
|
|
id,
|
|
circuitId: input.circuitId,
|
|
linkedProjectDeviceId: input.linkedProjectDeviceId ?? null,
|
|
legacyConsumerId: input.legacyConsumerId ?? null,
|
|
sortOrder: input.sortOrder,
|
|
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,
|
|
});
|
|
return id;
|
|
}
|
|
|
|
async update(
|
|
rowId: string,
|
|
input: CircuitDeviceRowUpdateInput
|
|
) {
|
|
await db
|
|
.update(circuitDeviceRows)
|
|
.set(toUpdateValues(input))
|
|
.where(eq(circuitDeviceRows.id, rowId));
|
|
}
|
|
|
|
updateLinkedRowsTransactional(
|
|
projectDeviceId: string,
|
|
changes: Array<{ rowId: string; input: CircuitDeviceRowUpdateInput }>
|
|
) {
|
|
db.transaction((tx) => {
|
|
for (const change of changes) {
|
|
const result = tx
|
|
.update(circuitDeviceRows)
|
|
.set(toUpdateValues(change.input))
|
|
.where(
|
|
and(
|
|
eq(circuitDeviceRows.id, change.rowId),
|
|
eq(circuitDeviceRows.linkedProjectDeviceId, projectDeviceId)
|
|
)
|
|
)
|
|
.run();
|
|
if (result.changes !== 1) {
|
|
throw new Error("A linked row changed before the operation could be completed.");
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
disconnectLinkedRowsTransactional(projectDeviceId: string, rowIds: string[]) {
|
|
db.transaction((tx) => {
|
|
for (const rowId of rowIds) {
|
|
const result = tx
|
|
.update(circuitDeviceRows)
|
|
.set({ linkedProjectDeviceId: null })
|
|
.where(
|
|
and(
|
|
eq(circuitDeviceRows.id, rowId),
|
|
eq(circuitDeviceRows.linkedProjectDeviceId, projectDeviceId)
|
|
)
|
|
)
|
|
.run();
|
|
if (result.changes !== 1) {
|
|
throw new Error("A linked row changed before the operation could be completed.");
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
reconnectRowsTransactional(projectDeviceId: string, rowIds: string[]) {
|
|
db.transaction((tx) => {
|
|
for (const rowId of rowIds) {
|
|
const result = tx
|
|
.update(circuitDeviceRows)
|
|
.set({ linkedProjectDeviceId: projectDeviceId })
|
|
.where(and(eq(circuitDeviceRows.id, rowId), isNull(circuitDeviceRows.linkedProjectDeviceId)))
|
|
.run();
|
|
if (result.changes !== 1) {
|
|
throw new Error("A disconnected row changed before the operation could be undone.");
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
async delete(rowId: string) {
|
|
await db.delete(circuitDeviceRows).where(eq(circuitDeviceRows.id, rowId));
|
|
}
|
|
|
|
async moveToCircuit(rowId: string, targetCircuitId: string, sortOrder: number) {
|
|
await db
|
|
.update(circuitDeviceRows)
|
|
.set({
|
|
circuitId: targetCircuitId,
|
|
sortOrder,
|
|
})
|
|
.where(eq(circuitDeviceRows.id, rowId));
|
|
}
|
|
|
|
moveRowsTransactional(input: CircuitDeviceRowsBulkMoveInput) {
|
|
if (input.rows.length === 0) {
|
|
throw new Error("Keine Gerätezeilen zum Verschieben angegeben.");
|
|
}
|
|
if (Boolean(input.targetCircuitId) === Boolean(input.createTargetCircuit)) {
|
|
throw new Error("Für die Mehrfachverschiebung muss genau ein Ziel angegeben werden.");
|
|
}
|
|
|
|
const rowIds = input.rows.map((row) => row.id);
|
|
if (new Set(rowIds).size !== rowIds.length) {
|
|
throw new Error("Gerätezeilen dürfen nicht mehrfach ausgewählt sein.");
|
|
}
|
|
|
|
const createdCircuitId = input.createTargetCircuit ? crypto.randomUUID() : undefined;
|
|
const targetCircuitId = input.targetCircuitId ?? createdCircuitId!;
|
|
|
|
// better-sqlite3 transactions must remain synchronous. Every row assignment,
|
|
// reserve-state update and optional target creation is committed or rolled back
|
|
// as one operation.
|
|
db.transaction((tx) => {
|
|
const persistedRows = tx
|
|
.select({ id: circuitDeviceRows.id, circuitId: circuitDeviceRows.circuitId })
|
|
.from(circuitDeviceRows)
|
|
.where(inArray(circuitDeviceRows.id, rowIds))
|
|
.all();
|
|
if (persistedRows.length !== input.rows.length) {
|
|
throw new Error("Eine oder mehrere Gerätezeilen sind ungültig.");
|
|
}
|
|
|
|
const persistedRowsById = new Map(persistedRows.map((row) => [row.id, row]));
|
|
for (const expectedRow of input.rows) {
|
|
if (persistedRowsById.get(expectedRow.id)?.circuitId !== expectedRow.expectedCircuitId) {
|
|
throw new Error("Eine Gerätezeile wurde vor Abschluss der Verschiebung verändert.");
|
|
}
|
|
}
|
|
|
|
let targetCircuit: { id: string; circuitListId: string };
|
|
if (input.createTargetCircuit) {
|
|
tx
|
|
.insert(circuits)
|
|
.values({
|
|
id: targetCircuitId,
|
|
circuitListId: input.createTargetCircuit.circuitListId,
|
|
sectionId: input.createTargetCircuit.sectionId,
|
|
equipmentIdentifier: input.createTargetCircuit.equipmentIdentifier,
|
|
displayName: input.createTargetCircuit.displayName,
|
|
sortOrder: input.createTargetCircuit.sortOrder,
|
|
isReserve: 0,
|
|
})
|
|
.run();
|
|
targetCircuit = {
|
|
id: targetCircuitId,
|
|
circuitListId: input.createTargetCircuit.circuitListId,
|
|
};
|
|
} else {
|
|
const [persistedTargetCircuit] = tx
|
|
.select({ id: circuits.id, circuitListId: circuits.circuitListId })
|
|
.from(circuits)
|
|
.where(eq(circuits.id, targetCircuitId))
|
|
.limit(1)
|
|
.all();
|
|
if (!persistedTargetCircuit) {
|
|
throw new Error("Der Zielstromkreis ist ungültig.");
|
|
}
|
|
targetCircuit = persistedTargetCircuit;
|
|
}
|
|
|
|
const sourceCircuitIds = [...new Set(input.rows.map((row) => row.expectedCircuitId))];
|
|
const sourceCircuits = tx
|
|
.select({ id: circuits.id, circuitListId: circuits.circuitListId })
|
|
.from(circuits)
|
|
.where(inArray(circuits.id, sourceCircuitIds))
|
|
.all();
|
|
if (sourceCircuits.length !== sourceCircuitIds.length) {
|
|
throw new Error("Einer oder mehrere Quellstromkreise sind ungültig.");
|
|
}
|
|
if (sourceCircuits.some((circuit) => circuit.circuitListId !== targetCircuit.circuitListId)) {
|
|
throw new Error("Alle verschobenen Zeilen müssen zur Stromkreisliste des Ziels gehören.");
|
|
}
|
|
|
|
const movedRowIdSet = new Set(rowIds);
|
|
const retainedTargetRows = tx
|
|
.select({ id: circuitDeviceRows.id, sortOrder: circuitDeviceRows.sortOrder })
|
|
.from(circuitDeviceRows)
|
|
.where(eq(circuitDeviceRows.circuitId, targetCircuitId))
|
|
.all()
|
|
.filter((row) => !movedRowIdSet.has(row.id));
|
|
const lastTargetSortOrder = retainedTargetRows.reduce(
|
|
(highest, row) => Math.max(highest, row.sortOrder),
|
|
0
|
|
);
|
|
|
|
for (let index = 0; index < input.rows.length; index += 1) {
|
|
const row = input.rows[index];
|
|
const result = tx
|
|
.update(circuitDeviceRows)
|
|
.set({
|
|
circuitId: targetCircuitId,
|
|
sortOrder: lastTargetSortOrder + (index + 1) * 10,
|
|
})
|
|
.where(
|
|
and(
|
|
eq(circuitDeviceRows.id, row.id),
|
|
eq(circuitDeviceRows.circuitId, row.expectedCircuitId)
|
|
)
|
|
)
|
|
.run();
|
|
if (result.changes !== 1) {
|
|
throw new Error("Eine Gerätezeile wurde vor Abschluss der Verschiebung verändert.");
|
|
}
|
|
}
|
|
|
|
for (const sourceCircuitId of sourceCircuitIds) {
|
|
const remainingRows = tx
|
|
.select({ id: circuitDeviceRows.id })
|
|
.from(circuitDeviceRows)
|
|
.where(eq(circuitDeviceRows.circuitId, sourceCircuitId))
|
|
.all();
|
|
tx
|
|
.update(circuits)
|
|
.set({ isReserve: remainingRows.length === 0 ? 1 : 0 })
|
|
.where(eq(circuits.id, sourceCircuitId))
|
|
.run();
|
|
}
|
|
tx.update(circuits).set({ isReserve: 0 }).where(eq(circuits.id, targetCircuitId)).run();
|
|
});
|
|
|
|
return {
|
|
movedRowIds: rowIds,
|
|
targetCircuitId,
|
|
createdCircuitId,
|
|
};
|
|
}
|
|
}
|