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">
|
||||
|
||||
Reference in New Issue
Block a user