Add distribution power summaries

This commit is contained in:
2026-07-29 19:02:18 +02:00
parent dcb303284c
commit 9ea081179d
28 changed files with 2668 additions and 77 deletions
+45
View File
@@ -116,6 +116,39 @@ body {
font-weight: 600;
}
.distribution-power-summary {
display: grid;
grid-template-columns: repeat(3, minmax(12rem, 1fr));
gap: 0.5rem;
padding: 0.55rem;
border: 1px solid #cbd5e1;
border-radius: 5px;
background: #f8fafc;
}
.distribution-power-summary > div {
display: flex;
flex-direction: column;
gap: 0.15rem;
}
.distribution-power-summary span {
color: #475569;
font-size: 0.74rem;
line-height: 1.2;
}
.distribution-power-summary strong {
color: #172033;
font-size: 0.95rem;
}
@media (max-width: 900px) {
.distribution-power-summary {
grid-template-columns: 1fr;
}
}
.column-settings-menu {
position: absolute;
z-index: 9;
@@ -695,6 +728,18 @@ body {
outline-offset: -2px;
}
.tree-grid .section-title {
display: flex;
align-items: baseline;
gap: 0.75rem;
}
.tree-grid .section-title span {
color: #475569;
font-size: 0.78rem;
font-weight: 500;
}
.tree-grid .cell-invalid {
outline: 2px solid #c2410c;
outline-offset: -2px;
+65 -2
View File
@@ -87,6 +87,10 @@ export default function ProjectDetailPage() {
const [editingBoardFloorId, setEditingBoardFloorId] = useState("");
const [editingBoardSupplyType, setEditingBoardSupplyType] =
useState<DistributionBoardSupplyType>("AV");
const [
editingBoardSimultaneityFactor,
setEditingBoardSimultaneityFactor,
] = useState("1");
const [floorName, setFloorName] = useState("");
const [roomNumber, setRoomNumber] = useState("");
const [roomName, setRoomName] = useState("");
@@ -398,6 +402,9 @@ export default function ProjectDetailPage() {
project?.enabledDistributionBoardSupplyTypes[0] ??
"AV"
);
setEditingBoardSimultaneityFactor(
String(board.simultaneityFactor)
);
}
function openBoardCreator() {
@@ -412,6 +419,17 @@ export default function ProjectDetailPage() {
if (!projectId || !project || !editingBoard) {
return;
}
const simultaneityFactor = Number(
editingBoardSimultaneityFactor
);
if (
!editingBoardSimultaneityFactor.trim() ||
!Number.isFinite(simultaneityFactor) ||
simultaneityFactor < 0 ||
simultaneityFactor > 1
) {
return;
}
setIsSaving(true);
setError(null);
try {
@@ -421,6 +439,7 @@ export default function ProjectDetailPage() {
{
floorId: editingBoardFloorId || null,
supplyType: editingBoardSupplyType,
simultaneityFactor,
},
project.currentRevision
);
@@ -702,6 +721,7 @@ export default function ProjectDetailPage() {
<th>Verteilung</th>
<th>Etage</th>
<th>Netzart</th>
<th>GZF</th>
<th className="text-end">Aktionen</th>
</tr>
</thead>
@@ -723,6 +743,15 @@ export default function ProjectDetailPage() {
]
: "Nicht festgelegt"}
</td>
<td>
{board.simultaneityFactor.toLocaleString(
"de-DE",
{
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}
)}
</td>
<td className="text-end">
<div className="d-inline-flex flex-wrap justify-content-end gap-2">
<button
@@ -752,7 +781,7 @@ export default function ProjectDetailPage() {
})}
{!boards.length ? (
<tr>
<td colSpan={4} className="text-center text-secondary py-4">
<td colSpan={5} className="text-center text-secondary py-4">
Noch keine Verteilungen vorhanden.
</td>
</tr>
@@ -1131,10 +1160,18 @@ export default function ProjectDetailPage() {
{editingBoard ? (
<FormModal
description={`${editingBoard.name}: Etagenzuordnung und Netzart ändern.`}
description={`${editingBoard.name}: Etagenzuordnung, Netzart und Gleichzeitigkeitsfaktor ändern.`}
isSaving={isSaving}
onClose={() => setEditingBoard(null)}
onSubmit={handleUpdateBoard}
submitDisabled={
!editingBoardSimultaneityFactor.trim() ||
!Number.isFinite(
Number(editingBoardSimultaneityFactor)
) ||
Number(editingBoardSimultaneityFactor) < 0 ||
Number(editingBoardSimultaneityFactor) > 1
}
submitLabel="Änderungen speichern"
title="Verteilung bearbeiten"
>
@@ -1187,6 +1224,32 @@ export default function ProjectDetailPage() {
))}
</select>
</div>
<div className="col-12 col-md-6">
<label
className="form-label"
htmlFor="edit-distribution-board-simultaneity-factor"
>
Verteilerweiter Gleichzeitigkeitsfaktor
</label>
<input
className="form-control"
id="edit-distribution-board-simultaneity-factor"
max="1"
min="0"
onChange={(event) =>
setEditingBoardSimultaneityFactor(
event.target.value
)
}
required
step="0.01"
type="number"
value={editingBoardSimultaneityFactor}
/>
<div className="form-text">
Wird auf die Gesamtleistung des Verteilers angewendet.
</div>
</div>
</div>
</FormModal>
) : null}
@@ -0,0 +1 @@
ALTER TABLE `distribution_boards` ADD `simultaneity_factor` real DEFAULT 1 NOT NULL;
File diff suppressed because it is too large Load Diff
+7
View File
@@ -162,6 +162,13 @@
"when": 1785314626476,
"tag": "0022_default_remaining_phase_types",
"breakpoints": true
},
{
"idx": 23,
"version": "6",
"when": 1785343645615,
"tag": "0023_cheerful_night_thrasher",
"breakpoints": true
}
]
}
@@ -14,6 +14,7 @@ import {
import {
assertDistributionBoardUpdateProjectCommand,
createDistributionBoardUpdateProjectCommand,
legacyDistributionBoardUpdateCommandSchemaVersion,
type DistributionBoardUpdateField,
type DistributionBoardUpdatePatch,
type DistributionBoardUpdateProjectCommand,
@@ -303,10 +304,19 @@ export class DistributionBoardStructureProjectCommandRepository
getDistributionBoardFieldValue(current, change.field),
])
) as DistributionBoardUpdatePatch;
const inverse = createDistributionBoardUpdateProjectCommand(
const currentInverse = createDistributionBoardUpdateProjectCommand(
current.id,
inversePatch
);
const inverse =
input.command.schemaVersion ===
legacyDistributionBoardUpdateCommandSchemaVersion
? ({
...currentInverse,
schemaVersion:
legacyDistributionBoardUpdateCommandSchemaVersion,
} as DistributionBoardUpdateProjectCommand)
: currentInverse;
const updated = database
.update(distributionBoards)
.set(patch)
@@ -379,8 +389,16 @@ function structureMatches(
sections: Array<typeof circuitSections.$inferSelect>;
}
) {
const {
simultaneityFactor,
...actualDistributionBoard
} = actual.board;
if (
!sameRecord(expected.distributionBoard, actual.board) ||
simultaneityFactor !== 1 ||
!sameRecord(
expected.distributionBoard,
actualDistributionBoard
) ||
!sameRecord(expected.circuitList, actual.list) ||
expected.sections.length !== actual.sections.length
) {
@@ -1,4 +1,4 @@
import { eq } from "drizzle-orm";
import { and, eq } from "drizzle-orm";
import type { AppDatabase } from "../database-context.js";
import { distributionBoards } from "../schema/distribution-boards.js";
@@ -12,4 +12,19 @@ export class DistributionBoardRepository {
.where(eq(distributionBoards.projectId, projectId));
}
async findById(projectId: string, distributionBoardId: string) {
return (
this.database
.select()
.from(distributionBoards)
.where(
and(
eq(distributionBoards.projectId, projectId),
eq(distributionBoards.id, distributionBoardId)
)
)
.get() ?? null
);
}
}
@@ -10,6 +10,7 @@ import {
previousProjectStateSnapshotSchemaVersion,
projectStateSnapshotSchemaVersion,
supplyTypesProjectStateSnapshotSchemaVersion,
voltageProjectStateSnapshotSchemaVersion,
} from "../../domain/models/project-state-snapshot.model.js";
import type {
CreateNamedProjectSnapshotInput,
@@ -149,6 +150,8 @@ export class ProjectSnapshotRepository implements ProjectSnapshotStore {
distributionBoardProjectStateSnapshotSchemaVersion &&
stored.schemaVersion !==
supplyTypesProjectStateSnapshotSchemaVersion &&
stored.schemaVersion !==
voltageProjectStateSnapshotSchemaVersion &&
stored.schemaVersion !== projectStateSnapshotSchemaVersion
) {
throw new Error("Project snapshot schema version is not supported.");
+4 -1
View File
@@ -1,4 +1,4 @@
import { sqliteTable, text } from "drizzle-orm/sqlite-core";
import { real, sqliteTable, text } from "drizzle-orm/sqlite-core";
import { floors } from "./floors.js";
import { projects } from "./projects.js";
import type { DistributionBoardSupplyType } from "../../shared/constants/distribution-board.js";
@@ -13,5 +13,8 @@ export const distributionBoards = sqliteTable("distribution_boards", {
onDelete: "set null",
}),
supplyType: text("supply_type").$type<DistributionBoardSupplyType>(),
simultaneityFactor: real("simultaneity_factor")
.notNull()
.default(1),
});
@@ -15,3 +15,37 @@ export function calculateCircuitTotalPower(
);
}
export function calculateSectionTotalPower(
circuits: Array<{ circuitTotalPower: number }>
): number {
return circuits.reduce(
(sum, circuit) => sum + circuit.circuitTotalPower,
0
);
}
export function calculateDistributionBoardTotalPower(
sections: Array<{ sectionTotalPower: number }>
): number {
return sections.reduce(
(sum, section) => sum + section.sectionTotalPower,
0
);
}
export function applyDistributionBoardSimultaneityFactor(
totalPower: number,
simultaneityFactor: number
): number {
if (
!Number.isFinite(simultaneityFactor) ||
simultaneityFactor < 0 ||
simultaneityFactor > 1
) {
throw new Error(
"Distribution-board simultaneity factor must be between zero and one."
);
}
return totalPower * simultaneityFactor;
}
+4
View File
@@ -52,6 +52,7 @@ export interface CircuitTreeSectionBlock {
displayName: string;
prefix: string;
sortOrder: number;
sectionTotalPower: number;
circuits: CircuitTreeCircuit[];
}
@@ -60,6 +61,9 @@ export interface CircuitTreeResponse {
currentRevision: number;
singlePhaseVoltageV: number;
threePhaseVoltageV: number;
distributionBoardSimultaneityFactor: number;
distributionBoardTotalPower: number;
distributionBoardTotalPowerWithSimultaneityFactor: number;
sections: CircuitTreeSectionBlock[];
}
@@ -6,11 +6,13 @@ import type { SerializedProjectCommand } from "./project-command.model.js";
export const distributionBoardUpdateCommandType =
"distribution-board.update" as const;
export const distributionBoardUpdateCommandSchemaVersion = 1 as const;
export const legacyDistributionBoardUpdateCommandSchemaVersion = 1 as const;
export const distributionBoardUpdateCommandSchemaVersion = 2 as const;
export interface DistributionBoardUpdateValues {
floorId: string | null;
supplyType: DistributionBoardSupplyType | null;
simultaneityFactor: number;
}
export type DistributionBoardUpdateField =
@@ -31,17 +33,27 @@ export interface DistributionBoardUpdateCommandPayload {
changes: DistributionBoardUpdateFieldChange[];
}
export interface DistributionBoardUpdateProjectCommand
interface CurrentDistributionBoardUpdateProjectCommand
extends SerializedProjectCommand<DistributionBoardUpdateCommandPayload> {
schemaVersion: typeof distributionBoardUpdateCommandSchemaVersion;
type: typeof distributionBoardUpdateCommandType;
}
interface LegacyDistributionBoardUpdateProjectCommand
extends SerializedProjectCommand<DistributionBoardUpdateCommandPayload> {
schemaVersion: typeof legacyDistributionBoardUpdateCommandSchemaVersion;
type: typeof distributionBoardUpdateCommandType;
}
export type DistributionBoardUpdateProjectCommand =
| CurrentDistributionBoardUpdateProjectCommand
| LegacyDistributionBoardUpdateProjectCommand;
export function createDistributionBoardUpdateProjectCommand(
distributionBoardId: string,
patch: DistributionBoardUpdatePatch
): DistributionBoardUpdateProjectCommand {
const command: DistributionBoardUpdateProjectCommand = {
): CurrentDistributionBoardUpdateProjectCommand {
const command: CurrentDistributionBoardUpdateProjectCommand = {
schemaVersion: distributionBoardUpdateCommandSchemaVersion,
type: distributionBoardUpdateCommandType,
payload: {
@@ -60,7 +72,9 @@ export function assertDistributionBoardUpdateProjectCommand(
command: SerializedProjectCommand<unknown>
): asserts command is DistributionBoardUpdateProjectCommand {
if (
command.schemaVersion !== distributionBoardUpdateCommandSchemaVersion ||
(command.schemaVersion !==
legacyDistributionBoardUpdateCommandSchemaVersion &&
command.schemaVersion !== distributionBoardUpdateCommandSchemaVersion) ||
command.type !== distributionBoardUpdateCommandType ||
!isPlainObject(command.payload)
) {
@@ -81,7 +95,11 @@ export function assertDistributionBoardUpdateProjectCommand(
for (const change of changes) {
if (
!isPlainObject(change) ||
(change.field !== "floorId" && change.field !== "supplyType") ||
(change.field !== "floorId" &&
change.field !== "supplyType" &&
(command.schemaVersion ===
legacyDistributionBoardUpdateCommandSchemaVersion ||
change.field !== "simultaneityFactor")) ||
seen.has(change.field)
) {
throw new Error(
@@ -95,13 +113,24 @@ export function assertDistributionBoardUpdateProjectCommand(
) {
throw new Error("floorId must be a non-empty string or null.");
}
} else if (change.field === "supplyType") {
if (
change.value !== null &&
!distributionBoardSupplyTypes.includes(
change.value as DistributionBoardSupplyType
)
) {
throw new Error("supplyType is invalid.");
}
} else if (
change.value !== null &&
!distributionBoardSupplyTypes.includes(
change.value as DistributionBoardSupplyType
)
typeof change.value !== "number" ||
!Number.isFinite(change.value) ||
change.value < 0 ||
change.value > 1
) {
throw new Error("supplyType is invalid.");
throw new Error(
"simultaneityFactor must be between zero and one."
);
}
seen.add(change.field);
}
@@ -13,7 +13,8 @@ export const legacyProjectStateSnapshotSchemaVersion = 1 as const;
export const previousProjectStateSnapshotSchemaVersion = 2 as const;
export const distributionBoardProjectStateSnapshotSchemaVersion = 3 as const;
export const supplyTypesProjectStateSnapshotSchemaVersion = 4 as const;
export const projectStateSnapshotSchemaVersion = 5 as const;
export const voltageProjectStateSnapshotSchemaVersion = 5 as const;
export const projectStateSnapshotSchemaVersion = 6 as const;
const idSchema = z.string().trim().min(1);
const nullableStringSchema = z.string().nullable();
@@ -57,6 +58,12 @@ const distributionBoardSchema = legacyDistributionBoardSchema
})
.strict();
const currentDistributionBoardSchema = distributionBoardSchema
.extend({
simultaneityFactor: finiteNumberSchema.min(0).max(1),
})
.strict();
const circuitListSchema = z
.object({
id: idSchema,
@@ -213,9 +220,17 @@ const supplyTypesProjectStateSnapshotSchema = z
})
.strict();
export const projectStateSnapshotSchema =
const voltageProjectStateSnapshotSchema =
supplyTypesProjectStateSnapshotSchema.extend({
schemaVersion: z.literal(
voltageProjectStateSnapshotSchemaVersion
),
});
export const projectStateSnapshotSchema =
voltageProjectStateSnapshotSchema.extend({
schemaVersion: z.literal(projectStateSnapshotSchemaVersion),
distributionBoards: z.array(currentDistributionBoardSchema),
});
export type ProjectStateSnapshot = z.infer<
@@ -228,34 +243,46 @@ export function parseProjectStateSnapshot(
const version = isPlainObject(value) ? value.schemaVersion : undefined;
const parsedSnapshot =
version === legacyProjectStateSnapshotSchemaVersion
? upgradeSupplyTypesProjectStateSnapshot(
upgradeDistributionBoardProjectStateSnapshot(
upgradePreviousProjectStateSnapshot(
upgradeLegacyProjectStateSnapshot(
legacyProjectStateSnapshotSchema.parse(value)
? upgradeVoltageProjectStateSnapshot(
upgradeSupplyTypesProjectStateSnapshot(
upgradeDistributionBoardProjectStateSnapshot(
upgradePreviousProjectStateSnapshot(
upgradeLegacyProjectStateSnapshot(
legacyProjectStateSnapshotSchema.parse(value)
)
)
)
)
)
: version === previousProjectStateSnapshotSchemaVersion
? upgradeSupplyTypesProjectStateSnapshot(
upgradeDistributionBoardProjectStateSnapshot(
upgradePreviousProjectStateSnapshot(
previousProjectStateSnapshotSchema.parse(value)
? upgradeVoltageProjectStateSnapshot(
upgradeSupplyTypesProjectStateSnapshot(
upgradeDistributionBoardProjectStateSnapshot(
upgradePreviousProjectStateSnapshot(
previousProjectStateSnapshotSchema.parse(value)
)
)
)
)
: version === distributionBoardProjectStateSnapshotSchemaVersion
? upgradeSupplyTypesProjectStateSnapshot(
upgradeDistributionBoardProjectStateSnapshot(
distributionBoardProjectStateSnapshotSchema.parse(value)
? upgradeVoltageProjectStateSnapshot(
upgradeSupplyTypesProjectStateSnapshot(
upgradeDistributionBoardProjectStateSnapshot(
distributionBoardProjectStateSnapshotSchema.parse(value)
)
)
)
: version === supplyTypesProjectStateSnapshotSchemaVersion
? upgradeSupplyTypesProjectStateSnapshot(
supplyTypesProjectStateSnapshotSchema.parse(value)
? upgradeVoltageProjectStateSnapshot(
upgradeSupplyTypesProjectStateSnapshot(
supplyTypesProjectStateSnapshotSchema.parse(value)
)
)
: projectStateSnapshotSchema.parse(value);
: version === voltageProjectStateSnapshotSchemaVersion
? upgradeVoltageProjectStateSnapshot(
voltageProjectStateSnapshotSchema.parse(value)
)
: projectStateSnapshotSchema.parse(value);
const snapshot = normalizeSnapshotPhaseTypes(parsedSnapshot);
assertProjectStateSnapshotRelations(snapshot);
return snapshot;
@@ -354,14 +381,14 @@ function upgradeDistributionBoardProjectStateSnapshot(
function upgradeSupplyTypesProjectStateSnapshot(
snapshot: z.infer<typeof supplyTypesProjectStateSnapshotSchema>
): ProjectStateSnapshot {
): z.infer<typeof voltageProjectStateSnapshotSchema> {
const settings = snapshot.project;
const sectionById = new Map(
snapshot.circuitSections.map((section) => [section.id, section])
);
return {
...snapshot,
schemaVersion: projectStateSnapshotSchemaVersion,
schemaVersion: voltageProjectStateSnapshotSchemaVersion,
projectDevices: snapshot.projectDevices.map((device) => ({
...device,
voltageV: resolveProjectVoltage(device.phaseType, settings),
@@ -383,6 +410,19 @@ function upgradeSupplyTypesProjectStateSnapshot(
};
}
function upgradeVoltageProjectStateSnapshot(
snapshot: z.infer<typeof voltageProjectStateSnapshotSchema>
): ProjectStateSnapshot {
return {
...snapshot,
schemaVersion: projectStateSnapshotSchemaVersion,
distributionBoards: snapshot.distributionBoards.map((board) => ({
...board,
simultaneityFactor: 1,
})),
};
}
function isPlainObject(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
+106 -25
View File
@@ -27,7 +27,11 @@ import {
deviceFieldKeys,
formatPhaseTypeLabel,
formatValue,
getCircuitSectionLabel,
getProjectColumnLayoutStorageKey,
isGridEditorControlTarget,
normalizeColumnOrder,
parseStoredColumnLayout,
} from "../utils/circuit-grid-model";
import {
buildCircuitDeviceRowInsertSnapshot,
@@ -131,7 +135,8 @@ type CircuitReorderDropIntent =
| { kind: "after-circuit"; sectionId: string; targetCircuitId: string; valid: boolean }
| { kind: "section-end"; sectionId: string; valid: boolean };
const COLUMN_LAYOUT_STORAGE_KEY = "circuitTreeEditor.columnLayout.v1";
const LEGACY_COLUMN_LAYOUT_STORAGE_KEY =
"circuitTreeEditor.columnLayout.v1";
function normalizeUiError(err: unknown): string {
const message = err instanceof Error ? err.message : "Der Vorgang ist fehlgeschlagen.";
@@ -222,6 +227,10 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
const [filterValueSearch, setFilterValueSearch] = useState("");
const [visibleColumnKeys, setVisibleColumnKeys] = useState<CellKey[]>(defaultVisibleColumnKeys);
const [columnOrder, setColumnOrder] = useState<CellKey[]>(allColumns.map((column) => column.key));
const [
loadedColumnLayoutProjectId,
setLoadedColumnLayoutProjectId,
] = useState<string | null>(null);
const [isColumnMenuOpen, setIsColumnMenuOpen] = useState(false);
const [columnSearch, setColumnSearch] = useState("");
const [draggingColumnKey, setDraggingColumnKey] = useState<CellKey | null>(null);
@@ -267,13 +276,6 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
}
}
function normalizeColumnOrder(keys: CellKey[]) {
const unique = [...new Set(keys)];
const allKeys = allColumns.map((column) => column.key);
const merged = [...unique, ...allKeys.filter((key) => !unique.includes(key))];
return ["equipmentIdentifier" as CellKey, ...merged.filter((key) => key !== "equipmentIdentifier")];
}
// Initial/identity-change tree load for current route context.
useEffect(() => {
void loadTree({ showLoading: true });
@@ -292,32 +294,48 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
}, [projectId]);
useEffect(() => {
setLoadedColumnLayoutProjectId(null);
setColumnOrder(allColumns.map((column) => column.key));
setVisibleColumnKeys(defaultVisibleColumnKeys);
try {
const raw = localStorage.getItem(COLUMN_LAYOUT_STORAGE_KEY);
if (!raw) {
const parsed = parseStoredColumnLayout(
localStorage.getItem(
getProjectColumnLayoutStorageKey(projectId)
) ??
localStorage.getItem(
LEGACY_COLUMN_LAYOUT_STORAGE_KEY
)
);
if (!parsed) {
return;
}
const parsed = JSON.parse(raw) as { order?: string[]; visible?: string[] };
const validKeys = new Set(allColumns.map((column) => column.key));
const parsedOrder = (parsed.order ?? []).filter((key): key is CellKey => validKeys.has(key as CellKey));
const parsedVisible = (parsed.visible ?? []).filter((key): key is CellKey => validKeys.has(key as CellKey));
if (!parsedOrder.length || !parsedVisible.length || !parsedVisible.includes("equipmentIdentifier")) {
return;
}
setColumnOrder(normalizeColumnOrder(parsedOrder));
setVisibleColumnKeys(parsedVisible);
setColumnOrder(parsed.order);
setVisibleColumnKeys(parsed.visible);
} catch {
// ignore invalid local storage and use defaults
} finally {
setLoadedColumnLayoutProjectId(projectId);
}
}, []);
}, [projectId]);
useEffect(() => {
if (loadedColumnLayoutProjectId !== projectId) {
return;
}
const payload = {
order: columnOrder,
visible: visibleColumnKeys,
};
localStorage.setItem(COLUMN_LAYOUT_STORAGE_KEY, JSON.stringify(payload));
}, [columnOrder, visibleColumnKeys]);
localStorage.setItem(
getProjectColumnLayoutStorageKey(projectId),
JSON.stringify(payload)
);
}, [
columnOrder,
loadedColumnLayoutProjectId,
projectId,
visibleColumnKeys,
]);
useEffect(() => {
const visible = new Set(visibleColumnKeys);
@@ -474,6 +492,51 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
);
}
function renderDistributionPowerSummary() {
if (!data) {
return null;
}
return (
<section
className="distribution-power-summary"
aria-label="Leistungszusammenfassung des Verteilers"
>
<div>
<span>Gesamtleistung Verteiler</span>
<strong>
{formatValue(
data.distributionBoardTotalPower,
"rowTotalPower"
)}{" "}
kW
</strong>
</div>
<div>
<span>Verteilerweiter Gleichzeitigkeitsfaktor</span>
<strong>
{formatValue(
data.distributionBoardSimultaneityFactor,
"simultaneityFactor"
)}
</strong>
</div>
<div>
<span>
Gesamtleistung Verteiler unter Berücksichtigung des
Gleichzeitigkeitsfaktors
</span>
<strong>
{formatValue(
data.distributionBoardTotalPowerWithSimultaneityFactor,
"rowTotalPower"
)}{" "}
kW
</strong>
</div>
</section>
);
}
function closeColumnSettingsMenu() {
setIsColumnMenuOpen(false);
setColumnSearch("");
@@ -2490,6 +2553,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
{renderColumnSettingsMenu("col-empty")}
</div>
{renderActiveViewSummary()}
{renderDistributionPowerSummary()}
{error ? (
<div className="notice error editor-error-notice" role="alert">
<span>{error}</span>
@@ -2598,6 +2662,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
{renderColumnSettingsMenu("col")}
</div>
{renderActiveViewSummary()}
{renderDistributionPowerSummary()}
{hasActiveSortOrFilter ? (
<div className="notice muted">Sortierung und Filter verändern nur die Ansicht. Die Neunummerierung ist währenddessen deaktiviert.</div>
) : null}
@@ -2701,14 +2766,18 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
const valid = !selectedProjectDevice || isProjectDevicePlacementValid(selectedProjectDevice, section);
return (
<option key={section.id} value={section.id} disabled={!valid}>
{section.displayName}{valid ? "" : " (nicht zulässig)"}
{getCircuitSectionLabel(section)}
{valid ? "" : " (nicht zulässig)"}
</option>
);
})}
</select>
</label>
{selectedProjectDevice && suggestedSection ? (
<p className="notice muted">Vorgeschlagener Bereich: {suggestedSection.displayName}</p>
<p className="notice muted">
Vorgeschlagener Bereich:{" "}
{getCircuitSectionLabel(suggestedSection)}
</p>
) : null}
<button
type="button"
@@ -2980,7 +3049,19 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
>
<td colSpan={visibleColumns.length + 1} className="section-drop-cell">
<div className="section-content">
<strong>{section.displayName}</strong>
<div className="section-title">
<strong>
{getCircuitSectionLabel(section)}
</strong>
<span>
Gesamtleistung Abschnitt:{" "}
{formatValue(
section.sectionTotalPower,
"rowTotalPower"
)}{" "}
kW
</span>
</div>
<div className="section-actions">
<button type="button" tabIndex={-1} onClick={() => void handleAddReserveCircuit(section.id)}>
Stromkreis hinzufügen
@@ -4,7 +4,10 @@ import { useEffect, useState } from "react";
import { Fragment } from "react";
import { getCircuitTree } from "../utils/api";
import type { CircuitTreeCircuitDto, CircuitTreeResponseDto } from "../types";
import { formatValue } from "../utils/circuit-grid-model";
import {
formatValue,
getCircuitSectionLabel,
} from "../utils/circuit-grid-model";
function renderCircuitSummaryLabel(circuit: CircuitTreeCircuitDto) {
if (circuit.displayName?.trim()) {
@@ -98,7 +101,7 @@ function SectionRows(props: { section: CircuitTreeResponseDto["sections"][number
<>
<tr className="section-row">
<td colSpan={21}>
<strong>{section.displayName}</strong>
<strong>{getCircuitSectionLabel(section)}</strong>
</td>
</tr>
{section.circuits.map((circuit) => {
+5
View File
@@ -95,6 +95,7 @@ export interface DistributionBoardDto {
name: string;
floorId: string | null;
supplyType: DistributionBoardSupplyType | null;
simultaneityFactor: number;
}
export interface DistributionBoardCommandResultDto
@@ -296,6 +297,7 @@ export interface CircuitTreeSectionDto {
displayName: string;
prefix: string;
sortOrder: number;
sectionTotalPower: number;
circuits: CircuitTreeCircuitDto[];
}
@@ -315,6 +317,9 @@ export interface CircuitTreeResponseDto {
currentRevision: number;
singlePhaseVoltageV: number;
threePhaseVoltageV: number;
distributionBoardSimultaneityFactor: number;
distributionBoardTotalPower: number;
distributionBoardTotalPowerWithSimultaneityFactor: number;
sections: CircuitTreeSectionDto[];
migrationReport?: CircuitTreeMigrationReportDto;
}
+1
View File
@@ -299,6 +299,7 @@ export function updateDistributionBoard(
input: {
floorId: string | null;
supplyType: NonNullable<DistributionBoardDto["supplyType"]>;
simultaneityFactor: number;
},
expectedRevision: number
) {
+90
View File
@@ -124,6 +124,96 @@ export const defaultVisibleColumnKeys = allColumns
.filter((column) => column.defaultVisible)
.map((column) => column.key);
const projectColumnLayoutStoragePrefix =
"circuitTreeEditor.columnLayout.v2";
export interface CircuitColumnLayout {
order: CellKey[];
visible: CellKey[];
}
const circuitSectionLabels: Record<string, string> = {
lighting: "Licht",
single_phase: "Einphasig",
three_phase: "Dreiphasig",
unassigned: "Nicht zugeordnet",
};
export function getCircuitSectionLabel(
section: { key: string; displayName: string }
): string {
return circuitSectionLabels[section.key] ?? section.displayName;
}
export function getProjectColumnLayoutStorageKey(
projectId: string
): string {
return `${projectColumnLayoutStoragePrefix}.${projectId}`;
}
export function normalizeColumnOrder(keys: CellKey[]): CellKey[] {
const unique = [...new Set(keys)];
const allKeys = allColumns.map((column) => column.key);
const merged = [
...unique,
...allKeys.filter((key) => !unique.includes(key)),
];
return [
"equipmentIdentifier",
...merged.filter((key) => key !== "equipmentIdentifier"),
];
}
export function parseStoredColumnLayout(
serialized: string | null
): CircuitColumnLayout | null {
if (!serialized) {
return null;
}
try {
const parsed = JSON.parse(serialized) as {
order?: unknown;
visible?: unknown;
};
if (
!Array.isArray(parsed.order) ||
!Array.isArray(parsed.visible)
) {
return null;
}
const validKeys = new Set(
allColumns.map((column) => column.key)
);
const order = parsed.order.filter(
(key): key is CellKey =>
typeof key === "string" &&
validKeys.has(key as CellKey)
);
const visible = [
...new Set(
parsed.visible.filter(
(key): key is CellKey =>
typeof key === "string" &&
validKeys.has(key as CellKey)
)
),
];
if (
!order.length ||
!visible.length ||
!visible.includes("equipmentIdentifier")
) {
return null;
}
return {
order: normalizeColumnOrder(order),
visible,
};
} catch {
return null;
}
}
const deviceOnlyColumns = new Set<CellKey>([
"quantity",
"powerPerUnit",
@@ -1,7 +1,10 @@
import type { Request, Response } from "express";
import {
applyDistributionBoardSimultaneityFactor,
calculateCircuitTotalPower,
calculateDistributionBoardTotalPower,
calculateRowTotalPower,
calculateSectionTotalPower,
} from "../../domain/calculations/circuit-power-calculation.js";
import type { CircuitTreeResponse } from "../../domain/models/circuit-tree.model.js";
import {
@@ -9,6 +12,7 @@ import {
circuitListRepository,
circuitRepository,
circuitSectionRepository,
distributionBoardRepository,
projectRepository,
} from "../composition/application-repositories.js";
@@ -37,6 +41,16 @@ export async function getCircuitTree(req: Request, res: Response) {
if (!project) {
return res.status(404).json({ error: "Project not found" });
}
const distributionBoard =
await distributionBoardRepository.findById(
projectId,
list.distributionBoardId
);
if (!distributionBoard) {
return res.status(404).json({
error: "Distribution board not found",
});
}
try {
const sections = await circuitSectionRepository.listByCircuitList(circuitListId);
@@ -57,12 +71,17 @@ export async function getCircuitTree(req: Request, res: Response) {
currentRevision: project.currentRevision,
singlePhaseVoltageV: project.singlePhaseVoltageV,
threePhaseVoltageV: project.threePhaseVoltageV,
distributionBoardSimultaneityFactor:
distributionBoard.simultaneityFactor,
distributionBoardTotalPower: 0,
distributionBoardTotalPowerWithSimultaneityFactor: 0,
sections: sections.map((section) => ({
id: section.id,
key: section.key,
displayName: section.displayName,
prefix: section.prefix,
sortOrder: section.sortOrder,
sectionTotalPower: 0,
circuits: [],
})),
};
@@ -123,6 +142,19 @@ export async function getCircuitTree(req: Request, res: Response) {
});
}
for (const section of tree.sections) {
section.sectionTotalPower = calculateSectionTotalPower(
section.circuits
);
}
tree.distributionBoardTotalPower =
calculateDistributionBoardTotalPower(tree.sections);
tree.distributionBoardTotalPowerWithSimultaneityFactor =
applyDistributionBoardSimultaneityFactor(
tree.distributionBoardTotalPower,
tree.distributionBoardSimultaneityFactor
);
return res.json(tree);
} catch (error) {
if (isMissingCircuitTreeSchemaError(error)) {
@@ -131,6 +163,10 @@ export async function getCircuitTree(req: Request, res: Response) {
currentRevision: project.currentRevision,
singlePhaseVoltageV: project.singlePhaseVoltageV,
threePhaseVoltageV: project.threePhaseVoltageV,
distributionBoardSimultaneityFactor:
distributionBoard.simultaneityFactor,
distributionBoardTotalPower: 0,
distributionBoardTotalPowerWithSimultaneityFactor: 0,
sections: [],
warning:
"Circuit-first tables are not available yet. Run database migrations (including 0008_circuit_first_model).",
@@ -51,7 +51,10 @@ export async function createDistributionBoard(req: Request, res: Response) {
});
return res.status(201).json({
...result,
distributionBoard: structure.distributionBoard,
distributionBoard: {
...structure.distributionBoard,
simultaneityFactor: 1,
},
});
} catch (error) {
return respondWithProjectCommandError(error, res);
@@ -83,6 +86,7 @@ export async function updateDistributionBoard(
{
floorId: parsed.data.floorId,
supplyType: parsed.data.supplyType,
simultaneityFactor: parsed.data.simultaneityFactor,
}
),
});
@@ -45,6 +45,7 @@ export const updateDistributionBoardSchema = z
expectedRevision: expectedProjectRevisionSchema,
floorId: z.string().trim().min(1).nullable(),
supplyType: z.enum(distributionBoardSupplyTypes),
simultaneityFactor: z.number().finite().min(0).max(1),
})
.strict();