diff --git a/docs/current-architecture.md b/docs/current-architecture.md index ed66858..d47765b 100644 --- a/docs/current-architecture.md +++ b/docs/current-architecture.md @@ -365,6 +365,12 @@ werden über beschriftete Modale angelegt, bearbeitet und nach Bestätigung entfernt. Der Frontend-API-Adapter serialisiert dafür die vorhandenen `distribution-board-component.*`-Commands; die verbindliche geräteabhängige Validierung und atomare Revision bleiben serverseitig. +Neue Stromkreisgruppen erhalten im Editor die höchste vorhandene +Gruppennummer ihrer Kategorie plus eins und ein daraus abgeleitetes Präfix. +Das Bearbeiten ändert ausschließlich den Anzeigenamen. Nur vollständig leere +Gruppen können über `circuit-group.delete` entfernt werden; für befüllte +Gruppen bleibt die Aktion bis zum gesonderten Warn- und Unterbaumdialog +gesperrt. `distribution-board.update` versioniert Etage, Netzart und den verteilerweiten Gleichzeitigkeitsfaktor gemeinsam und stellt alle Werte über dauerhaftes Undo/Redo wieder her. Der Faktor liegt zwischen `0` und `1` und diff --git a/docs/spec/08-current-product-backlog.md b/docs/spec/08-current-product-backlog.md index 6880875..01123a7 100644 --- a/docs/spec/08-current-product-backlog.md +++ b/docs/spec/08-current-product-backlog.md @@ -35,7 +35,9 @@ requirements and intended sequencing, not proof of implementation. - [x] Phase E2a: pure header/group/footer structure projection. - [x] Phase E2b: render the structure projection in the editor grid. - [x] Phase E3a: create, edit and delete mutable group and footer components. -- [ ] Phase E3b: group management and circuit-protection editing. +- [x] Phase E3b1: create, rename and safely delete empty circuit groups. +- [ ] Phase E3b2: circuit-protection editing. +- [ ] Phase E4: group reorder, renumber, circuit moves and populated-delete warning. - [ ] Phase E: editor projection and editing. - [ ] Phase F: documentation and full GUI verification. - [ ] Keep full electrical sizing and cable-dimensioning rules separate until diff --git a/docs/spec/09-distribution-board-components-and-protection-groups.md b/docs/spec/09-distribution-board-components-and-protection-groups.md index 26bc6df..20ca71f 100644 --- a/docs/spec/09-distribution-board-components-and-protection-groups.md +++ b/docs/spec/09-distribution-board-components-and-protection-groups.md @@ -741,7 +741,8 @@ Acceptance: Status: In progress. The complete tree read model E1, pure structural projection E2a, grid rendering E2b and mutable component editing E3a are -complete. Group management and circuit-protection editing remain pending. +complete. Basic group management E3b1 is also complete. Circuit-protection +editing and structural drag/renumber/delete workflows remain pending. - render fixed header components - render group components and circuit blocks @@ -770,6 +771,12 @@ Implemented in E1/E2a/E2b/E3a: - mutable group protection and auxiliary footer components use labeled modals, type-dependent protection fields and persistent project commands - fixed main-switch and surge-protection header components remain read-only +- users can create a group in any supported category; its number is the + category's highest existing number plus one and its prefix is generated +- group editing changes only the display name; category, number, prefix and + child BMKs remain stable +- an exactly empty group can be removed through the persistent group command; + populated deletion stays disabled until the dedicated warning UI is present Acceptance: diff --git a/src/frontend/components/circuit-group-modal.tsx b/src/frontend/components/circuit-group-modal.tsx new file mode 100644 index 0000000..ccfbc51 --- /dev/null +++ b/src/frontend/components/circuit-group-modal.tsx @@ -0,0 +1,97 @@ +"use client"; + +import { type FormEvent, useState } from "react"; +import { + circuitGroupCategories, + circuitGroupCategoryLabels, + type CircuitGroupCategory, +} from "../../shared/constants/circuit-group"; +import type { CircuitTreeSectionDto } from "../types"; +import { FormModal } from "./form-modal"; + +interface CircuitGroupModalProps { + initialSection?: CircuitTreeSectionDto; + isSaving: boolean; + onClose: () => void; + onSave: (values: { + category: CircuitGroupCategory; + displayName: string; + }) => Promise; +} + +export function CircuitGroupModal({ + initialSection, + isSaving, + onClose, + onSave, +}: CircuitGroupModalProps) { + const [category, setCategory] = useState( + initialSection?.category ?? "lighting" + ); + const [displayName, setDisplayName] = useState( + initialSection?.displayName ?? "" + ); + + async function handleSubmit(event: FormEvent) { + event.preventDefault(); + await onSave({ category, displayName }); + } + + return ( + +
+
+ + +
+
+ + setDisplayName(event.target.value)} + placeholder={ + initialSection + ? undefined + : "Leer lassen für den automatisch erzeugten Namen" + } + required={Boolean(initialSection)} + value={displayName} + /> +
+
+
+ ); +} diff --git a/src/frontend/components/circuit-tree-editor.tsx b/src/frontend/components/circuit-tree-editor.tsx index b3ae1fc..6ecd482 100644 --- a/src/frontend/components/circuit-tree-editor.tsx +++ b/src/frontend/components/circuit-tree-editor.tsx @@ -47,6 +47,12 @@ import { import { buildCircuitSectionRenumberAssignments, } from "../utils/circuit-section-renumber-command"; +import { + buildNewCircuitGroupSnapshot, + canDeleteCircuitGroup, + renameCircuitGroupSnapshot, + toCircuitGroupSnapshot, +} from "../utils/circuit-group-editing"; import { buildDistributionBoardComponentSnapshot, getNextComponentSortOrder, @@ -71,12 +77,14 @@ import type { VisibleGridRow } from "../utils/circuit-grid-projection"; import { deleteCircuitCommand, deleteCircuitDeviceRowCommand, + deleteCircuitGroupCommand, deleteDistributionBoardComponentCommand, getCircuitTree, getNextCircuitIdentifier, getProjectHistory, insertCircuitCommand, insertCircuitDeviceRowCommand, + insertCircuitGroupCommand, insertDistributionBoardComponentCommand, listProjectDevices, moveCircuitDeviceRowsCommand, @@ -87,6 +95,7 @@ import { redoProjectCommand, updateCircuitById, updateCircuitDeviceRowById, + updateCircuitGroupCommand, updateDistributionBoardComponentCommand, undoProjectCommand, } from "../utils/api"; @@ -107,6 +116,8 @@ import type { CircuitSnapshot, } from "../../domain/models/circuit-structure-project-command.model"; import { DistributionBoardComponentModal } from "./distribution-board-component-modal"; +import { CircuitGroupModal } from "./circuit-group-modal"; +import type { CircuitGroupCategory } from "../../shared/constants/circuit-group"; type SaveDirection = "stay" | "next" | "prev"; type StartEditMode = "selectExisting" | "replaceWithTypedChar"; @@ -187,6 +198,10 @@ type StructureComponentEditorIntent = component: CircuitTreeComponentDto; }; +type CircuitGroupEditorIntent = + | { kind: "create" } + | { kind: "edit"; section: CircuitTreeResponseDto["sections"][number] }; + function getFullColumnLabel(column: ColumnDef): string { return column.fullLabel ?? column.label; } @@ -229,6 +244,8 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str const [isSaving, setIsSaving] = useState(false); const [componentEditorIntent, setComponentEditorIntent] = useState(null); + const [circuitGroupEditorIntent, setCircuitGroupEditorIntent] = + useState(null); const [projectDevices, setProjectDevices] = useState([]); const [isProjectDeviceDrawerOpen, setIsProjectDeviceDrawerOpen] = useState(false); @@ -1138,6 +1155,76 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str }); } + async function handleSaveCircuitGroup(values: { + category: CircuitGroupCategory; + displayName: string; + }) { + const intent = circuitGroupEditorIntent; + if (!intent || !data) { + return; + } + await runCommand({ + label: + intent.kind === "create" + ? "Stromkreisgruppe hinzufügen" + : "Stromkreisgruppe bearbeiten", + redo: async () => { + const result = + intent.kind === "create" + ? await insertCircuitGroupCommand( + projectId, + getExpectedProjectRevision(), + buildNewCircuitGroupSnapshot({ + id: crypto.randomUUID(), + circuitListId, + category: values.category, + displayName: values.displayName, + sections: data.sections, + }) + ) + : await (() => { + const expected = toCircuitGroupSnapshot( + intent.section, + circuitListId + ); + return updateCircuitGroupCommand( + projectId, + getExpectedProjectRevision(), + expected, + renameCircuitGroupSnapshot(expected, values.displayName) + ); + })(); + applyProjectCommandResult(result); + setCircuitGroupEditorIntent(null); + return null; + }, + }); + } + + async function handleDeleteEmptyCircuitGroup( + section: CircuitTreeResponseDto["sections"][number] + ) { + if (!canDeleteCircuitGroup(section)) { + return; + } + if (!confirm(`Leere Stromkreisgruppe „${section.displayName}“ entfernen?`)) { + return; + } + await runCommand({ + label: "Leere Stromkreisgruppe entfernen", + redo: async () => { + const result = await deleteCircuitGroupCommand( + projectId, + getExpectedProjectRevision(), + toCircuitGroupSnapshot(section, circuitListId) + ); + applyProjectCommandResult(result); + setCircuitGroupEditorIntent(null); + return null; + }, + }); + } + async function handleRedo() { if (historyBusy || isSaving || !historyState || historyState.redoDepth === 0) { return; @@ -2735,6 +2822,18 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str } /> ) : null} + {circuitGroupEditorIntent ? ( + setCircuitGroupEditorIntent(null)} + onSave={handleSaveCircuitGroup} + /> + ) : null}
+
+ +