Add Linear XLSX follow-up import
This commit is contained in:
parent
1a4c8cad8c
commit
2581beee3c
28 changed files with 1114 additions and 31 deletions
|
|
@ -24,7 +24,9 @@ import { ProjectStateRestoreCommandRepository } from "../../db/repositories/proj
|
|||
import { ProjectCommandService } from "../../domain/services/project-command.service.js";
|
||||
import { ExternalModelConfigurationProjectCommandRepository } from "../../db/repositories/external-model-configuration-project-command.repository.js";
|
||||
import { ExternalInitialImportProjectCommandRepository } from "../../db/repositories/external-initial-import-project-command.repository.js";
|
||||
import { ExternalFollowUpImportProjectCommandRepository } from "../../db/repositories/external-follow-up-import-project-command.repository.js";
|
||||
import { ExternalObjectPlanningProjectCommandRepository } from "../../db/repositories/external-object-planning-project-command.repository.js";
|
||||
import { ExternalObjectDeleteMissingProjectCommandRepository } from "../../db/repositories/external-object-delete-missing-project-command.repository.js";
|
||||
import { ExternalObjectRowAssignmentProjectCommandRepository } from "../../db/repositories/external-object-row-assignment-project-command.repository.js";
|
||||
import { ExternalObjectNewRowProjectCommandRepository } from "../../db/repositories/external-object-new-row-project-command.repository.js";
|
||||
import { ExternalObjectNewCircuitProjectCommandRepository } from "../../db/repositories/external-object-new-circuit-project-command.repository.js";
|
||||
|
|
@ -76,8 +78,12 @@ export const externalModelConfigurationProjectCommandStore =
|
|||
new ExternalModelConfigurationProjectCommandRepository(db);
|
||||
export const externalInitialImportProjectCommandStore =
|
||||
new ExternalInitialImportProjectCommandRepository(db);
|
||||
export const externalFollowUpImportProjectCommandStore =
|
||||
new ExternalFollowUpImportProjectCommandRepository(db);
|
||||
export const externalObjectPlanningProjectCommandStore =
|
||||
new ExternalObjectPlanningProjectCommandRepository(db);
|
||||
export const externalObjectDeleteMissingProjectCommandStore =
|
||||
new ExternalObjectDeleteMissingProjectCommandRepository(db);
|
||||
export const externalObjectRowAssignmentProjectCommandStore =
|
||||
new ExternalObjectRowAssignmentProjectCommandRepository(db);
|
||||
export const externalObjectNewRowProjectCommandStore =
|
||||
|
|
@ -115,5 +121,7 @@ export const projectCommandService = new ProjectCommandService(
|
|||
externalObjectNewCircuitProjectCommandStore,
|
||||
estimateBalanceProjectCommandStore,
|
||||
externalInitialImportProjectCommandStore,
|
||||
externalObjectPlanningProjectCommandStore
|
||||
externalObjectPlanningProjectCommandStore,
|
||||
externalFollowUpImportProjectCommandStore,
|
||||
externalObjectDeleteMissingProjectCommandStore
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
import { randomUUID } from "node:crypto";
|
||||
import type { Request, Response } from "express";
|
||||
import type { CircuitDeviceRowSnapshot } from "../../domain/models/circuit-device-row-structure-project-command.model.js";
|
||||
import { createExternalFollowUpImportProjectCommand } from "../../domain/models/external-follow-up-import-project-command.model.js";
|
||||
import { createExternalXlsxFollowUpImportPlan } from "../../external-model/application/external-xlsx-follow-up-import.js";
|
||||
import { buildExternalXlsxFollowUpTarget } from "../../external-model/application/external-xlsx-follow-up-target.js";
|
||||
import { projectExternalXlsxReturn, type ExternalReturnRowContext } from "../../external-model/application/external-xlsx-return-projection.js";
|
||||
import { assertExternalModelConfiguration } from "../../external-model/configuration/external-model-configuration.js";
|
||||
import { distributionBoardRepository, externalModelConfigurationRepository, externalModelStateRepository, floorRepository, projectDeviceRepository, projectRepository, roomRepository } from "../composition/application-repositories.js";
|
||||
import { projectCommandService } from "../composition/project-command-stores.js";
|
||||
import { buildProjectCircuitTrees } from "./circuit-tree.controller.js";
|
||||
import { respondWithProjectCommandError } from "./project-command.controller.js";
|
||||
|
||||
export async function planExternalFollowUpImport(req: Request, res: Response) {
|
||||
const projectId = String(req.params.projectId ?? "");
|
||||
try {
|
||||
const expectedRevision = parseExpectedRevision(req.body?.expectedRevision);
|
||||
const context = await loadContext(projectId, expectedRevision, req.body?.configuration);
|
||||
const { plan } = await createExternalXlsxFollowUpImportPlan({
|
||||
fileName: requiredString(req.body?.fileName, "Dateiname"), bytes: decodeBase64(req.body?.contentBase64), configuration: context.configuration,
|
||||
catalogs: await loadCatalogs(projectId), currentState: context.state, currentReturnValuesByObjectId: context.returnValues,
|
||||
});
|
||||
return res.json(plan);
|
||||
} catch (error) { return res.status(400).json({ error: error instanceof Error ? error.message : "Folgeimport-Vorschau fehlgeschlagen." }); }
|
||||
}
|
||||
|
||||
export async function applyExternalFollowUpImport(req: Request, res: Response) {
|
||||
const projectId = String(req.params.projectId ?? "");
|
||||
try {
|
||||
const expectedRevision = parseExpectedRevision(req.body?.expectedRevision);
|
||||
const context = await loadContext(projectId, expectedRevision, req.body?.configuration);
|
||||
const bytes = decodeBase64(req.body?.contentBase64);
|
||||
const { plan, document } = await createExternalXlsxFollowUpImportPlan({
|
||||
fileName: requiredString(req.body?.fileName, "Dateiname"), bytes, configuration: context.configuration,
|
||||
catalogs: await loadCatalogs(projectId), currentState: context.state, currentReturnValuesByObjectId: context.returnValues,
|
||||
});
|
||||
if (plan.sha256 !== req.body?.expectedSha256) return res.status(409).json({ error: "Die XLSX-Datei stimmt nicht mehr mit der geprüften Vorschau überein.", code: "EXTERNAL_XLSX_FILE_CHANGED" });
|
||||
const built = buildExternalXlsxFollowUpTarget({
|
||||
projectId, expectedRevision, importedAtIso: new Date().toISOString(), configuration: context.configuration,
|
||||
originalContentBase64: String(req.body.contentBase64), document, plan, currentState: context.state,
|
||||
sourceChangeDecisions: requireArray(req.body?.sourceChangeDecisions, "Quelländerungsentscheidungen"),
|
||||
roomDecisions: requireArray(req.body?.roomDecisions, "Raumentscheidungen"),
|
||||
familyProjectDeviceDecisions: requireArray(req.body?.familyProjectDeviceDecisions, "ProjectDevice-Entscheidungen"),
|
||||
objectsWithoutRoomDefaultDistributionBoardId: nullableString(req.body?.objectsWithoutRoomDefaultDistributionBoardId, "Standardverteiler ohne Raum"),
|
||||
rowsById: context.rowsById, createId: randomUUID,
|
||||
});
|
||||
const targetConfiguration = { ...context.configurationSnapshot, configuration: context.configuration };
|
||||
const result = projectCommandService.executeUser({
|
||||
projectId, expectedRevision, description: `Revit-Folgeimport ${plan.fileName}`,
|
||||
command: createExternalFollowUpImportProjectCommand({ expectedConfiguration: context.configurationSnapshot, targetConfiguration, expectedState: context.state, targetState: built.targetState, rows: built.rowTransitions }),
|
||||
});
|
||||
return res.status(201).json({ ...result, import: { importBatchId: built.targetState.importBatches.at(-1)!.id, objectCount: plan.objectCount, newObjectCount: plan.newObjectCount, changedObjectCount: plan.changedObjectCount, missingObjectCount: plan.missingObjectCount } });
|
||||
} catch (error) { return respondWithProjectCommandError(error, res); }
|
||||
}
|
||||
|
||||
async function loadContext(projectId: string, expectedRevision: number, configurationValue: unknown) {
|
||||
const [project, treeResult] = await Promise.all([projectRepository.findById(projectId), buildProjectCircuitTrees(projectId)]);
|
||||
if (!project || !treeResult) throw new Error("Projekt nicht gefunden.");
|
||||
if (project.currentRevision !== expectedRevision || treeResult.currentRevision !== expectedRevision) throw new Error("Der Folgeimportplan ist wegen einer neueren Projektrevision ungültig.");
|
||||
const stored = externalModelConfigurationRepository.getByProject(projectId);
|
||||
if (!stored.configuration) throw new Error("Für das Projekt ist keine Revit-XLSX-Konfiguration gespeichert.");
|
||||
assertExternalModelConfiguration(configurationValue);
|
||||
const stateResult = externalModelStateRepository.getByProject(projectId);
|
||||
if (!stateResult.state.source) throw new Error("Für den Folgeimport fehlt ein bestehender Revit-Import.");
|
||||
const rowContexts = createRowContexts(treeResult);
|
||||
const returnValues = new Map(stateResult.state.objects.map((object) => [object.id, projectExternalXlsxReturn({ object, row: object.circuitDeviceRowId ? rowContexts.get(object.circuitDeviceRowId)?.returnContext ?? null : null, circuitMode: configurationValue.circuitMode, singlePhaseVoltageV: project.singlePhaseVoltageV, threePhaseVoltageV: project.threePhaseVoltageV }).values]));
|
||||
return { configuration: configurationValue, configurationSnapshot: stored.configuration, state: stateResult.state, returnValues, rowsById: new Map([...rowContexts].map(([id, value]) => [id, value.snapshot])) };
|
||||
}
|
||||
|
||||
function createRowContexts(treeResult: NonNullable<Awaited<ReturnType<typeof buildProjectCircuitTrees>>>) {
|
||||
const result = new Map<string, { returnContext: ExternalReturnRowContext; snapshot: CircuitDeviceRowSnapshot }>();
|
||||
for (const tree of treeResult.trees) for (const section of tree.sections) for (const circuit of section.circuits) for (const row of circuit.deviceRows) result.set(row.id, {
|
||||
returnContext: { rowId: row.id, displayName: row.displayName, powerPerUnitKw: row.powerPerUnit, cosPhi: row.cosPhi ?? null, phaseType: row.phaseType ?? null, category: section.category ?? null, circuitIdentifier: circuit.equipmentIdentifier, circuitVoltageV: circuit.voltage ?? null, distributionBoardName: tree.distributionBoardName },
|
||||
snapshot: { id: row.id, circuitId: circuit.id, linkedProjectDeviceId: row.linkedProjectDeviceId ?? null, sortOrder: row.sortOrder, name: row.name, displayName: row.displayName, phaseType: row.phaseType ?? null, connectionKind: row.connectionKind ?? null, costGroup: row.costGroup ?? null, category: row.category ?? null, level: row.level ?? null, roomId: row.roomId ?? null, roomNumberSnapshot: row.roomNumberSnapshot ?? null, roomNameSnapshot: row.roomNameSnapshot ?? null, quantity: row.quantity, manualQuantity: row.manualQuantity, powerPerUnit: row.powerPerUnit, simultaneityFactor: row.simultaneityFactor, cosPhi: row.cosPhi ?? null, remark: row.remark ?? null, overriddenFields: row.overriddenFields ?? null },
|
||||
});
|
||||
return result;
|
||||
}
|
||||
async function loadCatalogs(projectId: string) { const [floors, rooms, distributionBoards, projectDevices] = await Promise.all([floorRepository.listByProject(projectId), roomRepository.listByProject(projectId), distributionBoardRepository.listByProject(projectId), projectDeviceRepository.listByProject(projectId)]); return { floors, rooms, distributionBoards, projectDevices }; }
|
||||
function parseExpectedRevision(value: unknown) { if (!Number.isSafeInteger(value) || (value as number) < 0) throw new Error("Ungültige Projektrevision."); return value as number; }
|
||||
function decodeBase64(value: unknown) { if (typeof value !== "string" || !/^[A-Za-z0-9+/]*={0,2}$/.test(value) || value.length % 4 !== 0) throw new Error("XLSX-Inhalt ist nicht gültig Base64-kodiert."); const bytes = Buffer.from(value, "base64"); if (bytes.toString("base64") !== value) throw new Error("XLSX-Inhalt ist nicht kanonisch Base64-kodiert."); return bytes; }
|
||||
function requiredString(value: unknown, label: string) { if (typeof value !== "string" || !value.trim()) throw new Error(`${label} fehlt.`); return value.trim(); }
|
||||
function nullableString(value: unknown, label: string) { if (value === null || value === undefined || value === "") return null; if (typeof value !== "string") throw new Error(`${label} ist ungültig.`); return value; }
|
||||
function requireArray<T>(value: unknown, label: string): T[] { if (!Array.isArray(value)) throw new Error(`${label} fehlen.`); return value as T[]; }
|
||||
|
|
@ -13,7 +13,7 @@ export function getExternalInitialImportStatus(req: Request, res: Response) {
|
|||
const result = externalModelStateRepository.getByProject(projectId);
|
||||
if (!result.projectExists) return res.status(404).json({ error: "Projekt nicht gefunden." });
|
||||
const batch = result.state.importBatches.at(-1) ?? null;
|
||||
return res.json({ imported: result.state.source !== null, sourceName: result.state.source?.name ?? null, objectCount: result.state.objects.length, importedAtIso: batch?.importedAtIso ?? null, fileName: batch?.fileName ?? null });
|
||||
return res.json({ imported: result.state.source !== null, sourceName: result.state.source?.name ?? null, objectCount: batch?.objectCount ?? result.state.objects.length, missingObjectCount: result.state.objects.filter((object) => object.presenceStatus === "missing").length, importedAtIso: batch?.importedAtIso ?? null, fileName: batch?.fileName ?? null });
|
||||
}
|
||||
|
||||
export async function planExternalInitialImport(req: Request, res: Response) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import type { Request, Response } from "express";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { createExternalObjectPlanningUpdateProjectCommand } from "../../domain/models/external-object-planning-project-command.model.js";
|
||||
import { createExternalObjectDeleteMissingProjectCommand } from "../../domain/models/external-object-delete-missing-project-command.model.js";
|
||||
import { createExternalObjectRowAssignmentProjectCommand } from "../../domain/models/external-object-row-assignment-project-command.model.js";
|
||||
import type { CircuitDeviceRowSnapshot } from "../../domain/models/circuit-device-row-structure-project-command.model.js";
|
||||
import type { ExternalModelObjectPlanningValues } from "../../external-model/domain/external-model-contracts.js";
|
||||
|
|
@ -57,6 +58,39 @@ export function updateExternalObjects(req: Request, res: Response) {
|
|||
} catch (error) { return respondWithProjectCommandError(error, res); }
|
||||
}
|
||||
|
||||
export async function deleteMissingExternalObjects(req: Request, res: Response) {
|
||||
const projectId = String(req.params.projectId ?? "");
|
||||
try {
|
||||
const expectedRevision = parseExpectedRevision(req.body?.expectedRevision);
|
||||
const objectIds = parseObjectIds(req.body?.objectIds);
|
||||
if (req.body?.confirmed !== true) throw new Error("Das Löschen fehlender Revit-Objekte muss bestätigt werden.");
|
||||
const { state, projectExists } = externalModelStateRepository.getByProject(projectId);
|
||||
if (!projectExists) return res.status(404).json({ error: "Projekt nicht gefunden." });
|
||||
const byId = new Map(state.objects.map((object) => [object.id, object]));
|
||||
const selected = objectIds.map((id) => {
|
||||
const object = byId.get(id);
|
||||
if (!object || object.presenceStatus !== "missing") throw new Error("Nur im letzten Import fehlende Revit-Objekte dürfen gelöscht werden.");
|
||||
return object;
|
||||
});
|
||||
const treeResult = await buildProjectCircuitTrees(projectId);
|
||||
const quantitiesByRowId = new Map<string, number>();
|
||||
for (const object of selected) if (object.circuitDeviceRowId) quantitiesByRowId.set(object.circuitDeviceRowId, (quantitiesByRowId.get(object.circuitDeviceRowId) ?? 0) + object.planningValues.effectiveQuantity);
|
||||
const rows = [...quantitiesByRowId].map(([rowId, removedQuantity]) => {
|
||||
const location = findRow(treeResult, rowId);
|
||||
if (!location) throw new Error("Eine verknüpfte Gerätezeile wurde nicht gefunden.");
|
||||
const expected = toRowSnapshot(location.row, location.circuitId);
|
||||
return { expected, target: { ...expected, quantity: expected.quantity - removedQuantity } };
|
||||
});
|
||||
const result = projectCommandService.executeUser({
|
||||
projectId,
|
||||
expectedRevision,
|
||||
description: `${selected.length} fehlende Revit-Objekte gelöscht`,
|
||||
command: createExternalObjectDeleteMissingProjectCommand({ objects: selected.map((expected) => ({ expected, target: null })), rows }),
|
||||
});
|
||||
return res.json(result);
|
||||
} catch (error) { return respondWithProjectCommandError(error, res); }
|
||||
}
|
||||
|
||||
export async function assignExternalObjectsToRow(req: Request, res: Response) {
|
||||
const projectId = String(req.params.projectId ?? "");
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -63,8 +63,9 @@ import {
|
|||
getExternalInitialImportStatus,
|
||||
planExternalInitialImport,
|
||||
} from "../controllers/external-initial-import.controller.js";
|
||||
import { assignExternalObjectsToNewCircuit, assignExternalObjectsToNewRow, assignExternalObjectsToRow, detachExternalObjectsFromRow, listExternalObjects, updateExternalObjects } from "../controllers/external-objects.controller.js";
|
||||
import { assignExternalObjectsToNewCircuit, assignExternalObjectsToNewRow, assignExternalObjectsToRow, deleteMissingExternalObjects, detachExternalObjectsFromRow, listExternalObjects, updateExternalObjects } from "../controllers/external-objects.controller.js";
|
||||
import { createExternalReturnWorkbook, listExternalExportBatches } from "../controllers/external-return-workbook.controller.js";
|
||||
import { applyExternalFollowUpImport, planExternalFollowUpImport } from "../controllers/external-follow-up-import.controller.js";
|
||||
|
||||
export const projectRouter = Router();
|
||||
|
||||
|
|
@ -92,8 +93,11 @@ projectRouter.post("/:projectId/external-model/configuration/detect", detectExte
|
|||
projectRouter.get("/:projectId/external-model/import-status", getExternalInitialImportStatus);
|
||||
projectRouter.post("/:projectId/external-model/initial-import/plan", planExternalInitialImport);
|
||||
projectRouter.post("/:projectId/external-model/initial-import/apply", applyExternalInitialImport);
|
||||
projectRouter.post("/:projectId/external-model/follow-up-import/plan", planExternalFollowUpImport);
|
||||
projectRouter.post("/:projectId/external-model/follow-up-import/apply", applyExternalFollowUpImport);
|
||||
projectRouter.get("/:projectId/external-model/objects", listExternalObjects);
|
||||
projectRouter.put("/:projectId/external-model/objects", updateExternalObjects);
|
||||
projectRouter.post("/:projectId/external-model/objects/delete-missing", deleteMissingExternalObjects);
|
||||
projectRouter.post("/:projectId/external-model/objects/assign-row", assignExternalObjectsToRow);
|
||||
projectRouter.post("/:projectId/external-model/objects/assign-new-row", assignExternalObjectsToNewRow);
|
||||
projectRouter.post("/:projectId/external-model/objects/assign-new-circuit", assignExternalObjectsToNewCircuit);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue