Derive device voltages from project settings
This commit is contained in:
@@ -150,7 +150,11 @@ synchronizes linked circuit rows implicitly.
|
||||
- response:
|
||||
`{ "project": { ... }, "revision": { ... }, "history": { ... } }`
|
||||
- persistent Undo/Redo restores metadata, voltage values and the enabled
|
||||
distribution-board supply types together
|
||||
distribution-board supply types together; project-device and circuit
|
||||
voltages are recalculated from the restored project settings in the same
|
||||
transaction
|
||||
- project-device and circuit voltage are derived values and are not accepted
|
||||
as editable frontend fields
|
||||
- at least one of `AV`, `SV`, `EV`, `USV`, `MSR`, `SiBe` must be enabled;
|
||||
a supply type currently used by a distribution board cannot be disabled
|
||||
- unchanged values are rejected without creating a revision; a stale
|
||||
|
||||
@@ -265,7 +265,9 @@ Completed foundation:
|
||||
snapshots in the same transaction
|
||||
- `project.update-settings` persists both project voltage defaults, its exact
|
||||
inverse, revision and history transition atomically; the project page and
|
||||
existing PUT route require the current expected revision
|
||||
existing PUT route require the current expected revision; the same
|
||||
transaction derives every project-device and circuit voltage again, so
|
||||
Undo/Redo cannot leave mixed voltage states
|
||||
- `distribution-board.insert` persists the complete generated board, circuit
|
||||
list and default-section structure with stable ids; its delete inverse
|
||||
refuses changed or populated structures and the project-page POST tracks the
|
||||
|
||||
@@ -35,21 +35,27 @@ requirements and intended sequencing, not proof of implementation.
|
||||
- Persist changes through project commands so they remain undoable after a
|
||||
restart.
|
||||
|
||||
## Device Voltage Defaults
|
||||
## Device Voltage Derivation
|
||||
|
||||
- Derive voltage from the selected phase type and the project settings:
|
||||
- single-phase uses the project's single-phase voltage;
|
||||
- three-phase uses the project's three-phase voltage.
|
||||
- Apply this default consistently when creating global devices, project
|
||||
devices and manual circuit-list entries.
|
||||
- Define explicit override behavior before implementation so a user-entered
|
||||
exceptional voltage is not silently overwritten when phase type or project
|
||||
settings change.
|
||||
- Voltage is not editable on global devices, project devices or circuits.
|
||||
Global devices store only their phase; copying one into a project derives
|
||||
the target project's voltage.
|
||||
- Project-device voltage follows its phase. Circuit voltage follows the
|
||||
section phase; an unassigned circuit is three-phase only when all assigned
|
||||
device rows with a valid phase are three-phase.
|
||||
- Changing project voltage settings updates all project devices and circuits
|
||||
atomically in the same persistent Undo/Redo step.
|
||||
- Database migration `0019` and snapshot schema version `5` normalize older
|
||||
stored values. Version-four and older imports remain supported and are
|
||||
normalized while being upgraded.
|
||||
|
||||
## Recommended Sequence
|
||||
|
||||
1. Distribution-board floor and supply fields.
|
||||
2. Device voltage default and override semantics.
|
||||
2. Device voltage derivation without overrides.
|
||||
3. Snapshot-to-revision descriptions and history presentation.
|
||||
|
||||
The Revit/CSV/IFCGUID round-trip remains a separate jointly planned phase and
|
||||
|
||||
@@ -24,7 +24,6 @@ const emptyGlobalDevice: CreateGlobalDeviceInput = {
|
||||
quantity: 1,
|
||||
installedPowerPerUnitKw: 0.1,
|
||||
demandFactor: 1,
|
||||
voltageV: 230,
|
||||
phaseCount: 1,
|
||||
powerFactor: 1,
|
||||
note: "",
|
||||
@@ -49,7 +48,6 @@ export default function ProjectsPage() {
|
||||
quantity: "1",
|
||||
installedPowerPerUnitKw: "0.1",
|
||||
demandFactor: "1",
|
||||
voltageV: "230",
|
||||
phaseCount: "1",
|
||||
powerFactor: "1",
|
||||
note: "",
|
||||
@@ -153,7 +151,6 @@ export default function ProjectsPage() {
|
||||
quantity: Number(globalDeviceForm.quantity),
|
||||
installedPowerPerUnitKw: Number(globalDeviceForm.installedPowerPerUnitKw),
|
||||
demandFactor: Number(globalDeviceForm.demandFactor),
|
||||
voltageV: toOptionalNumber(globalDeviceForm.voltageV),
|
||||
phaseCount: globalDeviceForm.phaseCount === "3" ? 3 : 1,
|
||||
powerFactor: toOptionalNumber(globalDeviceForm.powerFactor),
|
||||
note: globalDeviceForm.note.trim() || undefined,
|
||||
@@ -170,7 +167,6 @@ export default function ProjectsPage() {
|
||||
quantity: String(emptyGlobalDevice.quantity),
|
||||
installedPowerPerUnitKw: String(emptyGlobalDevice.installedPowerPerUnitKw),
|
||||
demandFactor: String(emptyGlobalDevice.demandFactor),
|
||||
voltageV: String(emptyGlobalDevice.voltageV ?? ""),
|
||||
phaseCount: String(emptyGlobalDevice.phaseCount ?? 1),
|
||||
powerFactor: String(emptyGlobalDevice.powerFactor ?? ""),
|
||||
note: emptyGlobalDevice.note ?? "",
|
||||
@@ -207,7 +203,6 @@ export default function ProjectsPage() {
|
||||
quantity: device.quantity,
|
||||
installedPowerPerUnitKw: device.installedPowerPerUnitKw,
|
||||
demandFactor: device.demandFactor,
|
||||
voltageV: device.voltageV ?? undefined,
|
||||
phaseCount: device.phaseCount ?? undefined,
|
||||
powerFactor: device.powerFactor ?? undefined,
|
||||
note: device.note ?? undefined,
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
UPDATE `global_devices`
|
||||
SET `voltage_v` = NULL;
|
||||
--> statement-breakpoint
|
||||
UPDATE `project_devices` AS `pd`
|
||||
SET `voltage_v` = CASE
|
||||
WHEN `pd`.`phase_type` = 'three_phase' THEN `p`.`three_phase_voltage_v`
|
||||
ELSE `p`.`single_phase_voltage_v`
|
||||
END
|
||||
FROM `projects` AS `p`
|
||||
WHERE `p`.`id` = `pd`.`project_id`;
|
||||
--> statement-breakpoint
|
||||
WITH `derived_circuit_voltages` AS (
|
||||
SELECT
|
||||
`c`.`id` AS `circuit_id`,
|
||||
CASE
|
||||
WHEN `cs`.`key` = 'three_phase' THEN `p`.`three_phase_voltage_v`
|
||||
WHEN `cs`.`key` IN ('lighting', 'single_phase') THEN `p`.`single_phase_voltage_v`
|
||||
WHEN EXISTS (
|
||||
SELECT 1
|
||||
FROM `circuit_device_rows` AS `three_phase_row`
|
||||
WHERE `three_phase_row`.`circuit_id` = `c`.`id`
|
||||
AND `three_phase_row`.`phase_type` = 'three_phase'
|
||||
) AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM `circuit_device_rows` AS `other_row`
|
||||
WHERE `other_row`.`circuit_id` = `c`.`id`
|
||||
AND `other_row`.`phase_type` = 'single_phase'
|
||||
) THEN `p`.`three_phase_voltage_v`
|
||||
ELSE `p`.`single_phase_voltage_v`
|
||||
END AS `derived_voltage`
|
||||
FROM `circuits` AS `c`
|
||||
INNER JOIN `circuit_lists` AS `cl`
|
||||
ON `cl`.`id` = `c`.`circuit_list_id`
|
||||
INNER JOIN `projects` AS `p`
|
||||
ON `p`.`id` = `cl`.`project_id`
|
||||
INNER JOIN `circuit_sections` AS `cs`
|
||||
ON `cs`.`id` = `c`.`section_id`
|
||||
)
|
||||
UPDATE `circuits`
|
||||
SET `voltage` = (
|
||||
SELECT `derived_voltage`
|
||||
FROM `derived_circuit_voltages`
|
||||
WHERE `circuit_id` = `circuits`.`id`
|
||||
)
|
||||
WHERE `id` IN (
|
||||
SELECT `circuit_id`
|
||||
FROM `derived_circuit_voltages`
|
||||
);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -134,6 +134,13 @@
|
||||
"when": 1785308458789,
|
||||
"tag": "0018_fancy_argent",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 19,
|
||||
"version": "6",
|
||||
"when": 1785310907349,
|
||||
"tag": "0019_normalize_project_voltages",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -20,11 +20,14 @@ import { circuitLists } from "../schema/circuit-lists.js";
|
||||
import { circuitSections } from "../schema/circuit-sections.js";
|
||||
import { circuits } from "../schema/circuits.js";
|
||||
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
||||
import { updateDerivedCircuitVoltage } from "./project-voltage.persistence.js";
|
||||
import { resolveCircuitVoltage } from "./project-voltage.persistence.js";
|
||||
|
||||
interface PersistedMoveRow {
|
||||
id: string;
|
||||
circuitId: string;
|
||||
sortOrder: number;
|
||||
phaseType: string | null;
|
||||
}
|
||||
|
||||
export class CircuitDeviceRowMoveProjectCommandRepository
|
||||
@@ -85,6 +88,7 @@ export class CircuitDeviceRowMoveProjectCommandRepository
|
||||
);
|
||||
this.applyMoves(database, command.payload.moves);
|
||||
this.updateReserveStates(database, circuitIds);
|
||||
this.updateVoltages(database, projectId, circuitIds);
|
||||
return inverse;
|
||||
}
|
||||
|
||||
@@ -94,7 +98,16 @@ export class CircuitDeviceRowMoveProjectCommandRepository
|
||||
const { targetCircuit, targetCircuitAction, moves } =
|
||||
command.payload;
|
||||
const rowsById = this.loadExpectedRows(database, moves);
|
||||
this.assertCircuitLocation(database, projectId, targetCircuit);
|
||||
const appliedTargetCircuit = {
|
||||
...targetCircuit,
|
||||
voltage: resolveCircuitVoltage(
|
||||
database,
|
||||
projectId,
|
||||
targetCircuit.sectionId,
|
||||
moves.map((move) => rowsById.get(move.rowId)?.phaseType)
|
||||
),
|
||||
};
|
||||
this.assertCircuitLocation(database, projectId, appliedTargetCircuit);
|
||||
|
||||
if (targetCircuitAction === "create") {
|
||||
const sourceCircuitIds = [
|
||||
@@ -106,13 +119,13 @@ export class CircuitDeviceRowMoveProjectCommandRepository
|
||||
sourceCircuitIds,
|
||||
targetCircuit.circuitListId
|
||||
);
|
||||
this.assertTargetCircuitAvailable(database, targetCircuit);
|
||||
this.insertTargetCircuit(database, targetCircuit);
|
||||
this.assertTargetCircuitAvailable(database, appliedTargetCircuit);
|
||||
this.insertTargetCircuit(database, appliedTargetCircuit);
|
||||
|
||||
const inverse =
|
||||
createCircuitDeviceRowMoveWithNewCircuitProjectCommand(
|
||||
"delete",
|
||||
targetCircuit,
|
||||
appliedTargetCircuit,
|
||||
this.reverseMoves(moves, rowsById)
|
||||
);
|
||||
this.applyMoves(database, moves);
|
||||
@@ -120,12 +133,16 @@ export class CircuitDeviceRowMoveProjectCommandRepository
|
||||
...sourceCircuitIds,
|
||||
targetCircuit.id,
|
||||
]);
|
||||
this.updateVoltages(database, projectId, [
|
||||
...sourceCircuitIds,
|
||||
targetCircuit.id,
|
||||
]);
|
||||
return inverse;
|
||||
}
|
||||
|
||||
this.assertTargetCircuitUnchanged(
|
||||
database,
|
||||
targetCircuit,
|
||||
appliedTargetCircuit,
|
||||
moves.map((move) => move.rowId)
|
||||
);
|
||||
const destinationCircuitIds = [
|
||||
@@ -140,7 +157,7 @@ export class CircuitDeviceRowMoveProjectCommandRepository
|
||||
const inverse =
|
||||
createCircuitDeviceRowMoveWithNewCircuitProjectCommand(
|
||||
"create",
|
||||
targetCircuit,
|
||||
appliedTargetCircuit,
|
||||
this.reverseMoves(moves, rowsById)
|
||||
);
|
||||
this.applyMoves(database, moves);
|
||||
@@ -148,6 +165,11 @@ export class CircuitDeviceRowMoveProjectCommandRepository
|
||||
targetCircuit.id,
|
||||
...destinationCircuitIds,
|
||||
]);
|
||||
this.updateVoltages(
|
||||
database,
|
||||
projectId,
|
||||
destinationCircuitIds
|
||||
);
|
||||
const deleted = database
|
||||
.delete(circuits)
|
||||
.where(
|
||||
@@ -169,6 +191,16 @@ export class CircuitDeviceRowMoveProjectCommandRepository
|
||||
return inverse;
|
||||
}
|
||||
|
||||
private updateVoltages(
|
||||
database: AppDatabase,
|
||||
projectId: string,
|
||||
circuitIds: string[]
|
||||
) {
|
||||
for (const circuitId of new Set(circuitIds)) {
|
||||
updateDerivedCircuitVoltage(database, projectId, circuitId);
|
||||
}
|
||||
}
|
||||
|
||||
private loadExpectedRows(
|
||||
database: AppDatabase,
|
||||
moves: CircuitDeviceRowMoveAssignment[]
|
||||
@@ -179,6 +211,7 @@ export class CircuitDeviceRowMoveProjectCommandRepository
|
||||
id: circuitDeviceRows.id,
|
||||
circuitId: circuitDeviceRows.circuitId,
|
||||
sortOrder: circuitDeviceRows.sortOrder,
|
||||
phaseType: circuitDeviceRows.phaseType,
|
||||
})
|
||||
.from(circuitDeviceRows)
|
||||
.where(inArray(circuitDeviceRows.id, rowIds))
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
type CircuitDeviceRowPatchInput,
|
||||
} from "./circuit-device-row.persistence.js";
|
||||
import { executeProjectCommandTransactionWithAppliedForward } from "./project-command-transaction.persistence.js";
|
||||
import { updateDerivedCircuitVoltage } from "./project-voltage.persistence.js";
|
||||
|
||||
type CircuitDeviceRow = typeof circuitDeviceRows.$inferSelect;
|
||||
|
||||
@@ -117,6 +118,13 @@ export class CircuitDeviceRowProjectCommandRepository
|
||||
if (update.changes !== 1) {
|
||||
throw new Error("Circuit device row changed before command execution.");
|
||||
}
|
||||
if (patch.phaseType !== undefined) {
|
||||
updateDerivedCircuitVoltage(
|
||||
tx,
|
||||
input.projectId,
|
||||
current.circuitId
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
forward: appliedForward,
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
toCircuitDeviceRowSnapshot,
|
||||
} from "./circuit-device-row-structure.persistence.js";
|
||||
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
||||
import { updateDerivedCircuitVoltage } from "./project-voltage.persistence.js";
|
||||
|
||||
export class CircuitDeviceRowStructureProjectCommandRepository
|
||||
implements CircuitDeviceRowStructureProjectCommandStore
|
||||
@@ -101,6 +102,7 @@ export class CircuitDeviceRowStructureProjectCommandRepository
|
||||
if (circuitUpdate.changes !== 1) {
|
||||
throw new Error("Circuit changed before device-row insertion.");
|
||||
}
|
||||
updateDerivedCircuitVoltage(database, projectId, row.circuitId);
|
||||
|
||||
return createCircuitDeviceRowDeleteProjectCommand(
|
||||
row.id,
|
||||
@@ -153,6 +155,11 @@ export class CircuitDeviceRowStructureProjectCommandRepository
|
||||
if (circuitUpdate.changes !== 1) {
|
||||
throw new Error("Circuit changed before device-row deletion.");
|
||||
}
|
||||
updateDerivedCircuitVoltage(
|
||||
database,
|
||||
projectId,
|
||||
expectedCircuitId
|
||||
);
|
||||
|
||||
return createCircuitDeviceRowInsertProjectCommand(
|
||||
toCircuitDeviceRowSnapshot(row)
|
||||
|
||||
@@ -18,7 +18,9 @@ import {
|
||||
toCircuitPatchValues,
|
||||
type CircuitPatchPersistenceInput,
|
||||
} from "./circuit.persistence.js";
|
||||
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
||||
import { executeProjectCommandTransactionWithAppliedForward } from "./project-command-transaction.persistence.js";
|
||||
import { resolveCircuitVoltage } from "./project-voltage.persistence.js";
|
||||
import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
|
||||
|
||||
type CircuitRow = typeof circuits.$inferSelect;
|
||||
|
||||
@@ -30,7 +32,7 @@ export class CircuitProjectCommandRepository
|
||||
executeUpdate(input: ExecuteCircuitUpdateCommandInput) {
|
||||
assertCircuitUpdateProjectCommand(input.command);
|
||||
|
||||
return executeProjectCommandTransaction(
|
||||
return executeProjectCommandTransactionWithAppliedForward(
|
||||
this.database,
|
||||
input,
|
||||
(tx) => this.applyCommand(tx, input)
|
||||
@@ -71,14 +73,44 @@ export class CircuitProjectCommandRepository
|
||||
])
|
||||
) as CircuitPatchPersistenceInput;
|
||||
this.assertSectionInCircuitList(tx, current, patch.sectionId);
|
||||
if (input.source === "user") {
|
||||
if (
|
||||
input.command.payload.changes.every(
|
||||
(change) => change.field === "voltage"
|
||||
)
|
||||
) {
|
||||
throw new Error("Circuit voltage is derived and cannot be edited.");
|
||||
}
|
||||
const devicePhaseTypes = tx
|
||||
.select({ phaseType: circuitDeviceRows.phaseType })
|
||||
.from(circuitDeviceRows)
|
||||
.where(eq(circuitDeviceRows.circuitId, current.id))
|
||||
.all()
|
||||
.map((row) => row.phaseType);
|
||||
const derivedVoltage = resolveCircuitVoltage(
|
||||
tx,
|
||||
input.projectId,
|
||||
patch.sectionId ?? current.sectionId,
|
||||
devicePhaseTypes
|
||||
);
|
||||
if (derivedVoltage === current.voltage) {
|
||||
delete patch.voltage;
|
||||
} else {
|
||||
patch.voltage = derivedVoltage;
|
||||
}
|
||||
}
|
||||
this.assertUniqueEquipmentIdentifier(
|
||||
tx,
|
||||
current,
|
||||
patch.equipmentIdentifier
|
||||
);
|
||||
|
||||
const appliedForward = createCircuitUpdateProjectCommand(
|
||||
current.id,
|
||||
patch as CircuitUpdatePatch
|
||||
);
|
||||
const inversePatch = Object.fromEntries(
|
||||
input.command.payload.changes.map((change) => [
|
||||
appliedForward.payload.changes.map((change) => [
|
||||
change.field,
|
||||
getCircuitFieldValue(current, change.field),
|
||||
])
|
||||
@@ -97,7 +129,7 @@ export class CircuitProjectCommandRepository
|
||||
throw new Error("Circuit changed before command execution.");
|
||||
}
|
||||
|
||||
return inverse;
|
||||
return { forward: appliedForward, inverse };
|
||||
}
|
||||
|
||||
private assertSectionInCircuitList(
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
toCircuitDeviceRowSnapshot,
|
||||
} from "./circuit-device-row-structure.persistence.js";
|
||||
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
||||
import { resolveCircuitVoltage } from "./project-voltage.persistence.js";
|
||||
|
||||
export class CircuitStructureProjectCommandRepository
|
||||
implements CircuitStructureProjectCommandStore
|
||||
@@ -77,6 +78,19 @@ export class CircuitStructureProjectCommandRepository
|
||||
snapshot: CircuitSnapshot
|
||||
) {
|
||||
this.assertCircuitLocation(database, projectId, snapshot);
|
||||
if (source === "user") {
|
||||
const derivedVoltage = resolveCircuitVoltage(
|
||||
database,
|
||||
projectId,
|
||||
snapshot.sectionId,
|
||||
snapshot.deviceRows.map((row) => row.phaseType)
|
||||
);
|
||||
if (snapshot.voltage !== derivedVoltage) {
|
||||
throw new Error(
|
||||
"Circuit voltage must match the project phase voltage."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const existingCircuit = database
|
||||
.select({ id: circuits.id })
|
||||
|
||||
@@ -26,7 +26,7 @@ export class GlobalDeviceRepository {
|
||||
quantity: input.quantity,
|
||||
installedPowerPerUnitKw: input.installedPowerPerUnitKw,
|
||||
demandFactor: input.demandFactor,
|
||||
voltageV: input.voltageV ?? null,
|
||||
voltageV: null,
|
||||
phaseCount: input.phaseCount ?? null,
|
||||
powerFactor: input.powerFactor ?? null,
|
||||
note: input.note ?? null,
|
||||
@@ -45,7 +45,7 @@ export class GlobalDeviceRepository {
|
||||
quantity: input.quantity,
|
||||
installedPowerPerUnitKw: input.installedPowerPerUnitKw,
|
||||
demandFactor: input.demandFactor,
|
||||
voltageV: input.voltageV ?? null,
|
||||
voltageV: null,
|
||||
phaseCount: input.phaseCount ?? null,
|
||||
powerFactor: input.powerFactor ?? null,
|
||||
note: input.note ?? null,
|
||||
|
||||
@@ -12,7 +12,8 @@ import type {
|
||||
} from "../../domain/ports/project-device-project-command.store.js";
|
||||
import type { AppDatabase } from "../database-context.js";
|
||||
import { projectDevices } from "../schema/project-devices.js";
|
||||
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
||||
import { executeProjectCommandTransactionWithAppliedForward } from "./project-command-transaction.persistence.js";
|
||||
import { resolveProjectDeviceVoltage } from "./project-voltage.persistence.js";
|
||||
|
||||
type ProjectDeviceRow = typeof projectDevices.$inferSelect;
|
||||
|
||||
@@ -24,7 +25,7 @@ export class ProjectDeviceProjectCommandRepository
|
||||
executeUpdate(input: ExecuteProjectDeviceUpdateCommandInput) {
|
||||
assertProjectDeviceUpdateProjectCommand(input.command);
|
||||
|
||||
return executeProjectCommandTransaction(
|
||||
return executeProjectCommandTransactionWithAppliedForward(
|
||||
this.database,
|
||||
input,
|
||||
(tx) => this.applyCommand(tx, input)
|
||||
@@ -60,8 +61,40 @@ export class ProjectDeviceProjectCommandRepository
|
||||
change.value,
|
||||
])
|
||||
) as ProjectDeviceUpdatePatch;
|
||||
if (input.source === "user") {
|
||||
if (
|
||||
input.command.payload.changes.every(
|
||||
(change) => change.field === "voltageV"
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
"Project-device voltage is derived and cannot be edited."
|
||||
);
|
||||
}
|
||||
const targetPhaseType = patch.phaseType ?? current.phaseType;
|
||||
if (
|
||||
targetPhaseType !== "single_phase" &&
|
||||
targetPhaseType !== "three_phase"
|
||||
) {
|
||||
throw new Error("Project-device phase type is invalid.");
|
||||
}
|
||||
const derivedVoltage = resolveProjectDeviceVoltage(
|
||||
tx,
|
||||
input.projectId,
|
||||
targetPhaseType
|
||||
);
|
||||
if (derivedVoltage === current.voltageV) {
|
||||
delete patch.voltageV;
|
||||
} else {
|
||||
patch.voltageV = derivedVoltage;
|
||||
}
|
||||
}
|
||||
const appliedForward = createProjectDeviceUpdateProjectCommand(
|
||||
current.id,
|
||||
patch
|
||||
);
|
||||
const inversePatch = Object.fromEntries(
|
||||
input.command.payload.changes.map((change) => [
|
||||
appliedForward.payload.changes.map((change) => [
|
||||
change.field,
|
||||
getProjectDeviceFieldValue(current, change.field),
|
||||
])
|
||||
@@ -87,7 +120,7 @@ export class ProjectDeviceProjectCommandRepository
|
||||
);
|
||||
}
|
||||
|
||||
return inverse;
|
||||
return { forward: appliedForward, inverse };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import { circuitLists } from "../schema/circuit-lists.js";
|
||||
import { circuits } from "../schema/circuits.js";
|
||||
import { projectDevices } from "../schema/project-devices.js";
|
||||
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
||||
import { updateDerivedCircuitVoltage } from "./project-voltage.persistence.js";
|
||||
|
||||
type CircuitDeviceRow = typeof circuitDeviceRows.$inferSelect;
|
||||
|
||||
@@ -129,6 +130,25 @@ export class ProjectDeviceRowSyncProjectCommandRepository
|
||||
);
|
||||
}
|
||||
}
|
||||
const affectedCircuitIds = new Set(
|
||||
input.command.payload.rows
|
||||
.filter(
|
||||
(assignment) =>
|
||||
assignment.expected.phaseType !==
|
||||
assignment.target.phaseType
|
||||
)
|
||||
.map(
|
||||
(assignment) =>
|
||||
persistedById.get(assignment.rowId)!.circuitId
|
||||
)
|
||||
);
|
||||
for (const circuitId of affectedCircuitIds) {
|
||||
updateDerivedCircuitVoltage(
|
||||
tx,
|
||||
input.projectId,
|
||||
circuitId
|
||||
);
|
||||
}
|
||||
|
||||
return inverse;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import { projectDevices } from "../schema/project-devices.js";
|
||||
import { projects } from "../schema/projects.js";
|
||||
import { toCircuitDeviceRowSnapshot } from "./circuit-device-row-structure.persistence.js";
|
||||
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
||||
import { resolveProjectDeviceVoltage } from "./project-voltage.persistence.js";
|
||||
|
||||
const circuitDeviceRowSnapshotFields = [
|
||||
"id",
|
||||
@@ -125,6 +126,19 @@ export class ProjectDeviceStructureProjectCommandRepository
|
||||
if (!project) {
|
||||
throw new Error("Project does not exist.");
|
||||
}
|
||||
if (
|
||||
source === "user" &&
|
||||
snapshot.voltageV !==
|
||||
resolveProjectDeviceVoltage(
|
||||
database,
|
||||
projectId,
|
||||
snapshot.phaseType
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
"Project-device voltage must match the project phase voltage."
|
||||
);
|
||||
}
|
||||
const existing = database
|
||||
.select({ id: projectDevices.id })
|
||||
.from(projectDevices)
|
||||
|
||||
@@ -15,6 +15,7 @@ import type { AppDatabase } from "../database-context.js";
|
||||
import { projects } from "../schema/projects.js";
|
||||
import { distributionBoards } from "../schema/distribution-boards.js";
|
||||
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
||||
import { updateAllDerivedProjectVoltages } from "./project-voltage.persistence.js";
|
||||
|
||||
export class ProjectSettingsProjectCommandRepository
|
||||
implements ProjectSettingsProjectCommandStore
|
||||
@@ -94,6 +95,7 @@ export class ProjectSettingsProjectCommandRepository
|
||||
if (updated.changes !== 1) {
|
||||
throw new Error("Project changed before settings update.");
|
||||
}
|
||||
updateAllDerivedProjectVoltages(tx, input.projectId);
|
||||
|
||||
return inverse;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
legacyProjectStateSnapshotSchemaVersion,
|
||||
previousProjectStateSnapshotSchemaVersion,
|
||||
projectStateSnapshotSchemaVersion,
|
||||
supplyTypesProjectStateSnapshotSchemaVersion,
|
||||
} from "../../domain/models/project-state-snapshot.model.js";
|
||||
import type {
|
||||
CreateNamedProjectSnapshotInput,
|
||||
@@ -146,6 +147,8 @@ export class ProjectSnapshotRepository implements ProjectSnapshotStore {
|
||||
stored.schemaVersion !== previousProjectStateSnapshotSchemaVersion &&
|
||||
stored.schemaVersion !==
|
||||
distributionBoardProjectStateSnapshotSchemaVersion &&
|
||||
stored.schemaVersion !==
|
||||
supplyTypesProjectStateSnapshotSchemaVersion &&
|
||||
stored.schemaVersion !== projectStateSnapshotSchemaVersion
|
||||
) {
|
||||
throw new Error("Project snapshot schema version is not supported.");
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import {
|
||||
resolveCircuitPhaseType,
|
||||
resolveProjectVoltage,
|
||||
type ElectricalPhaseType,
|
||||
type ProjectVoltageSettings,
|
||||
} from "../../domain/services/project-voltage.service.js";
|
||||
import type { AppDatabase } from "../database-context.js";
|
||||
import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
|
||||
import { circuitLists } from "../schema/circuit-lists.js";
|
||||
import { circuitSections } from "../schema/circuit-sections.js";
|
||||
import { circuits } from "../schema/circuits.js";
|
||||
import { projectDevices } from "../schema/project-devices.js";
|
||||
import { projects } from "../schema/projects.js";
|
||||
|
||||
export function readProjectVoltageSettings(
|
||||
database: AppDatabase,
|
||||
projectId: string
|
||||
): ProjectVoltageSettings {
|
||||
const project = database
|
||||
.select({
|
||||
singlePhaseVoltageV: projects.singlePhaseVoltageV,
|
||||
threePhaseVoltageV: projects.threePhaseVoltageV,
|
||||
})
|
||||
.from(projects)
|
||||
.where(eq(projects.id, projectId))
|
||||
.get();
|
||||
if (!project) {
|
||||
throw new Error("Project not found.");
|
||||
}
|
||||
return project;
|
||||
}
|
||||
|
||||
export function resolveProjectDeviceVoltage(
|
||||
database: AppDatabase,
|
||||
projectId: string,
|
||||
phaseType: ElectricalPhaseType
|
||||
) {
|
||||
return resolveProjectVoltage(
|
||||
phaseType,
|
||||
readProjectVoltageSettings(database, projectId)
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveCircuitVoltage(
|
||||
database: AppDatabase,
|
||||
projectId: string,
|
||||
sectionId: string,
|
||||
devicePhaseTypes: ReadonlyArray<string | null | undefined> = []
|
||||
) {
|
||||
const section = database
|
||||
.select({ key: circuitSections.key })
|
||||
.from(circuitSections)
|
||||
.innerJoin(
|
||||
circuitLists,
|
||||
eq(circuitLists.id, circuitSections.circuitListId)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(circuitSections.id, sectionId),
|
||||
eq(circuitLists.projectId, projectId)
|
||||
)
|
||||
)
|
||||
.get();
|
||||
if (!section) {
|
||||
throw new Error("Circuit section does not belong to project.");
|
||||
}
|
||||
return resolveProjectVoltage(
|
||||
resolveCircuitPhaseType(section.key, devicePhaseTypes),
|
||||
readProjectVoltageSettings(database, projectId)
|
||||
);
|
||||
}
|
||||
|
||||
export function updateDerivedCircuitVoltage(
|
||||
database: AppDatabase,
|
||||
projectId: string,
|
||||
circuitId: string
|
||||
) {
|
||||
const circuit = database
|
||||
.select({
|
||||
id: circuits.id,
|
||||
sectionId: circuits.sectionId,
|
||||
})
|
||||
.from(circuits)
|
||||
.innerJoin(circuitLists, eq(circuitLists.id, circuits.circuitListId))
|
||||
.where(
|
||||
and(
|
||||
eq(circuits.id, circuitId),
|
||||
eq(circuitLists.projectId, projectId)
|
||||
)
|
||||
)
|
||||
.get();
|
||||
if (!circuit) {
|
||||
throw new Error("Circuit does not belong to project.");
|
||||
}
|
||||
const devicePhaseTypes = database
|
||||
.select({ phaseType: circuitDeviceRows.phaseType })
|
||||
.from(circuitDeviceRows)
|
||||
.where(eq(circuitDeviceRows.circuitId, circuitId))
|
||||
.all()
|
||||
.map((row) => row.phaseType);
|
||||
const voltage = resolveCircuitVoltage(
|
||||
database,
|
||||
projectId,
|
||||
circuit.sectionId,
|
||||
devicePhaseTypes
|
||||
);
|
||||
database
|
||||
.update(circuits)
|
||||
.set({ voltage })
|
||||
.where(eq(circuits.id, circuitId))
|
||||
.run();
|
||||
return voltage;
|
||||
}
|
||||
|
||||
export function updateAllDerivedProjectVoltages(
|
||||
database: AppDatabase,
|
||||
projectId: string
|
||||
) {
|
||||
const settings = readProjectVoltageSettings(database, projectId);
|
||||
const devices = database
|
||||
.select({
|
||||
id: projectDevices.id,
|
||||
phaseType: projectDevices.phaseType,
|
||||
})
|
||||
.from(projectDevices)
|
||||
.where(eq(projectDevices.projectId, projectId))
|
||||
.all();
|
||||
for (const device of devices) {
|
||||
if (
|
||||
device.phaseType !== "single_phase" &&
|
||||
device.phaseType !== "three_phase"
|
||||
) {
|
||||
throw new Error("Persisted project-device phase type is invalid.");
|
||||
}
|
||||
database
|
||||
.update(projectDevices)
|
||||
.set({
|
||||
voltageV: resolveProjectVoltage(device.phaseType, settings),
|
||||
})
|
||||
.where(eq(projectDevices.id, device.id))
|
||||
.run();
|
||||
}
|
||||
|
||||
const projectCircuits = database
|
||||
.select({ id: circuits.id })
|
||||
.from(circuits)
|
||||
.innerJoin(circuitLists, eq(circuitLists.id, circuits.circuitListId))
|
||||
.where(eq(circuitLists.projectId, projectId))
|
||||
.all();
|
||||
for (const circuit of projectCircuits) {
|
||||
updateDerivedCircuitVoltage(database, projectId, circuit.id);
|
||||
}
|
||||
}
|
||||
@@ -58,6 +58,8 @@ export interface CircuitTreeSectionBlock {
|
||||
export interface CircuitTreeResponse {
|
||||
circuitListId: string;
|
||||
currentRevision: number;
|
||||
singlePhaseVoltageV: number;
|
||||
threePhaseVoltageV: number;
|
||||
sections: CircuitTreeSectionBlock[];
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,16 @@ import {
|
||||
defaultDistributionBoardSupplyTypes,
|
||||
distributionBoardSupplyTypes,
|
||||
} from "../../shared/constants/distribution-board.js";
|
||||
import {
|
||||
resolveCircuitPhaseType,
|
||||
resolveProjectVoltage,
|
||||
} from "../services/project-voltage.service.js";
|
||||
|
||||
export const legacyProjectStateSnapshotSchemaVersion = 1 as const;
|
||||
export const previousProjectStateSnapshotSchemaVersion = 2 as const;
|
||||
export const distributionBoardProjectStateSnapshotSchemaVersion = 3 as const;
|
||||
export const projectStateSnapshotSchemaVersion = 4 as const;
|
||||
export const supplyTypesProjectStateSnapshotSchemaVersion = 4 as const;
|
||||
export const projectStateSnapshotSchemaVersion = 5 as const;
|
||||
|
||||
const idSchema = z.string().trim().min(1);
|
||||
const nullableStringSchema = z.string().nullable();
|
||||
@@ -198,15 +203,20 @@ const distributionBoardProjectStateSnapshotSchema = z
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const projectStateSnapshotSchema = z
|
||||
const supplyTypesProjectStateSnapshotSchema = z
|
||||
.object({
|
||||
schemaVersion: z.literal(projectStateSnapshotSchemaVersion),
|
||||
schemaVersion: z.literal(supplyTypesProjectStateSnapshotSchemaVersion),
|
||||
project: currentProjectSchema,
|
||||
distributionBoards: z.array(distributionBoardSchema),
|
||||
...commonProjectStateSnapshotContents,
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const projectStateSnapshotSchema =
|
||||
supplyTypesProjectStateSnapshotSchema.extend({
|
||||
schemaVersion: z.literal(projectStateSnapshotSchemaVersion),
|
||||
});
|
||||
|
||||
export type ProjectStateSnapshot = z.infer<
|
||||
typeof projectStateSnapshotSchema
|
||||
>;
|
||||
@@ -217,23 +227,33 @@ export function parseProjectStateSnapshot(
|
||||
const version = isPlainObject(value) ? value.schemaVersion : undefined;
|
||||
const snapshot =
|
||||
version === legacyProjectStateSnapshotSchemaVersion
|
||||
? upgradeDistributionBoardProjectStateSnapshot(
|
||||
? upgradeSupplyTypesProjectStateSnapshot(
|
||||
upgradeDistributionBoardProjectStateSnapshot(
|
||||
upgradePreviousProjectStateSnapshot(
|
||||
upgradeLegacyProjectStateSnapshot(
|
||||
legacyProjectStateSnapshotSchema.parse(value)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
: version === previousProjectStateSnapshotSchemaVersion
|
||||
? upgradeDistributionBoardProjectStateSnapshot(
|
||||
? upgradeSupplyTypesProjectStateSnapshot(
|
||||
upgradeDistributionBoardProjectStateSnapshot(
|
||||
upgradePreviousProjectStateSnapshot(
|
||||
previousProjectStateSnapshotSchema.parse(value)
|
||||
)
|
||||
)
|
||||
)
|
||||
: version === distributionBoardProjectStateSnapshotSchemaVersion
|
||||
? upgradeDistributionBoardProjectStateSnapshot(
|
||||
? upgradeSupplyTypesProjectStateSnapshot(
|
||||
upgradeDistributionBoardProjectStateSnapshot(
|
||||
distributionBoardProjectStateSnapshotSchema.parse(value)
|
||||
)
|
||||
)
|
||||
: version === supplyTypesProjectStateSnapshotSchemaVersion
|
||||
? upgradeSupplyTypesProjectStateSnapshot(
|
||||
supplyTypesProjectStateSnapshotSchema.parse(value)
|
||||
)
|
||||
: projectStateSnapshotSchema.parse(value);
|
||||
assertProjectStateSnapshotRelations(snapshot);
|
||||
return snapshot;
|
||||
@@ -271,10 +291,10 @@ function upgradePreviousProjectStateSnapshot(
|
||||
|
||||
function upgradeDistributionBoardProjectStateSnapshot(
|
||||
snapshot: z.infer<typeof distributionBoardProjectStateSnapshotSchema>
|
||||
): ProjectStateSnapshot {
|
||||
): z.infer<typeof supplyTypesProjectStateSnapshotSchema> {
|
||||
return {
|
||||
...snapshot,
|
||||
schemaVersion: projectStateSnapshotSchemaVersion,
|
||||
schemaVersion: supplyTypesProjectStateSnapshotSchemaVersion,
|
||||
project: {
|
||||
...snapshot.project,
|
||||
enabledDistributionBoardSupplyTypes: [
|
||||
@@ -284,6 +304,37 @@ function upgradeDistributionBoardProjectStateSnapshot(
|
||||
};
|
||||
}
|
||||
|
||||
function upgradeSupplyTypesProjectStateSnapshot(
|
||||
snapshot: z.infer<typeof supplyTypesProjectStateSnapshotSchema>
|
||||
): ProjectStateSnapshot {
|
||||
const settings = snapshot.project;
|
||||
const sectionById = new Map(
|
||||
snapshot.circuitSections.map((section) => [section.id, section])
|
||||
);
|
||||
return {
|
||||
...snapshot,
|
||||
schemaVersion: projectStateSnapshotSchemaVersion,
|
||||
projectDevices: snapshot.projectDevices.map((device) => ({
|
||||
...device,
|
||||
voltageV: resolveProjectVoltage(device.phaseType, settings),
|
||||
})),
|
||||
circuits: snapshot.circuits.map((circuit) => {
|
||||
const section = sectionById.get(circuit.sectionId);
|
||||
if (!section) {
|
||||
return circuit;
|
||||
}
|
||||
const phaseType = resolveCircuitPhaseType(
|
||||
section.key,
|
||||
circuit.deviceRows.map((row) => row.phaseType)
|
||||
);
|
||||
return {
|
||||
...circuit,
|
||||
voltage: resolveProjectVoltage(phaseType, settings),
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
@@ -364,6 +415,14 @@ function assertProjectStateSnapshotRelations(
|
||||
}
|
||||
for (const device of snapshot.projectDevices) {
|
||||
assertProjectOwnership(device.projectId, projectId, "project device");
|
||||
if (
|
||||
device.voltageV !==
|
||||
resolveProjectVoltage(device.phaseType, snapshot.project)
|
||||
) {
|
||||
throw new Error(
|
||||
"Snapshot project device voltage must match project settings."
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const floor of snapshot.floors) {
|
||||
assertProjectOwnership(floor.projectId, projectId, "floor");
|
||||
@@ -394,6 +453,18 @@ function assertProjectStateSnapshotRelations(
|
||||
"Snapshot circuit reserve state must match its device rows."
|
||||
);
|
||||
}
|
||||
const expectedVoltage = resolveProjectVoltage(
|
||||
resolveCircuitPhaseType(
|
||||
section.key,
|
||||
circuit.deviceRows.map((row) => row.phaseType)
|
||||
),
|
||||
snapshot.project
|
||||
);
|
||||
if (circuit.voltage !== expectedVoltage) {
|
||||
throw new Error(
|
||||
"Snapshot circuit voltage must match project settings."
|
||||
);
|
||||
}
|
||||
for (const row of circuit.deviceRows) {
|
||||
if (row.circuitId !== circuit.id) {
|
||||
throw new Error(
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
export type ElectricalPhaseType = "single_phase" | "three_phase";
|
||||
|
||||
export interface ProjectVoltageSettings {
|
||||
singlePhaseVoltageV: number;
|
||||
threePhaseVoltageV: number;
|
||||
}
|
||||
|
||||
export function resolveProjectVoltage(
|
||||
phaseType: ElectricalPhaseType,
|
||||
settings: ProjectVoltageSettings
|
||||
) {
|
||||
return phaseType === "three_phase"
|
||||
? settings.threePhaseVoltageV
|
||||
: settings.singlePhaseVoltageV;
|
||||
}
|
||||
|
||||
export function resolveCircuitPhaseType(
|
||||
sectionKey: string,
|
||||
devicePhaseTypes: ReadonlyArray<string | null | undefined> = []
|
||||
): ElectricalPhaseType {
|
||||
if (sectionKey === "three_phase") {
|
||||
return "three_phase";
|
||||
}
|
||||
if (sectionKey === "lighting" || sectionKey === "single_phase") {
|
||||
return "single_phase";
|
||||
}
|
||||
const assignedPhaseTypes = devicePhaseTypes.filter(
|
||||
(phaseType): phaseType is ElectricalPhaseType =>
|
||||
phaseType === "single_phase" || phaseType === "three_phase"
|
||||
);
|
||||
return assignedPhaseTypes.length > 0 &&
|
||||
assignedPhaseTypes.every((phaseType) => phaseType === "three_phase")
|
||||
? "three_phase"
|
||||
: "single_phase";
|
||||
}
|
||||
@@ -5,6 +5,10 @@ import {
|
||||
inferProjectDeviceSectionKey,
|
||||
isProjectDevicePlacementValid,
|
||||
} from "../../domain/services/project-device-placement.service";
|
||||
import {
|
||||
resolveCircuitPhaseType,
|
||||
resolveProjectVoltage,
|
||||
} from "../../domain/services/project-voltage.service";
|
||||
import {
|
||||
getInsertionSortOrder,
|
||||
resolveGridInsertionIntent,
|
||||
@@ -819,6 +823,19 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
values: CreateCircuitInputDto,
|
||||
deviceRowValues: CreateCircuitDeviceRowInputDto[] = []
|
||||
) {
|
||||
const section = data?.sections.find(
|
||||
(entry) => entry.id === values.sectionId
|
||||
);
|
||||
if (!section || !data) {
|
||||
throw new Error("Bereich wurde nicht gefunden.");
|
||||
}
|
||||
const voltage = resolveProjectVoltage(
|
||||
resolveCircuitPhaseType(
|
||||
section.key,
|
||||
deviceRowValues.map((row) => row.phaseType)
|
||||
),
|
||||
data
|
||||
);
|
||||
const circuitId = crypto.randomUUID();
|
||||
const deviceRows = deviceRowValues.map((row, index) =>
|
||||
createDeviceRowSnapshot(circuitId, row, (index + 1) * 10)
|
||||
@@ -826,7 +843,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
return buildCircuitInsertSnapshot({
|
||||
id: circuitId,
|
||||
circuitListId,
|
||||
values,
|
||||
values: { ...values, voltage },
|
||||
deviceRows,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -47,7 +47,6 @@ export function ProjectDeviceModal({
|
||||
simultaneityFactor: Number(values.simultaneityFactor),
|
||||
cosPhi: optionalNumber(values.cosPhi),
|
||||
remark: optionalString(values.remark),
|
||||
voltageV: optionalNumber(values.voltageV),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -182,13 +181,6 @@ export function ProjectDeviceModal({
|
||||
step="0.01"
|
||||
value={values.cosPhi}
|
||||
/>
|
||||
<NumberField
|
||||
id="device-voltage"
|
||||
label="Spannung [V]"
|
||||
min="1"
|
||||
onChange={(value) => update("voltageV", value)}
|
||||
value={values.voltageV}
|
||||
/>
|
||||
<div className="col-12">
|
||||
<label className="form-label" htmlFor="device-remark">
|
||||
Bemerkung
|
||||
@@ -272,7 +264,6 @@ function toFormValues(device?: ProjectDeviceDto) {
|
||||
simultaneityFactor: String(device?.simultaneityFactor ?? 1),
|
||||
cosPhi: String(device?.cosPhi ?? 1),
|
||||
remark: device?.remark ?? "",
|
||||
voltageV: String(device?.voltageV ?? ""),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -187,7 +187,6 @@ export interface CreateGlobalDeviceInput {
|
||||
quantity: number;
|
||||
installedPowerPerUnitKw: number;
|
||||
demandFactor: number;
|
||||
voltageV?: number;
|
||||
phaseCount?: 1 | 3;
|
||||
powerFactor?: number;
|
||||
note?: string;
|
||||
@@ -205,7 +204,6 @@ export interface CreateProjectDeviceInput {
|
||||
simultaneityFactor: number;
|
||||
cosPhi?: number;
|
||||
remark?: string;
|
||||
voltageV?: number;
|
||||
}
|
||||
|
||||
export interface ProjectDeviceSyncDifferenceDto {
|
||||
@@ -314,6 +312,8 @@ export interface CircuitTreeMigrationReportDto {
|
||||
export interface CircuitTreeResponseDto {
|
||||
circuitListId: string;
|
||||
currentRevision: number;
|
||||
singlePhaseVoltageV: number;
|
||||
threePhaseVoltageV: number;
|
||||
sections: CircuitTreeSectionDto[];
|
||||
migrationReport?: CircuitTreeMigrationReportDto;
|
||||
}
|
||||
|
||||
@@ -161,7 +161,6 @@ const circuitFieldKeys = new Set<CellKey>([
|
||||
"cableSummary",
|
||||
"rcdAssignment",
|
||||
"terminalDesignation",
|
||||
"voltage",
|
||||
"controlRequirement",
|
||||
"status",
|
||||
"isReserve",
|
||||
@@ -393,6 +392,7 @@ export function compareSortValues(left: GridValue, right: GridValue): number {
|
||||
|
||||
export function getCellKind(rowType: RowType, key: CellKey): CellKind {
|
||||
if (key === "rowTotalPower" || key === "circuitTotalPower") return "computed";
|
||||
if (key === "voltage") return "readonly";
|
||||
if (rowType === "section") return "readonly";
|
||||
if (rowType === "placeholder") {
|
||||
if (deviceFieldKeys.has(key)) return "deviceField";
|
||||
|
||||
@@ -149,6 +149,8 @@ function makeVisibleGridRow(
|
||||
value = circuit.circuitTotalPower;
|
||||
} else if (column.key === "circuitTotalPower" && circuit) {
|
||||
value = circuit.circuitTotalPower;
|
||||
} else if (column.key === "voltage" && circuit) {
|
||||
value = circuit.voltage;
|
||||
}
|
||||
|
||||
return { cellKey: column.key, editable, kind, value };
|
||||
|
||||
@@ -55,6 +55,8 @@ export async function getCircuitTree(req: Request, res: Response) {
|
||||
const tree: CircuitTreeResponse = {
|
||||
circuitListId,
|
||||
currentRevision: project.currentRevision,
|
||||
singlePhaseVoltageV: project.singlePhaseVoltageV,
|
||||
threePhaseVoltageV: project.threePhaseVoltageV,
|
||||
sections: sections.map((section) => ({
|
||||
id: section.id,
|
||||
key: section.key,
|
||||
@@ -127,6 +129,8 @@ export async function getCircuitTree(req: Request, res: Response) {
|
||||
return res.json({
|
||||
circuitListId,
|
||||
currentRevision: project.currentRevision,
|
||||
singlePhaseVoltageV: project.singlePhaseVoltageV,
|
||||
threePhaseVoltageV: project.threePhaseVoltageV,
|
||||
sections: [],
|
||||
warning:
|
||||
"Circuit-first tables are not available yet. Run database migrations (including 0008_circuit_first_model).",
|
||||
|
||||
@@ -70,7 +70,6 @@ export async function copyProjectDeviceToGlobal(req: Request, res: Response) {
|
||||
quantity: source.quantity,
|
||||
installedPowerPerUnitKw: source.powerPerUnit,
|
||||
demandFactor: source.simultaneityFactor,
|
||||
voltageV: source.voltageV ?? undefined,
|
||||
phaseCount: source.phaseType === "three_phase" ? 3 : 1,
|
||||
powerFactor: source.cosPhi ?? undefined,
|
||||
note: source.remark ?? undefined,
|
||||
|
||||
@@ -11,6 +11,7 @@ import { projectCommandService } from "../composition/project-command-stores.js"
|
||||
import {
|
||||
globalDeviceRepository,
|
||||
projectDeviceRepository,
|
||||
projectRepository,
|
||||
} from "../composition/application-repositories.js";
|
||||
import {
|
||||
createProjectDeviceCommandSchema,
|
||||
@@ -21,6 +22,7 @@ import {
|
||||
} from "../../shared/validation/project-device.schemas.js";
|
||||
import type { CreateProjectDeviceInput } from "../../shared/validation/project-device.schemas.js";
|
||||
import { respondWithProjectCommandError } from "./project-command.controller.js";
|
||||
import { resolveProjectVoltage } from "../../domain/services/project-voltage.service.js";
|
||||
|
||||
export async function listProjectDevicesByProject(req: Request, res: Response) {
|
||||
const { projectId } = req.params;
|
||||
@@ -41,9 +43,18 @@ export async function createProjectDevice(req: Request, res: Response) {
|
||||
return res.status(400).json({ error: parsed.error.flatten() });
|
||||
}
|
||||
const { expectedRevision, ...input } = parsed.data;
|
||||
const projectDevice = toProjectDeviceSnapshot(projectId, randomUUID(), input);
|
||||
|
||||
try {
|
||||
const project = await projectRepository.findById(projectId);
|
||||
if (!project) {
|
||||
return res.status(404).json({ error: "Project not found" });
|
||||
}
|
||||
const projectDevice = toProjectDeviceSnapshot(
|
||||
projectId,
|
||||
randomUUID(),
|
||||
input,
|
||||
project
|
||||
);
|
||||
const result = projectCommandService.executeUser({
|
||||
projectId,
|
||||
expectedRevision,
|
||||
@@ -73,13 +84,17 @@ export async function updateProjectDevice(req: Request, res: Response) {
|
||||
const { expectedRevision, ...input } = parsed.data;
|
||||
|
||||
try {
|
||||
const project = await projectRepository.findById(projectId);
|
||||
if (!project) {
|
||||
return res.status(404).json({ error: "Project not found" });
|
||||
}
|
||||
const result = projectCommandService.executeUser({
|
||||
projectId,
|
||||
expectedRevision,
|
||||
description: "Projektgerät bearbeiten",
|
||||
command: createProjectDeviceUpdateProjectCommand(
|
||||
projectDeviceId,
|
||||
toProjectDeviceValues(input)
|
||||
toProjectDeviceValues(input, project)
|
||||
),
|
||||
});
|
||||
// Linked rows are synchronized only through the explicit review flow.
|
||||
@@ -133,6 +148,10 @@ export async function copyGlobalDeviceToProject(req: Request, res: Response) {
|
||||
if (!source) {
|
||||
return res.status(404).json({ error: "Global device not found" });
|
||||
}
|
||||
const project = await projectRepository.findById(projectId);
|
||||
if (!project) {
|
||||
return res.status(404).json({ error: "Project not found" });
|
||||
}
|
||||
|
||||
const projectDevice = toProjectDeviceSnapshot(projectId, randomUUID(), {
|
||||
name: source.name,
|
||||
@@ -144,8 +163,7 @@ export async function copyGlobalDeviceToProject(req: Request, res: Response) {
|
||||
simultaneityFactor: source.demandFactor,
|
||||
cosPhi: source.powerFactor ?? undefined,
|
||||
remark: source.note ?? undefined,
|
||||
voltageV: source.voltageV ?? undefined,
|
||||
});
|
||||
}, project);
|
||||
|
||||
try {
|
||||
const result = projectCommandService.executeUser({
|
||||
@@ -167,16 +185,26 @@ export async function copyGlobalDeviceToProject(req: Request, res: Response) {
|
||||
function toProjectDeviceSnapshot(
|
||||
projectId: string,
|
||||
id: string,
|
||||
input: CreateProjectDeviceInput
|
||||
input: CreateProjectDeviceInput,
|
||||
project: {
|
||||
singlePhaseVoltageV: number;
|
||||
threePhaseVoltageV: number;
|
||||
}
|
||||
): ProjectDeviceSnapshot {
|
||||
return {
|
||||
id,
|
||||
projectId,
|
||||
...toProjectDeviceValues(input),
|
||||
...toProjectDeviceValues(input, project),
|
||||
};
|
||||
}
|
||||
|
||||
function toProjectDeviceValues(input: CreateProjectDeviceInput) {
|
||||
function toProjectDeviceValues(
|
||||
input: CreateProjectDeviceInput,
|
||||
project: {
|
||||
singlePhaseVoltageV: number;
|
||||
threePhaseVoltageV: number;
|
||||
}
|
||||
) {
|
||||
return {
|
||||
name: input.name,
|
||||
displayName: input.displayName,
|
||||
@@ -189,7 +217,7 @@ function toProjectDeviceValues(input: CreateProjectDeviceInput) {
|
||||
simultaneityFactor: input.simultaneityFactor,
|
||||
cosPhi: input.cosPhi ?? null,
|
||||
remark: input.remark ?? null,
|
||||
voltageV: input.voltageV ?? null,
|
||||
voltageV: resolveProjectVoltage(input.phaseType, project),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -7,11 +7,10 @@ export const createGlobalDeviceSchema = z.object({
|
||||
quantity: z.number().min(0),
|
||||
installedPowerPerUnitKw: z.number().min(0),
|
||||
demandFactor: z.number().min(0).max(1),
|
||||
voltageV: z.number().positive().optional(),
|
||||
phaseCount: z.union([z.literal(1), z.literal(3)]).optional(),
|
||||
powerFactor: z.number().min(0).max(1).optional(),
|
||||
note: z.string().optional(),
|
||||
});
|
||||
}).strict();
|
||||
|
||||
export const updateGlobalDeviceSchema = createGlobalDeviceSchema;
|
||||
|
||||
|
||||
@@ -14,9 +14,7 @@ export const createProjectDeviceSchema = z.object({
|
||||
simultaneityFactor: z.number().min(0).max(1),
|
||||
cosPhi: z.number().min(0).max(1).optional(),
|
||||
remark: z.string().optional(),
|
||||
// Transitional metadata used when importing from the legacy global-device library.
|
||||
voltageV: z.number().positive().optional(),
|
||||
});
|
||||
}).strict();
|
||||
|
||||
export const updateProjectDeviceSchema = createProjectDeviceSchema;
|
||||
|
||||
|
||||
@@ -4,6 +4,10 @@ import {
|
||||
calculateCircuitTotalPower,
|
||||
calculateRowTotalPower,
|
||||
} from "../src/domain/calculations/circuit-power-calculation.js";
|
||||
import {
|
||||
resolveCircuitPhaseType,
|
||||
resolveProjectVoltage,
|
||||
} from "../src/domain/services/project-voltage.service.js";
|
||||
|
||||
describe("circuit power calculation", () => {
|
||||
it("calculates row and circuit totals from device rows", () => {
|
||||
@@ -16,3 +20,28 @@ describe("circuit power calculation", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("project voltage derivation", () => {
|
||||
const settings = {
|
||||
singlePhaseVoltageV: 240,
|
||||
threePhaseVoltageV: 415,
|
||||
};
|
||||
|
||||
it("derives device voltage exclusively from phase and project settings", () => {
|
||||
assert.equal(resolveProjectVoltage("single_phase", settings), 240);
|
||||
assert.equal(resolveProjectVoltage("three_phase", settings), 415);
|
||||
});
|
||||
|
||||
it("uses section phase and handles unassigned circuits conservatively", () => {
|
||||
assert.equal(resolveCircuitPhaseType("lighting", ["three_phase"]), "single_phase");
|
||||
assert.equal(resolveCircuitPhaseType("single_phase", ["three_phase"]), "single_phase");
|
||||
assert.equal(resolveCircuitPhaseType("three_phase", ["single_phase"]), "three_phase");
|
||||
assert.equal(resolveCircuitPhaseType("unassigned", ["three_phase"]), "three_phase");
|
||||
assert.equal(
|
||||
resolveCircuitPhaseType("unassigned", ["three_phase", "single_phase"]),
|
||||
"single_phase"
|
||||
);
|
||||
assert.equal(resolveCircuitPhaseType("unassigned", []), "single_phase");
|
||||
assert.equal(resolveCircuitPhaseType("unassigned", [null]), "single_phase");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ describe("circuit project-command repository", () => {
|
||||
const circuit = getCircuit(fixture.context);
|
||||
assert.equal(circuit.displayName, null);
|
||||
assert.equal(circuit.protectionRatedCurrent, 16);
|
||||
assert.equal(circuit.voltage, 400);
|
||||
assert.equal(circuit.voltage, 230);
|
||||
assert.equal(circuit.controlRequirement, "DALI");
|
||||
assert.equal(circuit.isReserve, 1);
|
||||
assert.equal(executed.revision.revisionNumber, 1);
|
||||
@@ -138,7 +138,6 @@ describe("circuit project-command repository", () => {
|
||||
changes: [
|
||||
{ field: "displayName", value: "Licht Bestand" },
|
||||
{ field: "protectionRatedCurrent", value: 10 },
|
||||
{ field: "voltage", value: 230 },
|
||||
{ field: "controlRequirement", value: "none" },
|
||||
{ field: "isReserve", value: false },
|
||||
],
|
||||
@@ -149,7 +148,15 @@ describe("circuit project-command repository", () => {
|
||||
.from(projectChangeSets)
|
||||
.get();
|
||||
assert.ok(changeSet);
|
||||
assert.deepEqual(deserializeProjectCommand(changeSet.forwardPayloadJson), command);
|
||||
assert.deepEqual(
|
||||
deserializeProjectCommand(changeSet.forwardPayloadJson),
|
||||
createCircuitUpdateProjectCommand("circuit-1", {
|
||||
displayName: null,
|
||||
protectionRatedCurrent: 16,
|
||||
controlRequirement: "DALI",
|
||||
isReserve: true,
|
||||
})
|
||||
);
|
||||
assert.deepEqual(
|
||||
deserializeProjectCommand(changeSet.inversePayloadJson),
|
||||
executed.inverse
|
||||
|
||||
@@ -41,6 +41,20 @@ describe("project device circuit-first schema", () => {
|
||||
assert.equal(result.success, false);
|
||||
});
|
||||
|
||||
it("rejects manually supplied device voltages", () => {
|
||||
const result = createProjectDeviceSchema.safeParse({
|
||||
name: "Pumpe",
|
||||
displayName: "Pumpe",
|
||||
phaseType: "three_phase",
|
||||
quantity: 1,
|
||||
powerPerUnit: 2,
|
||||
simultaneityFactor: 1,
|
||||
voltageV: 500,
|
||||
});
|
||||
|
||||
assert.equal(result.success, false);
|
||||
});
|
||||
|
||||
it("requires a project revision for create and update commands", () => {
|
||||
const device = {
|
||||
name: "E-Line Pro",
|
||||
|
||||
@@ -12,6 +12,11 @@ import { ProjectSettingsProjectCommandRepository } from "../src/db/repositories/
|
||||
import { projectRevisions } from "../src/db/schema/project-revisions.js";
|
||||
import { projects } from "../src/db/schema/projects.js";
|
||||
import { distributionBoards } from "../src/db/schema/distribution-boards.js";
|
||||
import { circuitLists } from "../src/db/schema/circuit-lists.js";
|
||||
import { circuitSections } from "../src/db/schema/circuit-sections.js";
|
||||
import { circuits } from "../src/db/schema/circuits.js";
|
||||
import { projectDevices } from "../src/db/schema/project-devices.js";
|
||||
import { DistributionBoardFixtureRepository } from "./support/distribution-board-fixture.js";
|
||||
import { ProjectRevisionConflictError } from "../src/domain/errors/project-revision-conflict.error.js";
|
||||
import { createProjectSettingsUpdateProjectCommand } from "../src/domain/models/project-settings-project-command.model.js";
|
||||
import { updateProjectSettings } from "../src/frontend/utils/api.js";
|
||||
@@ -78,6 +83,86 @@ function settings(
|
||||
};
|
||||
}
|
||||
|
||||
function seedDerivedVoltageRecords(context: DatabaseContext) {
|
||||
context.db.insert(projectDevices).values([
|
||||
{
|
||||
id: "device-single",
|
||||
projectId: "project-1",
|
||||
name: "Leuchte",
|
||||
displayName: "Leuchte",
|
||||
phaseType: "single_phase",
|
||||
quantity: 1,
|
||||
powerPerUnit: 0.1,
|
||||
simultaneityFactor: 1,
|
||||
voltageV: 230,
|
||||
},
|
||||
{
|
||||
id: "device-three",
|
||||
projectId: "project-1",
|
||||
name: "Pumpe",
|
||||
displayName: "Pumpe",
|
||||
phaseType: "three_phase",
|
||||
quantity: 1,
|
||||
powerPerUnit: 2,
|
||||
simultaneityFactor: 1,
|
||||
voltageV: 400,
|
||||
},
|
||||
]).run();
|
||||
const board = new DistributionBoardFixtureRepository(
|
||||
context.db
|
||||
).createWithCircuitListAndDefaultSections("project-1", "UV-01");
|
||||
const list = context.db
|
||||
.select()
|
||||
.from(circuitLists)
|
||||
.where(eq(circuitLists.distributionBoardId, board.id))
|
||||
.get();
|
||||
assert.ok(list);
|
||||
const sections = context.db
|
||||
.select()
|
||||
.from(circuitSections)
|
||||
.where(eq(circuitSections.circuitListId, list.id))
|
||||
.all();
|
||||
const singleSection = sections.find((section) => section.key === "single_phase");
|
||||
const threeSection = sections.find((section) => section.key === "three_phase");
|
||||
assert.ok(singleSection);
|
||||
assert.ok(threeSection);
|
||||
context.db.insert(circuits).values([
|
||||
{
|
||||
id: "circuit-single",
|
||||
circuitListId: list.id,
|
||||
sectionId: singleSection.id,
|
||||
equipmentIdentifier: "-2F1",
|
||||
sortOrder: 10,
|
||||
voltage: 230,
|
||||
isReserve: 1,
|
||||
},
|
||||
{
|
||||
id: "circuit-three",
|
||||
circuitListId: list.id,
|
||||
sectionId: threeSection.id,
|
||||
equipmentIdentifier: "-3F1",
|
||||
sortOrder: 10,
|
||||
voltage: 400,
|
||||
isReserve: 1,
|
||||
},
|
||||
]).run();
|
||||
}
|
||||
|
||||
function getDerivedVoltages(context: DatabaseContext) {
|
||||
return {
|
||||
devices: context.db
|
||||
.select({ id: projectDevices.id, voltageV: projectDevices.voltageV })
|
||||
.from(projectDevices)
|
||||
.all()
|
||||
.sort((left, right) => left.id.localeCompare(right.id)),
|
||||
circuits: context.db
|
||||
.select({ id: circuits.id, voltage: circuits.voltage })
|
||||
.from(circuits)
|
||||
.all()
|
||||
.sort((left, right) => left.id.localeCompare(right.id)),
|
||||
};
|
||||
}
|
||||
|
||||
describe("project settings project-command repository", () => {
|
||||
it("validates settings commands and sends the expected revision from the frontend", async () => {
|
||||
assert.throws(
|
||||
@@ -137,6 +222,7 @@ describe("project settings project-command repository", () => {
|
||||
it("updates both voltages and supports persisted undo and redo", () => {
|
||||
const context = createTestDatabase();
|
||||
try {
|
||||
seedDerivedVoltageRecords(context);
|
||||
const repository = new ProjectSettingsProjectCommandRepository(
|
||||
context.db
|
||||
);
|
||||
@@ -171,6 +257,16 @@ describe("project settings project-command repository", () => {
|
||||
}),
|
||||
currentRevision: 1,
|
||||
});
|
||||
assert.deepEqual(getDerivedVoltages(context), {
|
||||
devices: [
|
||||
{ id: "device-single", voltageV: 240 },
|
||||
{ id: "device-three", voltageV: 415 },
|
||||
],
|
||||
circuits: [
|
||||
{ id: "circuit-single", voltage: 240 },
|
||||
{ id: "circuit-three", voltage: 415 },
|
||||
],
|
||||
});
|
||||
assert.deepEqual(forward.inverse.payload, {
|
||||
name: "Testprojekt",
|
||||
internalProjectNumber: "INT-1",
|
||||
@@ -216,6 +312,16 @@ describe("project settings project-command repository", () => {
|
||||
],
|
||||
currentRevision: 2,
|
||||
});
|
||||
assert.deepEqual(getDerivedVoltages(context), {
|
||||
devices: [
|
||||
{ id: "device-single", voltageV: 230 },
|
||||
{ id: "device-three", voltageV: 400 },
|
||||
],
|
||||
circuits: [
|
||||
{ id: "circuit-single", voltage: 230 },
|
||||
{ id: "circuit-three", voltage: 400 },
|
||||
],
|
||||
});
|
||||
|
||||
const redoStep = history.getNextCommand("project-1", "redo");
|
||||
assert.ok(redoStep);
|
||||
@@ -244,6 +350,16 @@ describe("project settings project-command repository", () => {
|
||||
],
|
||||
currentRevision: 3,
|
||||
});
|
||||
assert.deepEqual(getDerivedVoltages(context), {
|
||||
devices: [
|
||||
{ id: "device-single", voltageV: 240 },
|
||||
{ id: "device-three", voltageV: 415 },
|
||||
],
|
||||
circuits: [
|
||||
{ id: "circuit-single", voltage: 240 },
|
||||
{ id: "circuit-three", voltage: 415 },
|
||||
],
|
||||
});
|
||||
} finally {
|
||||
context.close();
|
||||
}
|
||||
|
||||
@@ -101,6 +101,7 @@ function createTestDatabase(): DatabaseContext {
|
||||
quantity: 1,
|
||||
powerPerUnit: 2.5,
|
||||
simultaneityFactor: 0.8,
|
||||
voltageV: 400,
|
||||
})
|
||||
.run();
|
||||
context.db
|
||||
@@ -112,6 +113,7 @@ function createTestDatabase(): DatabaseContext {
|
||||
equipmentIdentifier: "-1F1",
|
||||
displayName: "Ausgang",
|
||||
sortOrder: 10,
|
||||
voltage: 230,
|
||||
isReserve: 0,
|
||||
})
|
||||
.run();
|
||||
@@ -165,7 +167,10 @@ function changeCompleteProjectState(context: DatabaseContext) {
|
||||
.run();
|
||||
context.db
|
||||
.update(circuits)
|
||||
.set({ displayName: "Geänderter Stromkreis" })
|
||||
.set({
|
||||
displayName: "Geänderter Stromkreis",
|
||||
voltage: 240,
|
||||
})
|
||||
.where(eq(circuits.id, "circuit-1"))
|
||||
.run();
|
||||
context.db
|
||||
@@ -198,8 +203,14 @@ function changeCompleteProjectState(context: DatabaseContext) {
|
||||
quantity: 2,
|
||||
powerPerUnit: 0.05,
|
||||
simultaneityFactor: 1,
|
||||
voltageV: 240,
|
||||
})
|
||||
.run();
|
||||
context.db
|
||||
.update(projectDevices)
|
||||
.set({ voltageV: 415 })
|
||||
.where(eq(projectDevices.id, "device-1"))
|
||||
.run();
|
||||
new DistributionBoardFixtureRepository(
|
||||
context.db
|
||||
).createWithCircuitListAndDefaultSections("project-1", "UV-02");
|
||||
|
||||
@@ -128,6 +128,36 @@ describe("project state snapshot model", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("normalizes version-four device voltages to project settings", () => {
|
||||
const previous = {
|
||||
...minimalSnapshot(),
|
||||
schemaVersion: 4 as const,
|
||||
projectDevices: [
|
||||
{
|
||||
id: "device-1",
|
||||
projectId: "project-1",
|
||||
name: "Pumpe",
|
||||
displayName: "Pumpe",
|
||||
phaseType: "three_phase" as const,
|
||||
connectionKind: null,
|
||||
costGroup: null,
|
||||
category: null,
|
||||
quantity: 1,
|
||||
powerPerUnit: 2,
|
||||
simultaneityFactor: 1,
|
||||
cosPhi: null,
|
||||
remark: null,
|
||||
voltageV: 500,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const snapshot = parseProjectStateSnapshot(previous);
|
||||
|
||||
assert.equal(snapshot.schemaVersion, projectStateSnapshotSchemaVersion);
|
||||
assert.equal(snapshot.projectDevices[0]?.voltageV, 400);
|
||||
});
|
||||
|
||||
it("rejects duplicate ids and cross-project ownership", () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
|
||||
@@ -156,11 +156,11 @@ describe("project device modal presentation", () => {
|
||||
"Anzahl",
|
||||
"Leistung je Stück [kW]",
|
||||
"Gleichzeitigkeitsfaktor",
|
||||
"Spannung [V]",
|
||||
"Bemerkung",
|
||||
]) {
|
||||
assert.match(markup, new RegExp(label.replace("[", "\\[").replace("]", "\\]")));
|
||||
}
|
||||
assert.doesNotMatch(markup, /Spannung \[V\]/);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user