admin: surface silent failures, guard against double-submit on add

- Drinks.add()/patch() now check res.ok and alert() the error text,
  matching the pattern every other mutator in this file already
  used (Drinks.del(), everything in Bars) — previously a rejected
  name/price change just re-fetched the (unchanged) list with no
  indication anything went wrong.
- Stats.reload(), Drinks.reload(), Bars.reload() now .catch() a
  failed fetch/non-ok response instead of leaving an unhandled
  promise rejection — a network hiccup used to leave the section
  stuck on 'Lade…' forever with no visible error.
- Drinks 'Hinzufügen' and Bars 'Bar hinzufügen' are now disabled
  while their POST is in flight, mirroring the submitting guard
  Sale.tsx already uses — a fast double-tap could otherwise fire two
  requests before the first reload() resolved.
This commit is contained in:
iris 2026-07-29 20:28:34 +02:00 committed by mara
commit 59f6041a16

View file

@ -93,7 +93,9 @@ function fmtDay(day: string): string {
function Stats() {
const [data, setData] = useState<{ totals: Totals[]; per_drink: PerDrink[]; by_day: ByDay[] } | null>(null);
function reload() { fetch('/admin/api/stats').then(j).then(setData); }
function reload() {
fetch('/admin/api/stats').then(j).then(setData).catch(e => alert(`Fehler beim Laden: ${e}`));
}
useEffect(reload, []);
async function reset() {
@ -171,27 +173,37 @@ function Drinks() {
const [list, setList] = useState<Drink[]>([]);
const [name, setName] = useState('');
const [price, setPrice] = useState('');
const [adding, setAdding] = useState(false);
function reload() { fetch('/admin/api/drinks').then(j).then(setList); }
function reload() {
fetch('/admin/api/drinks').then(j).then(setList).catch(e => alert(`Fehler beim Laden: ${e}`));
}
useEffect(reload, []);
async function add() {
const cents = Math.round(parseFloat(price) * 100);
if (!name || !Number.isFinite(cents)) return;
await fetch('/admin/api/drinks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, price_cents: cents }),
});
setName(''); setPrice(''); reload();
if (!name || !Number.isFinite(cents) || adding) return;
setAdding(true);
try {
const res = await fetch('/admin/api/drinks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, price_cents: cents }),
});
if (!res.ok) { alert(`Fehler: ${await errText(res)}`); return; }
setName(''); setPrice(''); reload();
} finally {
setAdding(false);
}
}
async function patch(id: number, body: Partial<Drink>) {
await fetch(`/admin/api/drinks/${id}`, {
const res = await fetch(`/admin/api/drinks/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!res.ok) { alert(`Fehler: ${await errText(res)}`); return; }
reload();
}
@ -237,7 +249,7 @@ function Drinks() {
<div class="row">
<input placeholder="Name" value={name} onInput={(e: any) => setName(e.currentTarget.value)} />
<input placeholder="Preis €" type="number" step="0.01" value={price} onInput={(e: any) => setPrice(e.currentTarget.value)} />
<button onClick={add}>Hinzufügen</button>
<button onClick={add} disabled={adding}>Hinzufügen</button>
</div>
</>
);
@ -333,10 +345,11 @@ function Bars() {
const [bars, setBars] = useState<BarRow[]>([]);
const [drinks, setDrinks] = useState<Drink[]>([]);
const [newName, setNewName] = useState('');
const [addingBar, setAddingBar] = useState(false);
function reload() {
fetch('/admin/api/bars').then(j).then(setBars);
fetch('/admin/api/drinks').then(j).then(setDrinks);
fetch('/admin/api/bars').then(j).then(setBars).catch(e => alert(`Fehler beim Laden: ${e}`));
fetch('/admin/api/drinks').then(j).then(setDrinks).catch(e => alert(`Fehler beim Laden: ${e}`));
}
useEffect(reload, []);
@ -359,18 +372,23 @@ function Bars() {
async function addBar() {
const name = newName.trim();
if (!name) return;
const res = await fetch('/admin/api/bars', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, pfand_cents: 200 }),
});
if (!res.ok) {
alert(`Fehler: ${await errText(res)}`);
return;
if (!name || addingBar) return;
setAddingBar(true);
try {
const res = await fetch('/admin/api/bars', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, pfand_cents: 200 }),
});
if (!res.ok) {
alert(`Fehler: ${await errText(res)}`);
return;
}
setNewName('');
reload();
} finally {
setAddingBar(false);
}
setNewName('');
reload();
}
return (
@ -412,7 +430,7 @@ function Bars() {
value={newName}
onInput={(e: any) => setNewName(e.currentTarget.value)}
/>
<button onClick={addBar}>Bar hinzufügen</button>
<button onClick={addBar} disabled={addingBar}>Bar hinzufügen</button>
</div>
</>
);