Complete Revit object editor assignments
This commit is contained in:
parent
4489cd76f0
commit
77d775fde1
25 changed files with 853 additions and 77 deletions
|
|
@ -120,6 +120,11 @@ import {
|
|||
deleteCircuitGroupCommand,
|
||||
deleteCircuitGroupSubtreeCommand,
|
||||
deleteDistributionBoardComponentCommand,
|
||||
assignExternalProjectObjectsToRow,
|
||||
assignExternalProjectObjectsToNewRow,
|
||||
assignExternalProjectObjectsToNewCircuit,
|
||||
detachExternalProjectObjectsFromRow,
|
||||
getExternalProjectObjects,
|
||||
getCircuitTree,
|
||||
getNextCircuitIdentifier,
|
||||
getProjectHistory,
|
||||
|
|
@ -154,6 +159,7 @@ import type {
|
|||
ProjectCommandResultDto,
|
||||
ProjectDeviceDto,
|
||||
ProjectHistoryStateDto,
|
||||
ExternalProjectObjectsDto,
|
||||
} from "../types";
|
||||
import type {
|
||||
CircuitDeviceRowSnapshot,
|
||||
|
|
@ -330,6 +336,9 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
const [cableSizingEditorCircuit, setCableSizingEditorCircuit] =
|
||||
useState<CircuitTreeCircuitDto | null>(null);
|
||||
const [projectDevices, setProjectDevices] = useState<ProjectDeviceDto[]>([]);
|
||||
const [externalObjects, setExternalObjects] = useState<ExternalProjectObjectsDto | null>(null);
|
||||
const [isRevitDrawerOpen, setIsRevitDrawerOpen] = useState(false);
|
||||
const [draggingExternalObjectIds, setDraggingExternalObjectIds] = useState<string[]>([]);
|
||||
const [isProjectDeviceDrawerOpen, setIsProjectDeviceDrawerOpen] =
|
||||
useState(false);
|
||||
const [projectDeviceSearch, setProjectDeviceSearch] = useState("");
|
||||
|
|
@ -381,6 +390,26 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
() => orderedColumns.filter((column) => visibleColumnKeys.includes(column.key)),
|
||||
[orderedColumns, visibleColumnKeys]
|
||||
);
|
||||
const externalObjectGroups = useMemo(() => {
|
||||
if (!data || !externalObjects) return [];
|
||||
const groups = new Map<string, typeof externalObjects.objects>();
|
||||
for (const object of externalObjects.objects) {
|
||||
if (object.presenceStatus !== "present" || object.circuitDeviceRowId !== null || object.distributionBoardId !== data.distributionBoardId) continue;
|
||||
const key = JSON.stringify([
|
||||
object.roomId, object.selectionMarker, object.deviceTypeId,
|
||||
object.planningValues.category, object.linkedProjectDeviceId,
|
||||
object.planningValues,
|
||||
]);
|
||||
const entries = groups.get(key) ?? [];
|
||||
entries.push(object);
|
||||
groups.set(key, entries);
|
||||
}
|
||||
return [...groups.values()].map((objects) => ({
|
||||
key: objects.map((object) => object.id).sort().join("|"),
|
||||
objects,
|
||||
quantity: objects.reduce((sum, object) => sum + object.planningValues.effectiveQuantity, 0),
|
||||
}));
|
||||
}, [data, externalObjects]);
|
||||
|
||||
async function loadTree(options?: { showLoading?: boolean }) {
|
||||
const showLoading = options?.showLoading ?? false;
|
||||
|
|
@ -389,13 +418,17 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
}
|
||||
setError(null);
|
||||
try {
|
||||
const { tree, history } = await loadCircuitEditorSnapshot(
|
||||
() => getCircuitTree(projectId, circuitListId),
|
||||
() => getProjectHistory(projectId)
|
||||
);
|
||||
const [{ tree, history }, external] = await Promise.all([
|
||||
loadCircuitEditorSnapshot(
|
||||
() => getCircuitTree(projectId, circuitListId),
|
||||
() => getProjectHistory(projectId)
|
||||
),
|
||||
getExternalProjectObjects(projectId),
|
||||
]);
|
||||
projectRevisionRef.current = history.currentRevision;
|
||||
setHistoryState(history);
|
||||
setData(tree);
|
||||
setExternalObjects(external);
|
||||
} catch (err) {
|
||||
setError(normalizeUiError(err));
|
||||
} finally {
|
||||
|
|
@ -1321,12 +1354,20 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
const summary = subtree
|
||||
? summarizeCircuitGroupSubtree(subtree)
|
||||
: null;
|
||||
const externalObjectCount = section.circuits.reduce(
|
||||
(total, circuit) => total + circuit.deviceRows.reduce(
|
||||
(rowTotal, row) => rowTotal + (row.externalObjectCount ?? 0), 0
|
||||
), 0
|
||||
);
|
||||
const externalWarning = externalObjectCount > 0
|
||||
? `\n\n${externalObjectCount} Revit-Objekte werden wieder unzugeordnet.`
|
||||
: "";
|
||||
const warning = isEmpty
|
||||
? `Leere Stromkreisgruppe „${section.displayName}“ entfernen?`
|
||||
: `Stromkreisgruppe „${section.displayName}“ vollständig entfernen?\n\n` +
|
||||
`Dabei werden ${summary!.circuitCount} Stromkreis(e), ${summary!.deviceRowCount} Gerätezeile(n) und ${summary!.protectionComponentCount} Gruppenschutzgerät(e) gelöscht.\n\n` +
|
||||
"Die Änderung kann über die projektweite Versionshistorie rückgängig gemacht werden.";
|
||||
if (!confirm(warning)) {
|
||||
if (!confirm(warning + externalWarning)) {
|
||||
return;
|
||||
}
|
||||
await runCommand({
|
||||
|
|
@ -2878,11 +2919,15 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
setError("Die Gerätezeile ist ungültig.");
|
||||
return;
|
||||
}
|
||||
const externalWarning = (rowSnapshot.externalObjectCount ?? 0) > 0
|
||||
? `\n\n${rowSnapshot.externalObjectCount} Revit-Objekte werden wieder unzugeordnet.`
|
||||
: "";
|
||||
if (sourceCircuit.deviceRows.length === 1) {
|
||||
const keepAsReserve = confirm(
|
||||
`„${rowSnapshot.displayName}“ ist das letzte Gerät in ${sourceCircuit.equipmentIdentifier}.\n\n` +
|
||||
"OK: Gerät löschen und den leeren Stromkreis als Reserve behalten.\n" +
|
||||
"Abbrechen: anschließend entscheiden, ob stattdessen der gesamte Stromkreis gelöscht werden soll."
|
||||
"Abbrechen: anschließend entscheiden, ob stattdessen der gesamte Stromkreis gelöscht werden soll." +
|
||||
externalWarning
|
||||
);
|
||||
if (!keepAsReserve) {
|
||||
const deleteCircuit = confirm(
|
||||
|
|
@ -2894,7 +2939,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
}
|
||||
return;
|
||||
}
|
||||
} else if (!confirm(`Gerätezeile „${rowSnapshot.displayName}“ löschen?`)) {
|
||||
} else if (!confirm(`Gerätezeile „${rowSnapshot.displayName}“ löschen?${externalWarning}`)) {
|
||||
return;
|
||||
}
|
||||
await runCommand({
|
||||
|
|
@ -2917,11 +2962,18 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
setError("Der Stromkreis ist ungültig.");
|
||||
return;
|
||||
}
|
||||
const externalObjectCount = circuit.deviceRows.reduce(
|
||||
(total, row) => total + (row.externalObjectCount ?? 0), 0
|
||||
);
|
||||
const externalWarning = externalObjectCount > 0
|
||||
? `\n\n${externalObjectCount} Revit-Objekte werden wieder unzugeordnet.`
|
||||
: "";
|
||||
if (
|
||||
askForConfirmation &&
|
||||
!confirm(
|
||||
`Stromkreis ${circuit.equipmentIdentifier} und ${circuit.deviceRows.length} zugeordnete Gerätezeile(n) löschen?\n\n` +
|
||||
"Bestehende Betriebsmittelkennzeichen werden nicht neu nummeriert."
|
||||
"Bestehende Betriebsmittelkennzeichen werden nicht neu nummeriert." +
|
||||
externalWarning
|
||||
)
|
||||
) {
|
||||
return;
|
||||
|
|
@ -2958,6 +3010,56 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
await handleDeleteCircuit(intent.circuitId);
|
||||
}
|
||||
|
||||
async function handleAssignExternalObjects(rowId: string, objectIds: string[]) {
|
||||
if (!confirm(`${objectIds.length} Revit-Objekt(e) dieser Gerätezeile zuordnen?\n\nAbweichende Planungswerte werden ausdrücklich zugunsten der Gerätezeile bestätigt.`)) return;
|
||||
await runCommand({
|
||||
label: "Revit-Objekte zuordnen",
|
||||
redo: async () => {
|
||||
const result = await assignExternalProjectObjectsToRow(
|
||||
projectId,
|
||||
getExpectedProjectRevision(),
|
||||
objectIds,
|
||||
rowId,
|
||||
objectIds
|
||||
);
|
||||
applyProjectCommandResult(result);
|
||||
setDraggingExternalObjectIds([]);
|
||||
return null;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function handleAssignExternalObjectsToNewRow(circuitId: string, objectIds: string[]) {
|
||||
if (!confirm(`${objectIds.length} Revit-Objekt(e) als neue Gerätezeile anlegen?`)) return;
|
||||
await runCommand({ label: "Revit-Objekte als Gerätezeile zuordnen", redo: async () => {
|
||||
const result = await assignExternalProjectObjectsToNewRow(projectId, getExpectedProjectRevision(), objectIds, circuitId);
|
||||
applyProjectCommandResult(result); setDraggingExternalObjectIds([]); return null;
|
||||
}});
|
||||
}
|
||||
|
||||
async function handleAssignExternalObjectsToNewCircuit(sectionId: string, objectIds: string[]) {
|
||||
if (!confirm(`${objectIds.length} Revit-Objekt(e) als neuen Stromkreis anlegen?`)) return;
|
||||
await runCommand({ label: "Revit-Objekte als Stromkreis zuordnen", redo: async () => {
|
||||
const result = await assignExternalProjectObjectsToNewCircuit(projectId, getExpectedProjectRevision(), objectIds, sectionId);
|
||||
applyProjectCommandResult(result); setDraggingExternalObjectIds([]); return null;
|
||||
}});
|
||||
}
|
||||
|
||||
async function handleDetachExternalObjects(rowId: string) {
|
||||
const objectIds = externalObjects?.objects
|
||||
.filter((object) => object.circuitDeviceRowId === rowId)
|
||||
.map((object) => object.id) ?? [];
|
||||
if (objectIds.length === 0 || !confirm(`${objectIds.length} Revit-Objekt(e) lösen?\n\nDie aktuellen Werte der Gerätezeile werden als Planungswerte und Overrides übernommen.`)) return;
|
||||
await runCommand({
|
||||
label: "Revit-Objekte lösen",
|
||||
redo: async () => {
|
||||
const result = await detachExternalProjectObjectsFromRow(projectId, getExpectedProjectRevision(), objectIds);
|
||||
applyProjectCommandResult(result);
|
||||
return null;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Explicit renumber action through project history. Sorting and device rows
|
||||
// remain unchanged; the server handles collision-safe identifier swaps.
|
||||
async function handleRenumberSection(sectionId: string) {
|
||||
|
|
@ -3381,6 +3483,15 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
}
|
||||
showLabel
|
||||
/>
|
||||
<IconButton
|
||||
ariaControls="revit-object-drawer"
|
||||
ariaExpanded={isRevitDrawerOpen}
|
||||
className="project-device-drawer-toggle"
|
||||
icon={isRevitDrawerOpen ? PanelRightClose : PanelRightOpen}
|
||||
label={isRevitDrawerOpen ? "Revit-Objekte schließen" : `Revit-Objekte öffnen (${externalObjectGroups.reduce((sum, group) => sum + group.objects.length, 0)})`}
|
||||
onClick={() => setIsRevitDrawerOpen((current) => !current)}
|
||||
showLabel
|
||||
/>
|
||||
<IconButton
|
||||
ariaControls="project-device-drawer"
|
||||
ariaExpanded={isProjectDeviceDrawerOpen}
|
||||
|
|
@ -3419,6 +3530,62 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
) : null}
|
||||
{isSaving ? <div className="notice info">Wird gespeichert …</div> : null}
|
||||
<div className="tree-editor-layout">
|
||||
{isRevitDrawerOpen ? (
|
||||
<aside className="project-device-sidebar project-device-drawer" id="revit-object-drawer">
|
||||
<div className="project-device-drawer-header">
|
||||
<div>
|
||||
<h3>Revit-Objekte</h3>
|
||||
<span>{externalObjectGroups.reduce((sum, group) => sum + group.objects.length, 0)} unzugeordnet</span>
|
||||
</div>
|
||||
<button type="button" onClick={() => setIsRevitDrawerOpen(false)}>Schließen</button>
|
||||
</div>
|
||||
<div className="project-device-list">
|
||||
{externalObjectGroups.map((group) => {
|
||||
const first = group.objects[0]!;
|
||||
return (
|
||||
<details
|
||||
key={group.key}
|
||||
className={`project-device-item ${draggingExternalObjectIds[0] === first.id ? "dragging" : ""}`}
|
||||
>
|
||||
<summary
|
||||
draggable
|
||||
onDragStart={(event) => {
|
||||
const ids = group.objects.map((object) => object.id);
|
||||
setDraggingExternalObjectIds(ids);
|
||||
event.dataTransfer.effectAllowed = "move";
|
||||
event.dataTransfer.setData("application/x-revit-object-ids", JSON.stringify(ids));
|
||||
}}
|
||||
onDragEnd={() => setDraggingExternalObjectIds([])}
|
||||
>
|
||||
<strong>{first.planningValues.displayName || first.selectionMarker || first.familyAndType}</strong>
|
||||
<span>{first.sourceRoomNumber} {first.sourceRoomName}</span>
|
||||
<span>{group.objects.length} Objekt(e), Menge {formatValue(group.quantity, "quantity")}</span>
|
||||
<span>{first.familyAndType}</span>
|
||||
</summary>
|
||||
{group.objects.map((object) => (
|
||||
<button
|
||||
key={object.id}
|
||||
type="button"
|
||||
draggable
|
||||
className="btn btn-sm btn-outline-secondary mt-2 w-100 text-start"
|
||||
onDragStart={(event) => {
|
||||
setDraggingExternalObjectIds([object.id]);
|
||||
event.dataTransfer.effectAllowed = "move";
|
||||
event.dataTransfer.setData("application/x-revit-object-ids", JSON.stringify([object.id]));
|
||||
event.stopPropagation();
|
||||
}}
|
||||
onDragEnd={() => setDraggingExternalObjectIds([])}
|
||||
>
|
||||
{object.worksharingId}
|
||||
</button>
|
||||
))}
|
||||
</details>
|
||||
);
|
||||
})}
|
||||
{externalObjectGroups.length === 0 ? <p className="notice muted">Keine unzugeordneten Revit-Objekte für diesen Verteiler.</p> : null}
|
||||
</div>
|
||||
</aside>
|
||||
) : null}
|
||||
{isProjectDeviceDrawerOpen ? (
|
||||
<aside
|
||||
className="project-device-sidebar project-device-drawer"
|
||||
|
|
@ -4202,6 +4369,11 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
}`}
|
||||
onClick={() => setActiveSectionId(row.sectionId)}
|
||||
onDragOver={(event) => {
|
||||
if (draggingExternalObjectIds.length > 0 && (row.device || row.circuit || row.rowType === "placeholder")) {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = "move";
|
||||
return;
|
||||
}
|
||||
if (draggingCircuitCount > 0) {
|
||||
const valid = isCircuitDropTargetValid(
|
||||
row.sectionId
|
||||
|
|
@ -4342,6 +4514,21 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
});
|
||||
}}
|
||||
onDrop={(event) => {
|
||||
if (draggingExternalObjectIds.length > 0 && row.device) {
|
||||
event.preventDefault();
|
||||
void handleAssignExternalObjects(row.device.id, draggingExternalObjectIds);
|
||||
return;
|
||||
}
|
||||
if (draggingExternalObjectIds.length > 0 && row.rowType === "placeholder") {
|
||||
event.preventDefault();
|
||||
void handleAssignExternalObjectsToNewCircuit(row.sectionId, draggingExternalObjectIds);
|
||||
return;
|
||||
}
|
||||
if (draggingExternalObjectIds.length > 0 && row.circuit) {
|
||||
event.preventDefault();
|
||||
void handleAssignExternalObjectsToNewRow(row.circuit.id, draggingExternalObjectIds);
|
||||
return;
|
||||
}
|
||||
if (draggingCircuitCount > 0) {
|
||||
if (row.rowType === "placeholder") {
|
||||
void handleCircuitReorderDrop(event, {
|
||||
|
|
@ -4728,14 +4915,26 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
</>
|
||||
) : null}
|
||||
{row.device ? (
|
||||
<IconButton
|
||||
disabled={isSaving}
|
||||
icon={ListX}
|
||||
label="Gerät löschen"
|
||||
onClick={() => void handleDeleteDevice(row.device!.id)}
|
||||
tabIndex={-1}
|
||||
variant="danger"
|
||||
/>
|
||||
<>
|
||||
{(row.device.externalObjectCount ?? 0) > 0 ? (
|
||||
<button
|
||||
type="button"
|
||||
className="badge rounded-pill text-bg-info border-0"
|
||||
title="Verknüpfte Revit-Objekte anzeigen und lösen"
|
||||
onClick={() => void handleDetachExternalObjects(row.device!.id)}
|
||||
>
|
||||
Revit {row.device.externalObjectCount}
|
||||
</button>
|
||||
) : null}
|
||||
<IconButton
|
||||
disabled={isSaving}
|
||||
icon={ListX}
|
||||
label="Gerät löschen"
|
||||
onClick={() => void handleDeleteDevice(row.device!.id)}
|
||||
tabIndex={-1}
|
||||
variant="danger"
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
{dropIntent?.kind === "new-circuit" && row.rowType === "placeholder" && dropIntent.sectionId === row.sectionId ? (
|
||||
<span className="drop-hint">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue