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:
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user