Files
Vorrania/web/src/pages/ItemList.jsx
Scarriffle 826f4df4bd 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>
2026-07-29 13:58:07 +02:00

111 lines
5.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 { locationOptions, locationPathById } from "../locationPath";
import { categoryInfoMap } from "../categoryPath";
import CategoryPathLabel, { pathParts } from "../components/CategoryPathLabel";
/**
* Liste aller Einzelstücke (physische Exemplare, jedes mit eigener UID/Lagerort)
* anders als „Produkte", das nur die Katalog-Typen zeigt. Über die DataTable je
* Spalte filterbar, u.a. nach Lagerort. Lagerort und Shop sind inline änderbar.
*/
export default function ItemList() {
const { isAdmin } = useAuth();
const [items, setItems] = useState([]);
const [locations, setLocations] = useState([]);
const [shops, setShops] = useState([]);
const [categories, setCategories] = useState([]);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
async function load() {
setLoading(true);
try {
const [is, ls, ss, cs] = await Promise.all([
api.listAllItems(), api.listLocations(), api.listShops(), api.listCategories(),
]);
setItems(is); setLocations(ls); setShops(ss); setCategories(cs);
} catch (err) { setError(err.message); } finally { setLoading(false); }
}
useEffect(() => { load(); }, []);
// Kategorie als Pfad + Tokens (jede Ebene), damit "Klamotten" auch die
// Unterkategorien findet und die Oberkategorie im Filter wählbar ist.
const catInfo = useMemo(() => categoryInfoMap(categories), [categories]);
async function patch(it, body) {
try { await api.updateItem(it.id, body); await load(); }
catch (err) { setError(err.message); }
}
const preis = (it) => (it.price_cents != null
? `${(it.price_cents / 100).toFixed(2)} ${it.currency || ""}`.trim() : "");
const columns = [
{ key: "thumb", header: "", label: "Bild", fixed: true, width: 56,
render: (it) => <ProduktThumb productId={it.product_id} alt={it.product_name} /> },
{ key: "uid", header: "UID", width: 110, filterText: (it) => it.uid, sortValue: (it) => it.uid,
render: (it) => <span className="strong nowrap">{it.uid}</span> },
{ key: "product", header: "Produkt", grow: true, min: 180,
filterText: (it) => it.product_name || "", sortValue: (it) => it.product_name || "",
render: (it) => it.product_name || "" },
{ key: "brand", header: "Marke", width: 140, filterText: (it) => it.product_brand || "",
render: (it) => <span className="muted">{it.product_brand || ""}</span> },
{ key: "category", header: "Kategorie", width: 200,
filterText: (it) => catInfo.get(it.category_id)?.path || it.category_name || "",
filterValues: (it) => catInfo.get(it.category_id)?.tokens || (it.category_name ? [it.category_name] : [""]),
filterOptionLabel: (v) => (v === "" ? "(Leere)" : <CategoryPathLabel parts={pathParts(v)} />),
sortValue: (it) => catInfo.get(it.category_id)?.path || it.category_name || "",
render: (it) => <CategoryPathLabel parts={catInfo.get(it.category_id)?.parts} fallback={it.category_name || ""} /> },
{ key: "location", header: "Lagerort", width: 220,
filterText: (it) => locationPathById(it.location_id, locations),
render: (it) => (isAdmin ? (
<select value={it.location_id ?? ""} style={{ marginTop: 0 }}
onChange={(e) => patch(it, { location_id: e.target.value === "" ? null : e.target.value })}>
<option value=""> ohne </option>
{locationOptions(locations).map((o) => <option key={o.id} value={o.id}>{o.label}</option>)}
</select>
) : (locationPathById(it.location_id, locations) || "")) },
{ key: "shop", header: "Gekauft bei", width: 150, filterText: (it) => it.shop_name || "",
render: (it) => (isAdmin ? (
<select value={it.shop_id ?? ""} style={{ marginTop: 0 }}
onChange={(e) => patch(it, { shop_id: e.target.value === "" ? null : Number(e.target.value) })}>
<option value=""> unbekannt </option>
{shops.map((s) => <option key={s.id} value={s.id}>{s.name}</option>)}
</select>
) : (it.shop_name || "")) },
{ key: "acquired", header: "Gekauft am", width: 120, filterText: (it) => it.acquired_on || "",
sortValue: (it) => it.acquired_on || "", render: (it) => it.acquired_on || "" },
{ key: "warranty", header: "Garantie bis", width: 120, filterText: (it) => it.warranty_until || "",
sortValue: (it) => it.warranty_until || "", render: (it) => it.warranty_until || "" },
{ key: "price", header: "Preis", width: 110, align: "num",
sortValue: (it) => it.price_cents ?? -1, filterText: preis,
render: (it) => preis(it) || "" },
{ key: "note", header: "Notiz", width: 160, filterText: (it) => it.note || "",
render: (it) => <span className="muted">{it.note || ""}</span> },
{ key: "open", header: "", label: "Produkt öffnen", fixed: true, width: 84, align: "num",
render: (it) => <Link to={`/products/${it.product_id}`}>Produkt</Link> },
];
return (
<div>
<div className="page-head">
<div>
<h1>Einzelstücke</h1>
<div className="sub">{items.length} Exemplare · nach Lagerort, Produkt, Kategorie u.a. filterbar</div>
</div>
</div>
{error && <div className="alert error"><Icon name="alert" size={16} />{error}</div>}
<div className="card">
<DataTable id="items" columns={columns} rows={items} loading={loading}
getRowKey={(it) => it.id} empty="Noch keine Einzelstücke." />
</div>
</div>
);
}