Derive device voltages from project settings

This commit is contained in:
2026-07-29 09:53:58 +02:00
parent b1a11397b3
commit 084103bf54
39 changed files with 2696 additions and 76 deletions
+5 -1
View File
@@ -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
+13 -7
View File
@@ -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
-5
View File
@@ -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
+7
View File
@@ -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);
}
}
+2
View File
@@ -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(
upgradePreviousProjectStateSnapshot(
upgradeLegacyProjectStateSnapshot(
legacyProjectStateSnapshotSchema.parse(value)
? upgradeSupplyTypesProjectStateSnapshot(
upgradeDistributionBoardProjectStateSnapshot(
upgradePreviousProjectStateSnapshot(
upgradeLegacyProjectStateSnapshot(
legacyProjectStateSnapshotSchema.parse(value)
)
)
)
)
: version === previousProjectStateSnapshotSchemaVersion
? upgradeDistributionBoardProjectStateSnapshot(
upgradePreviousProjectStateSnapshot(
previousProjectStateSnapshotSchema.parse(value)
? upgradeSupplyTypesProjectStateSnapshot(
upgradeDistributionBoardProjectStateSnapshot(
upgradePreviousProjectStateSnapshot(
previousProjectStateSnapshotSchema.parse(value)
)
)
)
: version === distributionBoardProjectStateSnapshotSchemaVersion
? upgradeDistributionBoardProjectStateSnapshot(
distributionBoardProjectStateSnapshotSchema.parse(value)
? 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 ?? ""),
};
}
+2 -2
View File
@@ -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;
}
+1 -1
View File
@@ -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;
+29
View File
@@ -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
+14
View File
@@ -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");
+30
View File
@@ -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(
() =>
+1 -1
View File
@@ -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\]/);
});
});