import { useCallback, useEffect, useRef, useState } from 'react'; import { api, personName } from '../api.js'; import ProductInput from '../components/ProductInput.jsx'; import RecipeCheck from '../components/RecipeCheck.jsx'; import UnitSelect from '../components/UnitSelect.jsx'; import { fmtItems, parseQty, round2, STEP } from '../units.js'; // Geteilte Liste: regelmäßig nachladen, damit Änderungen der anderen ankommen const REFRESH_SECONDS = 15; const TAB_KEY = 'p42.haushaltTab'; const LISTS = { vorrat: { title: 'Vorrat', lead: 'Was zu Hause ist', empty: 'Noch nichts im Vorrat eingetragen.' }, einkauf: { title: 'Einkaufen', lead: 'Was besorgt werden muss', empty: 'Nichts zu besorgen.' }, }; function loadTab() { try { return localStorage.getItem(TAB_KEY) === 'einkauf' ? 'einkauf' : 'vorrat'; } catch { return 'vorrat'; } } export default function Haushalt() { const [data, setData] = useState(null); const [error, setError] = useState(null); const [busy, setBusy] = useState(false); const [tab, setTab] = useState(loadTab); const dialog = useRef(null); const load = useCallback(async () => { try { setData(await api('GET', '/api/haushalt')); } catch (e) { setError(e.message); } }, []); useEffect(() => { load(); const timer = setInterval(() => !document.hidden && load(), REFRESH_SECONDS * 1000); const onVisible = () => !document.hidden && load(); document.addEventListener('visibilitychange', onVisible); return () => { clearInterval(timer); document.removeEventListener('visibilitychange', onVisible); }; }, [load]); // Jede Änderung liefert den aktuellen Stand beider Listen zurück async function run(method, url, body) { setBusy(true); setError(null); try { setData(await api(method, url, body)); return true; } catch (e) { setError(e.message); // Bei Konflikten (z. B. gerade von jemand anderem gelöscht) frisch laden load(); return false; } finally { setBusy(false); } } function chooseTab(next) { setTab(next); try { localStorage.setItem(TAB_KEY, next); } catch { /* ignorieren */ } } const items = data?.items || []; const byList = { vorrat: items.filter((i) => i.list === 'vorrat'), einkauf: items.filter((i) => i.list === 'einkauf'), }; // Alle Zeilen eines Produkts (ein Produkt kann in mehreren Einheiten auf einer Liste stehen) const find = (list, name) => byList[list].filter((i) => i.name.toLocaleLowerCase('de-DE') === name.toLocaleLowerCase('de-DE')); const actions = { add: (list, name, quantity, unit) => run('POST', '/api/haushalt/items', { list, name, quantity, unit }), update: (item, patch) => run('PATCH', `/api/haushalt/items/${item.id}`, patch), move: (item) => run('POST', `/api/haushalt/items/${item.id}/move`), remove: (item) => run('DELETE', `/api/haushalt/items/${item.id}`), }; return ( <>

Vorrat & Einkauf

Gemeinsame Liste für alle Konten. Wird alle {REFRESH_SECONDS} Sekunden aktualisiert.

{data?.products.length > 0 && ( )}
{error &&

{error}

} {!data && !error &&

Lade …

} {data && ( <> run('POST', '/api/haushalt/rezept/uebernehmen', body)} />
{Object.entries(LISTS).map(([key, { title }]) => ( ))}
{Object.keys(LISTS).map((list) => (
)} run('DELETE', `/api/haushalt/produkte?name=${encodeURIComponent(name)}`)} /> ); } function ListPanel({ list, items, products, find, actions, busy, hidden }) { const { title, lead, empty } = LISTS[list]; const other = list === 'vorrat' ? 'einkauf' : 'vorrat'; // Zusatzinfo in den Vorschlägen: steht das Produkt schon irgendwo? function hint(name) { const here = find(list, name); if (here.length) return `schon da: ${fmtItems(here)}`; const there = find(other, name); if (there.length) return `${list === 'vorrat' ? 'auf Einkaufsliste' : 'im Vorrat'}: ${fmtItems(there)}`; return null; } return (

{title}

{lead}
{items.length === 0 ? (

{empty}

) : (
    {items.map((item) => ( ))}
)}
); } function AddForm({ list, products, hint, onAdd, busy }) { const [name, setName] = useState(''); const [quantity, setQuantity] = useState('1'); const [unit, setUnit] = useState('stk'); const [invalid, setInvalid] = useState(false); const nameInput = useRef(null); async function submit(e) { e.preventDefault(); const n = name.trim(); const q = parseQty(quantity, unit); setInvalid(q == null); if (!n || q == null) return; if (await onAdd(list, n, q, unit)) { setName(''); setQuantity('1'); nameInput.current?.focus(); } } return (
{ setQuantity(e.target.value); setInvalid(false); }} aria-invalid={invalid || undefined} aria-describedby={invalid ? `qty-error-${list}` : undefined} />
{invalid && (

{unit === 'stk' ? 'Anzahl als ganze Zahl von 1 bis 999 eingeben.' : 'Menge von 0,01 bis 999 eingeben, z. B. 1,5.'}

)} ); } // Menge direkt eintippen; gespeichert wird beim Verlassen des Felds oder mit Enter function QuantityInput({ item, label, onCommit, disabled }) { const shown = String(item.quantity).replace('.', ','); const [draft, setDraft] = useState(null); function commit() { if (draft == null) return; const q = parseQty(draft, item.unit); setDraft(null); if (q != null && q !== item.quantity) onCommit(q); } return ( setDraft(e.target.value)} onBlur={commit} onKeyDown={(e) => { if (e.key === 'Enter') e.currentTarget.blur(); if (e.key === 'Escape') setDraft(null); }} aria-label={`Menge ${label}`} /> ); } function ItemRow({ item, list, other, actions, busy }) { const isShopping = list === 'einkauf'; const step = STEP[item.unit]; const last = item.quantity - step <= 0; // Bei gleichem Produkt in mehreren Einheiten die Einheit mit ansagen const label = item.unit === 'stk' ? item.name : `${item.name} (${item.unit})`; return (
  • {isShopping && ( actions.move(item)} aria-label={`${label} gekauft – in den Vorrat übernehmen`} title="Gekauft: in den Vorrat übernehmen" /> )}
    {item.name} {other.length > 0 && `${isShopping ? 'im Vorrat' : 'auf Einkaufsliste'}: ${fmtItems(other)} · `} ✎ {personName(item.updatedBy)}
    actions.update(item, { quantity })} />
    actions.update(item, { unit })} disabled={busy} compact label={`Einheit ${item.name}`} />
    {!isShopping && ( )}
  • ); } function ProductsDialog({ ref, products, onDelete }) { const sorted = [...products].sort((a, b) => a.localeCompare(b, 'de-DE')); return (

    Vorschläge verwalten

    Tippfehler oder doppelte Schreibweisen hier löschen. Einträge auf den Listen bleiben erhalten.

      {sorted.map((name) => (
    • {name}
    • ))}
    ); }