Anzeigefehler: die Gruppenliste klebte immer min_stock_unit_name hinter den
Bestand - auch wenn die Gruppe laengst im Gruppen-Gebinde zaehlt. Aus 790 g bei
einem 190-g-Glas wurde so "4,16 Gramm" statt "4,16 Glaeser". Gerechnet war
richtig, beschriftet falsch.
Ursache war doppelte Logik: MinStock.jsx hatte grpPkgMode/grpFactor/grpUnit
lokal, Groups.jsx gar nicht. Die drei stehen jetzt in units.js und werden von
beiden benutzt. Die Einheiten-Spalte sagt ausserdem, wenn eine Gruppe im
Gebinde zaehlt ("zaehlt in Glaeser a 190 g") und bietet den Weg zurueck an.
Gruppenliste als Baum: eine Gruppe mit zwei Obergruppen erscheint unter beiden -
das ist die ehrliche Darstellung eines Graphen. Jede Zeile traegt ihren PFAD als
Schluessel, damit die vorhandene Baumlogik der DataTable unveraendert zurecht
kommt; die kennt nur einen Elternteil je Zeile, und ein Pfad hat genau einen.
Beim Sortieren/Filtern flacht sie wie gewohnt ab.
"1 Untergruppen" heisst jetzt "1 Untergruppe" - an allen vier Stellen ueber
einen gemeinsamen Helfer (anzahlWort), Web und App.
Lebensmittelliste bekommt eine Spalte "Gruppe" mit Filter. Dafuer liefert
ProductOut jetzt group_name, symmetrisch zu category_name, mit joinedload gegen
N+1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
143 lines
6.0 KiB
JavaScript
143 lines
6.0 KiB
JavaScript
import { useEffect, useState } from "react";
|
|
import { api } from "../api";
|
|
import Icon from "../components/Icon";
|
|
import ShoppingNeedText from "../components/ShoppingNeedText";
|
|
import { anzahlWort } from "../units";
|
|
|
|
export default function ShoppingList() {
|
|
const [items, setItems] = useState([]);
|
|
const [groups, setGroups] = useState([]);
|
|
const [byLocation, setByLocation] = useState([]);
|
|
const [checked, setChecked] = useState({});
|
|
const [error, setError] = useState(null);
|
|
|
|
useEffect(() => {
|
|
Promise.all([api.shoppingList(), api.groupShoppingList(), api.shoppingByLocation()])
|
|
.then(([p, g, l]) => { setItems(p); setGroups(g); setByLocation(l); })
|
|
.catch((e) => setError(e.message));
|
|
}, []);
|
|
|
|
const empty = items.length === 0 && groups.length === 0;
|
|
|
|
function toggle(key, val) {
|
|
setChecked((c) => ({ ...c, [key]: val }));
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<div className="page-head">
|
|
<div>
|
|
<h1>Einkaufsliste</h1>
|
|
<div className="sub">Produkte und Gruppen unter ihrem Mindestbestand</div>
|
|
</div>
|
|
</div>
|
|
{error && <div className="alert error"><Icon name="alert" size={16} />{error}</div>}
|
|
|
|
{byLocation.length > 0 && (
|
|
<div className="sub" style={{ marginBottom: "var(--sp-2)" }}>Überall (egal wo)</div>
|
|
)}
|
|
|
|
<div className="card">
|
|
{empty ? (
|
|
<div className="empty">Alle Mindestbestände erreicht.</div>
|
|
) : (
|
|
<ul className="checklist">
|
|
{groups.map((it) => {
|
|
const key = `g${it.group_id}`;
|
|
return (
|
|
<li key={key} className={checked[key] ? "done" : ""}>
|
|
<label>
|
|
<input type="checkbox" checked={!!checked[key]} onChange={(e) => toggle(key, e.target.checked)} />
|
|
<span className="badge accent">Gruppe</span>
|
|
{it.subgroup_count > 0 && (
|
|
<span className="badge" title="Bestand und Fehlmenge zählen die Untergruppen mit; Käufe für Untergruppen sind bereits abgezogen.">
|
|
inkl. {anzahlWort(it.subgroup_count, "Untergruppe", "Untergruppen")}
|
|
</span>
|
|
)}
|
|
<span className="item-name">{it.name}</span>
|
|
</label>
|
|
<span className="muted small">
|
|
<ShoppingNeedText need={it.need} stock={it.stock} minStock={it.min_stock} unitName={it.unit_name} />
|
|
</span>
|
|
</li>
|
|
);
|
|
})}
|
|
{items.map((it) => {
|
|
const key = `p${it.product_id}`;
|
|
return (
|
|
<li key={key} className={checked[key] ? "done" : ""}>
|
|
<label>
|
|
<input type="checkbox" checked={!!checked[key]} onChange={(e) => toggle(key, e.target.checked)} />
|
|
<span className="item-name">{it.name}</span>
|
|
</label>
|
|
<span className="muted small">
|
|
<ShoppingNeedText need={it.need} stock={it.stock} minStock={it.min_stock} unitName={it.base_unit} />
|
|
</span>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
|
|
{byLocation.map((loc) => (
|
|
<div key={loc.location_id} style={{ marginTop: "var(--sp-4)" }}>
|
|
<div className="sub" style={{ marginBottom: "var(--sp-2)" }}>
|
|
<Icon name="location" size={14} /> Bedarf: {loc.location_name}
|
|
</div>
|
|
<div className="card">
|
|
<ul className="checklist">
|
|
{loc.groups.map((it) => {
|
|
const key = `l${loc.location_id}g${it.group_id}`;
|
|
return (
|
|
<li key={key} className={checked[key] ? "done" : ""}>
|
|
<label>
|
|
<input type="checkbox" checked={!!checked[key]} onChange={(e) => toggle(key, e.target.checked)} />
|
|
<span className="badge accent">Gruppe</span>
|
|
{it.subgroup_count > 0 && (
|
|
<span className="badge" title="Bestand und Fehlmenge zählen die Untergruppen mit; Käufe für Untergruppen sind bereits abgezogen.">
|
|
inkl. {anzahlWort(it.subgroup_count, "Untergruppe", "Untergruppen")}
|
|
</span>
|
|
)}
|
|
<span className="item-name">{it.name}</span>
|
|
</label>
|
|
<span className="muted small">
|
|
<ShoppingNeedText need={it.need} stock={it.stock} minStock={it.min_stock} unitName={it.unit_name} />
|
|
</span>
|
|
</li>
|
|
);
|
|
})}
|
|
{loc.products.map((it) => {
|
|
const key = `l${loc.location_id}p${it.product_id}`;
|
|
return (
|
|
<li key={key} className={checked[key] ? "done" : ""}>
|
|
<label>
|
|
<input type="checkbox" checked={!!checked[key]} onChange={(e) => toggle(key, e.target.checked)} />
|
|
<span className="item-name">{it.name}</span>
|
|
</label>
|
|
<span className="muted small">
|
|
<ShoppingNeedText need={it.need} stock={it.stock} minStock={it.min_stock} unitName={it.unit_label} />
|
|
</span>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
<p className="muted small">
|
|
Das Abhaken hilft beim Einkaufen und wird (noch) nicht dauerhaft gespeichert.
|
|
</p>
|
|
<p className="muted small">
|
|
Bedarfe sind gegeneinander verrechnet: Was du für einen Lagerort oder eine
|
|
Untergruppe kaufst, deckt „Überall“ bzw. die Obergruppe mit ab. Hängt
|
|
dieselbe Untergruppe unter zwei Obergruppen, die einander <em>nicht</em>
|
|
enthalten (Grillwurst unter Wurst und unter Grillgut), stehen beide Bedarfe
|
|
getrennt da — ein Kauf kann dann beide decken. Ein eigener Mindestbestand auf
|
|
der gemeinsamen Untergruppe verrechnet auch das.
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|