Files
leistungsbilanz-ts/src/domain/services/project-device-sync.service.ts
T

232 lines
7.0 KiB
TypeScript

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<ProjectDeviceSyncRowSnapshot, ProjectDeviceSyncField>;
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<ProjectDeviceSyncSource | null>;
}
export interface LinkedProjectDeviceRowReader {
listLinkedByProjectDevice(
projectId: string,
projectDeviceId: string
): Promise<LinkedProjectDeviceRow[]>;
}
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<ProjectDeviceRowSyncProjectCommand> {
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<ProjectDeviceRowSyncProjectCommand> {
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]
);
}