Add distribution power summaries
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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.");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,7 +113,8 @@ export function assertDistributionBoardUpdateProjectCommand(
|
||||
) {
|
||||
throw new Error("floorId must be a non-empty string or null.");
|
||||
}
|
||||
} else if (
|
||||
} else if (change.field === "supplyType") {
|
||||
if (
|
||||
change.value !== null &&
|
||||
!distributionBoardSupplyTypes.includes(
|
||||
change.value as DistributionBoardSupplyType
|
||||
@@ -103,6 +122,16 @@ export function assertDistributionBoardUpdateProjectCommand(
|
||||
) {
|
||||
throw new Error("supplyType is invalid.");
|
||||
}
|
||||
} else if (
|
||||
typeof change.value !== "number" ||
|
||||
!Number.isFinite(change.value) ||
|
||||
change.value < 0 ||
|
||||
change.value > 1
|
||||
) {
|
||||
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,7 +243,8 @@ export function parseProjectStateSnapshot(
|
||||
const version = isPlainObject(value) ? value.schemaVersion : undefined;
|
||||
const parsedSnapshot =
|
||||
version === legacyProjectStateSnapshotSchemaVersion
|
||||
? upgradeSupplyTypesProjectStateSnapshot(
|
||||
? upgradeVoltageProjectStateSnapshot(
|
||||
upgradeSupplyTypesProjectStateSnapshot(
|
||||
upgradeDistributionBoardProjectStateSnapshot(
|
||||
upgradePreviousProjectStateSnapshot(
|
||||
upgradeLegacyProjectStateSnapshot(
|
||||
@@ -237,24 +253,35 @@ export function parseProjectStateSnapshot(
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
: version === previousProjectStateSnapshotSchemaVersion
|
||||
? upgradeSupplyTypesProjectStateSnapshot(
|
||||
? upgradeVoltageProjectStateSnapshot(
|
||||
upgradeSupplyTypesProjectStateSnapshot(
|
||||
upgradeDistributionBoardProjectStateSnapshot(
|
||||
upgradePreviousProjectStateSnapshot(
|
||||
previousProjectStateSnapshotSchema.parse(value)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
: version === distributionBoardProjectStateSnapshotSchemaVersion
|
||||
? upgradeSupplyTypesProjectStateSnapshot(
|
||||
? upgradeVoltageProjectStateSnapshot(
|
||||
upgradeSupplyTypesProjectStateSnapshot(
|
||||
upgradeDistributionBoardProjectStateSnapshot(
|
||||
distributionBoardProjectStateSnapshotSchema.parse(value)
|
||||
)
|
||||
)
|
||||
)
|
||||
: version === supplyTypesProjectStateSnapshotSchemaVersion
|
||||
? upgradeSupplyTypesProjectStateSnapshot(
|
||||
? upgradeVoltageProjectStateSnapshot(
|
||||
upgradeSupplyTypesProjectStateSnapshot(
|
||||
supplyTypesProjectStateSnapshotSchema.parse(value)
|
||||
)
|
||||
)
|
||||
: version === voltageProjectStateSnapshotSchemaVersion
|
||||
? upgradeVoltageProjectStateSnapshot(
|
||||
voltageProjectStateSnapshotSchema.parse(value)
|
||||
)
|
||||
: projectStateSnapshotSchema.parse(value);
|
||||
const snapshot = normalizeSnapshotPhaseTypes(parsedSnapshot);
|
||||
assertProjectStateSnapshotRelations(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);
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -299,6 +299,7 @@ export function updateDistributionBoard(
|
||||
input: {
|
||||
floorId: string | null;
|
||||
supplyType: NonNullable<DistributionBoardDto["supplyType"]>;
|
||||
simultaneityFactor: number;
|
||||
},
|
||||
expectedRevision: number
|
||||
) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user