diff --git a/docs/spec/07-implementation-phases-todo.md b/docs/spec/07-implementation-phases-todo.md index 275be93..f3f879e 100644 --- a/docs/spec/07-implementation-phases-todo.md +++ b/docs/spec/07-implementation-phases-todo.md @@ -290,6 +290,9 @@ Implemented layout foundation: - active sorts and filters are summarized above the grid and can be removed individually or reset together - column filter buttons show their selected-value count - filter menus provide value search, select-all/select-none controls and explicit cancel/apply actions +- filter values and visible columns use full-width selectable rows instead of permanent checkbox controls +- column settings use the same searchable panel pattern as column filters +- column headings are visually plain text while remaining keyboard-accessible sort controls Acceptance criteria: diff --git a/src/app/globals.css b/src/app/globals.css index 80b8a73..e3a6ab1 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -115,24 +115,62 @@ body { margin-top: 2rem; border: 1px solid #cfd7e5; background: #fff; - border-radius: 4px; + border-radius: 5px; box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12); - padding: 0.45rem; - width: 300px; + padding: 0.55rem; + width: 340px; + color: #1f2937; + text-align: left; } -.column-settings-actions { +.column-settings-title-row, +.column-settings-footer { display: flex; - justify-content: flex-end; - margin-bottom: 0.35rem; + align-items: center; + justify-content: space-between; + gap: 0.4rem; +} + +.column-settings-title-row { + margin-bottom: 0.25rem; + font-size: 0.82rem; +} + +.column-settings-close { + border: 0 !important; + background: transparent !important; + color: #4b5563; + padding: 0.05rem 0.2rem !important; + font-size: 1rem !important; + line-height: 1; + cursor: pointer; +} + +.column-settings-explanation { + margin-bottom: 0.4rem; + color: #4b5563; + font-size: 0.72rem; + line-height: 1.3; +} + +.column-settings-search { + width: 100%; + margin-bottom: 0.4rem; + border: 1px solid #c4cddc; + border-radius: 4px; + padding: 0.3rem 0.4rem; + font-size: 0.76rem; } .column-settings-list { display: flex; flex-direction: column; - gap: 0.25rem; - max-height: 340px; + gap: 0.15rem; + max-height: 300px; overflow: auto; + border: 1px solid #e1e6ef; + border-radius: 4px; + padding: 0.25rem; } .column-settings-item { @@ -142,7 +180,12 @@ body { gap: 0.4rem; border: 1px solid transparent; border-radius: 4px; - padding: 0.2rem 0.25rem; + padding: 0.1rem; +} + +.column-settings-item.selected { + border-color: #bfdbfe; + background: #eff6ff; } .column-settings-item.dragging { @@ -158,11 +201,32 @@ body { background: #f7fafc; } -.column-settings-item label { +.column-visibility-button { display: flex; - gap: 0.3rem; align-items: center; + gap: 0.35rem; + flex: 1; + min-width: 0; + border: 0 !important; + background: transparent !important; + padding: 0.2rem 0.25rem !important; + color: inherit; + text-align: left; font-size: 0.8rem; + cursor: pointer; +} + +.column-visibility-button:disabled { + cursor: default; +} + +.selection-marker { + display: inline-flex; + flex: 0 0 1rem; + align-items: center; + justify-content: center; + color: #1d4ed8; + font-weight: 700; } .column-settings-order { @@ -189,6 +253,23 @@ body { scrollbar-gutter: stable; } +.column-settings-empty { + padding: 0.55rem 0.35rem; + color: #6b7280; + font-size: 0.76rem; +} + +.column-settings-footer { + justify-content: flex-end; + margin-top: 0.45rem; +} + +.column-settings-footer button.primary { + border-color: #2563eb; + background: #2563eb; + color: #fff; +} + .tree-editor-layout { display: grid; grid-template-columns: minmax(240px, 320px) minmax(0, 1fr); @@ -300,7 +381,6 @@ body { gap: 0.3rem; } -.tree-grid .header-sort-btn, .tree-grid .header-filter-btn { border: 1px solid #c4cddc; background: #fff; @@ -310,7 +390,28 @@ body { } .tree-grid .header-sort-btn { + display: inline-flex; + align-items: center; + gap: 0.25rem; + border: 0; + background: transparent; + padding: 0; + color: #1f2937; + font: inherit; + font-size: 0.78rem; + font-weight: 600; text-align: left; + cursor: pointer; +} + +.tree-grid .header-sort-btn:hover span:first-child, +.tree-grid .header-sort-btn:focus-visible span:first-child { + text-decoration: underline; +} + +.tree-grid .sort-indicator { + color: #2563eb; + font-size: 0.85rem; } .tree-grid .header-filter-btn.active { @@ -407,9 +508,26 @@ body { .tree-grid .header-filter-item { display: flex; align-items: center; - gap: 0.25rem; + gap: 0.35rem; + width: 100%; + border: 1px solid transparent; + border-radius: 3px; + background: transparent; + padding: 0.25rem 0.3rem; + color: #1f2937; font-weight: 400; font-size: 0.75rem; + text-align: left; + cursor: pointer; +} + +.tree-grid .header-filter-item:hover { + background: #f3f4f6; +} + +.tree-grid .header-filter-item.selected { + border-color: #bfdbfe; + background: #eff6ff; } .tree-grid .header-filter-empty { diff --git a/src/frontend/components/circuit-tree-editor.tsx b/src/frontend/components/circuit-tree-editor.tsx index e0d19b0..0d743c9 100644 --- a/src/frontend/components/circuit-tree-editor.tsx +++ b/src/frontend/components/circuit-tree-editor.tsx @@ -127,7 +127,7 @@ type CircuitReorderDropIntent = const COLUMN_LAYOUT_STORAGE_KEY = "circuitTreeEditor.columnLayout.v1"; function normalizeUiError(err: unknown): string { - const message = err instanceof Error ? err.message : "Operation failed."; + const message = err instanceof Error ? err.message : "Der Vorgang ist fehlgeschlagen."; try { const parsed = JSON.parse(message) as { error?: string }; if (parsed.error?.includes("Duplicate equipmentIdentifier")) { @@ -202,6 +202,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str const [visibleColumnKeys, setVisibleColumnKeys] = useState(defaultVisibleColumnKeys); const [columnOrder, setColumnOrder] = useState(allColumns.map((column) => column.key)); const [isColumnMenuOpen, setIsColumnMenuOpen] = useState(false); + const [columnSearch, setColumnSearch] = useState(""); const [draggingColumnKey, setDraggingColumnKey] = useState(null); const [columnDropTargetKey, setColumnDropTargetKey] = useState(null); const [pendingFocus, setPendingFocus] = useState(null); @@ -404,16 +405,16 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str : undefined; return ( -
- Sort and filters +
+ Sortierung und Filter {sortState ? ( ) : null} @@ -426,7 +427,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str type="button" className="active-view-chip" onClick={() => clearColumnFilter(column.key)} - title={`Remove filter for ${column.label}`} + title={`Filter für ${column.label} entfernen`} > {column.label}: {valueSummary} @@ -434,12 +435,100 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str ); })}
); } + function closeColumnSettingsMenu() { + setIsColumnMenuOpen(false); + setColumnSearch(""); + } + + function toggleColumnSettingsMenu() { + setIsColumnMenuOpen((current) => { + if (current) { + setColumnSearch(""); + } + return !current; + }); + } + + function renderColumnSettingsMenu(keyPrefix: string) { + if (!isColumnMenuOpen) { + return null; + } + + const search = columnSearch.trim().toLocaleLowerCase("de-DE"); + const matchingColumns = orderedColumns.filter((column) => + column.label.toLocaleLowerCase("de-DE").includes(search) + ); + + return ( +
+
+ Spalten auswählen + +
+
+ Spalten ein- oder ausblenden. Die Reihenfolge lässt sich mit den Pfeilen oder per Ziehen ändern. +
+ setColumnSearch(event.target.value)} + placeholder="Spalten suchen …" + aria-label="Spalten suchen" + autoFocus + /> +
+ {matchingColumns.map((column) => { + const visible = visibleColumnKeys.includes(column.key); + return ( +
handleColumnDragStart(event, column.key)} + onDragOver={(event) => handleColumnDragOver(event, column.key)} + onDrop={(event) => handleColumnDrop(event, column.key)} + onDragEnd={() => handleColumnDragEnd()} + > + +
+ + +
+
+ ); + })} + {matchingColumns.length === 0 ?
Keine passende Spalte gefunden.
: null} +
+
+ + +
+
+ ); + } + const searchableProjectDevices = useMemo(() => { const term = projectDeviceSearch.trim().toLowerCase(); if (!term) { @@ -459,7 +548,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str section.circuits.map((circuit) => ({ id: circuit.id, sectionId: section.id, - label: `${circuit.equipmentIdentifier} - ${circuit.displayName || "Circuit"}`, + label: `${circuit.equipmentIdentifier} - ${circuit.displayName || "Stromkreis"}`, })) ); }, [data]); @@ -758,7 +847,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str // Sorting is view-only until explicitly applied; this avoids accidental persisted reorders. await runCommand({ - label: "Apply sorted order", + label: "Sortierte Reihenfolge übernehmen", redo: async () => { for (const section of changedSections) { await reorderSectionCircuits(section.sectionId, section.order); @@ -1096,7 +1185,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str ): Promise { const section = data?.sections.find((entry) => entry.id === sectionId); if (!section) { - throw new Error("Section not found."); + throw new Error("Bereich wurde nicht gefunden."); } const next = await getNextCircuitIdentifier(sectionId); const sortOrder = @@ -1105,15 +1194,15 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str const createdCircuit = (await createCircuit(projectId, circuitListId, { sectionId, equipmentIdentifier: next.nextIdentifier, - displayName: "New circuit", + displayName: "Neuer Stromkreis", sortOrder, isReserve: !isDeviceField, })) as CircuitTreeCircuitDto; if (isDeviceField) { const createdRow = (await createCircuitDeviceRow(createdCircuit.id, { - name: "Manual device", - displayName: "Manual device", + name: "Manuelles Gerät", + displayName: "Manuelles Gerät", phaseType: "single_phase", quantity: 1, powerPerUnit: 0, @@ -1179,7 +1268,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str if (row.rowType === "placeholder") { let createdCircuitId: string | null = null; const command: HistoryCommand = { - label: "Create from placeholder", + label: "Aus freier Zeile erstellen", redo: async () => { const selection = await createFromPlaceholder(row.sectionId, editingCell.cellKey, editingCell.draft); targetSelection = selection; @@ -1206,7 +1295,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str const previous = getCurrentDraftForCell(row, editingCell.cellKey, cell.kind); const next = editingCell.draft; const command: HistoryCommand = { - label: "Edit circuit field", + label: "Stromkreisfeld bearbeiten", redo: async () => { await patchCircuit(row.circuit!.id, editingCell.cellKey, next); return nextSelectionIntent(); @@ -1225,7 +1314,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str const previous = getCurrentDraftForCell(row, editingCell.cellKey, cell.kind); const next = editingCell.draft; const command: HistoryCommand = { - label: "Edit device field", + label: "Gerätefeld bearbeiten", redo: async () => { await patchDeviceRow(row.device!.id, editingCell.cellKey, next); return nextSelectionIntent(); @@ -1244,11 +1333,11 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str const next = editingCell.draft; let createdRowId: string | null = null; const command: HistoryCommand = { - label: "Create reserve device row", + label: "Gerätezeile im Reservestromkreis erstellen", redo: async () => { const created = (await createCircuitDeviceRow(row.circuit!.id, { - name: "Reserve load", - displayName: "Reserve load", + name: "Reserveverbraucher", + displayName: "Reserveverbraucher", phaseType: "single_phase", quantity: 1, powerPerUnit: 0, @@ -1290,7 +1379,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str } let createdCircuitId: string | null = null; await runCommand({ - label: "Add circuit", + label: "Stromkreis hinzufügen", redo: async () => { const next = await getNextCircuitIdentifier(sectionId); const sortOrder = getInsertionSortOrder(section.circuits, afterCircuitId); @@ -1334,11 +1423,11 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str const section = data?.sections.find((entry) => entry.id === sectionId); let createdRowId: string | null = null; await runCommand({ - label: "Add manual device", + label: "Manuelles Gerät hinzufügen", redo: async () => { const created = (await createCircuitDeviceRow(circuit.id, { - name: "Manual device", - displayName: "Manual device", + name: "Manuelles Gerät", + displayName: "Manuelles Gerät", phaseType: section?.key === "three_phase" ? "three_phase" : "single_phase", quantity: 1, powerPerUnit: 0, @@ -1406,7 +1495,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str activeSectionId ?? data?.sections[0]?.id ); if (!intent) { - setError("No section is available for insertion."); + setError("Zum Einfügen ist kein Bereich verfügbar."); return; } if (intent.kind === "circuit") { @@ -1417,7 +1506,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str .flatMap((sectionEntry) => sectionEntry.circuits) .find((entry) => entry.id === intent.circuitId); if (!circuit) { - setError("Invalid circuit id."); + setError("Der Stromkreis ist ungültig."); return; } await handleAddManualDevice(circuit, intent.sectionId, intent.afterDeviceRowId); @@ -1444,19 +1533,19 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str const expectedKey = inferProjectDeviceSectionKey(device); const expected = data?.sections.find((entry) => entry.key === expectedKey); if (!target) { - return "Invalid target section."; + return "Der Zielbereich ist ungültig."; } - return `${device.displayName || device.name} belongs in ${expected?.displayName ?? expectedKey}, not ${target.displayName}.`; + return `${device.displayName || device.name} gehört in den Bereich ${expected?.displayName ?? expectedKey}, nicht in ${target.displayName}.`; } async function handleAddProjectDeviceAsNewCircuit() { const device = resolveSelectedProjectDevice(); if (!device) { - setError("Please select a project device."); + setError("Bitte ein Projektgerät auswählen."); return; } if (!targetSectionId) { - setError("Please select a target section."); + setError("Bitte einen Zielbereich auswählen."); return; } if (!isProjectDeviceTargetValid(device, targetSectionId)) { @@ -1466,7 +1555,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str let createdCircuitId: string | null = null; let createdRowId: string | null = null; await runCommand({ - label: "Add project device as new circuit", + label: "Projektgerät als neuen Stromkreis hinzufügen", redo: async () => { const created = await insertProjectDeviceAsNewCircuit(device, targetSectionId); createdCircuitId = created.circuitId; @@ -1497,7 +1586,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str async function handleAddProjectDeviceToCircuit() { const device = resolveSelectedProjectDevice(); if (!device) { - setError("Please select a project device."); + setError("Bitte ein Projektgerät auswählen."); return; } let circuitId = targetCircuitId; @@ -1508,7 +1597,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str } } if (!circuitId) { - setError("Please select a target circuit."); + setError("Bitte einen Zielstromkreis auswählen."); return; } const sectionId = findCircuitSectionId(circuitId); @@ -1519,7 +1608,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str let createdRowId: string | null = null; await runCommand({ - label: "Add project device to circuit", + label: "Projektgerät zum Stromkreis hinzufügen", redo: async () => { const created = await insertProjectDeviceToCircuit(device, circuitId); createdRowId = created.rowId; @@ -1550,7 +1639,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str async function insertProjectDeviceAsNewCircuit(device: ProjectDeviceDto, sectionId: string) { const section = data?.sections.find((entry) => entry.id === sectionId); if (!section) { - throw new Error("Invalid target section."); + throw new Error("Der Zielbereich ist ungültig."); } const next = await getNextCircuitIdentifier(sectionId); const sortOrder = @@ -1806,10 +1895,10 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str const targetName = data?.sections.find((section) => section.id === targetSectionId)?.displayName ?? targetSectionId; return confirm( - `Move ${rowIds.length} device row(s) from ${sourceNames} to ${targetName}?\n\n` + - "This crosses a section boundary. Phase type, category, project-device links and local values remain unchanged. " + - "The target section's technical and numbering classification may therefore need manual review.\n\n" + - "Existing circuit identifiers will not be renumbered." + `${rowIds.length} Gerätezeile(n) von ${sourceNames} nach ${targetName} verschieben?\n\n` + + "Dabei wird eine Bereichsgrenze überschritten. Phasenart, Kategorie, Projektgeräte-Verknüpfungen und lokale Werte bleiben unverändert. " + + "Die technische und nummerische Zuordnung im Zielbereich muss daher möglicherweise manuell geprüft werden.\n\n" + + "Bestehende Betriebsmittelkennzeichen werden nicht neu nummeriert." ); } @@ -1818,12 +1907,12 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str async function applyCircuitReorder(intent: CircuitReorderDropIntent, sourceCircuitIds: string[]) { const section = data?.sections.find((entry) => entry.id === intent.sectionId); if (!section) { - throw new Error("Invalid section id."); + throw new Error("Der Bereich ist ungültig."); } const ids = section.circuits.map((circuit) => circuit.id); const block = sourceCircuitIds.filter((id) => ids.includes(id)); if (block.length === 0) { - throw new Error("Invalid source circuit."); + throw new Error("Der Quellstromkreis ist ungültig."); } const blockSet = new Set(block); const nextIds = ids.filter((id) => !blockSet.has(id)); @@ -1833,7 +1922,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str } else { const targetIndex = nextIds.indexOf(intent.targetCircuitId); if (targetIndex < 0) { - throw new Error("Invalid target circuit."); + throw new Error("Der Zielstromkreis ist ungültig."); } const insertIndex = intent.kind === "after-circuit" ? targetIndex + 1 : targetIndex; nextIds.splice(insertIndex, 0, ...block); @@ -1852,12 +1941,12 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str setDraggingDeviceRowIds([]); setDraggingCircuitIds([]); if (!draggedId) { - setError("Missing dragged project device."); + setError("Das gezogene Projektgerät fehlt."); return; } const device = projectDevices.find((entry) => entry.id === draggedId); if (!device) { - setError("Invalid project device drop source."); + setError("Die Quelle des gezogenen Projektgeräts ist ungültig."); return; } if (!intent.valid || !isProjectDeviceTargetValid(device, intent.sectionId)) { @@ -1869,7 +1958,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str if (intent.kind === "new-circuit") { let createdCircuitId: string | null = null; await runCommand({ - label: "Drop project device to new circuit", + label: "Projektgerät in neuen Stromkreis ziehen", redo: async () => { const created = await insertProjectDeviceAsNewCircuit(device, intent.sectionId); createdCircuitId = created.circuitId; @@ -1893,7 +1982,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str } let createdRowId: string | null = null; await runCommand({ - label: "Drop project device to circuit", + label: "Projektgerät in Stromkreis ziehen", redo: async () => { const created = await insertProjectDeviceToCircuit(device, intent.circuitId); createdRowId = created.rowId; @@ -1928,7 +2017,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str setDraggingDeviceRowIds([]); setDraggingCircuitIds([]); if (rowIds.length === 0) { - setError("Missing dragged device row."); + setError("Die gezogene Gerätezeile fehlt."); return; } @@ -1936,14 +2025,14 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str for (const rowId of rowIds) { const sourceCircuitId = findDeviceRowCircuitId(rowId); if (!sourceCircuitId) { - setError("Invalid dragged device row."); + setError("Die gezogene Gerätezeile ist ungültig."); return; } sourceByRowId.set(rowId, sourceCircuitId); } const sourceCircuitId = sourceByRowId.get(rowIds[0]); if (!sourceCircuitId) { - setError("Invalid dragged device row source."); + setError("Die Quelle der gezogenen Gerätezeile ist ungültig."); return; } @@ -1966,7 +2055,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str groupedBySource.get(source)!.push(rowId); } await runCommand({ - label: rowIds.length > 1 ? `Move ${rowIds.length} device rows` : "Move device row", + label: rowIds.length > 1 ? `${rowIds.length} Gerätezeilen verschieben` : "Gerätezeile verschieben", redo: async () => { await moveCircuitDeviceRowsBulk({ rowIds, targetCircuitId: intent.circuitId }); pendingSelectedDeviceRowIdsAfterReload.current = rowIds; @@ -1993,7 +2082,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str groupedBySource.get(source)!.push(rowId); } await runCommand({ - label: rowIds.length > 1 ? `Move ${rowIds.length} device rows to new circuit` : "Move device row to new circuit", + label: rowIds.length > 1 ? `${rowIds.length} Gerätezeilen in neuen Stromkreis verschieben` : "Gerätezeile in neuen Stromkreis verschieben", redo: async () => { const moved = await moveCircuitDeviceRowsBulk({ rowIds, @@ -2028,27 +2117,27 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str setDraggingCircuitId(null); setDraggingCircuitIds([]); if (sourceCircuitIds.length === 0) { - setError("Missing dragged circuit."); + setError("Der gezogene Stromkreis fehlt."); return; } if (!intent.valid) { - setError("Cross-section circuit move is not allowed in this phase."); + setError("Stromkreise können derzeit nicht bereichsübergreifend verschoben werden."); return; } const section = data?.sections.find((entry) => entry.id === intent.sectionId); if (!section) { - setError("Invalid section id."); + setError("Der Bereich ist ungültig."); return; } const sectionCircuitIds = new Set(section.circuits.map((circuit) => circuit.id)); if (sourceCircuitIds.some((id) => !sectionCircuitIds.has(id))) { - setError("Cross-section circuit move is not allowed in this phase."); + setError("Stromkreise können derzeit nicht bereichsübergreifend verschoben werden."); return; } const beforeOrder = section.circuits.map((circuit) => circuit.id); const primaryCircuitId = sourceCircuitIds[0]; await runCommand({ - label: sourceCircuitIds.length > 1 ? `Reorder ${sourceCircuitIds.length} circuits` : "Reorder circuits", + label: sourceCircuitIds.length > 1 ? `${sourceCircuitIds.length} Stromkreise neu anordnen` : "Stromkreise neu anordnen", redo: async () => { await applyCircuitReorder(intent, sourceCircuitIds); pendingSelectedCircuitIdsAfterReload.current = sourceCircuitIds; @@ -2080,31 +2169,31 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str const sourceCircuit = sourceCircuitId ? getCircuitSnapshot(sourceCircuitId) : null; const rowSnapshot = sourceCircuit?.deviceRows.find((row) => row.id === rowId); if (!sourceCircuitId || !sourceCircuit || !rowSnapshot) { - setError("Invalid device row id."); + setError("Die Gerätezeile ist ungültig."); return; } if (sourceCircuit.deviceRows.length === 1) { const keepAsReserve = confirm( - `"${rowSnapshot.displayName}" is the last device in ${sourceCircuit.equipmentIdentifier}.\n\n` + - "OK: delete the device and keep the empty circuit as reserve.\n" + - "Cancel: choose whether to delete the whole circuit instead." + `„${rowSnapshot.displayName}“ ist das letzte Gerät in ${sourceCircuit.equipmentIdentifier}.\n\n` + + "OK: Gerät löschen und den leeren Stromkreis als Reserve behalten.\n" + + "Abbrechen: anschließend entscheiden, ob stattdessen der gesamte Stromkreis gelöscht werden soll." ); if (!keepAsReserve) { const deleteCircuit = confirm( - `Delete the complete circuit ${sourceCircuit.equipmentIdentifier}?\n\n` + - "OK: delete circuit and device.\nCancel: do not delete anything." + `Den vollständigen Stromkreis ${sourceCircuit.equipmentIdentifier} löschen?\n\n` + + "OK: Stromkreis und Gerät löschen.\nAbbrechen: nichts löschen." ); if (deleteCircuit) { await handleDeleteCircuit(sourceCircuitId, false); } return; } - } else if (!confirm(`Delete device row "${rowSnapshot.displayName}"?`)) { + } else if (!confirm(`Gerätezeile „${rowSnapshot.displayName}“ löschen?`)) { return; } let recreatedRowId: string | null = null; await runCommand({ - label: "Delete device row", + label: "Gerätezeile löschen", redo: async () => { await deleteCircuitDeviceRowById(recreatedRowId ?? rowId); return null; @@ -2141,21 +2230,21 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str async function handleDeleteCircuit(circuitId: string, askForConfirmation = true) { const snapshot = getCircuitSnapshot(circuitId); if (!snapshot) { - setError("Invalid circuit id."); + setError("Der Stromkreis ist ungültig."); return; } if ( askForConfirmation && !confirm( - `Delete circuit ${snapshot.equipmentIdentifier} and ${snapshot.deviceRows.length} assigned device row(s)?\n\n` + - "Existing circuit identifiers will not be renumbered." + `Stromkreis ${snapshot.equipmentIdentifier} und ${snapshot.deviceRows.length} zugeordnete Gerätezeile(n) löschen?\n\n` + + "Bestehende Betriebsmittelkennzeichen werden nicht neu nummeriert." ) ) { return; } let recreatedCircuitId: string | null = null; await runCommand({ - label: "Delete circuit", + label: "Stromkreis löschen", redo: async () => { await deleteCircuitById(recreatedCircuitId ?? circuitId); return null; @@ -2193,7 +2282,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str // Explicit renumber action. Undo restores previous BMKs via safe section identifier update. async function handleRenumberSection(sectionId: string) { - if (!confirm("Renumber this section? Only circuits in this section will change.")) { + if (!confirm("Diesen Bereich neu nummerieren? Nur die Stromkreise in diesem Bereich werden geändert.")) { return; } const section = data?.sections.find((entry) => entry.id === sectionId); @@ -2205,7 +2294,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str equipmentIdentifier: circuit.equipmentIdentifier, })); await runCommand({ - label: "Renumber section", + label: "Bereich neu nummerieren", redo: async () => { await renumberCircuitSection(sectionId); return null; @@ -2334,86 +2423,44 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str } if (isLoading && !data) { - return
Loading circuit tree editor...
; + return
Stromkreislisteneditor wird geladen …
; } if (error && !data) { return
{error}
; } if (!data || data.sections.length === 0) { - return
No sections/circuits available.
; + return
Keine Bereiche oder Stromkreise vorhanden.
; } if (filteredSortedSections.length === 0) { return (
- - {isColumnMenuOpen ? ( -
-
- -
-
- {orderedColumns.map((column) => { - const visible = visibleColumnKeys.includes(column.key); - return ( -
handleColumnDragStart(event, column.key)} - onDragOver={(event) => handleColumnDragOver(event, column.key)} - onDrop={(event) => handleColumnDrop(event, column.key)} - onDragEnd={() => handleColumnDragEnd()} - > - -
- - -
-
- ); - })} -
-
- ) : null} + {renderColumnSettingsMenu("col-empty")}
{renderActiveViewSummary()} {error ? (
{error} - +
) : null} -
No matching circuits for active filters.
+
Für die aktiven Filter wurden keine passenden Stromkreise gefunden.
); } @@ -2456,100 +2503,58 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
{isSortedView ? ( ) : null} - - {isColumnMenuOpen ? ( -
-
- -
-
- {orderedColumns.map((column) => { - const visible = visibleColumnKeys.includes(column.key); - return ( -
handleColumnDragStart(event, column.key)} - onDragOver={(event) => handleColumnDragOver(event, column.key)} - onDrop={(event) => handleColumnDrop(event, column.key)} - onDragEnd={() => handleColumnDragEnd()} - > - -
- - -
-
- ); - })} -
-
- ) : null} + {renderColumnSettingsMenu("col")}
{renderActiveViewSummary()} {hasActiveSortOrFilter ? ( -
Sorting/filtering is view-only. Renumber is disabled while sort/filter is active.
+
Sortierung und Filter verändern nur die Ansicht. Die Neunummerierung ist währenddessen deaktiviert.
) : null} {isSortedView && hasActiveFilters ? ( -
Apply sorted order is disabled while filters are active.
+
Die sortierte Reihenfolge kann erst nach dem Zurücksetzen der Filter übernommen werden.
) : null} {error ? (
{error} - +
) : null} {duplicateEquipmentIdentifierCircuitIds.size > 0 ? (
- Duplicate equipment identifiers are highlighted. Renumbering remains an explicit action. + Doppelte Betriebsmittelkennzeichen sind hervorgehoben. Eine Neunummerierung erfolgt weiterhin nur ausdrücklich.
) : null} - {isSaving ?
Saving...
: null} + {isSaving ?
Wird gespeichert …
: null}
{selectedProjectDevice && suggestedSection ? ( -

Suggested section: {suggestedSection.displayName}

+

Vorgeschlagener Bereich: {suggestedSection.displayName}

) : null}
{filterDraftValues.length === 0 ? ( - Select at least one value. + Mindestens einen Wert auswählen. ) : ( )}
- +
@@ -2782,7 +2795,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str ) : null} ))} - Actions + Aktionen @@ -2853,22 +2866,22 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str {section.displayName}
{dropIntent?.kind === "new-circuit" && dropIntent.sectionId === section.id ? ( - {dropIntent.valid ? "drop here to create new circuit" : "device does not match this section"} + {dropIntent.valid ? "Hier ablegen, um einen neuen Stromkreis zu erstellen" : "Das Gerät passt nicht in diesen Bereich"} ) : null} @@ -3294,7 +3307,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str ref={inputRef} value={editingCell.draft} aria-invalid={hasDraftIdentifierConflict} - title={hasDraftIdentifierConflict ? "Equipment identifier already exists." : undefined} + title={hasDraftIdentifierConflict ? "Dieses Betriebsmittelkennzeichen ist bereits vorhanden." : undefined} onChange={(event) => setEditingCell((current) => current ? { ...current, draft: event.target.value } : current @@ -3359,42 +3372,42 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str tabIndex={-1} onClick={() => void handleAddManualDevice(row.circuit!, row.sectionId)} > - Add manual device + Manuelles Gerät hinzufügen ) : null} {row.device ? ( ) : null} {dropIntent?.kind === "new-circuit" && row.rowType === "placeholder" && dropIntent.sectionId === row.sectionId ? ( - {dropIntent.valid ? "new circuit in this section" : "device does not match this section"} + {dropIntent.valid ? "Neuer Stromkreis in diesem Bereich" : "Das Gerät passt nicht in diesen Bereich"} ) : null} {dropIntent?.kind === "add-to-circuit" && dropIntent.circuitId === row.circuit?.id ? ( - {dropIntent.valid ? "add to this circuit" : "device does not match this circuit section"} + {dropIntent.valid ? "Zu diesem Stromkreis hinzufügen" : "Das Gerät passt nicht in den Bereich dieses Stromkreises"} ) : null} {deviceMoveIntent?.kind === "move-to-new-circuit" && row.rowType === "placeholder" && deviceMoveIntent.sectionId === row.sectionId ? ( - {`move ${draggingDeviceCount || 1} device${draggingDeviceCount === 1 ? "" : "s"} to new circuit${deviceMoveIntent.requiresConfirmation ? " (confirmation required)" : ""}`} + {`${draggingDeviceCount || 1} Gerätezeile(n) in einen neuen Stromkreis verschieben${deviceMoveIntent.requiresConfirmation ? " (Bestätigung erforderlich)" : ""}`} ) : null} {deviceMoveIntent?.kind === "move-to-circuit" && deviceMoveIntent.circuitId === row.circuit?.id ? ( - {`move ${draggingDeviceCount || 1} device${draggingDeviceCount === 1 ? "" : "s"} to this circuit${deviceMoveIntent.requiresConfirmation ? " (confirmation required)" : ""}`} + {`${draggingDeviceCount || 1} Gerätezeile(n) in diesen Stromkreis verschieben${deviceMoveIntent.requiresConfirmation ? " (Bestätigung erforderlich)" : ""}`} ) : null} {circuitReorderIntent?.kind === "section-end" && @@ -3402,8 +3415,8 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str circuitReorderIntent.sectionId === row.sectionId ? ( {circuitReorderIntent.valid - ? `move ${draggingCircuitCount || 1} circuit${draggingCircuitCount === 1 ? "" : "s"} to section end` - : "cross-section move not allowed"} + ? `${draggingCircuitCount || 1} Stromkreis(e) ans Bereichsende verschieben` + : "Bereichsübergreifendes Verschieben nicht zulässig"} ) : null} {circuitReorderIntent && @@ -3412,9 +3425,9 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str {circuitReorderIntent.valid ? circuitReorderIntent.kind === "before-circuit" - ? `move ${draggingCircuitCount || 1} circuit${draggingCircuitCount === 1 ? "" : "s"} before this circuit` - : `move ${draggingCircuitCount || 1} circuit${draggingCircuitCount === 1 ? "" : "s"} after this circuit` - : "cross-section move not allowed"} + ? `${draggingCircuitCount || 1} Stromkreis(e) vor diesen Stromkreis verschieben` + : `${draggingCircuitCount || 1} Stromkreis(e) hinter diesen Stromkreis verschieben` + : "Bereichsübergreifendes Verschieben nicht zulässig"} ) : null} @@ -3426,7 +3439,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str

