Schritt 1: Fundament der Lebensmittel-Lagerverwaltung (Pantry)
Backend (FastAPI + PostgreSQL): Chargen mit MHD, FEFO-Auslagern, Einheiten-Umrechnung (Stueck/g/ml + Packungen), Open-Food-Facts-Lookup mit lokalem Fallback, JWT-Auth mit Rollen (Admin/Nutzer), erster Admin beim Setup, Einkaufsliste, Ablaufwarnung, Lagerorte, pytest fuer FEFO. Web-UI (React/Vite): Login, Dashboard, Ein-/Auslagern, Produkte, Lagerorte, Benutzerverwaltung, Einkaufsliste - rollenabhaengig. Deploy: docker-compose + install.sh (Docker-Autoinstall, Secrets), README und Roadmap fuer Schritt 2 (iOS) und Schritt 3. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
140
web/src/pages/CheckOut.jsx
Normal file
140
web/src/pages/CheckOut.jsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import { useState } from "react";
|
||||
import { api } from "../api";
|
||||
import { fmt, unitOptions, unitShort } from "../units";
|
||||
|
||||
export default function CheckOut() {
|
||||
const [barcode, setBarcode] = useState("");
|
||||
const [search, setSearch] = useState("");
|
||||
const [results, setResults] = useState([]);
|
||||
const [product, setProduct] = useState(null);
|
||||
|
||||
const [quantity, setQuantity] = useState("");
|
||||
const [unit, setUnit] = useState("");
|
||||
|
||||
const [error, setError] = useState(null);
|
||||
const [info, setInfo] = useState(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
function selectProduct(p) {
|
||||
setProduct(p);
|
||||
setResults([]);
|
||||
setUnit(unitOptions(p)[0].value);
|
||||
}
|
||||
|
||||
async function doLookup() {
|
||||
setError(null); setInfo(null);
|
||||
if (!barcode) return;
|
||||
try {
|
||||
const res = await api.lookup(barcode.trim());
|
||||
if (res.found && res.existing_product) {
|
||||
selectProduct(res.existing_product);
|
||||
} else {
|
||||
setError("Kein bekanntes Produkt zu diesem Barcode im Lager.");
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function doSearch(e) {
|
||||
e.preventDefault();
|
||||
try {
|
||||
setResults(await api.listProducts(search));
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function submit(e) {
|
||||
e.preventDefault();
|
||||
setError(null); setInfo(null); setBusy(true);
|
||||
try {
|
||||
const res = await api.checkOut({
|
||||
product_id: product.id,
|
||||
quantity: Number(quantity),
|
||||
unit,
|
||||
});
|
||||
setInfo(`Ausgelagert (${res.affected_lots.length} Charge(n) betroffen). Neuer Bestand: ${fmt(res.product_stock)} ${unitShort(product.base_unit)}`);
|
||||
setQuantity("");
|
||||
setProduct(await api.getProduct(product.id));
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="page-head"><h1>📤 Auslagern</h1></div>
|
||||
{error && <div className="alert error">{error}</div>}
|
||||
{info && <div className="alert info">{info}</div>}
|
||||
|
||||
{!product && (
|
||||
<div className="grid-2">
|
||||
<div className="card">
|
||||
<h2>Per Barcode</h2>
|
||||
<div className="row">
|
||||
<input className="grow" placeholder="Barcode" value={barcode}
|
||||
onChange={(e) => setBarcode(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && doLookup()} />
|
||||
<button className="btn primary" onClick={doLookup}>Suchen</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card">
|
||||
<h2>Aus Produktliste</h2>
|
||||
<form className="row" onSubmit={doSearch}>
|
||||
<input className="grow" placeholder="Name suchen…" value={search}
|
||||
onChange={(e) => setSearch(e.target.value)} />
|
||||
<button className="btn">Suchen</button>
|
||||
</form>
|
||||
<ul className="picklist">
|
||||
{results.map((p) => (
|
||||
<li key={p.id}>
|
||||
<button className="link-btn" onClick={() => selectProduct(p)}>
|
||||
{p.name} <span className="muted">({fmt(p.stock)} {unitShort(p.base_unit)})</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{product && (
|
||||
<form className="card form-narrow" onSubmit={submit}>
|
||||
<div className="selected-product">
|
||||
{product.image_url && <img className="thumb" src={product.image_url} alt="" />}
|
||||
<div>
|
||||
<strong>{product.name}</strong>
|
||||
<div className="muted">
|
||||
Verfügbar: {fmt(product.stock)} {unitShort(product.base_unit)}
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" className="btn ghost" onClick={() => setProduct(null)}>Ändern</button>
|
||||
</div>
|
||||
|
||||
<div className="row">
|
||||
<label className="grow">
|
||||
Menge
|
||||
<input type="number" step="any" min="0" value={quantity} required
|
||||
onChange={(e) => setQuantity(e.target.value)} autoFocus />
|
||||
</label>
|
||||
<label className="grow">
|
||||
Einheit
|
||||
<select value={unit} onChange={(e) => setUnit(e.target.value)}>
|
||||
{unitOptions(product).map((o) => (
|
||||
<option key={o.value} value={o.value}>{o.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<p className="muted small">
|
||||
Es wird automatisch die zuerst ablaufende Charge zuerst entnommen (FEFO).
|
||||
</p>
|
||||
<button className="btn primary" disabled={busy}>{busy ? "…" : "Auslagern"}</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user