Kategorien ein-/ausklappbar, Badge-Darstellung in Tabellenzellen korrigiert

Kategorien:
- Oberkategorien lassen sich per Pfeil zuklappen; ihre Unterkategorien
  verschwinden dann aus der Liste. Ausgeblendet wird ueber die gesamte
  Vorfahrenkette, nicht nur eine Ebene, damit auch mehrstufige Baeume stimmen.
- Ist eine Kategorie zugeklappt, zeigt ein Badge, wie viele Eintraege darunter
  liegen - sonst waere nicht erkennbar, dass dort noch etwas ist.
- Zeilen ohne Unterkategorien bekommen einen Platzhalter, damit alle Namen
  buendig stehen.

Badges in Zellen:
- In der Karten-Ansicht verteilte das Raster mehrere Elemente einer Zelle auf
  zwei Rasterzellen - deshalb rutschte "eingebaut" unter die Beschriftung.
  Jetzt Flexfluss, alle Teile stehen hinter der Beschriftung.
- Einheiten-Tabelle: Name und "eingebaut" liegen in einer Zeile zusammen.

Geprueft: "npm run build" laeuft durch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-23 08:46:39 +02:00
parent 546a6bfc90
commit 903392e953
3 changed files with 63 additions and 14 deletions

View File

@@ -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 (
<div>
<div className="page-head">
@@ -101,12 +126,20 @@ export default function Categories() {
</tr>
</thead>
<tbody>
{tree.map((c) => (
{sichtbar.map((c) => {
const kinder = childCount(c.id);
const zu = collapsed.has(c.id);
return (
<tr key={c.id}>
<td data-label="Kategorie">
<span style={{ display: "flex", alignItems: "center", gap: 8,
paddingLeft: c.depth * 22 }}>
{c.depth > 0 && <span className="muted"></span>}
<span className="cell-row" style={{ paddingLeft: c.depth * 22 }}>
{kinder > 0 ? (
<button type="button" className="btn-icon tree-toggle"
onClick={() => toggle(c.id)} aria-expanded={!zu}
title={zu ? "Unterkategorien einblenden" : "Unterkategorien ausblenden"}>
<Icon name="chevronRight" size={14} className={zu ? "" : "rotated"} />
</button>
) : <span className="tree-spacer" />}
{isAdmin ? (
<input defaultValue={c.name} style={{ marginTop: 0, minWidth: 150 }}
onBlur={(e) => {
@@ -114,8 +147,11 @@ export default function Categories() {
if (v && v !== c.name) patch(c, { name: v });
}} />
) : <span className="strong">{c.name}</span>}
{zu && kinder > 0 && (
<span className="badge nowrap">{kinder} ausgeblendet</span>
)}
{c.depth > 0 && c.parent_id && nameById[c.parent_id] && (
<span className="badge">in {nameById[c.parent_id]}</span>
<span className="badge nowrap">in {nameById[c.parent_id]}</span>
)}
</span>
</td>
@@ -128,7 +164,8 @@ export default function Categories() {
)}
</td>
</tr>
))}
);
})}
{categories.length === 0 && (
<tr><td colSpan={3} className="empty">Noch keine Kategorien.</td></tr>
)}

View File

@@ -70,7 +70,12 @@ export default function Units() {
<tbody>
{units.map((u) => (
<tr key={u.id}>
<td data-label="Name" className="strong">{u.name}{u.is_builtin && <span className="badge" style={{ marginLeft: 6 }}>eingebaut</span>}</td>
<td data-label="Name" className="strong">
<span className="cell-row">
<span>{u.name}</span>
{u.is_builtin && <span className="badge nowrap">eingebaut</span>}
</span>
</td>
<td data-label="Art" className="muted">{KIND_LABEL[u.kind] || u.kind}</td>
<td data-label="Faktor" className="num">{fmt(u.factor)} {baseUnitOfKind[u.kind]}</td>
<td className="num">

View File

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