forked from jappel/leistungsbilanz-ts
Configure circuit protection
This commit is contained in:
parent
18ca5eb3d4
commit
8898117ed4
18 changed files with 642 additions and 14 deletions
233
src/frontend/components/circuit-protection-modal.tsx
Normal file
233
src/frontend/components/circuit-protection-modal.tsx
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
"use client";
|
||||
|
||||
import { type FormEvent, useState } from "react";
|
||||
import {
|
||||
allowedRatedCurrentsAByProtectionDeviceType,
|
||||
breakerTripCharacteristics,
|
||||
fuseProtectionDeviceTypes,
|
||||
fuseUtilizationCategories,
|
||||
protectionDeviceTypeLabels,
|
||||
protectionDeviceTypes,
|
||||
ratedResidualCurrentsMa,
|
||||
rcdTypes,
|
||||
type ProtectionDeviceType,
|
||||
} from "../../shared/constants/protection-device";
|
||||
import type { CircuitTreeProtectionDeviceDto } from "../types";
|
||||
import { FormModal } from "./form-modal";
|
||||
|
||||
interface CircuitProtectionModalProps {
|
||||
equipmentIdentifier: string;
|
||||
initialProtection: CircuitTreeProtectionDeviceDto;
|
||||
isSaving: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (protection: CircuitTreeProtectionDeviceDto) => Promise<void>;
|
||||
}
|
||||
|
||||
export function CircuitProtectionModal({
|
||||
equipmentIdentifier,
|
||||
initialProtection,
|
||||
isSaving,
|
||||
onClose,
|
||||
onSave,
|
||||
}: CircuitProtectionModalProps) {
|
||||
const [type, setType] = useState(initialProtection.type);
|
||||
const [ratedCurrentA, setRatedCurrentA] = useState(
|
||||
initialProtection.ratedCurrentA
|
||||
);
|
||||
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 usesFuseCategory = (
|
||||
fuseProtectionDeviceTypes as readonly string[]
|
||||
).includes(type);
|
||||
const usesTripCharacteristic =
|
||||
type === "LS" || type === "FI_LS" || type === "AFDD";
|
||||
const usesResidualCurrent = type === "FI" || type === "FI_LS";
|
||||
const allowedRatedCurrents =
|
||||
allowedRatedCurrentsAByProtectionDeviceType[type];
|
||||
|
||||
function handleTypeChange(nextType: ProtectionDeviceType) {
|
||||
setType(nextType);
|
||||
setRatedCurrentA(
|
||||
allowedRatedCurrentsAByProtectionDeviceType[nextType][0]
|
||||
);
|
||||
setFuseUtilizationCategory("gG");
|
||||
setTripCharacteristic("B");
|
||||
setRcdType("A");
|
||||
setRatedResidualCurrentMa(30);
|
||||
}
|
||||
|
||||
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
await onSave({
|
||||
type,
|
||||
ratedCurrentA,
|
||||
...(usesFuseCategory ? { fuseUtilizationCategory } : {}),
|
||||
...(usesTripCharacteristic ? { tripCharacteristic } : {}),
|
||||
...(usesResidualCurrent
|
||||
? { rcdType, ratedResidualCurrentMa }
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<FormModal
|
||||
description="Die Auswahl ist planerisch gesetzt. Eine spätere Dimensionierung wird nur Empfehlungen und Warnungen ergänzen."
|
||||
isSaving={isSaving}
|
||||
onClose={onClose}
|
||||
onSubmit={handleSubmit}
|
||||
submitDisabled={
|
||||
!(allowedRatedCurrents as readonly number[]).includes(
|
||||
ratedCurrentA
|
||||
)
|
||||
}
|
||||
submitLabel="Schutzgerät speichern"
|
||||
title={`Schutzgerät bearbeiten – ${equipmentIdentifier}`}
|
||||
>
|
||||
<div className="row g-3">
|
||||
<div className="col-12 col-md-6">
|
||||
<label className="form-label" htmlFor="circuit-protection-type">
|
||||
Schutzgerät
|
||||
</label>
|
||||
<select
|
||||
autoFocus
|
||||
className="form-select"
|
||||
id="circuit-protection-type"
|
||||
onChange={(event) =>
|
||||
handleTypeChange(
|
||||
event.target.value as ProtectionDeviceType
|
||||
)
|
||||
}
|
||||
value={type}
|
||||
>
|
||||
{protectionDeviceTypes.map((entry) => (
|
||||
<option key={entry} value={entry}>
|
||||
{protectionDeviceTypeLabels[entry]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-12 col-md-6">
|
||||
<label className="form-label" htmlFor="circuit-protection-current">
|
||||
Bemessungsstrom
|
||||
</label>
|
||||
<select
|
||||
className="form-select"
|
||||
id="circuit-protection-current"
|
||||
onChange={(event) =>
|
||||
setRatedCurrentA(Number(event.target.value))
|
||||
}
|
||||
value={ratedCurrentA}
|
||||
>
|
||||
{allowedRatedCurrents.map((current) => (
|
||||
<option key={current} value={current}>
|
||||
{current} A
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
{usesFuseCategory ? (
|
||||
<div className="col-12 col-md-6">
|
||||
<label className="form-label" htmlFor="circuit-fuse-category">
|
||||
Sicherungscharakteristik
|
||||
</label>
|
||||
<select
|
||||
className="form-select"
|
||||
id="circuit-fuse-category"
|
||||
onChange={(event) =>
|
||||
setFuseUtilizationCategory(
|
||||
event.target
|
||||
.value as (typeof fuseUtilizationCategories)[number]
|
||||
)
|
||||
}
|
||||
value={fuseUtilizationCategory}
|
||||
>
|
||||
{fuseUtilizationCategories.map((entry) => (
|
||||
<option key={entry} value={entry}>
|
||||
{entry}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
) : null}
|
||||
{usesTripCharacteristic ? (
|
||||
<div className="col-12 col-md-6">
|
||||
<label className="form-label" htmlFor="circuit-trip-characteristic">
|
||||
Auslösecharakteristik
|
||||
</label>
|
||||
<select
|
||||
className="form-select"
|
||||
id="circuit-trip-characteristic"
|
||||
onChange={(event) =>
|
||||
setTripCharacteristic(
|
||||
event.target
|
||||
.value as (typeof breakerTripCharacteristics)[number]
|
||||
)
|
||||
}
|
||||
value={tripCharacteristic}
|
||||
>
|
||||
{breakerTripCharacteristics.map((entry) => (
|
||||
<option key={entry} value={entry}>
|
||||
{entry}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
) : null}
|
||||
{usesResidualCurrent ? (
|
||||
<>
|
||||
<div className="col-12 col-md-6">
|
||||
<label className="form-label" htmlFor="circuit-rcd-type">
|
||||
FI-Typ
|
||||
</label>
|
||||
<select
|
||||
className="form-select"
|
||||
id="circuit-rcd-type"
|
||||
onChange={(event) =>
|
||||
setRcdType(
|
||||
event.target.value as (typeof rcdTypes)[number]
|
||||
)
|
||||
}
|
||||
value={rcdType}
|
||||
>
|
||||
{rcdTypes.map((entry) => (
|
||||
<option key={entry} value={entry}>
|
||||
Typ {entry}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-12 col-md-6">
|
||||
<label className="form-label" htmlFor="circuit-residual-current">
|
||||
Bemessungsdifferenzstrom
|
||||
</label>
|
||||
<select
|
||||
className="form-select"
|
||||
id="circuit-residual-current"
|
||||
onChange={(event) =>
|
||||
setRatedResidualCurrentMa(Number(event.target.value))
|
||||
}
|
||||
value={ratedResidualCurrentMa}
|
||||
>
|
||||
{ratedResidualCurrentsMa.map((entry) => (
|
||||
<option key={entry} value={entry}>
|
||||
{entry} mA
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</FormModal>
|
||||
);
|
||||
}
|
||||
|
|
@ -38,6 +38,10 @@ import {
|
|||
buildCircuitDeviceRowInsertSnapshot,
|
||||
buildCircuitInsertSnapshot,
|
||||
} from "../utils/circuit-structure-command";
|
||||
import {
|
||||
getCircuitProtectionEditorInitialValue,
|
||||
toCircuitProtectionSnapshot,
|
||||
} from "../utils/circuit-protection-editing";
|
||||
import {
|
||||
buildCircuitDeviceRowMoveAssignments,
|
||||
} from "../utils/circuit-device-row-move-command";
|
||||
|
|
@ -95,6 +99,7 @@ import {
|
|||
redoProjectCommand,
|
||||
updateCircuitById,
|
||||
updateCircuitDeviceRowById,
|
||||
updateCircuitProtectionCommand,
|
||||
updateCircuitGroupCommand,
|
||||
updateDistributionBoardComponentCommand,
|
||||
undoProjectCommand,
|
||||
|
|
@ -102,6 +107,7 @@ import {
|
|||
import type {
|
||||
CircuitTreeCircuitDto,
|
||||
CircuitTreeComponentDto,
|
||||
CircuitTreeProtectionDeviceDto,
|
||||
CircuitTreeResponseDto,
|
||||
CreateCircuitDeviceRowInputDto,
|
||||
CreateCircuitInputDto,
|
||||
|
|
@ -117,6 +123,7 @@ import type {
|
|||
} from "../../domain/models/circuit-structure-project-command.model";
|
||||
import { DistributionBoardComponentModal } from "./distribution-board-component-modal";
|
||||
import { CircuitGroupModal } from "./circuit-group-modal";
|
||||
import { CircuitProtectionModal } from "./circuit-protection-modal";
|
||||
import type { CircuitGroupCategory } from "../../shared/constants/circuit-group";
|
||||
|
||||
type SaveDirection = "stay" | "next" | "prev";
|
||||
|
|
@ -246,6 +253,8 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
useState<StructureComponentEditorIntent | null>(null);
|
||||
const [circuitGroupEditorIntent, setCircuitGroupEditorIntent] =
|
||||
useState<CircuitGroupEditorIntent | null>(null);
|
||||
const [protectionEditorCircuit, setProtectionEditorCircuit] =
|
||||
useState<CircuitTreeCircuitDto | null>(null);
|
||||
const [projectDevices, setProjectDevices] = useState<ProjectDeviceDto[]>([]);
|
||||
const [isProjectDeviceDrawerOpen, setIsProjectDeviceDrawerOpen] =
|
||||
useState(false);
|
||||
|
|
@ -962,6 +971,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
circuitListId,
|
||||
values: { ...values, voltage },
|
||||
deviceRows,
|
||||
category: section.category,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -1225,6 +1235,35 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
});
|
||||
}
|
||||
|
||||
async function handleSaveCircuitProtection(
|
||||
protection: CircuitTreeProtectionDeviceDto
|
||||
) {
|
||||
const circuit = protectionEditorCircuit;
|
||||
if (!circuit) {
|
||||
return;
|
||||
}
|
||||
await runCommand({
|
||||
label: "Stromkreisschutz bearbeiten",
|
||||
redo: async () => {
|
||||
const result = await updateCircuitProtectionCommand(
|
||||
projectId,
|
||||
getExpectedProjectRevision(),
|
||||
circuit.id,
|
||||
circuit.protectionDevice
|
||||
? toCircuitProtectionSnapshot(
|
||||
circuit.id,
|
||||
circuit.protectionDevice
|
||||
)
|
||||
: null,
|
||||
toCircuitProtectionSnapshot(circuit.id, protection)
|
||||
);
|
||||
applyProjectCommandResult(result);
|
||||
setProtectionEditorCircuit(null);
|
||||
return null;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function handleRedo() {
|
||||
if (historyBusy || isSaving || !historyState || historyState.redoDepth === 0) {
|
||||
return;
|
||||
|
|
@ -2834,6 +2873,23 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
onSave={handleSaveCircuitGroup}
|
||||
/>
|
||||
) : null}
|
||||
{protectionEditorCircuit ? (
|
||||
<CircuitProtectionModal
|
||||
equipmentIdentifier={
|
||||
protectionEditorCircuit.equipmentIdentifier
|
||||
}
|
||||
initialProtection={getCircuitProtectionEditorInitialValue(
|
||||
data.sections.find(
|
||||
(section) =>
|
||||
section.id === protectionEditorCircuit.sectionId
|
||||
)?.category,
|
||||
protectionEditorCircuit.protectionDevice
|
||||
)}
|
||||
isSaving={isSaving}
|
||||
onClose={() => setProtectionEditorCircuit(null)}
|
||||
onSave={handleSaveCircuitProtection}
|
||||
/>
|
||||
) : null}
|
||||
<div className="editor-toolbar">
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -4033,6 +4089,15 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
>
|
||||
{row.circuit && row.rowType !== "deviceRow" ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={-1}
|
||||
onClick={() =>
|
||||
setProtectionEditorCircuit(row.circuit!)
|
||||
}
|
||||
>
|
||||
Schutzgerät
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={-1}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue