diff --git a/AGENTS.md b/AGENTS.md index 9ef5b87..93e5108 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -476,6 +476,17 @@ downstream board's demand after its own board simultaneity factor to the chosen feeding circuit as a read-only virtual row. Recursive chains and several boards on one circuit are supported. A board-only link affects hierarchy but adds no load until a feeding circuit is selected. +The distribution-board dialog can create one empty feeder circuit and link it +in the same `distribution-board.create-feeder-circuit` project command. Its +history inverse restores the complete previous board update before removing +only the unchanged generated circuit; Redo reuses its UUID and BMK. No +CircuitDeviceRow is created. The three-phase group is suggested, the optional +circuit display name stays empty by default, and protection remains open for +planner sizing. A circuit with exactly one downstream board and no device rows +is rendered as one compact row using the live board number/name; several +downstream boards or additional devices retain summary and child rows. Editing +the name or remark of an empty reserve circuit changes the circuit itself and +does not implicitly create a device row. Distribution-board floor assignment and a project-enabled supply type use `distribution-board.update`; both values are snapshot/export fields and one persistent undo step. diff --git a/docs/circuit-list-editor-api.md b/docs/circuit-list-editor-api.md index a8173f5..de724aa 100644 --- a/docs/circuit-list-editor-api.md +++ b/docs/circuit-list-editor-api.md @@ -269,6 +269,13 @@ commands. Endpoints, payloads and calculation rules: `docs/estimate-balance.md`. submitted info fields, are restored together by persistent Undo/Redo - `fedFrom` remains free text for external feeds such as HAK, metering, transformer or generator and for supplementary notes +- The project-command endpoint additionally accepts + `distribution-board.create-feeder-circuit` with an empty complete circuit + snapshot and an embedded version-2 `distribution-board.update`. Both writes + are one transaction, revision and Undo step. The generated circuit has no + device row; Redo keeps its UUID and BMK. The history-only inverse + `distribution-board.remove-feeder-circuit` restores the previous board values + and removes only the unchanged circuit. - `POST /projects/:projectId/distribution-boards/:distributionBoardId/copy` - body: `{ "name": "UV-02 Kopie", "expectedRevision": 14 }` - duplicates the complete distribution-board subtree with new owning UUIDs @@ -346,7 +353,9 @@ commands. Endpoints, payloads and calculation rules: `docs/estimate-balance.md`. footer components. Circuit and component protection devices use their separate one-to-one DTOs. Calculated row, circuit, section and board totals remain included. Circuit totals contain the demand of linked downstream - boards. `downstreamBoardLoads` exposes those contributions as read-only rows; + boards. `downstreamBoardLoads` exposes those contributions as read-only + values; the grid combines exactly one downstream board without device rows + into the circuit row and keeps child rows for mixed or multiple loads. `feedingReference` resolves the current board/circuit labels by id. Response sketch: diff --git a/docs/current-architecture.md b/docs/current-architecture.md index 920002d..71b7feb 100644 --- a/docs/current-architecture.md +++ b/docs/current-architecture.md @@ -477,6 +477,21 @@ Verweise nicht. Der speisende Stromkreis muss zur speisenden Verteilung gehören Selbstverweise und Zyklen werden abgelehnt. Löschbefehle für referenzierte Verteilungen und Stromkreise, einschließlich Unterbaum- und Undo-Pfaden, werden vor der Datenbankänderung fachlich gesperrt. +Im Bearbeiten-Dialog kann nach Auswahl der speisenden Verteilung direkt ein +neuer Einspeiseabgang geplant werden. Die Oberfläche schlägt die dreiphasige +Gruppe und das nächste BMK vor. Der Befehl +`distribution-board.create-feeder-circuit` legt den leeren Stromkreis an und +wendet die vollständige Verteilungsänderung in derselben Transaktion und +Revision an; es entsteht keine Gerätezeile. Schutz und Leitung bleiben bewusst +offen, weil sie aus Last und Verlegebedingungen geplant werden müssen. Undo +stellt zuerst die vorherigen Verteilungswerte wieder her und entfernt danach +nur den unveränderten erzeugten Stromkreis; Redo verwendet UUID und BMK erneut. +Bei genau einer gespeisten Unterverteilung ohne Gerätezeilen projiziert das +Grid nur eine kompakte Stromkreiszeile mit dem über die ID aufgelösten +Verteilernamen. Erst mehrere Unterverteilungen oder zusätzliche Geräte erzeugen +Sammel- und Unterzeilen. Anzeigename und Bemerkung eines leeren +Reservestromkreises gehören zum Stromkreis und erzeugen beim Bearbeiten keine +Gerätezeile. Die projektweite Circuit-Tree-Projektion berechnet rekursiv die Lasten aller Verteilungen. Die Leistung einer Unterverteilung nach ihrem eigenen Verteiler-GZF wird als virtuelle, schreibgeschützte Zeile genau dem gewählten diff --git a/docs/print-view.md b/docs/print-view.md index a843030..ecc00e2 100644 --- a/docs/print-view.md +++ b/docs/print-view.md @@ -38,8 +38,9 @@ automatisch bei späteren Änderungen in einem anderen Tab. 3. **Verteilerpläne (quer, je Verteiler)** – Titelleiste mit Verteilernummer, Bezeichnung, Örtlichkeit, Netzart, „Gespeist von“ und Pgl; darunter Kopfkomponenten, Gruppen, Stromkreise - (mehrzeilige mit Sammelzeile, eingerückten Gerätezeilen und - schreibgeschützten Unterverteilungszeilen), + (reine 1:1-Einspeiseabgänge kompakt in einer Zeile; sonst mehrzeilig mit + Sammelzeile, eingerückten Gerätezeilen und schreibgeschützten + Unterverteilungszeilen), Gruppensummen und Verteilersumme. Fehlende Kabelangaben sind als „offen“ markiert. Die Titelleiste steht im Tabellenkopf und wiederholt sich beim Drucken auf Folgeseiten. diff --git a/src/app/globals.css b/src/app/globals.css index d14431b..88df3e9 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1537,6 +1537,10 @@ a.kpi:hover { background: var(--panel-bg-subtle); } +.tree-grid .feeder-circuit-row td:nth-child(2) { + font-weight: 600; +} + .tree-grid .device-row td:first-child { color: var(--text-faint); } diff --git a/src/app/projects/[projectId]/page.tsx b/src/app/projects/[projectId]/page.tsx index 4a99cf0..37813c1 100644 --- a/src/app/projects/[projectId]/page.tsx +++ b/src/app/projects/[projectId]/page.tsx @@ -47,6 +47,7 @@ import { import { copyGlobalDeviceToProject, copyDistributionBoard, + createFeederCircuitAndUpdateDistributionBoard, copyProjectDeviceToGlobal, createDistributionBoard, createProjectDevice, @@ -71,6 +72,12 @@ import { updateProjectDevice, updateProjectSettings, } from "../../../frontend/utils/api"; +import { createClientId } from "../../../frontend/utils/client-id"; +import { + buildFeederCircuitSnapshot, + getDefaultFeederCircuitSection, + getNextFeederCircuitIdentifier, +} from "../../../frontend/utils/distribution-board-feeder-circuit"; import type { BuildingDto, CircuitListDto, @@ -146,6 +153,7 @@ const boardFilterCollapsedStorageKey = "leistungsbilanz:overview-filter-collapsed:distribution-boards"; const projectDeviceFilterCollapsedStorageKey = "leistungsbilanz:overview-filter-collapsed:project-devices"; +const createFeederCircuitSelectValue = "__create_feeder_circuit__"; export default function ProjectDetailPage() { const params = useParams<{ projectId: string }>(); @@ -175,6 +183,12 @@ export default function ProjectDetailPage() { const [editingBoardCopyName, setEditingBoardCopyName] = useState(""); const [editingFeedingBoardId, setEditingFeedingBoardId] = useState(""); const [editingFeedingCircuitId, setEditingFeedingCircuitId] = useState(""); + const [isCreatingFeederCircuit, setIsCreatingFeederCircuit] = + useState(false); + const [editingFeederCircuitSectionId, setEditingFeederCircuitSectionId] = + useState(""); + const [editingFeederCircuitDisplayName, setEditingFeederCircuitDisplayName] = + useState(""); const [editingBoardInfo, setEditingBoardInfo] = useState( toDistributionBoardInfoDraft({ @@ -350,6 +364,21 @@ export default function ProjectDetailPage() { const selectedFeedingCircuit = selectedFeedingCircuits.find( ({ circuit }) => circuit.id === editingFeedingCircuitId ); + const feederCircuitSections = + selectedFeedingTree?.sections.filter( + (section) => section.category !== undefined + ) ?? []; + const selectedNewFeederCircuitSection = feederCircuitSections.find( + (section) => section.id === editingFeederCircuitSectionId + ); + const newFeederCircuitIdentifier = selectedNewFeederCircuitSection + ? getNextFeederCircuitIdentifier(selectedNewFeederCircuitSection) + : null; + const selectedFeedingCircuitCategory = + selectedFeedingCircuit?.section.category ?? + (isCreatingFeederCircuit + ? selectedNewFeederCircuitSection?.category + : undefined); const selectedFeedingBoard = boards.find( (board) => board.id === editingFeedingBoardId ); @@ -556,6 +585,9 @@ export default function ProjectDetailPage() { setEditingBoardInfo(toDistributionBoardInfoDraft(board)); setEditingFeedingBoardId(board.feedingDistributionBoardId ?? ""); setEditingFeedingCircuitId(board.feedingCircuitId ?? ""); + setIsCreatingFeederCircuit(false); + setEditingFeederCircuitSectionId(""); + setEditingFeederCircuitDisplayName(""); } function openBoardCreator() { @@ -584,28 +616,56 @@ export default function ProjectDetailPage() { setIsSaving(true); setError(null); try { - const result = await updateDistributionBoard( - projectId, - editingBoard.id, - { - floorId: editingBoardFloorId || null, - supplyType: editingBoardSupplyType, - simultaneityFactor, - feedingDistributionBoardId: editingFeedingBoardId || null, - feedingCircuitId: editingFeedingCircuitId || null, - ...fromDistributionBoardInfoDraft(editingBoardInfo), - }, - project.currentRevision - ); - setBoards((current) => - current.map((board) => - board.id === result.distributionBoard.id - ? result.distributionBoard - : board - ) - ); + const boardInput = { + floorId: editingBoardFloorId || null, + supplyType: editingBoardSupplyType, + simultaneityFactor, + feedingDistributionBoardId: editingFeedingBoardId || null, + feedingCircuitId: editingFeedingCircuitId || null, + ...fromDistributionBoardInfoDraft(editingBoardInfo), + }; + let result; + if (isCreatingFeederCircuit) { + if (!editingFeedingBoardId || !selectedFeedingTree) { + throw new Error("Bitte eine speisende Verteilung wählen."); + } + if (!selectedNewFeederCircuitSection) { + throw new Error( + "Bitte eine Stromkreisgruppe für den neuen Abgang wählen." + ); + } + const circuit = buildFeederCircuitSnapshot({ + id: createClientId(), + tree: selectedFeedingTree, + sectionId: selectedNewFeederCircuitSection.id, + displayName: editingFeederCircuitDisplayName, + }); + result = await createFeederCircuitAndUpdateDistributionBoard( + projectId, + editingBoard.id, + { + ...boardInput, + feedingDistributionBoardId: editingFeedingBoardId, + feedingCircuitId: circuit.id, + }, + circuit, + project.currentRevision + ); + } else { + result = await updateDistributionBoard( + projectId, + editingBoard.id, + boardInput, + project.currentRevision + ); + } applyProjectRevision(result.history.currentRevision); - setCircuitTrees((await getProjectCircuitTrees(projectId)).trees); + const [updatedBoards, updatedTrees] = await Promise.all([ + listDistributionBoards(projectId), + getProjectCircuitTrees(projectId), + ]); + setBoards(updatedBoards); + setCircuitTrees(updatedTrees.trees); setEditingBoard(null); } catch (err) { setError( @@ -1567,8 +1627,16 @@ export default function ProjectDetailPage() { className="form-select" id="feeding-distribution-board" onChange={(event) => { - setEditingFeedingBoardId(event.target.value); + const boardId = event.target.value; + setEditingFeedingBoardId(boardId); setEditingFeedingCircuitId(""); + setIsCreatingFeederCircuit(false); + setEditingFeederCircuitSectionId( + getDefaultFeederCircuitSection( + circuitTreeByBoardId.get(boardId) + )?.id ?? "" + ); + setEditingFeederCircuitDisplayName(""); }} value={editingFeedingBoardId} > @@ -1588,10 +1656,31 @@ export default function ProjectDetailPage() { className="form-select" disabled={!editingFeedingBoardId} id="feeding-circuit" - onChange={(event) => setEditingFeedingCircuitId(event.target.value)} - value={editingFeedingCircuitId} + onChange={(event) => { + if (event.target.value === createFeederCircuitSelectValue) { + setEditingFeedingCircuitId(""); + setIsCreatingFeederCircuit(true); + setEditingFeederCircuitSectionId( + (current) => + current || + getDefaultFeederCircuitSection(selectedFeedingTree)?.id || + "" + ); + return; + } + setIsCreatingFeederCircuit(false); + setEditingFeedingCircuitId(event.target.value); + }} + value={ + isCreatingFeederCircuit + ? createFeederCircuitSelectValue + : editingFeedingCircuitId + } > + {selectedFeedingCircuits.map(({ circuit }) => (