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, }; } }