Gegenstands-Verwaltung (Non-Food) neben Lebensmitteln

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>
This commit is contained in:
Scarriffle
2026-07-25 15:33:56 +02:00
parent 580afcc133
commit 5b952524d7
29 changed files with 3116 additions and 115 deletions

131
web/src/pages/Shops.jsx Normal file
View File

@@ -0,0 +1,131 @@
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>
);
}