Mehrere EAN-Codes je Produkt und je Gruppe (mit Notiz) + Gruppen umbenennen

Barcodes:
- Neue Tabelle barcodes (code, note, product_id ODER group_id). Damit lassen sich
  einem Produkt mehrere Codes geben und einer Gruppe beliebig viele Marken
  zuordnen (z.B. alle Mehlmarken zu "Mehl"). Je Code eine Notiz wie
  "Mehl bei Aldi".
- Lookup loest jetzt auch Alias-Codes auf; ist ein Code einer Gruppe zugeordnet,
  liefert die API group_id/group_name zurueck. Beim Anlegen aus einem Scan wird
  diese Gruppe automatisch gesetzt (Vorrang vor dem Kategorie-Rateversuch).
- Endpunkte: POST/DELETE /products/{id}/barcodes und /groups/{id}/barcodes.
- Web: wiederverwendbare BarcodeList-Komponente, eingebunden im Produktformular
  und auf der Gruppen-Seite.

Gruppen:
- Namen lassen sich jetzt direkt in der Tabelle aendern (PATCH war vorhanden,
  nur die Oberflaeche fehlte).

UI-Fix: Auswahlfelder in Tabellen richten sich nach ihrem Text statt nach der
Zellenbreite (Gruppen-/Benutzer-Dropdowns waren abgeschnitten).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-22 14:48:14 +02:00
parent 8079d2d3f0
commit de6c8632a1
12 changed files with 341 additions and 48 deletions

View File

@@ -13,6 +13,8 @@ export default function CheckIn() {
const [barcode, setBarcode] = useState("");
const [product, setProduct] = useState(null);
const [suggestion, setSuggestion] = useState(null);
// Gruppe, die diesem Code zugeordnet ist (z.B. alle Mehl-Marken in "Mehl").
const [suggestionGroup, setSuggestionGroup] = useState({ id: "", name: "" });
const [unknownBarcode, setUnknownBarcode] = useState(null);
const [results, setResults] = useState([]);
const [search, setSearch] = useState("");
@@ -60,8 +62,10 @@ export default function CheckIn() {
setInfo(`Produkt erkannt: ${res.existing_product.name}`);
} else if (res.found && res.suggestion) {
setSuggestion(res.suggestion);
setSuggestionGroup({ id: res.group_id ? String(res.group_id) : "", name: res.group_name || "" });
} else {
setUnknownBarcode(barcode.trim());
setSuggestionGroup({ id: res.group_id ? String(res.group_id) : "", name: res.group_name || "" });
}
} catch (err) {
setError(err.message);
@@ -71,7 +75,9 @@ export default function CheckIn() {
async function createFromSuggestion() {
setError(null); setBusy(true);
try {
const payload = suggestionToProduct(suggestion, guessGroup(groups, suggestion));
// Ist der Code einer Gruppe zugeordnet, hat das Vorrang vor dem Kategorie-Rateversuch.
const groupId = suggestionGroup.id || guessGroup(groups, suggestion);
const payload = suggestionToProduct(suggestion, groupId);
const created = await api.createProduct(payload);
selectProduct(created);
setInfo(`Produkt "${created.name}" angelegt.`);
@@ -163,7 +169,10 @@ export default function CheckIn() {
{suggestion.brand ? `${suggestion.brand} · ` : ""}auf Open Food Facts gefunden
{suggestion.quantity_text ? ` · ${suggestion.quantity_text}` : ""}
</div>
<div className="muted small">Noch nicht im Katalog.</div>
<div className="muted small">
Noch nicht im Katalog.
{suggestionGroup.name && ` Gruppe: ${suggestionGroup.name}`}
</div>
</div>
{isAdmin ? (
<button className="btn primary" onClick={createFromSuggestion} disabled={busy}>