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

@@ -0,0 +1,60 @@
import { useState } from "react";
import Icon from "./Icon";
/**
* Liste zusätzlicher EAN-Codes mit Notiz ("Mehl bei Aldi").
* onAdd({ code, note }) und onDelete(code) werden vom Aufrufer bereitgestellt.
*/
export default function BarcodeList({ barcodes = [], onAdd, onDelete, disabled = false, hint }) {
const [code, setCode] = useState("");
const [note, setNote] = useState("");
const [busy, setBusy] = useState(false);
async function add(e) {
e.preventDefault();
if (!code.trim()) return;
setBusy(true);
try {
await onAdd({ code: code.trim(), note: note.trim() || null });
setCode("");
setNote("");
} finally {
setBusy(false);
}
}
return (
<div>
<ul className="simple-list">
{barcodes.map((b) => (
<li key={b.id ?? b.code}>
<span style={{ display: "flex", alignItems: "center", gap: 8, minWidth: 0 }}>
<code className="strong">{b.code}</code>
{b.note && <span className="muted small">{b.note}</span>}
</span>
{!disabled && (
<button className="btn-icon danger" title="Code entfernen"
onClick={() => onDelete(b.code)}>
<Icon name="trash" size={16} />
</button>
)}
</li>
))}
{barcodes.length === 0 && <li className="muted small">Noch keine zusätzlichen Codes.</li>}
</ul>
{!disabled && (
<form className="field-inline" onSubmit={add} style={{ marginTop: "var(--sp-3)" }}>
<input placeholder="EAN scannen oder eingeben" value={code}
onChange={(e) => setCode(e.target.value)} />
<input placeholder="Notiz, z.B. Mehl bei Aldi" value={note}
onChange={(e) => setNote(e.target.value)} />
<button className="btn" disabled={busy || !code.trim()}>
<Icon name="plus" size={16} />Hinzufügen
</button>
</form>
)}
{hint && <p className="muted small">{hint}</p>}
</div>
);
}