πŸ›’ Despensa

Acceso privado. Ingresa tu contraseΓ±a para continuar.

async function handleStandaloneTicket(fileOrArr){ const file = Array.isArray(fileOrArr) ? fileOrArr[0] : fileOrArr; if(!file) return; // ── Validate image before sending ── const validation = validateImage(file); if(!validation.ok){ toast_(validation.msg,"error",5000); return; } setScanningTicket(true); const isAdding = !!ticketScan; toast_(isAdding?"Agregando mΓ‘s productos del ticket…":"Leyendo ticket…","info",25000); try{ const b64 = await toB64(file); const text = await claude({ system:`Eres asistente de despensa en MΓ©xico. Analiza esta foto de ticket de compra (puede ser una secciΓ³n de un ticket largo). Extrae TODOS los productos visibles con nombre, precio unitario y cantidad. Si ves el total final, nombre de tienda o fecha inclΓΊyelos; si no, usa null. Responde ÚNICAMENTE con JSON sin backticks: {"store":"nombre o null","date":"dd/mm/yyyy o null","total":000.00 o null,"items":[{"name":"nombre","price":"00.00","qty":1,"category":"CategorΓ­a"}]} CategorΓ­as: Abarrotes, Carnes, LΓ‘cteos, PanaderΓ­a, Hogar, Limpieza, Bebidas, Frutas y Verduras, Otro. Si la imagen es ilegible, borrosa o no muestra un ticket, devuelve: {"error":"ilegible","items":[]} Si no hay productos visibles devuelve: {"items":[]}`, text:"Extrae productos, precios y datos del ticket de esta foto.", b64, mime:file.type||"image/jpeg", maxTokens:3000, }); setScanningTicket(false); const info = extractJSON(text,"object"); // ── Check if Claude flagged image as unreadable ── if(info?.error === "ilegible"){ toast_("⚠️ La foto no es legible. Consejos: β€’ MΓ‘s luz natural o artificial β€’ Foto de frente, sin Γ‘ngulo β€’ Acerca la cΓ‘mara al texto β€’ Evita sombras sobre el ticket","error",7000); return; } // ── Check parse quality ── const quality = checkParseQuality(info,"ticket"); if(!quality.ok){ toast_(quality.msg,"error",6000); return; } if(quality.warn){ toast_(quality.msg,"warn",5000); // Still add whatever we got } const newItems = info.items || []; const prevItems = ticketScan?.compareItems || ticketScan?.items || []; const prevNames = new Set(prevItems.map(i=>i.name?.toLowerCase())); const fresh = newItems.filter(i=>!prevNames.has(i.name?.toLowerCase())); const merged = [...prevItems, ...fresh]; const session = { store: ticketScan?.store || info.store || null, date: ticketScan?.date || info.date || null, total: info.total || ticketScan?.total || null, id: ticketScan?.id || uid(), sessionDate: ticketScan?.sessionDate || Date.now(), compareItems: merged, partial: true, }; setTicketScan(session); if(fresh.length > 0){ toast_(`+${fresh.length} producto${fresh.length!==1?"s":""} agregado${fresh.length!==1?"s":""}. Total: ${merged.length}`,"success",3500); } else { toast_("Sin productos nuevos en esta secciΓ³n β€” puede ser duplicado o sin texto legible","warn",4000); } }catch(err){ console.error("handleStandaloneTicket:",err); setScanningTicket(false); const msg = err.message?.includes("API") ? "Error de API key. Verifica tu key en Insights β†’ ConfiguraciΓ³n." : err.message?.includes("fetch") ? "Sin conexiΓ³n a internet." : `Error: ${err.message||"intenta de nuevo"}`; toast_(msg,"error",6000); } } async function closeTicketSession(){ if(!ticketScan) return; const items = ticketScan.compareItems || []; if(!items.length){ setTicketScan(null); return; } setComparingTicket(true); toast_(`Buscando precios en 6 tiendas para ${items.length} productos…`,"info",60000); const enriched = await Promise.all(items.map(async it => { if(it.prices) return it; // already enriched const prices = await fetchPrices(it.name, null); return {...it, prices}; })); const finalData = {...ticketScan, compareItems: enriched, partial: false}; setTicketScan(finalData); const histEntry = { id: ticketScan.id||uid(), date: ticketScan.sessionDate||Date.now(), items: enriched.map(i=>({...i,id:uid()})), realTotal: ticketScan.total||null, estimatedTotal: ticketScan.total||null, store: ticketScan.store||null, ticketDate: ticketScan.date||null, status:"complete", completedAt:Date.now(), nearbyStores, }; setHistory(p=>[histEntry,...p]); setComparingTicket(false); toast_("βœ“ Ticket cerrado y comparativa lista","success"); }