Added column sorting
This commit is contained in:
@@ -49,6 +49,7 @@ body {
|
||||
.editor-toolbar {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.editor-toolbar button {
|
||||
@@ -63,6 +64,75 @@ body {
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.column-settings-menu {
|
||||
position: absolute;
|
||||
z-index: 9;
|
||||
margin-top: 2rem;
|
||||
border: 1px solid #cfd7e5;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
|
||||
padding: 0.45rem;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.column-settings-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
|
||||
.column-settings-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
max-height: 340px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.column-settings-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.4rem;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
padding: 0.2rem 0.25rem;
|
||||
}
|
||||
|
||||
.column-settings-item.dragging {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.column-settings-item.drop-target {
|
||||
border-color: #2b6cb0;
|
||||
background: #ebf4ff;
|
||||
}
|
||||
|
||||
.column-settings-item.locked {
|
||||
background: #f7fafc;
|
||||
}
|
||||
|
||||
.column-settings-item label {
|
||||
display: flex;
|
||||
gap: 0.3rem;
|
||||
align-items: center;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.column-settings-order {
|
||||
display: flex;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
|
||||
.column-settings-order button {
|
||||
border: 1px solid #c4cddc;
|
||||
background: #fff;
|
||||
border-radius: 3px;
|
||||
font-size: 0.72rem;
|
||||
padding: 0.08rem 0.28rem;
|
||||
}
|
||||
|
||||
.tree-grid-wrap {
|
||||
overflow: auto;
|
||||
border: 1px solid #d9dee8;
|
||||
|
||||
@@ -34,7 +34,26 @@ type CellKey =
|
||||
| "protectionSummary"
|
||||
| "cableSummary"
|
||||
| "roomSummary"
|
||||
| "remark";
|
||||
| "remark"
|
||||
| "technicalName"
|
||||
| "connectionKind"
|
||||
| "phaseType"
|
||||
| "costGroup"
|
||||
| "category"
|
||||
| "level"
|
||||
| "roomNumberSnapshot"
|
||||
| "roomNameSnapshot"
|
||||
| "cosPhi"
|
||||
| "protectionType"
|
||||
| "protectionRatedCurrent"
|
||||
| "protectionCharacteristic"
|
||||
| "cableType"
|
||||
| "cableCrossSection"
|
||||
| "cableLength"
|
||||
| "rcdAssignment"
|
||||
| "terminalDesignation"
|
||||
| "status"
|
||||
| "isReserve";
|
||||
|
||||
type SaveDirection = "stay" | "next" | "prev";
|
||||
type StartEditMode = "selectExisting" | "replaceWithTypedChar";
|
||||
@@ -118,37 +137,117 @@ type CircuitReorderDropIntent =
|
||||
| { kind: "after-circuit"; sectionId: string; targetCircuitId: string; valid: boolean }
|
||||
| { kind: "section-end"; sectionId: string; valid: boolean };
|
||||
|
||||
const columns: Array<{ key: CellKey; label: string; numeric?: boolean }> = [
|
||||
{ key: "equipmentIdentifier", label: "Equipment identifier" },
|
||||
{ key: "displayName", label: "Display name" },
|
||||
{ key: "quantity", label: "Quantity", numeric: true },
|
||||
{ key: "powerPerUnit", label: "Power / unit", numeric: true },
|
||||
{ key: "simultaneityFactor", label: "Simultaneity", numeric: true },
|
||||
{ key: "rowTotalPower", label: "Row total", numeric: true },
|
||||
{ key: "circuitTotalPower", label: "Circuit total", numeric: true },
|
||||
{ key: "protectionSummary", label: "Protection summary" },
|
||||
{ key: "cableSummary", label: "Cable summary" },
|
||||
{ key: "roomSummary", label: "Room" },
|
||||
{ key: "remark", label: "Remark" },
|
||||
const COLUMN_LAYOUT_STORAGE_KEY = "circuitTreeEditor.columnLayout.v1";
|
||||
|
||||
interface ColumnDef {
|
||||
key: CellKey;
|
||||
label: string;
|
||||
numeric?: boolean;
|
||||
defaultVisible?: boolean;
|
||||
locked?: boolean;
|
||||
}
|
||||
|
||||
const allColumns: ColumnDef[] = [
|
||||
{ key: "equipmentIdentifier", label: "Equipment identifier", defaultVisible: true, locked: true },
|
||||
{ key: "displayName", label: "Display name", defaultVisible: true },
|
||||
{ key: "quantity", label: "Quantity", numeric: true, defaultVisible: true },
|
||||
{ key: "powerPerUnit", label: "Power / unit", numeric: true, defaultVisible: true },
|
||||
{ key: "simultaneityFactor", label: "Simultaneity", numeric: true, defaultVisible: true },
|
||||
{ key: "rowTotalPower", label: "Row total", numeric: true, defaultVisible: true },
|
||||
{ key: "circuitTotalPower", label: "Circuit total", numeric: true, defaultVisible: true },
|
||||
{ key: "protectionSummary", label: "Protection summary", defaultVisible: true },
|
||||
{ key: "cableSummary", label: "Cable summary", defaultVisible: true },
|
||||
{ key: "roomSummary", label: "Room", defaultVisible: true },
|
||||
{ key: "remark", label: "Remark", defaultVisible: true },
|
||||
{ key: "technicalName", label: "Technical name" },
|
||||
{ key: "connectionKind", label: "Connection kind" },
|
||||
{ key: "phaseType", label: "Phase type" },
|
||||
{ key: "costGroup", label: "Cost group" },
|
||||
{ key: "category", label: "Category" },
|
||||
{ key: "level", label: "Level" },
|
||||
{ key: "roomNumberSnapshot", label: "Room number" },
|
||||
{ key: "roomNameSnapshot", label: "Room name" },
|
||||
{ key: "cosPhi", label: "cosPhi", numeric: true },
|
||||
{ key: "protectionType", label: "Protection type" },
|
||||
{ key: "protectionRatedCurrent", label: "Protection rated current", numeric: true },
|
||||
{ key: "protectionCharacteristic", label: "Protection characteristic" },
|
||||
{ key: "cableType", label: "Cable type" },
|
||||
{ key: "cableCrossSection", label: "Cable cross-section" },
|
||||
{ key: "cableLength", label: "Cable length", numeric: true },
|
||||
{ key: "rcdAssignment", label: "RCD assignment" },
|
||||
{ key: "terminalDesignation", label: "Terminal designation" },
|
||||
{ key: "status", label: "Status" },
|
||||
{ key: "isReserve", label: "Reserve" },
|
||||
];
|
||||
|
||||
const deviceOnlyColumns = new Set<CellKey>(["quantity", "powerPerUnit", "simultaneityFactor", "rowTotalPower", "roomSummary"]);
|
||||
const circuitOnlyColumns = new Set<CellKey>(["equipmentIdentifier", "protectionSummary", "cableSummary", "circuitTotalPower"]);
|
||||
const defaultVisibleColumnKeys = allColumns.filter((column) => column.defaultVisible).map((column) => column.key);
|
||||
|
||||
const deviceFieldKeys = new Set<CellKey>([
|
||||
"displayName",
|
||||
"roomSummary",
|
||||
const deviceOnlyColumns = new Set<CellKey>([
|
||||
"quantity",
|
||||
"powerPerUnit",
|
||||
"simultaneityFactor",
|
||||
"rowTotalPower",
|
||||
"roomSummary",
|
||||
"technicalName",
|
||||
"connectionKind",
|
||||
"phaseType",
|
||||
"costGroup",
|
||||
"category",
|
||||
"level",
|
||||
"roomNumberSnapshot",
|
||||
"roomNameSnapshot",
|
||||
"cosPhi",
|
||||
]);
|
||||
const circuitOnlyColumns = new Set<CellKey>([
|
||||
"equipmentIdentifier",
|
||||
"protectionSummary",
|
||||
"cableSummary",
|
||||
"circuitTotalPower",
|
||||
"protectionType",
|
||||
"protectionRatedCurrent",
|
||||
"protectionCharacteristic",
|
||||
"cableType",
|
||||
"cableCrossSection",
|
||||
"cableLength",
|
||||
"rcdAssignment",
|
||||
"terminalDesignation",
|
||||
"status",
|
||||
"isReserve",
|
||||
]);
|
||||
|
||||
const deviceFieldKeys = new Set<CellKey>([
|
||||
"displayName",
|
||||
"technicalName",
|
||||
"connectionKind",
|
||||
"phaseType",
|
||||
"costGroup",
|
||||
"category",
|
||||
"level",
|
||||
"roomSummary",
|
||||
"roomNumberSnapshot",
|
||||
"roomNameSnapshot",
|
||||
"quantity",
|
||||
"powerPerUnit",
|
||||
"simultaneityFactor",
|
||||
"cosPhi",
|
||||
"remark",
|
||||
]);
|
||||
|
||||
const circuitFieldKeys = new Set<CellKey>([
|
||||
"equipmentIdentifier",
|
||||
"displayName",
|
||||
"protectionType",
|
||||
"protectionRatedCurrent",
|
||||
"protectionCharacteristic",
|
||||
"protectionSummary",
|
||||
"cableType",
|
||||
"cableCrossSection",
|
||||
"cableLength",
|
||||
"cableSummary",
|
||||
"rcdAssignment",
|
||||
"terminalDesignation",
|
||||
"status",
|
||||
"isReserve",
|
||||
"remark",
|
||||
]);
|
||||
|
||||
@@ -198,16 +297,34 @@ function parseNumeric(cellKey: CellKey, draft: string): number | undefined {
|
||||
|
||||
function getDeviceValue(device: CircuitTreeDeviceRowDto, key: CellKey): string | number | boolean | undefined {
|
||||
switch (key) {
|
||||
case "technicalName":
|
||||
return device.name;
|
||||
case "displayName":
|
||||
return device.displayName || device.name;
|
||||
case "phaseType":
|
||||
return device.phaseType;
|
||||
case "connectionKind":
|
||||
return device.connectionKind;
|
||||
case "costGroup":
|
||||
return device.costGroup;
|
||||
case "category":
|
||||
return device.category;
|
||||
case "level":
|
||||
return device.level;
|
||||
case "roomSummary":
|
||||
return [device.roomNumberSnapshot, device.roomNameSnapshot].filter(Boolean).join(" ").trim() || undefined;
|
||||
case "roomNumberSnapshot":
|
||||
return device.roomNumberSnapshot;
|
||||
case "roomNameSnapshot":
|
||||
return device.roomNameSnapshot;
|
||||
case "quantity":
|
||||
return device.quantity;
|
||||
case "powerPerUnit":
|
||||
return device.powerPerUnit;
|
||||
case "simultaneityFactor":
|
||||
return device.simultaneityFactor;
|
||||
case "cosPhi":
|
||||
return device.cosPhi;
|
||||
case "rowTotalPower":
|
||||
return device.rowTotalPower;
|
||||
case "remark":
|
||||
@@ -225,6 +342,12 @@ function getCircuitValue(circuit: CircuitTreeCircuitDto, key: CellKey): string |
|
||||
return circuit.displayName;
|
||||
case "circuitTotalPower":
|
||||
return circuit.circuitTotalPower;
|
||||
case "protectionType":
|
||||
return circuit.protectionType;
|
||||
case "protectionRatedCurrent":
|
||||
return circuit.protectionRatedCurrent;
|
||||
case "protectionCharacteristic":
|
||||
return circuit.protectionCharacteristic;
|
||||
case "protectionSummary": {
|
||||
const current =
|
||||
circuit.protectionRatedCurrent !== undefined && circuit.protectionRatedCurrent !== null
|
||||
@@ -240,6 +363,20 @@ function getCircuitValue(circuit: CircuitTreeCircuitDto, key: CellKey): string |
|
||||
circuit.cableLength !== undefined && circuit.cableLength !== null ? `${circuit.cableLength} m` : "";
|
||||
return [circuit.cableType, circuit.cableCrossSection, length].filter(Boolean).join(", ").trim() || undefined;
|
||||
}
|
||||
case "cableType":
|
||||
return circuit.cableType;
|
||||
case "cableCrossSection":
|
||||
return circuit.cableCrossSection;
|
||||
case "cableLength":
|
||||
return circuit.cableLength;
|
||||
case "rcdAssignment":
|
||||
return circuit.rcdAssignment;
|
||||
case "terminalDesignation":
|
||||
return circuit.terminalDesignation;
|
||||
case "status":
|
||||
return circuit.status;
|
||||
case "isReserve":
|
||||
return circuit.isReserve;
|
||||
case "remark":
|
||||
return circuit.remark;
|
||||
default:
|
||||
@@ -350,12 +487,26 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
const [sortState, setSortState] = useState<{ key: CellKey; direction: SortDirection } | null>(null);
|
||||
const [columnFilters, setColumnFilters] = useState<Partial<Record<CellKey, string[]>>>({});
|
||||
const [openFilterColumn, setOpenFilterColumn] = useState<CellKey | null>(null);
|
||||
const [visibleColumnKeys, setVisibleColumnKeys] = useState<CellKey[]>(defaultVisibleColumnKeys);
|
||||
const [columnOrder, setColumnOrder] = useState<CellKey[]>(allColumns.map((column) => column.key));
|
||||
const [isColumnMenuOpen, setIsColumnMenuOpen] = useState(false);
|
||||
const [draggingColumnKey, setDraggingColumnKey] = useState<CellKey | null>(null);
|
||||
const [columnDropTargetKey, setColumnDropTargetKey] = useState<CellKey | null>(null);
|
||||
const [pendingFocus, setPendingFocus] = useState<SelectedCell | null>(null);
|
||||
const pendingSelectionAfterReload = useRef<SelectionIntent | null>(null);
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
const focusTokenRef = useRef(1);
|
||||
|
||||
const orderedColumns = useMemo(
|
||||
() => columnOrder.map((key) => allColumns.find((column) => column.key === key)).filter(Boolean) as ColumnDef[],
|
||||
[columnOrder]
|
||||
);
|
||||
const visibleColumns = useMemo(
|
||||
() => orderedColumns.filter((column) => visibleColumnKeys.includes(column.key)),
|
||||
[orderedColumns, visibleColumnKeys]
|
||||
);
|
||||
|
||||
async function loadTree(options?: { showLoading?: boolean }) {
|
||||
const showLoading = options?.showLoading ?? false;
|
||||
if (showLoading) {
|
||||
@@ -374,6 +525,13 @@ 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")];
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
void loadTree({ showLoading: true });
|
||||
}, [projectId, circuitListId]);
|
||||
@@ -390,6 +548,54 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
void loadProjectDeviceList();
|
||||
}, [projectId]);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const raw = localStorage.getItem(COLUMN_LAYOUT_STORAGE_KEY);
|
||||
if (!raw) {
|
||||
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);
|
||||
} catch {
|
||||
// ignore invalid local storage and use defaults
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const payload = {
|
||||
order: columnOrder,
|
||||
visible: visibleColumnKeys,
|
||||
};
|
||||
localStorage.setItem(COLUMN_LAYOUT_STORAGE_KEY, JSON.stringify(payload));
|
||||
}, [columnOrder, visibleColumnKeys]);
|
||||
|
||||
useEffect(() => {
|
||||
const visible = new Set(visibleColumnKeys);
|
||||
setSortState((current) => {
|
||||
if (!current) {
|
||||
return current;
|
||||
}
|
||||
return visible.has(current.key) ? current : null;
|
||||
});
|
||||
setColumnFilters((current) => {
|
||||
const next: Partial<Record<CellKey, string[]>> = {};
|
||||
for (const [key, values] of Object.entries(current)) {
|
||||
if (visible.has(key as CellKey) && values && values.length > 0) {
|
||||
next[key as CellKey] = values;
|
||||
}
|
||||
}
|
||||
return next;
|
||||
});
|
||||
setOpenFilterColumn((current) => (current && visible.has(current) ? current : null));
|
||||
}, [visibleColumnKeys]);
|
||||
|
||||
const hasActiveFilters = useMemo(
|
||||
() => Object.values(columnFilters).some((values) => (values?.length ?? 0) > 0),
|
||||
[columnFilters]
|
||||
@@ -397,7 +603,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
|
||||
const distinctValuesByColumn = useMemo(() => {
|
||||
const result = {} as Record<CellKey, string[]>;
|
||||
for (const column of columns) {
|
||||
for (const column of visibleColumns) {
|
||||
const set = new Set<string>();
|
||||
for (const section of data?.sections ?? []) {
|
||||
for (const circuit of section.circuits) {
|
||||
@@ -410,7 +616,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
result[column.key] = [...set].sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
|
||||
}
|
||||
return result;
|
||||
}, [data]);
|
||||
}, [data, visibleColumns]);
|
||||
|
||||
const filteredSortedSections = useMemo(() => {
|
||||
if (!data) {
|
||||
@@ -492,7 +698,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
rowKey: `section:${section.id}`,
|
||||
rowType: "section",
|
||||
sectionId: section.id,
|
||||
cells: columns.map((col) => ({ cellKey: col.key, editable: false, kind: "readonly", value: undefined })),
|
||||
cells: allColumns.map((col) => ({ cellKey: col.key, editable: false, kind: "readonly", value: undefined })),
|
||||
});
|
||||
for (const circuit of section.circuits) {
|
||||
if (circuit.deviceRows.length === 0) {
|
||||
@@ -549,7 +755,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
: rowType === "deviceRow" && device
|
||||
? `device:${device.id}`
|
||||
: `${rowType}:${circuit?.id ?? sectionId}`;
|
||||
const cells = columns.map((col) => {
|
||||
const cells = allColumns.map((col) => {
|
||||
const kind = getCellKind(rowType, col.key);
|
||||
const editable = kind === "circuitField" || kind === "deviceField";
|
||||
let value: string | number | boolean | undefined;
|
||||
@@ -580,15 +786,18 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
);
|
||||
|
||||
const rowCellMap = useMemo(() => {
|
||||
const visibleKeySet = new Set(visibleColumnKeys);
|
||||
const map = new Map<string, CellKey[]>();
|
||||
for (const row of visibleRows) {
|
||||
const keys = row.cells.filter((cell) => cell.editable).map((cell) => cell.cellKey as CellKey);
|
||||
const keys = row.cells
|
||||
.filter((cell) => cell.editable && visibleKeySet.has(cell.cellKey))
|
||||
.map((cell) => cell.cellKey as CellKey);
|
||||
if (keys.length) {
|
||||
map.set(row.rowKey, keys);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}, [visibleRows]);
|
||||
}, [visibleRows, visibleColumnKeys]);
|
||||
|
||||
const editableRowOrder = useMemo(
|
||||
() => visibleRows.filter((row) => rowCellMap.has(row.rowKey)).map((row) => row.rowKey),
|
||||
@@ -783,6 +992,103 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
});
|
||||
}
|
||||
|
||||
function toggleColumnVisibility(key: CellKey) {
|
||||
const column = allColumns.find((entry) => entry.key === key);
|
||||
if (!column || column.locked) {
|
||||
return;
|
||||
}
|
||||
setVisibleColumnKeys((current) => {
|
||||
if (current.includes(key)) {
|
||||
return current.filter((entry) => entry !== key);
|
||||
}
|
||||
return [...current, key];
|
||||
});
|
||||
}
|
||||
|
||||
function moveColumn(key: CellKey, direction: -1 | 1) {
|
||||
if (key === "equipmentIdentifier") {
|
||||
return;
|
||||
}
|
||||
setColumnOrder((current) => {
|
||||
const index = current.indexOf(key);
|
||||
if (index < 0) {
|
||||
return current;
|
||||
}
|
||||
const nextIndex = index + direction;
|
||||
if (nextIndex <= 0 || nextIndex >= current.length) {
|
||||
return current;
|
||||
}
|
||||
const clone = [...current];
|
||||
const [item] = clone.splice(index, 1);
|
||||
clone.splice(nextIndex, 0, item);
|
||||
return normalizeColumnOrder(clone);
|
||||
});
|
||||
}
|
||||
|
||||
function resetColumns() {
|
||||
setVisibleColumnKeys(defaultVisibleColumnKeys);
|
||||
setColumnOrder(normalizeColumnOrder(allColumns.map((column) => column.key)));
|
||||
setOpenFilterColumn(null);
|
||||
}
|
||||
|
||||
function moveColumnByDrag(dragKey: CellKey, targetKey: CellKey) {
|
||||
if (dragKey === "equipmentIdentifier" || targetKey === "equipmentIdentifier") {
|
||||
return;
|
||||
}
|
||||
setColumnOrder((current) => {
|
||||
const from = current.indexOf(dragKey);
|
||||
const to = current.indexOf(targetKey);
|
||||
if (from < 1 || to < 1 || from === to) {
|
||||
return current;
|
||||
}
|
||||
const clone = [...current];
|
||||
const [item] = clone.splice(from, 1);
|
||||
const targetIndex = clone.indexOf(targetKey);
|
||||
if (targetIndex < 1) {
|
||||
return current;
|
||||
}
|
||||
clone.splice(targetIndex, 0, item);
|
||||
return normalizeColumnOrder(clone);
|
||||
});
|
||||
}
|
||||
|
||||
function handleColumnDragStart(event: DragEvent<HTMLDivElement>, key: CellKey) {
|
||||
if (key === "equipmentIdentifier") {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
setDraggingColumnKey(key);
|
||||
setColumnDropTargetKey(null);
|
||||
event.dataTransfer.effectAllowed = "move";
|
||||
event.dataTransfer.setData("application/x-column-key", key);
|
||||
}
|
||||
|
||||
function handleColumnDragOver(event: DragEvent<HTMLDivElement>, targetKey: CellKey) {
|
||||
if (!draggingColumnKey || targetKey === "equipmentIdentifier" || draggingColumnKey === targetKey) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = "move";
|
||||
setColumnDropTargetKey(targetKey);
|
||||
}
|
||||
|
||||
function handleColumnDrop(event: DragEvent<HTMLDivElement>, targetKey: CellKey) {
|
||||
event.preventDefault();
|
||||
const dragKey = draggingColumnKey;
|
||||
if (!dragKey || dragKey === targetKey) {
|
||||
setColumnDropTargetKey(null);
|
||||
return;
|
||||
}
|
||||
moveColumnByDrag(dragKey, targetKey);
|
||||
setDraggingColumnKey(null);
|
||||
setColumnDropTargetKey(null);
|
||||
}
|
||||
|
||||
function handleColumnDragEnd() {
|
||||
setDraggingColumnKey(null);
|
||||
setColumnDropTargetKey(null);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const intent = pendingSelectionAfterReload.current;
|
||||
if (!intent || !data) {
|
||||
@@ -805,6 +1111,13 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
setPendingFocus(null);
|
||||
}, [pendingFocus]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isColumnMenuOpen) {
|
||||
setDraggingColumnKey(null);
|
||||
setColumnDropTargetKey(null);
|
||||
}
|
||||
}, [isColumnMenuOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editingCell) {
|
||||
return;
|
||||
@@ -877,11 +1190,11 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
if (!targetCells.length) {
|
||||
return;
|
||||
}
|
||||
const preferred = columns.findIndex((col) => col.key === selectedCell.cellKey);
|
||||
const preferred = visibleColumns.findIndex((col) => col.key === selectedCell.cellKey);
|
||||
let best = targetCells[0];
|
||||
let bestDistance = Number.POSITIVE_INFINITY;
|
||||
for (const key of targetCells) {
|
||||
const idx = columns.findIndex((col) => col.key === key);
|
||||
const idx = visibleColumns.findIndex((col) => col.key === key);
|
||||
const dist = Math.abs(idx - preferred);
|
||||
if (dist < bestDistance) {
|
||||
bestDistance = dist;
|
||||
@@ -895,6 +1208,11 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
const payload: Record<string, unknown> = {};
|
||||
if (key === "protectionSummary" || key === "cableSummary") {
|
||||
payload.remark = draft.trim() === "" ? undefined : draft;
|
||||
} else if (["protectionRatedCurrent", "cableLength"].includes(key)) {
|
||||
payload[key] = parseNumeric(key, draft);
|
||||
} else if (key === "isReserve") {
|
||||
const normalized = draft.trim().toLowerCase();
|
||||
payload.isReserve = ["1", "true", "yes", "ja"].includes(normalized);
|
||||
} else {
|
||||
payload[key] = draft.trim() === "" ? undefined : draft;
|
||||
}
|
||||
@@ -903,7 +1221,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
|
||||
async function patchDeviceRow(rowId: string, key: CellKey, draft: string) {
|
||||
const payload: Record<string, unknown> = {};
|
||||
if (["quantity", "powerPerUnit", "simultaneityFactor"].includes(key)) {
|
||||
if (["quantity", "powerPerUnit", "simultaneityFactor", "cosPhi"].includes(key)) {
|
||||
payload[key] = parseNumeric(key, draft);
|
||||
} else if (key === "roomSummary") {
|
||||
const trimmed = draft.trim();
|
||||
@@ -1846,18 +2164,64 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
<button type="button" onClick={() => void handleRedo()} disabled={redoStack.length === 0 || historyBusy || isSaving}>
|
||||
Redo
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setSortState(null);
|
||||
setColumnFilters({});
|
||||
setOpenFilterColumn(null);
|
||||
}}
|
||||
disabled={!Boolean(sortState) && !hasActiveFilters}
|
||||
>
|
||||
Clear sort/filter
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setSortState(null);
|
||||
setColumnFilters({});
|
||||
setOpenFilterColumn(null);
|
||||
}}
|
||||
disabled={!Boolean(sortState) && !hasActiveFilters}
|
||||
>
|
||||
Clear sort/filter
|
||||
</button>
|
||||
<button type="button" onClick={() => setIsColumnMenuOpen((current) => !current)}>
|
||||
Columns
|
||||
</button>
|
||||
{isColumnMenuOpen ? (
|
||||
<div className="column-settings-menu">
|
||||
<div className="column-settings-actions">
|
||||
<button type="button" onClick={() => resetColumns()}>
|
||||
Reset columns
|
||||
</button>
|
||||
</div>
|
||||
<div className="column-settings-list">
|
||||
{orderedColumns.map((column) => {
|
||||
const visible = visibleColumnKeys.includes(column.key);
|
||||
return (
|
||||
<div
|
||||
key={`col-empty-${column.key}`}
|
||||
className={`column-settings-item ${draggingColumnKey === column.key ? "dragging" : ""} ${columnDropTargetKey === column.key ? "drop-target" : ""} ${column.locked ? "locked" : ""}`}
|
||||
draggable={!column.locked}
|
||||
onDragStart={(event) => handleColumnDragStart(event, column.key)}
|
||||
onDragOver={(event) => handleColumnDragOver(event, column.key)}
|
||||
onDrop={(event) => handleColumnDrop(event, column.key)}
|
||||
onDragEnd={() => handleColumnDragEnd()}
|
||||
>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={visible}
|
||||
disabled={column.locked}
|
||||
onChange={() => toggleColumnVisibility(column.key)}
|
||||
/>
|
||||
<span>{column.label}</span>
|
||||
</label>
|
||||
<div className="column-settings-order">
|
||||
<button type="button" onClick={() => moveColumn(column.key, -1)}>
|
||||
↑
|
||||
</button>
|
||||
<button type="button" onClick={() => moveColumn(column.key, 1)}>
|
||||
↓
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="notice muted">No matching circuits for active filters.</div>
|
||||
</div>
|
||||
);
|
||||
@@ -1885,6 +2249,9 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
Apply sorted order
|
||||
</button>
|
||||
) : null}
|
||||
<button type="button" onClick={() => setIsColumnMenuOpen((current) => !current)}>
|
||||
Columns
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
@@ -1896,6 +2263,49 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
>
|
||||
Clear sort/filter
|
||||
</button>
|
||||
{isColumnMenuOpen ? (
|
||||
<div className="column-settings-menu">
|
||||
<div className="column-settings-actions">
|
||||
<button type="button" onClick={() => resetColumns()}>
|
||||
Reset columns
|
||||
</button>
|
||||
</div>
|
||||
<div className="column-settings-list">
|
||||
{orderedColumns.map((column) => {
|
||||
const visible = visibleColumnKeys.includes(column.key);
|
||||
return (
|
||||
<div
|
||||
key={`col-${column.key}`}
|
||||
className={`column-settings-item ${draggingColumnKey === column.key ? "dragging" : ""} ${columnDropTargetKey === column.key ? "drop-target" : ""} ${column.locked ? "locked" : ""}`}
|
||||
draggable={!column.locked}
|
||||
onDragStart={(event) => handleColumnDragStart(event, column.key)}
|
||||
onDragOver={(event) => handleColumnDragOver(event, column.key)}
|
||||
onDrop={(event) => handleColumnDrop(event, column.key)}
|
||||
onDragEnd={() => handleColumnDragEnd()}
|
||||
>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={visible}
|
||||
disabled={column.locked}
|
||||
onChange={() => toggleColumnVisibility(column.key)}
|
||||
/>
|
||||
<span>{column.label}</span>
|
||||
</label>
|
||||
<div className="column-settings-order">
|
||||
<button type="button" onClick={() => moveColumn(column.key, -1)}>
|
||||
↑
|
||||
</button>
|
||||
<button type="button" onClick={() => moveColumn(column.key, 1)}>
|
||||
↓
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{hasActiveSortOrFilter ? (
|
||||
<div className="notice muted">Sorting/filtering is view-only. Renumber is disabled while sort/filter is active.</div>
|
||||
@@ -1995,7 +2405,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
<table className="tree-grid">
|
||||
<thead>
|
||||
<tr>
|
||||
{columns.map((column) => (
|
||||
{visibleColumns.map((column) => (
|
||||
<th key={column.key} className={column.numeric ? "num" : ""}>
|
||||
<div className="header-cell">
|
||||
<button
|
||||
@@ -2119,7 +2529,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
}
|
||||
}}
|
||||
>
|
||||
<td colSpan={columns.length + 1} className="section-drop-cell">
|
||||
<td colSpan={visibleColumns.length + 1} className="section-drop-cell">
|
||||
<div className="section-content">
|
||||
<strong>{section.displayName}</strong>
|
||||
<div className="section-actions">
|
||||
@@ -2364,7 +2774,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
}
|
||||
}}
|
||||
>
|
||||
{columns.map((column) => {
|
||||
{visibleColumns.map((column) => {
|
||||
const cell = row.cells.find((entry) => entry.cellKey === column.key)!;
|
||||
const isSelected =
|
||||
selectedCell?.rowKey === row.rowKey && selectedCell.cellKey === column.key;
|
||||
|
||||
Reference in New Issue
Block a user