Einkauf: Abhaken legt Produkte in den Wagen, "Einkauf beenden" übernimmt sie

- Abgehakte Produkte bleiben auf der Einkaufsliste, rutschen durchgestrichen
  nach unten ("Im Wagen") und lassen sich zurücknehmen
- Button "Einkauf beenden (n)" verschiebt alle abgehakten Produkte auf einmal
  in den Vorrat derselben Liste (Mengen werden zusammengeführt)
- Haken wird gespeichert (Spalte checked, Migration ergänzt sie) und ist für
  alle sichtbar, die die Liste sehen; erneut hinzugefügte Produkte verlieren
  den Haken
- Einträge gleiten per View Transition an ihren neuen Platz (aus bei
  "Animation: aus")

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
guyg1474andClaude Opus 5.5 committed 2026-10-15 12:11:13 +02:00
1 parent b30acb652a
commit f0e17b06b5
6 files changed
+109 -13

No files matched your search

+41 -9
View File
@@ -1,5 +1,7 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { flushSync } from 'react-dom';
import { api, personName } from '../api.js';
import { isCalm } from '../motion.js';
import { useAuth } from '../auth.jsx';
import ProductInput from '../components/ProductInput.jsx';
import RecipeCheck from '../components/RecipeCheck.jsx';
@@ -54,13 +56,21 @@ export default function Haushalt() {
};
}, [load]);
// Jede Änderung liefert den aktuellen Stand aller Listen zurück
async function run(method, url, body) {
// Jede Änderung liefert den aktuellen Stand aller Listen zurück.
// animate: Einträge gleiten an ihren neuen Platz (z. B. abgehakt → nach unten), wo der Browser es kann
async function run(method, url, body, { animate = false } = {}) {
setBusy(true);
setError(null);
try {
const next = await api(method, url, body);
setData(next);
if (animate && document.startViewTransition && !isCalm()) {
const root = document.documentElement;
root.classList.add('vt-items');
const vt = document.startViewTransition(() => flushSync(() => setData(next)));
vt.finished.finally(() => root.classList.remove('vt-items'));
} else {
setData(next);
}
return next;
} catch (e) {
setError(e.message);
@@ -123,6 +133,8 @@ export default function Haushalt() {
add: (list, name, quantity, unit) => run('POST', '/api/haushalt/items', { list, name, quantity, unit, listId: current?.id }),
update: (item, patch) => run('PATCH', `/api/haushalt/items/${item.id}`, patch),
move: (item) => run('POST', `/api/haushalt/items/${item.id}/move`),
check: (item, checked) => run('PATCH', `/api/haushalt/items/${item.id}`, { checked }, { animate: true }),
finish: () => run('POST', `/api/haushalt/listen/${current.id}/einkauf-beenden`, undefined, { animate: true }),
remove: (item) => run('DELETE', `/api/haushalt/items/${item.id}`),
};
@@ -220,6 +232,9 @@ export default function Haushalt() {
function ListPanel({ list, items, products, find, actions, busy, hidden }) {
const { title, lead, empty } = LISTS[list];
const other = list === 'vorrat' ? 'einkauf' : 'vorrat';
// Einkauf: Abgehaktes rutscht nach unten und wandert erst mit "Einkauf beenden" in den Vorrat
const open = items.filter((i) => !i.checked);
const inCart = items.filter((i) => i.checked);
// Zusatzinfo in den Vorschlägen: steht das Produkt schon irgendwo?
function hint(name) {
@@ -249,11 +264,28 @@ function ListPanel({ list, items, products, find, actions, busy, hidden }) {
<p className="hint household-empty">{empty}</p>
) : (
<ul className="household-list">
{items.map((item) => (
{open.map((item) => (
<ItemRow key={item.id} item={item} list={list} other={find(other, item.name)} actions={actions} busy={busy} />
))}
{inCart.length > 0 && (
<li className="household-cart-head" aria-hidden="true">Im Wagen</li>
)}
{inCart.map((item) => (
<ItemRow key={item.id} item={item} list={list} other={find(other, item.name)} actions={actions} busy={busy} />
))}
</ul>
)}
{inCart.length > 0 && (
<div className="household-finish">
<span className="hint">
{inCart.length} {inCart.length === 1 ? 'Produkt' : 'Produkte'} im Wagen
</span>
<button className="btn" type="button" disabled={busy} onClick={actions.finish}>
Einkauf beenden ({inCart.length})
</button>
</div>
)}
</section>
);
}
@@ -354,16 +386,16 @@ function ItemRow({ item, list, other, actions, busy }) {
const label = item.unit === 'stk' ? item.name : `${item.name} (${item.unit})`;
return (
<li className="household-item">
<li className={`household-item${item.checked ? ' checked' : ''}`} style={{ '--vt': `item-${item.id}` }}>
{isShopping && (
<input
type="checkbox"
className="household-check"
checked={false}
checked={item.checked}
disabled={busy}
onChange={() => actions.move(item)}
aria-label={`${label} gekauft – in den Vorrat übernehmen`}
title="Gekauft: in den Vorrat übernehmen"
onChange={() => actions.check(item, !item.checked)}
aria-label={item.checked ? `${label} im Wagen – Haken entfernen` : `${label} abhaken (im Wagen)`}
title={item.checked ? 'Im Wagen – zum Zurücknehmen klicken' : 'Abhaken: kommt mit „Einkauf beenden“ in den Vorrat'}
/>
)}