import { CircuitDeviceRowRepository } from "../../db/repositories/circuit-device-row.repository.js"; import { ProjectDeviceRepository } from "../../db/repositories/project-device.repository.js"; import { projectDeviceSyncFields, type ProjectDeviceSyncField, } from "../../shared/constants/project-device-sync-fields.js"; type ProjectDevice = NonNullable>>; type LinkedRow = Awaited>[number]; type SyncDependencies = { projectDeviceRepository: Pick; deviceRowRepository: Pick; }; export function parseOverriddenFields(value: string | null | undefined): ProjectDeviceSyncField[] { if (!value) { return []; } try { const parsed: unknown = JSON.parse(value); if (!Array.isArray(parsed)) { return []; } return parsed.filter((field): field is ProjectDeviceSyncField => projectDeviceSyncFields.includes(field as ProjectDeviceSyncField) ); } catch { return []; } } export function serializeOverriddenFields(fields: Iterable): string | undefined { const fieldSet = new Set(fields); const unique = projectDeviceSyncFields.filter((field) => fieldSet.has(field)); return unique.length > 0 ? JSON.stringify(unique) : undefined; } 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"]; constructor(deps?: Partial) { this.projectDeviceRepository = deps?.projectDeviceRepository ?? new ProjectDeviceRepository(); this.deviceRowRepository = deps?.deviceRowRepository ?? new CircuitDeviceRowRepository(); } 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)]; 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); await this.deviceRowRepository.update(row.id, next); } return this.getPreview(projectId, projectDeviceId); } async disconnect(projectId: string, projectDeviceId: string, rowIds: string[]) { const linkedRows = await this.deviceRowRepository.listLinkedByProjectDevice(projectId, projectDeviceId); const selectedRows = this.resolveSelectedRows(linkedRows, rowIds); for (const row of selectedRows) { await this.deviceRowRepository.update(row.id, { ...this.copyRow(row), linkedProjectDeviceId: undefined, }); } return { disconnectedRowIds: selectedRows.map((row) => row.id) }; } 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, }; } }