Files
Vorrania/web/src/pages/Shops.jsx
Scarriffle 5b952524d7 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>
2026-07-25 15:33:56 +02:00

132 lines
4.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}