Artikeldaten von Open Food Facts vergleichen und einzeln uebernehmen
Neuer Knopf unter "Nachschlagen" auf der Artikelseite. Er fragt OFF erneut und
stellt die Antwort den eigenen Daten gegenueber: Name, Marke, Packungsgroesse,
Kategorie und Bild, jeweils mit eigenem Uebernehmen-Knopf und einem
"Alle uebernehmen". Uebernommen wird nur ins Formular - gespeichert erst mit
"Speichern", damit man bis zuletzt bei den eigenen Daten bleiben kann.
Dafuer noetig: GET /products/{id}/off. /lookup taugt hier nicht, weil es bei
einem bekannten Barcode den eigenen Artikel meldet und OFF gar nicht erst
fragt. Der neue Endpunkt probiert auch die zusaetzlichen EAN-Codes durch - oft
ist nur einer davon bei OFF hinterlegt.
Damit ist das Bild unabhaengig davon nachholbar, ob ein Backup eine
Bildadresse enthielt.
Nebenbei: Der Nachschlagen-Knopf uebergab die Gruppenliste, wo die
Einheitenliste erwartet wird - die Einheit eines Vorschlags wurde deshalb nie
richtig zugeordnet.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import { useConfirm } from "../confirm";
|
||||
import { useAuth } from "../auth";
|
||||
import BarcodeList from "../components/BarcodeList";
|
||||
import Icon from "../components/Icon";
|
||||
import OffVergleich from "../components/OffVergleich";
|
||||
import ProduktBild from "../components/ProduktBild";
|
||||
import { useToast } from "../toast";
|
||||
import { useSettings } from "../settings";
|
||||
@@ -50,6 +51,10 @@ export default function ProductForm() {
|
||||
// Einheit für die Mindestbestand-Eingabe: "package" oder die ID einer Einheit.
|
||||
const [minUnit, setMinUnit] = useState("");
|
||||
const [warnDays, setWarnDays] = useState(7);
|
||||
// Offene Gegenüberstellung mit Open Food Facts (null = zu).
|
||||
const [offDaten, setOffDaten] = useState(null);
|
||||
const [offBusy, setOffBusy] = useState(false);
|
||||
const [bildVersion, setBildVersion] = useState(0);
|
||||
|
||||
const selectedUnit = units.find((u) => String(u.id) === String(form.unit_id)) || null;
|
||||
const unitFactor = selectedUnit ? selectedUnit.factor : 1;
|
||||
@@ -125,6 +130,74 @@ export default function ProductForm() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open Food Facts erneut befragen, obwohl der Artikel schon existiert.
|
||||
* Übernommen wird nichts von allein – das entscheidet die Gegenüberstellung.
|
||||
*/
|
||||
async function holeOffDaten() {
|
||||
setError(null);
|
||||
setOffBusy(true);
|
||||
try {
|
||||
const res = await api.offCompare(id);
|
||||
if (res.found && res.suggestion) {
|
||||
setOffDaten({ ...res.suggestion, category_id: res.category_id, category_name: res.category_name });
|
||||
} else {
|
||||
setOffDaten(null);
|
||||
toast("Open Food Facts kennt keinen der hinterlegten EAN-Codes.", "warn");
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setOffBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
/** Welche Felder die Gegenüberstellung zeigt – und woher ihr Wert kommt. */
|
||||
function offFelder() {
|
||||
if (!offDaten) return [];
|
||||
const kategorie = categories.find((c) => String(c.id) === String(form.category_id));
|
||||
return [
|
||||
{ schluessel: "name", titel: "Name", eigen: form.name, fremd: offDaten.name || "" },
|
||||
{ schluessel: "brand", titel: "Marke", eigen: form.brand, fremd: offDaten.brand || "" },
|
||||
{
|
||||
schluessel: "package_size", titel: "Packungsgröße",
|
||||
eigen: form.package_size, fremd: offDaten.package_size ?? "",
|
||||
},
|
||||
{
|
||||
schluessel: "category_id", titel: "Kategorie",
|
||||
eigen: form.category_id, fremd: offDaten.category_id ?? "",
|
||||
anzeigeEigen: kategorie ? kategorie.name : "–",
|
||||
anzeigeFremd: offDaten.category_name || "–",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function uebernehmen(schluessel) {
|
||||
if (!offDaten) return;
|
||||
if (schluessel === "image_url") {
|
||||
set("image_url", offDaten.image_url || "");
|
||||
return;
|
||||
}
|
||||
if (schluessel === "package_size") {
|
||||
set("package_size", offDaten.package_size != null ? String(offDaten.package_size) : "");
|
||||
return;
|
||||
}
|
||||
if (schluessel === "category_id") {
|
||||
set("category_id", offDaten.category_id ? String(offDaten.category_id) : "");
|
||||
return;
|
||||
}
|
||||
set(schluessel, offDaten[schluessel] || "");
|
||||
}
|
||||
|
||||
function alleUebernehmen() {
|
||||
offFelder().forEach((f) => {
|
||||
if (f.fremd !== "" && f.fremd !== null && String(f.fremd) !== String(f.eigen)) {
|
||||
uebernehmen(f.schluessel);
|
||||
}
|
||||
});
|
||||
if (offDaten?.image_url) uebernehmen("image_url");
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
try {
|
||||
@@ -215,6 +288,10 @@ export default function ProductForm() {
|
||||
toast("Gespeichert.");
|
||||
setProduct(await api.getProduct(id));
|
||||
setLots(await api.listLots(id));
|
||||
// Der Server holt bei geaenderter Bildadresse ein neues Bild. Ohne
|
||||
// diesen Zaehler zeigte die Seite weiter das alte aus dem Zwischenspeicher.
|
||||
setBildVersion((v) => v + 1);
|
||||
setOffDaten(null);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
@@ -276,9 +353,18 @@ export default function ProductForm() {
|
||||
<input value={form.barcode} onChange={(e) => set("barcode", e.target.value)} disabled={readOnly} />
|
||||
</label>
|
||||
{isAdmin && (
|
||||
<button type="button" className="btn" onClick={() => runLookup(form.barcode, groups, units)}>
|
||||
<Icon name="search" size={16} />Nachschlagen
|
||||
</button>
|
||||
<div className="knopf-spalte">
|
||||
<button type="button" className="btn" onClick={() => runLookup(form.barcode, units)}>
|
||||
<Icon name="search" size={16} />Nachschlagen
|
||||
</button>
|
||||
{!isNew && (
|
||||
<button type="button" className="btn" disabled={offBusy}
|
||||
onClick={holeOffDaten}>
|
||||
<Icon name="download" size={16} />
|
||||
{offBusy ? "Frage an…" : "Mit Open Food Facts vergleichen"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<label>
|
||||
@@ -290,8 +376,19 @@ export default function ProductForm() {
|
||||
<input value={form.brand} onChange={(e) => set("brand", e.target.value)} disabled={readOnly} />
|
||||
</label>
|
||||
</div>
|
||||
{!isNew && <ProduktBild productId={id} alt={form.name} />}
|
||||
{!isNew && <ProduktBild productId={id} alt={form.name} version={bildVersion} />}
|
||||
</div>
|
||||
|
||||
{offDaten && (
|
||||
<OffVergleich
|
||||
felder={offFelder()}
|
||||
bildEigenId={id}
|
||||
bildFremdUrl={offDaten.image_url || ""}
|
||||
onUebernehmen={uebernehmen}
|
||||
onAlle={alleUebernehmen}
|
||||
onSchliessen={() => setOffDaten(null)}
|
||||
/>
|
||||
)}
|
||||
<div className="row">
|
||||
<label className="grow">
|
||||
Einheit
|
||||
|
||||
Reference in New Issue
Block a user