Configure circuit protection
This commit is contained in:
@@ -378,6 +378,14 @@ Projektzugehörigkeit und schreibt Schutzgerät, Revision und Historienübergang
|
|||||||
atomar. Benutzer können Schutzdaten anlegen oder ändern, aber nicht entfernen;
|
atomar. Benutzer können Schutzdaten anlegen oder ändern, aber nicht entfernen;
|
||||||
Undo einer erstmaligen Anlage darf den zuvor fehlenden Datensatz exakt
|
Undo einer erstmaligen Anlage darf den zuvor fehlenden Datensatz exakt
|
||||||
wiederherstellen.
|
wiederherstellen.
|
||||||
|
Der vollständige `CircuitSnapshot` kann rückwärtskompatibel einen
|
||||||
|
`protectionDevice`-Datensatz enthalten. Neue gruppierte Stromkreise und durch
|
||||||
|
Geräteverschiebung erzeugte Zielstromkreise verwenden die vereinbarten
|
||||||
|
Kategorie-Standardwerte und schreiben Schutzgerät, Stromkreis sowie
|
||||||
|
Gerätezeilen atomar. Delete/Undo erfasst denselben Datensatz vollständig.
|
||||||
|
Im Editor sind die bisherigen Schutzspalten deshalb nur noch eine
|
||||||
|
schreibgeschützte Projektion der 1:1-Daten; Änderungen erfolgen über ein
|
||||||
|
geräteabhängiges Schutzgeräte-Modal und `circuit-protection.update`.
|
||||||
`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
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ requirements and intended sequencing, not proof of implementation.
|
|||||||
- [x] Phase E3a: create, edit and delete mutable group and footer components.
|
- [x] Phase E3a: create, edit and delete mutable group and footer components.
|
||||||
- [x] Phase E3b1: create, rename and safely delete empty circuit groups.
|
- [x] Phase E3b1: create, rename and safely delete empty circuit groups.
|
||||||
- [x] Phase E3b2a: persistent circuit-protection update command.
|
- [x] Phase E3b2a: persistent circuit-protection update command.
|
||||||
- [ ] Phase E3b2b: circuit-protection defaults on insert and editor modal.
|
- [x] Phase E3b2b: circuit-protection defaults on insert and editor modal.
|
||||||
- [ ] Phase E4: group reorder, renumber, circuit moves and populated-delete warning.
|
- [ ] Phase E4: group reorder, renumber, circuit moves and populated-delete warning.
|
||||||
- [ ] 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.
|
||||||
|
|||||||
@@ -743,8 +743,8 @@ Status: In progress. The complete tree read model E1, pure structural
|
|||||||
projection E2a, grid rendering E2b and mutable component editing E3a are
|
projection E2a, grid rendering E2b and mutable component editing E3a are
|
||||||
complete. Basic group management E3b1 is also complete. Circuit-protection
|
complete. Basic group management E3b1 is also complete. Circuit-protection
|
||||||
editing now has its persistent E3b2a command boundary; insertion defaults and
|
editing now has its persistent E3b2a command boundary; insertion defaults and
|
||||||
the editor modal as well as structural drag/renumber/delete workflows remain
|
the editor modal are complete in E3b2b. Structural drag/renumber/delete
|
||||||
pending.
|
workflows remain pending.
|
||||||
|
|
||||||
- render fixed header components
|
- render fixed header components
|
||||||
- render group components and circuit blocks
|
- render group components and circuit blocks
|
||||||
@@ -783,6 +783,12 @@ Implemented in E1/E2a/E2b/E3a:
|
|||||||
snapshot, rejects stale state and preserves exact persistent Undo/Redo
|
snapshot, rejects stale state and preserves exact persistent Undo/Redo
|
||||||
- user commands cannot remove circuit protection; a nullable target exists
|
- user commands cannot remove circuit protection; a nullable target exists
|
||||||
only as the inverse of adding protection to a retained circuit without it
|
only as the inverse of adding protection to a retained circuit without it
|
||||||
|
- newly inserted circuits and placeholder targets carry the category-specific
|
||||||
|
default protection in their complete atomic snapshot
|
||||||
|
- circuit deletion and restoration preserve the exact 1:1 protection row
|
||||||
|
together with the circuit and all device rows
|
||||||
|
- the editor modal exposes only fields valid for the selected protection type;
|
||||||
|
the old flat protection columns now project the new 1:1 data read-only
|
||||||
|
|
||||||
Acceptance:
|
Acceptance:
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import type {
|
|||||||
import type { AppDatabase } from "../database-context.js";
|
import type { AppDatabase } from "../database-context.js";
|
||||||
import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
|
import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
|
||||||
import { circuitLists } from "../schema/circuit-lists.js";
|
import { circuitLists } from "../schema/circuit-lists.js";
|
||||||
|
import { circuitProtectionDevices } from "../schema/circuit-protection-devices.js";
|
||||||
import { circuitSections } from "../schema/circuit-sections.js";
|
import { circuitSections } from "../schema/circuit-sections.js";
|
||||||
import { circuits } from "../schema/circuits.js";
|
import { circuits } from "../schema/circuits.js";
|
||||||
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
||||||
@@ -396,6 +397,12 @@ export class CircuitDeviceRowMoveProjectCommandRepository
|
|||||||
remark: snapshot.remark,
|
remark: snapshot.remark,
|
||||||
})
|
})
|
||||||
.run();
|
.run();
|
||||||
|
if (snapshot.protectionDevice) {
|
||||||
|
database
|
||||||
|
.insert(circuitProtectionDevices)
|
||||||
|
.values(snapshot.protectionDevice)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private assertTargetCircuitUnchanged(
|
private assertTargetCircuitUnchanged(
|
||||||
@@ -438,6 +445,27 @@ export class CircuitDeviceRowMoveProjectCommandRepository
|
|||||||
"Created target circuit changed before command execution."
|
"Created target circuit changed before command execution."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const protectionDevice = database
|
||||||
|
.select()
|
||||||
|
.from(circuitProtectionDevices)
|
||||||
|
.where(eq(circuitProtectionDevices.circuitId, snapshot.id))
|
||||||
|
.get();
|
||||||
|
if (
|
||||||
|
snapshot.protectionDevice === undefined
|
||||||
|
? protectionDevice !== undefined
|
||||||
|
: snapshot.protectionDevice === null
|
||||||
|
? protectionDevice !== undefined
|
||||||
|
: !protectionDevice ||
|
||||||
|
Object.entries(snapshot.protectionDevice).some(
|
||||||
|
([key, value]) =>
|
||||||
|
(protectionDevice as Record<string, unknown>)[key] !==
|
||||||
|
value
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Created target circuit protection changed before command execution."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const currentRowIds = database
|
const currentRowIds = database
|
||||||
.select({ id: circuitDeviceRows.id })
|
.select({ id: circuitDeviceRows.id })
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { isElectricalPhaseType } from "../../domain/services/project-voltage.ser
|
|||||||
import type { AppDatabase } from "../database-context.js";
|
import type { AppDatabase } from "../database-context.js";
|
||||||
import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
|
import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
|
||||||
import { circuitLists } from "../schema/circuit-lists.js";
|
import { circuitLists } from "../schema/circuit-lists.js";
|
||||||
|
import { circuitProtectionDevices } from "../schema/circuit-protection-devices.js";
|
||||||
import { circuitSections } from "../schema/circuit-sections.js";
|
import { circuitSections } from "../schema/circuit-sections.js";
|
||||||
import { circuits } from "../schema/circuits.js";
|
import { circuits } from "../schema/circuits.js";
|
||||||
import {
|
import {
|
||||||
@@ -175,6 +176,12 @@ export class CircuitStructureProjectCommandRepository
|
|||||||
if (snapshot.deviceRows.length > 0) {
|
if (snapshot.deviceRows.length > 0) {
|
||||||
database.insert(circuitDeviceRows).values(snapshot.deviceRows).run();
|
database.insert(circuitDeviceRows).values(snapshot.deviceRows).run();
|
||||||
}
|
}
|
||||||
|
if (snapshot.protectionDevice) {
|
||||||
|
database
|
||||||
|
.insert(circuitProtectionDevices)
|
||||||
|
.values(snapshot.protectionDevice)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
return createCircuitDeleteProjectCommand(
|
return createCircuitDeleteProjectCommand(
|
||||||
snapshot.id,
|
snapshot.id,
|
||||||
@@ -219,6 +226,11 @@ export class CircuitStructureProjectCommandRepository
|
|||||||
asc(circuitDeviceRows.id)
|
asc(circuitDeviceRows.id)
|
||||||
)
|
)
|
||||||
.all();
|
.all();
|
||||||
|
const protectionDevice = database
|
||||||
|
.select()
|
||||||
|
.from(circuitProtectionDevices)
|
||||||
|
.where(eq(circuitProtectionDevices.circuitId, circuit.id))
|
||||||
|
.get();
|
||||||
const inverse = createCircuitInsertProjectCommand({
|
const inverse = createCircuitInsertProjectCommand({
|
||||||
id: circuit.id,
|
id: circuit.id,
|
||||||
circuitListId: circuit.circuitListId,
|
circuitListId: circuit.circuitListId,
|
||||||
@@ -240,6 +252,7 @@ export class CircuitStructureProjectCommandRepository
|
|||||||
isReserve: Boolean(circuit.isReserve),
|
isReserve: Boolean(circuit.isReserve),
|
||||||
remark: circuit.remark,
|
remark: circuit.remark,
|
||||||
deviceRows: rows.map(toCircuitDeviceRowSnapshot),
|
deviceRows: rows.map(toCircuitDeviceRowSnapshot),
|
||||||
|
protectionDevice: protectionDevice ?? null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = database
|
const result = database
|
||||||
|
|||||||
@@ -66,17 +66,20 @@ export function assertCircuitProtectionUpdateProjectCommand(
|
|||||||
throw new Error("Circuit protection update must change state.");
|
throw new Error("Circuit protection update must change state.");
|
||||||
}
|
}
|
||||||
if (expected !== null) {
|
if (expected !== null) {
|
||||||
assertSnapshot(expected, circuitId);
|
assertCircuitProtectionSnapshot(expected, circuitId);
|
||||||
}
|
}
|
||||||
if (target !== null) {
|
if (target !== null) {
|
||||||
assertSnapshot(target, circuitId);
|
assertCircuitProtectionSnapshot(target, circuitId);
|
||||||
}
|
}
|
||||||
if (JSON.stringify(expected) === JSON.stringify(target)) {
|
if (JSON.stringify(expected) === JSON.stringify(target)) {
|
||||||
throw new Error("Circuit protection update must change state.");
|
throw new Error("Circuit protection update must change state.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function assertSnapshot(value: unknown, circuitId: string) {
|
export function assertCircuitProtectionSnapshot(
|
||||||
|
value: unknown,
|
||||||
|
circuitId: string
|
||||||
|
) {
|
||||||
if (
|
if (
|
||||||
!isPlainObject(value) ||
|
!isPlainObject(value) ||
|
||||||
Object.keys(value).length !== 7 ||
|
Object.keys(value).length !== 7 ||
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ import {
|
|||||||
type CircuitDeviceRowSnapshot,
|
type CircuitDeviceRowSnapshot,
|
||||||
} from "./circuit-device-row-structure-project-command.model.js";
|
} from "./circuit-device-row-structure-project-command.model.js";
|
||||||
import type { SerializedProjectCommand } from "./project-command.model.js";
|
import type { SerializedProjectCommand } from "./project-command.model.js";
|
||||||
|
import {
|
||||||
|
assertCircuitProtectionSnapshot,
|
||||||
|
type CircuitProtectionSnapshot,
|
||||||
|
} from "./circuit-protection-project-command.model.js";
|
||||||
|
|
||||||
export const circuitInsertCommandType = "circuit.insert" as const;
|
export const circuitInsertCommandType = "circuit.insert" as const;
|
||||||
export const circuitDeleteCommandType = "circuit.delete" as const;
|
export const circuitDeleteCommandType = "circuit.delete" as const;
|
||||||
@@ -31,6 +35,7 @@ export interface CircuitSnapshot {
|
|||||||
isReserve: boolean;
|
isReserve: boolean;
|
||||||
remark: string | null;
|
remark: string | null;
|
||||||
deviceRows: CircuitDeviceRowSnapshot[];
|
deviceRows: CircuitDeviceRowSnapshot[];
|
||||||
|
protectionDevice?: CircuitProtectionSnapshot | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CircuitInsertCommandPayload {
|
export interface CircuitInsertCommandPayload {
|
||||||
@@ -142,6 +147,15 @@ export function assertCircuitInsertProjectCommand(
|
|||||||
if (!Array.isArray(circuit.deviceRows)) {
|
if (!Array.isArray(circuit.deviceRows)) {
|
||||||
throw new Error("circuit.deviceRows must be an array.");
|
throw new Error("circuit.deviceRows must be an array.");
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
circuit.protectionDevice !== undefined &&
|
||||||
|
circuit.protectionDevice !== null
|
||||||
|
) {
|
||||||
|
assertCircuitProtectionSnapshot(
|
||||||
|
circuit.protectionDevice,
|
||||||
|
circuit.id as string
|
||||||
|
);
|
||||||
|
}
|
||||||
if (circuit.isReserve !== (circuit.deviceRows.length === 0)) {
|
if (circuit.isReserve !== (circuit.deviceRows.length === 0)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Circuit reserve state must match whether device rows exist."
|
"Circuit reserve state must match whether device rows exist."
|
||||||
|
|||||||
@@ -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,
|
buildCircuitDeviceRowInsertSnapshot,
|
||||||
buildCircuitInsertSnapshot,
|
buildCircuitInsertSnapshot,
|
||||||
} from "../utils/circuit-structure-command";
|
} from "../utils/circuit-structure-command";
|
||||||
|
import {
|
||||||
|
getCircuitProtectionEditorInitialValue,
|
||||||
|
toCircuitProtectionSnapshot,
|
||||||
|
} from "../utils/circuit-protection-editing";
|
||||||
import {
|
import {
|
||||||
buildCircuitDeviceRowMoveAssignments,
|
buildCircuitDeviceRowMoveAssignments,
|
||||||
} from "../utils/circuit-device-row-move-command";
|
} from "../utils/circuit-device-row-move-command";
|
||||||
@@ -95,6 +99,7 @@ import {
|
|||||||
redoProjectCommand,
|
redoProjectCommand,
|
||||||
updateCircuitById,
|
updateCircuitById,
|
||||||
updateCircuitDeviceRowById,
|
updateCircuitDeviceRowById,
|
||||||
|
updateCircuitProtectionCommand,
|
||||||
updateCircuitGroupCommand,
|
updateCircuitGroupCommand,
|
||||||
updateDistributionBoardComponentCommand,
|
updateDistributionBoardComponentCommand,
|
||||||
undoProjectCommand,
|
undoProjectCommand,
|
||||||
@@ -102,6 +107,7 @@ import {
|
|||||||
import type {
|
import type {
|
||||||
CircuitTreeCircuitDto,
|
CircuitTreeCircuitDto,
|
||||||
CircuitTreeComponentDto,
|
CircuitTreeComponentDto,
|
||||||
|
CircuitTreeProtectionDeviceDto,
|
||||||
CircuitTreeResponseDto,
|
CircuitTreeResponseDto,
|
||||||
CreateCircuitDeviceRowInputDto,
|
CreateCircuitDeviceRowInputDto,
|
||||||
CreateCircuitInputDto,
|
CreateCircuitInputDto,
|
||||||
@@ -117,6 +123,7 @@ import type {
|
|||||||
} from "../../domain/models/circuit-structure-project-command.model";
|
} from "../../domain/models/circuit-structure-project-command.model";
|
||||||
import { DistributionBoardComponentModal } from "./distribution-board-component-modal";
|
import { DistributionBoardComponentModal } from "./distribution-board-component-modal";
|
||||||
import { CircuitGroupModal } from "./circuit-group-modal";
|
import { CircuitGroupModal } from "./circuit-group-modal";
|
||||||
|
import { CircuitProtectionModal } from "./circuit-protection-modal";
|
||||||
import type { CircuitGroupCategory } from "../../shared/constants/circuit-group";
|
import type { CircuitGroupCategory } from "../../shared/constants/circuit-group";
|
||||||
|
|
||||||
type SaveDirection = "stay" | "next" | "prev";
|
type SaveDirection = "stay" | "next" | "prev";
|
||||||
@@ -246,6 +253,8 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
useState<StructureComponentEditorIntent | null>(null);
|
useState<StructureComponentEditorIntent | null>(null);
|
||||||
const [circuitGroupEditorIntent, setCircuitGroupEditorIntent] =
|
const [circuitGroupEditorIntent, setCircuitGroupEditorIntent] =
|
||||||
useState<CircuitGroupEditorIntent | null>(null);
|
useState<CircuitGroupEditorIntent | null>(null);
|
||||||
|
const [protectionEditorCircuit, setProtectionEditorCircuit] =
|
||||||
|
useState<CircuitTreeCircuitDto | null>(null);
|
||||||
const [projectDevices, setProjectDevices] = useState<ProjectDeviceDto[]>([]);
|
const [projectDevices, setProjectDevices] = useState<ProjectDeviceDto[]>([]);
|
||||||
const [isProjectDeviceDrawerOpen, setIsProjectDeviceDrawerOpen] =
|
const [isProjectDeviceDrawerOpen, setIsProjectDeviceDrawerOpen] =
|
||||||
useState(false);
|
useState(false);
|
||||||
@@ -962,6 +971,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
circuitListId,
|
circuitListId,
|
||||||
values: { ...values, voltage },
|
values: { ...values, voltage },
|
||||||
deviceRows,
|
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() {
|
async function handleRedo() {
|
||||||
if (historyBusy || isSaving || !historyState || historyState.redoDepth === 0) {
|
if (historyBusy || isSaving || !historyState || historyState.redoDepth === 0) {
|
||||||
return;
|
return;
|
||||||
@@ -2834,6 +2873,23 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
onSave={handleSaveCircuitGroup}
|
onSave={handleSaveCircuitGroup}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : 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">
|
<div className="editor-toolbar">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -4033,6 +4089,15 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
>
|
>
|
||||||
{row.circuit && row.rowType !== "deviceRow" ? (
|
{row.circuit && row.rowType !== "deviceRow" ? (
|
||||||
<>
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
tabIndex={-1}
|
||||||
|
onClick={() =>
|
||||||
|
setProtectionEditorCircuit(row.circuit!)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Schutzgerät
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
|
|||||||
@@ -58,6 +58,9 @@ import type {
|
|||||||
import type {
|
import type {
|
||||||
CircuitGroupSnapshot,
|
CircuitGroupSnapshot,
|
||||||
} from "../../domain/models/circuit-group-structure-project-command.model";
|
} from "../../domain/models/circuit-group-structure-project-command.model";
|
||||||
|
import type {
|
||||||
|
CircuitProtectionSnapshot,
|
||||||
|
} from "../../domain/models/circuit-protection-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, {
|
||||||
@@ -430,6 +433,25 @@ export function deleteCircuitGroupCommand(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function updateCircuitProtectionCommand(
|
||||||
|
projectId: string,
|
||||||
|
expectedRevision: number,
|
||||||
|
circuitId: string,
|
||||||
|
expected: CircuitProtectionSnapshot | null,
|
||||||
|
target: CircuitProtectionSnapshot
|
||||||
|
) {
|
||||||
|
return executeProjectCommand(
|
||||||
|
projectId,
|
||||||
|
expectedRevision,
|
||||||
|
{
|
||||||
|
schemaVersion: 1,
|
||||||
|
type: "circuit-protection.update",
|
||||||
|
payload: { circuitId, expected, target },
|
||||||
|
},
|
||||||
|
"Stromkreisschutz bearbeiten"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function updateCircuitById(
|
export function updateCircuitById(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
expectedRevision: number,
|
expectedRevision: number,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { CircuitTreeCircuitDto, CircuitTreeDeviceRowDto } from "../types.js";
|
import type { CircuitTreeCircuitDto, CircuitTreeDeviceRowDto } from "../types.js";
|
||||||
import type { CircuitUpdatePatch } from "../../domain/models/circuit-project-command.model.js";
|
import type { CircuitUpdatePatch } from "../../domain/models/circuit-project-command.model.js";
|
||||||
import type { CircuitDeviceRowUpdatePatch } from "../../domain/models/circuit-device-row-project-command.model.js";
|
import type { CircuitDeviceRowUpdatePatch } from "../../domain/models/circuit-device-row-project-command.model.js";
|
||||||
|
import { protectionDeviceTypeLabels } from "../../shared/constants/protection-device";
|
||||||
|
|
||||||
export type CellKey =
|
export type CellKey =
|
||||||
| "equipmentIdentifier"
|
| "equipmentIdentifier"
|
||||||
@@ -472,18 +473,54 @@ export function getDeviceValue(device: CircuitTreeDeviceRowDto, key: CellKey): G
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getCircuitValue(circuit: CircuitTreeCircuitDto, key: CellKey): GridValue {
|
export function getCircuitValue(circuit: CircuitTreeCircuitDto, key: CellKey): GridValue {
|
||||||
|
const protection = circuit.protectionDevice;
|
||||||
|
const protectionCharacteristic = protection
|
||||||
|
? [
|
||||||
|
protection.fuseUtilizationCategory,
|
||||||
|
protection.tripCharacteristic,
|
||||||
|
protection.rcdType ? `Typ ${protection.rcdType}` : undefined,
|
||||||
|
protection.ratedResidualCurrentMa !== undefined
|
||||||
|
? `${protection.ratedResidualCurrentMa} mA`
|
||||||
|
: undefined,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" · ")
|
||||||
|
: undefined;
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case "equipmentIdentifier": return circuit.equipmentIdentifier;
|
case "equipmentIdentifier": return circuit.equipmentIdentifier;
|
||||||
case "displayName": return circuit.displayName;
|
case "displayName": return circuit.displayName;
|
||||||
case "circuitTotalPower": return circuit.circuitTotalPower;
|
case "circuitTotalPower": return circuit.circuitTotalPower;
|
||||||
case "protectionType": return circuit.protectionType;
|
case "protectionType":
|
||||||
case "protectionRatedCurrent": return circuit.protectionRatedCurrent;
|
return protection
|
||||||
case "protectionCharacteristic": return circuit.protectionCharacteristic;
|
? protectionDeviceTypeLabels[protection.type]
|
||||||
|
: circuit.protectionType;
|
||||||
|
case "protectionRatedCurrent":
|
||||||
|
return protection?.ratedCurrentA ?? circuit.protectionRatedCurrent;
|
||||||
|
case "protectionCharacteristic":
|
||||||
|
return protectionCharacteristic || circuit.protectionCharacteristic;
|
||||||
case "protectionSummary": {
|
case "protectionSummary": {
|
||||||
const current = circuit.protectionRatedCurrent !== undefined && circuit.protectionRatedCurrent !== null
|
if (!protection) {
|
||||||
? `${circuit.protectionRatedCurrent}A`
|
const legacyCurrent =
|
||||||
|
circuit.protectionRatedCurrent !== undefined &&
|
||||||
|
circuit.protectionRatedCurrent !== null
|
||||||
|
? `${circuit.protectionRatedCurrent}A`
|
||||||
|
: "";
|
||||||
|
return [
|
||||||
|
circuit.protectionType,
|
||||||
|
legacyCurrent,
|
||||||
|
circuit.protectionCharacteristic,
|
||||||
|
].filter(Boolean).join(" ").trim() || undefined;
|
||||||
|
}
|
||||||
|
const currentValue =
|
||||||
|
protection.ratedCurrentA;
|
||||||
|
const current = currentValue !== undefined && currentValue !== null
|
||||||
|
? `${currentValue} A`
|
||||||
: "";
|
: "";
|
||||||
return [circuit.protectionType, current, circuit.protectionCharacteristic].filter(Boolean).join(" ").trim() || undefined;
|
return [
|
||||||
|
protectionDeviceTypeLabels[protection.type],
|
||||||
|
current,
|
||||||
|
protectionCharacteristic,
|
||||||
|
].filter(Boolean).join(" · ").trim() || undefined;
|
||||||
}
|
}
|
||||||
case "cableSummary": {
|
case "cableSummary": {
|
||||||
const length = circuit.cableLength !== undefined && circuit.cableLength !== null ? `${circuit.cableLength} m` : "";
|
const length = circuit.cableLength !== undefined && circuit.cableLength !== null ? `${circuit.cableLength} m` : "";
|
||||||
@@ -539,7 +576,14 @@ export function compareSortValues(left: GridValue, right: GridValue): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getCellKind(rowType: RowType, key: CellKey): CellKind {
|
export function getCellKind(rowType: RowType, key: CellKey): CellKind {
|
||||||
if (key === "rowTotalPower" || key === "circuitTotalPower") return "computed";
|
if (
|
||||||
|
key === "rowTotalPower" ||
|
||||||
|
key === "circuitTotalPower" ||
|
||||||
|
key === "protectionType" ||
|
||||||
|
key === "protectionRatedCurrent" ||
|
||||||
|
key === "protectionCharacteristic" ||
|
||||||
|
key === "protectionSummary"
|
||||||
|
) return "computed";
|
||||||
if (key === "voltage") return "readonly";
|
if (key === "voltage") return "readonly";
|
||||||
if (rowType === "section") return "readonly";
|
if (rowType === "section") return "readonly";
|
||||||
if (rowType === "placeholder") {
|
if (rowType === "placeholder") {
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import type {
|
||||||
|
CircuitProtectionSnapshot,
|
||||||
|
} from "../../domain/models/circuit-protection-project-command.model";
|
||||||
|
import { createDefaultCircuitProtection } from "../../domain/services/protection-device-defaults";
|
||||||
|
import type { CircuitGroupCategory } from "../../shared/constants/circuit-group";
|
||||||
|
import type {
|
||||||
|
CircuitTreeProtectionDeviceDto,
|
||||||
|
} from "../types";
|
||||||
|
|
||||||
|
export function toCircuitProtectionSnapshot(
|
||||||
|
circuitId: string,
|
||||||
|
protection: CircuitTreeProtectionDeviceDto
|
||||||
|
): CircuitProtectionSnapshot {
|
||||||
|
return {
|
||||||
|
circuitId,
|
||||||
|
type: protection.type,
|
||||||
|
ratedCurrentA: protection.ratedCurrentA,
|
||||||
|
fuseUtilizationCategory:
|
||||||
|
protection.fuseUtilizationCategory ?? null,
|
||||||
|
tripCharacteristic: protection.tripCharacteristic ?? null,
|
||||||
|
rcdType: protection.rcdType ?? null,
|
||||||
|
ratedResidualCurrentMa:
|
||||||
|
protection.ratedResidualCurrentMa ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCircuitProtectionEditorInitialValue(
|
||||||
|
category: CircuitGroupCategory | undefined,
|
||||||
|
protection: CircuitTreeProtectionDeviceDto | undefined
|
||||||
|
): CircuitTreeProtectionDeviceDto {
|
||||||
|
if (protection) {
|
||||||
|
return protection;
|
||||||
|
}
|
||||||
|
const fallback = createDefaultCircuitProtection(category ?? "lighting");
|
||||||
|
return { ...fallback };
|
||||||
|
}
|
||||||
@@ -8,6 +8,8 @@ import type {
|
|||||||
CreateCircuitDeviceRowInputDto,
|
CreateCircuitDeviceRowInputDto,
|
||||||
CreateCircuitInputDto,
|
CreateCircuitInputDto,
|
||||||
} from "../types.js";
|
} from "../types.js";
|
||||||
|
import type { CircuitGroupCategory } from "../../shared/constants/circuit-group";
|
||||||
|
import { createDefaultCircuitProtection } from "../../domain/services/protection-device-defaults";
|
||||||
|
|
||||||
export function buildCircuitDeviceRowInsertSnapshot(input: {
|
export function buildCircuitDeviceRowInsertSnapshot(input: {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -46,8 +48,12 @@ export function buildCircuitInsertSnapshot(input: {
|
|||||||
circuitListId: string;
|
circuitListId: string;
|
||||||
values: CreateCircuitInputDto;
|
values: CreateCircuitInputDto;
|
||||||
deviceRows: CircuitDeviceRowSnapshot[];
|
deviceRows: CircuitDeviceRowSnapshot[];
|
||||||
|
category?: CircuitGroupCategory;
|
||||||
}): CircuitSnapshot {
|
}): CircuitSnapshot {
|
||||||
const { id, circuitListId, values, deviceRows } = input;
|
const { id, circuitListId, values, deviceRows, category } = input;
|
||||||
|
const defaultProtection = category
|
||||||
|
? createDefaultCircuitProtection(category)
|
||||||
|
: null;
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
circuitListId,
|
circuitListId,
|
||||||
@@ -69,5 +75,30 @@ export function buildCircuitInsertSnapshot(input: {
|
|||||||
isReserve: deviceRows.length === 0,
|
isReserve: deviceRows.length === 0,
|
||||||
remark: values.remark ?? null,
|
remark: values.remark ?? null,
|
||||||
deviceRows,
|
deviceRows,
|
||||||
|
...(defaultProtection
|
||||||
|
? {
|
||||||
|
protectionDevice: {
|
||||||
|
circuitId: id,
|
||||||
|
type: defaultProtection.type,
|
||||||
|
ratedCurrentA: defaultProtection.ratedCurrentA,
|
||||||
|
fuseUtilizationCategory:
|
||||||
|
"fuseUtilizationCategory" in defaultProtection
|
||||||
|
? defaultProtection.fuseUtilizationCategory
|
||||||
|
: null,
|
||||||
|
tripCharacteristic:
|
||||||
|
"tripCharacteristic" in defaultProtection
|
||||||
|
? defaultProtection.tripCharacteristic
|
||||||
|
: null,
|
||||||
|
rcdType:
|
||||||
|
"rcdType" in defaultProtection
|
||||||
|
? defaultProtection.rcdType
|
||||||
|
: null,
|
||||||
|
ratedResidualCurrentMa:
|
||||||
|
"ratedResidualCurrentMa" in defaultProtection
|
||||||
|
? defaultProtection.ratedResidualCurrentMa
|
||||||
|
: null,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { CircuitDeviceRowMoveProjectCommandRepository } from "../src/db/reposito
|
|||||||
import { DistributionBoardFixtureRepository } from "./support/distribution-board-fixture.js";
|
import { DistributionBoardFixtureRepository } from "./support/distribution-board-fixture.js";
|
||||||
import { ProjectHistoryRepository } from "../src/db/repositories/project-history.repository.js";
|
import { ProjectHistoryRepository } from "../src/db/repositories/project-history.repository.js";
|
||||||
import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js";
|
import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js";
|
||||||
|
import { circuitProtectionDevices } from "../src/db/schema/circuit-protection-devices.js";
|
||||||
import { circuitSections } from "../src/db/schema/circuit-sections.js";
|
import { circuitSections } from "../src/db/schema/circuit-sections.js";
|
||||||
import { circuits } from "../src/db/schema/circuits.js";
|
import { circuits } from "../src/db/schema/circuits.js";
|
||||||
import { projectRevisions } from "../src/db/schema/project-revisions.js";
|
import { projectRevisions } from "../src/db/schema/project-revisions.js";
|
||||||
@@ -193,6 +194,15 @@ function createMoveTargetSnapshot(
|
|||||||
isReserve: true,
|
isReserve: true,
|
||||||
remark: null,
|
remark: null,
|
||||||
deviceRows: [],
|
deviceRows: [],
|
||||||
|
protectionDevice: {
|
||||||
|
circuitId: "circuit-new",
|
||||||
|
type: "LS",
|
||||||
|
ratedCurrentA: 10,
|
||||||
|
fuseUtilizationCategory: null,
|
||||||
|
tripCharacteristic: "B",
|
||||||
|
rcdType: null,
|
||||||
|
ratedResidualCurrentMa: null,
|
||||||
|
},
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -520,6 +530,14 @@ describe("circuit device-row move project-command repository", () => {
|
|||||||
"-1F4"
|
"-1F4"
|
||||||
);
|
);
|
||||||
assert.equal(isReserve(fixture.context, "circuit-new"), false);
|
assert.equal(isReserve(fixture.context, "circuit-new"), false);
|
||||||
|
assert.equal(
|
||||||
|
fixture.context.db
|
||||||
|
.select({ ratedCurrentA: circuitProtectionDevices.ratedCurrentA })
|
||||||
|
.from(circuitProtectionDevices)
|
||||||
|
.where(eq(circuitProtectionDevices.circuitId, "circuit-new"))
|
||||||
|
.get()?.ratedCurrentA,
|
||||||
|
10
|
||||||
|
);
|
||||||
assert.equal(isReserve(fixture.context, "circuit-1"), false);
|
assert.equal(isReserve(fixture.context, "circuit-1"), false);
|
||||||
assert.equal(isReserve(fixture.context, "circuit-2"), true);
|
assert.equal(isReserve(fixture.context, "circuit-2"), true);
|
||||||
|
|
||||||
@@ -538,6 +556,14 @@ describe("circuit device-row move project-command repository", () => {
|
|||||||
.get(),
|
.get(),
|
||||||
undefined
|
undefined
|
||||||
);
|
);
|
||||||
|
assert.equal(
|
||||||
|
fixture.context.db
|
||||||
|
.select()
|
||||||
|
.from(circuitProtectionDevices)
|
||||||
|
.where(eq(circuitProtectionDevices.circuitId, "circuit-new"))
|
||||||
|
.get(),
|
||||||
|
undefined
|
||||||
|
);
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
[
|
[
|
||||||
getRow(fixture.context, "row-1"),
|
getRow(fixture.context, "row-1"),
|
||||||
@@ -562,6 +588,14 @@ describe("circuit device-row move project-command repository", () => {
|
|||||||
getRow(fixture.context, "row-2")?.circuitId,
|
getRow(fixture.context, "row-2")?.circuitId,
|
||||||
"circuit-new"
|
"circuit-new"
|
||||||
);
|
);
|
||||||
|
assert.equal(
|
||||||
|
fixture.context.db
|
||||||
|
.select({ type: circuitProtectionDevices.type })
|
||||||
|
.from(circuitProtectionDevices)
|
||||||
|
.where(eq(circuitProtectionDevices.circuitId, "circuit-new"))
|
||||||
|
.get()?.type,
|
||||||
|
"LS"
|
||||||
|
);
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
new ProjectHistoryRepository(fixture.context.db).getState(
|
new ProjectHistoryRepository(fixture.context.db).getState(
|
||||||
"project-1"
|
"project-1"
|
||||||
|
|||||||
@@ -128,12 +128,29 @@ describe("circuit grid model", () => {
|
|||||||
assert.equal(getCellKind("circuitSummary", "controlRequirement"), "circuitField");
|
assert.equal(getCellKind("circuitSummary", "controlRequirement"), "circuitField");
|
||||||
assert.equal(getCellKind("deviceRow", "controlRequirement"), "readonly");
|
assert.equal(getCellKind("deviceRow", "controlRequirement"), "readonly");
|
||||||
assert.equal(getCellKind("circuitCompact", "rowTotalPower"), "computed");
|
assert.equal(getCellKind("circuitCompact", "rowTotalPower"), "computed");
|
||||||
|
assert.equal(getCellKind("circuitCompact", "protectionType"), "computed");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("projects circuit and device values without mixing ownership", () => {
|
it("projects circuit and device values without mixing ownership", () => {
|
||||||
assert.equal(getDeviceValue(device, "roomSummary"), "1.01 Office");
|
assert.equal(getDeviceValue(device, "roomSummary"), "1.01 Office");
|
||||||
assert.equal(getDeviceValue(device, "rowTotalPower"), 0.08);
|
assert.equal(getDeviceValue(device, "rowTotalPower"), 0.08);
|
||||||
assert.equal(getCircuitValue(circuit, "protectionSummary"), "MCB 16A B");
|
assert.equal(getCircuitValue(circuit, "protectionSummary"), "MCB 16A B");
|
||||||
|
assert.equal(
|
||||||
|
getCircuitValue(
|
||||||
|
{
|
||||||
|
...circuit,
|
||||||
|
protectionDevice: {
|
||||||
|
type: "FI_LS",
|
||||||
|
ratedCurrentA: 16,
|
||||||
|
tripCharacteristic: "B",
|
||||||
|
rcdType: "A",
|
||||||
|
ratedResidualCurrentMa: 30,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"protectionSummary"
|
||||||
|
),
|
||||||
|
"FI/LS · 16 A · B · Typ A · 30 mA"
|
||||||
|
);
|
||||||
assert.equal(getCircuitValue(circuit, "voltage"), 230);
|
assert.equal(getCircuitValue(circuit, "voltage"), 230);
|
||||||
assert.equal(getCircuitValue(circuit, "controlRequirement"), "DALI");
|
assert.equal(getCircuitValue(circuit, "controlRequirement"), "DALI");
|
||||||
assert.equal(getCircuitValue(circuit, "cableSummary"), "NYM-J, 1.5 mm², 20 m");
|
assert.equal(getCircuitValue(circuit, "cableSummary"), "NYM-J, 1.5 mm², 20 m");
|
||||||
|
|||||||
@@ -65,12 +65,22 @@ describe("circuit structure command adapters", () => {
|
|||||||
controlRequirement: "DALI",
|
controlRequirement: "DALI",
|
||||||
},
|
},
|
||||||
deviceRows: [],
|
deviceRows: [],
|
||||||
|
category: "single_phase",
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.equal(reserve.isReserve, true);
|
assert.equal(reserve.isReserve, true);
|
||||||
assert.equal(reserve.displayName, null);
|
assert.equal(reserve.displayName, null);
|
||||||
assert.equal(reserve.voltage, 230);
|
assert.equal(reserve.voltage, 230);
|
||||||
assert.equal(reserve.controlRequirement, "DALI");
|
assert.equal(reserve.controlRequirement, "DALI");
|
||||||
|
assert.deepEqual(reserve.protectionDevice, {
|
||||||
|
circuitId: "circuit-1",
|
||||||
|
type: "FI_LS",
|
||||||
|
ratedCurrentA: 16,
|
||||||
|
fuseUtilizationCategory: null,
|
||||||
|
tripCharacteristic: "B",
|
||||||
|
rcdType: "A",
|
||||||
|
ratedResidualCurrentMa: 30,
|
||||||
|
});
|
||||||
|
|
||||||
const row = buildCircuitDeviceRowInsertSnapshot({
|
const row = buildCircuitDeviceRowInsertSnapshot({
|
||||||
id: "row-1",
|
id: "row-1",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { CircuitStructureProjectCommandRepository } from "../src/db/repositories
|
|||||||
import { DistributionBoardFixtureRepository } from "./support/distribution-board-fixture.js";
|
import { DistributionBoardFixtureRepository } from "./support/distribution-board-fixture.js";
|
||||||
import { ProjectHistoryRepository } from "../src/db/repositories/project-history.repository.js";
|
import { ProjectHistoryRepository } from "../src/db/repositories/project-history.repository.js";
|
||||||
import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js";
|
import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js";
|
||||||
|
import { circuitProtectionDevices } from "../src/db/schema/circuit-protection-devices.js";
|
||||||
import { circuitSections } from "../src/db/schema/circuit-sections.js";
|
import { circuitSections } from "../src/db/schema/circuit-sections.js";
|
||||||
import { circuits } from "../src/db/schema/circuits.js";
|
import { circuits } from "../src/db/schema/circuits.js";
|
||||||
import { projectDevices } from "../src/db/schema/project-devices.js";
|
import { projectDevices } from "../src/db/schema/project-devices.js";
|
||||||
@@ -165,6 +166,18 @@ function createTestDatabase(): TestFixture {
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
.run();
|
.run();
|
||||||
|
context.db
|
||||||
|
.insert(circuitProtectionDevices)
|
||||||
|
.values({
|
||||||
|
circuitId: "circuit-existing",
|
||||||
|
type: "LS",
|
||||||
|
ratedCurrentA: 10,
|
||||||
|
fuseUtilizationCategory: null,
|
||||||
|
tripCharacteristic: "B",
|
||||||
|
rcdType: null,
|
||||||
|
ratedResidualCurrentMa: null,
|
||||||
|
})
|
||||||
|
.run();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
context,
|
context,
|
||||||
@@ -377,6 +390,11 @@ describe("circuit structure project-command repository", () => {
|
|||||||
"circuit-existing"
|
"circuit-existing"
|
||||||
);
|
);
|
||||||
const beforeRows = getRows(fixture.context, "circuit-existing");
|
const beforeRows = getRows(fixture.context, "circuit-existing");
|
||||||
|
const beforeProtection = fixture.context.db
|
||||||
|
.select()
|
||||||
|
.from(circuitProtectionDevices)
|
||||||
|
.where(eq(circuitProtectionDevices.circuitId, "circuit-existing"))
|
||||||
|
.get();
|
||||||
assert.ok(beforeCircuit);
|
assert.ok(beforeCircuit);
|
||||||
const store = new CircuitStructureProjectCommandRepository(
|
const store = new CircuitStructureProjectCommandRepository(
|
||||||
fixture.context.db
|
fixture.context.db
|
||||||
@@ -395,6 +413,14 @@ describe("circuit structure project-command repository", () => {
|
|||||||
undefined
|
undefined
|
||||||
);
|
);
|
||||||
assert.equal(getRows(fixture.context, "circuit-existing").length, 0);
|
assert.equal(getRows(fixture.context, "circuit-existing").length, 0);
|
||||||
|
assert.equal(
|
||||||
|
fixture.context.db
|
||||||
|
.select()
|
||||||
|
.from(circuitProtectionDevices)
|
||||||
|
.where(eq(circuitProtectionDevices.circuitId, "circuit-existing"))
|
||||||
|
.get(),
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
store.execute({
|
store.execute({
|
||||||
projectId: "project-1",
|
projectId: "project-1",
|
||||||
@@ -411,6 +437,14 @@ describe("circuit structure project-command repository", () => {
|
|||||||
getRows(fixture.context, "circuit-existing"),
|
getRows(fixture.context, "circuit-existing"),
|
||||||
beforeRows
|
beforeRows
|
||||||
);
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
fixture.context.db
|
||||||
|
.select()
|
||||||
|
.from(circuitProtectionDevices)
|
||||||
|
.where(eq(circuitProtectionDevices.circuitId, "circuit-existing"))
|
||||||
|
.get(),
|
||||||
|
beforeProtection
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
fixture.context.close();
|
fixture.context.close();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ import {
|
|||||||
} from "../src/domain/services/protection-device-defaults.js";
|
} from "../src/domain/services/protection-device-defaults.js";
|
||||||
import { protectionDeviceConfigurationSchema } from "../src/shared/validation/protection-device.schemas.js";
|
import { protectionDeviceConfigurationSchema } from "../src/shared/validation/protection-device.schemas.js";
|
||||||
import "./circuit-protection-project-command.repository.test.js";
|
import "./circuit-protection-project-command.repository.test.js";
|
||||||
|
import {
|
||||||
|
getCircuitProtectionEditorInitialValue,
|
||||||
|
toCircuitProtectionSnapshot,
|
||||||
|
} from "../src/frontend/utils/circuit-protection-editing.js";
|
||||||
|
|
||||||
describe("protection device catalog", () => {
|
describe("protection device catalog", () => {
|
||||||
it("contains exactly the agreed protection device types", () => {
|
it("contains exactly the agreed protection device types", () => {
|
||||||
@@ -175,6 +179,32 @@ describe("protection device validation", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("protection device defaults", () => {
|
describe("protection device defaults", () => {
|
||||||
|
it("maps editor values to complete circuit-owned snapshots", () => {
|
||||||
|
const initial = getCircuitProtectionEditorInitialValue(
|
||||||
|
"single_phase",
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
assert.deepEqual(initial, {
|
||||||
|
type: "FI_LS",
|
||||||
|
ratedCurrentA: 16,
|
||||||
|
tripCharacteristic: "B",
|
||||||
|
rcdType: "A",
|
||||||
|
ratedResidualCurrentMa: 30,
|
||||||
|
});
|
||||||
|
assert.deepEqual(
|
||||||
|
toCircuitProtectionSnapshot("circuit-1", initial),
|
||||||
|
{
|
||||||
|
circuitId: "circuit-1",
|
||||||
|
type: "FI_LS",
|
||||||
|
ratedCurrentA: 16,
|
||||||
|
fuseUtilizationCategory: null,
|
||||||
|
tripCharacteristic: "B",
|
||||||
|
rcdType: "A",
|
||||||
|
ratedResidualCurrentMa: 30,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("creates the agreed circuit defaults", () => {
|
it("creates the agreed circuit defaults", () => {
|
||||||
assert.deepEqual(createDefaultCircuitProtection("lighting"), {
|
assert.deepEqual(createDefaultCircuitProtection("lighting"), {
|
||||||
type: "LS",
|
type: "LS",
|
||||||
|
|||||||
Reference in New Issue
Block a user