'use client'; import { useState, useEffect, useCallback } from 'react'; import Link from 'next/link'; import type { Recipe } from '../../features/inventory/types'; const DAYS_SV = ['Måndag', 'Tisdag', 'Onsdag', 'Torsdag', 'Fredag', 'Lördag', 'Söndag']; type MealPlanEntry = { id: number; date: string; servings: number | null; recipe: Pick & { servings: number | null; ingredients: { quantity: string; unit: string; note: string | null; product: { id: number; name: string; canonicalName: string | null } }[]; }; }; type ShoppingItem = { productId: number; name: string; quantity: number; unit: string }; type InventoryCompareItem = { productId: number; name: string; required: number; unit: string; available: number; missing: number; status: 'enough' | 'missing'; }; function getWeekDates(offset = 0): string[] { const now = new Date(); const day = now.getDay(); const monday = new Date(now); monday.setDate(now.getDate() - (day === 0 ? 6 : day - 1) + offset * 7); return Array.from({ length: 7 }, (_, i) => { const d = new Date(monday); d.setDate(monday.getDate() + i); return d.toISOString().slice(0, 10); }); } export default function MealPlanClient({ recipes }: { recipes: Recipe[] }) { const [weekOffset, setWeekOffset] = useState(0); const [entries, setEntries] = useState([]); const [shopping, setShopping] = useState([]); const [inventoryCompare, setInventoryCompare] = useState([]); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(null); // date being saved const weekDates = getWeekDates(weekOffset); const from = weekDates[0]; const to = weekDates[6]; const weekLabel = (() => { const f = new Date(from); const t = new Date(to); return `${f.toLocaleDateString('sv-SE', { day: 'numeric', month: 'short' })} – ${t.toLocaleDateString('sv-SE', { day: 'numeric', month: 'short', year: 'numeric' })}`; })(); const load = useCallback(async () => { setLoading(true); try { const [entriesRes, shoppingRes, compareRes] = await Promise.all([ fetch(`/api/meal-plan-proxy?from=${from}&to=${to}`), fetch(`/api/meal-plan-proxy/shopping?from=${from}&to=${to}`), fetch(`/api/meal-plan-proxy/inventory-compare?from=${from}&to=${to}`), ]); const entriesData = await entriesRes.json(); setEntries(Array.isArray(entriesData) ? entriesData : []); if (shoppingRes.ok) setShopping(await shoppingRes.json()); else setShopping([]); if (compareRes.ok) setInventoryCompare(await compareRes.json()); else setInventoryCompare([]); } catch { setEntries([]); setShopping([]); setInventoryCompare([]); } finally { setLoading(false); } }, [from, to]); useEffect(() => { load(); }, [load]); const entryForDate = (date: string) => entries.find((e) => e.date.slice(0, 10) === date); const handleSelect = async (date: string, recipeId: string) => { setSaving(date); try { if (!recipeId) { await fetch(`/api/meal-plan-proxy?date=${date}`, { method: 'DELETE' }); } else { const existing = entryForDate(date); await fetch('/api/meal-plan-proxy', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ date, recipeId: Number(recipeId), servings: existing?.servings ?? null }), }); } await load(); } finally { setSaving(null); } }; const plannedCount = weekDates.filter((d) => entryForDate(d)).length; const handleServingsChange = async (date: string, servings: number | null) => { const entry = entryForDate(date); if (!entry) return; await fetch('/api/meal-plan-proxy', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ date, recipeId: entry.recipe.id, servings }), }); await load(); }; return (
{/* Veckonavigering */}
{weekLabel} {weekOffset !== 0 && ( )}
{loading ? (

Laddar...

) : ( <> {/* Veckovy */}
{weekDates.map((date, i) => { const entry = entryForDate(date); const isSaving = saving === date; const isToday = date === new Date().toISOString().slice(0, 10); return (
{DAYS_SV[i]}
{new Date(date).toLocaleDateString('sv-SE', { day: 'numeric', month: 'short' })}
{entry && ( Visa recept → )} {entry && entry.recipe.servings && (
Port.: handleServingsChange(date, e.target.value ? Number(e.target.value) : null)} style={{ width: '52px', padding: '0.25rem 0.4rem', border: '1px solid #ced4da', borderRadius: '4px', fontSize: '0.82rem' }} /> {entry.servings && entry.servings !== entry.recipe.servings && ( )}
)} {isSaving && Sparar...}
); })}
{/* Samlad ingredienslista */}

Inköpslista ({plannedCount} {plannedCount === 1 ? 'recept' : 'recept'} planerade)

{plannedCount === 0 ? (

Välj recept ovan för att se en samlad ingredienslista.

) : shopping.length === 0 ? (

Laddar ingredienser...

) : (
    {shopping.map((item) => (
  • {item.quantity % 1 === 0 ? item.quantity : item.quantity.toFixed(1)} {item.unit} {item.name}
  • ))}
)}
{/* Inventariejämförelse */} {plannedCount > 0 && inventoryCompare.length > 0 && (

Inventariegranskning

Vad du har hemma vs. vad veckans recept kräver.

{(() => { const missingCount = inventoryCompare.filter((i) => i.status === 'missing').length; return missingCount === 0 ? (

✓ Du har allt hemma!

) : (

{missingCount} ingrediens{missingCount !== 1 ? 'er' : ''} saknas eller räcker inte

); })()}
    {inventoryCompare.map((item) => (
  • {item.name} {' '} {item.required % 1 === 0 ? item.required : item.required.toFixed(1)} {item.unit} behövs {' · '} {item.available % 1 === 0 ? item.available : item.available.toFixed(1)} {item.unit} hemma {item.status === 'missing' && item.missing > 0 && ( Saknar {item.missing % 1 === 0 ? item.missing : item.missing.toFixed(1)} {item.unit} )} {item.status === 'enough' && ( )}
  • ))}
)} )}
); } function btnStyle(bg?: string): React.CSSProperties { return { padding: '0.45rem 0.9rem', background: bg || '#f0f0f0', color: bg ? '#fff' : '#333', border: '1px solid ' + (bg || '#ccc'), borderRadius: '6px', cursor: 'pointer', fontSize: '0.9rem', fontWeight: 500, }; }