import { createProjectDeviceRowSyncProjectCommand, projectDeviceSyncRowSnapshotFields, type ProjectDeviceRowSyncProjectCommand, type ProjectDeviceSyncRowSnapshot, } from "../models/project-device-row-sync-project-command.model.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"; export type ProjectDeviceSyncSource = { id: string; } & Pick; export type LinkedProjectDeviceRow = ProjectDeviceSyncRowSnapshot & { id: string; circuitId: string; equipmentIdentifier: string; circuitDisplayName: string | null; circuitListId: string; circuitListName: string; distributionBoardId: string; distributionBoardName: string; }; export interface ProjectDeviceSyncSourceReader { findById( projectId: string, projectDeviceId: string ): Promise; } export interface LinkedProjectDeviceRowReader { listLinkedByProjectDevice( projectId: string, projectDeviceId: string ): Promise; } export interface ProjectDeviceSyncDependencies { projectDeviceRepository: ProjectDeviceSyncSourceReader; deviceRowRepository: LinkedProjectDeviceRowReader; } function sourceValue( projectDevice: ProjectDeviceSyncSource, field: ProjectDeviceSyncField ) { return projectDevice[field]; } function valuesEqual(left: unknown, right: unknown) { return (left ?? null) === (right ?? null); } function buildDifferences( projectDevice: ProjectDeviceSyncSource, row: LinkedProjectDeviceRow ) { 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: ProjectDeviceSyncSourceReader; private readonly deviceRowRepository: LinkedProjectDeviceRowReader; constructor(deps: ProjectDeviceSyncDependencies) { this.projectDeviceRepository = deps.projectDeviceRepository; this.deviceRowRepository = deps.deviceRowRepository; } 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 createSynchronizeCommand( projectId: string, projectDeviceId: string, rowIds: string[], fields: ProjectDeviceSyncField[] ): Promise { 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 assignments = selectedRows.flatMap((row) => { const expected = toSyncSnapshot(row); const target = { ...expected }; for (const field of selectedFields) { Object.assign(target, { [field]: sourceValue(projectDevice, field) ?? null, }); } const remainingOverrides = parseOverriddenFields(row.overriddenFields).filter( (field) => !selectedFields.includes(field) ); target.overriddenFields = serializeOverriddenFields(remainingOverrides) ?? null; return snapshotsEqual(expected, target) ? [] : [{ rowId: row.id, expected, target }]; }); return createProjectDeviceRowSyncProjectCommand( projectDeviceId, "synchronize", assignments ); } async createDisconnectCommand( projectId: string, projectDeviceId: string, rowIds: string[] ): Promise { 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); return createProjectDeviceRowSyncProjectCommand( projectDeviceId, "disconnect", selectedRows.map((row) => { const expected = toSyncSnapshot(row); return { rowId: row.id, expected, target: { ...expected, linkedProjectDeviceId: null, }, }; }) ); } private resolveSelectedRows( linkedRows: LinkedProjectDeviceRow[], 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 LinkedProjectDeviceRow[]; if (selectedRows.length !== uniqueRowIds.length) { throw new Error("One or more rows are not linked to this project device."); } return selectedRows; } } function toSyncSnapshot( row: LinkedProjectDeviceRow ): ProjectDeviceSyncRowSnapshot { return { linkedProjectDeviceId: row.linkedProjectDeviceId, name: row.name, displayName: row.displayName, phaseType: row.phaseType, connectionKind: row.connectionKind, costGroup: row.costGroup, category: row.category, quantity: row.quantity, powerPerUnit: row.powerPerUnit, simultaneityFactor: row.simultaneityFactor, cosPhi: row.cosPhi, remark: row.remark, overriddenFields: row.overriddenFields, }; } function snapshotsEqual( left: ProjectDeviceSyncRowSnapshot, right: ProjectDeviceSyncRowSnapshot ) { return projectDeviceSyncRowSnapshotFields.every( (field) => left[field] === right[field] ); }