"use client"; import {useEffect,useState} from "react"; type Flyer = { id:number; title:string; source_url:string; valid_from:string|null; valid_to:string|null; downloaded_at:string; page_count:number; extraction_status:string; store_name:string; address:string|null; offer_count:number; thumbnail_url:string|null; external_flyer_id:number|null; merchant:string|null; postal_code:string|null; }; type Offer = {id:number;item_name:string;brand:string|null;price:number|string;price_text:string|null;image_url:string|null;valid_from:string|null;valid_to:string|null;merchant:string|null;flyer_title:string|null;store_name:string;}; type Ingredient = {name:string;quantity:number;unit:string}; type Suggestion = {id:number;name:string;description:string;servings:number;instructions:string;estimated_cost:number;ingredients:Ingredient[];promotion_matches:{ingredient:string;item_name:string;price:number;price_text:string|null;price_unit:string;calculated_cost:number|null;store_name:string;valid_to:string|null}[];promotion_count:number;cost_is_partial:boolean}; type ShoppingItem = {ingredient_name:string;unit:string;quantity:number;recipe_count:number}; type SourceStatus = { store_name:string; address:string; source_url:string; last_checked_at:string|null; last_success_at:string|null; last_error:string|null; flyer_count:number; offer_count:number; }; export default function Home(){ const [s,setS]=useState({household_size:4,weekly_budget:150,dinners_per_week:7,currency:"CAD"}); const [stores,setStores]=useState([]); const [flyers,setFlyers]=useState([]); const [offers,setOffers]=useState([]); const [sources,setSources]=useState([]); const [msg,setMsg]=useState("Connexion…"); const [importMsg,setImportMsg]=useState(""); const [importing,setImporting]=useState(false); const [suggestions,setSuggestions]=useState([]); const [selectedIds,setSelectedIds]=useState([]); const [generating,setGenerating]=useState(false); const [savingPlan,setSavingPlan]=useState(false); const [planMsg,setPlanMsg]=useState(""); const [shoppingList,setShoppingList]=useState([]); const [savedMeals,setSavedMeals]=useState([]); const [showShopping,setShowShopping]=useState(false); const today=new Date(); const monday=new Date(today); monday.setDate(today.getDate()-((today.getDay()+6)%7)); const weekStart=`${monday.getFullYear()}-${String(monday.getMonth()+1).padStart(2,"0")}-${String(monday.getDate()).padStart(2,"0")}`; const selectedRecipes=suggestions.filter(x=>selectedIds.includes(x.id)); const selectedCost=selectedRecipes.reduce((sum,x)=>sum+(x.estimated_cost===null?0:Number(x.estimated_cost)),0); const selectedCostsComplete=selectedRecipes.every(x=>!x.cost_is_partial&&x.estimated_cost!==null); async function loadFlyers(){ try{ const [f,st,o]=await Promise.all([ fetch("/api/flyers").then(r=>r.ok?r.json():[]), fetch("/api/flyer-import/status").then(r=>r.ok?r.json():{sources:[]}), fetch("/api/offers").then(r=>r.ok?r.json():[]) ]); setFlyers(f); setSources(st.sources||[]); setOffers(o); }catch{} } useEffect(()=>{ Promise.all([fetch("/api/settings").then(r=>r.json()),fetch("/api/stores").then(r=>r.json())]) .then(([a,b])=>{setS(a);setStores(b);setMsg("Application connectée")}) .catch(()=>setMsg("API non disponible")); loadFlyers(); },[]); async function save(){ const r=await fetch("/api/settings",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)}); setMsg(r.ok?"Paramètres enregistrés":"Erreur d'enregistrement"); } async function generateSuggestions(){ setGenerating(true); setPlanMsg("Analyse des promotions et préparation des idées de soupers…"); try{ const r=await fetch("/api/meal-suggestions"); if(!r.ok) throw new Error("Impossible de générer les suggestions"); const data=await r.json(); setSuggestions(data.suggestions||[]); setSelectedIds((data.suggestions||[]).slice(0,Number(s.dinners_per_week)||7).map((x:Suggestion)=>x.id)); setPlanMsg(`${(data.suggestions||[]).length} idées analysées selon les promotions disponibles.`); }catch(e:any){setPlanMsg(e.message||"Erreur lors de la génération");} finally{setGenerating(false);} } async function saveMealPlan(){ if(!selectedIds.length){setPlanMsg("Sélectionne au moins un souper.");return;} setSavingPlan(true);setPlanMsg("Enregistrement du menu…"); try{ const r=await fetch("/api/meal-plan",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({week_start:weekStart,recipe_ids:selectedIds})}); const data=await r.json(); if(!r.ok) throw new Error(data.detail||"Impossible d'enregistrer le menu"); const [meals,list]=await Promise.all([ fetch(`/api/meal-plan?week_start=${weekStart}`).then(x=>x.json()), fetch(`/api/shopping-list?week_start=${weekStart}`).then(x=>x.json()) ]); setSavedMeals(meals);setShoppingList(list);setShowShopping(true); setPlanMsg(`Menu enregistré : ${data.meals_saved} soupers planifiés.`); }catch(e:any){setPlanMsg(e.message||"Erreur d'enregistrement");} finally{setSavingPlan(false);} } function toggleRecipe(id:number){ setSelectedIds(current=>current.includes(id)?current.filter(x=>x!==id):current.length>=Number(s.dinners_per_week)?current:[...current,id]); } async function checkFlyers(){ setImporting(true); setImportMsg("Vérification des circulaires en cours…"); try{ const r=await fetch("/api/flyer-import/run",{method:"POST"}); const data=await r.json(); if(!r.ok) throw new Error(data.detail||"Impossible de lancer l'importation"); setImportMsg("Vérification lancée. Actualise la page dans quelques instants pour voir le résultat."); window.setTimeout(loadFlyers,15000); }catch(e:any){setImportMsg(e.message||"Erreur");} finally{setImporting(false);} } const dateLabel=(v:string|null)=>v?new Date(v).toLocaleString("fr-CA"):"—"; return
TON ASSISTANT CULINAIRE