- Ctrl+Plus inserts below the selected circuit or device context. Use Undo to revert the insertion. + Strg+Plus fügt unterhalb des ausgewählten Stromkreises oder Geräts ein. Mit „Rückgängig“ lässt sich das Einfügen zurücknehmen.

); diff --git a/src/frontend/utils/circuit-grid-model.ts b/src/frontend/utils/circuit-grid-model.ts index e1326c5..56908a5 100644 --- a/src/frontend/utils/circuit-grid-model.ts +++ b/src/frontend/utils/circuit-grid-model.ts @@ -55,36 +55,36 @@ export interface ColumnDef { // BMK stays locked as the first column because circuit identity and circuit drag // handles depend on an always-visible equipment identifier. export 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: "voltage", label: "Voltage", numeric: true }, - { key: "controlRequirement", label: "Control requirement" }, + { key: "equipmentIdentifier", label: "Betriebsmittelkennzeichen", defaultVisible: true, locked: true }, + { key: "displayName", label: "Anzeigename", defaultVisible: true }, + { key: "quantity", label: "Anzahl", numeric: true, defaultVisible: true }, + { key: "powerPerUnit", label: "Leistung / Gerät", numeric: true, defaultVisible: true }, + { key: "simultaneityFactor", label: "Gleichzeitigkeit", numeric: true, defaultVisible: true }, + { key: "rowTotalPower", label: "Zeilensumme", numeric: true, defaultVisible: true }, + { key: "circuitTotalPower", label: "Stromkreissumme", numeric: true, defaultVisible: true }, + { key: "protectionSummary", label: "Schutz", defaultVisible: true }, + { key: "cableSummary", label: "Kabel", defaultVisible: true }, + { key: "roomSummary", label: "Raum", defaultVisible: true }, + { key: "remark", label: "Bemerkung", defaultVisible: true }, + { key: "technicalName", label: "Technischer Name" }, + { key: "connectionKind", label: "Anschlussart" }, + { key: "phaseType", label: "Phasenart" }, + { key: "costGroup", label: "Kostengruppe" }, + { key: "category", label: "Kategorie" }, + { key: "level", label: "Ebene" }, + { key: "roomNumberSnapshot", label: "Raumnummer" }, + { key: "roomNameSnapshot", label: "Raumname" }, + { key: "cosPhi", label: "cos φ", numeric: true }, + { key: "protectionType", label: "Schutzart" }, + { key: "protectionRatedCurrent", label: "Bemessungsstrom", numeric: true }, + { key: "protectionCharacteristic", label: "Charakteristik" }, + { key: "cableType", label: "Kabeltyp" }, + { key: "cableCrossSection", label: "Kabelquerschnitt" }, + { key: "cableLength", label: "Kabellänge", numeric: true }, + { key: "rcdAssignment", label: "RCD-Zuordnung" }, + { key: "terminalDesignation", label: "Klemmenbezeichnung" }, + { key: "voltage", label: "Spannung", numeric: true }, + { key: "controlRequirement", label: "Steuerungsanforderung" }, { key: "status", label: "Status" }, { key: "isReserve", label: "Reserve" }, ]; @@ -172,7 +172,7 @@ export function formatValue(value: GridValue): string { return "-"; } if (typeof value === "boolean") { - return value ? "Yes" : "No"; + return value ? "Ja" : "Nein"; } return String(value); }