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;
}
|