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