import { CircuitDeviceRowRepository } from "../../db/repositories/circuit-device-row.repository.js"; import { ProjectDeviceRepository } from "../../db/repositories/project-device.repository.js"; import type { ProjectDeviceRowSyncStore } from "../ports/project-device-row-sync.store.js"; import { projectDeviceSyncFields, type ProjectDeviceSyncField, } from "../../shared/constants/project-device-sync-fields.js"; import { parseOverriddenFields, serializeOverriddenFields, } from "./project-device-overrides.js"; export { parseOverriddenFields, serializeOverriddenFields, } from "./project-device-overrides.js"; type ProjectDevice = NonNullable>>; type LinkedRow = Awaited>[number]; type SyncDependencies = { projectDeviceRepository: Pick; deviceRowRepository: Pick< CircuitDeviceRowRepository, | "listLinkedByProjectDevice" | "listLinkStatesByIds" >; deviceRowSyncStore: ProjectDeviceRowSyncStore; }; export interface ProjectDeviceSyncRestoreRow { rowId: string; values: Partial>; overriddenFields: ProjectDeviceSyncField[]; } function sourceValue(projectDevice: ProjectDevice, field: ProjectDeviceSyncField) { return projectDevice[field]; } function valuesEqual(left: unknown, right: unknown) { return (left ?? null) === (right ?? null); } function buildDifferences(projectDevice: ProjectDevice, row: LinkedRow) { return projectDeviceSyncFields .filter((field) => !valuesEqual(row[field], sourceValue(projectDevice, field))) .map((field) => ({ field, currentValue: row[field] ?? null, sourceValue: sourceValue(projectDevice, field) ?? null, isOverridden: parseOverriddenFields(row.overriddenFields).includes(field), })); } export class ProjectDeviceSyncService { private readonly projectDeviceRepository: SyncDependencies["projectDeviceRepository"]; private readonly deviceRowRepository: SyncDependencies["deviceRowRepository"]; private readonly deviceRowSyncStore?: SyncDependencies["deviceRowSyncStore"]; constructor(deps?: Partial) { this.projectDeviceRepository = deps?.projectDeviceRepository ?? new ProjectDeviceRepository(); this.deviceRowRepository = deps?.deviceRowRepository ?? new CircuitDeviceRowRepository(); this.deviceRowSyncStore = deps?.deviceRowSyncStore; } private getDeviceRowSyncStore() { if (!this.deviceRowSyncStore) { throw new Error("Project-device row synchronization is not configured."); } return this.deviceRowSyncStore; } async getPreview(projectId: string, projectDeviceId: string) { const projectDevice = await this.projectDeviceRepository.findById(projectId, projectDeviceId); if (!projectDevice) { throw new Error("Project device not found."); } const rows = await this.deviceRowRepository.listLinkedByProjectDevice(projectId, projectDeviceId); return { projectDevice: { id: projectDevice.id, name: projectDevice.name, displayName: projectDevice.displayName, }, rows: rows.map((row) => ({ rowId: row.id, circuitId: row.circuitId, equipmentIdentifier: row.equipmentIdentifier, circuitDisplayName: row.circuitDisplayName, circuitListId: row.circuitListId, circuitListName: row.circuitListName, distributionBoardId: row.distributionBoardId, distributionBoardName: row.distributionBoardName, rowDisplayName: row.displayName, overriddenFields: parseOverriddenFields(row.overriddenFields), differences: buildDifferences(projectDevice, row), })), }; } async synchronize( projectId: string, projectDeviceId: string, rowIds: string[], fields: ProjectDeviceSyncField[] ) { const projectDevice = await this.projectDeviceRepository.findById(projectId, projectDeviceId); if (!projectDevice) { throw new Error("Project device not found."); } const linkedRows = await this.deviceRowRepository.listLinkedByProjectDevice(projectId, projectDeviceId); const selectedRows = this.resolveSelectedRows(linkedRows, rowIds); const selectedFields = [...new Set(fields)]; const undoRows: ProjectDeviceSyncRestoreRow[] = selectedRows.map((row) => ({ rowId: row.id, values: Object.fromEntries(selectedFields.map((field) => [field, row[field] ?? null])), overriddenFields: parseOverriddenFields(row.overriddenFields), })); const changes = []; for (const row of selectedRows) { const next = this.copyRow(row); for (const field of selectedFields) { Object.assign(next, { [field]: sourceValue(projectDevice, field) ?? undefined }); } const remainingOverrides = parseOverriddenFields(row.overriddenFields).filter( (field) => !selectedFields.includes(field) ); next.overriddenFields = serializeOverriddenFields(remainingOverrides); changes.push({ rowId: row.id, input: next }); } this.getDeviceRowSyncStore().updateLinkedRows(projectDeviceId, changes); return { preview: await this.getPreview(projectId, projectDeviceId), undo: { rows: undoRows }, }; } async disconnect(projectId: string, projectDeviceId: string, rowIds: string[]) { const linkedRows = await this.deviceRowRepository.listLinkedByProjectDevice(projectId, projectDeviceId); const selectedRows = this.resolveSelectedRows(linkedRows, rowIds); const disconnectedRowIds = selectedRows.map((row) => row.id); this.getDeviceRowSyncStore().disconnectLinkedRows( projectDeviceId, disconnectedRowIds ); return { disconnectedRowIds, undo: { rowIds: disconnectedRowIds } }; } async restore( projectId: string, projectDeviceId: string, restoreRows: ProjectDeviceSyncRestoreRow[] ) { const linkedRows = await this.deviceRowRepository.listLinkedByProjectDevice(projectId, projectDeviceId); const selectedRows = this.resolveSelectedRows(linkedRows, restoreRows.map((row) => row.rowId)); const restoreById = new Map(restoreRows.map((row) => [row.rowId, row])); const changes = selectedRows.map((row) => { const restore = restoreById.get(row.id)!; const next = this.copyRow(row); for (const [field, value] of Object.entries(restore.values) as Array< [ProjectDeviceSyncField, string | number | null] >) { Object.assign(next, { [field]: value ?? undefined }); } next.overriddenFields = serializeOverriddenFields(restore.overriddenFields); return { rowId: row.id, input: next }; }); this.getDeviceRowSyncStore().updateLinkedRows(projectDeviceId, changes); return this.getPreview(projectId, projectDeviceId); } async reconnect(projectId: string, projectDeviceId: string, rowIds: string[]) { const projectDevice = await this.projectDeviceRepository.findById(projectId, projectDeviceId); if (!projectDevice) { throw new Error("Project device not found."); } const uniqueRowIds = [...new Set(rowIds)]; const linkStates = await this.deviceRowRepository.listLinkStatesByIds(projectId, uniqueRowIds); if (linkStates.length !== uniqueRowIds.length || linkStates.some((row) => row.linkedProjectDeviceId !== null)) { throw new Error("One or more rows cannot be reconnected."); } this.getDeviceRowSyncStore().reconnectRows(projectDeviceId, uniqueRowIds); return this.getPreview(projectId, projectDeviceId); } private resolveSelectedRows(linkedRows: LinkedRow[], rowIds: string[]) { const uniqueRowIds = [...new Set(rowIds)]; const byId = new Map(linkedRows.map((row) => [row.id, row])); const selectedRows = uniqueRowIds.map((rowId) => byId.get(rowId)).filter(Boolean) as LinkedRow[]; if (selectedRows.length !== uniqueRowIds.length) { throw new Error("One or more rows are not linked to this project device."); } return selectedRows; } private copyRow(row: LinkedRow) { return { linkedProjectDeviceId: row.linkedProjectDeviceId ?? undefined, 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, }; } }