Bonjour ! 👋

Préparons une semaine de soupers savoureux sans dépasser ton budget.

CETTE SEMAINE

Les bons repas
commencent ici.

Des menus personnalisés selon les rabais de tes épiceries préférées.

Configurer mon budget →
🥗
Budget hebdomadaire

{Number(s.weekly_budget).toFixed(2)} $

Soupers à planifier

{s.dinners_per_week}

Personnes

{s.household_size}

TES COMMERCES

Épiceries suivies

{stores.map(x=>
✓ {x.name}{x.address&&

{x.address}

}
)}
RABAIS DE TES MAGASINS

Circulaires IGA et Metro

Importation automatique quotidienne à 6 h (heure de Montréal) des circulaires régionales Flipp pour J7R 6S4. Les articles avec nom et prix sont enregistrés dans ta base locale.

{importMsg&&

{importMsg}

}
{sources.map(x=>
{x.store_name}

{x.address}

Dernière vérification : {dateLabel(x.last_checked_at)} Circulaires actives : {x.flyer_count}Promotions disponibles : {x.offer_count} {x.last_error&&

{x.last_error}

}
)}
{flyers.map(f=>
{f.thumbnail_url&&} {f.store_name}

{f.title}

{f.offer_count} promotions · Valide du {f.valid_from||"—"} au {f.valid_to||"—"}

Circulaire régionale · Code postal {f.postal_code||"J7R6S4"} Importée : {dateLabel(f.downloaded_at)} Source Flipp ↗
)} {flyers.length===0&&
Aucune circulaire régionale importée pour le moment. Utilise le bouton pour lancer une vérification.
}

Promotions en cours

Prix des circulaires régionales; ils peuvent varier selon la succursale et la disponibilité.

{offers.slice(0,60).map(o=>
{o.image_url?:
🛒
}
{o.store_name}{o.item_name} {o.brand&&{o.brand}}{o.price_text||`${Number(o.price).toFixed(2)} $`} Jusqu'au {o.valid_to||"—"}
)}
{offers.length===0&&
Les promotions apparaîtront ici après la première importation Flipp.
}
PERSONNALISATION

Ton budget et ton foyer

}