Compare commits
No commits in common. "9165b8238c2ed67228ee0af9db5556fdf77e434c" and "552362bad1be045c3ade67d51cf4eee3a8707f31" have entirely different histories.
9165b8238c
...
552362bad1
22 changed files with 54 additions and 1141 deletions
11
AGENTS.md
11
AGENTS.md
|
|
@ -476,17 +476,6 @@ 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
|
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
|
on one circuit are supported. A board-only link affects hierarchy but adds no
|
||||||
load until a feeding circuit is selected.
|
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 floor assignment and a project-enabled supply type use
|
||||||
`distribution-board.update`; both values are snapshot/export fields and one
|
`distribution-board.update`; both values are snapshot/export fields and one
|
||||||
persistent undo step.
|
persistent undo step.
|
||||||
|
|
|
||||||
|
|
@ -269,13 +269,6 @@ commands. Endpoints, payloads and calculation rules: `docs/estimate-balance.md`.
|
||||||
submitted info fields, are restored together by persistent Undo/Redo
|
submitted info fields, are restored together by persistent Undo/Redo
|
||||||
- `fedFrom` remains free text for external feeds such as HAK, metering,
|
- `fedFrom` remains free text for external feeds such as HAK, metering,
|
||||||
transformer or generator and for supplementary notes
|
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`
|
- `POST /projects/:projectId/distribution-boards/:distributionBoardId/copy`
|
||||||
- body: `{ "name": "UV-02 Kopie", "expectedRevision": 14 }`
|
- body: `{ "name": "UV-02 Kopie", "expectedRevision": 14 }`
|
||||||
- duplicates the complete distribution-board subtree with new owning UUIDs
|
- duplicates the complete distribution-board subtree with new owning UUIDs
|
||||||
|
|
@ -353,9 +346,7 @@ commands. Endpoints, payloads and calculation rules: `docs/estimate-balance.md`.
|
||||||
footer components. Circuit and component protection devices use their
|
footer components. Circuit and component protection devices use their
|
||||||
separate one-to-one DTOs. Calculated row, circuit, section and board totals
|
separate one-to-one DTOs. Calculated row, circuit, section and board totals
|
||||||
remain included. Circuit totals contain the demand of linked downstream
|
remain included. Circuit totals contain the demand of linked downstream
|
||||||
boards. `downstreamBoardLoads` exposes those contributions as read-only
|
boards. `downstreamBoardLoads` exposes those contributions as read-only rows;
|
||||||
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.
|
`feedingReference` resolves the current board/circuit labels by id.
|
||||||
|
|
||||||
Response sketch:
|
Response sketch:
|
||||||
|
|
|
||||||
|
|
@ -477,21 +477,6 @@ Verweise nicht. Der speisende Stromkreis muss zur speisenden Verteilung gehören
|
||||||
Selbstverweise und Zyklen werden abgelehnt. Löschbefehle für referenzierte
|
Selbstverweise und Zyklen werden abgelehnt. Löschbefehle für referenzierte
|
||||||
Verteilungen und Stromkreise, einschließlich Unterbaum- und Undo-Pfaden, werden
|
Verteilungen und Stromkreise, einschließlich Unterbaum- und Undo-Pfaden, werden
|
||||||
vor der Datenbankänderung fachlich gesperrt.
|
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
|
Die projektweite Circuit-Tree-Projektion berechnet rekursiv die Lasten aller
|
||||||
Verteilungen. Die Leistung einer Unterverteilung nach ihrem eigenen
|
Verteilungen. Die Leistung einer Unterverteilung nach ihrem eigenen
|
||||||
Verteiler-GZF wird als virtuelle, schreibgeschützte Zeile genau dem gewählten
|
Verteiler-GZF wird als virtuelle, schreibgeschützte Zeile genau dem gewählten
|
||||||
|
|
|
||||||
|
|
@ -38,9 +38,8 @@ automatisch bei späteren Änderungen in einem anderen Tab.
|
||||||
3. **Verteilerpläne (quer, je Verteiler)** – Titelleiste mit
|
3. **Verteilerpläne (quer, je Verteiler)** – Titelleiste mit
|
||||||
Verteilernummer, Bezeichnung, Örtlichkeit, Netzart, „Gespeist von“ und
|
Verteilernummer, Bezeichnung, Örtlichkeit, Netzart, „Gespeist von“ und
|
||||||
P<sub>gl</sub>; darunter Kopfkomponenten, Gruppen, Stromkreise
|
P<sub>gl</sub>; darunter Kopfkomponenten, Gruppen, Stromkreise
|
||||||
(reine 1:1-Einspeiseabgänge kompakt in einer Zeile; sonst mehrzeilig mit
|
(mehrzeilige mit Sammelzeile, eingerückten Gerätezeilen und
|
||||||
Sammelzeile, eingerückten Gerätezeilen und schreibgeschützten
|
schreibgeschützten Unterverteilungszeilen),
|
||||||
Unterverteilungszeilen),
|
|
||||||
Gruppensummen und Verteilersumme. Fehlende Kabelangaben sind als „offen“
|
Gruppensummen und Verteilersumme. Fehlende Kabelangaben sind als „offen“
|
||||||
markiert. Die Titelleiste steht im Tabellenkopf und wiederholt sich beim
|
markiert. Die Titelleiste steht im Tabellenkopf und wiederholt sich beim
|
||||||
Drucken auf Folgeseiten.
|
Drucken auf Folgeseiten.
|
||||||
|
|
|
||||||
|
|
@ -1537,10 +1537,6 @@ a.kpi:hover {
|
||||||
background: var(--panel-bg-subtle);
|
background: var(--panel-bg-subtle);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-grid .feeder-circuit-row td:nth-child(2) {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tree-grid .device-row td:first-child {
|
.tree-grid .device-row td:first-child {
|
||||||
color: var(--text-faint);
|
color: var(--text-faint);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,6 @@ import {
|
||||||
import {
|
import {
|
||||||
copyGlobalDeviceToProject,
|
copyGlobalDeviceToProject,
|
||||||
copyDistributionBoard,
|
copyDistributionBoard,
|
||||||
createFeederCircuitAndUpdateDistributionBoard,
|
|
||||||
copyProjectDeviceToGlobal,
|
copyProjectDeviceToGlobal,
|
||||||
createDistributionBoard,
|
createDistributionBoard,
|
||||||
createProjectDevice,
|
createProjectDevice,
|
||||||
|
|
@ -72,12 +71,6 @@ import {
|
||||||
updateProjectDevice,
|
updateProjectDevice,
|
||||||
updateProjectSettings,
|
updateProjectSettings,
|
||||||
} from "../../../frontend/utils/api";
|
} from "../../../frontend/utils/api";
|
||||||
import { createClientId } from "../../../frontend/utils/client-id";
|
|
||||||
import {
|
|
||||||
buildFeederCircuitSnapshot,
|
|
||||||
getDefaultFeederCircuitSection,
|
|
||||||
getNextFeederCircuitIdentifier,
|
|
||||||
} from "../../../frontend/utils/distribution-board-feeder-circuit";
|
|
||||||
import type {
|
import type {
|
||||||
BuildingDto,
|
BuildingDto,
|
||||||
CircuitListDto,
|
CircuitListDto,
|
||||||
|
|
@ -153,7 +146,6 @@ const boardFilterCollapsedStorageKey =
|
||||||
"leistungsbilanz:overview-filter-collapsed:distribution-boards";
|
"leistungsbilanz:overview-filter-collapsed:distribution-boards";
|
||||||
const projectDeviceFilterCollapsedStorageKey =
|
const projectDeviceFilterCollapsedStorageKey =
|
||||||
"leistungsbilanz:overview-filter-collapsed:project-devices";
|
"leistungsbilanz:overview-filter-collapsed:project-devices";
|
||||||
const createFeederCircuitSelectValue = "__create_feeder_circuit__";
|
|
||||||
|
|
||||||
export default function ProjectDetailPage() {
|
export default function ProjectDetailPage() {
|
||||||
const params = useParams<{ projectId: string }>();
|
const params = useParams<{ projectId: string }>();
|
||||||
|
|
@ -183,12 +175,6 @@ export default function ProjectDetailPage() {
|
||||||
const [editingBoardCopyName, setEditingBoardCopyName] = useState("");
|
const [editingBoardCopyName, setEditingBoardCopyName] = useState("");
|
||||||
const [editingFeedingBoardId, setEditingFeedingBoardId] = useState("");
|
const [editingFeedingBoardId, setEditingFeedingBoardId] = useState("");
|
||||||
const [editingFeedingCircuitId, setEditingFeedingCircuitId] = useState("");
|
const [editingFeedingCircuitId, setEditingFeedingCircuitId] = useState("");
|
||||||
const [isCreatingFeederCircuit, setIsCreatingFeederCircuit] =
|
|
||||||
useState(false);
|
|
||||||
const [editingFeederCircuitSectionId, setEditingFeederCircuitSectionId] =
|
|
||||||
useState("");
|
|
||||||
const [editingFeederCircuitDisplayName, setEditingFeederCircuitDisplayName] =
|
|
||||||
useState("");
|
|
||||||
const [editingBoardInfo, setEditingBoardInfo] =
|
const [editingBoardInfo, setEditingBoardInfo] =
|
||||||
useState<DistributionBoardInfoDraft>(
|
useState<DistributionBoardInfoDraft>(
|
||||||
toDistributionBoardInfoDraft({
|
toDistributionBoardInfoDraft({
|
||||||
|
|
@ -364,21 +350,6 @@ export default function ProjectDetailPage() {
|
||||||
const selectedFeedingCircuit = selectedFeedingCircuits.find(
|
const selectedFeedingCircuit = selectedFeedingCircuits.find(
|
||||||
({ circuit }) => circuit.id === editingFeedingCircuitId
|
({ 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(
|
const selectedFeedingBoard = boards.find(
|
||||||
(board) => board.id === editingFeedingBoardId
|
(board) => board.id === editingFeedingBoardId
|
||||||
);
|
);
|
||||||
|
|
@ -585,9 +556,6 @@ export default function ProjectDetailPage() {
|
||||||
setEditingBoardInfo(toDistributionBoardInfoDraft(board));
|
setEditingBoardInfo(toDistributionBoardInfoDraft(board));
|
||||||
setEditingFeedingBoardId(board.feedingDistributionBoardId ?? "");
|
setEditingFeedingBoardId(board.feedingDistributionBoardId ?? "");
|
||||||
setEditingFeedingCircuitId(board.feedingCircuitId ?? "");
|
setEditingFeedingCircuitId(board.feedingCircuitId ?? "");
|
||||||
setIsCreatingFeederCircuit(false);
|
|
||||||
setEditingFeederCircuitSectionId("");
|
|
||||||
setEditingFeederCircuitDisplayName("");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function openBoardCreator() {
|
function openBoardCreator() {
|
||||||
|
|
@ -616,56 +584,28 @@ export default function ProjectDetailPage() {
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const boardInput = {
|
const result = await updateDistributionBoard(
|
||||||
|
projectId,
|
||||||
|
editingBoard.id,
|
||||||
|
{
|
||||||
floorId: editingBoardFloorId || null,
|
floorId: editingBoardFloorId || null,
|
||||||
supplyType: editingBoardSupplyType,
|
supplyType: editingBoardSupplyType,
|
||||||
simultaneityFactor,
|
simultaneityFactor,
|
||||||
feedingDistributionBoardId: editingFeedingBoardId || null,
|
feedingDistributionBoardId: editingFeedingBoardId || null,
|
||||||
feedingCircuitId: editingFeedingCircuitId || null,
|
feedingCircuitId: editingFeedingCircuitId || null,
|
||||||
...fromDistributionBoardInfoDraft(editingBoardInfo),
|
...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
|
project.currentRevision
|
||||||
);
|
);
|
||||||
} else {
|
setBoards((current) =>
|
||||||
result = await updateDistributionBoard(
|
current.map((board) =>
|
||||||
projectId,
|
board.id === result.distributionBoard.id
|
||||||
editingBoard.id,
|
? result.distributionBoard
|
||||||
boardInput,
|
: board
|
||||||
project.currentRevision
|
)
|
||||||
);
|
);
|
||||||
}
|
|
||||||
applyProjectRevision(result.history.currentRevision);
|
applyProjectRevision(result.history.currentRevision);
|
||||||
const [updatedBoards, updatedTrees] = await Promise.all([
|
setCircuitTrees((await getProjectCircuitTrees(projectId)).trees);
|
||||||
listDistributionBoards(projectId),
|
|
||||||
getProjectCircuitTrees(projectId),
|
|
||||||
]);
|
|
||||||
setBoards(updatedBoards);
|
|
||||||
setCircuitTrees(updatedTrees.trees);
|
|
||||||
setEditingBoard(null);
|
setEditingBoard(null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(
|
setError(
|
||||||
|
|
@ -1627,16 +1567,8 @@ export default function ProjectDetailPage() {
|
||||||
className="form-select"
|
className="form-select"
|
||||||
id="feeding-distribution-board"
|
id="feeding-distribution-board"
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
const boardId = event.target.value;
|
setEditingFeedingBoardId(event.target.value);
|
||||||
setEditingFeedingBoardId(boardId);
|
|
||||||
setEditingFeedingCircuitId("");
|
setEditingFeedingCircuitId("");
|
||||||
setIsCreatingFeederCircuit(false);
|
|
||||||
setEditingFeederCircuitSectionId(
|
|
||||||
getDefaultFeederCircuitSection(
|
|
||||||
circuitTreeByBoardId.get(boardId)
|
|
||||||
)?.id ?? ""
|
|
||||||
);
|
|
||||||
setEditingFeederCircuitDisplayName("");
|
|
||||||
}}
|
}}
|
||||||
value={editingFeedingBoardId}
|
value={editingFeedingBoardId}
|
||||||
>
|
>
|
||||||
|
|
@ -1656,31 +1588,10 @@ export default function ProjectDetailPage() {
|
||||||
className="form-select"
|
className="form-select"
|
||||||
disabled={!editingFeedingBoardId}
|
disabled={!editingFeedingBoardId}
|
||||||
id="feeding-circuit"
|
id="feeding-circuit"
|
||||||
onChange={(event) => {
|
onChange={(event) => setEditingFeedingCircuitId(event.target.value)}
|
||||||
if (event.target.value === createFeederCircuitSelectValue) {
|
value={editingFeedingCircuitId}
|
||||||
setEditingFeedingCircuitId("");
|
|
||||||
setIsCreatingFeederCircuit(true);
|
|
||||||
setEditingFeederCircuitSectionId(
|
|
||||||
(current) =>
|
|
||||||
current ||
|
|
||||||
getDefaultFeederCircuitSection(selectedFeedingTree)?.id ||
|
|
||||||
""
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setIsCreatingFeederCircuit(false);
|
|
||||||
setEditingFeedingCircuitId(event.target.value);
|
|
||||||
}}
|
|
||||||
value={
|
|
||||||
isCreatingFeederCircuit
|
|
||||||
? createFeederCircuitSelectValue
|
|
||||||
: editingFeedingCircuitId
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<option value="">Nur Verteilung bekannt</option>
|
<option value="">Nur Verteilung bekannt</option>
|
||||||
<option value={createFeederCircuitSelectValue}>
|
|
||||||
+ Neuen Einspeiseabgang anlegen …
|
|
||||||
</option>
|
|
||||||
{selectedFeedingCircuits.map(({ circuit }) => (
|
{selectedFeedingCircuits.map(({ circuit }) => (
|
||||||
<option key={circuit.id} value={circuit.id}>
|
<option key={circuit.id} value={circuit.id}>
|
||||||
{circuit.equipmentIdentifier}
|
{circuit.equipmentIdentifier}
|
||||||
|
|
@ -1689,62 +1600,7 @@ export default function ProjectDetailPage() {
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
{isCreatingFeederCircuit ? (
|
{editingFeedingBoardId && !editingFeedingCircuitId ? (
|
||||||
<div className="col-12">
|
|
||||||
<div className="border rounded p-3">
|
|
||||||
<div className="row g-3">
|
|
||||||
<div className="col-12 col-md-6">
|
|
||||||
<label className="form-label" htmlFor="new-feeder-circuit-section">
|
|
||||||
Stromkreisgruppe
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
className="form-select"
|
|
||||||
id="new-feeder-circuit-section"
|
|
||||||
onChange={(event) =>
|
|
||||||
setEditingFeederCircuitSectionId(event.target.value)
|
|
||||||
}
|
|
||||||
required
|
|
||||||
value={editingFeederCircuitSectionId}
|
|
||||||
>
|
|
||||||
{feederCircuitSections.map((section) => (
|
|
||||||
<option key={section.id} value={section.id}>
|
|
||||||
{section.displayName}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
<div className="form-text">
|
|
||||||
Vorgesehenes BMK: {newFeederCircuitIdentifier ?? "–"}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="col-12 col-md-6">
|
|
||||||
<label className="form-label" htmlFor="new-feeder-circuit-name">
|
|
||||||
Abgangsbezeichnung (optional)
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
className="form-control"
|
|
||||||
id="new-feeder-circuit-name"
|
|
||||||
onChange={(event) =>
|
|
||||||
setEditingFeederCircuitDisplayName(event.target.value)
|
|
||||||
}
|
|
||||||
placeholder="z. B. Steigetrasse Ost"
|
|
||||||
value={editingFeederCircuitDisplayName}
|
|
||||||
/>
|
|
||||||
<div className="form-text">
|
|
||||||
Ohne Zusatzbezeichnung wird der Name der gespeisten
|
|
||||||
Verteilung angezeigt.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="form-text mt-2">
|
|
||||||
Der Abgang wird erst beim Speichern angelegt und gemeinsam
|
|
||||||
mit dieser Verteilung verknüpft. Es entsteht keine Gerätezeile.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{editingFeedingBoardId &&
|
|
||||||
!editingFeedingCircuitId &&
|
|
||||||
!isCreatingFeederCircuit ? (
|
|
||||||
<div className="col-12">
|
<div className="col-12">
|
||||||
<div className="alert alert-warning mb-0 py-2">
|
<div className="alert alert-warning mb-0 py-2">
|
||||||
Ohne speisenden Abgang wird die Hierarchie angezeigt, die
|
Ohne speisenden Abgang wird die Hierarchie angezeigt, die
|
||||||
|
|
@ -1762,8 +1618,8 @@ export default function ProjectDetailPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{selectedFeedingCircuitCategory !== undefined &&
|
{selectedFeedingCircuit &&
|
||||||
selectedFeedingCircuitCategory !== "three_phase" ? (
|
selectedFeedingCircuit.section.category !== "three_phase" ? (
|
||||||
<div className="col-12">
|
<div className="col-12">
|
||||||
<div className="alert alert-warning mb-0 py-2">
|
<div className="alert alert-warning mb-0 py-2">
|
||||||
Der gewählte Abgang ist 1-phasig. Bitte prüfen, ob diese
|
Der gewählte Abgang ist 1-phasig. Bitte prüfen, ob diese
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ export class CircuitStructureProjectCommandRepository
|
||||||
this.database,
|
this.database,
|
||||||
input,
|
input,
|
||||||
(tx) =>
|
(tx) =>
|
||||||
this.applyInTransaction(
|
this.applyCommand(
|
||||||
tx,
|
tx,
|
||||||
input.projectId,
|
input.projectId,
|
||||||
input.source,
|
input.source,
|
||||||
|
|
@ -49,11 +49,7 @@ export class CircuitStructureProjectCommandRepository
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private applyCommand(
|
||||||
* Applies the circuit write inside an already-owned project transaction.
|
|
||||||
* Composite project commands use this without creating a second revision.
|
|
||||||
*/
|
|
||||||
applyInTransaction(
|
|
||||||
database: AppDatabase,
|
database: AppDatabase,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
source: ExecuteCircuitStructureCommandInput["source"],
|
source: ExecuteCircuitStructureCommandInput["source"],
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { and, asc, eq, inArray } from "drizzle-orm";
|
import { and, asc, eq, inArray } from "drizzle-orm";
|
||||||
import { isDeepStrictEqual } from "node:util";
|
|
||||||
import {
|
import {
|
||||||
assertDistributionBoardDeleteProjectCommand,
|
assertDistributionBoardDeleteProjectCommand,
|
||||||
assertDistributionBoardInsertProjectCommand,
|
assertDistributionBoardInsertProjectCommand,
|
||||||
|
|
@ -18,20 +17,6 @@ import {
|
||||||
type DistributionBoardUpdateProjectCommand,
|
type DistributionBoardUpdateProjectCommand,
|
||||||
type DistributionBoardUpdateValues,
|
type DistributionBoardUpdateValues,
|
||||||
} from "../../domain/models/distribution-board-project-command.model.js";
|
} from "../../domain/models/distribution-board-project-command.model.js";
|
||||||
import {
|
|
||||||
assertDistributionBoardFeederCircuitProjectCommand,
|
|
||||||
createDistributionBoardCreateFeederCircuitProjectCommand,
|
|
||||||
createDistributionBoardRemoveFeederCircuitProjectCommand,
|
|
||||||
distributionBoardCreateFeederCircuitCommandType,
|
|
||||||
distributionBoardRemoveFeederCircuitCommandType,
|
|
||||||
type DistributionBoardFeederCircuitProjectCommand,
|
|
||||||
} from "../../domain/models/distribution-board-feeder-circuit-project-command.model.js";
|
|
||||||
import {
|
|
||||||
circuitDeleteCommandType,
|
|
||||||
circuitInsertCommandType,
|
|
||||||
createCircuitDeleteProjectCommand,
|
|
||||||
createCircuitInsertProjectCommand,
|
|
||||||
} from "../../domain/models/circuit-structure-project-command.model.js";
|
|
||||||
import type {
|
import type {
|
||||||
DistributionBoardStructureProjectCommandStore,
|
DistributionBoardStructureProjectCommandStore,
|
||||||
ExecuteDistributionBoardStructureCommandInput,
|
ExecuteDistributionBoardStructureCommandInput,
|
||||||
|
|
@ -46,7 +31,6 @@ import { floors } from "../schema/floors.js";
|
||||||
import { projects } from "../schema/projects.js";
|
import { projects } from "../schema/projects.js";
|
||||||
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
||||||
import { assertBoardsAreNotReferencedAsFeeders } from "./distribution-board-feeding-deletion-guard.persistence.js";
|
import { assertBoardsAreNotReferencedAsFeeders } from "./distribution-board-feeding-deletion-guard.persistence.js";
|
||||||
import { CircuitStructureProjectCommandRepository } from "./circuit-structure-project-command.repository.js";
|
|
||||||
|
|
||||||
export class DistributionBoardStructureProjectCommandRepository
|
export class DistributionBoardStructureProjectCommandRepository
|
||||||
implements DistributionBoardStructureProjectCommandStore
|
implements DistributionBoardStructureProjectCommandStore
|
||||||
|
|
@ -78,30 +62,6 @@ export class DistributionBoardStructureProjectCommandRepository
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
executeFeederCircuit(
|
|
||||||
input: Omit<
|
|
||||||
ExecuteDistributionBoardStructureCommandInput,
|
|
||||||
"command"
|
|
||||||
> & {
|
|
||||||
command: DistributionBoardFeederCircuitProjectCommand;
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
assertDistributionBoardFeederCircuitProjectCommand(input.command);
|
|
||||||
if (
|
|
||||||
input.source === "user" &&
|
|
||||||
input.command.type === distributionBoardRemoveFeederCircuitCommandType
|
|
||||||
) {
|
|
||||||
throw new Error(
|
|
||||||
"Generated feeder circuits may only be removed through project history."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return executeProjectCommandTransaction(
|
|
||||||
this.database,
|
|
||||||
input,
|
|
||||||
(tx) => this.applyFeederCircuitCommand(tx, input)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private applyCommand(
|
private applyCommand(
|
||||||
database: AppDatabase,
|
database: AppDatabase,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
|
|
@ -407,64 +367,6 @@ export class DistributionBoardStructureProjectCommandRepository
|
||||||
return inverse;
|
return inverse;
|
||||||
}
|
}
|
||||||
|
|
||||||
private applyFeederCircuitCommand(
|
|
||||||
database: AppDatabase,
|
|
||||||
input: Omit<
|
|
||||||
ExecuteDistributionBoardStructureCommandInput,
|
|
||||||
"command"
|
|
||||||
> & {
|
|
||||||
command: DistributionBoardFeederCircuitProjectCommand;
|
|
||||||
}
|
|
||||||
): DistributionBoardFeederCircuitProjectCommand {
|
|
||||||
const circuitRepository =
|
|
||||||
new CircuitStructureProjectCommandRepository(database);
|
|
||||||
const { circuit, boardUpdate } = input.command.payload;
|
|
||||||
if (
|
|
||||||
input.command.type === distributionBoardCreateFeederCircuitCommandType
|
|
||||||
) {
|
|
||||||
const circuitInverse = circuitRepository.applyInTransaction(
|
|
||||||
database,
|
|
||||||
input.projectId,
|
|
||||||
input.source,
|
|
||||||
createCircuitInsertProjectCommand(circuit)
|
|
||||||
);
|
|
||||||
if (circuitInverse.type !== circuitDeleteCommandType) {
|
|
||||||
throw new Error("Created feeder circuit returned an invalid inverse.");
|
|
||||||
}
|
|
||||||
const boardInverse = this.update(database, {
|
|
||||||
...input,
|
|
||||||
command: boardUpdate,
|
|
||||||
});
|
|
||||||
return createDistributionBoardRemoveFeederCircuitProjectCommand(
|
|
||||||
circuit,
|
|
||||||
boardInverse
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const boardInverse = this.update(database, {
|
|
||||||
...input,
|
|
||||||
command: boardUpdate,
|
|
||||||
});
|
|
||||||
const circuitInverse = circuitRepository.applyInTransaction(
|
|
||||||
database,
|
|
||||||
input.projectId,
|
|
||||||
input.source,
|
|
||||||
createCircuitDeleteProjectCommand(circuit.id, circuit.circuitListId)
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
circuitInverse.type !== circuitInsertCommandType ||
|
|
||||||
!isDeepStrictEqual(circuitInverse.payload.circuit, circuit)
|
|
||||||
) {
|
|
||||||
throw new Error(
|
|
||||||
"Generated feeder circuit changed before history removal."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return createDistributionBoardCreateFeederCircuitProjectCommand(
|
|
||||||
circuit,
|
|
||||||
boardInverse
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private assertFeedingReference(
|
private assertFeedingReference(
|
||||||
database: AppDatabase,
|
database: AppDatabase,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
|
|
|
||||||
|
|
@ -1,132 +0,0 @@
|
||||||
import {
|
|
||||||
assertCircuitInsertProjectCommand,
|
|
||||||
circuitInsertCommandType,
|
|
||||||
circuitStructureCommandSchemaVersion,
|
|
||||||
type CircuitSnapshot,
|
|
||||||
} from "./circuit-structure-project-command.model.js";
|
|
||||||
import {
|
|
||||||
assertDistributionBoardUpdateProjectCommand,
|
|
||||||
type DistributionBoardUpdateProjectCommand,
|
|
||||||
} from "./distribution-board-project-command.model.js";
|
|
||||||
import type { SerializedProjectCommand } from "./project-command.model.js";
|
|
||||||
|
|
||||||
export const distributionBoardCreateFeederCircuitCommandType =
|
|
||||||
"distribution-board.create-feeder-circuit" as const;
|
|
||||||
export const distributionBoardRemoveFeederCircuitCommandType =
|
|
||||||
"distribution-board.remove-feeder-circuit" as const;
|
|
||||||
export const distributionBoardFeederCircuitCommandSchemaVersion = 1 as const;
|
|
||||||
|
|
||||||
export interface DistributionBoardFeederCircuitCommandPayload {
|
|
||||||
circuit: CircuitSnapshot;
|
|
||||||
boardUpdate: DistributionBoardUpdateProjectCommand;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DistributionBoardCreateFeederCircuitProjectCommand
|
|
||||||
extends SerializedProjectCommand<DistributionBoardFeederCircuitCommandPayload> {
|
|
||||||
schemaVersion: typeof distributionBoardFeederCircuitCommandSchemaVersion;
|
|
||||||
type: typeof distributionBoardCreateFeederCircuitCommandType;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DistributionBoardRemoveFeederCircuitProjectCommand
|
|
||||||
extends SerializedProjectCommand<DistributionBoardFeederCircuitCommandPayload> {
|
|
||||||
schemaVersion: typeof distributionBoardFeederCircuitCommandSchemaVersion;
|
|
||||||
type: typeof distributionBoardRemoveFeederCircuitCommandType;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type DistributionBoardFeederCircuitProjectCommand =
|
|
||||||
| DistributionBoardCreateFeederCircuitProjectCommand
|
|
||||||
| DistributionBoardRemoveFeederCircuitProjectCommand;
|
|
||||||
|
|
||||||
export function createDistributionBoardCreateFeederCircuitProjectCommand(
|
|
||||||
circuit: CircuitSnapshot,
|
|
||||||
boardUpdate: DistributionBoardUpdateProjectCommand
|
|
||||||
): DistributionBoardCreateFeederCircuitProjectCommand {
|
|
||||||
const command: DistributionBoardCreateFeederCircuitProjectCommand = {
|
|
||||||
schemaVersion: distributionBoardFeederCircuitCommandSchemaVersion,
|
|
||||||
type: distributionBoardCreateFeederCircuitCommandType,
|
|
||||||
payload: { circuit, boardUpdate },
|
|
||||||
};
|
|
||||||
assertDistributionBoardFeederCircuitProjectCommand(command);
|
|
||||||
return command;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createDistributionBoardRemoveFeederCircuitProjectCommand(
|
|
||||||
circuit: CircuitSnapshot,
|
|
||||||
boardUpdate: DistributionBoardUpdateProjectCommand
|
|
||||||
): DistributionBoardRemoveFeederCircuitProjectCommand {
|
|
||||||
const command: DistributionBoardRemoveFeederCircuitProjectCommand = {
|
|
||||||
schemaVersion: distributionBoardFeederCircuitCommandSchemaVersion,
|
|
||||||
type: distributionBoardRemoveFeederCircuitCommandType,
|
|
||||||
payload: { circuit, boardUpdate },
|
|
||||||
};
|
|
||||||
assertDistributionBoardFeederCircuitProjectCommand(command);
|
|
||||||
return command;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function assertDistributionBoardFeederCircuitProjectCommand(
|
|
||||||
command: SerializedProjectCommand<unknown>
|
|
||||||
): asserts command is DistributionBoardFeederCircuitProjectCommand {
|
|
||||||
if (
|
|
||||||
command.schemaVersion !==
|
|
||||||
distributionBoardFeederCircuitCommandSchemaVersion ||
|
|
||||||
(command.type !== distributionBoardCreateFeederCircuitCommandType &&
|
|
||||||
command.type !== distributionBoardRemoveFeederCircuitCommandType) ||
|
|
||||||
!isPlainObject(command.payload)
|
|
||||||
) {
|
|
||||||
throw new Error("Unsupported distribution-board feeder-circuit command.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const { circuit, boardUpdate } = command.payload;
|
|
||||||
if (!isPlainObject(boardUpdate)) {
|
|
||||||
throw new Error("Feeder-circuit command requires a board update.");
|
|
||||||
}
|
|
||||||
assertCircuitInsertProjectCommand({
|
|
||||||
schemaVersion: circuitStructureCommandSchemaVersion,
|
|
||||||
type: circuitInsertCommandType,
|
|
||||||
payload: { circuit },
|
|
||||||
});
|
|
||||||
const boardUpdateCommand =
|
|
||||||
boardUpdate as unknown as SerializedProjectCommand<unknown>;
|
|
||||||
assertDistributionBoardUpdateProjectCommand(boardUpdateCommand);
|
|
||||||
|
|
||||||
const typedCircuit = circuit as CircuitSnapshot;
|
|
||||||
const typedBoardUpdate: DistributionBoardUpdateProjectCommand =
|
|
||||||
boardUpdateCommand;
|
|
||||||
if (typedCircuit.deviceRows.length !== 0 || !typedCircuit.isReserve) {
|
|
||||||
throw new Error(
|
|
||||||
"A generated feeder circuit must be empty before the board load is linked."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const feedingBoard = getChangeValue(
|
|
||||||
typedBoardUpdate,
|
|
||||||
"feedingDistributionBoardId"
|
|
||||||
);
|
|
||||||
const feedingCircuit = getChangeValue(
|
|
||||||
typedBoardUpdate,
|
|
||||||
"feedingCircuitId"
|
|
||||||
);
|
|
||||||
if (feedingBoard === undefined || feedingCircuit === undefined) {
|
|
||||||
throw new Error(
|
|
||||||
"Feeder-circuit commands must update both logical feeding references."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
command.type === distributionBoardCreateFeederCircuitCommandType &&
|
|
||||||
(typeof feedingBoard !== "string" || feedingCircuit !== typedCircuit.id)
|
|
||||||
) {
|
|
||||||
throw new Error(
|
|
||||||
"The created feeder circuit must be linked by the distribution-board update."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getChangeValue(
|
|
||||||
command: DistributionBoardUpdateProjectCommand,
|
|
||||||
field: "feedingDistributionBoardId" | "feedingCircuitId"
|
|
||||||
) {
|
|
||||||
return command.payload.changes.find((change) => change.field === field)?.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
||||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import type { DistributionBoardStructureProjectCommand } from "../models/distribution-board-structure-project-command.model.js";
|
import type { DistributionBoardStructureProjectCommand } from "../models/distribution-board-structure-project-command.model.js";
|
||||||
import type { DistributionBoardUpdateProjectCommand } from "../models/distribution-board-project-command.model.js";
|
import type { DistributionBoardUpdateProjectCommand } from "../models/distribution-board-project-command.model.js";
|
||||||
import type { DistributionBoardFeederCircuitProjectCommand } from "../models/distribution-board-feeder-circuit-project-command.model.js";
|
|
||||||
import type {
|
import type {
|
||||||
AppendedProjectRevision,
|
AppendedProjectRevision,
|
||||||
ProjectRevisionSource,
|
ProjectRevisionSource,
|
||||||
|
|
@ -36,15 +35,4 @@ export interface DistributionBoardStructureProjectCommandStore {
|
||||||
revision: AppendedProjectRevision;
|
revision: AppendedProjectRevision;
|
||||||
inverse: DistributionBoardUpdateProjectCommand;
|
inverse: DistributionBoardUpdateProjectCommand;
|
||||||
};
|
};
|
||||||
executeFeederCircuit(
|
|
||||||
input: Omit<
|
|
||||||
ExecuteDistributionBoardStructureCommandInput,
|
|
||||||
"command"
|
|
||||||
> & {
|
|
||||||
command: DistributionBoardFeederCircuitProjectCommand;
|
|
||||||
}
|
|
||||||
): {
|
|
||||||
revision: AppendedProjectRevision;
|
|
||||||
inverse: DistributionBoardFeederCircuitProjectCommand;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,11 +45,6 @@ import {
|
||||||
assertDistributionBoardUpdateProjectCommand,
|
assertDistributionBoardUpdateProjectCommand,
|
||||||
distributionBoardUpdateCommandType,
|
distributionBoardUpdateCommandType,
|
||||||
} from "../models/distribution-board-project-command.model.js";
|
} from "../models/distribution-board-project-command.model.js";
|
||||||
import {
|
|
||||||
assertDistributionBoardFeederCircuitProjectCommand,
|
|
||||||
distributionBoardCreateFeederCircuitCommandType,
|
|
||||||
distributionBoardRemoveFeederCircuitCommandType,
|
|
||||||
} from "../models/distribution-board-feeder-circuit-project-command.model.js";
|
|
||||||
import {
|
import {
|
||||||
assertDistributionBoardComponentDeleteProjectCommand,
|
assertDistributionBoardComponentDeleteProjectCommand,
|
||||||
assertDistributionBoardComponentInsertProjectCommand,
|
assertDistributionBoardComponentInsertProjectCommand,
|
||||||
|
|
@ -423,14 +418,6 @@ export class ProjectCommandService implements ProjectCommandExecutor {
|
||||||
command: input.command,
|
command: input.command,
|
||||||
}).revision;
|
}).revision;
|
||||||
}
|
}
|
||||||
case distributionBoardCreateFeederCircuitCommandType:
|
|
||||||
case distributionBoardRemoveFeederCircuitCommandType: {
|
|
||||||
assertDistributionBoardFeederCircuitProjectCommand(input.command);
|
|
||||||
return this.distributionBoardStructureStore.executeFeederCircuit({
|
|
||||||
...input,
|
|
||||||
command: input.command,
|
|
||||||
}).revision;
|
|
||||||
}
|
|
||||||
case distributionBoardComponentInsertCommandType: {
|
case distributionBoardComponentInsertCommandType: {
|
||||||
assertDistributionBoardComponentInsertProjectCommand(
|
assertDistributionBoardComponentInsertProjectCommand(
|
||||||
input.command
|
input.command
|
||||||
|
|
|
||||||
|
|
@ -1766,7 +1766,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||||
const currentDisplay =
|
const currentDisplay =
|
||||||
mode === "replaceWithTypedChar" && cell.cellKey !== "phaseType"
|
mode === "replaceWithTypedChar" && cell.cellKey !== "phaseType"
|
||||||
? typedChar ?? ""
|
? typedChar ?? ""
|
||||||
: String(visibleCell.editValue ?? visibleCell.value ?? "");
|
: String(visibleCell.value ?? "");
|
||||||
focusTokenRef.current += 1;
|
focusTokenRef.current += 1;
|
||||||
setEditingCell({
|
setEditingCell({
|
||||||
...cell,
|
...cell,
|
||||||
|
|
@ -4077,9 +4077,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||||
key={row.rowKey}
|
key={row.rowKey}
|
||||||
className={`${
|
className={`${
|
||||||
row.rowType === "circuitSummary"
|
row.rowType === "circuitSummary"
|
||||||
? row.downstreamBoardLoad
|
? "summary-row"
|
||||||
? "feeder-circuit-row"
|
|
||||||
: "summary-row"
|
|
||||||
: row.rowType === "deviceRow"
|
: row.rowType === "deviceRow"
|
||||||
? "device-row"
|
? "device-row"
|
||||||
: row.rowType === "downstreamBoardLoad"
|
: row.rowType === "downstreamBoardLoad"
|
||||||
|
|
@ -4710,19 +4708,11 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
disabled={
|
disabled={isSaving}
|
||||||
isSaving ||
|
|
||||||
(row.circuit.downstreamBoardLoads?.length ?? 0) > 0
|
|
||||||
}
|
|
||||||
icon={Trash2}
|
icon={Trash2}
|
||||||
label="Stromkreis löschen"
|
label="Stromkreis löschen"
|
||||||
onClick={() => void handleDeleteCircuit(row.circuit!.id)}
|
onClick={() => void handleDeleteCircuit(row.circuit!.id)}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
title={
|
|
||||||
(row.circuit.downstreamBoardLoads?.length ?? 0) > 0
|
|
||||||
? "Speist eine Unterverteilung – zuerst deren Einspeisung ändern"
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
variant="danger"
|
variant="danger"
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -431,25 +431,6 @@ function CircuitRows({ circuit }: { circuit: CircuitTreeCircuitDto }) {
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (rows.length === 0 && downstreamBoards.length === 1) {
|
|
||||||
const boardLabel =
|
|
||||||
downstreamBoards[0].boardNumber ?? downstreamBoards[0].name;
|
|
||||||
return (
|
|
||||||
<tr className="pd-circuit">
|
|
||||||
<td className="pd-bmk">{circuit.equipmentIdentifier}</td>
|
|
||||||
<td>
|
|
||||||
{boardLabel}
|
|
||||||
{circuit.displayName ? (
|
|
||||||
<span className="pd-sub"> · {circuit.displayName}</span>
|
|
||||||
) : null}
|
|
||||||
</td>
|
|
||||||
<td>–</td>
|
|
||||||
<td /><td /><td />
|
|
||||||
<td className="pd-r"><strong>{formatKilowatt(circuit.circuitTotalPower)}</strong></td>
|
|
||||||
{circuitCells}
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (rows.length === 1 && downstreamBoards.length === 0) {
|
if (rows.length === 1 && downstreamBoards.length === 0) {
|
||||||
const row = rows[0];
|
const row = rows[0];
|
||||||
const title = circuit.displayName ?? row.displayName;
|
const title = circuit.displayName ?? row.displayName;
|
||||||
|
|
|
||||||
|
|
@ -366,7 +366,10 @@ export function createDistributionBoard(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UpdateDistributionBoardInput = {
|
export function updateDistributionBoard(
|
||||||
|
projectId: string,
|
||||||
|
distributionBoardId: string,
|
||||||
|
input: {
|
||||||
floorId: string | null;
|
floorId: string | null;
|
||||||
supplyType: NonNullable<DistributionBoardDto["supplyType"]>;
|
supplyType: NonNullable<DistributionBoardDto["supplyType"]>;
|
||||||
simultaneityFactor: number;
|
simultaneityFactor: number;
|
||||||
|
|
@ -376,12 +379,7 @@ export type UpdateDistributionBoardInput = {
|
||||||
DistributionBoardDto,
|
DistributionBoardDto,
|
||||||
"feedingDistributionBoardId" | "feedingCircuitId"
|
"feedingDistributionBoardId" | "feedingCircuitId"
|
||||||
>
|
>
|
||||||
>;
|
>,
|
||||||
|
|
||||||
export function updateDistributionBoard(
|
|
||||||
projectId: string,
|
|
||||||
distributionBoardId: string,
|
|
||||||
input: UpdateDistributionBoardInput,
|
|
||||||
expectedRevision: number
|
expectedRevision: number
|
||||||
) {
|
) {
|
||||||
return request<DistributionBoardCommandResultDto>(
|
return request<DistributionBoardCommandResultDto>(
|
||||||
|
|
@ -393,41 +391,6 @@ export function updateDistributionBoard(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createFeederCircuitAndUpdateDistributionBoard(
|
|
||||||
projectId: string,
|
|
||||||
distributionBoardId: string,
|
|
||||||
input: UpdateDistributionBoardInput & {
|
|
||||||
feedingDistributionBoardId: string;
|
|
||||||
feedingCircuitId: string;
|
|
||||||
},
|
|
||||||
circuit: CircuitSnapshot,
|
|
||||||
expectedRevision: number
|
|
||||||
) {
|
|
||||||
return executeProjectCommand(
|
|
||||||
projectId,
|
|
||||||
expectedRevision,
|
|
||||||
{
|
|
||||||
schemaVersion: 1,
|
|
||||||
type: "distribution-board.create-feeder-circuit",
|
|
||||||
payload: {
|
|
||||||
circuit,
|
|
||||||
boardUpdate: {
|
|
||||||
schemaVersion: 2,
|
|
||||||
type: "distribution-board.update",
|
|
||||||
payload: {
|
|
||||||
distributionBoardId,
|
|
||||||
changes: Object.entries(input).map(([field, value]) => ({
|
|
||||||
field,
|
|
||||||
value,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"Einspeiseabgang anlegen und Verteilung verknüpfen"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const documentLogoUrl = "/api/document-branding/logo";
|
export const documentLogoUrl = "/api/document-branding/logo";
|
||||||
|
|
||||||
export function getDocumentBranding() {
|
export function getDocumentBranding() {
|
||||||
|
|
|
||||||
|
|
@ -617,11 +617,7 @@ export function getCellKind(rowType: RowType, key: CellKey): CellKind {
|
||||||
if (rowType === "deviceRow") return deviceFieldKeys.has(key) ? "deviceField" : "readonly";
|
if (rowType === "deviceRow") return deviceFieldKeys.has(key) ? "deviceField" : "readonly";
|
||||||
if (rowType === "downstreamBoardLoad") return "readonly";
|
if (rowType === "downstreamBoardLoad") return "readonly";
|
||||||
if (rowType === "circuitSummary") return circuitFieldKeys.has(key) ? "circuitField" : "readonly";
|
if (rowType === "circuitSummary") return circuitFieldKeys.has(key) ? "circuitField" : "readonly";
|
||||||
if (rowType === "reserveCircuit") {
|
if (rowType === "reserveCircuit" || rowType === "circuitCompact") {
|
||||||
if (circuitFieldKeys.has(key)) return "circuitField";
|
|
||||||
if (deviceFieldKeys.has(key)) return "deviceField";
|
|
||||||
}
|
|
||||||
if (rowType === "circuitCompact") {
|
|
||||||
if (deviceFieldKeys.has(key)) return "deviceField";
|
if (deviceFieldKeys.has(key)) return "deviceField";
|
||||||
if (circuitFieldKeys.has(key)) return "circuitField";
|
if (circuitFieldKeys.has(key)) return "circuitField";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,8 +37,6 @@ export interface VisibleGridCell {
|
||||||
editable: boolean;
|
editable: boolean;
|
||||||
kind: CellKind;
|
kind: CellKind;
|
||||||
value: GridValue;
|
value: GridValue;
|
||||||
/** Separate persisted value when the displayed value is derived. */
|
|
||||||
editValue?: GridValue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VisibleGridRow {
|
export interface VisibleGridRow {
|
||||||
|
|
@ -146,7 +144,6 @@ function makeVisibleGridRow(
|
||||||
const kind = getCellKind(rowType, column.key);
|
const kind = getCellKind(rowType, column.key);
|
||||||
const editable = kind === "circuitField" || kind === "deviceField";
|
const editable = kind === "circuitField" || kind === "deviceField";
|
||||||
let value: GridValue;
|
let value: GridValue;
|
||||||
let editValue: GridValue | undefined;
|
|
||||||
|
|
||||||
if (rowType === "downstreamBoardLoad" && downstreamBoardLoad) {
|
if (rowType === "downstreamBoardLoad" && downstreamBoardLoad) {
|
||||||
value = column.key === "displayName"
|
value = column.key === "displayName"
|
||||||
|
|
@ -158,35 +155,6 @@ function makeVisibleGridRow(
|
||||||
: column.key === "remark"
|
: column.key === "remark"
|
||||||
? "Automatisch aus der verknüpften Verteilung"
|
? "Automatisch aus der verknüpften Verteilung"
|
||||||
: undefined;
|
: undefined;
|
||||||
} else if (
|
|
||||||
rowType === "circuitSummary" &&
|
|
||||||
circuit &&
|
|
||||||
downstreamBoardLoad &&
|
|
||||||
circuit.deviceRows.length === 0
|
|
||||||
) {
|
|
||||||
if (column.key === "displayName") {
|
|
||||||
const boardLabel =
|
|
||||||
downstreamBoardLoad.boardNumber ?? downstreamBoardLoad.name;
|
|
||||||
value = circuit.displayName
|
|
||||||
? `${boardLabel} · ${circuit.displayName}`
|
|
||||||
: boardLabel;
|
|
||||||
editValue = circuit.displayName ?? "";
|
|
||||||
} else if (column.key === "cosPhi") {
|
|
||||||
value = circuit.circuitCosPhi;
|
|
||||||
} else if (
|
|
||||||
kind === "computed" &&
|
|
||||||
circuitOnlyColumns.has(column.key)
|
|
||||||
) {
|
|
||||||
value = getCircuitValue(circuit, column.key);
|
|
||||||
} else if (kind === "circuitField") {
|
|
||||||
value = getCircuitValue(circuit, column.key);
|
|
||||||
} else if (column.key === "rowTotalPower") {
|
|
||||||
value = circuit.circuitTotalPower;
|
|
||||||
} else if (column.key === "circuitTotalPower") {
|
|
||||||
value = circuit.circuitTotalPower;
|
|
||||||
} else if (column.key === "voltage") {
|
|
||||||
value = circuit.voltage;
|
|
||||||
}
|
|
||||||
} else if (rowType === "placeholder") {
|
} else if (rowType === "placeholder") {
|
||||||
value = column.key === "equipmentIdentifier" ? "-frei-" : undefined;
|
value = column.key === "equipmentIdentifier" ? "-frei-" : undefined;
|
||||||
} else if (kind === "deviceField" && device) {
|
} else if (kind === "deviceField" && device) {
|
||||||
|
|
@ -210,7 +178,7 @@ function makeVisibleGridRow(
|
||||||
value = circuit.voltage;
|
value = circuit.voltage;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { cellKey: column.key, editable, kind, value, editValue };
|
return { cellKey: column.key, editable, kind, value };
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -279,19 +247,6 @@ export function buildVisibleGridRowsWithStructure(
|
||||||
const downstreamLoads = circuit.downstreamBoardLoads ?? [];
|
const downstreamLoads = circuit.downstreamBoardLoads ?? [];
|
||||||
if (circuit.deviceRows.length === 0 && downstreamLoads.length === 0) {
|
if (circuit.deviceRows.length === 0 && downstreamLoads.length === 0) {
|
||||||
rows.push(makeVisibleGridRow("reserveCircuit", section.id, circuit));
|
rows.push(makeVisibleGridRow("reserveCircuit", section.id, circuit));
|
||||||
} else if (
|
|
||||||
circuit.deviceRows.length === 0 &&
|
|
||||||
downstreamLoads.length === 1
|
|
||||||
) {
|
|
||||||
rows.push(
|
|
||||||
makeVisibleGridRow(
|
|
||||||
"circuitSummary",
|
|
||||||
section.id,
|
|
||||||
circuit,
|
|
||||||
undefined,
|
|
||||||
downstreamLoads[0]
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else if (circuit.deviceRows.length === 1 && downstreamLoads.length === 0) {
|
} else if (circuit.deviceRows.length === 1 && downstreamLoads.length === 0) {
|
||||||
rows.push(makeVisibleGridRow("circuitCompact", section.id, circuit, circuit.deviceRows[0]));
|
rows.push(makeVisibleGridRow("circuitCompact", section.id, circuit, circuit.deviceRows[0]));
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
import type { CircuitSnapshot } from "../../domain/models/circuit-structure-project-command.model.js";
|
|
||||||
import type {
|
|
||||||
CircuitTreeResponseDto,
|
|
||||||
CircuitTreeSectionDto,
|
|
||||||
} from "../types";
|
|
||||||
import { buildCircuitInsertSnapshot } from "./circuit-structure-command";
|
|
||||||
|
|
||||||
export function getDefaultFeederCircuitSection(
|
|
||||||
tree: CircuitTreeResponseDto | undefined
|
|
||||||
) {
|
|
||||||
if (!tree) return undefined;
|
|
||||||
return (
|
|
||||||
tree.sections.find((section) => section.category === "three_phase") ??
|
|
||||||
tree.sections.find((section) => section.category !== undefined)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getNextFeederCircuitIdentifier(
|
|
||||||
section: CircuitTreeSectionDto
|
|
||||||
) {
|
|
||||||
let highestSuffix = 0;
|
|
||||||
for (const circuit of section.circuits) {
|
|
||||||
if (!circuit.equipmentIdentifier.startsWith(section.prefix)) continue;
|
|
||||||
const suffix = circuit.equipmentIdentifier.slice(section.prefix.length);
|
|
||||||
if (!/^\d+$/.test(suffix)) continue;
|
|
||||||
highestSuffix = Math.max(highestSuffix, Number(suffix));
|
|
||||||
}
|
|
||||||
return `${section.prefix}${highestSuffix + 1}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildFeederCircuitSnapshot(input: {
|
|
||||||
id: string;
|
|
||||||
tree: CircuitTreeResponseDto;
|
|
||||||
sectionId: string;
|
|
||||||
displayName: string;
|
|
||||||
}): CircuitSnapshot {
|
|
||||||
const section = input.tree.sections.find(
|
|
||||||
(candidate) => candidate.id === input.sectionId
|
|
||||||
);
|
|
||||||
if (!section || !section.category) {
|
|
||||||
throw new Error("Die gewählte Stromkreisgruppe ist nicht verfügbar.");
|
|
||||||
}
|
|
||||||
const snapshot = buildCircuitInsertSnapshot({
|
|
||||||
id: input.id,
|
|
||||||
circuitListId: input.tree.circuitListId,
|
|
||||||
category: section.category,
|
|
||||||
deviceRows: [],
|
|
||||||
values: {
|
|
||||||
sectionId: section.id,
|
|
||||||
equipmentIdentifier: getNextFeederCircuitIdentifier(section),
|
|
||||||
displayName: input.displayName.trim() || undefined,
|
|
||||||
sortOrder:
|
|
||||||
section.circuits.length === 0
|
|
||||||
? 10
|
|
||||||
: Math.max(...section.circuits.map((circuit) => circuit.sortOrder)) +
|
|
||||||
10,
|
|
||||||
voltage:
|
|
||||||
section.category === "three_phase"
|
|
||||||
? input.tree.threePhaseVoltageV
|
|
||||||
: input.tree.singlePhaseVoltageV,
|
|
||||||
isReserve: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
// A distribution-board feeder is not a generic final circuit. Its protection
|
|
||||||
// must be selected from the calculated load and cable, so no 16 A group
|
|
||||||
// default is persisted here.
|
|
||||||
return { ...snapshot, protectionDevice: null };
|
|
||||||
}
|
|
||||||
|
|
@ -32,10 +32,6 @@ const commandTypeLabels: Record<string, string> = {
|
||||||
"project.update-settings": "Projekteinstellungen bearbeitet",
|
"project.update-settings": "Projekteinstellungen bearbeitet",
|
||||||
"distribution-board.insert": "Verteilung angelegt",
|
"distribution-board.insert": "Verteilung angelegt",
|
||||||
"distribution-board.update": "Verteilung bearbeitet",
|
"distribution-board.update": "Verteilung bearbeitet",
|
||||||
"distribution-board.create-feeder-circuit":
|
|
||||||
"Einspeiseabgang angelegt und verknüpft",
|
|
||||||
"distribution-board.remove-feeder-circuit":
|
|
||||||
"Einspeiseabgang und Verknüpfung entfernt",
|
|
||||||
"distribution-board.delete": "Verteilung entfernt",
|
"distribution-board.delete": "Verteilung entfernt",
|
||||||
"distribution-board.insert-subtree": "Verteilung vollständig eingefügt",
|
"distribution-board.insert-subtree": "Verteilung vollständig eingefügt",
|
||||||
"distribution-board.delete-subtree": "Verteilung vollständig entfernt",
|
"distribution-board.delete-subtree": "Verteilung vollständig entfernt",
|
||||||
|
|
|
||||||
|
|
@ -133,9 +133,7 @@ describe("circuit grid model", () => {
|
||||||
assert.equal(getCellKind("circuitCompact", "equipmentIdentifier"), "circuitField");
|
assert.equal(getCellKind("circuitCompact", "equipmentIdentifier"), "circuitField");
|
||||||
assert.equal(getCellKind("circuitSummary", "displayName"), "circuitField");
|
assert.equal(getCellKind("circuitSummary", "displayName"), "circuitField");
|
||||||
assert.equal(getCellKind("deviceRow", "displayName"), "deviceField");
|
assert.equal(getCellKind("deviceRow", "displayName"), "deviceField");
|
||||||
assert.equal(getCellKind("reserveCircuit", "displayName"), "circuitField");
|
assert.equal(getCellKind("reserveCircuit", "remark"), "deviceField");
|
||||||
assert.equal(getCellKind("reserveCircuit", "remark"), "circuitField");
|
|
||||||
assert.equal(getCellKind("reserveCircuit", "powerPerUnit"), "deviceField");
|
|
||||||
assert.equal(getCellKind("placeholder", "displayName"), "deviceField");
|
assert.equal(getCellKind("placeholder", "displayName"), "deviceField");
|
||||||
assert.equal(getCellKind("deviceRow", "equipmentIdentifier"), "readonly");
|
assert.equal(getCellKind("deviceRow", "equipmentIdentifier"), "readonly");
|
||||||
assert.equal(getCellKind("circuitSummary", "controlRequirement"), "circuitField");
|
assert.equal(getCellKind("circuitSummary", "controlRequirement"), "circuitField");
|
||||||
|
|
|
||||||
|
|
@ -179,10 +179,9 @@ describe("circuit grid projection", () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders one linked downstream board as one compact editable circuit row", () => {
|
it("renders linked downstream boards as separate read-only rows", () => {
|
||||||
const feeder = {
|
const feeder = {
|
||||||
...reserveCircuit,
|
...reserveCircuit,
|
||||||
displayName: undefined,
|
|
||||||
isReserve: false,
|
isReserve: false,
|
||||||
circuitTotalPower: 4,
|
circuitTotalPower: 4,
|
||||||
circuitApparentPower: 5,
|
circuitApparentPower: 5,
|
||||||
|
|
@ -203,65 +202,19 @@ describe("circuit grid projection", () => {
|
||||||
assert.deepEqual(rows.map((row) => row.rowType), [
|
assert.deepEqual(rows.map((row) => row.rowType), [
|
||||||
"section",
|
"section",
|
||||||
"circuitSummary",
|
"circuitSummary",
|
||||||
|
"downstreamBoardLoad",
|
||||||
"placeholder",
|
"placeholder",
|
||||||
]);
|
]);
|
||||||
const loadRow = rows[1];
|
const loadRow = rows[2];
|
||||||
assert.equal(
|
assert.equal(
|
||||||
loadRow.cells.find((cell) => cell.cellKey === "displayName")?.value,
|
loadRow.cells.find((cell) => cell.cellKey === "displayName")?.value,
|
||||||
"UV-02"
|
"Unterverteilung UV-02"
|
||||||
);
|
|
||||||
assert.equal(
|
|
||||||
loadRow.cells.find((cell) => cell.cellKey === "displayName")?.editValue,
|
|
||||||
""
|
|
||||||
);
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
loadRow.cells.find((cell) => cell.cellKey === "rowTotalPower")?.value,
|
loadRow.cells.find((cell) => cell.cellKey === "rowTotalPower")?.value,
|
||||||
4
|
4
|
||||||
);
|
);
|
||||||
assert.equal(
|
assert.equal(loadRow.cells.every((cell) => !cell.editable), true);
|
||||||
loadRow.cells.find((cell) => cell.cellKey === "displayName")?.kind,
|
|
||||||
"circuitField"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("keeps separate read-only rows for several linked downstream boards", () => {
|
|
||||||
const feeder = {
|
|
||||||
...reserveCircuit,
|
|
||||||
isReserve: false,
|
|
||||||
circuitTotalPower: 6,
|
|
||||||
circuitApparentPower: 7,
|
|
||||||
circuitCosPhi: 6 / 7,
|
|
||||||
downstreamBoardLoads: [
|
|
||||||
{
|
|
||||||
distributionBoardId: "board-a",
|
|
||||||
name: "Unterverteilung A",
|
|
||||||
boardNumber: "UV-A",
|
|
||||||
activePowerKw: 4,
|
|
||||||
apparentPowerKva: 5,
|
|
||||||
cosPhi: 0.8,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
distributionBoardId: "board-b",
|
|
||||||
name: "Unterverteilung B",
|
|
||||||
boardNumber: "UV-B",
|
|
||||||
activePowerKw: 2,
|
|
||||||
apparentPowerKva: 2,
|
|
||||||
cosPhi: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
const rows = buildVisibleGridRowsWithStructure(
|
|
||||||
[{ ...sections[0], circuits: [feeder] }],
|
|
||||||
{ headerComponents: [], footerComponents: [] }
|
|
||||||
);
|
|
||||||
assert.deepEqual(rows.map((row) => row.rowType), [
|
|
||||||
"section",
|
|
||||||
"circuitSummary",
|
|
||||||
"downstreamBoardLoad",
|
|
||||||
"downstreamBoardLoad",
|
|
||||||
"placeholder",
|
|
||||||
]);
|
|
||||||
assert.equal(rows[2].cells.every((cell) => !cell.editable), true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows circuit protection once on each visible circuit row", () => {
|
it("shows circuit protection once on each visible circuit row", () => {
|
||||||
|
|
|
||||||
|
|
@ -1,240 +0,0 @@
|
||||||
import path from "node:path";
|
|
||||||
import assert from "node:assert/strict";
|
|
||||||
import { describe, it } from "node:test";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
|
||||||
import {
|
|
||||||
createDatabaseContext,
|
|
||||||
type DatabaseContext,
|
|
||||||
} from "../src/db/database-context.js";
|
|
||||||
import { DistributionBoardStructureProjectCommandRepository } from "../src/db/repositories/distribution-board-structure-project-command.repository.js";
|
|
||||||
import { ProjectHistoryRepository } from "../src/db/repositories/project-history.repository.js";
|
|
||||||
import { circuits } from "../src/db/schema/circuits.js";
|
|
||||||
import { distributionBoards } from "../src/db/schema/distribution-boards.js";
|
|
||||||
import { projects } from "../src/db/schema/projects.js";
|
|
||||||
import {
|
|
||||||
assertDistributionBoardFeederCircuitProjectCommand,
|
|
||||||
createDistributionBoardCreateFeederCircuitProjectCommand,
|
|
||||||
} from "../src/domain/models/distribution-board-feeder-circuit-project-command.model.js";
|
|
||||||
import { createDistributionBoardUpdateProjectCommand } from "../src/domain/models/distribution-board-project-command.model.js";
|
|
||||||
import {
|
|
||||||
createDistributionBoardInsertProjectCommand,
|
|
||||||
createDistributionBoardStructureSnapshot,
|
|
||||||
} from "../src/domain/models/distribution-board-structure-project-command.model.js";
|
|
||||||
import { buildCircuitInsertSnapshot } from "../src/frontend/utils/circuit-structure-command.js";
|
|
||||||
|
|
||||||
function createTestDatabase(): DatabaseContext {
|
|
||||||
const context = createDatabaseContext(":memory:");
|
|
||||||
migrate(context.db, {
|
|
||||||
migrationsFolder: path.resolve("src", "db", "migrations"),
|
|
||||||
});
|
|
||||||
context.db
|
|
||||||
.insert(projects)
|
|
||||||
.values({ id: "project-1", name: "Test project" })
|
|
||||||
.run();
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupBoards(
|
|
||||||
context: DatabaseContext,
|
|
||||||
repository: DistributionBoardStructureProjectCommandRepository
|
|
||||||
) {
|
|
||||||
const source = createDistributionBoardStructureSnapshot(
|
|
||||||
"project-1",
|
|
||||||
"NSHV"
|
|
||||||
);
|
|
||||||
const downstream = createDistributionBoardStructureSnapshot(
|
|
||||||
"project-1",
|
|
||||||
"UV EG"
|
|
||||||
);
|
|
||||||
repository.execute({
|
|
||||||
projectId: "project-1",
|
|
||||||
expectedRevision: 0,
|
|
||||||
source: "user",
|
|
||||||
command: createDistributionBoardInsertProjectCommand(source),
|
|
||||||
});
|
|
||||||
repository.execute({
|
|
||||||
projectId: "project-1",
|
|
||||||
expectedRevision: 1,
|
|
||||||
source: "user",
|
|
||||||
command: createDistributionBoardInsertProjectCommand(downstream),
|
|
||||||
});
|
|
||||||
const section = source.sections.find(
|
|
||||||
(candidate) => candidate.key === "three_phase"
|
|
||||||
)!;
|
|
||||||
const circuit = {
|
|
||||||
...buildCircuitInsertSnapshot({
|
|
||||||
id: "feeder-circuit",
|
|
||||||
circuitListId: source.circuitList.id,
|
|
||||||
category: "three_phase",
|
|
||||||
deviceRows: [],
|
|
||||||
values: {
|
|
||||||
sectionId: section.id,
|
|
||||||
equipmentIdentifier: `${section.prefix}1`,
|
|
||||||
displayName: undefined,
|
|
||||||
sortOrder: 10,
|
|
||||||
voltage: 400,
|
|
||||||
isReserve: true,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
protectionDevice: null,
|
|
||||||
};
|
|
||||||
const boardUpdate = createDistributionBoardUpdateProjectCommand(
|
|
||||||
downstream.distributionBoard.id,
|
|
||||||
{
|
|
||||||
feedingDistributionBoardId: source.distributionBoard.id,
|
|
||||||
feedingCircuitId: circuit.id,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const command =
|
|
||||||
createDistributionBoardCreateFeederCircuitProjectCommand(
|
|
||||||
circuit,
|
|
||||||
boardUpdate
|
|
||||||
);
|
|
||||||
return { source, downstream, circuit, command };
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("distribution-board feeder-circuit project command", () => {
|
|
||||||
it("creates and links one feeder atomically and supports undo and redo", () => {
|
|
||||||
const context = createTestDatabase();
|
|
||||||
try {
|
|
||||||
const repository =
|
|
||||||
new DistributionBoardStructureProjectCommandRepository(context.db);
|
|
||||||
const history = new ProjectHistoryRepository(context.db);
|
|
||||||
const { source, downstream, circuit, command } = setupBoards(
|
|
||||||
context,
|
|
||||||
repository
|
|
||||||
);
|
|
||||||
|
|
||||||
repository.executeFeederCircuit({
|
|
||||||
projectId: "project-1",
|
|
||||||
expectedRevision: 2,
|
|
||||||
source: "user",
|
|
||||||
command,
|
|
||||||
});
|
|
||||||
assert.ok(
|
|
||||||
context.db
|
|
||||||
.select()
|
|
||||||
.from(circuits)
|
|
||||||
.where(eq(circuits.id, circuit.id))
|
|
||||||
.get()
|
|
||||||
);
|
|
||||||
assert.deepEqual(
|
|
||||||
context.db
|
|
||||||
.select({
|
|
||||||
boardId: distributionBoards.feedingDistributionBoardId,
|
|
||||||
circuitId: distributionBoards.feedingCircuitId,
|
|
||||||
})
|
|
||||||
.from(distributionBoards)
|
|
||||||
.where(eq(distributionBoards.id, downstream.distributionBoard.id))
|
|
||||||
.get(),
|
|
||||||
{
|
|
||||||
boardId: source.distributionBoard.id,
|
|
||||||
circuitId: circuit.id,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const undo = history.getNextCommand("project-1", "undo");
|
|
||||||
assert.ok(undo);
|
|
||||||
assertDistributionBoardFeederCircuitProjectCommand(undo.command);
|
|
||||||
repository.executeFeederCircuit({
|
|
||||||
projectId: "project-1",
|
|
||||||
expectedRevision: 3,
|
|
||||||
source: "undo",
|
|
||||||
historyTargetChangeSetId: undo.changeSetId,
|
|
||||||
command: undo.command,
|
|
||||||
});
|
|
||||||
assert.equal(
|
|
||||||
context.db
|
|
||||||
.select()
|
|
||||||
.from(circuits)
|
|
||||||
.where(eq(circuits.id, circuit.id))
|
|
||||||
.get(),
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
assert.deepEqual(
|
|
||||||
context.db
|
|
||||||
.select({
|
|
||||||
boardId: distributionBoards.feedingDistributionBoardId,
|
|
||||||
circuitId: distributionBoards.feedingCircuitId,
|
|
||||||
})
|
|
||||||
.from(distributionBoards)
|
|
||||||
.where(eq(distributionBoards.id, downstream.distributionBoard.id))
|
|
||||||
.get(),
|
|
||||||
{ boardId: null, circuitId: null }
|
|
||||||
);
|
|
||||||
|
|
||||||
const redo = history.getNextCommand("project-1", "redo");
|
|
||||||
assert.ok(redo);
|
|
||||||
assertDistributionBoardFeederCircuitProjectCommand(redo.command);
|
|
||||||
repository.executeFeederCircuit({
|
|
||||||
projectId: "project-1",
|
|
||||||
expectedRevision: 4,
|
|
||||||
source: "redo",
|
|
||||||
historyTargetChangeSetId: redo.changeSetId,
|
|
||||||
command: redo.command,
|
|
||||||
});
|
|
||||||
assert.equal(
|
|
||||||
context.db
|
|
||||||
.select({ equipmentIdentifier: circuits.equipmentIdentifier })
|
|
||||||
.from(circuits)
|
|
||||||
.where(eq(circuits.id, circuit.id))
|
|
||||||
.get()?.equipmentIdentifier,
|
|
||||||
circuit.equipmentIdentifier
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
context.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rolls back unlinking when the generated circuit changed before undo", () => {
|
|
||||||
const context = createTestDatabase();
|
|
||||||
try {
|
|
||||||
const repository =
|
|
||||||
new DistributionBoardStructureProjectCommandRepository(context.db);
|
|
||||||
const history = new ProjectHistoryRepository(context.db);
|
|
||||||
const { downstream, circuit, command } = setupBoards(context, repository);
|
|
||||||
repository.executeFeederCircuit({
|
|
||||||
projectId: "project-1",
|
|
||||||
expectedRevision: 2,
|
|
||||||
source: "user",
|
|
||||||
command,
|
|
||||||
});
|
|
||||||
context.db
|
|
||||||
.update(circuits)
|
|
||||||
.set({ displayName: "Außerhalb der Historie geändert" })
|
|
||||||
.where(eq(circuits.id, circuit.id))
|
|
||||||
.run();
|
|
||||||
|
|
||||||
const undo = history.getNextCommand("project-1", "undo");
|
|
||||||
assert.ok(undo);
|
|
||||||
assertDistributionBoardFeederCircuitProjectCommand(undo.command);
|
|
||||||
assert.throws(
|
|
||||||
() =>
|
|
||||||
repository.executeFeederCircuit({
|
|
||||||
projectId: "project-1",
|
|
||||||
expectedRevision: 3,
|
|
||||||
source: "undo",
|
|
||||||
historyTargetChangeSetId: undo.changeSetId,
|
|
||||||
command: undo.command,
|
|
||||||
}),
|
|
||||||
/changed before history removal/
|
|
||||||
);
|
|
||||||
const board = context.db
|
|
||||||
.select()
|
|
||||||
.from(distributionBoards)
|
|
||||||
.where(eq(distributionBoards.id, downstream.distributionBoard.id))
|
|
||||||
.get();
|
|
||||||
assert.equal(board?.feedingCircuitId, circuit.id);
|
|
||||||
assert.ok(
|
|
||||||
context.db
|
|
||||||
.select()
|
|
||||||
.from(circuits)
|
|
||||||
.where(eq(circuits.id, circuit.id))
|
|
||||||
.get()
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
context.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,168 +0,0 @@
|
||||||
import assert from "node:assert/strict";
|
|
||||||
import { describe, it } from "node:test";
|
|
||||||
import {
|
|
||||||
buildFeederCircuitSnapshot,
|
|
||||||
getDefaultFeederCircuitSection,
|
|
||||||
getNextFeederCircuitIdentifier,
|
|
||||||
} from "../src/frontend/utils/distribution-board-feeder-circuit.js";
|
|
||||||
import type { CircuitTreeResponseDto } from "../src/frontend/types.js";
|
|
||||||
import { createFeederCircuitAndUpdateDistributionBoard } from "../src/frontend/utils/api.js";
|
|
||||||
|
|
||||||
function tree(): CircuitTreeResponseDto {
|
|
||||||
return {
|
|
||||||
circuitListId: "list-main",
|
|
||||||
distributionBoardId: "board-main",
|
|
||||||
distributionBoardName: "NSHV",
|
|
||||||
currentRevision: 2,
|
|
||||||
singlePhaseVoltageV: 230,
|
|
||||||
threePhaseVoltageV: 400,
|
|
||||||
distributionBoardSimultaneityFactor: 1,
|
|
||||||
distributionBoardTotalPower: 0,
|
|
||||||
distributionBoardTotalPowerWithSimultaneityFactor: 0,
|
|
||||||
headerComponents: [],
|
|
||||||
footerComponents: [],
|
|
||||||
sections: [
|
|
||||||
{
|
|
||||||
id: "single",
|
|
||||||
key: "single_phase",
|
|
||||||
category: "single_phase",
|
|
||||||
groupNumber: 1,
|
|
||||||
displayName: "1-phasig 1",
|
|
||||||
prefix: "-2F1.",
|
|
||||||
sortOrder: 10,
|
|
||||||
sectionTotalPower: 0,
|
|
||||||
components: [],
|
|
||||||
circuits: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "three",
|
|
||||||
key: "three_phase",
|
|
||||||
category: "three_phase",
|
|
||||||
groupNumber: 1,
|
|
||||||
displayName: "3-phasig 1",
|
|
||||||
prefix: "-3F1.",
|
|
||||||
sortOrder: 20,
|
|
||||||
sectionTotalPower: 0,
|
|
||||||
components: [],
|
|
||||||
circuits: [
|
|
||||||
{
|
|
||||||
id: "existing-2",
|
|
||||||
circuitListId: "list-main",
|
|
||||||
sectionId: "three",
|
|
||||||
equipmentIdentifier: "-3F1.2",
|
|
||||||
sortOrder: 10,
|
|
||||||
isReserve: true,
|
|
||||||
circuitTotalPower: 0,
|
|
||||||
circuitApparentPower: 0,
|
|
||||||
circuitCosPhi: 1,
|
|
||||||
deviceRows: [],
|
|
||||||
downstreamBoardLoads: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "existing-5",
|
|
||||||
circuitListId: "list-main",
|
|
||||||
sectionId: "three",
|
|
||||||
equipmentIdentifier: "-3F1.5",
|
|
||||||
sortOrder: 30,
|
|
||||||
isReserve: true,
|
|
||||||
circuitTotalPower: 0,
|
|
||||||
circuitApparentPower: 0,
|
|
||||||
circuitCosPhi: 1,
|
|
||||||
deviceRows: [],
|
|
||||||
downstreamBoardLoads: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("distribution-board feeder circuit planning", () => {
|
|
||||||
it("prefers a three-phase section and does not fill BMK gaps", () => {
|
|
||||||
const input = tree();
|
|
||||||
const section = getDefaultFeederCircuitSection(input)!;
|
|
||||||
assert.equal(section.id, "three");
|
|
||||||
assert.equal(getNextFeederCircuitIdentifier(section), "-3F1.6");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("builds an empty circuit with stable id, optional name and defaults", () => {
|
|
||||||
const input = tree();
|
|
||||||
const snapshot = buildFeederCircuitSnapshot({
|
|
||||||
id: "new-feeder",
|
|
||||||
tree: input,
|
|
||||||
sectionId: "three",
|
|
||||||
displayName: " ",
|
|
||||||
});
|
|
||||||
assert.equal(snapshot.id, "new-feeder");
|
|
||||||
assert.equal(snapshot.equipmentIdentifier, "-3F1.6");
|
|
||||||
assert.equal(snapshot.displayName, null);
|
|
||||||
assert.equal(snapshot.sortOrder, 40);
|
|
||||||
assert.equal(snapshot.voltage, 400);
|
|
||||||
assert.equal(snapshot.isReserve, true);
|
|
||||||
assert.deepEqual(snapshot.deviceRows, []);
|
|
||||||
assert.equal(snapshot.protectionDevice, null);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("sends circuit creation and board linking as one project command", async () => {
|
|
||||||
const input = tree();
|
|
||||||
const circuit = buildFeederCircuitSnapshot({
|
|
||||||
id: "new-feeder",
|
|
||||||
tree: input,
|
|
||||||
sectionId: "three",
|
|
||||||
displayName: "",
|
|
||||||
});
|
|
||||||
const requests: Array<{ url: string; method: string; body: any }> = [];
|
|
||||||
const originalFetch = globalThis.fetch;
|
|
||||||
globalThis.fetch = async (request, init) => {
|
|
||||||
requests.push({
|
|
||||||
url: String(request),
|
|
||||||
method: init?.method ?? "GET",
|
|
||||||
body: JSON.parse(String(init?.body)),
|
|
||||||
});
|
|
||||||
return new Response(JSON.stringify({}), {
|
|
||||||
status: 200,
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
await createFeederCircuitAndUpdateDistributionBoard(
|
|
||||||
"project-1",
|
|
||||||
"board-child",
|
|
||||||
{
|
|
||||||
floorId: null,
|
|
||||||
supplyType: "AV",
|
|
||||||
simultaneityFactor: 0.8,
|
|
||||||
feedingDistributionBoardId: "board-main",
|
|
||||||
feedingCircuitId: circuit.id,
|
|
||||||
},
|
|
||||||
circuit,
|
|
||||||
7
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
globalThis.fetch = originalFetch;
|
|
||||||
}
|
|
||||||
assert.equal(requests.length, 1);
|
|
||||||
assert.equal(requests[0].url, "/api/projects/project-1/commands");
|
|
||||||
assert.equal(requests[0].method, "POST");
|
|
||||||
assert.equal(requests[0].body.expectedRevision, 7);
|
|
||||||
assert.equal(
|
|
||||||
requests[0].body.command.type,
|
|
||||||
"distribution-board.create-feeder-circuit"
|
|
||||||
);
|
|
||||||
assert.equal(requests[0].body.command.payload.circuit.id, "new-feeder");
|
|
||||||
assert.deepEqual(
|
|
||||||
requests[0].body.command.payload.boardUpdate.payload.changes
|
|
||||||
.filter((change: { field: string }) =>
|
|
||||||
change.field.startsWith("feeding")
|
|
||||||
)
|
|
||||||
.map((change: { field: string; value: unknown }) => [
|
|
||||||
change.field,
|
|
||||||
change.value,
|
|
||||||
]),
|
|
||||||
[
|
|
||||||
["feedingDistributionBoardId", "board-main"],
|
|
||||||
["feedingCircuitId", "new-feeder"],
|
|
||||||
]
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue