Die Kategorie bestimmt die Verwaltungsart (food/object). Gegenstaende: Menge je Lagerort statt Chargen/MHD, Umlagern (ohne Grund) und Entfernen mit Pflicht-Grund samt Entnahme-Statistik. Beliebig viele eigene Felder je Kategorie (vererbt an Unterkategorien), "gekauft bei" ueber eine verwaltbare Shop-Liste und ein Produktlink. Barcode-Lookup zusaetzlich ueber Open Products Facts. Der Lebensmittel-Teil (Chargen/MHD/FEFO) bleibt unveraendert. Umgesetzt in Backend (FastAPI, +Tests gruen), Web (React) und iOS (SwiftUI). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
132 lines
4.2 KiB
JavaScript
132 lines
4.2 KiB
JavaScript
import { useEffect, useState } from "react";
|
||
import { api } from "../api";
|
||
import { useConfirm } from "../confirm";
|
||
import { useAuth } from "../auth";
|
||
import Icon from "../components/Icon";
|
||
|
||
/**
|
||
* Shops / Bezugsquellen: verwaltbare Liste für „gekauft bei“ an Gegenständen.
|
||
* Am Artikel ist die Auswahl optional (unbekannt / mehrere Herkünfte = leer).
|
||
*/
|
||
export default function Shops() {
|
||
const confirm = useConfirm();
|
||
const { isAdmin } = useAuth();
|
||
const [shops, setShops] = useState([]);
|
||
const [form, setForm] = useState({ name: "", website: "" });
|
||
const [error, setError] = useState(null);
|
||
|
||
async function load() {
|
||
try {
|
||
setShops(await api.listShops());
|
||
} catch (err) {
|
||
setError(err.message);
|
||
}
|
||
}
|
||
|
||
useEffect(() => { load(); }, []);
|
||
|
||
async function add(e) {
|
||
e.preventDefault();
|
||
setError(null);
|
||
try {
|
||
await api.createShop({ name: form.name, website: form.website || null });
|
||
setForm({ name: "", website: "" });
|
||
load();
|
||
} catch (err) {
|
||
setError(err.message);
|
||
}
|
||
}
|
||
|
||
async function patch(shop, body) {
|
||
setError(null);
|
||
try {
|
||
await api.updateShop(shop.id, body);
|
||
load();
|
||
} catch (err) {
|
||
setError(err.message);
|
||
}
|
||
}
|
||
|
||
async function remove(shop) {
|
||
const hinweis = shop.product_count
|
||
? ` ${shop.product_count} Artikel verlieren die Bezugsquelle, bleiben aber erhalten.`
|
||
: "";
|
||
const ok = await confirm({
|
||
title: `Shop „${shop.name}“ löschen?`,
|
||
message: `Die Liste ist nur eine Auswahlhilfe.${hinweis}`,
|
||
confirmLabel: "Löschen",
|
||
danger: true,
|
||
});
|
||
if (!ok) return;
|
||
try {
|
||
await api.deleteShop(shop.id);
|
||
load();
|
||
} catch (err) {
|
||
setError(err.message);
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div>
|
||
<div className="page-head">
|
||
<div>
|
||
<h1>Shops</h1>
|
||
<div className="sub">
|
||
Bezugsquellen für „gekauft bei“ an Gegenständen – einmal anlegen, überall auswählbar.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{error && <div className="alert error"><Icon name="alert" size={16} />{error}</div>}
|
||
|
||
<div className="card form-narrow">
|
||
{isAdmin && (
|
||
<form onSubmit={add}>
|
||
<div className="row">
|
||
<label className="grow">
|
||
Name
|
||
<input placeholder="z.B. MediaMarkt" value={form.name}
|
||
onChange={(e) => setForm({ ...form, name: e.target.value })} required />
|
||
</label>
|
||
<label className="grow">
|
||
Website (optional)
|
||
<input placeholder="https://…" value={form.website}
|
||
onChange={(e) => setForm({ ...form, website: e.target.value })} />
|
||
</label>
|
||
</div>
|
||
<button className="btn primary"><Icon name="plus" size={16} />Hinzufügen</button>
|
||
</form>
|
||
)}
|
||
|
||
<ul className="simple-list">
|
||
{shops.map((s) => (
|
||
<li key={s.id}>
|
||
<span style={{ display: "flex", alignItems: "center", gap: 8, flex: 1 }}>
|
||
<Icon name="cart" size={15} className="muted" />
|
||
{isAdmin ? (
|
||
<input defaultValue={s.name} style={{ marginTop: 0, minWidth: 140 }}
|
||
onBlur={(e) => {
|
||
const v = e.target.value.trim();
|
||
if (v && v !== s.name) patch(s, { name: v });
|
||
}} />
|
||
) : <span className="strong">{s.name}</span>}
|
||
{s.website && (
|
||
<a href={s.website} target="_blank" rel="noreferrer" className="muted small">
|
||
{s.website.replace(/^https?:\/\//, "")}
|
||
</a>
|
||
)}
|
||
{s.product_count > 0 && <span className="badge nowrap">{s.product_count} Artikel</span>}
|
||
</span>
|
||
{isAdmin && (
|
||
<button className="btn-icon danger" onClick={() => remove(s)} title="Löschen">
|
||
<Icon name="trash" size={16} />
|
||
</button>
|
||
)}
|
||
</li>
|
||
))}
|
||
{shops.length === 0 && <li className="empty">Noch keine Shops.</li>}
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|