import React, { useEffect, useMemo, useState, useCallback, startTransition, useDeferredValue } from "react"; import { getCurrentUser, getProducts, createBooking, getFavorites, updateFavorites, logout, API_BASE, getCategories } from "../api"; import ProductCard from "./ProductCard"; import CartItem from "./CartItem"; import { useLocation, useNavigate } from "react-router-dom"; const DEFAULT_BG1 = "#0a6bff"; const DEFAULT_BG2 = "#00e0aa"; function applyOrderColorsFromStorage() { const c1 = localStorage.getItem("order_bg1") || DEFAULT_BG1; const c2 = localStorage.getItem("order_bg2") || DEFAULT_BG2; document.documentElement.style.setProperty("--order-bg1", c1); document.documentElement.style.setProperty("--order-bg2", c2); } // --- NEU: lokale Bestände anhand des Warenkorbs reduzieren function decrementStockLocal(prevProducts, cart) { const delta = {}; for (const { product, quantity } of Object.values(cart)) { delta[product.id] = (delta[product.id] || 0) + quantity; } return prevProducts.map((p) => delta[p.id] ? { ...p, stock: (Number(p.stock) || 0) - delta[p.id] } : p ); } const PAGE_SIZE = 20; // deterministische Farbe je Kategorie function hueFromString(s) { let h = 0; for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) | 0; return (h >>> 0) % 360; } // Pastell (inaktiv) vs. kräftig + Glow (aktiv) function chipStyleFor(cat, active) { const strong = (h) => ({ bg: `hsl(${h} 85% 72%)`, br: `hsla(${h} 65% 35% / .60)`, glow: `0 0 0 5px hsla(${h} 95% 55% / .40), 0 0 22px hsla(${h} 95% 55% / .50)`, fg: "#0b1220", }); const soft = (h) => ({ bg: `hsl(${h} 65% 90%)`, br: `hsla(${h} 40% 40% / .35)`, glow: "none", fg: "#0b1220", }); if (cat === "Alle") return active ? { bg:"hsl(210 75% 74%)", br:"hsla(210 65% 35%/.6)", glow:"0 0 0 5px hsla(210 95% 55%/.4),0 0 22px hsla(210 95% 55%/.5)", fg:"#0b1220" } : { bg:"hsl(210 55% 90%)", br:"hsla(210 40% 40%/.35)", glow:"none", fg:"#0b1220" }; if (cat === "Favoriten") return active ? { bg:"hsl(52 90% 72%)", br:"hsla(52 70% 35%/.6)", glow:"0 0 0 5px hsla(52 98% 50%/.4),0 0 22px hsla(52 98% 50%/.5)", fg:"#0b1220" } : { bg:"hsl(52 80% 90%)", br:"hsla(52 55% 40%/.35)", glow:"none", fg:"#0b1220" }; const h = hueFromString(cat); return active ? strong(h) : soft(h); } function ringColorForCategory(cat) { if (!cat) return "#22c55e"; // nutzt deine bestehende Farblogik der Chips: return chipStyleFor(cat, true).br; // starke Border-Farbe der Kategorie } // robustes Aktiv-Flag (API kann bool/zahl/string liefern) function isActiveProduct(p) { const v = p?.is_active; return v === true || v === 1 || v === "true" || v === "1"; } const MemoProductCard = React.memo(ProductCard); export default function Order({ user: userProp }) { const navigate = useNavigate(); const location = useLocation(); const stateUser = location.state?.user || null; const [user, setUser] = useState(userProp || stateUser || null); const [products, setProducts] = useState([]); const [categories, setCategories] = useState([]); const [selectedCategory, setSelectedCategory] = useState("Alle"); const deferredCategory = useDeferredValue(selectedCategory); // INP const [page, setPage] = useState(1); const [cart, setCart] = useState({}); const [favorites, setFavorites] = useState(new Set()); const [error, setError] = useState(""); const [loading, setLoading] = useState(true); const collator = useMemo(() => new Intl.Collator("de", { sensitivity: "base" }), []); useEffect(() => { let cancelled = false; // Farben sofort anwenden + Listener registrieren applyOrderColorsFromStorage(); const onChange = (e) => { const { bg1, bg2 } = e.detail || {}; if (bg1) document.documentElement.style.setProperty("--order-bg1", bg1); if (bg2) document.documentElement.style.setProperty("--order-bg2", bg2); }; window.addEventListener("order-bg-change", onChange); // Daten laden (async () => { setLoading(true); try { const [u, prod, cats] = await Promise.all([ userProp || stateUser ? (userProp || stateUser) : getCurrentUser(), getProducts(), typeof getCategories === "function" ? getCategories() : Promise.resolve(null), ]); if (cancelled) return; setUser(u || null); setProducts(Array.isArray(prod) ? prod : []); setCategories( cats && Array.isArray(cats) ? cats : Array.from(new Set((prod || []).map((p) => p.category).filter(Boolean))) ); if (u?.id) { try { const f = await getFavorites(u.id); if (!cancelled && Array.isArray(f)) setFavorites(new Set(f)); } catch {} } } catch (e) { if (!cancelled) setError(e?.message || "Initiale Daten konnten nicht geladen werden."); } finally { if (!cancelled) setLoading(false); } })(); // Cleanup des Effects return () => { cancelled = true; window.removeEventListener("order-bg-change", onChange); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [userProp, stateUser]); const activeProducts = useMemo( () => (products || []).filter(isActiveProduct), [products] ); const sortedProducts = useMemo(() => { const arr = (activeProducts || []).slice(); arr.sort((a, b) => collator.compare(a?.name || "", b?.name || "")); return arr; }, [activeProducts, collator]); const filtered = useMemo(() => { if (deferredCategory === "Favoriten") { return sortedProducts.filter((p) => favorites.has(p.id)); } if (deferredCategory === "Alle") return sortedProducts; return sortedProducts.filter((p) => p.category === deferredCategory); }, [sortedProducts, deferredCategory, favorites]); const onSelectCategory = useCallback((cat) => { startTransition(() => { setSelectedCategory(cat); setPage(1); }); }, []); const DEFAULT_AVATAR = "/avatar-default.png"; const avatarSrc = useMemo(() => { const raw = user?.avatar_url?.trim(); if (!raw) return DEFAULT_AVATAR; if (/^(https?:|data:|blob:|\/\/)/i.test(raw)) return raw; if (raw === DEFAULT_AVATAR) return raw; return `${API_BASE}${raw.startsWith("/") ? raw : `/${raw}`}`; }, [user?.avatar_url, API_BASE]); const total = filtered.length; const pageCount = Math.max(1, Math.ceil(total / PAGE_SIZE)); const currentPage = Math.min(page, pageCount); const startIdx = (currentPage - 1) * PAGE_SIZE; const endIdx = Math.min(startIdx + PAGE_SIZE, total); const pageItems = useMemo( () => filtered.slice(startIdx, endIdx), [filtered, startIdx, endIdx] ); const handleSelect = useCallback((product) => { startTransition(() => { setCart((prev) => { const qty = prev[product.id]?.quantity || 0; return { ...prev, [product.id]: { product, quantity: qty + 1 } }; }); }); }, []); const handleIncrement = useCallback((id) => startTransition(() => { setCart((prev) => ({ ...prev, [id]: { ...prev[id], quantity: prev[id].quantity + 1 } })); }), []); const handleDecrement = useCallback((id) => startTransition(() => { setCart((prev) => { const item = prev[id]; if (!item) return prev; if (item.quantity <= 1) { const { [id]: _, ...rest } = prev; return rest; } return { ...prev, [id]: { ...item, quantity: item.quantity - 1 } }; }); }), []); const handleToggleFavorite = useCallback(async (id) => { const before = new Set(favorites); const next = new Set(favorites); next.has(id) ? next.delete(id) : next.add(id); setFavorites(next); try { if (!user?.id) throw new Error("Kein Nutzer geladen."); await updateFavorites(user.id, [...next]); } catch (e) { setFavorites(before); setError(e?.message || "Favoriten konnten nicht gespeichert werden."); } }, [favorites, user?.id]); const cartItems = useMemo(() => Object.values(cart), [cart]); const cartTotalCents = useMemo( () => cartItems.reduce((s, { product, quantity }) => s + product.price_cents * quantity, 0), [cartItems] ); const handlePay = useCallback(async () => { const balance = user?.balance_cents ?? 0; if (balance < cartTotalCents) { setError("Zu wenig Geld."); return; } try { for (const { product, quantity } of cartItems) { await createBooking( user.id, product.id, quantity, product.price_cents * quantity ); } setProducts((prev) => decrementStockLocal(prev, cart)); try { const refreshed = await getCurrentUser(); if (refreshed) setUser(refreshed); } catch {} setCart({}); try { await logout(); } finally { navigate("/", { replace: true }); } } catch (e) { setError(e?.message || "Bezahlen fehlgeschlagen."); } }, [cartItems, cartTotalCents, navigate, user?.balance_cents, user?.id]); const handleLogout = useCallback(async () => { try { await logout(); } finally { navigate("/", { replace: true }); } }, [navigate]); const effectiveBalanceCents = (user?.balance_cents ?? 0) - cartTotalCents; const balance = (effectiveBalanceCents / 100).toFixed(2).replace(".", ","); const balanceColor = effectiveBalanceCents <= 0 ? "text-red-400" : "text-green-400"; // Kategorien-UI aus aktiven Produkten ableiten (ohne useMemo) const catsBase = (categories && categories.length) ? categories : Array.from(new Set((activeProducts || []).map(p => p.category).filter(Boolean))); const catsForUI = ["Alle", ...catsBase, "Favoriten"]; if (loading) { return