Manage distribution board components

This commit is contained in:
2026-07-31 07:29:25 +02:00
parent 31589875b5
commit 756e307bd8
9 changed files with 877 additions and 8 deletions
@@ -47,6 +47,15 @@ import {
import {
buildCircuitSectionRenumberAssignments,
} from "../utils/circuit-section-renumber-command";
import {
buildDistributionBoardComponentSnapshot,
getNextComponentSortOrder,
getSuggestedGroupComponentIdentifier,
toDistributionBoardComponentSnapshot,
updateDistributionBoardComponentSnapshot,
type DistributionBoardComponentEditorValues,
type MutableDistributionBoardComponentRole,
} from "../utils/distribution-board-component-editing";
import { loadCircuitEditorSnapshot } from "../utils/circuit-editor-history";
import type {
CellKey,
@@ -62,11 +71,13 @@ import type { VisibleGridRow } from "../utils/circuit-grid-projection";
import {
deleteCircuitCommand,
deleteCircuitDeviceRowCommand,
deleteDistributionBoardComponentCommand,
getCircuitTree,
getNextCircuitIdentifier,
getProjectHistory,
insertCircuitCommand,
insertCircuitDeviceRowCommand,
insertDistributionBoardComponentCommand,
listProjectDevices,
moveCircuitDeviceRowsCommand,
moveCircuitDeviceRowsToNewCircuitCommand,
@@ -76,10 +87,12 @@ import {
redoProjectCommand,
updateCircuitById,
updateCircuitDeviceRowById,
updateDistributionBoardComponentCommand,
undoProjectCommand,
} from "../utils/api";
import type {
CircuitTreeCircuitDto,
CircuitTreeComponentDto,
CircuitTreeResponseDto,
CreateCircuitDeviceRowInputDto,
CreateCircuitInputDto,
@@ -93,6 +106,7 @@ import type {
import type {
CircuitSnapshot,
} from "../../domain/models/circuit-structure-project-command.model";
import { DistributionBoardComponentModal } from "./distribution-board-component-modal";
type SaveDirection = "stay" | "next" | "prev";
type StartEditMode = "selectExisting" | "replaceWithTypedChar";
@@ -161,6 +175,18 @@ function normalizeUiError(err: unknown): string {
return message;
}
type StructureComponentEditorIntent =
| {
kind: "create";
role: MutableDistributionBoardComponentRole;
sectionId?: string;
initialEquipmentIdentifier?: string;
}
| {
kind: "edit";
component: CircuitTreeComponentDto;
};
function getFullColumnLabel(column: ColumnDef): string {
return column.fullLabel ?? column.label;
}
@@ -201,6 +227,8 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
const [editingCell, setEditingCell] = useState<EditingCell | null>(null);
const [activeSectionId, setActiveSectionId] = useState<string | null>(null);
const [isSaving, setIsSaving] = useState(false);
const [componentEditorIntent, setComponentEditorIntent] =
useState<StructureComponentEditorIntent | null>(null);
const [projectDevices, setProjectDevices] = useState<ProjectDeviceDto[]>([]);
const [isProjectDeviceDrawerOpen, setIsProjectDeviceDrawerOpen] =
useState(false);
@@ -1031,6 +1059,85 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
await applyHistory("undo");
}
async function handleSaveDistributionBoardComponent(
values: DistributionBoardComponentEditorValues
) {
const intent = componentEditorIntent;
if (!intent || !data) {
return;
}
await runCommand({
label:
intent.kind === "edit"
? "Verteilerkomponente bearbeiten"
: "Verteilerkomponente hinzufügen",
redo: async () => {
const result =
intent.kind === "edit"
? await (() => {
const expected = toDistributionBoardComponentSnapshot(
intent.component
);
return updateDistributionBoardComponentCommand(
projectId,
getExpectedProjectRevision(),
expected,
updateDistributionBoardComponentSnapshot(expected, values)
);
})()
: await (() => {
const components =
intent.role === "auxiliary"
? data.footerComponents
: data.sections.find(
(section) => section.id === intent.sectionId
)?.components ?? [];
const snapshot = buildDistributionBoardComponentSnapshot({
id: crypto.randomUUID(),
circuitListId,
role: intent.role,
sectionId: intent.sectionId,
sortOrder: getNextComponentSortOrder(components),
values,
});
return insertDistributionBoardComponentCommand(
projectId,
getExpectedProjectRevision(),
snapshot
);
})();
applyProjectCommandResult(result);
setComponentEditorIntent(null);
return null;
},
});
}
async function handleDeleteDistributionBoardComponent(
component: CircuitTreeComponentDto
) {
if (
!confirm(
`Verteilerkomponente „${component.equipmentIdentifier} ${component.name}“ entfernen?`
)
) {
return;
}
await runCommand({
label: "Verteilerkomponente entfernen",
redo: async () => {
const result = await deleteDistributionBoardComponentCommand(
projectId,
getExpectedProjectRevision(),
toDistributionBoardComponentSnapshot(component)
);
applyProjectCommandResult(result);
setComponentEditorIntent(null);
return null;
},
});
}
async function handleRedo() {
if (historyBusy || isSaving || !historyState || historyState.redoDepth === 0) {
return;
@@ -2605,6 +2712,29 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
return (
<div className="tree-editor-shell">
{componentEditorIntent ? (
<DistributionBoardComponentModal
initialComponent={
componentEditorIntent.kind === "edit"
? componentEditorIntent.component
: undefined
}
initialEquipmentIdentifier={
componentEditorIntent.kind === "create"
? componentEditorIntent.initialEquipmentIdentifier
: undefined
}
isSaving={isSaving}
onClose={() => setComponentEditorIntent(null)}
onSave={handleSaveDistributionBoardComponent}
role={
componentEditorIntent.kind === "edit"
? (componentEditorIntent.component
.role as MutableDistributionBoardComponentRole)
: componentEditorIntent.role
}
/>
) : null}
<div className="editor-toolbar">
<button
type="button"
@@ -2656,6 +2786,18 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
? "Projektgeräte schließen"
: `Projektgeräte öffnen (${projectDevices.length})`}
</button>
<button
type="button"
disabled={isSaving || historyBusy}
onClick={() =>
setComponentEditorIntent({
kind: "create",
role: "auxiliary",
})
}
>
Verteilergerät hinzufügen
</button>
<button
type="button"
onClick={clearSortAndFilters}
@@ -2996,6 +3138,10 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
row.rowType === "footerComponent"
) {
const component = row.component!;
const isMutableComponent =
component.role === "group_upstream_protection" ||
component.role === "group_residual_current_protection" ||
component.role === "auxiliary";
const protection = component.protectionDevice;
const protectionSummary = protection
? [
@@ -3033,6 +3179,39 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
{protectionSummary}
</span>
) : null}
<span className="structure-component-actions">
{isMutableComponent ? (
<>
<button
type="button"
disabled={isSaving}
onClick={() =>
setComponentEditorIntent({
kind: "edit",
component,
})
}
>
Bearbeiten
</button>
<button
type="button"
disabled={isSaving}
onClick={() =>
void handleDeleteDistributionBoardComponent(
component
)
}
>
Entfernen
</button>
</>
) : (
<span className="structure-component-fixed">
Feste Verteilerkomponente
</span>
)}
</span>
</div>
</td>
</tr>
@@ -3040,6 +3219,14 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
}
if (row.rowType === "section") {
const section = data.sections.find((entry) => entry.id === row.sectionId)!;
const hasUpstreamProtection = section.components.some(
(component) =>
component.role === "group_upstream_protection"
);
const hasGroupRcd = section.components.some(
(component) =>
component.role === "group_residual_current_protection"
);
return (
<tr
key={row.rowKey}
@@ -3115,6 +3302,52 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
</span>
</div>
<div className="section-actions">
<button
type="button"
tabIndex={-1}
disabled={
hasUpstreamProtection ||
!section.category ||
!section.groupNumber
}
onClick={() =>
setComponentEditorIntent({
kind: "create",
role: "group_upstream_protection",
sectionId: section.id,
initialEquipmentIdentifier:
getSuggestedGroupComponentIdentifier(
section,
"group_upstream_protection"
),
})
}
>
Vorsicherung hinzufügen
</button>
<button
type="button"
tabIndex={-1}
disabled={
hasGroupRcd ||
!section.category ||
!section.groupNumber
}
onClick={() =>
setComponentEditorIntent({
kind: "create",
role: "group_residual_current_protection",
sectionId: section.id,
initialEquipmentIdentifier:
getSuggestedGroupComponentIdentifier(
section,
"group_residual_current_protection"
),
})
}
>
Gruppen-FI hinzufügen
</button>
<button type="button" tabIndex={-1} onClick={() => void handleAddReserveCircuit(section.id)}>
Stromkreis hinzufügen
</button>