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:
@@ -4,6 +4,7 @@ import { useConfirm } from "../confirm";
|
||||
import { useAuth } from "../auth";
|
||||
import BarcodeList from "../components/BarcodeList";
|
||||
import Icon from "../components/Icon";
|
||||
import DataTable from "../components/DataTable";
|
||||
import LocationMinStock from "../components/LocationMinStock";
|
||||
import { fmt } from "../units";
|
||||
|
||||
@@ -77,6 +78,52 @@ export default function Groups() {
|
||||
|
||||
const selected = groups.find((g) => g.id === selectedId) || null;
|
||||
|
||||
const columns = [
|
||||
{ key: "name", header: "Gruppe", grow: true, min: 160,
|
||||
filterText: (g) => g.name, sortValue: (g) => g.name,
|
||||
render: (g) => {
|
||||
const low = g.min_stock != null && g.stock < g.min_stock;
|
||||
return (
|
||||
<span className="cell-row">
|
||||
{isAdmin ? (
|
||||
<input defaultValue={g.name} style={{ marginTop: 0, minWidth: 120 }}
|
||||
onBlur={(e) => { const v = e.target.value.trim(); if (v && v !== g.name) patch(g, { name: v }); }} />
|
||||
) : <span className="strong">{g.name}</span>}
|
||||
{low && <span className="badge warn">niedrig</span>}
|
||||
</span>
|
||||
);
|
||||
} },
|
||||
{ key: "products", header: "Produkte", width: 100, align: "num",
|
||||
sortValue: (g) => g.product_count, render: (g) => <span className="muted">{g.product_count}</span> },
|
||||
{ key: "stock", header: "Bestand", width: 130, align: "num",
|
||||
sortValue: (g) => g.stock, render: (g) => `${fmt(g.stock)} ${g.min_stock_unit_name || ""}` },
|
||||
{ key: "min", header: "Mindestbestand", width: 150, sortValue: (g) => g.min_stock ?? -1,
|
||||
render: (g) => (isAdmin ? (
|
||||
<input type="number" step="any" defaultValue={g.min_stock ?? ""} style={{ marginTop: 0, minWidth: 90 }}
|
||||
onBlur={(e) => { const v = e.target.value; if (v !== String(g.min_stock ?? "")) patch(g, { min_stock: v === "" ? null : Number(v) }); }} />
|
||||
) : (g.min_stock != null ? fmt(g.min_stock) : "–")) },
|
||||
{ key: "unit", header: "Einheit", width: 150, filterText: (g) => g.min_stock_unit_name || "",
|
||||
render: (g) => (isAdmin ? (
|
||||
<select value={g.min_stock_unit_id ?? ""} style={{ marginTop: 0 }}
|
||||
onChange={(e) => patch(g, { min_stock_unit_id: e.target.value === "" ? null : Number(e.target.value) })}>
|
||||
<option value="">– Basiseinheit –</option>
|
||||
{units.map((u) => <option key={u.id} value={u.id}>{u.name}</option>)}
|
||||
</select>
|
||||
) : (g.min_stock_unit_name || "–")) },
|
||||
{ key: "codes", header: "EAN-Codes", width: 130, align: "num",
|
||||
render: (g) => (
|
||||
<button className="link-btn" onClick={() => setSelectedId(g.id)}>
|
||||
{(g.product_barcodes?.length || 0) + (g.barcodes?.length || 0)} verwalten
|
||||
</button>
|
||||
) },
|
||||
{ key: "actions", header: "", fixed: true, width: 56, align: "num",
|
||||
render: (g) => isAdmin && (
|
||||
<button className="btn-icon danger" onClick={() => remove(g)} title="Löschen">
|
||||
<Icon name="trash" size={16} />
|
||||
</button>
|
||||
) },
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="page-head">
|
||||
@@ -90,83 +137,9 @@ export default function Groups() {
|
||||
{error && <div className="alert error"><Icon name="alert" size={16} />{error}</div>}
|
||||
|
||||
<div className="grid-2 wide-aside">
|
||||
<div className="card" style={{ padding: 0 }}>
|
||||
<div className="table-wrap">
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Gruppe</th>
|
||||
<th className="num">Produkte</th>
|
||||
<th className="num">Bestand</th>
|
||||
<th>Mindestbestand</th>
|
||||
<th>Einheit</th>
|
||||
<th className="num">EAN-Codes</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{groups.map((g) => {
|
||||
const low = g.min_stock != null && g.stock < g.min_stock;
|
||||
return (
|
||||
<tr key={g.id}>
|
||||
<td data-label="Gruppe">
|
||||
<span className="cell-row">
|
||||
{isAdmin ? (
|
||||
<input defaultValue={g.name} style={{ marginTop: 0, minWidth: 120 }}
|
||||
onBlur={(e) => {
|
||||
const v = e.target.value.trim();
|
||||
if (v && v !== g.name) patch(g, { name: v });
|
||||
}} />
|
||||
) : <span className="strong">{g.name}</span>}
|
||||
{low && <span className="badge warn">niedrig</span>}
|
||||
</span>
|
||||
</td>
|
||||
<td data-label="Produkte" className="num muted">{g.product_count}</td>
|
||||
<td data-label="Bestand" className="num">{fmt(g.stock)} {g.min_stock_unit_name || ""}</td>
|
||||
<td data-label="Mindestbestand">
|
||||
{isAdmin ? (
|
||||
<input type="number" step="any" defaultValue={g.min_stock ?? ""}
|
||||
style={{ marginTop: 0, minWidth: 90 }}
|
||||
onBlur={(e) => {
|
||||
const v = e.target.value;
|
||||
if (v !== String(g.min_stock ?? "")) {
|
||||
patch(g, { min_stock: v === "" ? null : Number(v) });
|
||||
}
|
||||
}} />
|
||||
) : (g.min_stock != null ? fmt(g.min_stock) : "–")}
|
||||
</td>
|
||||
<td data-label="Einheit">
|
||||
{isAdmin ? (
|
||||
<select value={g.min_stock_unit_id ?? ""} style={{ marginTop: 0 }}
|
||||
onChange={(e) => patch(g, {
|
||||
min_stock_unit_id: e.target.value === "" ? null : Number(e.target.value),
|
||||
})}>
|
||||
<option value="">– Basiseinheit –</option>
|
||||
{units.map((u) => <option key={u.id} value={u.id}>{u.name}</option>)}
|
||||
</select>
|
||||
) : (g.min_stock_unit_name || "–")}
|
||||
</td>
|
||||
<td data-label="EAN-Codes" className="num">
|
||||
<button className="link-btn" onClick={() => setSelectedId(g.id)}>
|
||||
{/* Beide Herkünfte zählen, sonst steht bei einer Gruppe
|
||||
mit Artikeln irreführend eine 0. */}
|
||||
{(g.product_barcodes?.length || 0) + (g.barcodes?.length || 0)} verwalten
|
||||
</button>
|
||||
</td>
|
||||
<td className="num">
|
||||
{isAdmin && (
|
||||
<button className="btn-icon danger" onClick={() => remove(g)} title="Löschen">
|
||||
<Icon name="trash" size={16} />
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
{groups.length === 0 && <tr><td colSpan={7} className="empty">Noch keine Gruppen.</td></tr>}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="card">
|
||||
<DataTable id="groups" columns={columns} rows={groups}
|
||||
getRowKey={(g) => g.id} empty="Noch keine Gruppen." />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user