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`.
|
verwenden `rowTotalPower`, Stromkreis-Sammelzeilen `circuitTotalPower`.
|
||||||
Zahlen werden ausschließlich für die Anzeige deutsch und begrenzt formatiert;
|
Zahlen werden ausschließlich für die Anzeige deutsch und begrenzt formatiert;
|
||||||
gespeicherte Werte und Bearbeitungsentwürfe behalten ihre volle Genauigkeit.
|
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
|
`circuit.reorder-section` speichert die erwartete und neue Sortierposition
|
||||||
jedes Stromkreises eines vollständigen Abschnitts. Forward, Undo und Redo
|
jedes Stromkreises eines vollständigen Abschnitts. Forward, Undo und Redo
|
||||||
ändern ausschließlich `sortOrder`; Stromkreisblöcke, Gerätezeilen und BMKs
|
ä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
|
Etagenreferenz sowie eine Netzart aus dem Projektkatalog. Anlage und
|
||||||
nachträgliche Bearbeitung prüfen die Projektzugehörigkeit der Etage und die
|
nachträgliche Bearbeitung prüfen die Projektzugehörigkeit der Etage und die
|
||||||
Freigabe der Netzart in den Projekteinstellungen.
|
Freigabe der Netzart in den Projekteinstellungen.
|
||||||
`distribution-board.update` versioniert Etage und Netzart gemeinsam und stellt
|
`distribution-board.update` versioniert Etage, Netzart und den
|
||||||
beide Werte über dauerhaftes Undo/Redo wieder her. Snapshot-Schema 4 und der
|
verteilerweiten Gleichzeitigkeitsfaktor gemeinsam und stellt alle Werte über
|
||||||
portable Projekttransfer enthalten diese Felder; Schema 1/2 sowie gespeicherte
|
dauerhaftes Undo/Redo wieder her. Der Faktor liegt zwischen `0` und `1` und
|
||||||
Version-1-Strukturcommands werden ohne erfundene Zuordnung hochgestuft.
|
ist für bestehende sowie neu angelegte Verteilungen standardmäßig `1`.
|
||||||
Snapshot-Schema 3 wird mit allen sechs Netzarten als Projektauswahl
|
Snapshot-Schema 6 und der portable Projekttransfer enthalten diesen Wert;
|
||||||
hochgestuft.
|
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
|
`project-floor.insert` und `project-room.insert` versionieren die Anlage von
|
||||||
Geschossen und Räumen mit stabilen UUIDs. Die vollständigen Datensätze bilden
|
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
|
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
|
stored values. Version-four and older imports remain supported and are
|
||||||
normalized while being upgraded.
|
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
|
## Recommended Sequence
|
||||||
|
|
||||||
1. [x] Distribution-board floor and supply fields.
|
1. [x] Distribution-board floor and supply fields.
|
||||||
2. [x] Device voltage derivation without overrides.
|
2. [x] Device voltage derivation without overrides.
|
||||||
3. [x] Snapshot-to-revision descriptions and history presentation.
|
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
|
The Revit/CSV/IFCGUID round-trip remains a separate jointly planned phase and
|
||||||
must not be inferred from these tasks.
|
must not be inferred from these tasks.
|
||||||
|
|||||||
@@ -116,6 +116,39 @@ body {
|
|||||||
font-weight: 600;
|
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 {
|
.column-settings-menu {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 9;
|
z-index: 9;
|
||||||
@@ -695,6 +728,18 @@ body {
|
|||||||
outline-offset: -2px;
|
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 {
|
.tree-grid .cell-invalid {
|
||||||
outline: 2px solid #c2410c;
|
outline: 2px solid #c2410c;
|
||||||
outline-offset: -2px;
|
outline-offset: -2px;
|
||||||
|
|||||||
@@ -87,6 +87,10 @@ export default function ProjectDetailPage() {
|
|||||||
const [editingBoardFloorId, setEditingBoardFloorId] = useState("");
|
const [editingBoardFloorId, setEditingBoardFloorId] = useState("");
|
||||||
const [editingBoardSupplyType, setEditingBoardSupplyType] =
|
const [editingBoardSupplyType, setEditingBoardSupplyType] =
|
||||||
useState<DistributionBoardSupplyType>("AV");
|
useState<DistributionBoardSupplyType>("AV");
|
||||||
|
const [
|
||||||
|
editingBoardSimultaneityFactor,
|
||||||
|
setEditingBoardSimultaneityFactor,
|
||||||
|
] = useState("1");
|
||||||
const [floorName, setFloorName] = useState("");
|
const [floorName, setFloorName] = useState("");
|
||||||
const [roomNumber, setRoomNumber] = useState("");
|
const [roomNumber, setRoomNumber] = useState("");
|
||||||
const [roomName, setRoomName] = useState("");
|
const [roomName, setRoomName] = useState("");
|
||||||
@@ -398,6 +402,9 @@ export default function ProjectDetailPage() {
|
|||||||
project?.enabledDistributionBoardSupplyTypes[0] ??
|
project?.enabledDistributionBoardSupplyTypes[0] ??
|
||||||
"AV"
|
"AV"
|
||||||
);
|
);
|
||||||
|
setEditingBoardSimultaneityFactor(
|
||||||
|
String(board.simultaneityFactor)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function openBoardCreator() {
|
function openBoardCreator() {
|
||||||
@@ -412,6 +419,17 @@ export default function ProjectDetailPage() {
|
|||||||
if (!projectId || !project || !editingBoard) {
|
if (!projectId || !project || !editingBoard) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const simultaneityFactor = Number(
|
||||||
|
editingBoardSimultaneityFactor
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
!editingBoardSimultaneityFactor.trim() ||
|
||||||
|
!Number.isFinite(simultaneityFactor) ||
|
||||||
|
simultaneityFactor < 0 ||
|
||||||
|
simultaneityFactor > 1
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
@@ -421,6 +439,7 @@ export default function ProjectDetailPage() {
|
|||||||
{
|
{
|
||||||
floorId: editingBoardFloorId || null,
|
floorId: editingBoardFloorId || null,
|
||||||
supplyType: editingBoardSupplyType,
|
supplyType: editingBoardSupplyType,
|
||||||
|
simultaneityFactor,
|
||||||
},
|
},
|
||||||
project.currentRevision
|
project.currentRevision
|
||||||
);
|
);
|
||||||
@@ -702,6 +721,7 @@ export default function ProjectDetailPage() {
|
|||||||
<th>Verteilung</th>
|
<th>Verteilung</th>
|
||||||
<th>Etage</th>
|
<th>Etage</th>
|
||||||
<th>Netzart</th>
|
<th>Netzart</th>
|
||||||
|
<th>GZF</th>
|
||||||
<th className="text-end">Aktionen</th>
|
<th className="text-end">Aktionen</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -723,6 +743,15 @@ export default function ProjectDetailPage() {
|
|||||||
]
|
]
|
||||||
: "Nicht festgelegt"}
|
: "Nicht festgelegt"}
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
{board.simultaneityFactor.toLocaleString(
|
||||||
|
"de-DE",
|
||||||
|
{
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
<td className="text-end">
|
<td className="text-end">
|
||||||
<div className="d-inline-flex flex-wrap justify-content-end gap-2">
|
<div className="d-inline-flex flex-wrap justify-content-end gap-2">
|
||||||
<button
|
<button
|
||||||
@@ -752,7 +781,7 @@ export default function ProjectDetailPage() {
|
|||||||
})}
|
})}
|
||||||
{!boards.length ? (
|
{!boards.length ? (
|
||||||
<tr>
|
<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.
|
Noch keine Verteilungen vorhanden.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -1131,10 +1160,18 @@ export default function ProjectDetailPage() {
|
|||||||
|
|
||||||
{editingBoard ? (
|
{editingBoard ? (
|
||||||
<FormModal
|
<FormModal
|
||||||
description={`${editingBoard.name}: Etagenzuordnung und Netzart ändern.`}
|
description={`${editingBoard.name}: Etagenzuordnung, Netzart und Gleichzeitigkeitsfaktor ändern.`}
|
||||||
isSaving={isSaving}
|
isSaving={isSaving}
|
||||||
onClose={() => setEditingBoard(null)}
|
onClose={() => setEditingBoard(null)}
|
||||||
onSubmit={handleUpdateBoard}
|
onSubmit={handleUpdateBoard}
|
||||||
|
submitDisabled={
|
||||||
|
!editingBoardSimultaneityFactor.trim() ||
|
||||||
|
!Number.isFinite(
|
||||||
|
Number(editingBoardSimultaneityFactor)
|
||||||
|
) ||
|
||||||
|
Number(editingBoardSimultaneityFactor) < 0 ||
|
||||||
|
Number(editingBoardSimultaneityFactor) > 1
|
||||||
|
}
|
||||||
submitLabel="Änderungen speichern"
|
submitLabel="Änderungen speichern"
|
||||||
title="Verteilung bearbeiten"
|
title="Verteilung bearbeiten"
|
||||||
>
|
>
|
||||||
@@ -1187,6 +1224,32 @@ export default function ProjectDetailPage() {
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</FormModal>
|
</FormModal>
|
||||||
) : null}
|
) : 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,
|
"when": 1785314626476,
|
||||||
"tag": "0022_default_remaining_phase_types",
|
"tag": "0022_default_remaining_phase_types",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 23,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1785343645615,
|
||||||
|
"tag": "0023_cheerful_night_thrasher",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
assertDistributionBoardUpdateProjectCommand,
|
assertDistributionBoardUpdateProjectCommand,
|
||||||
createDistributionBoardUpdateProjectCommand,
|
createDistributionBoardUpdateProjectCommand,
|
||||||
|
legacyDistributionBoardUpdateCommandSchemaVersion,
|
||||||
type DistributionBoardUpdateField,
|
type DistributionBoardUpdateField,
|
||||||
type DistributionBoardUpdatePatch,
|
type DistributionBoardUpdatePatch,
|
||||||
type DistributionBoardUpdateProjectCommand,
|
type DistributionBoardUpdateProjectCommand,
|
||||||
@@ -303,10 +304,19 @@ export class DistributionBoardStructureProjectCommandRepository
|
|||||||
getDistributionBoardFieldValue(current, change.field),
|
getDistributionBoardFieldValue(current, change.field),
|
||||||
])
|
])
|
||||||
) as DistributionBoardUpdatePatch;
|
) as DistributionBoardUpdatePatch;
|
||||||
const inverse = createDistributionBoardUpdateProjectCommand(
|
const currentInverse = createDistributionBoardUpdateProjectCommand(
|
||||||
current.id,
|
current.id,
|
||||||
inversePatch
|
inversePatch
|
||||||
);
|
);
|
||||||
|
const inverse =
|
||||||
|
input.command.schemaVersion ===
|
||||||
|
legacyDistributionBoardUpdateCommandSchemaVersion
|
||||||
|
? ({
|
||||||
|
...currentInverse,
|
||||||
|
schemaVersion:
|
||||||
|
legacyDistributionBoardUpdateCommandSchemaVersion,
|
||||||
|
} as DistributionBoardUpdateProjectCommand)
|
||||||
|
: currentInverse;
|
||||||
const updated = database
|
const updated = database
|
||||||
.update(distributionBoards)
|
.update(distributionBoards)
|
||||||
.set(patch)
|
.set(patch)
|
||||||
@@ -379,8 +389,16 @@ function structureMatches(
|
|||||||
sections: Array<typeof circuitSections.$inferSelect>;
|
sections: Array<typeof circuitSections.$inferSelect>;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
const {
|
||||||
|
simultaneityFactor,
|
||||||
|
...actualDistributionBoard
|
||||||
|
} = actual.board;
|
||||||
if (
|
if (
|
||||||
!sameRecord(expected.distributionBoard, actual.board) ||
|
simultaneityFactor !== 1 ||
|
||||||
|
!sameRecord(
|
||||||
|
expected.distributionBoard,
|
||||||
|
actualDistributionBoard
|
||||||
|
) ||
|
||||||
!sameRecord(expected.circuitList, actual.list) ||
|
!sameRecord(expected.circuitList, actual.list) ||
|
||||||
expected.sections.length !== actual.sections.length
|
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 type { AppDatabase } from "../database-context.js";
|
||||||
import { distributionBoards } from "../schema/distribution-boards.js";
|
import { distributionBoards } from "../schema/distribution-boards.js";
|
||||||
|
|
||||||
@@ -12,4 +12,19 @@ export class DistributionBoardRepository {
|
|||||||
.where(eq(distributionBoards.projectId, projectId));
|
.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,
|
previousProjectStateSnapshotSchemaVersion,
|
||||||
projectStateSnapshotSchemaVersion,
|
projectStateSnapshotSchemaVersion,
|
||||||
supplyTypesProjectStateSnapshotSchemaVersion,
|
supplyTypesProjectStateSnapshotSchemaVersion,
|
||||||
|
voltageProjectStateSnapshotSchemaVersion,
|
||||||
} from "../../domain/models/project-state-snapshot.model.js";
|
} from "../../domain/models/project-state-snapshot.model.js";
|
||||||
import type {
|
import type {
|
||||||
CreateNamedProjectSnapshotInput,
|
CreateNamedProjectSnapshotInput,
|
||||||
@@ -149,6 +150,8 @@ export class ProjectSnapshotRepository implements ProjectSnapshotStore {
|
|||||||
distributionBoardProjectStateSnapshotSchemaVersion &&
|
distributionBoardProjectStateSnapshotSchemaVersion &&
|
||||||
stored.schemaVersion !==
|
stored.schemaVersion !==
|
||||||
supplyTypesProjectStateSnapshotSchemaVersion &&
|
supplyTypesProjectStateSnapshotSchemaVersion &&
|
||||||
|
stored.schemaVersion !==
|
||||||
|
voltageProjectStateSnapshotSchemaVersion &&
|
||||||
stored.schemaVersion !== projectStateSnapshotSchemaVersion
|
stored.schemaVersion !== projectStateSnapshotSchemaVersion
|
||||||
) {
|
) {
|
||||||
throw new Error("Project snapshot schema version is not supported.");
|
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 { floors } from "./floors.js";
|
||||||
import { projects } from "./projects.js";
|
import { projects } from "./projects.js";
|
||||||
import type { DistributionBoardSupplyType } from "../../shared/constants/distribution-board.js";
|
import type { DistributionBoardSupplyType } from "../../shared/constants/distribution-board.js";
|
||||||
@@ -13,5 +13,8 @@ export const distributionBoards = sqliteTable("distribution_boards", {
|
|||||||
onDelete: "set null",
|
onDelete: "set null",
|
||||||
}),
|
}),
|
||||||
supplyType: text("supply_type").$type<DistributionBoardSupplyType>(),
|
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;
|
displayName: string;
|
||||||
prefix: string;
|
prefix: string;
|
||||||
sortOrder: number;
|
sortOrder: number;
|
||||||
|
sectionTotalPower: number;
|
||||||
circuits: CircuitTreeCircuit[];
|
circuits: CircuitTreeCircuit[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,6 +61,9 @@ export interface CircuitTreeResponse {
|
|||||||
currentRevision: number;
|
currentRevision: number;
|
||||||
singlePhaseVoltageV: number;
|
singlePhaseVoltageV: number;
|
||||||
threePhaseVoltageV: number;
|
threePhaseVoltageV: number;
|
||||||
|
distributionBoardSimultaneityFactor: number;
|
||||||
|
distributionBoardTotalPower: number;
|
||||||
|
distributionBoardTotalPowerWithSimultaneityFactor: number;
|
||||||
sections: CircuitTreeSectionBlock[];
|
sections: CircuitTreeSectionBlock[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ import type { SerializedProjectCommand } from "./project-command.model.js";
|
|||||||
|
|
||||||
export const distributionBoardUpdateCommandType =
|
export const distributionBoardUpdateCommandType =
|
||||||
"distribution-board.update" as const;
|
"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 {
|
export interface DistributionBoardUpdateValues {
|
||||||
floorId: string | null;
|
floorId: string | null;
|
||||||
supplyType: DistributionBoardSupplyType | null;
|
supplyType: DistributionBoardSupplyType | null;
|
||||||
|
simultaneityFactor: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DistributionBoardUpdateField =
|
export type DistributionBoardUpdateField =
|
||||||
@@ -31,17 +33,27 @@ export interface DistributionBoardUpdateCommandPayload {
|
|||||||
changes: DistributionBoardUpdateFieldChange[];
|
changes: DistributionBoardUpdateFieldChange[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DistributionBoardUpdateProjectCommand
|
interface CurrentDistributionBoardUpdateProjectCommand
|
||||||
extends SerializedProjectCommand<DistributionBoardUpdateCommandPayload> {
|
extends SerializedProjectCommand<DistributionBoardUpdateCommandPayload> {
|
||||||
schemaVersion: typeof distributionBoardUpdateCommandSchemaVersion;
|
schemaVersion: typeof distributionBoardUpdateCommandSchemaVersion;
|
||||||
type: typeof distributionBoardUpdateCommandType;
|
type: typeof distributionBoardUpdateCommandType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface LegacyDistributionBoardUpdateProjectCommand
|
||||||
|
extends SerializedProjectCommand<DistributionBoardUpdateCommandPayload> {
|
||||||
|
schemaVersion: typeof legacyDistributionBoardUpdateCommandSchemaVersion;
|
||||||
|
type: typeof distributionBoardUpdateCommandType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DistributionBoardUpdateProjectCommand =
|
||||||
|
| CurrentDistributionBoardUpdateProjectCommand
|
||||||
|
| LegacyDistributionBoardUpdateProjectCommand;
|
||||||
|
|
||||||
export function createDistributionBoardUpdateProjectCommand(
|
export function createDistributionBoardUpdateProjectCommand(
|
||||||
distributionBoardId: string,
|
distributionBoardId: string,
|
||||||
patch: DistributionBoardUpdatePatch
|
patch: DistributionBoardUpdatePatch
|
||||||
): DistributionBoardUpdateProjectCommand {
|
): CurrentDistributionBoardUpdateProjectCommand {
|
||||||
const command: DistributionBoardUpdateProjectCommand = {
|
const command: CurrentDistributionBoardUpdateProjectCommand = {
|
||||||
schemaVersion: distributionBoardUpdateCommandSchemaVersion,
|
schemaVersion: distributionBoardUpdateCommandSchemaVersion,
|
||||||
type: distributionBoardUpdateCommandType,
|
type: distributionBoardUpdateCommandType,
|
||||||
payload: {
|
payload: {
|
||||||
@@ -60,7 +72,9 @@ export function assertDistributionBoardUpdateProjectCommand(
|
|||||||
command: SerializedProjectCommand<unknown>
|
command: SerializedProjectCommand<unknown>
|
||||||
): asserts command is DistributionBoardUpdateProjectCommand {
|
): asserts command is DistributionBoardUpdateProjectCommand {
|
||||||
if (
|
if (
|
||||||
command.schemaVersion !== distributionBoardUpdateCommandSchemaVersion ||
|
(command.schemaVersion !==
|
||||||
|
legacyDistributionBoardUpdateCommandSchemaVersion &&
|
||||||
|
command.schemaVersion !== distributionBoardUpdateCommandSchemaVersion) ||
|
||||||
command.type !== distributionBoardUpdateCommandType ||
|
command.type !== distributionBoardUpdateCommandType ||
|
||||||
!isPlainObject(command.payload)
|
!isPlainObject(command.payload)
|
||||||
) {
|
) {
|
||||||
@@ -81,7 +95,11 @@ export function assertDistributionBoardUpdateProjectCommand(
|
|||||||
for (const change of changes) {
|
for (const change of changes) {
|
||||||
if (
|
if (
|
||||||
!isPlainObject(change) ||
|
!isPlainObject(change) ||
|
||||||
(change.field !== "floorId" && change.field !== "supplyType") ||
|
(change.field !== "floorId" &&
|
||||||
|
change.field !== "supplyType" &&
|
||||||
|
(command.schemaVersion ===
|
||||||
|
legacyDistributionBoardUpdateCommandSchemaVersion ||
|
||||||
|
change.field !== "simultaneityFactor")) ||
|
||||||
seen.has(change.field)
|
seen.has(change.field)
|
||||||
) {
|
) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -95,13 +113,24 @@ export function assertDistributionBoardUpdateProjectCommand(
|
|||||||
) {
|
) {
|
||||||
throw new Error("floorId must be a non-empty string or null.");
|
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 (
|
} else if (
|
||||||
change.value !== null &&
|
typeof change.value !== "number" ||
|
||||||
!distributionBoardSupplyTypes.includes(
|
!Number.isFinite(change.value) ||
|
||||||
change.value as DistributionBoardSupplyType
|
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);
|
seen.add(change.field);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ export const legacyProjectStateSnapshotSchemaVersion = 1 as const;
|
|||||||
export const previousProjectStateSnapshotSchemaVersion = 2 as const;
|
export const previousProjectStateSnapshotSchemaVersion = 2 as const;
|
||||||
export const distributionBoardProjectStateSnapshotSchemaVersion = 3 as const;
|
export const distributionBoardProjectStateSnapshotSchemaVersion = 3 as const;
|
||||||
export const supplyTypesProjectStateSnapshotSchemaVersion = 4 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 idSchema = z.string().trim().min(1);
|
||||||
const nullableStringSchema = z.string().nullable();
|
const nullableStringSchema = z.string().nullable();
|
||||||
@@ -57,6 +58,12 @@ const distributionBoardSchema = legacyDistributionBoardSchema
|
|||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
|
const currentDistributionBoardSchema = distributionBoardSchema
|
||||||
|
.extend({
|
||||||
|
simultaneityFactor: finiteNumberSchema.min(0).max(1),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
const circuitListSchema = z
|
const circuitListSchema = z
|
||||||
.object({
|
.object({
|
||||||
id: idSchema,
|
id: idSchema,
|
||||||
@@ -213,9 +220,17 @@ const supplyTypesProjectStateSnapshotSchema = z
|
|||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
export const projectStateSnapshotSchema =
|
const voltageProjectStateSnapshotSchema =
|
||||||
supplyTypesProjectStateSnapshotSchema.extend({
|
supplyTypesProjectStateSnapshotSchema.extend({
|
||||||
|
schemaVersion: z.literal(
|
||||||
|
voltageProjectStateSnapshotSchemaVersion
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const projectStateSnapshotSchema =
|
||||||
|
voltageProjectStateSnapshotSchema.extend({
|
||||||
schemaVersion: z.literal(projectStateSnapshotSchemaVersion),
|
schemaVersion: z.literal(projectStateSnapshotSchemaVersion),
|
||||||
|
distributionBoards: z.array(currentDistributionBoardSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type ProjectStateSnapshot = z.infer<
|
export type ProjectStateSnapshot = z.infer<
|
||||||
@@ -228,34 +243,46 @@ export function parseProjectStateSnapshot(
|
|||||||
const version = isPlainObject(value) ? value.schemaVersion : undefined;
|
const version = isPlainObject(value) ? value.schemaVersion : undefined;
|
||||||
const parsedSnapshot =
|
const parsedSnapshot =
|
||||||
version === legacyProjectStateSnapshotSchemaVersion
|
version === legacyProjectStateSnapshotSchemaVersion
|
||||||
? upgradeSupplyTypesProjectStateSnapshot(
|
? upgradeVoltageProjectStateSnapshot(
|
||||||
upgradeDistributionBoardProjectStateSnapshot(
|
upgradeSupplyTypesProjectStateSnapshot(
|
||||||
upgradePreviousProjectStateSnapshot(
|
upgradeDistributionBoardProjectStateSnapshot(
|
||||||
upgradeLegacyProjectStateSnapshot(
|
upgradePreviousProjectStateSnapshot(
|
||||||
legacyProjectStateSnapshotSchema.parse(value)
|
upgradeLegacyProjectStateSnapshot(
|
||||||
|
legacyProjectStateSnapshotSchema.parse(value)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
: version === previousProjectStateSnapshotSchemaVersion
|
: version === previousProjectStateSnapshotSchemaVersion
|
||||||
? upgradeSupplyTypesProjectStateSnapshot(
|
? upgradeVoltageProjectStateSnapshot(
|
||||||
upgradeDistributionBoardProjectStateSnapshot(
|
upgradeSupplyTypesProjectStateSnapshot(
|
||||||
upgradePreviousProjectStateSnapshot(
|
upgradeDistributionBoardProjectStateSnapshot(
|
||||||
previousProjectStateSnapshotSchema.parse(value)
|
upgradePreviousProjectStateSnapshot(
|
||||||
|
previousProjectStateSnapshotSchema.parse(value)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
: version === distributionBoardProjectStateSnapshotSchemaVersion
|
: version === distributionBoardProjectStateSnapshotSchemaVersion
|
||||||
? upgradeSupplyTypesProjectStateSnapshot(
|
? upgradeVoltageProjectStateSnapshot(
|
||||||
upgradeDistributionBoardProjectStateSnapshot(
|
upgradeSupplyTypesProjectStateSnapshot(
|
||||||
distributionBoardProjectStateSnapshotSchema.parse(value)
|
upgradeDistributionBoardProjectStateSnapshot(
|
||||||
|
distributionBoardProjectStateSnapshotSchema.parse(value)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
: version === supplyTypesProjectStateSnapshotSchemaVersion
|
: version === supplyTypesProjectStateSnapshotSchemaVersion
|
||||||
? upgradeSupplyTypesProjectStateSnapshot(
|
? upgradeVoltageProjectStateSnapshot(
|
||||||
supplyTypesProjectStateSnapshotSchema.parse(value)
|
upgradeSupplyTypesProjectStateSnapshot(
|
||||||
|
supplyTypesProjectStateSnapshotSchema.parse(value)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
: projectStateSnapshotSchema.parse(value);
|
: version === voltageProjectStateSnapshotSchemaVersion
|
||||||
|
? upgradeVoltageProjectStateSnapshot(
|
||||||
|
voltageProjectStateSnapshotSchema.parse(value)
|
||||||
|
)
|
||||||
|
: projectStateSnapshotSchema.parse(value);
|
||||||
const snapshot = normalizeSnapshotPhaseTypes(parsedSnapshot);
|
const snapshot = normalizeSnapshotPhaseTypes(parsedSnapshot);
|
||||||
assertProjectStateSnapshotRelations(snapshot);
|
assertProjectStateSnapshotRelations(snapshot);
|
||||||
return snapshot;
|
return snapshot;
|
||||||
@@ -354,14 +381,14 @@ function upgradeDistributionBoardProjectStateSnapshot(
|
|||||||
|
|
||||||
function upgradeSupplyTypesProjectStateSnapshot(
|
function upgradeSupplyTypesProjectStateSnapshot(
|
||||||
snapshot: z.infer<typeof supplyTypesProjectStateSnapshotSchema>
|
snapshot: z.infer<typeof supplyTypesProjectStateSnapshotSchema>
|
||||||
): ProjectStateSnapshot {
|
): z.infer<typeof voltageProjectStateSnapshotSchema> {
|
||||||
const settings = snapshot.project;
|
const settings = snapshot.project;
|
||||||
const sectionById = new Map(
|
const sectionById = new Map(
|
||||||
snapshot.circuitSections.map((section) => [section.id, section])
|
snapshot.circuitSections.map((section) => [section.id, section])
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
...snapshot,
|
...snapshot,
|
||||||
schemaVersion: projectStateSnapshotSchemaVersion,
|
schemaVersion: voltageProjectStateSnapshotSchemaVersion,
|
||||||
projectDevices: snapshot.projectDevices.map((device) => ({
|
projectDevices: snapshot.projectDevices.map((device) => ({
|
||||||
...device,
|
...device,
|
||||||
voltageV: resolveProjectVoltage(device.phaseType, settings),
|
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> {
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,11 @@ import {
|
|||||||
deviceFieldKeys,
|
deviceFieldKeys,
|
||||||
formatPhaseTypeLabel,
|
formatPhaseTypeLabel,
|
||||||
formatValue,
|
formatValue,
|
||||||
|
getCircuitSectionLabel,
|
||||||
|
getProjectColumnLayoutStorageKey,
|
||||||
isGridEditorControlTarget,
|
isGridEditorControlTarget,
|
||||||
|
normalizeColumnOrder,
|
||||||
|
parseStoredColumnLayout,
|
||||||
} from "../utils/circuit-grid-model";
|
} from "../utils/circuit-grid-model";
|
||||||
import {
|
import {
|
||||||
buildCircuitDeviceRowInsertSnapshot,
|
buildCircuitDeviceRowInsertSnapshot,
|
||||||
@@ -131,7 +135,8 @@ type CircuitReorderDropIntent =
|
|||||||
| { kind: "after-circuit"; sectionId: string; targetCircuitId: string; valid: boolean }
|
| { kind: "after-circuit"; sectionId: string; targetCircuitId: string; valid: boolean }
|
||||||
| { kind: "section-end"; sectionId: 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 {
|
function normalizeUiError(err: unknown): string {
|
||||||
const message = err instanceof Error ? err.message : "Der Vorgang ist fehlgeschlagen.";
|
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 [filterValueSearch, setFilterValueSearch] = useState("");
|
||||||
const [visibleColumnKeys, setVisibleColumnKeys] = useState<CellKey[]>(defaultVisibleColumnKeys);
|
const [visibleColumnKeys, setVisibleColumnKeys] = useState<CellKey[]>(defaultVisibleColumnKeys);
|
||||||
const [columnOrder, setColumnOrder] = useState<CellKey[]>(allColumns.map((column) => column.key));
|
const [columnOrder, setColumnOrder] = useState<CellKey[]>(allColumns.map((column) => column.key));
|
||||||
|
const [
|
||||||
|
loadedColumnLayoutProjectId,
|
||||||
|
setLoadedColumnLayoutProjectId,
|
||||||
|
] = useState<string | null>(null);
|
||||||
const [isColumnMenuOpen, setIsColumnMenuOpen] = useState(false);
|
const [isColumnMenuOpen, setIsColumnMenuOpen] = useState(false);
|
||||||
const [columnSearch, setColumnSearch] = useState("");
|
const [columnSearch, setColumnSearch] = useState("");
|
||||||
const [draggingColumnKey, setDraggingColumnKey] = useState<CellKey | null>(null);
|
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.
|
// Initial/identity-change tree load for current route context.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void loadTree({ showLoading: true });
|
void loadTree({ showLoading: true });
|
||||||
@@ -292,32 +294,48 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
}, [projectId]);
|
}, [projectId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
setLoadedColumnLayoutProjectId(null);
|
||||||
|
setColumnOrder(allColumns.map((column) => column.key));
|
||||||
|
setVisibleColumnKeys(defaultVisibleColumnKeys);
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(COLUMN_LAYOUT_STORAGE_KEY);
|
const parsed = parseStoredColumnLayout(
|
||||||
if (!raw) {
|
localStorage.getItem(
|
||||||
|
getProjectColumnLayoutStorageKey(projectId)
|
||||||
|
) ??
|
||||||
|
localStorage.getItem(
|
||||||
|
LEGACY_COLUMN_LAYOUT_STORAGE_KEY
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (!parsed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const parsed = JSON.parse(raw) as { order?: string[]; visible?: string[] };
|
setColumnOrder(parsed.order);
|
||||||
const validKeys = new Set(allColumns.map((column) => column.key));
|
setVisibleColumnKeys(parsed.visible);
|
||||||
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);
|
|
||||||
} catch {
|
} catch {
|
||||||
// ignore invalid local storage and use defaults
|
// ignore invalid local storage and use defaults
|
||||||
|
} finally {
|
||||||
|
setLoadedColumnLayoutProjectId(projectId);
|
||||||
}
|
}
|
||||||
}, []);
|
}, [projectId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (loadedColumnLayoutProjectId !== projectId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const payload = {
|
const payload = {
|
||||||
order: columnOrder,
|
order: columnOrder,
|
||||||
visible: visibleColumnKeys,
|
visible: visibleColumnKeys,
|
||||||
};
|
};
|
||||||
localStorage.setItem(COLUMN_LAYOUT_STORAGE_KEY, JSON.stringify(payload));
|
localStorage.setItem(
|
||||||
}, [columnOrder, visibleColumnKeys]);
|
getProjectColumnLayoutStorageKey(projectId),
|
||||||
|
JSON.stringify(payload)
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
columnOrder,
|
||||||
|
loadedColumnLayoutProjectId,
|
||||||
|
projectId,
|
||||||
|
visibleColumnKeys,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const visible = new Set(visibleColumnKeys);
|
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() {
|
function closeColumnSettingsMenu() {
|
||||||
setIsColumnMenuOpen(false);
|
setIsColumnMenuOpen(false);
|
||||||
setColumnSearch("");
|
setColumnSearch("");
|
||||||
@@ -2490,6 +2553,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
{renderColumnSettingsMenu("col-empty")}
|
{renderColumnSettingsMenu("col-empty")}
|
||||||
</div>
|
</div>
|
||||||
{renderActiveViewSummary()}
|
{renderActiveViewSummary()}
|
||||||
|
{renderDistributionPowerSummary()}
|
||||||
{error ? (
|
{error ? (
|
||||||
<div className="notice error editor-error-notice" role="alert">
|
<div className="notice error editor-error-notice" role="alert">
|
||||||
<span>{error}</span>
|
<span>{error}</span>
|
||||||
@@ -2598,6 +2662,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
{renderColumnSettingsMenu("col")}
|
{renderColumnSettingsMenu("col")}
|
||||||
</div>
|
</div>
|
||||||
{renderActiveViewSummary()}
|
{renderActiveViewSummary()}
|
||||||
|
{renderDistributionPowerSummary()}
|
||||||
{hasActiveSortOrFilter ? (
|
{hasActiveSortOrFilter ? (
|
||||||
<div className="notice muted">Sortierung und Filter verändern nur die Ansicht. Die Neunummerierung ist währenddessen deaktiviert.</div>
|
<div className="notice muted">Sortierung und Filter verändern nur die Ansicht. Die Neunummerierung ist währenddessen deaktiviert.</div>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -2701,14 +2766,18 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
const valid = !selectedProjectDevice || isProjectDevicePlacementValid(selectedProjectDevice, section);
|
const valid = !selectedProjectDevice || isProjectDevicePlacementValid(selectedProjectDevice, section);
|
||||||
return (
|
return (
|
||||||
<option key={section.id} value={section.id} disabled={!valid}>
|
<option key={section.id} value={section.id} disabled={!valid}>
|
||||||
{section.displayName}{valid ? "" : " (nicht zulässig)"}
|
{getCircuitSectionLabel(section)}
|
||||||
|
{valid ? "" : " (nicht zulässig)"}
|
||||||
</option>
|
</option>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
{selectedProjectDevice && suggestedSection ? (
|
{selectedProjectDevice && suggestedSection ? (
|
||||||
<p className="notice muted">Vorgeschlagener Bereich: {suggestedSection.displayName}</p>
|
<p className="notice muted">
|
||||||
|
Vorgeschlagener Bereich:{" "}
|
||||||
|
{getCircuitSectionLabel(suggestedSection)}
|
||||||
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -2980,7 +3049,19 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
>
|
>
|
||||||
<td colSpan={visibleColumns.length + 1} className="section-drop-cell">
|
<td colSpan={visibleColumns.length + 1} className="section-drop-cell">
|
||||||
<div className="section-content">
|
<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">
|
<div className="section-actions">
|
||||||
<button type="button" tabIndex={-1} onClick={() => void handleAddReserveCircuit(section.id)}>
|
<button type="button" tabIndex={-1} onClick={() => void handleAddReserveCircuit(section.id)}>
|
||||||
Stromkreis hinzufügen
|
Stromkreis hinzufügen
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ import { useEffect, useState } from "react";
|
|||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
import { getCircuitTree } from "../utils/api";
|
import { getCircuitTree } from "../utils/api";
|
||||||
import type { CircuitTreeCircuitDto, CircuitTreeResponseDto } from "../types";
|
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) {
|
function renderCircuitSummaryLabel(circuit: CircuitTreeCircuitDto) {
|
||||||
if (circuit.displayName?.trim()) {
|
if (circuit.displayName?.trim()) {
|
||||||
@@ -98,7 +101,7 @@ function SectionRows(props: { section: CircuitTreeResponseDto["sections"][number
|
|||||||
<>
|
<>
|
||||||
<tr className="section-row">
|
<tr className="section-row">
|
||||||
<td colSpan={21}>
|
<td colSpan={21}>
|
||||||
<strong>{section.displayName}</strong>
|
<strong>{getCircuitSectionLabel(section)}</strong>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{section.circuits.map((circuit) => {
|
{section.circuits.map((circuit) => {
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ export interface DistributionBoardDto {
|
|||||||
name: string;
|
name: string;
|
||||||
floorId: string | null;
|
floorId: string | null;
|
||||||
supplyType: DistributionBoardSupplyType | null;
|
supplyType: DistributionBoardSupplyType | null;
|
||||||
|
simultaneityFactor: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DistributionBoardCommandResultDto
|
export interface DistributionBoardCommandResultDto
|
||||||
@@ -296,6 +297,7 @@ export interface CircuitTreeSectionDto {
|
|||||||
displayName: string;
|
displayName: string;
|
||||||
prefix: string;
|
prefix: string;
|
||||||
sortOrder: number;
|
sortOrder: number;
|
||||||
|
sectionTotalPower: number;
|
||||||
circuits: CircuitTreeCircuitDto[];
|
circuits: CircuitTreeCircuitDto[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,6 +317,9 @@ export interface CircuitTreeResponseDto {
|
|||||||
currentRevision: number;
|
currentRevision: number;
|
||||||
singlePhaseVoltageV: number;
|
singlePhaseVoltageV: number;
|
||||||
threePhaseVoltageV: number;
|
threePhaseVoltageV: number;
|
||||||
|
distributionBoardSimultaneityFactor: number;
|
||||||
|
distributionBoardTotalPower: number;
|
||||||
|
distributionBoardTotalPowerWithSimultaneityFactor: number;
|
||||||
sections: CircuitTreeSectionDto[];
|
sections: CircuitTreeSectionDto[];
|
||||||
migrationReport?: CircuitTreeMigrationReportDto;
|
migrationReport?: CircuitTreeMigrationReportDto;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -299,6 +299,7 @@ export function updateDistributionBoard(
|
|||||||
input: {
|
input: {
|
||||||
floorId: string | null;
|
floorId: string | null;
|
||||||
supplyType: NonNullable<DistributionBoardDto["supplyType"]>;
|
supplyType: NonNullable<DistributionBoardDto["supplyType"]>;
|
||||||
|
simultaneityFactor: number;
|
||||||
},
|
},
|
||||||
expectedRevision: number
|
expectedRevision: number
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -124,6 +124,96 @@ export const defaultVisibleColumnKeys = allColumns
|
|||||||
.filter((column) => column.defaultVisible)
|
.filter((column) => column.defaultVisible)
|
||||||
.map((column) => column.key);
|
.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>([
|
const deviceOnlyColumns = new Set<CellKey>([
|
||||||
"quantity",
|
"quantity",
|
||||||
"powerPerUnit",
|
"powerPerUnit",
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import {
|
import {
|
||||||
|
applyDistributionBoardSimultaneityFactor,
|
||||||
calculateCircuitTotalPower,
|
calculateCircuitTotalPower,
|
||||||
|
calculateDistributionBoardTotalPower,
|
||||||
calculateRowTotalPower,
|
calculateRowTotalPower,
|
||||||
|
calculateSectionTotalPower,
|
||||||
} from "../../domain/calculations/circuit-power-calculation.js";
|
} from "../../domain/calculations/circuit-power-calculation.js";
|
||||||
import type { CircuitTreeResponse } from "../../domain/models/circuit-tree.model.js";
|
import type { CircuitTreeResponse } from "../../domain/models/circuit-tree.model.js";
|
||||||
import {
|
import {
|
||||||
@@ -9,6 +12,7 @@ import {
|
|||||||
circuitListRepository,
|
circuitListRepository,
|
||||||
circuitRepository,
|
circuitRepository,
|
||||||
circuitSectionRepository,
|
circuitSectionRepository,
|
||||||
|
distributionBoardRepository,
|
||||||
projectRepository,
|
projectRepository,
|
||||||
} from "../composition/application-repositories.js";
|
} from "../composition/application-repositories.js";
|
||||||
|
|
||||||
@@ -37,6 +41,16 @@ export async function getCircuitTree(req: Request, res: Response) {
|
|||||||
if (!project) {
|
if (!project) {
|
||||||
return res.status(404).json({ error: "Project not found" });
|
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 {
|
try {
|
||||||
const sections = await circuitSectionRepository.listByCircuitList(circuitListId);
|
const sections = await circuitSectionRepository.listByCircuitList(circuitListId);
|
||||||
@@ -57,12 +71,17 @@ export async function getCircuitTree(req: Request, res: Response) {
|
|||||||
currentRevision: project.currentRevision,
|
currentRevision: project.currentRevision,
|
||||||
singlePhaseVoltageV: project.singlePhaseVoltageV,
|
singlePhaseVoltageV: project.singlePhaseVoltageV,
|
||||||
threePhaseVoltageV: project.threePhaseVoltageV,
|
threePhaseVoltageV: project.threePhaseVoltageV,
|
||||||
|
distributionBoardSimultaneityFactor:
|
||||||
|
distributionBoard.simultaneityFactor,
|
||||||
|
distributionBoardTotalPower: 0,
|
||||||
|
distributionBoardTotalPowerWithSimultaneityFactor: 0,
|
||||||
sections: sections.map((section) => ({
|
sections: sections.map((section) => ({
|
||||||
id: section.id,
|
id: section.id,
|
||||||
key: section.key,
|
key: section.key,
|
||||||
displayName: section.displayName,
|
displayName: section.displayName,
|
||||||
prefix: section.prefix,
|
prefix: section.prefix,
|
||||||
sortOrder: section.sortOrder,
|
sortOrder: section.sortOrder,
|
||||||
|
sectionTotalPower: 0,
|
||||||
circuits: [],
|
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);
|
return res.json(tree);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isMissingCircuitTreeSchemaError(error)) {
|
if (isMissingCircuitTreeSchemaError(error)) {
|
||||||
@@ -131,6 +163,10 @@ export async function getCircuitTree(req: Request, res: Response) {
|
|||||||
currentRevision: project.currentRevision,
|
currentRevision: project.currentRevision,
|
||||||
singlePhaseVoltageV: project.singlePhaseVoltageV,
|
singlePhaseVoltageV: project.singlePhaseVoltageV,
|
||||||
threePhaseVoltageV: project.threePhaseVoltageV,
|
threePhaseVoltageV: project.threePhaseVoltageV,
|
||||||
|
distributionBoardSimultaneityFactor:
|
||||||
|
distributionBoard.simultaneityFactor,
|
||||||
|
distributionBoardTotalPower: 0,
|
||||||
|
distributionBoardTotalPowerWithSimultaneityFactor: 0,
|
||||||
sections: [],
|
sections: [],
|
||||||
warning:
|
warning:
|
||||||
"Circuit-first tables are not available yet. Run database migrations (including 0008_circuit_first_model).",
|
"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({
|
return res.status(201).json({
|
||||||
...result,
|
...result,
|
||||||
distributionBoard: structure.distributionBoard,
|
distributionBoard: {
|
||||||
|
...structure.distributionBoard,
|
||||||
|
simultaneityFactor: 1,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return respondWithProjectCommandError(error, res);
|
return respondWithProjectCommandError(error, res);
|
||||||
@@ -83,6 +86,7 @@ export async function updateDistributionBoard(
|
|||||||
{
|
{
|
||||||
floorId: parsed.data.floorId,
|
floorId: parsed.data.floorId,
|
||||||
supplyType: parsed.data.supplyType,
|
supplyType: parsed.data.supplyType,
|
||||||
|
simultaneityFactor: parsed.data.simultaneityFactor,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export const updateDistributionBoardSchema = z
|
|||||||
expectedRevision: expectedProjectRevisionSchema,
|
expectedRevision: expectedProjectRevisionSchema,
|
||||||
floorId: z.string().trim().min(1).nullable(),
|
floorId: z.string().trim().min(1).nullable(),
|
||||||
supplyType: z.enum(distributionBoardSupplyTypes),
|
supplyType: z.enum(distributionBoardSupplyTypes),
|
||||||
|
simultaneityFactor: z.number().finite().min(0).max(1),
|
||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,14 @@ import {
|
|||||||
buildCircuitEditPatch,
|
buildCircuitEditPatch,
|
||||||
buildDeviceRowEditPatch,
|
buildDeviceRowEditPatch,
|
||||||
formatValue,
|
formatValue,
|
||||||
|
getCircuitSectionLabel,
|
||||||
|
getProjectColumnLayoutStorageKey,
|
||||||
isGridEditorControlTarget,
|
isGridEditorControlTarget,
|
||||||
getBlockSortValue,
|
getBlockSortValue,
|
||||||
getCellKind,
|
getCellKind,
|
||||||
getCircuitValue,
|
getCircuitValue,
|
||||||
getDeviceValue,
|
getDeviceValue,
|
||||||
|
parseStoredColumnLayout,
|
||||||
parseNumeric,
|
parseNumeric,
|
||||||
} from "../src/frontend/utils/circuit-grid-model.js";
|
} from "../src/frontend/utils/circuit-grid-model.js";
|
||||||
import type { CircuitTreeCircuitDto, CircuitTreeDeviceRowDto } from "../src/frontend/types.js";
|
import type { CircuitTreeCircuitDto, CircuitTreeDeviceRowDto } from "../src/frontend/types.js";
|
||||||
@@ -73,6 +76,47 @@ describe("circuit grid model", () => {
|
|||||||
assert.equal(allColumns[0].locked, true);
|
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", () => {
|
it("maps shared fields to the correct level for every row shape", () => {
|
||||||
assert.equal(getCellKind("circuitCompact", "displayName"), "deviceField");
|
assert.equal(getCellKind("circuitCompact", "displayName"), "deviceField");
|
||||||
assert.equal(getCellKind("circuitCompact", "equipmentIdentifier"), "circuitField");
|
assert.equal(getCellKind("circuitCompact", "equipmentIdentifier"), "circuitField");
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import { describe, it } from "node:test";
|
import { describe, it } from "node:test";
|
||||||
import {
|
import {
|
||||||
|
applyDistributionBoardSimultaneityFactor,
|
||||||
calculateCircuitTotalPower,
|
calculateCircuitTotalPower,
|
||||||
|
calculateDistributionBoardTotalPower,
|
||||||
calculateRowTotalPower,
|
calculateRowTotalPower,
|
||||||
|
calculateSectionTotalPower,
|
||||||
} from "../src/domain/calculations/circuit-power-calculation.js";
|
} from "../src/domain/calculations/circuit-power-calculation.js";
|
||||||
import {
|
import {
|
||||||
resolveCircuitPhaseType,
|
resolveCircuitPhaseType,
|
||||||
@@ -19,6 +22,32 @@ describe("circuit power calculation", () => {
|
|||||||
]);
|
]);
|
||||||
assert.equal(total, 4.5);
|
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", () => {
|
describe("project voltage derivation", () => {
|
||||||
|
|||||||
@@ -22,7 +22,10 @@ import {
|
|||||||
createDistributionBoardStructureSnapshot,
|
createDistributionBoardStructureSnapshot,
|
||||||
type DistributionBoardStructureProjectCommand,
|
type DistributionBoardStructureProjectCommand,
|
||||||
} from "../src/domain/models/distribution-board-structure-project-command.model.js";
|
} 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 {
|
import {
|
||||||
createDistributionBoard,
|
createDistributionBoard,
|
||||||
updateDistributionBoard,
|
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();
|
const context = createTestDatabase();
|
||||||
try {
|
try {
|
||||||
context.db
|
context.db
|
||||||
@@ -335,7 +338,11 @@ describe("distribution-board structure project command", () => {
|
|||||||
source: "user",
|
source: "user",
|
||||||
command: createDistributionBoardUpdateProjectCommand(
|
command: createDistributionBoardUpdateProjectCommand(
|
||||||
structure.distributionBoard.id,
|
structure.distributionBoard.id,
|
||||||
{ floorId: "floor-1", supplyType: "SV" }
|
{
|
||||||
|
floorId: "floor-1",
|
||||||
|
supplyType: "SV",
|
||||||
|
simultaneityFactor: 0.75,
|
||||||
|
}
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
@@ -343,10 +350,16 @@ describe("distribution-board structure project command", () => {
|
|||||||
.select({
|
.select({
|
||||||
floorId: distributionBoards.floorId,
|
floorId: distributionBoards.floorId,
|
||||||
supplyType: distributionBoards.supplyType,
|
supplyType: distributionBoards.supplyType,
|
||||||
|
simultaneityFactor:
|
||||||
|
distributionBoards.simultaneityFactor,
|
||||||
})
|
})
|
||||||
.from(distributionBoards)
|
.from(distributionBoards)
|
||||||
.get(),
|
.get(),
|
||||||
{ floorId: "floor-1", supplyType: "SV" }
|
{
|
||||||
|
floorId: "floor-1",
|
||||||
|
supplyType: "SV",
|
||||||
|
simultaneityFactor: 0.75,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const undoStep = new ProjectHistoryRepository(
|
const undoStep = new ProjectHistoryRepository(
|
||||||
@@ -365,10 +378,16 @@ describe("distribution-board structure project command", () => {
|
|||||||
.select({
|
.select({
|
||||||
floorId: distributionBoards.floorId,
|
floorId: distributionBoards.floorId,
|
||||||
supplyType: distributionBoards.supplyType,
|
supplyType: distributionBoards.supplyType,
|
||||||
|
simultaneityFactor:
|
||||||
|
distributionBoards.simultaneityFactor,
|
||||||
})
|
})
|
||||||
.from(distributionBoards)
|
.from(distributionBoards)
|
||||||
.get(),
|
.get(),
|
||||||
{ floorId: null, supplyType: "AV" }
|
{
|
||||||
|
floorId: null,
|
||||||
|
supplyType: "AV",
|
||||||
|
simultaneityFactor: 1,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
assert.throws(
|
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 () => {
|
it("sends distribution-board updates to the revision-safe API", async () => {
|
||||||
const requests: Array<{ url: string; method: string; body: unknown }> = [];
|
const requests: Array<{ url: string; method: string; body: unknown }> = [];
|
||||||
const originalFetch = globalThis.fetch;
|
const originalFetch = globalThis.fetch;
|
||||||
@@ -413,7 +478,11 @@ describe("distribution-board structure project command", () => {
|
|||||||
await updateDistributionBoard(
|
await updateDistributionBoard(
|
||||||
"project-1",
|
"project-1",
|
||||||
"board-1",
|
"board-1",
|
||||||
{ floorId: "floor-1", supplyType: "USV" },
|
{
|
||||||
|
floorId: "floor-1",
|
||||||
|
supplyType: "USV",
|
||||||
|
simultaneityFactor: 0.8,
|
||||||
|
},
|
||||||
9
|
9
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -426,6 +495,7 @@ describe("distribution-board structure project command", () => {
|
|||||||
body: {
|
body: {
|
||||||
floorId: "floor-1",
|
floorId: "floor-1",
|
||||||
supplyType: "USV",
|
supplyType: "USV",
|
||||||
|
simultaneityFactor: 0.8,
|
||||||
expectedRevision: 9,
|
expectedRevision: 9,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -67,7 +67,11 @@ function createTestDatabase(): DatabaseContext {
|
|||||||
.run();
|
.run();
|
||||||
context.db
|
context.db
|
||||||
.update(distributionBoards)
|
.update(distributionBoards)
|
||||||
.set({ floorId: "floor-1", supplyType: "SV" })
|
.set({
|
||||||
|
floorId: "floor-1",
|
||||||
|
supplyType: "SV",
|
||||||
|
simultaneityFactor: 0.65,
|
||||||
|
})
|
||||||
.where(eq(distributionBoards.id, board.id))
|
.where(eq(distributionBoards.id, board.id))
|
||||||
.run();
|
.run();
|
||||||
context.db
|
context.db
|
||||||
@@ -176,6 +180,10 @@ describe("project snapshot repository", () => {
|
|||||||
assert.equal(state.distributionBoards.length, 1);
|
assert.equal(state.distributionBoards.length, 1);
|
||||||
assert.equal(state.distributionBoards[0].floorId, "floor-1");
|
assert.equal(state.distributionBoards[0].floorId, "floor-1");
|
||||||
assert.equal(state.distributionBoards[0].supplyType, "SV");
|
assert.equal(state.distributionBoards[0].supplyType, "SV");
|
||||||
|
assert.equal(
|
||||||
|
state.distributionBoards[0].simultaneityFactor,
|
||||||
|
0.65
|
||||||
|
);
|
||||||
assert.equal(state.circuitLists.length, 1);
|
assert.equal(state.circuitLists.length, 1);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
state.circuitSections.length,
|
state.circuitSections.length,
|
||||||
@@ -293,6 +301,7 @@ describe("project snapshot repository", () => {
|
|||||||
>) {
|
>) {
|
||||||
delete board.floorId;
|
delete board.floorId;
|
||||||
delete board.supplyType;
|
delete board.supplyType;
|
||||||
|
delete board.simultaneityFactor;
|
||||||
}
|
}
|
||||||
const legacyPayloadJson = JSON.stringify(currentPayload);
|
const legacyPayloadJson = JSON.stringify(currentPayload);
|
||||||
context.db
|
context.db
|
||||||
@@ -375,6 +384,10 @@ describe("portable project transfer", () => {
|
|||||||
copied.state.distributionBoards[0].supplyType,
|
copied.state.distributionBoards[0].supplyType,
|
||||||
"SV"
|
"SV"
|
||||||
);
|
);
|
||||||
|
assert.equal(
|
||||||
|
copied.state.distributionBoards[0].simultaneityFactor,
|
||||||
|
0.65
|
||||||
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
copied.state.circuits[0].deviceRows[0].legacyConsumerId,
|
copied.state.circuits[0].deviceRows[0].legacyConsumerId,
|
||||||
null
|
null
|
||||||
@@ -431,6 +444,7 @@ describe("portable project transfer", () => {
|
|||||||
for (const board of legacyState.distributionBoards) {
|
for (const board of legacyState.distributionBoards) {
|
||||||
delete board.floorId;
|
delete board.floorId;
|
||||||
delete board.supplyType;
|
delete board.supplyType;
|
||||||
|
delete board.simultaneityFactor;
|
||||||
}
|
}
|
||||||
const legacyPayloadJson = JSON.stringify(legacyState);
|
const legacyPayloadJson = JSON.stringify(legacyState);
|
||||||
const duplicate = repository.importDuplicate({
|
const duplicate = repository.importDuplicate({
|
||||||
@@ -448,6 +462,10 @@ describe("portable project transfer", () => {
|
|||||||
assert.ok(copied);
|
assert.ok(copied);
|
||||||
assert.equal(copied.state.distributionBoards[0].floorId, null);
|
assert.equal(copied.state.distributionBoards[0].floorId, null);
|
||||||
assert.equal(copied.state.distributionBoards[0].supplyType, null);
|
assert.equal(copied.state.distributionBoards[0].supplyType, null);
|
||||||
|
assert.equal(
|
||||||
|
copied.state.distributionBoards[0].simultaneityFactor,
|
||||||
|
1
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
context.close();
|
context.close();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ describe("project state snapshot model", () => {
|
|||||||
...previous.distributionBoards[0],
|
...previous.distributionBoards[0],
|
||||||
floorId: null,
|
floorId: null,
|
||||||
supplyType: null,
|
supplyType: null,
|
||||||
|
simultaneityFactor: 1,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@@ -158,6 +159,30 @@ describe("project state snapshot model", () => {
|
|||||||
assert.equal(snapshot.projectDevices[0]?.voltageV, 400);
|
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", () => {
|
it("rejects duplicate ids and cross-project ownership", () => {
|
||||||
assert.throws(
|
assert.throws(
|
||||||
() =>
|
() =>
|
||||||
@@ -170,6 +195,7 @@ describe("project state snapshot model", () => {
|
|||||||
name: "UV 1",
|
name: "UV 1",
|
||||||
floorId: null,
|
floorId: null,
|
||||||
supplyType: null,
|
supplyType: null,
|
||||||
|
simultaneityFactor: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "board-1",
|
id: "board-1",
|
||||||
@@ -177,6 +203,7 @@ describe("project state snapshot model", () => {
|
|||||||
name: "UV 2",
|
name: "UV 2",
|
||||||
floorId: null,
|
floorId: null,
|
||||||
supplyType: null,
|
supplyType: null,
|
||||||
|
simultaneityFactor: 1,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
@@ -211,6 +238,7 @@ describe("project state snapshot model", () => {
|
|||||||
name: "UV 1",
|
name: "UV 1",
|
||||||
floorId: "missing",
|
floorId: "missing",
|
||||||
supplyType: "AV",
|
supplyType: "AV",
|
||||||
|
simultaneityFactor: 1,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
|||||||
Reference in New Issue
Block a user