Add Revit return workbook export
This commit is contained in:
parent
77d775fde1
commit
1a4c8cad8c
17 changed files with 3783 additions and 6 deletions
|
|
@ -15,6 +15,7 @@ import { ProjectRepository } from "../../db/repositories/project.repository.js";
|
|||
import { RoomRepository } from "../../db/repositories/room.repository.js";
|
||||
import { ExternalModelConfigurationRepository } from "../../db/repositories/external-model-configuration.repository.js";
|
||||
import { ExternalModelStateRepository } from "../../db/repositories/external-model-state.repository.js";
|
||||
import { ExternalExportBatchRepository } from "../../db/repositories/external-export-batch.repository.js";
|
||||
import { DocumentBrandingRepository } from "../../db/repositories/document-branding.repository.js";
|
||||
import { UserRepository } from "../../db/repositories/user.repository.js";
|
||||
|
||||
|
|
@ -39,5 +40,6 @@ export const roomRepository = new RoomRepository(db);
|
|||
export const externalModelConfigurationRepository =
|
||||
new ExternalModelConfigurationRepository(db);
|
||||
export const externalModelStateRepository = new ExternalModelStateRepository(db);
|
||||
export const externalExportBatchRepository = new ExternalExportBatchRepository(db);
|
||||
export const documentBrandingRepository = new DocumentBrandingRepository(db);
|
||||
export const userRepository = new UserRepository(db);
|
||||
|
|
|
|||
180
src/server/controllers/external-return-workbook.controller.ts
Normal file
180
src/server/controllers/external-return-workbook.controller.ts
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
import { createHash, randomUUID } from "node:crypto";
|
||||
import type { Request, Response } from "express";
|
||||
import type { CircuitGroupCategory } from "../../shared/constants/circuit-group.js";
|
||||
import type { ExternalExportScope } from "../../db/schema/external-export-batches.js";
|
||||
import { projectExternalXlsxReturn, type ExternalReturnRowContext } from "../../external-model/application/external-xlsx-return-projection.js";
|
||||
import { createExternalXlsxReturn } from "../../external-model/xlsx/external-xlsx-transport.js";
|
||||
import {
|
||||
externalExportBatchRepository,
|
||||
externalModelStateRepository,
|
||||
projectRepository,
|
||||
} from "../composition/application-repositories.js";
|
||||
import { buildProjectCircuitTrees } from "./circuit-tree.controller.js";
|
||||
|
||||
const exportScopes = new Set<ExternalExportScope>(["all", "deviating", "selected"]);
|
||||
|
||||
export async function createExternalReturnWorkbook(req: Request, res: Response) {
|
||||
const projectId = String(req.params.projectId ?? "");
|
||||
try {
|
||||
const expectedRevision = parseExpectedRevision(req.body?.expectedRevision);
|
||||
const scope = parseScope(req.body?.scope);
|
||||
const selectedObjectIds = scope === "selected" ? parseSelectedIds(req.body?.selectedObjectIds) : [];
|
||||
if (scope === "selected" && selectedObjectIds.length === 0) {
|
||||
return res.status(400).json({ error: "Für die ausgewählte Rückgabe wurden keine Revit-Objekte markiert." });
|
||||
}
|
||||
|
||||
const project = await projectRepository.findById(projectId);
|
||||
if (!project) return res.status(404).json({ error: "Projekt nicht gefunden." });
|
||||
if (project.currentRevision !== expectedRevision) {
|
||||
return res.status(409).json({
|
||||
error: "Die Rückgabe ist wegen einer neueren Projektrevision ungültig.",
|
||||
code: "PROJECT_REVISION_CONFLICT",
|
||||
expectedRevision,
|
||||
currentRevision: project.currentRevision,
|
||||
});
|
||||
}
|
||||
|
||||
const { state } = externalModelStateRepository.getByProject(projectId);
|
||||
const importBatch = state.importBatches.at(-1);
|
||||
if (!importBatch?.originalContentBase64 || !importBatch.configurationSnapshot) {
|
||||
return res.status(409).json({
|
||||
error: "Für die Rückgabe fehlt der letzte vollständige Revit-Import.",
|
||||
code: "EXTERNAL_XLSX_IMPORT_REQUIRED",
|
||||
});
|
||||
}
|
||||
const treeResult = await buildProjectCircuitTrees(projectId);
|
||||
if (!treeResult || treeResult.currentRevision !== expectedRevision) {
|
||||
return res.status(409).json({
|
||||
error: "Die Rückgabe ist wegen einer neueren Projektrevision ungültig.",
|
||||
code: "PROJECT_REVISION_CONFLICT",
|
||||
});
|
||||
}
|
||||
|
||||
const rowById = new Map<string, ExternalReturnRowContext>();
|
||||
for (const tree of treeResult.trees) {
|
||||
for (const section of tree.sections) {
|
||||
for (const circuit of section.circuits) {
|
||||
for (const row of circuit.deviceRows) {
|
||||
rowById.set(row.id, {
|
||||
rowId: row.id,
|
||||
displayName: row.displayName,
|
||||
powerPerUnitKw: row.powerPerUnit,
|
||||
cosPhi: row.cosPhi ?? null,
|
||||
phaseType: row.phaseType ?? null,
|
||||
category: (section.category ?? null) as CircuitGroupCategory | null,
|
||||
circuitIdentifier: circuit.equipmentIdentifier,
|
||||
circuitVoltageV: circuit.voltage ?? null,
|
||||
distributionBoardName: tree.distributionBoardName,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const projections = state.objects
|
||||
.filter((object) => object.presenceStatus === "present" && object.lastSeenImportBatchId === importBatch.id)
|
||||
.map((object) => projectExternalXlsxReturn({
|
||||
object,
|
||||
row: object.circuitDeviceRowId ? rowById.get(object.circuitDeviceRowId) ?? null : null,
|
||||
circuitMode: importBatch.configurationSnapshot!.circuitMode,
|
||||
singlePhaseVoltageV: project.singlePhaseVoltageV,
|
||||
threePhaseVoltageV: project.threePhaseVoltageV,
|
||||
}));
|
||||
const projectionById = new Map(projections.map((projection) => [projection.objectId, projection]));
|
||||
for (const objectId of selectedObjectIds) {
|
||||
if (!projectionById.has(objectId)) {
|
||||
return res.status(400).json({ error: "Ein ausgewähltes Revit-Objekt gehört nicht zum letzten Import." });
|
||||
}
|
||||
}
|
||||
const selected = scope === "all"
|
||||
? projections
|
||||
: scope === "deviating"
|
||||
? projections.filter((projection) => projection.differsFromImported)
|
||||
: selectedObjectIds.map((id) => projectionById.get(id)!);
|
||||
if (selected.length === 0) {
|
||||
return res.status(400).json({ error: "Der gewählte Rückgabeumfang enthält keine Revit-Objekte." });
|
||||
}
|
||||
|
||||
const result = await createExternalXlsxReturn({
|
||||
fileName: importBatch.fileName,
|
||||
bytes: Buffer.from(importBatch.originalContentBase64, "base64"),
|
||||
configuration: importBatch.configurationSnapshot,
|
||||
selectedWorksharingIds: selected.map((projection) => projection.worksharingId),
|
||||
updates: selected.map((projection) => ({
|
||||
worksharingId: projection.worksharingId,
|
||||
values: projection.values,
|
||||
})),
|
||||
});
|
||||
const fileName = `${safeFileName(project.name)}_Revit-Rueckgabe_${new Date().toISOString().slice(0, 10)}.xlsx`;
|
||||
const sha256 = hash(result.bytes);
|
||||
const configurationSha256 = hash(Buffer.from(JSON.stringify(importBatch.configurationSnapshot), "utf8"));
|
||||
const currentProject = await projectRepository.findById(projectId);
|
||||
if (!currentProject || currentProject.currentRevision !== expectedRevision) {
|
||||
return res.status(409).json({
|
||||
error: "Die Rückgabe ist wegen einer neueren Projektrevision ungültig.",
|
||||
code: "PROJECT_REVISION_CONFLICT",
|
||||
expectedRevision,
|
||||
currentRevision: currentProject?.currentRevision ?? null,
|
||||
});
|
||||
}
|
||||
externalExportBatchRepository.append({
|
||||
id: randomUUID(),
|
||||
projectId,
|
||||
projectRevision: expectedRevision,
|
||||
importBatchId: importBatch.id,
|
||||
exportedAtIso: new Date().toISOString(),
|
||||
fileName,
|
||||
sha256,
|
||||
configurationSnapshot: importBatch.configurationSnapshot,
|
||||
configurationSha256,
|
||||
scope,
|
||||
objectCount: result.exportedObjectCount,
|
||||
});
|
||||
|
||||
const projectionWarnings = selected.flatMap((projection) =>
|
||||
projection.warnings.map((warning) => `${projection.worksharingId}: ${warning}`)
|
||||
);
|
||||
return res.json({
|
||||
fileName,
|
||||
contentBase64: Buffer.from(result.bytes).toString("base64"),
|
||||
sha256,
|
||||
objectCount: result.exportedObjectCount,
|
||||
warnings: [...new Set([...result.issues.map((issue) => issue.message), ...projectionWarnings])],
|
||||
});
|
||||
} catch (error) {
|
||||
return res.status(400).json({ error: error instanceof Error ? error.message : "Revit-Rückgabe fehlgeschlagen." });
|
||||
}
|
||||
}
|
||||
|
||||
export async function listExternalExportBatches(req: Request, res: Response) {
|
||||
const projectId = String(req.params.projectId ?? "");
|
||||
const project = await projectRepository.findById(projectId);
|
||||
if (!project) return res.status(404).json({ error: "Projekt nicht gefunden." });
|
||||
return res.json(externalExportBatchRepository.listByProject(projectId).map(({ configurationSnapshot: _configuration, ...batch }) => batch));
|
||||
}
|
||||
|
||||
function parseExpectedRevision(value: unknown) {
|
||||
if (!Number.isSafeInteger(value) || (value as number) < 0) throw new Error("Ungültige Projektrevision.");
|
||||
return value as number;
|
||||
}
|
||||
|
||||
function parseScope(value: unknown): ExternalExportScope {
|
||||
if (typeof value !== "string" || !exportScopes.has(value as ExternalExportScope)) throw new Error("Ungültiger Rückgabeumfang.");
|
||||
return value as ExternalExportScope;
|
||||
}
|
||||
|
||||
function parseSelectedIds(value: unknown) {
|
||||
if (value === undefined) return [];
|
||||
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string") || new Set(value).size !== value.length) {
|
||||
throw new Error("Ungültige Objektauswahl.");
|
||||
}
|
||||
return value as string[];
|
||||
}
|
||||
|
||||
function safeFileName(value: string) {
|
||||
return value.trim().replace(/[<>:"/\\|?*\u0000-\u001f]/g, "_") || "Projekt";
|
||||
}
|
||||
|
||||
function hash(bytes: Uint8Array) {
|
||||
return createHash("sha256").update(bytes).digest("hex");
|
||||
}
|
||||
|
|
@ -64,6 +64,7 @@ import {
|
|||
planExternalInitialImport,
|
||||
} from "../controllers/external-initial-import.controller.js";
|
||||
import { assignExternalObjectsToNewCircuit, assignExternalObjectsToNewRow, assignExternalObjectsToRow, detachExternalObjectsFromRow, listExternalObjects, updateExternalObjects } from "../controllers/external-objects.controller.js";
|
||||
import { createExternalReturnWorkbook, listExternalExportBatches } from "../controllers/external-return-workbook.controller.js";
|
||||
|
||||
export const projectRouter = Router();
|
||||
|
||||
|
|
@ -97,6 +98,8 @@ projectRouter.post("/:projectId/external-model/objects/assign-row", assignExtern
|
|||
projectRouter.post("/:projectId/external-model/objects/assign-new-row", assignExternalObjectsToNewRow);
|
||||
projectRouter.post("/:projectId/external-model/objects/assign-new-circuit", assignExternalObjectsToNewCircuit);
|
||||
projectRouter.post("/:projectId/external-model/objects/detach-row", detachExternalObjectsFromRow);
|
||||
projectRouter.post("/:projectId/external-model/return-workbook", createExternalReturnWorkbook);
|
||||
projectRouter.get("/:projectId/external-model/export-batches", listExternalExportBatches);
|
||||
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