Web: Kategorien (Baum), Gruppen, Einheiten, Gebinde auf DataTable
Restliche Tabellenseiten auf die filterbare DataTable umgestellt. Kategorien nutzen den neuen Tree-Modus (ein-/ausklappbar wie bisher; sobald gefiltert/sortiert wird, flache Liste). Inline-Bearbeitung (Namen, Art, uebergeordnet, Mindestbestand, Gebinde-Formen) bleibt in den Zell-Renderern erhalten; Seiten-Formulare/Panels unveraendert. Damit haben Produkte, Einzelstuecke, Kategorien, Gruppen, Einheiten und Gebinde die gleiche Filter-/Spalten-Bedienung. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,10 @@ import Icon from "./Icon";
|
||||
* Spalten-Def: { key, header, render(row)→JSX, filterText(row)? (aktiviert Filter),
|
||||
* sortValue(row)? (Sortierung), align:"num", grow:true (füllt Restbreite),
|
||||
* fixed:true (kein Reorder/Resize), width, min }.
|
||||
*
|
||||
* `tree` (optional): { column, idOf(row), parentIdOf(row) } – zeigt die Zeilen als
|
||||
* ein-/ausklappbaren Baum in der Spalte `column`. Sobald gefiltert oder sortiert
|
||||
* wird, wechselt die Ansicht auf eine flache, gefilterte Liste.
|
||||
*/
|
||||
const DEFAULT_WIDTH = 150;
|
||||
|
||||
@@ -24,7 +28,7 @@ function savePersisted(id, data) {
|
||||
|
||||
export default function DataTable({
|
||||
id, columns, rows, getRowKey, rowClassName,
|
||||
empty = "Nichts gefunden.", toolbarExtra = null,
|
||||
empty = "Nichts gefunden.", toolbarExtra = null, tree = null,
|
||||
}) {
|
||||
const colByKey = useMemo(() => Object.fromEntries(columns.map((c) => [c.key, c])), [columns]);
|
||||
|
||||
@@ -39,6 +43,7 @@ export default function DataTable({
|
||||
const [sort, setSort] = useState(null); // { key, dir }
|
||||
const [filterShown, setFilterShown] = useState(true);
|
||||
const [openFilter, setOpenFilter] = useState(null);
|
||||
const [collapsed, setCollapsed] = useState(() => new Set());
|
||||
|
||||
useEffect(() => { savePersisted(id, { order, widths }); }, [id, order, widths]);
|
||||
|
||||
@@ -77,7 +82,13 @@ export default function DataTable({
|
||||
return m;
|
||||
}, [columns, rows]);
|
||||
|
||||
const visible = useMemo(() => {
|
||||
const anyFilter = orderedCols.some((c) => {
|
||||
const f = filters[c.key];
|
||||
return f && (f.text || (f.values && f.values.size !== (distinctByKey[c.key]?.length ?? 0)));
|
||||
});
|
||||
|
||||
// Flach gefiltert + sortiert.
|
||||
const flat = useMemo(() => {
|
||||
let out = rows.filter((r) => orderedCols.every((c) => {
|
||||
const f = filters[c.key];
|
||||
if (!f || !c.filterText) return true;
|
||||
@@ -100,6 +111,49 @@ export default function DataTable({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [rows, order, filters, sort]);
|
||||
|
||||
// Baum-Reihenfolge (nur wenn tree und weder gefiltert noch sortiert).
|
||||
const treeActive = Boolean(tree) && !anyFilter && !sort;
|
||||
const treeData = useMemo(() => {
|
||||
if (!tree) return null;
|
||||
const byId = new Map(rows.map((r) => [tree.idOf(r), r]));
|
||||
const byParent = new Map();
|
||||
for (const r of rows) {
|
||||
const p = tree.parentIdOf(r);
|
||||
if (!byParent.has(p)) byParent.set(p, []);
|
||||
byParent.get(p).push(r);
|
||||
}
|
||||
const ids = new Set(rows.map(tree.idOf));
|
||||
const order2 = [];
|
||||
const walk = (r, depth) => {
|
||||
const kids = byParent.get(tree.idOf(r)) || [];
|
||||
order2.push({ row: r, depth, hasChildren: kids.length > 0 });
|
||||
for (const k of kids) walk(k, depth + 1);
|
||||
};
|
||||
for (const r of rows) {
|
||||
const p = tree.parentIdOf(r);
|
||||
if (p == null || !ids.has(p)) walk(r, 0);
|
||||
}
|
||||
return { order: order2, byId };
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [rows, tree]);
|
||||
|
||||
const displayRows = useMemo(() => {
|
||||
if (treeActive && treeData) {
|
||||
const hidden = (row) => {
|
||||
let p = tree.parentIdOf(row);
|
||||
while (p != null) {
|
||||
if (collapsed.has(p)) return true;
|
||||
const pr = treeData.byId.get(p);
|
||||
p = pr ? tree.parentIdOf(pr) : null;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
return treeData.order.filter((n) => !hidden(n.row));
|
||||
}
|
||||
return flat.map((row) => ({ row, depth: 0, hasChildren: false }));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [treeActive, treeData, flat, collapsed]);
|
||||
|
||||
function toggleSort(c) {
|
||||
if (!c.sortValue && !c.filterText) return;
|
||||
setSort((s) => {
|
||||
@@ -108,6 +162,9 @@ export default function DataTable({
|
||||
return null;
|
||||
});
|
||||
}
|
||||
function toggleCollapse(cid) {
|
||||
setCollapsed((s) => { const n = new Set(s); if (n.has(cid)) n.delete(cid); else n.add(cid); return n; });
|
||||
}
|
||||
|
||||
// --- Resize (Pointer) ---
|
||||
const resizing = useRef(null);
|
||||
@@ -208,14 +265,28 @@ export default function DataTable({
|
||||
)}
|
||||
</thead>
|
||||
<tbody>
|
||||
{visible.map((r) => (
|
||||
<tr key={getRowKey(r)} className={rowClassName?.(r) || ""}>
|
||||
{displayRows.map(({ row, depth, hasChildren }) => (
|
||||
<tr key={getRowKey(row)} className={rowClassName?.(row) || ""}>
|
||||
{orderedCols.map((c) => (
|
||||
<td key={c.key} className={c.align === "num" ? "num" : ""}>{c.render(r)}</td>
|
||||
<td key={c.key} className={c.align === "num" ? "num" : ""}>
|
||||
{tree && treeActive && c.key === tree.column ? (
|
||||
<span className="dt-tree-cell" style={{ paddingLeft: depth * 18 }}>
|
||||
{hasChildren ? (
|
||||
<button type="button" className="btn-icon tree-toggle"
|
||||
onClick={() => toggleCollapse(tree.idOf(row))}
|
||||
title={collapsed.has(tree.idOf(row)) ? "Ausklappen" : "Einklappen"}>
|
||||
<Icon name="chevronRight" size={12}
|
||||
className={collapsed.has(tree.idOf(row)) ? "" : "rotated"} />
|
||||
</button>
|
||||
) : <span className="tree-spacer-sm" />}
|
||||
{c.render(row)}
|
||||
</span>
|
||||
) : c.render(row)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
{visible.length === 0 && (
|
||||
{displayRows.length === 0 && (
|
||||
<tr><td className="empty" colSpan={orderedCols.length}>{empty}</td></tr>
|
||||
)}
|
||||
</tbody>
|
||||
|
||||
Reference in New Issue
Block a user