forked from jappel/leistungsbilanz-ts
Add explicit "no protection device" state for circuits
Circuits can now record that they deliberately have no protection device (type NONE, shown as "–"), separate from circuits whose protection is still open. Migration 0017 makes rated_current_a nullable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
1f3b903809
commit
38019fe087
27 changed files with 3673 additions and 132 deletions
|
|
@ -70,6 +70,9 @@ See `docs/current-architecture.md` for the complete module and request flow.
|
||||||
- Equipment identifiers belong to circuits, not to every device row.
|
- Equipment identifiers belong to circuits, not to every device row.
|
||||||
- Device rows inside a circuit do not have their own equipment identifiers.
|
- Device rows inside a circuit do not have their own equipment identifiers.
|
||||||
- Protection and cable data belong to the circuit, not to individual devices.
|
- Protection and cable data belong to the circuit, not to individual devices.
|
||||||
|
- A circuit protection record of type `NONE` means "deliberately no protection
|
||||||
|
device" (shown as "–"); a circuit without any protection record is still
|
||||||
|
open. `NONE` is circuit-only, never a component protection.
|
||||||
- Device-level values include quantity, power per unit, simultaneity factor, cosPhi, room data, cost group and category.
|
- Device-level values include quantity, power per unit, simultaneity factor, cosPhi, room data, cost group and category.
|
||||||
- Circuit total power is the sum of all device rows in that circuit plus the
|
- Circuit total power is the sum of all device rows in that circuit plus the
|
||||||
read-only loads of logically linked downstream boards on that feeder circuit.
|
read-only loads of logically linked downstream boards on that feeder circuit.
|
||||||
|
|
|
||||||
|
|
@ -460,6 +460,13 @@ 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 Typ `NONE` („Kein Schutzgerät“) hält die bewusste Entscheidung fest, dass
|
||||||
|
ein Stromkreis kein eigenes Schutzgerät erhält; Bemessungsstrom und alle
|
||||||
|
abhängigen Felder sind dann `null` (Migration `0017` macht
|
||||||
|
`rated_current_a` nullable). Editor und Seitenansicht zeigen „–“. Ein
|
||||||
|
Stromkreis ganz ohne Datensatz gilt dagegen weiter als offen (Seitenansicht
|
||||||
|
„offen“). `NONE` gibt es nur für Stromkreise, nicht für
|
||||||
|
Verteilerkomponenten; die Leitungsberechnung übernimmt dafür kein Schutzgerät.
|
||||||
Der vollständige `CircuitSnapshot` enthält den zugehörigen
|
Der vollständige `CircuitSnapshot` enthält den zugehörigen
|
||||||
`protectionDevice`-Datensatz, sofern einer vorhanden ist. Neue Stromkreise und
|
`protectionDevice`-Datensatz, sofern einer vorhanden ist. Neue Stromkreise und
|
||||||
durch Geräteverschiebung erzeugte Zielstromkreise verwenden die vereinbarten
|
durch Geräteverschiebung erzeugte Zielstromkreise verwenden die vereinbarten
|
||||||
|
|
|
||||||
16
src/db/migrations/0017_circuit_protection_none.sql
Normal file
16
src/db/migrations/0017_circuit_protection_none.sql
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
PRAGMA foreign_keys=OFF;--> statement-breakpoint
|
||||||
|
CREATE TABLE `__new_circuit_protection_devices` (
|
||||||
|
`circuit_id` text PRIMARY KEY NOT NULL,
|
||||||
|
`type` text NOT NULL,
|
||||||
|
`rated_current_a` real,
|
||||||
|
`fuse_utilization_category` text,
|
||||||
|
`trip_characteristic` text,
|
||||||
|
`rcd_type` text,
|
||||||
|
`rated_residual_current_ma` real,
|
||||||
|
FOREIGN KEY (`circuit_id`) REFERENCES `circuits`(`id`) ON UPDATE no action ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
INSERT INTO `__new_circuit_protection_devices`("circuit_id", "type", "rated_current_a", "fuse_utilization_category", "trip_characteristic", "rcd_type", "rated_residual_current_ma") SELECT "circuit_id", "type", "rated_current_a", "fuse_utilization_category", "trip_characteristic", "rcd_type", "rated_residual_current_ma" FROM `circuit_protection_devices`;--> statement-breakpoint
|
||||||
|
DROP TABLE `circuit_protection_devices`;--> statement-breakpoint
|
||||||
|
ALTER TABLE `__new_circuit_protection_devices` RENAME TO `circuit_protection_devices`;--> statement-breakpoint
|
||||||
|
PRAGMA foreign_keys=ON;
|
||||||
3210
src/db/migrations/meta/0017_snapshot.json
Normal file
3210
src/db/migrations/meta/0017_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -120,6 +120,13 @@
|
||||||
"when": 1793277078601,
|
"when": 1793277078601,
|
||||||
"tag": "0016_single_device_circuit_names",
|
"tag": "0016_single_device_circuit_names",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 17,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1793359753805,
|
||||||
|
"tag": "0017_circuit_protection_none",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { real, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
import { real, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||||
import type {
|
import type {
|
||||||
BreakerTripCharacteristic,
|
BreakerTripCharacteristic,
|
||||||
|
CircuitProtectionDeviceType,
|
||||||
FuseUtilizationCategory,
|
FuseUtilizationCategory,
|
||||||
ProtectionDeviceType,
|
|
||||||
RcdType,
|
RcdType,
|
||||||
} from "../../shared/constants/protection-device.js";
|
} from "../../shared/constants/protection-device.js";
|
||||||
import { circuits } from "./circuits.js";
|
import { circuits } from "./circuits.js";
|
||||||
|
|
@ -13,8 +13,9 @@ export const circuitProtectionDevices = sqliteTable(
|
||||||
circuitId: text("circuit_id")
|
circuitId: text("circuit_id")
|
||||||
.primaryKey()
|
.primaryKey()
|
||||||
.references(() => circuits.id, { onDelete: "cascade" }),
|
.references(() => circuits.id, { onDelete: "cascade" }),
|
||||||
type: text("type").$type<ProtectionDeviceType>().notNull(),
|
// "NONE" stores an explicit "no protection device" with null values.
|
||||||
ratedCurrentA: real("rated_current_a").notNull(),
|
type: text("type").$type<CircuitProtectionDeviceType>().notNull(),
|
||||||
|
ratedCurrentA: real("rated_current_a"),
|
||||||
fuseUtilizationCategory: text(
|
fuseUtilizationCategory: text(
|
||||||
"fuse_utilization_category"
|
"fuse_utilization_category"
|
||||||
).$type<FuseUtilizationCategory>(),
|
).$type<FuseUtilizationCategory>(),
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import type {
|
import type {
|
||||||
BreakerTripCharacteristic,
|
BreakerTripCharacteristic,
|
||||||
|
CircuitProtectionDeviceType,
|
||||||
FuseUtilizationCategory,
|
FuseUtilizationCategory,
|
||||||
ProtectionDeviceType,
|
|
||||||
RcdType,
|
RcdType,
|
||||||
} from "../../shared/constants/protection-device.js";
|
} from "../../shared/constants/protection-device.js";
|
||||||
import { protectionDeviceConfigurationSchema } from "../../shared/validation/protection-device.schemas.js";
|
import { isValidCircuitProtectionRecord } from "../../shared/validation/protection-device.schemas.js";
|
||||||
import {
|
import {
|
||||||
assertCircuitGroupSnapshot,
|
assertCircuitGroupSnapshot,
|
||||||
type CircuitGroupSnapshot,
|
type CircuitGroupSnapshot,
|
||||||
|
|
@ -22,8 +22,8 @@ import {
|
||||||
|
|
||||||
export interface CircuitProtectionDeviceSnapshot {
|
export interface CircuitProtectionDeviceSnapshot {
|
||||||
circuitId: string;
|
circuitId: string;
|
||||||
type: ProtectionDeviceType;
|
type: CircuitProtectionDeviceType;
|
||||||
ratedCurrentA: number;
|
ratedCurrentA: number | null;
|
||||||
fuseUtilizationCategory: FuseUtilizationCategory | null;
|
fuseUtilizationCategory: FuseUtilizationCategory | null;
|
||||||
tripCharacteristic: BreakerTripCharacteristic | null;
|
tripCharacteristic: BreakerTripCharacteristic | null;
|
||||||
rcdType: RcdType | null;
|
rcdType: RcdType | null;
|
||||||
|
|
@ -143,21 +143,7 @@ function assertCircuitProtectionDeviceSnapshot(
|
||||||
) {
|
) {
|
||||||
throw new Error("Circuit protection-device snapshot is invalid.");
|
throw new Error("Circuit protection-device snapshot is invalid.");
|
||||||
}
|
}
|
||||||
const result = protectionDeviceConfigurationSchema.safeParse({
|
if (!isValidCircuitProtectionRecord(value)) {
|
||||||
type: value.type,
|
|
||||||
ratedCurrentA: value.ratedCurrentA,
|
|
||||||
...(value.fuseUtilizationCategory === null
|
|
||||||
? {}
|
|
||||||
: { fuseUtilizationCategory: value.fuseUtilizationCategory }),
|
|
||||||
...(value.tripCharacteristic === null
|
|
||||||
? {}
|
|
||||||
: { tripCharacteristic: value.tripCharacteristic }),
|
|
||||||
...(value.rcdType === null ? {} : { rcdType: value.rcdType }),
|
|
||||||
...(value.ratedResidualCurrentMa === null
|
|
||||||
? {}
|
|
||||||
: { ratedResidualCurrentMa: value.ratedResidualCurrentMa }),
|
|
||||||
});
|
|
||||||
if (!result.success) {
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Circuit protection-device configuration is invalid."
|
"Circuit protection-device configuration is invalid."
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { protectionDeviceConfigurationSchema } from "../../shared/validation/protection-device.schemas.js";
|
import { isValidCircuitProtectionRecord } from "../../shared/validation/protection-device.schemas.js";
|
||||||
import type {
|
import type {
|
||||||
BreakerTripCharacteristic,
|
BreakerTripCharacteristic,
|
||||||
|
CircuitProtectionDeviceType,
|
||||||
FuseUtilizationCategory,
|
FuseUtilizationCategory,
|
||||||
ProtectionDeviceType,
|
|
||||||
RcdType,
|
RcdType,
|
||||||
} from "../../shared/constants/protection-device.js";
|
} from "../../shared/constants/protection-device.js";
|
||||||
import type { SerializedProjectCommand } from "./project-command.model.js";
|
import type { SerializedProjectCommand } from "./project-command.model.js";
|
||||||
|
|
@ -11,10 +11,12 @@ export const circuitProtectionUpdateCommandType =
|
||||||
"circuit-protection.update" as const;
|
"circuit-protection.update" as const;
|
||||||
export const circuitProtectionUpdateCommandSchemaVersion = 1 as const;
|
export const circuitProtectionUpdateCommandSchemaVersion = 1 as const;
|
||||||
|
|
||||||
|
// type "NONE" records an explicit "no protection device" decision; all
|
||||||
|
// values are then null. A missing snapshot (null) means "not yet planned".
|
||||||
export interface CircuitProtectionSnapshot {
|
export interface CircuitProtectionSnapshot {
|
||||||
circuitId: string;
|
circuitId: string;
|
||||||
type: ProtectionDeviceType;
|
type: CircuitProtectionDeviceType;
|
||||||
ratedCurrentA: number;
|
ratedCurrentA: number | null;
|
||||||
fuseUtilizationCategory: FuseUtilizationCategory | null;
|
fuseUtilizationCategory: FuseUtilizationCategory | null;
|
||||||
tripCharacteristic: BreakerTripCharacteristic | null;
|
tripCharacteristic: BreakerTripCharacteristic | null;
|
||||||
rcdType: RcdType | null;
|
rcdType: RcdType | null;
|
||||||
|
|
@ -105,21 +107,7 @@ export function assertCircuitProtectionSnapshot(
|
||||||
) {
|
) {
|
||||||
throw new Error("Circuit-protection snapshot is invalid.");
|
throw new Error("Circuit-protection snapshot is invalid.");
|
||||||
}
|
}
|
||||||
const result = protectionDeviceConfigurationSchema.safeParse({
|
if (!isValidCircuitProtectionRecord(value)) {
|
||||||
type: value.type,
|
|
||||||
ratedCurrentA: value.ratedCurrentA,
|
|
||||||
...(value.fuseUtilizationCategory === null
|
|
||||||
? {}
|
|
||||||
: { fuseUtilizationCategory: value.fuseUtilizationCategory }),
|
|
||||||
...(value.tripCharacteristic === null
|
|
||||||
? {}
|
|
||||||
: { tripCharacteristic: value.tripCharacteristic }),
|
|
||||||
...(value.rcdType === null ? {} : { rcdType: value.rcdType }),
|
|
||||||
...(value.ratedResidualCurrentMa === null
|
|
||||||
? {}
|
|
||||||
: { ratedResidualCurrentMa: value.ratedResidualCurrentMa }),
|
|
||||||
});
|
|
||||||
if (!result.success) {
|
|
||||||
throw new Error("Circuit-protection configuration is invalid.");
|
throw new Error("Circuit-protection configuration is invalid.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ export interface CircuitTreeCircuit {
|
||||||
circuitCosPhi: number;
|
circuitCosPhi: number;
|
||||||
deviceRows: CircuitTreeDeviceRow[];
|
deviceRows: CircuitTreeDeviceRow[];
|
||||||
downstreamBoardLoads: CircuitTreeDownstreamBoardLoad[];
|
downstreamBoardLoads: CircuitTreeDownstreamBoardLoad[];
|
||||||
protectionDevice?: CircuitTreeProtectionDevice;
|
protectionDevice?: CircuitTreeCircuitProtectionDevice;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CircuitTreeSectionBlock {
|
export interface CircuitTreeSectionBlock {
|
||||||
|
|
@ -116,6 +116,7 @@ import type {
|
||||||
import type {
|
import type {
|
||||||
BreakerTripCharacteristic,
|
BreakerTripCharacteristic,
|
||||||
FuseUtilizationCategory,
|
FuseUtilizationCategory,
|
||||||
|
NoCircuitProtectionDeviceType,
|
||||||
ProtectionDeviceType,
|
ProtectionDeviceType,
|
||||||
RcdType,
|
RcdType,
|
||||||
} from "../../shared/constants/protection-device.js";
|
} from "../../shared/constants/protection-device.js";
|
||||||
|
|
@ -129,6 +130,15 @@ export interface CircuitTreeProtectionDevice {
|
||||||
ratedResidualCurrentMa?: number;
|
ratedResidualCurrentMa?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Explicit "no protection device" on a circuit; shown as "–".
|
||||||
|
export interface CircuitTreeNoProtectionDevice {
|
||||||
|
type: NoCircuitProtectionDeviceType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CircuitTreeCircuitProtectionDevice =
|
||||||
|
| CircuitTreeProtectionDevice
|
||||||
|
| CircuitTreeNoProtectionDevice;
|
||||||
|
|
||||||
export interface CircuitTreeComponent {
|
export interface CircuitTreeComponent {
|
||||||
id: string;
|
id: string;
|
||||||
circuitListId: string;
|
circuitListId: string;
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,13 @@ import {
|
||||||
distributionBoardComponentPlacements,
|
distributionBoardComponentPlacements,
|
||||||
distributionBoardComponentRoles,
|
distributionBoardComponentRoles,
|
||||||
} from "../../shared/constants/distribution-board-component.js";
|
} from "../../shared/constants/distribution-board-component.js";
|
||||||
import { protectionDeviceConfigurationSchema } from "../../shared/validation/protection-device.schemas.js";
|
import {
|
||||||
|
isValidCircuitProtectionRecord,
|
||||||
|
isValidProtectionRecord,
|
||||||
|
} from "../../shared/validation/protection-device.schemas.js";
|
||||||
import {
|
import {
|
||||||
breakerTripCharacteristics,
|
breakerTripCharacteristics,
|
||||||
|
circuitProtectionDeviceTypes,
|
||||||
fuseUtilizationCategories,
|
fuseUtilizationCategories,
|
||||||
protectionDeviceTypes,
|
protectionDeviceTypes,
|
||||||
rcdTypes,
|
rcdTypes,
|
||||||
|
|
@ -311,45 +315,15 @@ const distributionBoardComponentSchema = z
|
||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
const persistedProtectionDeviceFields = {
|
const persistedProtectionDeviceOptionFields = {
|
||||||
type: z.enum(protectionDeviceTypes),
|
|
||||||
ratedCurrentA: finiteNumberSchema.positive(),
|
|
||||||
fuseUtilizationCategory: z.enum(fuseUtilizationCategories).nullable(),
|
fuseUtilizationCategory: z.enum(fuseUtilizationCategories).nullable(),
|
||||||
tripCharacteristic: z.enum(breakerTripCharacteristics).nullable(),
|
tripCharacteristic: z.enum(breakerTripCharacteristics).nullable(),
|
||||||
rcdType: z.enum(rcdTypes).nullable(),
|
rcdType: z.enum(rcdTypes).nullable(),
|
||||||
ratedResidualCurrentMa: finiteNumberSchema.positive().nullable(),
|
ratedResidualCurrentMa: finiteNumberSchema.positive().nullable(),
|
||||||
};
|
};
|
||||||
|
|
||||||
function validatePersistedProtectionDevice(
|
function addProtectionIssue(valid: boolean, context: z.RefinementCtx) {
|
||||||
value: {
|
if (!valid) {
|
||||||
type: (typeof protectionDeviceTypes)[number];
|
|
||||||
ratedCurrentA: number;
|
|
||||||
fuseUtilizationCategory:
|
|
||||||
| (typeof fuseUtilizationCategories)[number]
|
|
||||||
| null;
|
|
||||||
tripCharacteristic:
|
|
||||||
| (typeof breakerTripCharacteristics)[number]
|
|
||||||
| null;
|
|
||||||
rcdType: (typeof rcdTypes)[number] | null;
|
|
||||||
ratedResidualCurrentMa: number | null;
|
|
||||||
},
|
|
||||||
context: z.RefinementCtx
|
|
||||||
) {
|
|
||||||
const result = protectionDeviceConfigurationSchema.safeParse({
|
|
||||||
type: value.type,
|
|
||||||
ratedCurrentA: value.ratedCurrentA,
|
|
||||||
...(value.fuseUtilizationCategory === null
|
|
||||||
? {}
|
|
||||||
: { fuseUtilizationCategory: value.fuseUtilizationCategory }),
|
|
||||||
...(value.tripCharacteristic === null
|
|
||||||
? {}
|
|
||||||
: { tripCharacteristic: value.tripCharacteristic }),
|
|
||||||
...(value.rcdType === null ? {} : { rcdType: value.rcdType }),
|
|
||||||
...(value.ratedResidualCurrentMa === null
|
|
||||||
? {}
|
|
||||||
: { ratedResidualCurrentMa: value.ratedResidualCurrentMa }),
|
|
||||||
});
|
|
||||||
if (!result.success) {
|
|
||||||
context.addIssue({
|
context.addIssue({
|
||||||
code: "custom",
|
code: "custom",
|
||||||
message: "Snapshot protection-device configuration is invalid.",
|
message: "Snapshot protection-device configuration is invalid.",
|
||||||
|
|
@ -357,21 +331,30 @@ function validatePersistedProtectionDevice(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Circuits may additionally store the explicit "no protection device" state.
|
||||||
const circuitProtectionDeviceSchema = z
|
const circuitProtectionDeviceSchema = z
|
||||||
.object({
|
.object({
|
||||||
circuitId: idSchema,
|
circuitId: idSchema,
|
||||||
...persistedProtectionDeviceFields,
|
type: z.enum(circuitProtectionDeviceTypes),
|
||||||
|
ratedCurrentA: finiteNumberSchema.positive().nullable(),
|
||||||
|
...persistedProtectionDeviceOptionFields,
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
.superRefine(validatePersistedProtectionDevice);
|
.superRefine((value, context) =>
|
||||||
|
addProtectionIssue(isValidCircuitProtectionRecord(value), context)
|
||||||
|
);
|
||||||
|
|
||||||
const componentProtectionDeviceSchema = z
|
const componentProtectionDeviceSchema = z
|
||||||
.object({
|
.object({
|
||||||
componentId: idSchema,
|
componentId: idSchema,
|
||||||
...persistedProtectionDeviceFields,
|
type: z.enum(protectionDeviceTypes),
|
||||||
|
ratedCurrentA: finiteNumberSchema.positive(),
|
||||||
|
...persistedProtectionDeviceOptionFields,
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
.superRefine(validatePersistedProtectionDevice);
|
.superRefine((value, context) =>
|
||||||
|
addProtectionIssue(isValidProtectionRecord(value), context)
|
||||||
|
);
|
||||||
|
|
||||||
const externalCsvConfigurationSnapshotSchema = z
|
const externalCsvConfigurationSnapshotSchema = z
|
||||||
.object({
|
.object({
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,11 @@ import type {
|
||||||
CircuitCategory,
|
CircuitCategory,
|
||||||
ExistingProtection,
|
ExistingProtection,
|
||||||
} from "../../cable-sizing/domain/cable-sizing-calculation";
|
} from "../../cable-sizing/domain/cable-sizing-calculation";
|
||||||
import type { CircuitTreeCircuitDto, CircuitTreeProtectionDeviceDto } from "../types";
|
import { noCircuitProtectionDeviceType } from "../../shared/constants/protection-device";
|
||||||
|
import type {
|
||||||
|
CircuitTreeCircuitDto,
|
||||||
|
CircuitTreeCircuitProtectionDeviceDto,
|
||||||
|
} from "../types";
|
||||||
|
|
||||||
/** Three-phase if the circuit group or any device row is three-phase. */
|
/** Three-phase if the circuit group or any device row is three-phase. */
|
||||||
export function deriveCircuitPhase(
|
export function deriveCircuitPhase(
|
||||||
|
|
@ -41,12 +45,17 @@ export function deriveCircuitCosPhi(circuit: CircuitTreeCircuitDto): number {
|
||||||
* Maps the circuit's protective device to the overload-protection kind of the
|
* Maps the circuit's protective device to the overload-protection kind of the
|
||||||
* calculation. Circuit-breakers (LS, FI/LS, AFDD) are "breaker", D0 and NH
|
* calculation. Circuit-breakers (LS, FI/LS, AFDD) are "breaker", D0 and NH
|
||||||
* fuse links "fuse"; a residual-current device without overcurrent release
|
* fuse links "fuse"; a residual-current device without overcurrent release
|
||||||
* (FI) provides no overload protection and is ignored.
|
* (FI) provides no overload protection and is ignored, like an explicit
|
||||||
|
* "no protection device".
|
||||||
*/
|
*/
|
||||||
export function toExistingProtection(
|
export function toExistingProtection(
|
||||||
protection: CircuitTreeProtectionDeviceDto | undefined
|
protection: CircuitTreeCircuitProtectionDeviceDto | undefined
|
||||||
): ExistingProtection | undefined {
|
): ExistingProtection | undefined {
|
||||||
if (!protection || protection.type === "FI") {
|
if (
|
||||||
|
!protection ||
|
||||||
|
protection.type === "FI" ||
|
||||||
|
protection.type === noCircuitProtectionDeviceType
|
||||||
|
) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const kind =
|
const kind =
|
||||||
|
|
|
||||||
|
|
@ -4,23 +4,24 @@ import { type FormEvent, useState } from "react";
|
||||||
import {
|
import {
|
||||||
allowedRatedCurrentsAByProtectionDeviceType,
|
allowedRatedCurrentsAByProtectionDeviceType,
|
||||||
breakerTripCharacteristics,
|
breakerTripCharacteristics,
|
||||||
|
circuitProtectionDeviceTypeLabels,
|
||||||
fuseProtectionDeviceTypes,
|
fuseProtectionDeviceTypes,
|
||||||
fuseUtilizationCategories,
|
fuseUtilizationCategories,
|
||||||
protectionDeviceTypeLabels,
|
noCircuitProtectionDeviceType,
|
||||||
protectionDeviceTypes,
|
protectionDeviceTypes,
|
||||||
ratedResidualCurrentsMa,
|
ratedResidualCurrentsMa,
|
||||||
rcdTypes,
|
rcdTypes,
|
||||||
type ProtectionDeviceType,
|
type CircuitProtectionDeviceType,
|
||||||
} from "../../shared/constants/protection-device";
|
} from "../../shared/constants/protection-device";
|
||||||
import type { CircuitTreeProtectionDeviceDto } from "../types";
|
import type { CircuitTreeCircuitProtectionDeviceDto } from "../types";
|
||||||
import { FormModal } from "./form-modal";
|
import { FormModal } from "./form-modal";
|
||||||
|
|
||||||
interface CircuitProtectionModalProps {
|
interface CircuitProtectionModalProps {
|
||||||
equipmentIdentifier: string;
|
equipmentIdentifier: string;
|
||||||
initialProtection: CircuitTreeProtectionDeviceDto;
|
initialProtection: CircuitTreeCircuitProtectionDeviceDto;
|
||||||
isSaving: boolean;
|
isSaving: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSave: (protection: CircuitTreeProtectionDeviceDto) => Promise<void>;
|
onSave: (protection: CircuitTreeCircuitProtectionDeviceDto) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CircuitProtectionModal({
|
export function CircuitProtectionModal({
|
||||||
|
|
@ -30,35 +31,45 @@ export function CircuitProtectionModal({
|
||||||
onClose,
|
onClose,
|
||||||
onSave,
|
onSave,
|
||||||
}: CircuitProtectionModalProps) {
|
}: CircuitProtectionModalProps) {
|
||||||
const [type, setType] = useState(initialProtection.type);
|
const initialDevice =
|
||||||
|
initialProtection.type === noCircuitProtectionDeviceType
|
||||||
|
? undefined
|
||||||
|
: initialProtection;
|
||||||
|
const [type, setType] = useState<CircuitProtectionDeviceType>(
|
||||||
|
initialProtection.type
|
||||||
|
);
|
||||||
const [ratedCurrentA, setRatedCurrentA] = useState(
|
const [ratedCurrentA, setRatedCurrentA] = useState(
|
||||||
initialProtection.ratedCurrentA
|
initialDevice?.ratedCurrentA ?? 0
|
||||||
);
|
);
|
||||||
const [fuseUtilizationCategory, setFuseUtilizationCategory] = useState(
|
const [fuseUtilizationCategory, setFuseUtilizationCategory] = useState(
|
||||||
initialProtection.fuseUtilizationCategory ?? "gG"
|
initialDevice?.fuseUtilizationCategory ?? "gG"
|
||||||
);
|
);
|
||||||
const [tripCharacteristic, setTripCharacteristic] = useState(
|
const [tripCharacteristic, setTripCharacteristic] = useState(
|
||||||
initialProtection.tripCharacteristic ?? "B"
|
initialDevice?.tripCharacteristic ?? "B"
|
||||||
);
|
);
|
||||||
const [rcdType, setRcdType] = useState(
|
const [rcdType, setRcdType] = useState(
|
||||||
initialProtection.rcdType ?? "A"
|
initialDevice?.rcdType ?? "A"
|
||||||
);
|
);
|
||||||
const [ratedResidualCurrentMa, setRatedResidualCurrentMa] = useState(
|
const [ratedResidualCurrentMa, setRatedResidualCurrentMa] = useState(
|
||||||
initialProtection.ratedResidualCurrentMa ?? 30
|
initialDevice?.ratedResidualCurrentMa ?? 30
|
||||||
);
|
);
|
||||||
|
const hasNoDevice = type === noCircuitProtectionDeviceType;
|
||||||
const usesFuseCategory = (
|
const usesFuseCategory = (
|
||||||
fuseProtectionDeviceTypes as readonly string[]
|
fuseProtectionDeviceTypes as readonly string[]
|
||||||
).includes(type);
|
).includes(type);
|
||||||
const usesTripCharacteristic =
|
const usesTripCharacteristic =
|
||||||
type === "LS" || type === "FI_LS" || type === "AFDD";
|
type === "LS" || type === "FI_LS" || type === "AFDD";
|
||||||
const usesResidualCurrent = type === "FI" || type === "FI_LS";
|
const usesResidualCurrent = type === "FI" || type === "FI_LS";
|
||||||
const allowedRatedCurrents =
|
const allowedRatedCurrents: readonly number[] = hasNoDevice
|
||||||
allowedRatedCurrentsAByProtectionDeviceType[type];
|
? []
|
||||||
|
: allowedRatedCurrentsAByProtectionDeviceType[type];
|
||||||
|
|
||||||
function handleTypeChange(nextType: ProtectionDeviceType) {
|
function handleTypeChange(nextType: CircuitProtectionDeviceType) {
|
||||||
setType(nextType);
|
setType(nextType);
|
||||||
setRatedCurrentA(
|
setRatedCurrentA(
|
||||||
allowedRatedCurrentsAByProtectionDeviceType[nextType][0]
|
nextType === noCircuitProtectionDeviceType
|
||||||
|
? 0
|
||||||
|
: allowedRatedCurrentsAByProtectionDeviceType[nextType][0]
|
||||||
);
|
);
|
||||||
setFuseUtilizationCategory("gG");
|
setFuseUtilizationCategory("gG");
|
||||||
setTripCharacteristic("B");
|
setTripCharacteristic("B");
|
||||||
|
|
@ -68,6 +79,10 @@ export function CircuitProtectionModal({
|
||||||
|
|
||||||
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
if (type === noCircuitProtectionDeviceType) {
|
||||||
|
await onSave({ type });
|
||||||
|
return;
|
||||||
|
}
|
||||||
await onSave({
|
await onSave({
|
||||||
type,
|
type,
|
||||||
ratedCurrentA,
|
ratedCurrentA,
|
||||||
|
|
@ -86,9 +101,7 @@ export function CircuitProtectionModal({
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
submitDisabled={
|
submitDisabled={
|
||||||
!(allowedRatedCurrents as readonly number[]).includes(
|
!hasNoDevice && !allowedRatedCurrents.includes(ratedCurrentA)
|
||||||
ratedCurrentA
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
submitLabel="Schutzgerät speichern"
|
submitLabel="Schutzgerät speichern"
|
||||||
title={`Schutzgerät bearbeiten – ${equipmentIdentifier}`}
|
title={`Schutzgerät bearbeiten – ${equipmentIdentifier}`}
|
||||||
|
|
@ -104,18 +117,29 @@ export function CircuitProtectionModal({
|
||||||
id="circuit-protection-type"
|
id="circuit-protection-type"
|
||||||
onChange={(event) =>
|
onChange={(event) =>
|
||||||
handleTypeChange(
|
handleTypeChange(
|
||||||
event.target.value as ProtectionDeviceType
|
event.target.value as CircuitProtectionDeviceType
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
value={type}
|
value={type}
|
||||||
>
|
>
|
||||||
|
<option value={noCircuitProtectionDeviceType}>
|
||||||
|
{circuitProtectionDeviceTypeLabels.NONE} (–)
|
||||||
|
</option>
|
||||||
{protectionDeviceTypes.map((entry) => (
|
{protectionDeviceTypes.map((entry) => (
|
||||||
<option key={entry} value={entry}>
|
<option key={entry} value={entry}>
|
||||||
{protectionDeviceTypeLabels[entry]}
|
{circuitProtectionDeviceTypeLabels[entry]}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
{hasNoDevice ? (
|
||||||
|
<div className="col-12">
|
||||||
|
<p className="text-secondary small mb-0">
|
||||||
|
Der Stromkreis erhält bewusst kein eigenes Schutzgerät und wird
|
||||||
|
in Liste und Seitenansicht mit „–“ geführt.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div className="col-12 col-md-6">
|
<div className="col-12 col-md-6">
|
||||||
<label className="form-label" htmlFor="circuit-protection-current">
|
<label className="form-label" htmlFor="circuit-protection-current">
|
||||||
Bemessungsstrom
|
Bemessungsstrom
|
||||||
|
|
@ -135,6 +159,7 @@ export function CircuitProtectionModal({
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
{usesFuseCategory ? (
|
{usesFuseCategory ? (
|
||||||
<div className="col-12 col-md-6">
|
<div className="col-12 col-md-6">
|
||||||
<label className="form-label" htmlFor="circuit-fuse-category">
|
<label className="form-label" htmlFor="circuit-fuse-category">
|
||||||
|
|
|
||||||
|
|
@ -170,8 +170,8 @@ import {
|
||||||
} from "../utils/circuit-grid-clipboard";
|
} from "../utils/circuit-grid-clipboard";
|
||||||
import type {
|
import type {
|
||||||
CircuitTreeCircuitDto,
|
CircuitTreeCircuitDto,
|
||||||
|
CircuitTreeCircuitProtectionDeviceDto,
|
||||||
CircuitTreeComponentDto,
|
CircuitTreeComponentDto,
|
||||||
CircuitTreeProtectionDeviceDto,
|
|
||||||
CircuitTreeResponseDto,
|
CircuitTreeResponseDto,
|
||||||
CreateCircuitDeviceRowInputDto,
|
CreateCircuitDeviceRowInputDto,
|
||||||
CreateCircuitInputDto,
|
CreateCircuitInputDto,
|
||||||
|
|
@ -1561,7 +1561,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSaveCircuitProtection(
|
async function handleSaveCircuitProtection(
|
||||||
protection: CircuitTreeProtectionDeviceDto
|
protection: CircuitTreeCircuitProtectionDeviceDto
|
||||||
) {
|
) {
|
||||||
const circuit = protectionEditorCircuit;
|
const circuit = protectionEditorCircuit;
|
||||||
if (!circuit) {
|
if (!circuit) {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,12 @@
|
||||||
import { protectionDeviceTypeLabels } from "../../shared/constants/protection-device";
|
import {
|
||||||
|
noCircuitProtectionDeviceText,
|
||||||
|
noCircuitProtectionDeviceType,
|
||||||
|
protectionDeviceTypeLabels,
|
||||||
|
} from "../../shared/constants/protection-device";
|
||||||
import { orderDistributionBoardsByFeed } from "../utils/distribution-board-hierarchy";
|
import { orderDistributionBoardsByFeed } from "../utils/distribution-board-hierarchy";
|
||||||
import type {
|
import type {
|
||||||
CircuitTreeCircuitDto,
|
CircuitTreeCircuitDto,
|
||||||
CircuitTreeProtectionDeviceDto,
|
CircuitTreeCircuitProtectionDeviceDto,
|
||||||
CircuitTreeResponseDto,
|
CircuitTreeResponseDto,
|
||||||
DistributionBoardDto,
|
DistributionBoardDto,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
|
|
@ -140,11 +144,14 @@ export const formatFactor = (value: number) => kilowattFormat.format(value);
|
||||||
export const formatCurrent = (value: number) => currentFormat.format(value);
|
export const formatCurrent = (value: number) => currentFormat.format(value);
|
||||||
|
|
||||||
export function formatProtectionDevice(
|
export function formatProtectionDevice(
|
||||||
protection: CircuitTreeProtectionDeviceDto | undefined
|
protection: CircuitTreeCircuitProtectionDeviceDto | undefined
|
||||||
): string | null {
|
): string | null {
|
||||||
if (!protection) {
|
if (!protection) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (protection.type === noCircuitProtectionDeviceType) {
|
||||||
|
return noCircuitProtectionDeviceText;
|
||||||
|
}
|
||||||
const parts = [
|
const parts = [
|
||||||
protectionDeviceTypeLabels[protection.type],
|
protectionDeviceTypeLabels[protection.type],
|
||||||
protection.tripCharacteristic,
|
protection.tripCharacteristic,
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import type {
|
||||||
import type {
|
import type {
|
||||||
BreakerTripCharacteristic,
|
BreakerTripCharacteristic,
|
||||||
FuseUtilizationCategory,
|
FuseUtilizationCategory,
|
||||||
|
NoCircuitProtectionDeviceType,
|
||||||
ProtectionDeviceType,
|
ProtectionDeviceType,
|
||||||
RcdType,
|
RcdType,
|
||||||
} from "../shared/constants/protection-device";
|
} from "../shared/constants/protection-device";
|
||||||
|
|
@ -517,6 +518,16 @@ export interface CircuitTreeProtectionDeviceDto {
|
||||||
ratedResidualCurrentMa?: number;
|
ratedResidualCurrentMa?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Explicit "no protection device" on a circuit; shown as "–". A circuit
|
||||||
|
// without protectionDevice is still open for planning.
|
||||||
|
export interface CircuitTreeNoProtectionDeviceDto {
|
||||||
|
type: NoCircuitProtectionDeviceType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CircuitTreeCircuitProtectionDeviceDto =
|
||||||
|
| CircuitTreeProtectionDeviceDto
|
||||||
|
| CircuitTreeNoProtectionDeviceDto;
|
||||||
|
|
||||||
export interface CircuitTreeComponentDto {
|
export interface CircuitTreeComponentDto {
|
||||||
id: string;
|
id: string;
|
||||||
circuitListId: string;
|
circuitListId: string;
|
||||||
|
|
@ -551,7 +562,7 @@ export interface CircuitTreeCircuitDto {
|
||||||
circuitCosPhi: number;
|
circuitCosPhi: number;
|
||||||
deviceRows: CircuitTreeDeviceRowDto[];
|
deviceRows: CircuitTreeDeviceRowDto[];
|
||||||
downstreamBoardLoads: CircuitTreeDownstreamBoardLoadDto[];
|
downstreamBoardLoads: CircuitTreeDownstreamBoardLoadDto[];
|
||||||
protectionDevice?: CircuitTreeProtectionDeviceDto;
|
protectionDevice?: CircuitTreeCircuitProtectionDeviceDto;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CircuitTreeSectionDto {
|
export interface CircuitTreeSectionDto {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
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";
|
import {
|
||||||
|
noCircuitProtectionDeviceText,
|
||||||
|
noCircuitProtectionDeviceType,
|
||||||
|
protectionDeviceTypeLabels,
|
||||||
|
} from "../../shared/constants/protection-device";
|
||||||
|
|
||||||
export type CellKey =
|
export type CellKey =
|
||||||
| "equipmentIdentifier"
|
| "equipmentIdentifier"
|
||||||
|
|
@ -531,7 +535,12 @@ 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 hasNoProtectionDevice =
|
||||||
|
circuit.protectionDevice?.type === noCircuitProtectionDeviceType;
|
||||||
|
const protection =
|
||||||
|
circuit.protectionDevice?.type === noCircuitProtectionDeviceType
|
||||||
|
? undefined
|
||||||
|
: circuit.protectionDevice;
|
||||||
const protectionCharacteristic = protection
|
const protectionCharacteristic = protection
|
||||||
? [
|
? [
|
||||||
protection.fuseUtilizationCategory,
|
protection.fuseUtilizationCategory,
|
||||||
|
|
@ -549,6 +558,9 @@ export function getCircuitValue(circuit: CircuitTreeCircuitDto, key: CellKey): G
|
||||||
case "displayName": return circuit.displayName;
|
case "displayName": return circuit.displayName;
|
||||||
case "circuitTotalPower": return circuit.circuitTotalPower;
|
case "circuitTotalPower": return circuit.circuitTotalPower;
|
||||||
case "protectionType":
|
case "protectionType":
|
||||||
|
if (hasNoProtectionDevice) {
|
||||||
|
return noCircuitProtectionDeviceText;
|
||||||
|
}
|
||||||
return protection
|
return protection
|
||||||
? protectionDeviceTypeLabels[protection.type]
|
? protectionDeviceTypeLabels[protection.type]
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
@ -557,6 +569,9 @@ export function getCircuitValue(circuit: CircuitTreeCircuitDto, key: CellKey): G
|
||||||
case "protectionCharacteristic":
|
case "protectionCharacteristic":
|
||||||
return protectionCharacteristic || undefined;
|
return protectionCharacteristic || undefined;
|
||||||
case "protectionSummary": {
|
case "protectionSummary": {
|
||||||
|
if (hasNoProtectionDevice) {
|
||||||
|
return noCircuitProtectionDeviceText;
|
||||||
|
}
|
||||||
if (!protection) {
|
if (!protection) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import type {
|
||||||
import {
|
import {
|
||||||
toDistributionBoardComponentSnapshot,
|
toDistributionBoardComponentSnapshot,
|
||||||
} from "./distribution-board-component-editing";
|
} from "./distribution-board-component-editing";
|
||||||
|
import { toCircuitProtectionSnapshot } from "./circuit-protection-editing";
|
||||||
|
|
||||||
export function toCircuitGroupSnapshot(
|
export function toCircuitGroupSnapshot(
|
||||||
section: CircuitTreeSectionDto,
|
section: CircuitTreeSectionDto,
|
||||||
|
|
@ -159,21 +160,7 @@ export function buildCircuitGroupSubtreeSnapshot(
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
protectionDevice: circuit.protectionDevice
|
protectionDevice: circuit.protectionDevice
|
||||||
? {
|
? toCircuitProtectionSnapshot(circuit.id, circuit.protectionDevice)
|
||||||
circuitId: circuit.id,
|
|
||||||
type: circuit.protectionDevice.type,
|
|
||||||
ratedCurrentA:
|
|
||||||
circuit.protectionDevice.ratedCurrentA,
|
|
||||||
fuseUtilizationCategory:
|
|
||||||
circuit.protectionDevice.fuseUtilizationCategory ??
|
|
||||||
null,
|
|
||||||
tripCharacteristic:
|
|
||||||
circuit.protectionDevice.tripCharacteristic ?? null,
|
|
||||||
rcdType: circuit.protectionDevice.rcdType ?? null,
|
|
||||||
ratedResidualCurrentMa:
|
|
||||||
circuit.protectionDevice.ratedResidualCurrentMa ??
|
|
||||||
null,
|
|
||||||
}
|
|
||||||
: null,
|
: null,
|
||||||
}));
|
}));
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,26 @@ import type {
|
||||||
} from "../../domain/models/circuit-protection-project-command.model";
|
} from "../../domain/models/circuit-protection-project-command.model";
|
||||||
import { createDefaultCircuitProtection } from "../../domain/services/protection-device-defaults";
|
import { createDefaultCircuitProtection } from "../../domain/services/protection-device-defaults";
|
||||||
import type { CircuitGroupCategory } from "../../shared/constants/circuit-group";
|
import type { CircuitGroupCategory } from "../../shared/constants/circuit-group";
|
||||||
|
import { noCircuitProtectionDeviceType } from "../../shared/constants/protection-device";
|
||||||
import type {
|
import type {
|
||||||
CircuitTreeProtectionDeviceDto,
|
CircuitTreeCircuitProtectionDeviceDto,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
|
|
||||||
export function toCircuitProtectionSnapshot(
|
export function toCircuitProtectionSnapshot(
|
||||||
circuitId: string,
|
circuitId: string,
|
||||||
protection: CircuitTreeProtectionDeviceDto
|
protection: CircuitTreeCircuitProtectionDeviceDto
|
||||||
): CircuitProtectionSnapshot {
|
): CircuitProtectionSnapshot {
|
||||||
|
if (protection.type === noCircuitProtectionDeviceType) {
|
||||||
|
return {
|
||||||
|
circuitId,
|
||||||
|
type: noCircuitProtectionDeviceType,
|
||||||
|
ratedCurrentA: null,
|
||||||
|
fuseUtilizationCategory: null,
|
||||||
|
tripCharacteristic: null,
|
||||||
|
rcdType: null,
|
||||||
|
ratedResidualCurrentMa: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
circuitId,
|
circuitId,
|
||||||
type: protection.type,
|
type: protection.type,
|
||||||
|
|
@ -26,8 +38,8 @@ export function toCircuitProtectionSnapshot(
|
||||||
|
|
||||||
export function getCircuitProtectionEditorInitialValue(
|
export function getCircuitProtectionEditorInitialValue(
|
||||||
category: CircuitGroupCategory | undefined,
|
category: CircuitGroupCategory | undefined,
|
||||||
protection: CircuitTreeProtectionDeviceDto | undefined
|
protection: CircuitTreeCircuitProtectionDeviceDto | undefined
|
||||||
): CircuitTreeProtectionDeviceDto {
|
): CircuitTreeCircuitProtectionDeviceDto {
|
||||||
if (protection) {
|
if (protection) {
|
||||||
return protection;
|
return protection;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import type { Request, Response } from "express";
|
||||||
import { calculateDistributionBoardLoads } from "../../domain/calculations/distribution-board-load-calculation.js";
|
import { calculateDistributionBoardLoads } from "../../domain/calculations/distribution-board-load-calculation.js";
|
||||||
import { calculateRowTotalPower } from "../../domain/calculations/circuit-power-calculation.js";
|
import { calculateRowTotalPower } from "../../domain/calculations/circuit-power-calculation.js";
|
||||||
import type {
|
import type {
|
||||||
|
CircuitTreeCircuitProtectionDevice,
|
||||||
CircuitTreeProtectionDevice,
|
CircuitTreeProtectionDevice,
|
||||||
CircuitTreeResponse,
|
CircuitTreeResponse,
|
||||||
ProjectCircuitTreesResponse,
|
ProjectCircuitTreesResponse,
|
||||||
|
|
@ -17,6 +18,7 @@ import {
|
||||||
externalModelStateRepository,
|
externalModelStateRepository,
|
||||||
projectRepository,
|
projectRepository,
|
||||||
} from "../composition/application-repositories.js";
|
} from "../composition/application-repositories.js";
|
||||||
|
import { noCircuitProtectionDeviceType } from "../../shared/constants/protection-device.js";
|
||||||
|
|
||||||
export function isMissingCircuitTreeSchemaError(error: unknown): boolean {
|
export function isMissingCircuitTreeSchemaError(error: unknown): boolean {
|
||||||
if (!(error instanceof Error)) return false;
|
if (!(error instanceof Error)) return false;
|
||||||
|
|
@ -289,7 +291,9 @@ export async function buildProjectCircuitTrees(
|
||||||
"de"
|
"de"
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
protectionDevice: protection ? toProtectionDeviceDto(protection) : undefined,
|
protectionDevice: protection
|
||||||
|
? toCircuitProtectionDeviceDto(protection)
|
||||||
|
: undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
for (const section of tree.sections) {
|
for (const section of tree.sections) {
|
||||||
|
|
@ -358,6 +362,25 @@ function emptyCircuitTree(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toCircuitProtectionDeviceDto(
|
||||||
|
value: Omit<Parameters<typeof toProtectionDeviceDto>[0], "type" | "ratedCurrentA"> & {
|
||||||
|
type: CircuitTreeCircuitProtectionDevice["type"];
|
||||||
|
ratedCurrentA: number | null;
|
||||||
|
}
|
||||||
|
): CircuitTreeCircuitProtectionDevice {
|
||||||
|
if (value.type === noCircuitProtectionDeviceType) {
|
||||||
|
return { type: value.type };
|
||||||
|
}
|
||||||
|
if (value.ratedCurrentA === null) {
|
||||||
|
throw new Error("Circuit protection device lacks a rated current.");
|
||||||
|
}
|
||||||
|
return toProtectionDeviceDto({
|
||||||
|
...value,
|
||||||
|
type: value.type,
|
||||||
|
ratedCurrentA: value.ratedCurrentA,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function toProtectionDeviceDto(value: {
|
function toProtectionDeviceDto(value: {
|
||||||
type: CircuitTreeProtectionDevice["type"];
|
type: CircuitTreeProtectionDevice["type"];
|
||||||
ratedCurrentA: number;
|
ratedCurrentA: number;
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,24 @@ export const protectionDeviceTypes = [
|
||||||
|
|
||||||
export type ProtectionDeviceType = (typeof protectionDeviceTypes)[number];
|
export type ProtectionDeviceType = (typeof protectionDeviceTypes)[number];
|
||||||
|
|
||||||
|
// A circuit may explicitly have no protection device ("–"). This differs from
|
||||||
|
// a circuit without any protection record, which is still open for planning.
|
||||||
|
// Distribution-board components always carry a real protection device.
|
||||||
|
export const noCircuitProtectionDeviceType = "NONE" as const;
|
||||||
|
export type NoCircuitProtectionDeviceType =
|
||||||
|
typeof noCircuitProtectionDeviceType;
|
||||||
|
|
||||||
|
// Display text of the explicit "no protection device" state.
|
||||||
|
export const noCircuitProtectionDeviceText = "–";
|
||||||
|
|
||||||
|
export const circuitProtectionDeviceTypes = [
|
||||||
|
...protectionDeviceTypes,
|
||||||
|
noCircuitProtectionDeviceType,
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type CircuitProtectionDeviceType =
|
||||||
|
(typeof circuitProtectionDeviceTypes)[number];
|
||||||
|
|
||||||
export const fuseUtilizationCategories = ["gG", "gR"] as const;
|
export const fuseUtilizationCategories = ["gG", "gR"] as const;
|
||||||
export type FuseUtilizationCategory =
|
export type FuseUtilizationCategory =
|
||||||
(typeof fuseUtilizationCategories)[number];
|
(typeof fuseUtilizationCategories)[number];
|
||||||
|
|
@ -91,6 +109,14 @@ export const protectionDeviceTypeLabels: Record<ProtectionDeviceType, string> =
|
||||||
AFDD: "AFDD",
|
AFDD: "AFDD",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const circuitProtectionDeviceTypeLabels: Record<
|
||||||
|
CircuitProtectionDeviceType,
|
||||||
|
string
|
||||||
|
> = {
|
||||||
|
...protectionDeviceTypeLabels,
|
||||||
|
NONE: "Kein Schutzgerät",
|
||||||
|
};
|
||||||
|
|
||||||
export function isAllowedRatedCurrentA(
|
export function isAllowedRatedCurrentA(
|
||||||
type: ProtectionDeviceType,
|
type: ProtectionDeviceType,
|
||||||
ratedCurrentA: number
|
ratedCurrentA: number
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import {
|
||||||
fuseUtilizationCategories,
|
fuseUtilizationCategories,
|
||||||
isAllowedRatedCurrentA,
|
isAllowedRatedCurrentA,
|
||||||
isAllowedRatedResidualCurrentMa,
|
isAllowedRatedResidualCurrentMa,
|
||||||
|
noCircuitProtectionDeviceType,
|
||||||
protectionDeviceTypes,
|
protectionDeviceTypes,
|
||||||
rcdTypes,
|
rcdTypes,
|
||||||
} from "../constants/protection-device.js";
|
} from "../constants/protection-device.js";
|
||||||
|
|
@ -93,3 +94,40 @@ export const protectionDeviceConfigurationSchema = z
|
||||||
export type ProtectionDeviceConfigurationInput = z.infer<
|
export type ProtectionDeviceConfigurationInput = z.infer<
|
||||||
typeof protectionDeviceConfigurationSchema
|
typeof protectionDeviceConfigurationSchema
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
// Persisted circuit protection record: nullable columns instead of optional
|
||||||
|
// fields. Either the explicit "no protection device" state with every value
|
||||||
|
// null, or one valid protection device configuration.
|
||||||
|
export function isValidCircuitProtectionRecord(value: Readonly<
|
||||||
|
Record<string, unknown>
|
||||||
|
>): boolean {
|
||||||
|
if (value.type === noCircuitProtectionDeviceType) {
|
||||||
|
return (
|
||||||
|
value.ratedCurrentA === null &&
|
||||||
|
value.fuseUtilizationCategory === null &&
|
||||||
|
value.tripCharacteristic === null &&
|
||||||
|
value.rcdType === null &&
|
||||||
|
value.ratedResidualCurrentMa === null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return isValidProtectionRecord(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isValidProtectionRecord(value: Readonly<
|
||||||
|
Record<string, unknown>
|
||||||
|
>): boolean {
|
||||||
|
return protectionDeviceConfigurationSchema.safeParse({
|
||||||
|
type: value.type,
|
||||||
|
ratedCurrentA: value.ratedCurrentA,
|
||||||
|
...(value.fuseUtilizationCategory === null
|
||||||
|
? {}
|
||||||
|
: { fuseUtilizationCategory: value.fuseUtilizationCategory }),
|
||||||
|
...(value.tripCharacteristic === null
|
||||||
|
? {}
|
||||||
|
: { tripCharacteristic: value.tripCharacteristic }),
|
||||||
|
...(value.rcdType === null ? {} : { rcdType: value.rcdType }),
|
||||||
|
...(value.ratedResidualCurrentMa === null
|
||||||
|
? {}
|
||||||
|
: { ratedResidualCurrentMa: value.ratedResidualCurrentMa }),
|
||||||
|
}).success;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -160,6 +160,47 @@ describe("circuit grid clipboard", () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("copies an explicit no-protection decision onto other circuits", () => {
|
||||||
|
const none = makeCircuit("circuit-5", [makeDevice("row-5", 0.1)], {
|
||||||
|
protectionDevice: { type: "NONE" },
|
||||||
|
});
|
||||||
|
const noneRows = buildVisibleGridRowsWithStructure(
|
||||||
|
[{ ...sections[0], circuits: [none, target] }],
|
||||||
|
{ headerComponents: [], footerComponents: [] }
|
||||||
|
);
|
||||||
|
const [noneRow, targetRow] = ["circuitCompact:circuit-5", "circuitCompact:circuit-2"].map(
|
||||||
|
(rowKey) => noneRows.find((row) => row.rowKey === rowKey)!
|
||||||
|
);
|
||||||
|
const clipboard = buildCellClipboard([noneRow], ["protectionSummary"]);
|
||||||
|
assert.equal(clipboard.text, "–");
|
||||||
|
const plan = planCellWrites([
|
||||||
|
{ row: targetRow, cellKey: "protectionSummary", value: clipboard.cells[0][0] },
|
||||||
|
]);
|
||||||
|
assert.deepEqual(plan.updates.map((update) => update.payload), [
|
||||||
|
{
|
||||||
|
circuitId: "circuit-2",
|
||||||
|
expected: {
|
||||||
|
circuitId: "circuit-2",
|
||||||
|
type: "LS",
|
||||||
|
ratedCurrentA: 10,
|
||||||
|
fuseUtilizationCategory: null,
|
||||||
|
tripCharacteristic: "B",
|
||||||
|
rcdType: null,
|
||||||
|
ratedResidualCurrentMa: null,
|
||||||
|
},
|
||||||
|
target: {
|
||||||
|
circuitId: "circuit-2",
|
||||||
|
type: "NONE",
|
||||||
|
ratedCurrentA: null,
|
||||||
|
fuseUtilizationCategory: null,
|
||||||
|
tripCharacteristic: null,
|
||||||
|
rcdType: null,
|
||||||
|
ratedResidualCurrentMa: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it("fills one value into every selected row and skips rows that cannot take it", () => {
|
it("fills one value into every selected row and skips rows that cannot take it", () => {
|
||||||
const selectedRowKeys = [
|
const selectedRowKeys = [
|
||||||
"circuitCompact:circuit-1",
|
"circuitCompact:circuit-1",
|
||||||
|
|
|
||||||
|
|
@ -197,6 +197,18 @@ describe("circuit grid model", () => {
|
||||||
),
|
),
|
||||||
"FI/LS · 16 A · B · Typ A · 30 mA"
|
"FI/LS · 16 A · B · Typ A · 30 mA"
|
||||||
);
|
);
|
||||||
|
const withoutDevice = {
|
||||||
|
...circuit,
|
||||||
|
protectionDevice: { type: "NONE" as const },
|
||||||
|
};
|
||||||
|
assert.equal(getCircuitValue(withoutDevice, "protectionSummary"), "–");
|
||||||
|
assert.equal(getCircuitValue(withoutDevice, "protectionType"), "–");
|
||||||
|
assert.equal(getCircuitValue(withoutDevice, "protectionRatedCurrent"), undefined);
|
||||||
|
// A circuit without any protection record is still open.
|
||||||
|
assert.equal(
|
||||||
|
getCircuitValue({ ...circuit, protectionDevice: undefined }, "protectionSummary"),
|
||||||
|
undefined
|
||||||
|
);
|
||||||
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");
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,70 @@ describe("circuit-protection project command", () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("stores an explicit no-protection decision and undoes it to the device", () => {
|
||||||
|
const context = createTestDatabase();
|
||||||
|
try {
|
||||||
|
const repository = new CircuitProtectionProjectCommandRepository(
|
||||||
|
context.db
|
||||||
|
);
|
||||||
|
const history = new ProjectHistoryRepository(context.db);
|
||||||
|
context.db.insert(circuitProtectionDevices).values(protection).run();
|
||||||
|
const none: CircuitProtectionSnapshot = {
|
||||||
|
circuitId: "circuit-1",
|
||||||
|
type: "NONE",
|
||||||
|
ratedCurrentA: null,
|
||||||
|
fuseUtilizationCategory: null,
|
||||||
|
tripCharacteristic: null,
|
||||||
|
rcdType: null,
|
||||||
|
ratedResidualCurrentMa: null,
|
||||||
|
};
|
||||||
|
const executed = repository.execute({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 0,
|
||||||
|
source: "user",
|
||||||
|
command: createCircuitProtectionUpdateProjectCommand(
|
||||||
|
"circuit-1",
|
||||||
|
protection,
|
||||||
|
none
|
||||||
|
),
|
||||||
|
});
|
||||||
|
assert.deepEqual(
|
||||||
|
context.db.select().from(circuitProtectionDevices).get(),
|
||||||
|
none
|
||||||
|
);
|
||||||
|
const undo = history.getNextCommand("project-1", "undo");
|
||||||
|
assert.ok(undo);
|
||||||
|
repository.execute({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 1,
|
||||||
|
source: "undo",
|
||||||
|
historyTargetChangeSetId: undo.changeSetId,
|
||||||
|
command: executed.inverse,
|
||||||
|
});
|
||||||
|
assert.deepEqual(
|
||||||
|
context.db.select().from(circuitProtectionDevices).get(),
|
||||||
|
protection
|
||||||
|
);
|
||||||
|
for (const invalid of [
|
||||||
|
{ ...none, ratedCurrentA: 16 },
|
||||||
|
{ ...none, tripCharacteristic: "B" as const },
|
||||||
|
{ ...protection, ratedCurrentA: null },
|
||||||
|
]) {
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
createCircuitProtectionUpdateProjectCommand(
|
||||||
|
"circuit-1",
|
||||||
|
protection,
|
||||||
|
invalid
|
||||||
|
),
|
||||||
|
/configuration is invalid/
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
context.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("rejects stale state, foreign ownership and user removal", () => {
|
it("rejects stale state, foreign ownership and user removal", () => {
|
||||||
const context = createTestDatabase();
|
const context = createTestDatabase();
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -243,6 +243,7 @@ describe("print document model", () => {
|
||||||
}),
|
}),
|
||||||
"FI/LS B 16 A, 30 mA Typ A"
|
"FI/LS B 16 A, 30 mA Typ A"
|
||||||
);
|
);
|
||||||
|
assert.equal(formatProtectionDevice({ type: "NONE" }), "–");
|
||||||
assert.equal(formatProtectionDevice(undefined), null);
|
assert.equal(formatProtectionDevice(undefined), null);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -559,6 +559,44 @@ describe("project state snapshot model", () => {
|
||||||
() => parseProjectStateSnapshot(invalidProtection),
|
() => parseProjectStateSnapshot(invalidProtection),
|
||||||
/protection-device configuration is invalid/
|
/protection-device configuration is invalid/
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const protectedState = protectedSnapshot();
|
||||||
|
const noProtectionRecord = {
|
||||||
|
circuitId: protectedState.circuitProtectionDevices[0]!.circuitId,
|
||||||
|
type: "NONE",
|
||||||
|
ratedCurrentA: null,
|
||||||
|
fuseUtilizationCategory: null,
|
||||||
|
tripCharacteristic: null,
|
||||||
|
rcdType: null,
|
||||||
|
ratedResidualCurrentMa: null,
|
||||||
|
};
|
||||||
|
assert.equal(
|
||||||
|
parseProjectStateSnapshot({
|
||||||
|
...protectedState,
|
||||||
|
circuitProtectionDevices: [noProtectionRecord],
|
||||||
|
}).circuitProtectionDevices[0]?.type,
|
||||||
|
"NONE"
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
parseProjectStateSnapshot({
|
||||||
|
...protectedState,
|
||||||
|
circuitProtectionDevices: [
|
||||||
|
{ ...noProtectionRecord, ratedCurrentA: 16 },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
/protection-device configuration is invalid/
|
||||||
|
);
|
||||||
|
const componentWithoutDevice = {
|
||||||
|
...protectedState,
|
||||||
|
distributionBoardComponentProtectionDevices: [
|
||||||
|
{
|
||||||
|
...protectedState.distributionBoardComponentProtectionDevices[0]!,
|
||||||
|
type: "NONE",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
assert.throws(() => parseProjectStateSnapshot(componentWithoutDevice));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects duplicate ids and invalid ownership", () => {
|
it("rejects duplicate ids and invalid ownership", () => {
|
||||||
|
|
|
||||||
|
|
@ -205,6 +205,27 @@ describe("protection device defaults", () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("maps the explicit no-protection state to a record without values", () => {
|
||||||
|
const none = getCircuitProtectionEditorInitialValue("single_phase", {
|
||||||
|
type: "NONE",
|
||||||
|
});
|
||||||
|
assert.deepEqual(none, { type: "NONE" });
|
||||||
|
assert.deepEqual(toCircuitProtectionSnapshot("circuit-1", none), {
|
||||||
|
circuitId: "circuit-1",
|
||||||
|
type: "NONE",
|
||||||
|
ratedCurrentA: null,
|
||||||
|
fuseUtilizationCategory: null,
|
||||||
|
tripCharacteristic: null,
|
||||||
|
rcdType: null,
|
||||||
|
ratedResidualCurrentMa: null,
|
||||||
|
});
|
||||||
|
// Distribution-board components never take the circuit-only state.
|
||||||
|
assert.equal(
|
||||||
|
protectionDeviceConfigurationSchema.safeParse({ type: "NONE" }).success,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("creates the agreed circuit defaults", () => {
|
it("creates the agreed circuit defaults", () => {
|
||||||
assert.deepEqual(createDefaultCircuitProtection("lighting"), {
|
assert.deepEqual(createDefaultCircuitProtection("lighting"), {
|
||||||
type: "LS",
|
type: "LS",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue