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">