diff --git a/web/src/pages/Categories.jsx b/web/src/pages/Categories.jsx index 9d61bdc..c72e764 100644 --- a/web/src/pages/Categories.jsx +++ b/web/src/pages/Categories.jsx @@ -17,6 +17,17 @@ export default function Categories() { const [categories, setCategories] = useState([]); const [form, setForm] = useState({ name: "", parent_id: "" }); const [error, setError] = useState(null); + // Eingeklappte Oberkategorien; ihre Unterkategorien werden ausgeblendet. + const [collapsed, setCollapsed] = useState(() => new Set()); + + function toggle(id) { + setCollapsed((alt) => { + const neu = new Set(alt); + if (neu.has(id)) neu.delete(id); + else neu.add(id); + return neu; + }); + } async function load() { try { @@ -75,6 +86,20 @@ export default function Categories() { const tree = asTree(categories); const nameById = Object.fromEntries(categories.map((c) => [c.id, c.name])); + // Verschachtelung kann mehrstufig sein: eine Zeile ist versteckt, sobald + // irgendein Vorfahre eingeklappt ist – nicht nur der direkte Elternteil. + const parentOf = Object.fromEntries(categories.map((c) => [c.id, c.parent_id])); + const childCount = (id) => categories.filter((c) => c.parent_id === id).length; + function versteckt(id) { + let p = parentOf[id]; + while (p != null) { + if (collapsed.has(p)) return true; + p = parentOf[p]; + } + return false; + } + const sichtbar = tree.filter((c) => !versteckt(c.id)); + return (
@@ -101,12 +126,20 @@ export default function Categories() { - {tree.map((c) => ( + {sichtbar.map((c) => { + const kinder = childCount(c.id); + const zu = collapsed.has(c.id); + return ( - - {c.depth > 0 && } + + {kinder > 0 ? ( + + ) : } {isAdmin ? ( { @@ -114,8 +147,11 @@ export default function Categories() { if (v && v !== c.name) patch(c, { name: v }); }} /> ) : {c.name}} + {zu && kinder > 0 && ( + {kinder} ausgeblendet + )} {c.depth > 0 && c.parent_id && nameById[c.parent_id] && ( - in {nameById[c.parent_id]} + in {nameById[c.parent_id]} )} @@ -128,7 +164,8 @@ export default function Categories() { )} - ))} + ); + })} {categories.length === 0 && ( Noch keine Kategorien. )} diff --git a/web/src/pages/Units.jsx b/web/src/pages/Units.jsx index 6a7d09f..60e1e18 100644 --- a/web/src/pages/Units.jsx +++ b/web/src/pages/Units.jsx @@ -70,7 +70,12 @@ export default function Units() { {units.map((u) => ( - {u.name}{u.is_builtin && eingebaut} + + + {u.name} + {u.is_builtin && eingebaut} + + {KIND_LABEL[u.kind] || u.kind} {fmt(u.factor)} {baseUnitOfKind[u.kind]} diff --git a/web/src/styles.css b/web/src/styles.css index 2fce1ab..3fb0c16 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -269,6 +269,13 @@ td select { width: auto; min-width: 0; max-width: 100%; } /* Beschriftung mit Erklaerung im Tooltip: die gepunktete Linie zeigt an, dass dort etwas steht - so bleibt das Formular kurz, ohne Wissen zu verstecken. */ .tip { border-bottom: 1px dotted var(--border-strong); } + +/* Auf-/Zuklappen von Unterkategorien */ +.tree-toggle { padding: 2px; flex: 0 0 auto; } +.tree-toggle .icon { transition: transform 0.12s ease; } +.tree-toggle .icon.rotated { transform: rotate(90deg); } +/* Platzhalter, damit Zeilen ohne Unterkategorien buendig zu denen mit stehen. */ +.tree-spacer { display: inline-block; flex: 0 0 auto; width: 24px; } /* In Zahlenspalten sitzt der Inhalt rechts, passend zur Ausrichtung der Spalte. */ .table td.num .cell-row { justify-content: flex-end; } .table tbody tr:last-child td { border-bottom: none; } @@ -363,18 +370,18 @@ td select { width: auto; min-width: 0; max-width: 100%; } padding: var(--sp-2) var(--sp-3); margin-bottom: var(--sp-3); background: var(--surface); } - /* Zwei feste Spalten statt "space-between": Beschriftung links, Wert direkt - daneben. Vorher wurde der Wert an den rechten Rand geschoben, wodurch - Beschriftung und Wert weit auseinanderstanden und die Zeile unruhig wirkte. */ + /* Beschriftung links, Wert direkt daneben. Bewusst Flex statt Raster: bei + einem Raster landet ein zweites Element in der Zelle (z.B. das Badge + "eingebaut") in der naechsten Rasterzelle - also unter der Beschriftung. + Im Flexfluss reihen sich alle Teile hinter der Beschriftung auf. */ .table td { border: none; padding: 4px 0; - display: grid; - grid-template-columns: minmax(84px, 34%) minmax(0, 1fr); - align-items: baseline; - gap: var(--sp-3); text-align: left; + display: flex; align-items: baseline; + gap: var(--sp-2); text-align: left; flex-wrap: wrap; } .table td::before { content: attr(data-label); + flex: 0 0 84px; color: var(--muted); font-size: 0.7rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; }