Add distribution power summaries

This commit is contained in:
2026-07-29 19:02:18 +02:00
parent dcb303284c
commit 9ea081179d
28 changed files with 2668 additions and 77 deletions
+19 -6
View File
@@ -184,6 +184,15 @@ Stromkreisleistungen in einer gemeinsamen Spalte `Gesamtsumme`: Gerätezeilen
verwenden `rowTotalPower`, Stromkreis-Sammelzeilen `circuitTotalPower`.
Zahlen werden ausschließlich für die Anzeige deutsch und begrenzt formatiert;
gespeicherte Werte und Bearbeitungsentwürfe behalten ihre volle Genauigkeit.
Jeder Abschnitt liefert und zeigt seine aufsummierte Stromkreisleistung. Die
Stromkreisliste zeigt außerdem die ungefilterte Gesamtleistung des Verteilers,
den am Verteiler gespeicherten Gleichzeitigkeitsfaktor und die daraus
abgeleitete Gesamtleistung unter Berücksichtigung dieses Faktors. Sortierung
und Filter verändern diese fachlichen Summen nicht.
Reihenfolge und Sichtbarkeit der Grid-Spalten sind reine UI-Präferenzen. Sie
werden im Browser unter einem projektspezifischen Schlüssel gespeichert und
deshalb beim Wechsel zwischen Verteilern desselben Projekts wiederverwendet,
ohne Projektrevisionen oder fachliche Snapshots zu erzeugen.
`circuit.reorder-section` speichert die erwartete und neue Sortierposition
jedes Stromkreises eines vollständigen Abschnitts. Forward, Undo und Redo
ändern ausschließlich `sortOrder`; Stromkreisblöcke, Gerätezeilen und BMKs
@@ -271,12 +280,16 @@ Controller-Schreibweg ist entfernt. Verteilungen besitzen eine optionale
Etagenreferenz sowie eine Netzart aus dem Projektkatalog. Anlage und
nachträgliche Bearbeitung prüfen die Projektzugehörigkeit der Etage und die
Freigabe der Netzart in den Projekteinstellungen.
`distribution-board.update` versioniert Etage und Netzart gemeinsam und stellt
beide Werte über dauerhaftes Undo/Redo wieder her. Snapshot-Schema 4 und der
portable Projekttransfer enthalten diese Felder; Schema 1/2 sowie gespeicherte
Version-1-Strukturcommands werden ohne erfundene Zuordnung hochgestuft.
Snapshot-Schema 3 wird mit allen sechs Netzarten als Projektauswahl
hochgestuft.
`distribution-board.update` versioniert Etage, Netzart und den
verteilerweiten Gleichzeitigkeitsfaktor gemeinsam und stellt alle Werte über
dauerhaftes Undo/Redo wieder her. Der Faktor liegt zwischen `0` und `1` und
ist für bestehende sowie neu angelegte Verteilungen standardmäßig `1`.
Snapshot-Schema 6 und der portable Projekttransfer enthalten diesen Wert;
Schema 5 und älter werden mit dem neutralen Faktor `1` hochgestuft.
Snapshot-Schema 4 enthält bereits Etage und Netzart; Schema 1/2 sowie
gespeicherte Version-1-Strukturcommands werden ohne erfundene Zuordnung
hochgestuft. Snapshot-Schema 3 wird mit allen sechs Netzarten als
Projektauswahl hochgestuft.
`project-floor.insert` und `project-room.insert` versionieren die Anlage von
Geschossen und Räumen mit stabilen UUIDs. Die vollständigen Datensätze bilden
jeweils die persistierte Inverse für Undo/Redo. Ein Geschoss wird durch Undo nur
+13
View File
@@ -52,11 +52,24 @@ requirements and intended sequencing, not proof of implementation.
stored values. Version-four and older imports remain supported and are
normalized while being upgraded.
## Circuit List Power Summary
- [x] Show the total power of every circuit section.
- [x] Show the unadjusted total power of the complete distribution board.
- [x] Store a distribution-board simultaneity factor between zero and one in
the distribution-board settings.
- [x] Show the distribution-board total after applying that factor.
- [x] Preserve the factor in project commands, snapshots and portable project
transfers.
- [x] Keep column visibility and order as a project-specific UI preference so
all distribution boards in the same project open with the same columns.
## Recommended Sequence
1. [x] Distribution-board floor and supply fields.
2. [x] Device voltage derivation without overrides.
3. [x] Snapshot-to-revision descriptions and history presentation.
4. [x] Distribution-board power summary and project column layout.
The Revit/CSV/IFCGUID round-trip remains a separate jointly planned phase and
must not be inferred from these tasks.
+45
View File
@@ -116,6 +116,39 @@ body {
font-weight: 600;
}
.distribution-power-summary {
display: grid;
grid-template-columns: repeat(3, minmax(12rem, 1fr));
gap: 0.5rem;
padding: 0.55rem;
border: 1px solid #cbd5e1;
border-radius: 5px;
background: #f8fafc;
}
.distribution-power-summary > div {
display: flex;
flex-direction: column;
gap: 0.15rem;
}
.distribution-power-summary span {
color: #475569;
font-size: 0.74rem;
line-height: 1.2;
}
.distribution-power-summary strong {
color: #172033;
font-size: 0.95rem;
}
@media (max-width: 900px) {
.distribution-power-summary {
grid-template-columns: 1fr;
}
}
.column-settings-menu {
position: absolute;
z-index: 9;
@@ -695,6 +728,18 @@ body {
outline-offset: -2px;
}
.tree-grid .section-title {
display: flex;
align-items: baseline;
gap: 0.75rem;
}
.tree-grid .section-title span {
color: #475569;
font-size: 0.78rem;
font-weight: 500;
}
.tree-grid .cell-invalid {
outline: 2px solid #c2410c;
outline-offset: -2px;
+65 -2
View File
@@ -87,6 +87,10 @@ export default function ProjectDetailPage() {
const [editingBoardFloorId, setEditingBoardFloorId] = useState("");
const [editingBoardSupplyType, setEditingBoardSupplyType] =
useState<DistributionBoardSupplyType>("AV");
const [
editingBoardSimultaneityFactor,
setEditingBoardSimultaneityFactor,
] = useState("1");
const [floorName, setFloorName] = useState("");
const [roomNumber, setRoomNumber] = useState("");
const [roomName, setRoomName] = useState("");
@@ -398,6 +402,9 @@ export default function ProjectDetailPage() {
project?.enabledDistributionBoardSupplyTypes[0] ??
"AV"
);
setEditingBoardSimultaneityFactor(
String(board.simultaneityFactor)
);
}
function openBoardCreator() {
@@ -412,6 +419,17 @@ export default function ProjectDetailPage() {
if (!projectId || !project || !editingBoard) {
return;
}
const simultaneityFactor = Number(
editingBoardSimultaneityFactor
);
if (
!editingBoardSimultaneityFactor.trim() ||
!Number.isFinite(simultaneityFactor) ||
simultaneityFactor < 0 ||
simultaneityFactor > 1
) {
return;
}
setIsSaving(true);
setError(null);
try {
@@ -421,6 +439,7 @@ export default function ProjectDetailPage() {
{
floorId: editingBoardFloorId || null,
supplyType: editingBoardSupplyType,
simultaneityFactor,
},
project.currentRevision
);
@@ -702,6 +721,7 @@ export default function ProjectDetailPage() {
<th>Verteilung</th>
<th>Etage</th>
<th>Netzart</th>
<th>GZF</th>
<th className="text-end">Aktionen</th>
</tr>
</thead>
@@ -723,6 +743,15 @@ export default function ProjectDetailPage() {
]
: "Nicht festgelegt"}
</td>
<td>
{board.simultaneityFactor.toLocaleString(
"de-DE",
{
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}
)}
</td>
<td className="text-end">
<div className="d-inline-flex flex-wrap justify-content-end gap-2">
<button
@@ -752,7 +781,7 @@ export default function ProjectDetailPage() {
})}
{!boards.length ? (
<tr>
<td colSpan={4} className="text-center text-secondary py-4">
<td colSpan={5} className="text-center text-secondary py-4">
Noch keine Verteilungen vorhanden.
</td>
</tr>
@@ -1131,10 +1160,18 @@ export default function ProjectDetailPage() {
{editingBoard ? (
<FormModal
description={`${editingBoard.name}: Etagenzuordnung und Netzart ändern.`}
description={`${editingBoard.name}: Etagenzuordnung, Netzart und Gleichzeitigkeitsfaktor ändern.`}
isSaving={isSaving}
onClose={() => setEditingBoard(null)}
onSubmit={handleUpdateBoard}
submitDisabled={
!editingBoardSimultaneityFactor.trim() ||
!Number.isFinite(
Number(editingBoardSimultaneityFactor)
) ||
Number(editingBoardSimultaneityFactor) < 0 ||
Number(editingBoardSimultaneityFactor) > 1
}
submitLabel="Änderungen speichern"
title="Verteilung bearbeiten"
>
@@ -1187,6 +1224,32 @@ export default function ProjectDetailPage() {
))}
</select>
</div>
<div className="col-12 col-md-6">
<label
className="form-label"
htmlFor="edit-distribution-board-simultaneity-factor"
>
Verteilerweiter Gleichzeitigkeitsfaktor
</label>
<input
className="form-control"
id="edit-distribution-board-simultaneity-factor"
max="1"
min="0"
onChange={(event) =>
setEditingBoardSimultaneityFactor(
event.target.value
)
}
required
step="0.01"
type="number"
value={editingBoardSimultaneityFactor}
/>
<div className="form-text">
Wird auf die Gesamtleistung des Verteilers angewendet.
</div>
</div>
</div>
</FormModal>
) : null}
@@ -0,0 +1 @@
ALTER TABLE `distribution_boards` ADD `simultaneity_factor` real DEFAULT 1 NOT NULL;
File diff suppressed because it is too large Load Diff
+7
View File
@@ -162,6 +162,13 @@
"when": 1785314626476,
"tag": "0022_default_remaining_phase_types",
"breakpoints": true
},
{
"idx": 23,
"version": "6",
"when": 1785343645615,
"tag": "0023_cheerful_night_thrasher",
"breakpoints": true
}
]
}
@@ -14,6 +14,7 @@ import {
import {
assertDistributionBoardUpdateProjectCommand,
createDistributionBoardUpdateProjectCommand,
legacyDistributionBoardUpdateCommandSchemaVersion,
type DistributionBoardUpdateField,
type DistributionBoardUpdatePatch,
type DistributionBoardUpdateProjectCommand,
@@ -303,10 +304,19 @@ export class DistributionBoardStructureProjectCommandRepository
getDistributionBoardFieldValue(current, change.field),
])
) as DistributionBoardUpdatePatch;
const inverse = createDistributionBoardUpdateProjectCommand(
const currentInverse = createDistributionBoardUpdateProjectCommand(
current.id,
inversePatch
);
const inverse =
input.command.schemaVersion ===
legacyDistributionBoardUpdateCommandSchemaVersion
? ({
...currentInverse,
schemaVersion:
legacyDistributionBoardUpdateCommandSchemaVersion,
} as DistributionBoardUpdateProjectCommand)
: currentInverse;
const updated = database
.update(distributionBoards)
.set(patch)
@@ -379,8 +389,16 @@ function structureMatches(
sections: Array<typeof circuitSections.$inferSelect>;
}
) {
const {
simultaneityFactor,
...actualDistributionBoard
} = actual.board;
if (
!sameRecord(expected.distributionBoard, actual.board) ||
simultaneityFactor !== 1 ||
!sameRecord(
expected.distributionBoard,
actualDistributionBoard
) ||
!sameRecord(expected.circuitList, actual.list) ||
expected.sections.length !== actual.sections.length
) {
@@ -1,4 +1,4 @@
import { eq } from "drizzle-orm";
import { and, eq } from "drizzle-orm";
import type { AppDatabase } from "../database-context.js";
import { distributionBoards } from "../schema/distribution-boards.js";
@@ -12,4 +12,19 @@ export class DistributionBoardRepository {
.where(eq(distributionBoards.projectId, projectId));
}
async findById(projectId: string, distributionBoardId: string) {
return (
this.database
.select()
.from(distributionBoards)
.where(
and(
eq(distributionBoards.projectId, projectId),
eq(distributionBoards.id, distributionBoardId)
)
)
.get() ?? null
);
}
}
@@ -10,6 +10,7 @@ import {
previousProjectStateSnapshotSchemaVersion,
projectStateSnapshotSchemaVersion,
supplyTypesProjectStateSnapshotSchemaVersion,
voltageProjectStateSnapshotSchemaVersion,
} from "../../domain/models/project-state-snapshot.model.js";
import type {
CreateNamedProjectSnapshotInput,
@@ -149,6 +150,8 @@ export class ProjectSnapshotRepository implements ProjectSnapshotStore {
distributionBoardProjectStateSnapshotSchemaVersion &&
stored.schemaVersion !==
supplyTypesProjectStateSnapshotSchemaVersion &&
stored.schemaVersion !==
voltageProjectStateSnapshotSchemaVersion &&
stored.schemaVersion !== projectStateSnapshotSchemaVersion
) {
throw new Error("Project snapshot schema version is not supported.");
+4 -1
View File
@@ -1,4 +1,4 @@
import { sqliteTable, text } from "drizzle-orm/sqlite-core";
import { real, sqliteTable, text } from "drizzle-orm/sqlite-core";
import { floors } from "./floors.js";
import { projects } from "./projects.js";
import type { DistributionBoardSupplyType } from "../../shared/constants/distribution-board.js";
@@ -13,5 +13,8 @@ export const distributionBoards = sqliteTable("distribution_boards", {
onDelete: "set null",
}),
supplyType: text("supply_type").$type<DistributionBoardSupplyType>(),
simultaneityFactor: real("simultaneity_factor")
.notNull()
.default(1),
});
@@ -15,3 +15,37 @@ export function calculateCircuitTotalPower(
);
}
export function calculateSectionTotalPower(
circuits: Array<{ circuitTotalPower: number }>
): number {
return circuits.reduce(
(sum, circuit) => sum + circuit.circuitTotalPower,
0
);
}
export function calculateDistributionBoardTotalPower(
sections: Array<{ sectionTotalPower: number }>
): number {
return sections.reduce(
(sum, section) => sum + section.sectionTotalPower,
0
);
}
export function applyDistributionBoardSimultaneityFactor(
totalPower: number,
simultaneityFactor: number
): number {
if (
!Number.isFinite(simultaneityFactor) ||
simultaneityFactor < 0 ||
simultaneityFactor > 1
) {
throw new Error(
"Distribution-board simultaneity factor must be between zero and one."
);
}
return totalPower * simultaneityFactor;
}
+4
View File
@@ -52,6 +52,7 @@ export interface CircuitTreeSectionBlock {
displayName: string;
prefix: string;
sortOrder: number;
sectionTotalPower: number;
circuits: CircuitTreeCircuit[];
}
@@ -60,6 +61,9 @@ export interface CircuitTreeResponse {
currentRevision: number;
singlePhaseVoltageV: number;
threePhaseVoltageV: number;
distributionBoardSimultaneityFactor: number;
distributionBoardTotalPower: number;
distributionBoardTotalPowerWithSimultaneityFactor: number;
sections: CircuitTreeSectionBlock[];
}
@@ -6,11 +6,13 @@ import type { SerializedProjectCommand } from "./project-command.model.js";
export const distributionBoardUpdateCommandType =
"distribution-board.update" as const;
export const distributionBoardUpdateCommandSchemaVersion = 1 as const;
export const legacyDistributionBoardUpdateCommandSchemaVersion = 1 as const;
export const distributionBoardUpdateCommandSchemaVersion = 2 as const;
export interface DistributionBoardUpdateValues {
floorId: string | null;
supplyType: DistributionBoardSupplyType | null;
simultaneityFactor: number;
}
export type DistributionBoardUpdateField =
@@ -31,17 +33,27 @@ export interface DistributionBoardUpdateCommandPayload {
changes: DistributionBoardUpdateFieldChange[];
}
export interface DistributionBoardUpdateProjectCommand
interface CurrentDistributionBoardUpdateProjectCommand
extends SerializedProjectCommand<DistributionBoardUpdateCommandPayload> {
schemaVersion: typeof distributionBoardUpdateCommandSchemaVersion;
type: typeof distributionBoardUpdateCommandType;
}
interface LegacyDistributionBoardUpdateProjectCommand
extends SerializedProjectCommand<DistributionBoardUpdateCommandPayload> {
schemaVersion: typeof legacyDistributionBoardUpdateCommandSchemaVersion;
type: typeof distributionBoardUpdateCommandType;
}
export type DistributionBoardUpdateProjectCommand =
| CurrentDistributionBoardUpdateProjectCommand
| LegacyDistributionBoardUpdateProjectCommand;
export function createDistributionBoardUpdateProjectCommand(
distributionBoardId: string,
patch: DistributionBoardUpdatePatch
): DistributionBoardUpdateProjectCommand {
const command: DistributionBoardUpdateProjectCommand = {
): CurrentDistributionBoardUpdateProjectCommand {
const command: CurrentDistributionBoardUpdateProjectCommand = {
schemaVersion: distributionBoardUpdateCommandSchemaVersion,
type: distributionBoardUpdateCommandType,
payload: {
@@ -60,7 +72,9 @@ export function assertDistributionBoardUpdateProjectCommand(
command: SerializedProjectCommand<unknown>
): asserts command is DistributionBoardUpdateProjectCommand {
if (
command.schemaVersion !== distributionBoardUpdateCommandSchemaVersion ||
(command.schemaVersion !==
legacyDistributionBoardUpdateCommandSchemaVersion &&
command.schemaVersion !== distributionBoardUpdateCommandSchemaVersion) ||
command.type !== distributionBoardUpdateCommandType ||
!isPlainObject(command.payload)
) {
@@ -81,7 +95,11 @@ export function assertDistributionBoardUpdateProjectCommand(
for (const change of changes) {
if (
!isPlainObject(change) ||
(change.field !== "floorId" && change.field !== "supplyType") ||
(change.field !== "floorId" &&
change.field !== "supplyType" &&
(command.schemaVersion ===
legacyDistributionBoardUpdateCommandSchemaVersion ||
change.field !== "simultaneityFactor")) ||
seen.has(change.field)
) {
throw new Error(
@@ -95,13 +113,24 @@ export function assertDistributionBoardUpdateProjectCommand(
) {
throw new Error("floorId must be a non-empty string or null.");
}
} else if (change.field === "supplyType") {
if (
change.value !== null &&
!distributionBoardSupplyTypes.includes(
change.value as DistributionBoardSupplyType
)
) {
throw new Error("supplyType is invalid.");
}
} else if (
change.value !== null &&
!distributionBoardSupplyTypes.includes(
change.value as DistributionBoardSupplyType
)
typeof change.value !== "number" ||
!Number.isFinite(change.value) ||
change.value < 0 ||
change.value > 1
) {
throw new Error("supplyType is invalid.");
throw new Error(
"simultaneityFactor must be between zero and one."
);
}
seen.add(change.field);
}
@@ -13,7 +13,8 @@ export const legacyProjectStateSnapshotSchemaVersion = 1 as const;
export const previousProjectStateSnapshotSchemaVersion = 2 as const;
export const distributionBoardProjectStateSnapshotSchemaVersion = 3 as const;
export const supplyTypesProjectStateSnapshotSchemaVersion = 4 as const;
export const projectStateSnapshotSchemaVersion = 5 as const;
export const voltageProjectStateSnapshotSchemaVersion = 5 as const;
export const projectStateSnapshotSchemaVersion = 6 as const;
const idSchema = z.string().trim().min(1);
const nullableStringSchema = z.string().nullable();
@@ -57,6 +58,12 @@ const distributionBoardSchema = legacyDistributionBoardSchema
})
.strict();
const currentDistributionBoardSchema = distributionBoardSchema
.extend({
simultaneityFactor: finiteNumberSchema.min(0).max(1),
})
.strict();
const circuitListSchema = z
.object({
id: idSchema,
@@ -213,9 +220,17 @@ const supplyTypesProjectStateSnapshotSchema = z
})
.strict();
export const projectStateSnapshotSchema =
const voltageProjectStateSnapshotSchema =
supplyTypesProjectStateSnapshotSchema.extend({
schemaVersion: z.literal(
voltageProjectStateSnapshotSchemaVersion
),
});
export const projectStateSnapshotSchema =
voltageProjectStateSnapshotSchema.extend({
schemaVersion: z.literal(projectStateSnapshotSchemaVersion),
distributionBoards: z.array(currentDistributionBoardSchema),
});
export type ProjectStateSnapshot = z.infer<
@@ -228,34 +243,46 @@ export function parseProjectStateSnapshot(
const version = isPlainObject(value) ? value.schemaVersion : undefined;
const parsedSnapshot =
version === legacyProjectStateSnapshotSchemaVersion
? upgradeSupplyTypesProjectStateSnapshot(
upgradeDistributionBoardProjectStateSnapshot(
upgradePreviousProjectStateSnapshot(
upgradeLegacyProjectStateSnapshot(
legacyProjectStateSnapshotSchema.parse(value)
? upgradeVoltageProjectStateSnapshot(
upgradeSupplyTypesProjectStateSnapshot(
upgradeDistributionBoardProjectStateSnapshot(
upgradePreviousProjectStateSnapshot(
upgradeLegacyProjectStateSnapshot(
legacyProjectStateSnapshotSchema.parse(value)
)
)
)
)
)
: version === previousProjectStateSnapshotSchemaVersion
? upgradeSupplyTypesProjectStateSnapshot(
upgradeDistributionBoardProjectStateSnapshot(
upgradePreviousProjectStateSnapshot(
previousProjectStateSnapshotSchema.parse(value)
? upgradeVoltageProjectStateSnapshot(
upgradeSupplyTypesProjectStateSnapshot(
upgradeDistributionBoardProjectStateSnapshot(
upgradePreviousProjectStateSnapshot(
previousProjectStateSnapshotSchema.parse(value)
)
)
)
)
: version === distributionBoardProjectStateSnapshotSchemaVersion
? upgradeSupplyTypesProjectStateSnapshot(
upgradeDistributionBoardProjectStateSnapshot(
distributionBoardProjectStateSnapshotSchema.parse(value)
? upgradeVoltageProjectStateSnapshot(
upgradeSupplyTypesProjectStateSnapshot(
upgradeDistributionBoardProjectStateSnapshot(
distributionBoardProjectStateSnapshotSchema.parse(value)
)
)
)
: version === supplyTypesProjectStateSnapshotSchemaVersion
? upgradeSupplyTypesProjectStateSnapshot(
supplyTypesProjectStateSnapshotSchema.parse(value)
? upgradeVoltageProjectStateSnapshot(
upgradeSupplyTypesProjectStateSnapshot(
supplyTypesProjectStateSnapshotSchema.parse(value)
)
)
: projectStateSnapshotSchema.parse(value);
: version === voltageProjectStateSnapshotSchemaVersion
? upgradeVoltageProjectStateSnapshot(
voltageProjectStateSnapshotSchema.parse(value)
)
: projectStateSnapshotSchema.parse(value);
const snapshot = normalizeSnapshotPhaseTypes(parsedSnapshot);
assertProjectStateSnapshotRelations(snapshot);
return snapshot;
@@ -354,14 +381,14 @@ function upgradeDistributionBoardProjectStateSnapshot(
function upgradeSupplyTypesProjectStateSnapshot(
snapshot: z.infer<typeof supplyTypesProjectStateSnapshotSchema>
): ProjectStateSnapshot {
): z.infer<typeof voltageProjectStateSnapshotSchema> {
const settings = snapshot.project;
const sectionById = new Map(
snapshot.circuitSections.map((section) => [section.id, section])
);
return {
...snapshot,
schemaVersion: projectStateSnapshotSchemaVersion,
schemaVersion: voltageProjectStateSnapshotSchemaVersion,
projectDevices: snapshot.projectDevices.map((device) => ({
...device,
voltageV: resolveProjectVoltage(device.phaseType, settings),
@@ -383,6 +410,19 @@ function upgradeSupplyTypesProjectStateSnapshot(
};
}
function upgradeVoltageProjectStateSnapshot(
snapshot: z.infer<typeof voltageProjectStateSnapshotSchema>
): ProjectStateSnapshot {
return {
...snapshot,
schemaVersion: projectStateSnapshotSchemaVersion,
distributionBoards: snapshot.distributionBoards.map((board) => ({
...board,
simultaneityFactor: 1,
})),
};
}
function isPlainObject(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
+106 -25
View File
@@ -27,7 +27,11 @@ import {
deviceFieldKeys,
formatPhaseTypeLabel,
formatValue,
getCircuitSectionLabel,
getProjectColumnLayoutStorageKey,
isGridEditorControlTarget,
normalizeColumnOrder,
parseStoredColumnLayout,
} from "../utils/circuit-grid-model";
import {
buildCircuitDeviceRowInsertSnapshot,
@@ -131,7 +135,8 @@ type CircuitReorderDropIntent =
| { kind: "after-circuit"; sectionId: string; targetCircuitId: string; valid: boolean }
| { kind: "section-end"; sectionId: string; valid: boolean };
const COLUMN_LAYOUT_STORAGE_KEY = "circuitTreeEditor.columnLayout.v1";
const LEGACY_COLUMN_LAYOUT_STORAGE_KEY =
"circuitTreeEditor.columnLayout.v1";
function normalizeUiError(err: unknown): string {
const message = err instanceof Error ? err.message : "Der Vorgang ist fehlgeschlagen.";
@@ -222,6 +227,10 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
const [filterValueSearch, setFilterValueSearch] = useState("");
const [visibleColumnKeys, setVisibleColumnKeys] = useState<CellKey[]>(defaultVisibleColumnKeys);
const [columnOrder, setColumnOrder] = useState<CellKey[]>(allColumns.map((column) => column.key));
const [
loadedColumnLayoutProjectId,
setLoadedColumnLayoutProjectId,
] = useState<string | null>(null);
const [isColumnMenuOpen, setIsColumnMenuOpen] = useState(false);
const [columnSearch, setColumnSearch] = useState("");
const [draggingColumnKey, setDraggingColumnKey] = useState<CellKey | null>(null);
@@ -267,13 +276,6 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
}
}
function normalizeColumnOrder(keys: CellKey[]) {
const unique = [...new Set(keys)];
const allKeys = allColumns.map((column) => column.key);
const merged = [...unique, ...allKeys.filter((key) => !unique.includes(key))];
return ["equipmentIdentifier" as CellKey, ...merged.filter((key) => key !== "equipmentIdentifier")];
}
// Initial/identity-change tree load for current route context.
useEffect(() => {
void loadTree({ showLoading: true });
@@ -292,32 +294,48 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
}, [projectId]);
useEffect(() => {
setLoadedColumnLayoutProjectId(null);
setColumnOrder(allColumns.map((column) => column.key));
setVisibleColumnKeys(defaultVisibleColumnKeys);
try {
const raw = localStorage.getItem(COLUMN_LAYOUT_STORAGE_KEY);
if (!raw) {
const parsed = parseStoredColumnLayout(
localStorage.getItem(
getProjectColumnLayoutStorageKey(projectId)
) ??
localStorage.getItem(
LEGACY_COLUMN_LAYOUT_STORAGE_KEY
)
);
if (!parsed) {
return;
}
const parsed = JSON.parse(raw) as { order?: string[]; visible?: string[] };
const validKeys = new Set(allColumns.map((column) => column.key));
const parsedOrder = (parsed.order ?? []).filter((key): key is CellKey => validKeys.has(key as CellKey));
const parsedVisible = (parsed.visible ?? []).filter((key): key is CellKey => validKeys.has(key as CellKey));
if (!parsedOrder.length || !parsedVisible.length || !parsedVisible.includes("equipmentIdentifier")) {
return;
}
setColumnOrder(normalizeColumnOrder(parsedOrder));
setVisibleColumnKeys(parsedVisible);
setColumnOrder(parsed.order);
setVisibleColumnKeys(parsed.visible);
} catch {
// ignore invalid local storage and use defaults
} finally {
setLoadedColumnLayoutProjectId(projectId);
}
}, []);
}, [projectId]);
useEffect(() => {
if (loadedColumnLayoutProjectId !== projectId) {
return;
}
const payload = {
order: columnOrder,
visible: visibleColumnKeys,
};
localStorage.setItem(COLUMN_LAYOUT_STORAGE_KEY, JSON.stringify(payload));
}, [columnOrder, visibleColumnKeys]);
localStorage.setItem(
getProjectColumnLayoutStorageKey(projectId),
JSON.stringify(payload)
);
}, [
columnOrder,
loadedColumnLayoutProjectId,
projectId,
visibleColumnKeys,
]);
useEffect(() => {
const visible = new Set(visibleColumnKeys);
@@ -474,6 +492,51 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
);
}
function renderDistributionPowerSummary() {
if (!data) {
return null;
}
return (
<section
className="distribution-power-summary"
aria-label="Leistungszusammenfassung des Verteilers"
>
<div>
<span>Gesamtleistung Verteiler</span>
<strong>
{formatValue(
data.distributionBoardTotalPower,
"rowTotalPower"
)}{" "}
kW
</strong>
</div>
<div>
<span>Verteilerweiter Gleichzeitigkeitsfaktor</span>
<strong>
{formatValue(
data.distributionBoardSimultaneityFactor,
"simultaneityFactor"
)}
</strong>
</div>
<div>
<span>
Gesamtleistung Verteiler unter Berücksichtigung des
Gleichzeitigkeitsfaktors
</span>
<strong>
{formatValue(
data.distributionBoardTotalPowerWithSimultaneityFactor,
"rowTotalPower"
)}{" "}
kW
</strong>
</div>
</section>
);
}
function closeColumnSettingsMenu() {
setIsColumnMenuOpen(false);
setColumnSearch("");
@@ -2490,6 +2553,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
{renderColumnSettingsMenu("col-empty")}
</div>
{renderActiveViewSummary()}
{renderDistributionPowerSummary()}
{error ? (
<div className="notice error editor-error-notice" role="alert">
<span>{error}</span>
@@ -2598,6 +2662,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
{renderColumnSettingsMenu("col")}
</div>
{renderActiveViewSummary()}
{renderDistributionPowerSummary()}
{hasActiveSortOrFilter ? (
<div className="notice muted">Sortierung und Filter verändern nur die Ansicht. Die Neunummerierung ist währenddessen deaktiviert.</div>
) : null}
@@ -2701,14 +2766,18 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
const valid = !selectedProjectDevice || isProjectDevicePlacementValid(selectedProjectDevice, section);
return (
<option key={section.id} value={section.id} disabled={!valid}>
{section.displayName}{valid ? "" : " (nicht zulässig)"}
{getCircuitSectionLabel(section)}
{valid ? "" : " (nicht zulässig)"}
</option>
);
})}
</select>
</label>
{selectedProjectDevice && suggestedSection ? (
<p className="notice muted">Vorgeschlagener Bereich: {suggestedSection.displayName}</p>
<p className="notice muted">
Vorgeschlagener Bereich:{" "}
{getCircuitSectionLabel(suggestedSection)}
</p>
) : null}
<button
type="button"
@@ -2980,7 +3049,19 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
>
<td colSpan={visibleColumns.length + 1} className="section-drop-cell">
<div className="section-content">
<strong>{section.displayName}</strong>
<div className="section-title">
<strong>
{getCircuitSectionLabel(section)}
</strong>
<span>
Gesamtleistung Abschnitt:{" "}
{formatValue(
section.sectionTotalPower,
"rowTotalPower"
)}{" "}
kW
</span>
</div>
<div className="section-actions">
<button type="button" tabIndex={-1} onClick={() => void handleAddReserveCircuit(section.id)}>
Stromkreis hinzufügen
@@ -4,7 +4,10 @@ import { useEffect, useState } from "react";
import { Fragment } from "react";
import { getCircuitTree } from "../utils/api";
import type { CircuitTreeCircuitDto, CircuitTreeResponseDto } from "../types";
import { formatValue } from "../utils/circuit-grid-model";
import {
formatValue,
getCircuitSectionLabel,
} from "../utils/circuit-grid-model";
function renderCircuitSummaryLabel(circuit: CircuitTreeCircuitDto) {
if (circuit.displayName?.trim()) {
@@ -98,7 +101,7 @@ function SectionRows(props: { section: CircuitTreeResponseDto["sections"][number
<>
<tr className="section-row">
<td colSpan={21}>
<strong>{section.displayName}</strong>
<strong>{getCircuitSectionLabel(section)}</strong>
</td>
</tr>
{section.circuits.map((circuit) => {
+5
View File
@@ -95,6 +95,7 @@ export interface DistributionBoardDto {
name: string;
floorId: string | null;
supplyType: DistributionBoardSupplyType | null;
simultaneityFactor: number;
}
export interface DistributionBoardCommandResultDto
@@ -296,6 +297,7 @@ export interface CircuitTreeSectionDto {
displayName: string;
prefix: string;
sortOrder: number;
sectionTotalPower: number;
circuits: CircuitTreeCircuitDto[];
}
@@ -315,6 +317,9 @@ export interface CircuitTreeResponseDto {
currentRevision: number;
singlePhaseVoltageV: number;
threePhaseVoltageV: number;
distributionBoardSimultaneityFactor: number;
distributionBoardTotalPower: number;
distributionBoardTotalPowerWithSimultaneityFactor: number;
sections: CircuitTreeSectionDto[];
migrationReport?: CircuitTreeMigrationReportDto;
}
+1
View File
@@ -299,6 +299,7 @@ export function updateDistributionBoard(
input: {
floorId: string | null;
supplyType: NonNullable<DistributionBoardDto["supplyType"]>;
simultaneityFactor: number;
},
expectedRevision: number
) {
+90
View File
@@ -124,6 +124,96 @@ export const defaultVisibleColumnKeys = allColumns
.filter((column) => column.defaultVisible)
.map((column) => column.key);
const projectColumnLayoutStoragePrefix =
"circuitTreeEditor.columnLayout.v2";
export interface CircuitColumnLayout {
order: CellKey[];
visible: CellKey[];
}
const circuitSectionLabels: Record<string, string> = {
lighting: "Licht",
single_phase: "Einphasig",
three_phase: "Dreiphasig",
unassigned: "Nicht zugeordnet",
};
export function getCircuitSectionLabel(
section: { key: string; displayName: string }
): string {
return circuitSectionLabels[section.key] ?? section.displayName;
}
export function getProjectColumnLayoutStorageKey(
projectId: string
): string {
return `${projectColumnLayoutStoragePrefix}.${projectId}`;
}
export function normalizeColumnOrder(keys: CellKey[]): CellKey[] {
const unique = [...new Set(keys)];
const allKeys = allColumns.map((column) => column.key);
const merged = [
...unique,
...allKeys.filter((key) => !unique.includes(key)),
];
return [
"equipmentIdentifier",
...merged.filter((key) => key !== "equipmentIdentifier"),
];
}
export function parseStoredColumnLayout(
serialized: string | null
): CircuitColumnLayout | null {
if (!serialized) {
return null;
}
try {
const parsed = JSON.parse(serialized) as {
order?: unknown;
visible?: unknown;
};
if (
!Array.isArray(parsed.order) ||
!Array.isArray(parsed.visible)
) {
return null;
}
const validKeys = new Set(
allColumns.map((column) => column.key)
);
const order = parsed.order.filter(
(key): key is CellKey =>
typeof key === "string" &&
validKeys.has(key as CellKey)
);
const visible = [
...new Set(
parsed.visible.filter(
(key): key is CellKey =>
typeof key === "string" &&
validKeys.has(key as CellKey)
)
),
];
if (
!order.length ||
!visible.length ||
!visible.includes("equipmentIdentifier")
) {
return null;
}
return {
order: normalizeColumnOrder(order),
visible,
};
} catch {
return null;
}
}
const deviceOnlyColumns = new Set<CellKey>([
"quantity",
"powerPerUnit",
@@ -1,7 +1,10 @@
import type { Request, Response } from "express";
import {
applyDistributionBoardSimultaneityFactor,
calculateCircuitTotalPower,
calculateDistributionBoardTotalPower,
calculateRowTotalPower,
calculateSectionTotalPower,
} from "../../domain/calculations/circuit-power-calculation.js";
import type { CircuitTreeResponse } from "../../domain/models/circuit-tree.model.js";
import {
@@ -9,6 +12,7 @@ import {
circuitListRepository,
circuitRepository,
circuitSectionRepository,
distributionBoardRepository,
projectRepository,
} from "../composition/application-repositories.js";
@@ -37,6 +41,16 @@ export async function getCircuitTree(req: Request, res: Response) {
if (!project) {
return res.status(404).json({ error: "Project not found" });
}
const distributionBoard =
await distributionBoardRepository.findById(
projectId,
list.distributionBoardId
);
if (!distributionBoard) {
return res.status(404).json({
error: "Distribution board not found",
});
}
try {
const sections = await circuitSectionRepository.listByCircuitList(circuitListId);
@@ -57,12 +71,17 @@ export async function getCircuitTree(req: Request, res: Response) {
currentRevision: project.currentRevision,
singlePhaseVoltageV: project.singlePhaseVoltageV,
threePhaseVoltageV: project.threePhaseVoltageV,
distributionBoardSimultaneityFactor:
distributionBoard.simultaneityFactor,
distributionBoardTotalPower: 0,
distributionBoardTotalPowerWithSimultaneityFactor: 0,
sections: sections.map((section) => ({
id: section.id,
key: section.key,
displayName: section.displayName,
prefix: section.prefix,
sortOrder: section.sortOrder,
sectionTotalPower: 0,
circuits: [],
})),
};
@@ -123,6 +142,19 @@ export async function getCircuitTree(req: Request, res: Response) {
});
}
for (const section of tree.sections) {
section.sectionTotalPower = calculateSectionTotalPower(
section.circuits
);
}
tree.distributionBoardTotalPower =
calculateDistributionBoardTotalPower(tree.sections);
tree.distributionBoardTotalPowerWithSimultaneityFactor =
applyDistributionBoardSimultaneityFactor(
tree.distributionBoardTotalPower,
tree.distributionBoardSimultaneityFactor
);
return res.json(tree);
} catch (error) {
if (isMissingCircuitTreeSchemaError(error)) {
@@ -131,6 +163,10 @@ export async function getCircuitTree(req: Request, res: Response) {
currentRevision: project.currentRevision,
singlePhaseVoltageV: project.singlePhaseVoltageV,
threePhaseVoltageV: project.threePhaseVoltageV,
distributionBoardSimultaneityFactor:
distributionBoard.simultaneityFactor,
distributionBoardTotalPower: 0,
distributionBoardTotalPowerWithSimultaneityFactor: 0,
sections: [],
warning:
"Circuit-first tables are not available yet. Run database migrations (including 0008_circuit_first_model).",
@@ -51,7 +51,10 @@ export async function createDistributionBoard(req: Request, res: Response) {
});
return res.status(201).json({
...result,
distributionBoard: structure.distributionBoard,
distributionBoard: {
...structure.distributionBoard,
simultaneityFactor: 1,
},
});
} catch (error) {
return respondWithProjectCommandError(error, res);
@@ -83,6 +86,7 @@ export async function updateDistributionBoard(
{
floorId: parsed.data.floorId,
supplyType: parsed.data.supplyType,
simultaneityFactor: parsed.data.simultaneityFactor,
}
),
});
@@ -45,6 +45,7 @@ export const updateDistributionBoardSchema = z
expectedRevision: expectedProjectRevisionSchema,
floorId: z.string().trim().min(1).nullable(),
supplyType: z.enum(distributionBoardSupplyTypes),
simultaneityFactor: z.number().finite().min(0).max(1),
})
.strict();
+44
View File
@@ -5,11 +5,14 @@ import {
buildCircuitEditPatch,
buildDeviceRowEditPatch,
formatValue,
getCircuitSectionLabel,
getProjectColumnLayoutStorageKey,
isGridEditorControlTarget,
getBlockSortValue,
getCellKind,
getCircuitValue,
getDeviceValue,
parseStoredColumnLayout,
parseNumeric,
} from "../src/frontend/utils/circuit-grid-model.js";
import type { CircuitTreeCircuitDto, CircuitTreeDeviceRowDto } from "../src/frontend/types.js";
@@ -73,6 +76,47 @@ describe("circuit grid model", () => {
assert.equal(allColumns[0].locked, true);
});
it("stores valid column layouts under a project-specific key", () => {
assert.equal(
getProjectColumnLayoutStorageKey("project-1"),
"circuitTreeEditor.columnLayout.v2.project-1"
);
const layout = parseStoredColumnLayout(
JSON.stringify({
order: ["displayName", "equipmentIdentifier"],
visible: [
"displayName",
"equipmentIdentifier",
"unknown",
],
})
);
assert.ok(layout);
assert.equal(layout.order[0], "equipmentIdentifier");
assert.deepEqual(layout.visible, [
"displayName",
"equipmentIdentifier",
]);
assert.equal(parseStoredColumnLayout("{invalid"), null);
});
it("shows stable circuit sections with German labels", () => {
assert.equal(
getCircuitSectionLabel({
key: "lighting",
displayName: "Lighting",
}),
"Licht"
);
assert.equal(
getCircuitSectionLabel({
key: "custom",
displayName: "Sonderbereich",
}),
"Sonderbereich"
);
});
it("maps shared fields to the correct level for every row shape", () => {
assert.equal(getCellKind("circuitCompact", "displayName"), "deviceField");
assert.equal(getCellKind("circuitCompact", "equipmentIdentifier"), "circuitField");
+29
View File
@@ -1,8 +1,11 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
applyDistributionBoardSimultaneityFactor,
calculateCircuitTotalPower,
calculateDistributionBoardTotalPower,
calculateRowTotalPower,
calculateSectionTotalPower,
} from "../src/domain/calculations/circuit-power-calculation.js";
import {
resolveCircuitPhaseType,
@@ -19,6 +22,32 @@ describe("circuit power calculation", () => {
]);
assert.equal(total, 4.5);
});
it("calculates section and distribution-board totals", () => {
const lighting = calculateSectionTotalPower([
{ circuitTotalPower: 1.5 },
{ circuitTotalPower: 2.25 },
]);
const singlePhase = calculateSectionTotalPower([
{ circuitTotalPower: 3 },
]);
const total = calculateDistributionBoardTotalPower([
{ sectionTotalPower: lighting },
{ sectionTotalPower: singlePhase },
{ sectionTotalPower: 0 },
]);
assert.equal(lighting, 3.75);
assert.equal(total, 6.75);
assert.equal(
applyDistributionBoardSimultaneityFactor(total, 0.8),
5.4
);
assert.throws(
() => applyDistributionBoardSimultaneityFactor(total, 1.1),
/between zero and one/
);
});
});
describe("project voltage derivation", () => {
@@ -22,7 +22,10 @@ import {
createDistributionBoardStructureSnapshot,
type DistributionBoardStructureProjectCommand,
} from "../src/domain/models/distribution-board-structure-project-command.model.js";
import { createDistributionBoardUpdateProjectCommand } from "../src/domain/models/distribution-board-project-command.model.js";
import {
createDistributionBoardUpdateProjectCommand,
type DistributionBoardUpdateProjectCommand,
} from "../src/domain/models/distribution-board-project-command.model.js";
import {
createDistributionBoard,
updateDistributionBoard,
@@ -293,7 +296,7 @@ describe("distribution-board structure project command", () => {
}
});
it("updates floor and supply type with persisted undo and validates project ownership", () => {
it("updates floor, supply type and simultaneity factor with persisted undo", () => {
const context = createTestDatabase();
try {
context.db
@@ -335,7 +338,11 @@ describe("distribution-board structure project command", () => {
source: "user",
command: createDistributionBoardUpdateProjectCommand(
structure.distributionBoard.id,
{ floorId: "floor-1", supplyType: "SV" }
{
floorId: "floor-1",
supplyType: "SV",
simultaneityFactor: 0.75,
}
),
});
assert.deepEqual(
@@ -343,10 +350,16 @@ describe("distribution-board structure project command", () => {
.select({
floorId: distributionBoards.floorId,
supplyType: distributionBoards.supplyType,
simultaneityFactor:
distributionBoards.simultaneityFactor,
})
.from(distributionBoards)
.get(),
{ floorId: "floor-1", supplyType: "SV" }
{
floorId: "floor-1",
supplyType: "SV",
simultaneityFactor: 0.75,
}
);
const undoStep = new ProjectHistoryRepository(
@@ -365,10 +378,16 @@ describe("distribution-board structure project command", () => {
.select({
floorId: distributionBoards.floorId,
supplyType: distributionBoards.supplyType,
simultaneityFactor:
distributionBoards.simultaneityFactor,
})
.from(distributionBoards)
.get(),
{ floorId: null, supplyType: "AV" }
{
floorId: null,
supplyType: "AV",
simultaneityFactor: 1,
}
);
assert.throws(
() =>
@@ -395,6 +414,52 @@ describe("distribution-board structure project command", () => {
}
});
it("keeps stored version-one board updates executable", () => {
const context = createTestDatabase();
try {
const repository =
new DistributionBoardStructureProjectCommandRepository(
context.db
);
const structure = createDistributionBoardStructureSnapshot(
"project-1",
"UV Alt",
{ supplyType: "AV" }
);
repository.execute({
projectId: "project-1",
expectedRevision: 0,
source: "user",
command:
createDistributionBoardInsertProjectCommand(structure),
});
const legacyUpdate = {
schemaVersion: 1,
type: "distribution-board.update",
payload: {
distributionBoardId: structure.distributionBoard.id,
changes: [{ field: "supplyType", value: "SV" }],
},
} as DistributionBoardUpdateProjectCommand;
repository.executeUpdate({
projectId: "project-1",
expectedRevision: 1,
source: "user",
command: legacyUpdate,
});
const board = context.db
.select()
.from(distributionBoards)
.get();
assert.equal(board?.supplyType, "SV");
assert.equal(board?.simultaneityFactor, 1);
} finally {
context.close();
}
});
it("sends distribution-board updates to the revision-safe API", async () => {
const requests: Array<{ url: string; method: string; body: unknown }> = [];
const originalFetch = globalThis.fetch;
@@ -413,7 +478,11 @@ describe("distribution-board structure project command", () => {
await updateDistributionBoard(
"project-1",
"board-1",
{ floorId: "floor-1", supplyType: "USV" },
{
floorId: "floor-1",
supplyType: "USV",
simultaneityFactor: 0.8,
},
9
);
} finally {
@@ -426,6 +495,7 @@ describe("distribution-board structure project command", () => {
body: {
floorId: "floor-1",
supplyType: "USV",
simultaneityFactor: 0.8,
expectedRevision: 9,
},
},
+19 -1
View File
@@ -67,7 +67,11 @@ function createTestDatabase(): DatabaseContext {
.run();
context.db
.update(distributionBoards)
.set({ floorId: "floor-1", supplyType: "SV" })
.set({
floorId: "floor-1",
supplyType: "SV",
simultaneityFactor: 0.65,
})
.where(eq(distributionBoards.id, board.id))
.run();
context.db
@@ -176,6 +180,10 @@ describe("project snapshot repository", () => {
assert.equal(state.distributionBoards.length, 1);
assert.equal(state.distributionBoards[0].floorId, "floor-1");
assert.equal(state.distributionBoards[0].supplyType, "SV");
assert.equal(
state.distributionBoards[0].simultaneityFactor,
0.65
);
assert.equal(state.circuitLists.length, 1);
assert.equal(
state.circuitSections.length,
@@ -293,6 +301,7 @@ describe("project snapshot repository", () => {
>) {
delete board.floorId;
delete board.supplyType;
delete board.simultaneityFactor;
}
const legacyPayloadJson = JSON.stringify(currentPayload);
context.db
@@ -375,6 +384,10 @@ describe("portable project transfer", () => {
copied.state.distributionBoards[0].supplyType,
"SV"
);
assert.equal(
copied.state.distributionBoards[0].simultaneityFactor,
0.65
);
assert.equal(
copied.state.circuits[0].deviceRows[0].legacyConsumerId,
null
@@ -431,6 +444,7 @@ describe("portable project transfer", () => {
for (const board of legacyState.distributionBoards) {
delete board.floorId;
delete board.supplyType;
delete board.simultaneityFactor;
}
const legacyPayloadJson = JSON.stringify(legacyState);
const duplicate = repository.importDuplicate({
@@ -448,6 +462,10 @@ describe("portable project transfer", () => {
assert.ok(copied);
assert.equal(copied.state.distributionBoards[0].floorId, null);
assert.equal(copied.state.distributionBoards[0].supplyType, null);
assert.equal(
copied.state.distributionBoards[0].simultaneityFactor,
1
);
} finally {
context.close();
}
+28
View File
@@ -103,6 +103,7 @@ describe("project state snapshot model", () => {
...previous.distributionBoards[0],
floorId: null,
supplyType: null,
simultaneityFactor: 1,
},
]);
});
@@ -158,6 +159,30 @@ describe("project state snapshot model", () => {
assert.equal(snapshot.projectDevices[0]?.voltageV, 400);
});
it("upgrades version-five distribution boards with a neutral factor", () => {
const previous = {
...minimalSnapshot(),
schemaVersion: 5 as const,
distributionBoards: [
{
id: "board-1",
projectId: "project-1",
name: "UV 1",
floorId: null,
supplyType: "AV" as const,
},
],
};
const snapshot = parseProjectStateSnapshot(previous);
assert.equal(snapshot.schemaVersion, projectStateSnapshotSchemaVersion);
assert.equal(
snapshot.distributionBoards[0]?.simultaneityFactor,
1
);
});
it("rejects duplicate ids and cross-project ownership", () => {
assert.throws(
() =>
@@ -170,6 +195,7 @@ describe("project state snapshot model", () => {
name: "UV 1",
floorId: null,
supplyType: null,
simultaneityFactor: 1,
},
{
id: "board-1",
@@ -177,6 +203,7 @@ describe("project state snapshot model", () => {
name: "UV 2",
floorId: null,
supplyType: null,
simultaneityFactor: 1,
},
],
}),
@@ -211,6 +238,7 @@ describe("project state snapshot model", () => {
name: "UV 1",
floorId: "missing",
supplyType: "AV",
simultaneityFactor: 1,
},
],
}),