Complete Revit object editor assignments
This commit is contained in:
parent
4489cd76f0
commit
77d775fde1
25 changed files with 853 additions and 77 deletions
|
|
@ -14,6 +14,7 @@ import {
|
|||
circuitSectionRepository,
|
||||
distributionBoardComponentRepository,
|
||||
distributionBoardRepository,
|
||||
externalModelStateRepository,
|
||||
projectRepository,
|
||||
} from "../composition/application-repositories.js";
|
||||
|
||||
|
|
@ -88,6 +89,13 @@ export async function buildProjectCircuitTrees(
|
|||
circuitListRepository.listByProject(projectId),
|
||||
distributionBoardRepository.listByProject(projectId),
|
||||
]);
|
||||
const externalState = externalModelStateRepository.getByProject(projectId).state;
|
||||
const externalCountByRowId = new Map<string, number>();
|
||||
for (const object of externalState.objects) {
|
||||
if (object.circuitDeviceRowId) {
|
||||
externalCountByRowId.set(object.circuitDeviceRowId, (externalCountByRowId.get(object.circuitDeviceRowId) ?? 0) + 1);
|
||||
}
|
||||
}
|
||||
const listIds = lists.map((entry) => entry.id);
|
||||
const [sections, circuits, components] = await Promise.all([
|
||||
circuitSectionRepository.listByCircuitLists(listIds),
|
||||
|
|
@ -237,6 +245,7 @@ export async function buildProjectCircuitTrees(
|
|||
row.powerPerUnit,
|
||||
row.simultaneityFactor
|
||||
),
|
||||
externalObjectCount: externalCountByRowId.get(row.id) ?? 0,
|
||||
}));
|
||||
const circuitLoad = loads.circuits.get(circuit.id);
|
||||
if (!circuitLoad) throw new Error("Circuit load is missing.");
|
||||
|
|
|
|||
|
|
@ -1,9 +1,17 @@
|
|||
import type { Request, Response } from "express";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { createExternalObjectPlanningUpdateProjectCommand } from "../../domain/models/external-object-planning-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";
|
||||
import { distributionBoardRepository, externalModelStateRepository, projectDeviceRepository, projectRepository } from "../composition/application-repositories.js";
|
||||
import { projectCommandService } from "../composition/project-command-stores.js";
|
||||
import { respondWithProjectCommandError } from "./project-command.controller.js";
|
||||
import { buildProjectCircuitTrees } from "./circuit-tree.controller.js";
|
||||
import { createExternalObjectNewRowProjectCommand, externalObjectAssignToNewRowCommandType } from "../../domain/models/external-object-new-row-project-command.model.js";
|
||||
import { createExternalObjectNewCircuitProjectCommand, externalObjectAssignToNewCircuitCommandType } from "../../domain/models/external-object-new-circuit-project-command.model.js";
|
||||
import { createDefaultCircuitProtection } from "../../domain/services/protection-device-defaults.js";
|
||||
import { circuitNumberingService } from "../composition/circuit-numbering-service.js";
|
||||
|
||||
export async function listExternalObjects(req: Request, res: Response) {
|
||||
const projectId = String(req.params.projectId ?? "");
|
||||
|
|
@ -49,6 +57,221 @@ export function updateExternalObjects(req: Request, res: Response) {
|
|||
} catch (error) { return respondWithProjectCommandError(error, res); }
|
||||
}
|
||||
|
||||
export async function assignExternalObjectsToRow(req: Request, res: Response) {
|
||||
const projectId = String(req.params.projectId ?? "");
|
||||
try {
|
||||
const expectedRevision = parseExpectedRevision(req.body?.expectedRevision);
|
||||
const objectIds = parseObjectIds(req.body?.objectIds);
|
||||
const rowId = requiredId(req.body?.rowId, "Ungültige Gerätezeile.");
|
||||
const confirmedConflictObjectIds = Array.isArray(req.body?.confirmedConflictObjectIds)
|
||||
? req.body.confirmedConflictObjectIds.map((value: unknown) => requiredId(value, "Ungültige Konfliktbestätigung."))
|
||||
: [];
|
||||
const { state, projectExists } = externalModelStateRepository.getByProject(projectId);
|
||||
if (!projectExists) return res.status(404).json({ error: "Projekt nicht gefunden." });
|
||||
const treeResult = await buildProjectCircuitTrees(projectId);
|
||||
const location = findRow(treeResult, rowId);
|
||||
if (!location) throw new Error("Gerätezeile wurde 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) throw new Error("Ein Revit-Objekt wurde nicht gefunden.");
|
||||
if (object.circuitDeviceRowId !== null) throw new Error("Ein Revit-Objekt ist bereits einer Gerätezeile zugeordnet.");
|
||||
return object;
|
||||
});
|
||||
const expectedRow = toRowSnapshot(location.row, location.circuitId);
|
||||
const addedQuantity = selected.reduce((sum, object) => sum + object.planningValues.effectiveQuantity, 0);
|
||||
const targetRow = { ...expectedRow, quantity: expectedRow.quantity + addedQuantity };
|
||||
const objects = selected.map((expected) => ({
|
||||
expected,
|
||||
target: { ...expected, distributionBoardId: location.distributionBoardId, circuitDeviceRowId: rowId },
|
||||
}));
|
||||
const result = projectCommandService.executeUser({
|
||||
projectId,
|
||||
expectedRevision,
|
||||
description: `${objects.length} Revit-Objekte zugeordnet`,
|
||||
command: createExternalObjectRowAssignmentProjectCommand({
|
||||
rows: [{ expected: expectedRow, target: targetRow }],
|
||||
objects,
|
||||
confirmedConflictObjectIds,
|
||||
}),
|
||||
});
|
||||
return res.json(result);
|
||||
} catch (error) { return respondWithProjectCommandError(error, res); }
|
||||
}
|
||||
|
||||
export async function detachExternalObjectsFromRow(req: Request, res: Response) {
|
||||
const projectId = String(req.params.projectId ?? "");
|
||||
try {
|
||||
const expectedRevision = parseExpectedRevision(req.body?.expectedRevision);
|
||||
const objectIds = parseObjectIds(req.body?.objectIds);
|
||||
const { state, projectExists } = externalModelStateRepository.getByProject(projectId);
|
||||
if (!projectExists) return res.status(404).json({ error: "Projekt nicht gefunden." });
|
||||
const treeResult = await buildProjectCircuitTrees(projectId);
|
||||
const byId = new Map(state.objects.map((object) => [object.id, object]));
|
||||
const selected = objectIds.map((id) => {
|
||||
const object = byId.get(id);
|
||||
if (!object?.circuitDeviceRowId) throw new Error("Ein Revit-Objekt ist keiner Gerätezeile zugeordnet.");
|
||||
return object;
|
||||
});
|
||||
const rowIds = new Set(selected.map((object) => object.circuitDeviceRowId!));
|
||||
if (rowIds.size !== 1) throw new Error("Revit-Objekte können nur aus derselben Gerätezeile gemeinsam gelöst werden.");
|
||||
const rowId = [...rowIds][0]!;
|
||||
const location = findRow(treeResult, rowId);
|
||||
if (!location) throw new Error("Gerätezeile wurde nicht gefunden.");
|
||||
const expectedRow = toRowSnapshot(location.row, location.circuitId);
|
||||
const removedQuantity = selected.reduce((sum, object) => sum + object.planningValues.effectiveQuantity, 0);
|
||||
const targetRow = { ...expectedRow, quantity: expectedRow.quantity - removedQuantity };
|
||||
const copiedFields = ["displayName", "category", "connectionKind", "powerPerUnitW", "simultaneityFactor", "cosPhi", "costGroup", "remark"] as const;
|
||||
const objects = selected.map((expected) => ({
|
||||
expected,
|
||||
target: {
|
||||
...expected,
|
||||
circuitDeviceRowId: null,
|
||||
distributionBoardId: location.distributionBoardId,
|
||||
linkedProjectDeviceId: expectedRow.linkedProjectDeviceId,
|
||||
planningValues: {
|
||||
...expected.planningValues,
|
||||
displayName: expectedRow.displayName,
|
||||
category: location.category,
|
||||
connectionKind: expectedRow.connectionKind,
|
||||
powerPerUnitW: expectedRow.powerPerUnit * 1000,
|
||||
simultaneityFactor: expectedRow.simultaneityFactor,
|
||||
cosPhi: expectedRow.cosPhi,
|
||||
costGroup: expectedRow.costGroup,
|
||||
remark: expectedRow.remark,
|
||||
},
|
||||
overriddenFields: [...new Set([...expected.overriddenFields, ...copiedFields])],
|
||||
},
|
||||
}));
|
||||
const result = projectCommandService.executeUser({
|
||||
projectId,
|
||||
expectedRevision,
|
||||
description: `${objects.length} Revit-Objekte gelöst`,
|
||||
command: createExternalObjectRowAssignmentProjectCommand({
|
||||
rows: [{ expected: expectedRow, target: targetRow }],
|
||||
objects,
|
||||
confirmedConflictObjectIds: [],
|
||||
}),
|
||||
});
|
||||
return res.json(result);
|
||||
} catch (error) { return respondWithProjectCommandError(error, res); }
|
||||
}
|
||||
|
||||
export async function assignExternalObjectsToNewRow(req: Request, res: Response) {
|
||||
const projectId = String(req.params.projectId ?? "");
|
||||
try {
|
||||
const expectedRevision = parseExpectedRevision(req.body?.expectedRevision);
|
||||
const objectIds = parseObjectIds(req.body?.objectIds);
|
||||
const circuitId = requiredId(req.body?.circuitId, "Ungültiger Stromkreis.");
|
||||
const context = await loadAssignmentContext(projectId, objectIds);
|
||||
const circuitLocation = context.treeResult?.trees.flatMap((tree) => tree.sections.flatMap((section) => section.circuits.map((circuit) => ({ tree, section, circuit })))).find((entry) => entry.circuit.id === circuitId);
|
||||
if (!circuitLocation || !circuitLocation.section.category) throw new Error("Stromkreis wurde nicht gefunden.");
|
||||
const row = buildExternalRow(context.objects, context.roomId, circuitId, Math.max(0, ...circuitLocation.circuit.deviceRows.map((entry) => entry.sortOrder)) + 10);
|
||||
const transitions = context.objects.map((expected) => ({ expected, target: { ...expected, distributionBoardId: circuitLocation.tree.distributionBoardId, circuitDeviceRowId: row.id } }));
|
||||
const result = projectCommandService.executeUser({ projectId, expectedRevision, description: `${transitions.length} Revit-Objekte als neue Gerätezeile zugeordnet`, command: createExternalObjectNewRowProjectCommand(externalObjectAssignToNewRowCommandType, { row, objects: transitions, confirmedConflictObjectIds: objectIds }) });
|
||||
return res.json(result);
|
||||
} catch (error) { return respondWithProjectCommandError(error, res); }
|
||||
}
|
||||
|
||||
export async function assignExternalObjectsToNewCircuit(req: Request, res: Response) {
|
||||
const projectId = String(req.params.projectId ?? "");
|
||||
try {
|
||||
const expectedRevision = parseExpectedRevision(req.body?.expectedRevision);
|
||||
const objectIds = parseObjectIds(req.body?.objectIds);
|
||||
const sectionId = requiredId(req.body?.sectionId, "Ungültige Stromkreisgruppe.");
|
||||
const context = await loadAssignmentContext(projectId, objectIds);
|
||||
const sectionLocation = context.treeResult?.trees.flatMap((tree) => tree.sections.map((section) => ({ tree, section }))).find((entry) => entry.section.id === sectionId);
|
||||
if (!sectionLocation?.section.category) throw new Error("Stromkreisgruppe wurde nicht gefunden.");
|
||||
const circuitId = randomUUID();
|
||||
const row = buildExternalRow(context.objects, context.roomId, circuitId, 10);
|
||||
const protection = createDefaultCircuitProtection(sectionLocation.section.category);
|
||||
const circuit = {
|
||||
id: circuitId,
|
||||
circuitListId: sectionLocation.tree.circuitListId,
|
||||
sectionId,
|
||||
equipmentIdentifier: await circuitNumberingService.getNextIdentifier(sectionId),
|
||||
displayName: null,
|
||||
sortOrder: Math.max(0, ...sectionLocation.section.circuits.map((entry) => entry.sortOrder)) + 10,
|
||||
cableType: null, cableCrossSection: null, cableLength: null, rcdAssignment: null,
|
||||
terminalDesignation: null,
|
||||
voltage: sectionLocation.section.category === "three_phase" ? sectionLocation.tree.threePhaseVoltageV : sectionLocation.tree.singlePhaseVoltageV,
|
||||
controlRequirement: null, status: null, isReserve: false, remark: null,
|
||||
deviceRows: [row],
|
||||
protectionDevice: {
|
||||
circuitId,
|
||||
type: protection.type,
|
||||
ratedCurrentA: protection.ratedCurrentA,
|
||||
fuseUtilizationCategory: "fuseUtilizationCategory" in protection ? protection.fuseUtilizationCategory : null,
|
||||
tripCharacteristic: "tripCharacteristic" in protection ? protection.tripCharacteristic : null,
|
||||
rcdType: "rcdType" in protection ? protection.rcdType : null,
|
||||
ratedResidualCurrentMa: "ratedResidualCurrentMa" in protection ? protection.ratedResidualCurrentMa : null,
|
||||
},
|
||||
};
|
||||
const transitions = context.objects.map((expected) => ({ expected, target: { ...expected, distributionBoardId: sectionLocation.tree.distributionBoardId, circuitDeviceRowId: row.id } }));
|
||||
const result = projectCommandService.executeUser({ projectId, expectedRevision, description: `${transitions.length} Revit-Objekte als neuen Stromkreis zugeordnet`, command: createExternalObjectNewCircuitProjectCommand(externalObjectAssignToNewCircuitCommandType, { circuit, objects: transitions, confirmedConflictObjectIds: objectIds }) });
|
||||
return res.json(result);
|
||||
} catch (error) { return respondWithProjectCommandError(error, res); }
|
||||
}
|
||||
|
||||
async function loadAssignmentContext(projectId: string, objectIds: string[]) {
|
||||
const { state, projectExists } = externalModelStateRepository.getByProject(projectId);
|
||||
if (!projectExists) throw new Error("Projekt nicht gefunden.");
|
||||
const byId = new Map(state.objects.map((object) => [object.id, object]));
|
||||
const objects = objectIds.map((id) => {
|
||||
const object = byId.get(id);
|
||||
if (!object) throw new Error("Ein Revit-Objekt wurde nicht gefunden.");
|
||||
if (object.circuitDeviceRowId !== null) throw new Error("Ein Revit-Objekt ist bereits zugeordnet.");
|
||||
return object;
|
||||
});
|
||||
const roomIds = new Set(objects.map((object) => object.externalRoomMappingId === null ? null : state.roomMappings.find((mapping) => mapping.id === object.externalRoomMappingId)?.roomId ?? null));
|
||||
if (roomIds.size !== 1) throw new Error("Revit-Objekte aus verschiedenen Räumen benötigen getrennte Gerätezeilen.");
|
||||
return { objects, roomId: [...roomIds][0]!, treeResult: await buildProjectCircuitTrees(projectId) };
|
||||
}
|
||||
|
||||
function buildExternalRow(objects: ReturnType<typeof externalModelStateRepository.getByProject>["state"]["objects"], roomId: string | null, circuitId: string, sortOrder: number): CircuitDeviceRowSnapshot {
|
||||
const first = objects[0]!;
|
||||
const planning = first.planningValues;
|
||||
const quantity = objects.reduce((sum, object) => sum + object.planningValues.effectiveQuantity, 0);
|
||||
return {
|
||||
id: randomUUID(), circuitId, linkedProjectDeviceId: first.linkedProjectDeviceId,
|
||||
sortOrder, name: first.acceptedSourceValues.familyAndType,
|
||||
displayName: planning.displayName ?? (scalarText(first.acceptedSourceValues.selectionMarker) || first.acceptedSourceValues.familyAndType),
|
||||
phaseType: planning.category === "three_phase" ? "three_phase" : "single_phase",
|
||||
connectionKind: planning.connectionKind, costGroup: planning.costGroup,
|
||||
category: planning.category, level: null,
|
||||
roomId,
|
||||
roomNumberSnapshot: scalarText(first.acceptedSourceValues.roomNumber) || null,
|
||||
roomNameSnapshot: scalarText(first.acceptedSourceValues.roomName) || null,
|
||||
quantity, manualQuantity: 0,
|
||||
powerPerUnit: (planning.powerPerUnitW ?? 0) / 1000,
|
||||
simultaneityFactor: planning.simultaneityFactor ?? 1,
|
||||
cosPhi: planning.cosPhi, remark: planning.remark, overriddenFields: null,
|
||||
};
|
||||
}
|
||||
|
||||
function parseExpectedRevision(value: unknown) {
|
||||
if (!Number.isSafeInteger(value) || (value as number) < 0) throw new Error("Ungültige Projektrevision.");
|
||||
return value as number;
|
||||
}
|
||||
function parseObjectIds(value: unknown): string[] {
|
||||
if (!Array.isArray(value) || value.length === 0) throw new Error("Keine Revit-Objekte ausgewählt.");
|
||||
const ids = value.map((entry) => requiredId(entry, "Ungültige Revit-Objekt-ID."));
|
||||
if (new Set(ids).size !== ids.length) throw new Error("Revit-Objekte sind doppelt ausgewählt.");
|
||||
return ids;
|
||||
}
|
||||
function requiredId(value: unknown, message: string) { if (typeof value !== "string" || !value) throw new Error(message); return value; }
|
||||
function findRow(result: Awaited<ReturnType<typeof buildProjectCircuitTrees>>, rowId: string) {
|
||||
if (!result) return null;
|
||||
for (const tree of result.trees) for (const section of tree.sections) for (const circuit of section.circuits) {
|
||||
const row = circuit.deviceRows.find((entry) => entry.id === rowId);
|
||||
if (row) return { row, circuitId: circuit.id, distributionBoardId: tree.distributionBoardId, category: section.category ?? null };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function toRowSnapshot(row: NonNullable<ReturnType<typeof findRow>>["row"], circuitId: string): CircuitDeviceRowSnapshot {
|
||||
return { id: row.id, circuitId, 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 };
|
||||
}
|
||||
|
||||
const planningFields = ["displayName", "category", "connectionKind", "effectiveQuantity", "powerPerUnitW", "simultaneityFactor", "cosPhi", "costGroup", "remark"] as const satisfies readonly (keyof ExternalModelObjectPlanningValues)[];
|
||||
function scalarText(value: string | number | boolean | null) { return value === null ? "" : String(value); }
|
||||
function isRecord(value: unknown): value is Record<string, unknown> { return value !== null && typeof value === "object" && !Array.isArray(value); }
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ import {
|
|||
getExternalInitialImportStatus,
|
||||
planExternalInitialImport,
|
||||
} from "../controllers/external-initial-import.controller.js";
|
||||
import { listExternalObjects, updateExternalObjects } from "../controllers/external-objects.controller.js";
|
||||
import { assignExternalObjectsToNewCircuit, assignExternalObjectsToNewRow, assignExternalObjectsToRow, detachExternalObjectsFromRow, listExternalObjects, updateExternalObjects } from "../controllers/external-objects.controller.js";
|
||||
|
||||
export const projectRouter = Router();
|
||||
|
||||
|
|
@ -93,6 +93,10 @@ projectRouter.post("/:projectId/external-model/initial-import/plan", planExterna
|
|||
projectRouter.post("/:projectId/external-model/initial-import/apply", applyExternalInitialImport);
|
||||
projectRouter.get("/:projectId/external-model/objects", listExternalObjects);
|
||||
projectRouter.put("/:projectId/external-model/objects", updateExternalObjects);
|
||||
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);
|
||||
projectRouter.post("/:projectId/external-model/objects/detach-row", detachExternalObjectsFromRow);
|
||||
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