Render protected circuit structure

This commit is contained in:
2026-07-31 07:22:26 +02:00
parent ac21d6eb5d
commit 31589875b5
11 changed files with 254 additions and 29 deletions
+34
View File
@@ -657,6 +657,40 @@ body {
font-weight: 600;
}
.tree-grid .structure-component-row td {
padding: 0.38rem 0.6rem;
border-bottom-color: #d8dee9;
}
.tree-grid .structure-component-row.headerComponent td {
background: #e2e8f0;
}
.tree-grid .structure-component-row.groupComponent td {
background: #f8fafc;
}
.tree-grid .structure-component-row.footerComponent td {
background: #f1f5f9;
}
.tree-grid .structure-component-content {
display: flex;
align-items: baseline;
gap: 0.75rem;
min-width: 0;
}
.tree-grid .structure-component-content strong {
flex: 0 0 auto;
font-variant-numeric: tabular-nums;
}
.tree-grid .structure-component-protection {
color: #475569;
font-size: 0.78rem;
}
.tree-grid .section-drop-cell {
padding: 0.45rem 0.6rem;
}
@@ -11,6 +11,7 @@ import {
} from "../../domain/services/project-voltage.service";
import {
getInsertionSortOrder,
isGridInsertionRowType,
resolveGridInsertionIntent,
} from "../utils/circuit-grid-insertion";
import {
@@ -53,7 +54,7 @@ import type {
RowType,
} from "../utils/circuit-grid-model";
import {
buildVisibleGridRows,
buildVisibleGridRowsWithStructure,
filterAndSortCircuitSections,
getDistinctFilterValues,
} from "../utils/circuit-grid-projection";
@@ -386,8 +387,11 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
// visibleRows is the single normalized grid used by render + navigation + editability.
// This avoids selected/rendered/editable state drifting apart after filters/sorts/reloads.
const visibleRows = useMemo(() => {
return buildVisibleGridRows(filteredSortedSections);
}, [filteredSortedSections]);
return buildVisibleGridRowsWithStructure(filteredSortedSections, {
headerComponents: data?.headerComponents ?? [],
footerComponents: data?.footerComponents ?? [],
});
}, [data, filteredSortedSections]);
function clearColumnFilter(key: CellKey) {
setColumnFilters((current) => {
@@ -1660,7 +1664,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
const row = selectedCell ? findRow(selectedCell.rowKey) : null;
const cell = row?.cells.find((entry) => entry.cellKey === selectedCell?.cellKey);
const selection =
row && cell && row.rowType !== "section"
row && cell && isGridInsertionRowType(row.rowType)
? {
rowType: row.rowType,
cellKind: cell.kind,
@@ -2351,7 +2355,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
const row = selectedCell ? findRow(selectedCell.rowKey) : null;
const cell = row?.cells.find((entry) => entry.cellKey === selectedCell?.cellKey);
const intent = resolveGridDeleteIntent(
row && cell && row.rowType !== "section"
row && cell && isGridInsertionRowType(row.rowType)
? {
rowType: row.rowType,
cellKind: cell.kind,
@@ -2986,6 +2990,54 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
</thead>
<tbody>
{visibleRows.map((row) => {
if (
row.rowType === "headerComponent" ||
row.rowType === "groupComponent" ||
row.rowType === "footerComponent"
) {
const component = row.component!;
const protection = component.protectionDevice;
const protectionSummary = protection
? [
protection.type,
`${formatValue(
protection.ratedCurrentA,
"protectionRatedCurrent"
)} A`,
protection.fuseUtilizationCategory,
protection.tripCharacteristic,
protection.rcdType
? `Typ ${protection.rcdType}`
: undefined,
protection.ratedResidualCurrentMa !== undefined
? `${formatValue(
protection.ratedResidualCurrentMa,
"protectionRatedCurrent"
)} mA`
: undefined,
]
.filter(Boolean)
.join(" · ")
: null;
return (
<tr
key={row.rowKey}
className={`structure-component-row ${row.rowType}`}
>
<td colSpan={visibleColumns.length + 1}>
<div className="structure-component-content">
<strong>{component.equipmentIdentifier}</strong>
<span>{component.name}</span>
{protectionSummary ? (
<span className="structure-component-protection">
{protectionSummary}
</span>
) : null}
</div>
</td>
</tr>
);
}
if (row.rowType === "section") {
const section = data.sections.find((entry) => entry.id === row.sectionId)!;
return (
@@ -7,6 +7,16 @@ export type GridInsertionRowType =
export type GridInsertionCellKind = "circuitField" | "deviceField" | "readonly" | "computed";
export function isGridInsertionRowType(rowType: string): rowType is GridInsertionRowType {
return (
rowType === "circuitCompact" ||
rowType === "circuitSummary" ||
rowType === "deviceRow" ||
rowType === "reserveCircuit" ||
rowType === "placeholder"
);
}
export interface GridInsertionSelection {
rowType: GridInsertionRowType;
cellKind: GridInsertionCellKind;
+4 -1
View File
@@ -37,12 +37,15 @@ export type CellKey =
| "isReserve";
export type RowType =
| "headerComponent"
| "section"
| "groupComponent"
| "circuitCompact"
| "circuitSummary"
| "deviceRow"
| "reserveCircuit"
| "placeholder";
| "placeholder"
| "footerComponent";
export type CellKind = "circuitField" | "deviceField" | "computed" | "readonly";
export type GridValue = string | number | boolean | undefined;
+61 -15
View File
@@ -1,8 +1,10 @@
import type {
CircuitTreeCircuitDto,
CircuitTreeComponentDto,
CircuitTreeDeviceRowDto,
CircuitTreeSectionDto,
} from "../types.js";
import { buildCircuitStructureProjection } from "./circuit-structure-projection";
import {
allColumns,
circuitOnlyColumns,
@@ -42,6 +44,7 @@ export interface VisibleGridRow {
sectionId: string;
circuit?: CircuitTreeCircuitDto;
device?: CircuitTreeDeviceRowDto;
component?: CircuitTreeComponentDto;
cells: VisibleGridCell[];
}
@@ -160,21 +163,64 @@ function makeVisibleGridRow(
}
export function buildVisibleGridRows(sections: readonly CircuitTreeSectionDto[]): VisibleGridRow[] {
const rows: VisibleGridRow[] = [];
for (const section of sections) {
rows.push({
rowKey: `section:${section.id}`,
rowType: "section",
sectionId: section.id,
cells: allColumns.map((column) => ({
cellKey: column.key,
editable: false,
kind: "readonly",
value: undefined,
})),
});
return buildVisibleGridRowsWithStructure(sections, {
headerComponents: [],
footerComponents: [],
});
}
for (const circuit of section.circuits) {
export function buildVisibleGridRowsWithStructure(
sections: readonly CircuitTreeSectionDto[],
structure: {
headerComponents: readonly CircuitTreeComponentDto[];
footerComponents: readonly CircuitTreeComponentDto[];
}
): VisibleGridRow[] {
const rows: VisibleGridRow[] = [];
const projected = buildCircuitStructureProjection({
headerComponents: [...structure.headerComponents],
sections: [...sections],
footerComponents: [...structure.footerComponents],
});
for (const row of projected) {
if ("component" in row) {
const component = row.component;
rows.push({
rowKey: row.rowKey,
rowType: row.rowType,
sectionId: component.sectionId ?? `__${row.zone}__`,
component,
cells: allColumns.map((column) => ({
cellKey: column.key,
editable: false,
kind: "readonly",
value:
column.key === "equipmentIdentifier"
? component.equipmentIdentifier
: column.key === "displayName"
? component.name
: undefined,
})),
});
continue;
}
const section = row.section;
if (row.rowType === "groupHeader") {
rows.push({
rowKey: `section:${section.id}`,
rowType: "section",
sectionId: section.id,
cells: allColumns.map((column) => ({
cellKey: column.key,
editable: false,
kind: "readonly",
value: undefined,
})),
});
continue;
}
if (row.rowType === "circuitBlock") {
const circuit = row.circuit;
if (circuit.deviceRows.length === 0) {
rows.push(makeVisibleGridRow("reserveCircuit", section.id, circuit));
} else if (circuit.deviceRows.length === 1) {
@@ -185,8 +231,8 @@ export function buildVisibleGridRows(sections: readonly CircuitTreeSectionDto[])
rows.push(makeVisibleGridRow("deviceRow", section.id, circuit, device));
}
}
continue;
}
rows.push(makeVisibleGridRow("placeholder", section.id));
}
return rows;
@@ -55,7 +55,7 @@ export function buildCircuitStructureProjection(
zone: "group",
section,
});
for (const component of ordered(section.components)) {
for (const component of ordered(section.components ?? [])) {
rows.push({
rowKey: `group-component:${component.id}`,
rowType: "groupComponent",