Web: Chargenliste (mit Sammel-Lagerort) + Ladezustand auf allen Tabellen
Neue Seite "Chargen": alle Chargen ueber alle Artikel (Artikel, Menge, MHD, Lagerort, eingelagert). Lagerort je Zeile direkt aenderbar; alle Chargen ohne Ort per Klick zuweisbar (bulk-location) - gut, wenn der Lagerort erst spaeter angelegt wurde. DataTable bekommt einen loading-Prop: waehrend des Ladens steht "Wird geladen..." statt der Leer-Meldung. Auf ALLEN Tabellen angewendet (Produkte, Mindestbestaende, Kategorien, Gruppen, Einheiten, Gebinde, Einzelstuecke, Chargen). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ import Login from "./pages/Login";
|
|||||||
import Dashboard from "./pages/Dashboard";
|
import Dashboard from "./pages/Dashboard";
|
||||||
import Products from "./pages/Products";
|
import Products from "./pages/Products";
|
||||||
import ItemList from "./pages/ItemList";
|
import ItemList from "./pages/ItemList";
|
||||||
|
import Charges from "./pages/Charges";
|
||||||
import MinStock from "./pages/MinStock";
|
import MinStock from "./pages/MinStock";
|
||||||
import ProductForm from "./pages/ProductForm";
|
import ProductForm from "./pages/ProductForm";
|
||||||
import CheckIn from "./pages/CheckIn";
|
import CheckIn from "./pages/CheckIn";
|
||||||
@@ -79,6 +80,7 @@ function Sidebar() {
|
|||||||
<NavItem to="/lebensmittel" icon="food" label="Lebensmittel" />
|
<NavItem to="/lebensmittel" icon="food" label="Lebensmittel" />
|
||||||
<NavItem to="/gegenstaende" icon="package" label="Gegenstände" />
|
<NavItem to="/gegenstaende" icon="package" label="Gegenstände" />
|
||||||
<NavItem to="/items" icon="tag" label="Einzelstücke" />
|
<NavItem to="/items" icon="tag" label="Einzelstücke" />
|
||||||
|
<NavItem to="/chargen" icon="box" label="Chargen" />
|
||||||
<NavItem to="/shopping" icon="cart" label="Einkaufsliste" />
|
<NavItem to="/shopping" icon="cart" label="Einkaufsliste" />
|
||||||
<NavItem to="/mindestbestaende" icon="check" label="Mindestbestände" />
|
<NavItem to="/mindestbestaende" icon="check" label="Mindestbestände" />
|
||||||
<NavItem to="/history" icon="history" label="Verlauf" />
|
<NavItem to="/history" icon="history" label="Verlauf" />
|
||||||
@@ -146,6 +148,7 @@ export default function App() {
|
|||||||
<Route path="/gegenstaende" element={<Protected wide><Products fixedType="object" /></Protected>} />
|
<Route path="/gegenstaende" element={<Protected wide><Products fixedType="object" /></Protected>} />
|
||||||
<Route path="/products" element={<Protected wide><Products /></Protected>} />
|
<Route path="/products" element={<Protected wide><Products /></Protected>} />
|
||||||
<Route path="/items" element={<Protected wide><ItemList /></Protected>} />
|
<Route path="/items" element={<Protected wide><ItemList /></Protected>} />
|
||||||
|
<Route path="/chargen" element={<Protected wide><Charges /></Protected>} />
|
||||||
<Route path="/products/new" element={<Protected adminOnly><ProductForm /></Protected>} />
|
<Route path="/products/new" element={<Protected adminOnly><ProductForm /></Protected>} />
|
||||||
<Route path="/products/:id" element={<Protected><ProductForm /></Protected>} />
|
<Route path="/products/:id" element={<Protected><ProductForm /></Protected>} />
|
||||||
<Route path="/i/:uid" element={<Protected><ItemResolve /></Protected>} />
|
<Route path="/i/:uid" element={<Protected><ItemResolve /></Protected>} />
|
||||||
|
|||||||
@@ -190,6 +190,11 @@ export const api = {
|
|||||||
listLots: (productId) =>
|
listLots: (productId) =>
|
||||||
request(`/lots${productId ? `?product_id=${productId}` : ""}`),
|
request(`/lots${productId ? `?product_id=${productId}` : ""}`),
|
||||||
updateLot: (id, body) => request(`/lots/${id}`, { method: "PATCH", body }),
|
updateLot: (id, body) => request(`/lots/${id}`, { method: "PATCH", body }),
|
||||||
|
// Alle Chargen (über alle Artikel) mit Artikel-Infos.
|
||||||
|
listAllLots: () => request("/lots/rows"),
|
||||||
|
// Lagerort mehrerer Chargen auf einmal setzen (null = leeren).
|
||||||
|
bulkLotLocation: (lotIds, locationId) =>
|
||||||
|
request("/lots/bulk-location", { method: "POST", body: { lot_ids: lotIds, location_id: locationId || null } }),
|
||||||
deleteLot: (id) => request(`/lots/${id}`, { method: "DELETE" }),
|
deleteLot: (id) => request(`/lots/${id}`, { method: "DELETE" }),
|
||||||
|
|
||||||
// Mindestbestand je Lagerort (ersetzt jeweils die komplette Liste).
|
// Mindestbestand je Lagerort (ersetzt jeweils die komplette Liste).
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ function savePersisted(id, data) {
|
|||||||
|
|
||||||
export default function DataTable({
|
export default function DataTable({
|
||||||
id, columns, rows, getRowKey, rowClassName,
|
id, columns, rows, getRowKey, rowClassName,
|
||||||
empty = "Nichts gefunden.", toolbarExtra = null, tree = null,
|
empty = "Nichts gefunden.", loading = false, toolbarExtra = null, tree = null,
|
||||||
}) {
|
}) {
|
||||||
const colByKey = useMemo(() => Object.fromEntries(columns.map((c) => [c.key, c])), [columns]);
|
const colByKey = useMemo(() => Object.fromEntries(columns.map((c) => [c.key, c])), [columns]);
|
||||||
|
|
||||||
@@ -382,7 +382,11 @@ export default function DataTable({
|
|||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
{displayRows.length === 0 && (
|
{displayRows.length === 0 && (
|
||||||
<tr><td className="empty" colSpan={orderedCols.length}>{empty}</td></tr>
|
// Während des Ladens NICHT „nichts gefunden" behaupten – die Leer-
|
||||||
|
// Meldung kommt erst, wenn die Daten wirklich da (und leer) sind.
|
||||||
|
<tr><td className="empty" colSpan={orderedCols.length}>
|
||||||
|
{loading ? "Wird geladen…" : empty}
|
||||||
|
</td></tr>
|
||||||
)}
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -26,12 +26,16 @@ export default function Categories() {
|
|||||||
const [feldKat, setFeldKat] = useState(null);
|
const [feldKat, setFeldKat] = useState(null);
|
||||||
// "" = alle, sonst "food"/"object": trennt Lebensmittel und Gegenstände.
|
// "" = alle, sonst "food"/"object": trennt Lebensmittel und Gegenstände.
|
||||||
const [typFilter, setTypFilter] = useState("");
|
const [typFilter, setTypFilter] = useState("");
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
setCategories(await api.listCategories());
|
setCategories(await api.listCategories());
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message);
|
setError(err.message);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,7 +174,7 @@ export default function Categories() {
|
|||||||
getRowKey={(c) => c.id}
|
getRowKey={(c) => c.id}
|
||||||
rowClassName={(c) => (feldKat && feldKat.id === c.id ? "row-active" : "")}
|
rowClassName={(c) => (feldKat && feldKat.id === c.id ? "row-active" : "")}
|
||||||
tree={{ column: "name", idOf: (c) => c.id, parentIdOf: (c) => c.parent_id }}
|
tree={{ column: "name", idOf: (c) => c.id, parentIdOf: (c) => c.parent_id }}
|
||||||
empty="Noch keine Kategorien." />
|
loading={loading} empty="Noch keine Kategorien." />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="stack">
|
<div className="stack">
|
||||||
|
|||||||
147
web/src/pages/Charges.jsx
Normal file
147
web/src/pages/Charges.jsx
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { api } from "../api";
|
||||||
|
import { useAuth } from "../auth";
|
||||||
|
import Icon from "../components/Icon";
|
||||||
|
import DataTable from "../components/DataTable";
|
||||||
|
import { ProduktThumb } from "../components/ProduktBild";
|
||||||
|
import { useSettings } from "../settings";
|
||||||
|
import { locationOptions, locationPathById } from "../locationPath";
|
||||||
|
import { fmt, gebinde, unitShort } from "../units";
|
||||||
|
|
||||||
|
// Artikeleinheit einer Charge (Gebinde, sonst Produkteinheit).
|
||||||
|
const artFactor = (l) => (l.package_size && l.package_size > 0 ? l.package_size : (l.unit_factor || 1));
|
||||||
|
function mengeText(l) {
|
||||||
|
const menge = l.quantity / artFactor(l);
|
||||||
|
const label = l.package_size && l.package_size > 0
|
||||||
|
? gebinde(menge, l.package_label)
|
||||||
|
: (l.unit_name || unitShort(l.base_unit));
|
||||||
|
return `${fmt(menge)} ${label}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Übergreifende Chargenliste (wie die Einzelstückliste, aber für Chargen). Zeigt
|
||||||
|
* je Charge Artikel, Menge, MHD und Lagerort; der Lagerort ist je Zeile direkt
|
||||||
|
* änderbar, und alle Chargen ohne Ort lassen sich in einem Klick zuordnen.
|
||||||
|
*/
|
||||||
|
export default function Charges() {
|
||||||
|
const { isAdmin } = useAuth();
|
||||||
|
const { formatBestBefore } = useSettings();
|
||||||
|
const [lots, setLots] = useState([]);
|
||||||
|
const [locations, setLocations] = useState([]);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [bulkLoc, setBulkLoc] = useState("");
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const [ls, locs] = await Promise.all([api.listAllLots(), api.listLocations()]);
|
||||||
|
setLots(ls);
|
||||||
|
setLocations(locs);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
useEffect(() => { load(); }, []);
|
||||||
|
|
||||||
|
const ohneOrt = useMemo(() => lots.filter((l) => !l.location_id), [lots]);
|
||||||
|
|
||||||
|
async function setLocation(lotId, locId) {
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
await api.updateLot(lotId, { location_id: locId || null });
|
||||||
|
setLots((ls) => ls.map((l) => (l.id === lotId ? { ...l, location_id: locId || null } : l)));
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyBulk() {
|
||||||
|
if (!bulkLoc || ohneOrt.length === 0) return;
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
await api.bulkLotLocation(ohneOrt.map((l) => l.id), bulkLoc);
|
||||||
|
setBulkLoc("");
|
||||||
|
await load();
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ key: "thumb", header: "", label: "Bild", fixed: true, width: 56,
|
||||||
|
render: (l) => <ProduktThumb productId={l.product_id} alt={l.product_name} /> },
|
||||||
|
{ key: "product", header: "Artikel", grow: true, min: 180,
|
||||||
|
filterText: (l) => `${l.product_name} ${l.product_brand || ""}`, sortValue: (l) => l.product_name,
|
||||||
|
render: (l) => (
|
||||||
|
<span className="cell-row">
|
||||||
|
<span className="strong">{l.product_name}</span>
|
||||||
|
{l.product_brand && <span className="muted small">{l.product_brand}</span>}
|
||||||
|
</span>
|
||||||
|
) },
|
||||||
|
{ key: "menge", header: "Menge", width: 150, align: "num",
|
||||||
|
sortValue: (l) => l.quantity / artFactor(l), render: mengeText },
|
||||||
|
{ key: "mhd", header: "MHD", width: 150,
|
||||||
|
filterText: (l) => (l.best_before ? formatBestBefore(l.best_before, l.best_before_precision) : ""),
|
||||||
|
sortValue: (l) => l.best_before || "9999-99-99",
|
||||||
|
render: (l) => (l.best_before ? formatBestBefore(l.best_before, l.best_before_precision) : <span className="muted">–</span>) },
|
||||||
|
{ key: "location", header: "Lagerort", width: 240,
|
||||||
|
filterText: (l) => (l.location_id ? (locationPathById(l.location_id, locations) || "") : "(ohne)"),
|
||||||
|
filterValues: (l) => [l.location_id ? (locationPathById(l.location_id, locations) || "?") : "(ohne)"],
|
||||||
|
render: (l) => (isAdmin ? (
|
||||||
|
<select value={l.location_id || ""} style={{ marginTop: 0, minWidth: 150 }}
|
||||||
|
onChange={(e) => setLocation(l.id, e.target.value)}>
|
||||||
|
<option value="">– ohne –</option>
|
||||||
|
{locationOptions(locations).map((o) => <option key={o.id} value={o.id}>{o.label}</option>)}
|
||||||
|
</select>
|
||||||
|
) : (
|
||||||
|
l.location_id ? (locationPathById(l.location_id, locations) || "?") : <span className="muted">– ohne –</span>
|
||||||
|
)) },
|
||||||
|
{ key: "created", header: "Eingelagert", width: 130,
|
||||||
|
sortValue: (l) => new Date(l.created_at).getTime(),
|
||||||
|
render: (l) => <span className="muted">{new Date(l.created_at).toLocaleDateString("de-DE")}</span> },
|
||||||
|
{ key: "details", header: "", label: "Artikel", fixed: true, width: 84, align: "num",
|
||||||
|
render: (l) => <Link to={`/products/${l.product_id}`}>Artikel</Link> },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="page-head">
|
||||||
|
<div>
|
||||||
|
<h1>Chargen</h1>
|
||||||
|
<div className="sub">
|
||||||
|
{loading ? "Wird geladen…" : `${lots.length} Chargen · ${ohneOrt.length} ohne Lagerort`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <div className="alert error"><Icon name="alert" size={16} />{error}</div>}
|
||||||
|
|
||||||
|
{isAdmin && ohneOrt.length > 0 && (
|
||||||
|
<div className="card" style={{ marginBottom: "var(--sp-3)" }}>
|
||||||
|
<div className="card-head"><Icon name="location" /><h2>Lagerort für alle ohne Ort setzen</h2></div>
|
||||||
|
<p className="muted small mt-0">
|
||||||
|
{ohneOrt.length} Charge(n) haben noch keinen Lagerort. Ort wählen und in einem Klick allen zuweisen.
|
||||||
|
</p>
|
||||||
|
<div className="field-inline">
|
||||||
|
<select value={bulkLoc} onChange={(e) => setBulkLoc(e.target.value)} style={{ maxWidth: 360 }}>
|
||||||
|
<option value="">– Lagerort wählen –</option>
|
||||||
|
{locationOptions(locations).map((o) => <option key={o.id} value={o.id}>{o.label}</option>)}
|
||||||
|
</select>
|
||||||
|
<button className="btn primary" disabled={!bulkLoc} onClick={applyBulk}>
|
||||||
|
<Icon name="check" size={16} />Auf alle {ohneOrt.length} anwenden
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="card">
|
||||||
|
<DataTable id="charges" columns={columns} rows={lots} loading={loading}
|
||||||
|
getRowKey={(l) => l.id} empty="Keine Chargen im Bestand." />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -17,8 +17,10 @@ export default function Groups() {
|
|||||||
const [form, setForm] = useState({ name: "", min_stock: "", min_stock_unit_id: "" });
|
const [form, setForm] = useState({ name: "", min_stock: "", min_stock_unit_id: "" });
|
||||||
const [selectedId, setSelectedId] = useState(null);
|
const [selectedId, setSelectedId] = useState(null);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const [gs, us, ls] = await Promise.all([
|
const [gs, us, ls] = await Promise.all([
|
||||||
api.listGroups(), api.listUnits(), api.listLocations(),
|
api.listGroups(), api.listUnits(), api.listLocations(),
|
||||||
@@ -28,6 +30,8 @@ export default function Groups() {
|
|||||||
setLocations(ls);
|
setLocations(ls);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message);
|
setError(err.message);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,7 +142,7 @@ export default function Groups() {
|
|||||||
|
|
||||||
<div className="grid-2 wide-aside">
|
<div className="grid-2 wide-aside">
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<DataTable id="groups" columns={columns} rows={groups}
|
<DataTable id="groups" columns={columns} rows={groups} loading={loading}
|
||||||
getRowKey={(g) => g.id} empty="Noch keine Gruppen." />
|
getRowKey={(g) => g.id} empty="Noch keine Gruppen." />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -21,14 +21,16 @@ export default function ItemList() {
|
|||||||
const [shops, setShops] = useState([]);
|
const [shops, setShops] = useState([]);
|
||||||
const [categories, setCategories] = useState([]);
|
const [categories, setCategories] = useState([]);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const [is, ls, ss, cs] = await Promise.all([
|
const [is, ls, ss, cs] = await Promise.all([
|
||||||
api.listAllItems(), api.listLocations(), api.listShops(), api.listCategories(),
|
api.listAllItems(), api.listLocations(), api.listShops(), api.listCategories(),
|
||||||
]);
|
]);
|
||||||
setItems(is); setLocations(ls); setShops(ss); setCategories(cs);
|
setItems(is); setLocations(ls); setShops(ss); setCategories(cs);
|
||||||
} catch (err) { setError(err.message); }
|
} catch (err) { setError(err.message); } finally { setLoading(false); }
|
||||||
}
|
}
|
||||||
useEffect(() => { load(); }, []);
|
useEffect(() => { load(); }, []);
|
||||||
|
|
||||||
@@ -100,7 +102,7 @@ export default function ItemList() {
|
|||||||
</div>
|
</div>
|
||||||
{error && <div className="alert error"><Icon name="alert" size={16} />{error}</div>}
|
{error && <div className="alert error"><Icon name="alert" size={16} />{error}</div>}
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<DataTable id="items" columns={columns} rows={items}
|
<DataTable id="items" columns={columns} rows={items} loading={loading}
|
||||||
getRowKey={(it) => it.id} empty="Noch keine Einzelstücke." />
|
getRowKey={(it) => it.id} empty="Noch keine Einzelstücke." />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -35,17 +35,19 @@ export default function MinStock() {
|
|||||||
const [locations, setLocations] = useState([]);
|
const [locations, setLocations] = useState([]);
|
||||||
const [categories, setCategories] = useState([]);
|
const [categories, setCategories] = useState([]);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
// „+ hinzufügen"-Dialog: Ziel (Produkt/Gruppe) → Geltung (Gesamt/Lagerort) → Menge.
|
// „+ hinzufügen"-Dialog: Ziel (Produkt/Gruppe) → Geltung (Gesamt/Lagerort) → Menge.
|
||||||
const [showAdd, setShowAdd] = useState(false);
|
const [showAdd, setShowAdd] = useState(false);
|
||||||
const [draft, setDraft] = useState(EMPTY_DRAFT);
|
const [draft, setDraft] = useState(EMPTY_DRAFT);
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const [ps, gs, ls, cs] = await Promise.all([
|
const [ps, gs, ls, cs] = await Promise.all([
|
||||||
api.listProducts("", ""), api.listGroups(), api.listLocations(), api.listCategories(),
|
api.listProducts("", ""), api.listGroups(), api.listLocations(), api.listCategories(),
|
||||||
]);
|
]);
|
||||||
setProducts(ps); setGroups(gs); setLocations(ls); setCategories(cs);
|
setProducts(ps); setGroups(gs); setLocations(ls); setCategories(cs);
|
||||||
} catch (err) { setError(err.message); }
|
} catch (err) { setError(err.message); } finally { setLoading(false); }
|
||||||
}
|
}
|
||||||
useEffect(() => { load(); }, []);
|
useEffect(() => { load(); }, []);
|
||||||
|
|
||||||
@@ -285,7 +287,7 @@ export default function MinStock() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<DataTable id="min-stock" columns={columns} rows={rows}
|
<DataTable id="min-stock" columns={columns} rows={rows} loading={loading}
|
||||||
getRowKey={(r) => r.key} empty="Noch keine Mindestbestände." />
|
getRowKey={(r) => r.key} empty="Noch keine Mindestbestände." />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,8 +19,10 @@ export default function PackageTypes() {
|
|||||||
const [form, setForm] = useState({ singular: "", plural: "" });
|
const [form, setForm] = useState({ singular: "", plural: "" });
|
||||||
const [bearbeitung, setBearbeitung] = useState(null); // { id, singular, plural }
|
const [bearbeitung, setBearbeitung] = useState(null); // { id, singular, plural }
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
setArten(await api.listPackageTypes());
|
setArten(await api.listPackageTypes());
|
||||||
// Die Mehrzahlformen stecken in einer Modulvariablen, die beim Login
|
// Die Mehrzahlformen stecken in einer Modulvariablen, die beim Login
|
||||||
@@ -29,6 +31,8 @@ export default function PackageTypes() {
|
|||||||
reloadSettings();
|
reloadSettings();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message);
|
setError(err.message);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +140,7 @@ export default function PackageTypes() {
|
|||||||
|
|
||||||
<div className="grid-2 wide-aside">
|
<div className="grid-2 wide-aside">
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<DataTable id="package-types" columns={columns} rows={arten}
|
<DataTable id="package-types" columns={columns} rows={arten} loading={loading}
|
||||||
getRowKey={(a) => a.id} empty="Noch keine Gebinde." />
|
getRowKey={(a) => a.id} empty="Noch keine Gebinde." />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -153,8 +153,7 @@ export default function Products({ fixedType = null }) {
|
|||||||
|
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<DataTable id={fixedType ? `products-${fixedType}` : "products"} columns={columns} rows={sichtbar}
|
<DataTable id={fixedType ? `products-${fixedType}` : "products"} columns={columns} rows={sichtbar}
|
||||||
getRowKey={(p) => p.id}
|
getRowKey={(p) => p.id} loading={loading} empty="Keine Produkte gefunden." />
|
||||||
empty={loading ? "Produkte werden geladen…" : "Keine Produkte gefunden."} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,12 +12,16 @@ export default function Units() {
|
|||||||
const [units, setUnits] = useState([]);
|
const [units, setUnits] = useState([]);
|
||||||
const [form, setForm] = useState({ name: "", kind: "weight", factor: "" });
|
const [form, setForm] = useState({ name: "", kind: "weight", factor: "" });
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
setUnits(await api.listUnits());
|
setUnits(await api.listUnits());
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message);
|
setError(err.message);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +91,7 @@ export default function Units() {
|
|||||||
|
|
||||||
<div className="grid-2 wide-aside">
|
<div className="grid-2 wide-aside">
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<DataTable id="units" columns={columns} rows={units}
|
<DataTable id="units" columns={columns} rows={units} loading={loading}
|
||||||
getRowKey={(u) => u.id} empty="Noch keine Einheiten." />
|
getRowKey={(u) => u.id} empty="Noch keine Einheiten." />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user