forked from jappel/leistungsbilanz-ts
Add Linear XLSX initial import
This commit is contained in:
parent
c633e78cd4
commit
bfdd048269
20 changed files with 932 additions and 85 deletions
|
|
@ -23,6 +23,7 @@ import { ProjectSettingsProjectCommandRepository } from "../../db/repositories/p
|
|||
import { ProjectStateRestoreCommandRepository } from "../../db/repositories/project-state-restore-command.repository.js";
|
||||
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 { 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";
|
||||
|
|
@ -72,6 +73,8 @@ export const projectStateRestoreCommandStore =
|
|||
export const projectHistoryStore = new ProjectHistoryRepository(db);
|
||||
export const externalModelConfigurationProjectCommandStore =
|
||||
new ExternalModelConfigurationProjectCommandRepository(db);
|
||||
export const externalInitialImportProjectCommandStore =
|
||||
new ExternalInitialImportProjectCommandRepository(db);
|
||||
export const externalObjectRowAssignmentProjectCommandStore =
|
||||
new ExternalObjectRowAssignmentProjectCommandRepository(db);
|
||||
export const externalObjectNewRowProjectCommandStore =
|
||||
|
|
@ -107,5 +110,6 @@ export const projectCommandService = new ProjectCommandService(
|
|||
externalObjectRowAssignmentProjectCommandStore,
|
||||
externalObjectNewRowProjectCommandStore,
|
||||
externalObjectNewCircuitProjectCommandStore,
|
||||
estimateBalanceProjectCommandStore
|
||||
estimateBalanceProjectCommandStore,
|
||||
externalInitialImportProjectCommandStore
|
||||
);
|
||||
|
|
|
|||
68
src/server/controllers/external-initial-import.controller.ts
Normal file
68
src/server/controllers/external-initial-import.controller.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { randomUUID } from "node:crypto";
|
||||
import type { Request, Response } from "express";
|
||||
import { createEmptyExternalModelState, createExternalInitialImportProjectCommand } from "../../domain/models/external-initial-import-project-command.model.js";
|
||||
import { createExternalXlsxInitialImportPlan } from "../../external-model/application/external-xlsx-initial-import.js";
|
||||
import { buildExternalXlsxInitialImportTarget } from "../../external-model/application/external-xlsx-initial-import-target.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 { respondWithProjectCommandError } from "./project-command.controller.js";
|
||||
|
||||
export function getExternalInitialImportStatus(req: Request, res: Response) {
|
||||
const projectId = String(req.params.projectId ?? "");
|
||||
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 });
|
||||
}
|
||||
|
||||
export async function planExternalInitialImport(req: Request, res: Response) {
|
||||
const projectId = String(req.params.projectId ?? "");
|
||||
try {
|
||||
const expectedRevision = req.body?.expectedRevision;
|
||||
if (!Number.isSafeInteger(expectedRevision) || expectedRevision < 0) throw new Error("Ungültige Projektrevision.");
|
||||
const stored = externalModelConfigurationRepository.getByProject(projectId);
|
||||
if (!stored.projectExists) return res.status(404).json({ error: "Projekt nicht gefunden." });
|
||||
if (!stored.configuration) return res.status(409).json({ error: "Für das Projekt ist noch keine Revit-XLSX-Konfiguration gespeichert.", code: "EXTERNAL_XLSX_CONFIGURATION_REQUIRED" });
|
||||
const project = await projectRepository.findById(projectId);
|
||||
if (!project || project.currentRevision !== expectedRevision) return res.status(409).json({ error: "Der Erstimportplan ist wegen einer neueren Projektrevision ungültig.", code: "PROJECT_REVISION_CONFLICT", expectedRevision, currentRevision: project?.currentRevision ?? null });
|
||||
if (externalModelStateRepository.getByProject(projectId).state.source) return res.status(409).json({ error: "Für dieses Projekt wurde bereits ein Revit-Modell importiert.", code: "EXTERNAL_INITIAL_IMPORT_ALREADY_APPLIED" });
|
||||
const configuration = req.body?.configuration ?? stored.configuration.configuration;
|
||||
assertExternalModelConfiguration(configuration);
|
||||
const { plan } = await createExternalXlsxInitialImportPlan({ fileName: requiredString(req.body?.fileName, "Dateiname"), bytes: decodeBase64(req.body?.contentBase64), configuration, catalogs: await loadCatalogs(projectId) });
|
||||
return res.json(plan);
|
||||
} catch (error) {
|
||||
return res.status(400).json({ error: error instanceof Error ? error.message : "Erstimport-Vorschau fehlgeschlagen." });
|
||||
}
|
||||
}
|
||||
|
||||
export async function applyExternalInitialImport(req: Request, res: Response) {
|
||||
const projectId = String(req.params.projectId ?? "");
|
||||
try {
|
||||
const expectedRevision = req.body?.expectedRevision;
|
||||
if (!Number.isSafeInteger(expectedRevision) || expectedRevision < 0) throw new Error("Ungültige Projektrevision.");
|
||||
const stored = externalModelConfigurationRepository.getByProject(projectId);
|
||||
if (!stored.projectExists) return res.status(404).json({ error: "Projekt nicht gefunden." });
|
||||
if (!stored.configuration) return res.status(409).json({ error: "Für das Projekt ist noch keine Revit-XLSX-Konfiguration gespeichert.", code: "EXTERNAL_XLSX_CONFIGURATION_REQUIRED" });
|
||||
if (externalModelStateRepository.getByProject(projectId).state.source) return res.status(409).json({ error: "Für dieses Projekt wurde bereits ein Revit-Modell importiert.", code: "EXTERNAL_INITIAL_IMPORT_ALREADY_APPLIED" });
|
||||
assertExternalModelConfiguration(req.body?.configuration);
|
||||
const bytes = decodeBase64(req.body?.contentBase64);
|
||||
const { plan, document } = await createExternalXlsxInitialImportPlan({ fileName: requiredString(req.body?.fileName, "Dateiname"), bytes, configuration: req.body.configuration, catalogs: await loadCatalogs(projectId) });
|
||||
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 target = buildExternalXlsxInitialImportTarget({ projectId, expectedRevision, sourceName: requiredString(req.body?.sourceName, "Quellenname"), importedAtIso: new Date().toISOString(), configuration: req.body.configuration, originalContentBase64: req.body.contentBase64, document, plan, roomDecisions: requireArray(req.body?.roomDecisions, "Raumentscheidungen"), familyProjectDeviceDecisions: requireArray(req.body?.familyProjectDeviceDecisions, "ProjectDevice-Entscheidungen"), objectsWithoutRoomDefaultDistributionBoardId: nullableString(req.body?.objectsWithoutRoomDefaultDistributionBoardId, "Standardverteiler ohne Raum"), createId: randomUUID });
|
||||
const targetConfiguration = { id: stored.configuration.id, projectId, configuration: req.body.configuration };
|
||||
const result = projectCommandService.executeUser({ projectId, expectedRevision, description: `Revit-Erstimport ${plan.fileName}`, command: createExternalInitialImportProjectCommand(stored.configuration, targetConfiguration, createEmptyExternalModelState(), target) });
|
||||
return res.status(201).json({ ...result, import: { sourceId: target.source!.id, importBatchId: target.importBatches[0].id, objectCount: target.objects.length } });
|
||||
} catch (error) {
|
||||
return respondWithProjectCommandError(error, res);
|
||||
}
|
||||
}
|
||||
|
||||
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 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[]; }
|
||||
|
|
@ -58,6 +58,11 @@ import {
|
|||
detectExternalModelWorkbook,
|
||||
updateExternalModelConfiguration,
|
||||
} from "../controllers/external-model-configuration.controller.js";
|
||||
import {
|
||||
applyExternalInitialImport,
|
||||
getExternalInitialImportStatus,
|
||||
planExternalInitialImport,
|
||||
} from "../controllers/external-initial-import.controller.js";
|
||||
|
||||
export const projectRouter = Router();
|
||||
|
||||
|
|
@ -82,6 +87,9 @@ projectRouter.put("/:projectId", updateProjectSettings);
|
|||
projectRouter.get("/:projectId/external-model/configuration", getExternalModelConfiguration);
|
||||
projectRouter.put("/:projectId/external-model/configuration", updateExternalModelConfiguration);
|
||||
projectRouter.post("/:projectId/external-model/configuration/detect", detectExternalModelWorkbook);
|
||||
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.get("/:projectId/distribution-boards", listDistributionBoardsByProject);
|
||||
projectRouter.post("/:projectId/distribution-boards", createDistributionBoard);
|
||||
projectRouter.post(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue