Manage distribution board components
This commit is contained in:
@@ -357,9 +357,14 @@ vollständige Stromkreisblöcke und der freie Einfügeplatz. Leere Gruppen bleib
|
|||||||
sichtbar; Komponenten werden stabil nach `sortOrder` und ID sortiert.
|
sichtbar; Komponenten werden stabil nach `sortOrder` und ID sortiert.
|
||||||
`circuit-grid-projection.ts` normalisiert diese Struktur zusammen mit den
|
`circuit-grid-projection.ts` normalisiert diese Struktur zusammen mit den
|
||||||
bisherigen kompakten und mehrzeiligen Stromkreisblöcken für den Editor. Kopf-,
|
bisherigen kompakten und mehrzeiligen Stromkreisblöcken für den Editor. Kopf-,
|
||||||
Gruppen- und Fußkomponenten erscheinen dort zunächst als schreibgeschützte
|
Gruppen- und Fußkomponenten erscheinen als Zeilen über die volle Tabellenbreite
|
||||||
Zeilen über die volle Tabellenbreite und nehmen nicht an Zellauswahl,
|
und nehmen nicht an Zellauswahl oder Tastatur-Einfügen teil. Hauptschalter und
|
||||||
Tastatur-Einfügen oder Löschen teil.
|
Überspannungsableiter bleiben feste, schreibgeschützte Kopfkomponenten.
|
||||||
|
Optionale Gruppenvorsicherungen, Gruppen-FIs und zusätzliche Verteilergeräte
|
||||||
|
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.
|
||||||
`distribution-board.update` versioniert Etage, Netzart und den
|
`distribution-board.update` versioniert Etage, Netzart und den
|
||||||
verteilerweiten Gleichzeitigkeitsfaktor gemeinsam und stellt alle Werte über
|
verteilerweiten Gleichzeitigkeitsfaktor gemeinsam und stellt alle Werte über
|
||||||
dauerhaftes Undo/Redo wieder her. Der Faktor liegt zwischen `0` und `1` und
|
dauerhaftes Undo/Redo wieder her. Der Faktor liegt zwischen `0` und `1` und
|
||||||
@@ -433,8 +438,9 @@ Kopieren in ein Projekt erzeugt ein eigenständiges Projektgerät.
|
|||||||
Snapshot-Schema 7. Persistente Insert/Delete/Update-Commands für
|
Snapshot-Schema 7. Persistente Insert/Delete/Update-Commands für
|
||||||
veränderliche Verteilerkomponenten, Gruppen einschließlich befüllter
|
veränderliche Verteilerkomponenten, Gruppen einschließlich befüllter
|
||||||
Unterbäume sowie vollständige Gruppensortierung sind integriert. Der Editor
|
Unterbäume sowie vollständige Gruppensortierung sind integriert. Der Editor
|
||||||
zeigt die geschützte Struktur bereits schreibgeschützt an; dedizierte
|
zeigt die geschützte Struktur an und bearbeitet veränderliche Gruppen- und
|
||||||
Bearbeitungsfunktionen folgen in einem abgegrenzten Arbeitspaket.
|
Fußkomponenten über dedizierte Command-Modale. Gruppenverwaltung und
|
||||||
|
Stromkreisschutz folgen in weiteren abgegrenzten Arbeitspaketen.
|
||||||
|
|
||||||
PostgreSQL ist bewusst nicht implementiert. Die Domainregeln und
|
PostgreSQL ist bewusst nicht implementiert. Die Domainregeln und
|
||||||
Transaktionsgrenzen sollen portabel bleiben; Schema und Betriebsmodell benötigen
|
Transaktionsgrenzen sollen portabel bleiben; Schema und Betriebsmodell benötigen
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ requirements and intended sequencing, not proof of implementation.
|
|||||||
- [x] Phase E1: tree read model for components, groups and protection devices.
|
- [x] Phase E1: tree read model for components, groups and protection devices.
|
||||||
- [x] Phase E2a: pure header/group/footer structure projection.
|
- [x] Phase E2a: pure header/group/footer structure projection.
|
||||||
- [x] Phase E2b: render the structure projection in the editor grid.
|
- [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.
|
||||||
- [ ] Phase E: editor projection and editing.
|
- [ ] Phase E: editor projection and editing.
|
||||||
- [ ] Phase F: documentation and full GUI verification.
|
- [ ] Phase F: documentation and full GUI verification.
|
||||||
- [ ] Keep full electrical sizing and cable-dimensioning rules separate until
|
- [ ] Keep full electrical sizing and cable-dimensioning rules separate until
|
||||||
|
|||||||
@@ -740,8 +740,8 @@ Acceptance:
|
|||||||
### E. Editor Projection and Editing
|
### E. Editor Projection and Editing
|
||||||
|
|
||||||
Status: In progress. The complete tree read model E1, pure structural
|
Status: In progress. The complete tree read model E1, pure structural
|
||||||
projection E2a and read-only grid rendering E2b are complete. Editing remains
|
projection E2a, grid rendering E2b and mutable component editing E3a are
|
||||||
pending.
|
complete. Group management and circuit-protection editing remain pending.
|
||||||
|
|
||||||
- render fixed header components
|
- render fixed header components
|
||||||
- render group components and circuit blocks
|
- render group components and circuit blocks
|
||||||
@@ -750,7 +750,7 @@ pending.
|
|||||||
- add type-dependent protection editing
|
- add type-dependent protection editing
|
||||||
- keep spreadsheet selection, keyboard and drag behavior coherent
|
- keep spreadsheet selection, keyboard and drag behavior coherent
|
||||||
|
|
||||||
Implemented in E1/E2a/E2b:
|
Implemented in E1/E2a/E2b/E3a:
|
||||||
|
|
||||||
- the tree response exposes fixed header, group and auxiliary footer
|
- the tree response exposes fixed header, group and auxiliary footer
|
||||||
components plus both one-to-one protection layers
|
components plus both one-to-one protection layers
|
||||||
@@ -764,6 +764,12 @@ Implemented in E1/E2a/E2b:
|
|||||||
components as read-only full-width rows
|
components as read-only full-width rows
|
||||||
- structural component rows are excluded from spreadsheet cell navigation,
|
- structural component rows are excluded from spreadsheet cell navigation,
|
||||||
insertion and deletion commands until their dedicated editing controls exist
|
insertion and deletion commands until their dedicated editing controls exist
|
||||||
|
- group headings offer at most one optional upstream protection device and one
|
||||||
|
optional group RCD; the agreed group-RCD default is applied only when it is
|
||||||
|
created
|
||||||
|
- 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
|
||||||
|
|
||||||
Acceptance:
|
Acceptance:
|
||||||
|
|
||||||
|
|||||||
@@ -691,6 +691,23 @@ body {
|
|||||||
font-size: 0.78rem;
|
font-size: 0.78rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tree-grid .structure-component-actions {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-grid .structure-component-actions button {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0.18rem 0.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-grid .structure-component-fixed {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
.tree-grid .section-drop-cell {
|
.tree-grid .section-drop-cell {
|
||||||
padding: 0.45rem 0.6rem;
|
padding: 0.45rem 0.6rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,15 @@ import {
|
|||||||
import {
|
import {
|
||||||
buildCircuitSectionRenumberAssignments,
|
buildCircuitSectionRenumberAssignments,
|
||||||
} from "../utils/circuit-section-renumber-command";
|
} from "../utils/circuit-section-renumber-command";
|
||||||
|
import {
|
||||||
|
buildDistributionBoardComponentSnapshot,
|
||||||
|
getNextComponentSortOrder,
|
||||||
|
getSuggestedGroupComponentIdentifier,
|
||||||
|
toDistributionBoardComponentSnapshot,
|
||||||
|
updateDistributionBoardComponentSnapshot,
|
||||||
|
type DistributionBoardComponentEditorValues,
|
||||||
|
type MutableDistributionBoardComponentRole,
|
||||||
|
} from "../utils/distribution-board-component-editing";
|
||||||
import { loadCircuitEditorSnapshot } from "../utils/circuit-editor-history";
|
import { loadCircuitEditorSnapshot } from "../utils/circuit-editor-history";
|
||||||
import type {
|
import type {
|
||||||
CellKey,
|
CellKey,
|
||||||
@@ -62,11 +71,13 @@ import type { VisibleGridRow } from "../utils/circuit-grid-projection";
|
|||||||
import {
|
import {
|
||||||
deleteCircuitCommand,
|
deleteCircuitCommand,
|
||||||
deleteCircuitDeviceRowCommand,
|
deleteCircuitDeviceRowCommand,
|
||||||
|
deleteDistributionBoardComponentCommand,
|
||||||
getCircuitTree,
|
getCircuitTree,
|
||||||
getNextCircuitIdentifier,
|
getNextCircuitIdentifier,
|
||||||
getProjectHistory,
|
getProjectHistory,
|
||||||
insertCircuitCommand,
|
insertCircuitCommand,
|
||||||
insertCircuitDeviceRowCommand,
|
insertCircuitDeviceRowCommand,
|
||||||
|
insertDistributionBoardComponentCommand,
|
||||||
listProjectDevices,
|
listProjectDevices,
|
||||||
moveCircuitDeviceRowsCommand,
|
moveCircuitDeviceRowsCommand,
|
||||||
moveCircuitDeviceRowsToNewCircuitCommand,
|
moveCircuitDeviceRowsToNewCircuitCommand,
|
||||||
@@ -76,10 +87,12 @@ import {
|
|||||||
redoProjectCommand,
|
redoProjectCommand,
|
||||||
updateCircuitById,
|
updateCircuitById,
|
||||||
updateCircuitDeviceRowById,
|
updateCircuitDeviceRowById,
|
||||||
|
updateDistributionBoardComponentCommand,
|
||||||
undoProjectCommand,
|
undoProjectCommand,
|
||||||
} from "../utils/api";
|
} from "../utils/api";
|
||||||
import type {
|
import type {
|
||||||
CircuitTreeCircuitDto,
|
CircuitTreeCircuitDto,
|
||||||
|
CircuitTreeComponentDto,
|
||||||
CircuitTreeResponseDto,
|
CircuitTreeResponseDto,
|
||||||
CreateCircuitDeviceRowInputDto,
|
CreateCircuitDeviceRowInputDto,
|
||||||
CreateCircuitInputDto,
|
CreateCircuitInputDto,
|
||||||
@@ -93,6 +106,7 @@ import type {
|
|||||||
import type {
|
import type {
|
||||||
CircuitSnapshot,
|
CircuitSnapshot,
|
||||||
} from "../../domain/models/circuit-structure-project-command.model";
|
} from "../../domain/models/circuit-structure-project-command.model";
|
||||||
|
import { DistributionBoardComponentModal } from "./distribution-board-component-modal";
|
||||||
|
|
||||||
type SaveDirection = "stay" | "next" | "prev";
|
type SaveDirection = "stay" | "next" | "prev";
|
||||||
type StartEditMode = "selectExisting" | "replaceWithTypedChar";
|
type StartEditMode = "selectExisting" | "replaceWithTypedChar";
|
||||||
@@ -161,6 +175,18 @@ function normalizeUiError(err: unknown): string {
|
|||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type StructureComponentEditorIntent =
|
||||||
|
| {
|
||||||
|
kind: "create";
|
||||||
|
role: MutableDistributionBoardComponentRole;
|
||||||
|
sectionId?: string;
|
||||||
|
initialEquipmentIdentifier?: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
kind: "edit";
|
||||||
|
component: CircuitTreeComponentDto;
|
||||||
|
};
|
||||||
|
|
||||||
function getFullColumnLabel(column: ColumnDef): string {
|
function getFullColumnLabel(column: ColumnDef): string {
|
||||||
return column.fullLabel ?? column.label;
|
return column.fullLabel ?? column.label;
|
||||||
}
|
}
|
||||||
@@ -201,6 +227,8 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
const [editingCell, setEditingCell] = useState<EditingCell | null>(null);
|
const [editingCell, setEditingCell] = useState<EditingCell | null>(null);
|
||||||
const [activeSectionId, setActiveSectionId] = useState<string | null>(null);
|
const [activeSectionId, setActiveSectionId] = useState<string | null>(null);
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
const [componentEditorIntent, setComponentEditorIntent] =
|
||||||
|
useState<StructureComponentEditorIntent | null>(null);
|
||||||
const [projectDevices, setProjectDevices] = useState<ProjectDeviceDto[]>([]);
|
const [projectDevices, setProjectDevices] = useState<ProjectDeviceDto[]>([]);
|
||||||
const [isProjectDeviceDrawerOpen, setIsProjectDeviceDrawerOpen] =
|
const [isProjectDeviceDrawerOpen, setIsProjectDeviceDrawerOpen] =
|
||||||
useState(false);
|
useState(false);
|
||||||
@@ -1031,6 +1059,85 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
await applyHistory("undo");
|
await applyHistory("undo");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleSaveDistributionBoardComponent(
|
||||||
|
values: DistributionBoardComponentEditorValues
|
||||||
|
) {
|
||||||
|
const intent = componentEditorIntent;
|
||||||
|
if (!intent || !data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await runCommand({
|
||||||
|
label:
|
||||||
|
intent.kind === "edit"
|
||||||
|
? "Verteilerkomponente bearbeiten"
|
||||||
|
: "Verteilerkomponente hinzufügen",
|
||||||
|
redo: async () => {
|
||||||
|
const result =
|
||||||
|
intent.kind === "edit"
|
||||||
|
? await (() => {
|
||||||
|
const expected = toDistributionBoardComponentSnapshot(
|
||||||
|
intent.component
|
||||||
|
);
|
||||||
|
return updateDistributionBoardComponentCommand(
|
||||||
|
projectId,
|
||||||
|
getExpectedProjectRevision(),
|
||||||
|
expected,
|
||||||
|
updateDistributionBoardComponentSnapshot(expected, values)
|
||||||
|
);
|
||||||
|
})()
|
||||||
|
: await (() => {
|
||||||
|
const components =
|
||||||
|
intent.role === "auxiliary"
|
||||||
|
? data.footerComponents
|
||||||
|
: data.sections.find(
|
||||||
|
(section) => section.id === intent.sectionId
|
||||||
|
)?.components ?? [];
|
||||||
|
const snapshot = buildDistributionBoardComponentSnapshot({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
circuitListId,
|
||||||
|
role: intent.role,
|
||||||
|
sectionId: intent.sectionId,
|
||||||
|
sortOrder: getNextComponentSortOrder(components),
|
||||||
|
values,
|
||||||
|
});
|
||||||
|
return insertDistributionBoardComponentCommand(
|
||||||
|
projectId,
|
||||||
|
getExpectedProjectRevision(),
|
||||||
|
snapshot
|
||||||
|
);
|
||||||
|
})();
|
||||||
|
applyProjectCommandResult(result);
|
||||||
|
setComponentEditorIntent(null);
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDeleteDistributionBoardComponent(
|
||||||
|
component: CircuitTreeComponentDto
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
!confirm(
|
||||||
|
`Verteilerkomponente „${component.equipmentIdentifier} ${component.name}“ entfernen?`
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await runCommand({
|
||||||
|
label: "Verteilerkomponente entfernen",
|
||||||
|
redo: async () => {
|
||||||
|
const result = await deleteDistributionBoardComponentCommand(
|
||||||
|
projectId,
|
||||||
|
getExpectedProjectRevision(),
|
||||||
|
toDistributionBoardComponentSnapshot(component)
|
||||||
|
);
|
||||||
|
applyProjectCommandResult(result);
|
||||||
|
setComponentEditorIntent(null);
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function handleRedo() {
|
async function handleRedo() {
|
||||||
if (historyBusy || isSaving || !historyState || historyState.redoDepth === 0) {
|
if (historyBusy || isSaving || !historyState || historyState.redoDepth === 0) {
|
||||||
return;
|
return;
|
||||||
@@ -2605,6 +2712,29 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tree-editor-shell">
|
<div className="tree-editor-shell">
|
||||||
|
{componentEditorIntent ? (
|
||||||
|
<DistributionBoardComponentModal
|
||||||
|
initialComponent={
|
||||||
|
componentEditorIntent.kind === "edit"
|
||||||
|
? componentEditorIntent.component
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
initialEquipmentIdentifier={
|
||||||
|
componentEditorIntent.kind === "create"
|
||||||
|
? componentEditorIntent.initialEquipmentIdentifier
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
isSaving={isSaving}
|
||||||
|
onClose={() => setComponentEditorIntent(null)}
|
||||||
|
onSave={handleSaveDistributionBoardComponent}
|
||||||
|
role={
|
||||||
|
componentEditorIntent.kind === "edit"
|
||||||
|
? (componentEditorIntent.component
|
||||||
|
.role as MutableDistributionBoardComponentRole)
|
||||||
|
: componentEditorIntent.role
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
<div className="editor-toolbar">
|
<div className="editor-toolbar">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -2656,6 +2786,18 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
? "Projektgeräte schließen"
|
? "Projektgeräte schließen"
|
||||||
: `Projektgeräte öffnen (${projectDevices.length})`}
|
: `Projektgeräte öffnen (${projectDevices.length})`}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={isSaving || historyBusy}
|
||||||
|
onClick={() =>
|
||||||
|
setComponentEditorIntent({
|
||||||
|
kind: "create",
|
||||||
|
role: "auxiliary",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Verteilergerät hinzufügen
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={clearSortAndFilters}
|
onClick={clearSortAndFilters}
|
||||||
@@ -2996,6 +3138,10 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
row.rowType === "footerComponent"
|
row.rowType === "footerComponent"
|
||||||
) {
|
) {
|
||||||
const component = row.component!;
|
const component = row.component!;
|
||||||
|
const isMutableComponent =
|
||||||
|
component.role === "group_upstream_protection" ||
|
||||||
|
component.role === "group_residual_current_protection" ||
|
||||||
|
component.role === "auxiliary";
|
||||||
const protection = component.protectionDevice;
|
const protection = component.protectionDevice;
|
||||||
const protectionSummary = protection
|
const protectionSummary = protection
|
||||||
? [
|
? [
|
||||||
@@ -3033,6 +3179,39 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
{protectionSummary}
|
{protectionSummary}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
|
<span className="structure-component-actions">
|
||||||
|
{isMutableComponent ? (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={isSaving}
|
||||||
|
onClick={() =>
|
||||||
|
setComponentEditorIntent({
|
||||||
|
kind: "edit",
|
||||||
|
component,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Bearbeiten
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={isSaving}
|
||||||
|
onClick={() =>
|
||||||
|
void handleDeleteDistributionBoardComponent(
|
||||||
|
component
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Entfernen
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span className="structure-component-fixed">
|
||||||
|
Feste Verteilerkomponente
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -3040,6 +3219,14 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
}
|
}
|
||||||
if (row.rowType === "section") {
|
if (row.rowType === "section") {
|
||||||
const section = data.sections.find((entry) => entry.id === row.sectionId)!;
|
const section = data.sections.find((entry) => entry.id === row.sectionId)!;
|
||||||
|
const hasUpstreamProtection = section.components.some(
|
||||||
|
(component) =>
|
||||||
|
component.role === "group_upstream_protection"
|
||||||
|
);
|
||||||
|
const hasGroupRcd = section.components.some(
|
||||||
|
(component) =>
|
||||||
|
component.role === "group_residual_current_protection"
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<tr
|
<tr
|
||||||
key={row.rowKey}
|
key={row.rowKey}
|
||||||
@@ -3115,6 +3302,52 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="section-actions">
|
<div className="section-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
tabIndex={-1}
|
||||||
|
disabled={
|
||||||
|
hasUpstreamProtection ||
|
||||||
|
!section.category ||
|
||||||
|
!section.groupNumber
|
||||||
|
}
|
||||||
|
onClick={() =>
|
||||||
|
setComponentEditorIntent({
|
||||||
|
kind: "create",
|
||||||
|
role: "group_upstream_protection",
|
||||||
|
sectionId: section.id,
|
||||||
|
initialEquipmentIdentifier:
|
||||||
|
getSuggestedGroupComponentIdentifier(
|
||||||
|
section,
|
||||||
|
"group_upstream_protection"
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Vorsicherung hinzufügen
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
tabIndex={-1}
|
||||||
|
disabled={
|
||||||
|
hasGroupRcd ||
|
||||||
|
!section.category ||
|
||||||
|
!section.groupNumber
|
||||||
|
}
|
||||||
|
onClick={() =>
|
||||||
|
setComponentEditorIntent({
|
||||||
|
kind: "create",
|
||||||
|
role: "group_residual_current_protection",
|
||||||
|
sectionId: section.id,
|
||||||
|
initialEquipmentIdentifier:
|
||||||
|
getSuggestedGroupComponentIdentifier(
|
||||||
|
section,
|
||||||
|
"group_residual_current_protection"
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Gruppen-FI hinzufügen
|
||||||
|
</button>
|
||||||
<button type="button" tabIndex={-1} onClick={() => void handleAddReserveCircuit(section.id)}>
|
<button type="button" tabIndex={-1} onClick={() => void handleAddReserveCircuit(section.id)}>
|
||||||
Stromkreis hinzufügen
|
Stromkreis hinzufügen
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -0,0 +1,316 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { type FormEvent, useState } from "react";
|
||||||
|
import {
|
||||||
|
allowedRatedCurrentsAByProtectionDeviceType,
|
||||||
|
breakerTripCharacteristics,
|
||||||
|
fuseProtectionDeviceTypes,
|
||||||
|
fuseUtilizationCategories,
|
||||||
|
protectionDeviceTypeLabels,
|
||||||
|
ratedResidualCurrentsMa,
|
||||||
|
rcdTypes,
|
||||||
|
type ProtectionDeviceType,
|
||||||
|
} from "../../shared/constants/protection-device";
|
||||||
|
import type { CircuitTreeComponentDto } from "../types";
|
||||||
|
import type {
|
||||||
|
DistributionBoardComponentEditorValues,
|
||||||
|
MutableDistributionBoardComponentRole,
|
||||||
|
} from "../utils/distribution-board-component-editing";
|
||||||
|
import { FormModal } from "./form-modal";
|
||||||
|
|
||||||
|
interface DistributionBoardComponentModalProps {
|
||||||
|
initialComponent?: CircuitTreeComponentDto;
|
||||||
|
initialEquipmentIdentifier?: string;
|
||||||
|
isSaving: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSave: (values: DistributionBoardComponentEditorValues) => Promise<void>;
|
||||||
|
role: MutableDistributionBoardComponentRole;
|
||||||
|
}
|
||||||
|
|
||||||
|
const upstreamProtectionTypes: readonly ProtectionDeviceType[] = [
|
||||||
|
...fuseProtectionDeviceTypes,
|
||||||
|
"LS",
|
||||||
|
];
|
||||||
|
|
||||||
|
export function DistributionBoardComponentModal({
|
||||||
|
initialComponent,
|
||||||
|
initialEquipmentIdentifier = "",
|
||||||
|
isSaving,
|
||||||
|
onClose,
|
||||||
|
onSave,
|
||||||
|
role,
|
||||||
|
}: DistributionBoardComponentModalProps) {
|
||||||
|
const [equipmentIdentifier, setEquipmentIdentifier] = useState(
|
||||||
|
initialComponent?.equipmentIdentifier ?? initialEquipmentIdentifier
|
||||||
|
);
|
||||||
|
const [name, setName] = useState(
|
||||||
|
initialComponent?.name ?? defaultName(role)
|
||||||
|
);
|
||||||
|
const initialProtection = initialComponent?.protectionDevice;
|
||||||
|
const [protectionType, setProtectionType] = useState<
|
||||||
|
ProtectionDeviceType | ""
|
||||||
|
>(
|
||||||
|
initialProtection?.type ??
|
||||||
|
(role === "group_residual_current_protection" ? "FI" : "")
|
||||||
|
);
|
||||||
|
const [ratedCurrentA, setRatedCurrentA] = useState(
|
||||||
|
initialProtection?.ratedCurrentA ??
|
||||||
|
(role === "group_residual_current_protection" ? 40 : 0)
|
||||||
|
);
|
||||||
|
const [fuseUtilizationCategory, setFuseUtilizationCategory] = useState(
|
||||||
|
initialProtection?.fuseUtilizationCategory ?? "gG"
|
||||||
|
);
|
||||||
|
const [tripCharacteristic, setTripCharacteristic] = useState(
|
||||||
|
initialProtection?.tripCharacteristic ?? "B"
|
||||||
|
);
|
||||||
|
const [rcdType, setRcdType] = useState(
|
||||||
|
initialProtection?.rcdType ?? "A"
|
||||||
|
);
|
||||||
|
const [ratedResidualCurrentMa, setRatedResidualCurrentMa] = useState(
|
||||||
|
initialProtection?.ratedResidualCurrentMa ?? 30
|
||||||
|
);
|
||||||
|
|
||||||
|
const isAuxiliary = role === "auxiliary";
|
||||||
|
const availableProtectionTypes =
|
||||||
|
role === "group_residual_current_protection"
|
||||||
|
? (["FI"] as const)
|
||||||
|
: upstreamProtectionTypes;
|
||||||
|
const usesFuseCategory =
|
||||||
|
protectionType !== "" &&
|
||||||
|
(fuseProtectionDeviceTypes as readonly string[]).includes(protectionType);
|
||||||
|
const usesTripCharacteristic = protectionType === "LS";
|
||||||
|
const usesResidualCurrent = protectionType === "FI";
|
||||||
|
const allowedRatedCurrents =
|
||||||
|
protectionType === ""
|
||||||
|
? []
|
||||||
|
: allowedRatedCurrentsAByProtectionDeviceType[protectionType];
|
||||||
|
|
||||||
|
function handleProtectionTypeChange(type: ProtectionDeviceType) {
|
||||||
|
setProtectionType(type);
|
||||||
|
setRatedCurrentA(allowedRatedCurrentsAByProtectionDeviceType[type][0]);
|
||||||
|
setFuseUtilizationCategory("gG");
|
||||||
|
setTripCharacteristic("B");
|
||||||
|
setRcdType("A");
|
||||||
|
setRatedResidualCurrentMa(30);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
||||||
|
event.preventDefault();
|
||||||
|
await onSave({
|
||||||
|
equipmentIdentifier,
|
||||||
|
name,
|
||||||
|
...(!isAuxiliary && protectionType
|
||||||
|
? {
|
||||||
|
protectionDevice: {
|
||||||
|
type: protectionType,
|
||||||
|
ratedCurrentA,
|
||||||
|
...(usesFuseCategory
|
||||||
|
? { fuseUtilizationCategory }
|
||||||
|
: {}),
|
||||||
|
...(usesTripCharacteristic
|
||||||
|
? { tripCharacteristic }
|
||||||
|
: {}),
|
||||||
|
...(usesResidualCurrent
|
||||||
|
? { rcdType, ratedResidualCurrentMa }
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValid =
|
||||||
|
equipmentIdentifier.trim().length > 0 &&
|
||||||
|
name.trim().length > 0 &&
|
||||||
|
(isAuxiliary ||
|
||||||
|
(protectionType !== "" &&
|
||||||
|
(allowedRatedCurrents as readonly number[]).includes(ratedCurrentA)));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormModal
|
||||||
|
description={
|
||||||
|
isAuxiliary
|
||||||
|
? "Zusätzliches Gerät im Verteiler. BMK und Name werden manuell vergeben."
|
||||||
|
: "Schutzgerät dieser Stromkreisgruppe konfigurieren."
|
||||||
|
}
|
||||||
|
isSaving={isSaving}
|
||||||
|
onClose={onClose}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
submitDisabled={!isValid}
|
||||||
|
submitLabel={initialComponent ? "Änderungen speichern" : "Komponente anlegen"}
|
||||||
|
title={`${initialComponent ? "Komponente bearbeiten" : "Komponente hinzufügen"} – ${defaultName(role)}`}
|
||||||
|
>
|
||||||
|
<div className="row g-3">
|
||||||
|
<div className="col-12 col-md-6">
|
||||||
|
<label className="form-label" htmlFor="component-equipment-identifier">
|
||||||
|
Betriebsmittelkennzeichen
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
autoFocus
|
||||||
|
className="form-control"
|
||||||
|
id="component-equipment-identifier"
|
||||||
|
onChange={(event) => setEquipmentIdentifier(event.target.value)}
|
||||||
|
required
|
||||||
|
value={equipmentIdentifier}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-12 col-md-6">
|
||||||
|
<label className="form-label" htmlFor="component-name">
|
||||||
|
Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className="form-control"
|
||||||
|
id="component-name"
|
||||||
|
onChange={(event) => setName(event.target.value)}
|
||||||
|
required
|
||||||
|
value={name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{!isAuxiliary ? (
|
||||||
|
<>
|
||||||
|
<div className="col-12 col-md-6">
|
||||||
|
<label className="form-label" htmlFor="component-protection-type">
|
||||||
|
Schutzgerät
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className="form-select"
|
||||||
|
disabled={role === "group_residual_current_protection"}
|
||||||
|
id="component-protection-type"
|
||||||
|
onChange={(event) =>
|
||||||
|
handleProtectionTypeChange(
|
||||||
|
event.target.value as ProtectionDeviceType
|
||||||
|
)
|
||||||
|
}
|
||||||
|
required
|
||||||
|
value={protectionType}
|
||||||
|
>
|
||||||
|
<option value="">Schutzgerät auswählen</option>
|
||||||
|
{availableProtectionTypes.map((type) => (
|
||||||
|
<option key={type} value={type}>
|
||||||
|
{protectionDeviceTypeLabels[type]}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="col-12 col-md-6">
|
||||||
|
<label className="form-label" htmlFor="component-rated-current">
|
||||||
|
Bemessungsstrom
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className="form-select"
|
||||||
|
disabled={!protectionType}
|
||||||
|
id="component-rated-current"
|
||||||
|
onChange={(event) => setRatedCurrentA(Number(event.target.value))}
|
||||||
|
value={ratedCurrentA}
|
||||||
|
>
|
||||||
|
{allowedRatedCurrents.map((ratedCurrent) => (
|
||||||
|
<option key={ratedCurrent} value={ratedCurrent}>
|
||||||
|
{ratedCurrent} A
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{usesFuseCategory ? (
|
||||||
|
<div className="col-12 col-md-6">
|
||||||
|
<label className="form-label" htmlFor="component-fuse-category">
|
||||||
|
Sicherungscharakteristik
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className="form-select"
|
||||||
|
id="component-fuse-category"
|
||||||
|
onChange={(event) =>
|
||||||
|
setFuseUtilizationCategory(
|
||||||
|
event.target.value as (typeof fuseUtilizationCategories)[number]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
value={fuseUtilizationCategory}
|
||||||
|
>
|
||||||
|
{fuseUtilizationCategories.map((category) => (
|
||||||
|
<option key={category} value={category}>
|
||||||
|
{category}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{usesTripCharacteristic ? (
|
||||||
|
<div className="col-12 col-md-6">
|
||||||
|
<label className="form-label" htmlFor="component-trip-characteristic">
|
||||||
|
Auslösecharakteristik
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className="form-select"
|
||||||
|
id="component-trip-characteristic"
|
||||||
|
onChange={(event) =>
|
||||||
|
setTripCharacteristic(
|
||||||
|
event.target.value as (typeof breakerTripCharacteristics)[number]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
value={tripCharacteristic}
|
||||||
|
>
|
||||||
|
{breakerTripCharacteristics.map((characteristic) => (
|
||||||
|
<option key={characteristic} value={characteristic}>
|
||||||
|
{characteristic}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{usesResidualCurrent ? (
|
||||||
|
<>
|
||||||
|
<div className="col-12 col-md-6">
|
||||||
|
<label className="form-label" htmlFor="component-rcd-type">
|
||||||
|
FI-Typ
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className="form-select"
|
||||||
|
id="component-rcd-type"
|
||||||
|
onChange={(event) =>
|
||||||
|
setRcdType(event.target.value as (typeof rcdTypes)[number])
|
||||||
|
}
|
||||||
|
value={rcdType}
|
||||||
|
>
|
||||||
|
{rcdTypes.map((type) => (
|
||||||
|
<option key={type} value={type}>
|
||||||
|
Typ {type}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="col-12 col-md-6">
|
||||||
|
<label className="form-label" htmlFor="component-residual-current">
|
||||||
|
Bemessungsdifferenzstrom
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className="form-select"
|
||||||
|
id="component-residual-current"
|
||||||
|
onChange={(event) =>
|
||||||
|
setRatedResidualCurrentMa(Number(event.target.value))
|
||||||
|
}
|
||||||
|
value={ratedResidualCurrentMa}
|
||||||
|
>
|
||||||
|
{ratedResidualCurrentsMa.map((ratedResidualCurrent) => (
|
||||||
|
<option key={ratedResidualCurrent} value={ratedResidualCurrent}>
|
||||||
|
{ratedResidualCurrent} mA
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</FormModal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultName(role: MutableDistributionBoardComponentRole): string {
|
||||||
|
if (role === "group_upstream_protection") {
|
||||||
|
return "Gruppenvorsicherung";
|
||||||
|
}
|
||||||
|
if (role === "group_residual_current_protection") {
|
||||||
|
return "Gruppen-FI";
|
||||||
|
}
|
||||||
|
return "Verteilergerät";
|
||||||
|
}
|
||||||
@@ -52,6 +52,9 @@ import type {
|
|||||||
import type {
|
import type {
|
||||||
CircuitSectionRenumberAssignment,
|
CircuitSectionRenumberAssignment,
|
||||||
} from "../../domain/models/circuit-section-renumber-project-command.model";
|
} from "../../domain/models/circuit-section-renumber-project-command.model";
|
||||||
|
import type {
|
||||||
|
DistributionBoardComponentSnapshot,
|
||||||
|
} from "../../domain/models/distribution-board-component-structure-project-command.model";
|
||||||
|
|
||||||
async function request<T>(url: string, init?: RequestInit): Promise<T> {
|
async function request<T>(url: string, init?: RequestInit): Promise<T> {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
@@ -320,6 +323,58 @@ export function getCircuitTree(projectId: string, circuitListId: string) {
|
|||||||
return request<CircuitTreeResponseDto>(`/api/projects/${projectId}/circuit-lists/${circuitListId}/tree`);
|
return request<CircuitTreeResponseDto>(`/api/projects/${projectId}/circuit-lists/${circuitListId}/tree`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function insertDistributionBoardComponentCommand(
|
||||||
|
projectId: string,
|
||||||
|
expectedRevision: number,
|
||||||
|
snapshot: DistributionBoardComponentSnapshot
|
||||||
|
) {
|
||||||
|
return executeProjectCommand(
|
||||||
|
projectId,
|
||||||
|
expectedRevision,
|
||||||
|
{
|
||||||
|
schemaVersion: 1,
|
||||||
|
type: "distribution-board-component.insert",
|
||||||
|
payload: { snapshot },
|
||||||
|
},
|
||||||
|
"Verteilerkomponente hinzufügen"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateDistributionBoardComponentCommand(
|
||||||
|
projectId: string,
|
||||||
|
expectedRevision: number,
|
||||||
|
expected: DistributionBoardComponentSnapshot,
|
||||||
|
target: DistributionBoardComponentSnapshot
|
||||||
|
) {
|
||||||
|
return executeProjectCommand(
|
||||||
|
projectId,
|
||||||
|
expectedRevision,
|
||||||
|
{
|
||||||
|
schemaVersion: 1,
|
||||||
|
type: "distribution-board-component.update",
|
||||||
|
payload: { expected, target },
|
||||||
|
},
|
||||||
|
"Verteilerkomponente bearbeiten"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteDistributionBoardComponentCommand(
|
||||||
|
projectId: string,
|
||||||
|
expectedRevision: number,
|
||||||
|
snapshot: DistributionBoardComponentSnapshot
|
||||||
|
) {
|
||||||
|
return executeProjectCommand(
|
||||||
|
projectId,
|
||||||
|
expectedRevision,
|
||||||
|
{
|
||||||
|
schemaVersion: 1,
|
||||||
|
type: "distribution-board-component.delete",
|
||||||
|
payload: { snapshot },
|
||||||
|
},
|
||||||
|
"Verteilerkomponente entfernen"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function updateCircuitById(
|
export function updateCircuitById(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
expectedRevision: number,
|
expectedRevision: number,
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
import type {
|
||||||
|
DistributionBoardComponentSnapshot,
|
||||||
|
} from "../../domain/models/distribution-board-component-structure-project-command.model";
|
||||||
|
import type {
|
||||||
|
CircuitTreeComponentDto,
|
||||||
|
CircuitTreeProtectionDeviceDto,
|
||||||
|
CircuitTreeSectionDto,
|
||||||
|
} from "../types";
|
||||||
|
import { circuitGroupCategoryNumbers } from "../../shared/constants/circuit-group";
|
||||||
|
|
||||||
|
export type MutableDistributionBoardComponentRole =
|
||||||
|
| "group_upstream_protection"
|
||||||
|
| "group_residual_current_protection"
|
||||||
|
| "auxiliary";
|
||||||
|
|
||||||
|
export interface DistributionBoardComponentEditorValues {
|
||||||
|
equipmentIdentifier: string;
|
||||||
|
name: string;
|
||||||
|
protectionDevice?: CircuitTreeProtectionDeviceDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toDistributionBoardComponentSnapshot(
|
||||||
|
component: CircuitTreeComponentDto
|
||||||
|
): DistributionBoardComponentSnapshot {
|
||||||
|
if (
|
||||||
|
component.role !== "group_upstream_protection" &&
|
||||||
|
component.role !== "group_residual_current_protection" &&
|
||||||
|
component.role !== "auxiliary"
|
||||||
|
) {
|
||||||
|
throw new Error("Feste Verteilerkomponenten können hier nicht bearbeitet werden.");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
component: {
|
||||||
|
id: component.id,
|
||||||
|
circuitListId: component.circuitListId,
|
||||||
|
sectionId: component.sectionId ?? null,
|
||||||
|
equipmentIdentifier: component.equipmentIdentifier,
|
||||||
|
name: component.name,
|
||||||
|
role: component.role,
|
||||||
|
placement: component.placement,
|
||||||
|
sortOrder: component.sortOrder,
|
||||||
|
},
|
||||||
|
protectionDevice: component.protectionDevice
|
||||||
|
? {
|
||||||
|
componentId: component.id,
|
||||||
|
type: component.protectionDevice.type,
|
||||||
|
ratedCurrentA: component.protectionDevice.ratedCurrentA,
|
||||||
|
fuseUtilizationCategory:
|
||||||
|
component.protectionDevice.fuseUtilizationCategory ?? null,
|
||||||
|
tripCharacteristic:
|
||||||
|
component.protectionDevice.tripCharacteristic ?? null,
|
||||||
|
rcdType: component.protectionDevice.rcdType ?? null,
|
||||||
|
ratedResidualCurrentMa:
|
||||||
|
component.protectionDevice.ratedResidualCurrentMa ?? null,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildDistributionBoardComponentSnapshot(input: {
|
||||||
|
id: string;
|
||||||
|
circuitListId: string;
|
||||||
|
role: MutableDistributionBoardComponentRole;
|
||||||
|
sectionId?: string;
|
||||||
|
sortOrder: number;
|
||||||
|
values: DistributionBoardComponentEditorValues;
|
||||||
|
}): DistributionBoardComponentSnapshot {
|
||||||
|
const groupComponent = input.role !== "auxiliary";
|
||||||
|
if (groupComponent && !input.sectionId) {
|
||||||
|
throw new Error("Für den Gruppenschutz fehlt die Stromkreisgruppe.");
|
||||||
|
}
|
||||||
|
if (groupComponent && !input.values.protectionDevice) {
|
||||||
|
throw new Error("Für den Gruppenschutz fehlt die Schutzgerätekonfiguration.");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
component: {
|
||||||
|
id: input.id,
|
||||||
|
circuitListId: input.circuitListId,
|
||||||
|
sectionId: groupComponent ? input.sectionId! : null,
|
||||||
|
equipmentIdentifier: input.values.equipmentIdentifier.trim(),
|
||||||
|
name: input.values.name.trim(),
|
||||||
|
role: input.role,
|
||||||
|
placement: groupComponent ? "group" : "footer",
|
||||||
|
sortOrder: input.sortOrder,
|
||||||
|
},
|
||||||
|
protectionDevice:
|
||||||
|
groupComponent && input.values.protectionDevice
|
||||||
|
? {
|
||||||
|
componentId: input.id,
|
||||||
|
type: input.values.protectionDevice.type,
|
||||||
|
ratedCurrentA: input.values.protectionDevice.ratedCurrentA,
|
||||||
|
fuseUtilizationCategory:
|
||||||
|
input.values.protectionDevice.fuseUtilizationCategory ?? null,
|
||||||
|
tripCharacteristic:
|
||||||
|
input.values.protectionDevice.tripCharacteristic ?? null,
|
||||||
|
rcdType: input.values.protectionDevice.rcdType ?? null,
|
||||||
|
ratedResidualCurrentMa:
|
||||||
|
input.values.protectionDevice.ratedResidualCurrentMa ?? null,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateDistributionBoardComponentSnapshot(
|
||||||
|
expected: DistributionBoardComponentSnapshot,
|
||||||
|
values: DistributionBoardComponentEditorValues
|
||||||
|
): DistributionBoardComponentSnapshot {
|
||||||
|
return buildDistributionBoardComponentSnapshot({
|
||||||
|
id: expected.component.id,
|
||||||
|
circuitListId: expected.component.circuitListId,
|
||||||
|
role: expected.component.role,
|
||||||
|
sectionId: expected.component.sectionId ?? undefined,
|
||||||
|
sortOrder: expected.component.sortOrder,
|
||||||
|
values,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSuggestedGroupComponentIdentifier(
|
||||||
|
section: CircuitTreeSectionDto,
|
||||||
|
role:
|
||||||
|
| "group_upstream_protection"
|
||||||
|
| "group_residual_current_protection"
|
||||||
|
): string {
|
||||||
|
if (!section.category || !section.groupNumber) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const categoryNumber = circuitGroupCategoryNumbers[section.category];
|
||||||
|
const deviceLetter =
|
||||||
|
role === "group_upstream_protection" ? "F" : "Q";
|
||||||
|
return `-${categoryNumber}${deviceLetter}${section.groupNumber}.0`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getNextComponentSortOrder(
|
||||||
|
components: readonly { sortOrder: number }[]
|
||||||
|
): number {
|
||||||
|
return components.length === 0
|
||||||
|
? 10
|
||||||
|
: Math.max(...components.map((component) => component.sortOrder)) + 10;
|
||||||
|
}
|
||||||
@@ -7,6 +7,13 @@ import {
|
|||||||
distributionBoardComponentRoles,
|
distributionBoardComponentRoles,
|
||||||
} from "../src/shared/constants/distribution-board-component.js";
|
} from "../src/shared/constants/distribution-board-component.js";
|
||||||
import { circuitGroupCategories } from "../src/shared/constants/circuit-group.js";
|
import { circuitGroupCategories } from "../src/shared/constants/circuit-group.js";
|
||||||
|
import {
|
||||||
|
buildDistributionBoardComponentSnapshot,
|
||||||
|
getNextComponentSortOrder,
|
||||||
|
getSuggestedGroupComponentIdentifier,
|
||||||
|
toDistributionBoardComponentSnapshot,
|
||||||
|
updateDistributionBoardComponentSnapshot,
|
||||||
|
} from "../src/frontend/utils/distribution-board-component-editing.js";
|
||||||
import "./distribution-board-component-structure-project-command.repository.test.js";
|
import "./distribution-board-component-structure-project-command.repository.test.js";
|
||||||
|
|
||||||
describe("distribution board component catalog", () => {
|
describe("distribution board component catalog", () => {
|
||||||
@@ -47,4 +54,92 @@ describe("distribution board component catalog", () => {
|
|||||||
placement: "header",
|
placement: "header",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("builds exact editable component snapshots for persistent commands", () => {
|
||||||
|
const groupRcd = buildDistributionBoardComponentSnapshot({
|
||||||
|
id: "component-1",
|
||||||
|
circuitListId: "list-1",
|
||||||
|
role: "group_residual_current_protection",
|
||||||
|
sectionId: "section-1",
|
||||||
|
sortOrder: 20,
|
||||||
|
values: {
|
||||||
|
equipmentIdentifier: " -1Q1.0 ",
|
||||||
|
name: " Gruppen-FI ",
|
||||||
|
protectionDevice: {
|
||||||
|
type: "FI",
|
||||||
|
ratedCurrentA: 40,
|
||||||
|
rcdType: "A",
|
||||||
|
ratedResidualCurrentMa: 30,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(groupRcd, {
|
||||||
|
component: {
|
||||||
|
id: "component-1",
|
||||||
|
circuitListId: "list-1",
|
||||||
|
sectionId: "section-1",
|
||||||
|
equipmentIdentifier: "-1Q1.0",
|
||||||
|
name: "Gruppen-FI",
|
||||||
|
role: "group_residual_current_protection",
|
||||||
|
placement: "group",
|
||||||
|
sortOrder: 20,
|
||||||
|
},
|
||||||
|
protectionDevice: {
|
||||||
|
componentId: "component-1",
|
||||||
|
type: "FI",
|
||||||
|
ratedCurrentA: 40,
|
||||||
|
fuseUtilizationCategory: null,
|
||||||
|
tripCharacteristic: null,
|
||||||
|
rcdType: "A",
|
||||||
|
ratedResidualCurrentMa: 30,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
assert.deepEqual(
|
||||||
|
updateDistributionBoardComponentSnapshot(groupRcd, {
|
||||||
|
equipmentIdentifier: "-1Q1.0",
|
||||||
|
name: "FI Beleuchtung",
|
||||||
|
protectionDevice: {
|
||||||
|
type: "FI",
|
||||||
|
ratedCurrentA: 63,
|
||||||
|
rcdType: "B+",
|
||||||
|
ratedResidualCurrentMa: 300,
|
||||||
|
},
|
||||||
|
}).component,
|
||||||
|
{ ...groupRcd.component, name: "FI Beleuchtung" }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps tree DTOs and derives stable group suggestions and sort positions", () => {
|
||||||
|
const snapshot = toDistributionBoardComponentSnapshot({
|
||||||
|
id: "actor-1",
|
||||||
|
circuitListId: "list-1",
|
||||||
|
equipmentIdentifier: "-K1",
|
||||||
|
name: "KNX Aktor",
|
||||||
|
role: "auxiliary",
|
||||||
|
placement: "footer",
|
||||||
|
sortOrder: 10,
|
||||||
|
});
|
||||||
|
assert.equal(snapshot.component.sectionId, null);
|
||||||
|
assert.equal(snapshot.protectionDevice, null);
|
||||||
|
assert.equal(getNextComponentSortOrder([{ sortOrder: 10 }, { sortOrder: 40 }]), 50);
|
||||||
|
assert.equal(
|
||||||
|
getSuggestedGroupComponentIdentifier(
|
||||||
|
{
|
||||||
|
id: "section-1",
|
||||||
|
key: "lighting",
|
||||||
|
displayName: "Beleuchtung 2",
|
||||||
|
prefix: "-1F2",
|
||||||
|
sortOrder: 10,
|
||||||
|
category: "lighting",
|
||||||
|
groupNumber: 2,
|
||||||
|
sectionTotalPower: 0,
|
||||||
|
components: [],
|
||||||
|
circuits: [],
|
||||||
|
},
|
||||||
|
"group_upstream_protection"
|
||||||
|
),
|
||||||
|
"-1F2.0"
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user