/** * Dispensa Manager - Main Application JS * Complete pantry management with barcode scanning and AI identification */ // ===== CONFIGURATION ===== const API_BASE = 'api/index.php'; const LOCATIONS = { 'dispensa': { icon: 'πŸ—„οΈ', label: 'Dispensa' }, 'frigo': { icon: '🧊', label: 'Frigo' }, 'freezer': { icon: '❄️', label: 'Freezer' }, 'altro': { icon: 'πŸ“¦', label: 'Altro' }, }; const CATEGORY_ICONS = { 'latticini': 'πŸ₯›', 'carne': 'πŸ₯©', 'pesce': '🐟', 'frutta': '🍎', 'verdura': 'πŸ₯¬', 'pasta': '🍝', 'pane': '🍞', 'surgelati': '🧊', 'bevande': 'πŸ₯€', 'condimenti': 'πŸ§‚', 'snack': 'πŸͺ', 'conserve': 'πŸ₯«', 'cereali': '🌾', 'igiene': '🧴', 'pulizia': '🧹', 'altro': 'πŸ“¦' }; // Auto-detect location based on category and product name const CATEGORY_LOCATION = { 'latticini': 'frigo', 'carne': 'frigo', 'pesce': 'frigo', 'frutta': 'frigo', 'verdura': 'frigo', 'surgelati': 'freezer', 'pasta': 'dispensa', 'pane': 'dispensa', 'bevande': 'dispensa', 'condimenti': 'dispensa', 'snack': 'dispensa', 'conserve': 'dispensa', 'cereali': 'dispensa', 'igiene': 'altro', 'pulizia': 'altro', 'altro': 'dispensa' }; // Map Open Food Facts categories to local categories function mapToLocalCategory(ofCategory, productName) { if (!ofCategory) { // No category tag β€” try to guess from product name return guessCategoryFromName(productName || ''); } const cat = ofCategory.toLowerCase(); // Direct match with our local keys for (const key of Object.keys(CATEGORY_ICONS)) { if (cat === key) return key; } // Handle specific Open Food Facts tags FIRST (before generic regex) // "plant-based-foods-and-beverages" is a catch-all β€” use product name to decide if (/plant-based-foods/.test(cat)) { return guessCategoryFromName(productName || ''); } // "beverages-and-beverages-preparations" = actual beverages if (/^en:beverages/.test(cat)) return 'bevande'; // sweeteners = condimenti if (/sweetener|dolcific/.test(cat)) return 'condimenti'; // Specific tag patterns if (/dairy|lait|cheese|fromage|yoghurt|milk|latticin|latte/.test(cat)) return 'latticini'; if (/meat|viande|carne|sausage|salum|prosciutt/.test(cat)) return 'carne'; if (/fish|poisson|pesce|seafood|tuna|tonno|salmone/.test(cat)) return 'pesce'; if (/fruit|frutta|juice|succo|apple|banana/.test(cat)) return 'frutta'; if (/vegetable|verdur|legum|salad|insalat|tomato|pomodor/.test(cat)) return 'verdura'; if (/pasta|rice|riso|noodle|spaghetti|penne|grain/.test(cat)) return 'pasta'; if (/bread|pane|forno|biscott|toast|cracker|grissini|fette/.test(cat)) return 'pane'; if (/frozen|surgelΓ©|surgel|gelat/.test(cat)) return 'surgelati'; if (/sauce|condiment|oil|olio|vinegar|aceto|mayo|ketchup|spice|salt|sugar|zuccher/.test(cat)) return 'condimenti'; if (/snack|chip|crisp|chocolate|cioccolat|candy|biscuit|cookie|wafer|merendine|patatine/.test(cat)) return 'snack'; if (/conserve|canned|can|pelati|passata|preserve|jam|marmellat|miele|honey/.test(cat)) return 'conserve'; if (/cereal|muesli|granola|oat|fiocchi/.test(cat)) return 'cereali'; if (/hygiene|soap|shampoo|igien|dentifricio|deodorant/.test(cat)) return 'igiene'; if (/clean|detergent|pulizia|detersiv/.test(cat)) return 'pulizia'; // Beverage check LAST (to avoid false matches on compound tags) if (/^(?!.*plant-based).*(beverage|drink|boisson|bevand|water|acqua|beer|birra|wine|vino|coffee|caffΓ¨|tea\b)/.test(cat)) return 'bevande'; return 'altro'; } // Guess a local category purely from product name function guessCategoryFromName(name) { if (!name) return 'altro'; const n = name.toLowerCase(); // Pasta & Rice if (/spaghetti|penne|fusilli|rigatoni|linguine|orecchiette|farfalle|pasta\b|riso\b|basmati|carnaroli|arborio/.test(n)) return 'pasta'; // Pane & Forno if (/pane\b|fette biscottate|grissini|cracker|toast|piadina|piadelle|focaccia|panini|sandwich|taralli/.test(n)) return 'pane'; // Conserve if (/passata|pelati|pomodoro|sugo|polpa di pomod|marmellata|miele|legumi|ceci|fagioli|lenticchie|olive/.test(n)) return 'conserve'; // Condimenti if (/olio\b|aceto|sale\b|pepe\b|zucchero|zuccher|farina|maionese|ketchup|senape|salsa/.test(n)) return 'condimenti'; // Bevande if (/acqua|birra|vino|succo|spremuta|coca.cola|aranciata|caffΓ¨|tΓ¨\b|tea\b|latte\b/.test(n)) return 'bevande'; // Latticini if (/latte\b|yogurt|formaggio|mozzarella|burro|panna|ricotta|mascarpone|gorgonzola|parmigiano|grana\b/.test(n)) return 'latticini'; // Carne if (/pollo|manzo|maiale|vitello|tacchino|prosciutto|salame|bresaola|mortadella|wurstel|speck/.test(n)) return 'carne'; // Pesce if (/tonno|salmone|merluzzo|pesce|sgombro|gamberi|acciughe/.test(n)) return 'pesce'; // Frutta if (/mela|mele|banana|arancia|pera|fragola|uva|kiwi|limone|frutta/.test(n)) return 'frutta'; // Verdura if (/insalata|zucchina|pomodor|cipolla|carota|spinaci|rucola|peperoni|melanzane|broccoli|patata/.test(n)) return 'verdura'; // Surgelati if (/surgelat|frozen|findus|4.salti|gelato/.test(n)) return 'surgelati'; // Snack if (/biscott|cioccolat|nutella|merendine|patatine|caramelle|wafer|sfornatini/.test(n)) return 'snack'; // Cereali if (/cereali|muesli|fiocchi|granola|polenta/.test(n)) return 'cereali'; // Igiene / Pulizia if (/sapone|shampoo|dentifricio|deodorante/.test(n)) return 'igiene'; if (/detersivo|pulito|sgrassatore/.test(n)) return 'pulizia'; return 'altro'; } // Determine safety level for expired products // Returns { level: 'danger'|'warning'|'ok', icon, label, tip } function getExpiredSafety(item, daysExpired) { const cat = mapToLocalCategory(item.category || '', item.name || ''); const loc = (item.location || '').toLowerCase(); const name = (item.name || '').toLowerCase(); // HIGH RISK categories - perishable, can be dangerous // Latticini freschi, carne, pesce, verdura, frutta const highRisk = ['latticini', 'carne', 'pesce', 'verdura', 'frutta']; // MEDIUM RISK - check before consuming // Pane, surgelati, bevande (fresh juices, milk) const medRisk = ['pane', 'surgelati']; // Items in frigo are more perishable const inFrigo = loc === 'frigo'; if (highRisk.includes(cat)) { if (daysExpired <= 2 && inFrigo) { return { level: 'warning', icon: 'πŸ‘€', label: 'Controlla', tip: 'Scaduto da poco, controlla odore e aspetto prima di consumare' }; } return { level: 'danger', icon: 'πŸ—‘οΈ', label: 'Buttare', tip: 'Prodotto deperibile scaduto: da buttare per sicurezza' }; } if (medRisk.includes(cat)) { if (daysExpired <= 7) { return { level: 'warning', icon: 'πŸ‘€', label: 'Controlla', tip: 'Controlla aspetto e odore prima di consumare' }; } if (daysExpired <= 30) { return { level: 'warning', icon: 'πŸ‘€', label: 'Controlla', tip: 'Scaduto da un po\', verificare bene prima dell\'uso' }; } return { level: 'danger', icon: 'πŸ—‘οΈ', label: 'Buttare', tip: 'Troppo tempo dalla scadenza, meglio buttare' }; } // LOW RISK - long shelf life items // Pasta, conserve, condimenti, cereali, snack, bevande confezionate // "Da consumarsi preferibilmente entro" = TMC, safe well past expiry if (daysExpired <= 30) { return { level: 'ok', icon: 'βœ…', label: 'OK', tip: 'Prodotto a lunga conservazione, ancora sicuro da consumare' }; } if (daysExpired <= 180) { return { level: 'warning', icon: 'πŸ‘€', label: 'Controlla', tip: 'Scaduto da oltre un mese, controllare integritΓ  confezione' }; } return { level: 'danger', icon: 'πŸ—‘οΈ', label: 'Buttare', tip: 'Scaduto da troppo tempo, meglio non rischiare' }; } // Nice Italian labels for local categories const CATEGORY_LABELS = { 'latticini': 'πŸ₯› Latticini', 'carne': 'πŸ₯© Carne', 'pesce': '🐟 Pesce', 'frutta': '🍎 Frutta', 'verdura': 'πŸ₯¬ Verdura', 'pasta': '🍝 Pasta & Riso', 'pane': '🍞 Pane & Forno', 'surgelati': '🧊 Surgelati', 'bevande': 'πŸ₯€ Bevande', 'condimenti': 'πŸ§‚ Condimenti', 'snack': 'πŸͺ Snack & Dolci', 'conserve': 'πŸ₯« Conserve', 'cereali': '🌾 Cereali & Legumi', 'igiene': '🧴 Igiene', 'pulizia': '🧹 Pulizia', 'altro': 'πŸ“¦ Altro' }; // Detect best unit/quantity from Open Food Facts quantity_info string // Returns the actual package weight/volume as default (e.g. 700g β†’ unit:'g', quantity:700) function detectUnitAndQuantity(quantityInfo) { if (!quantityInfo) return { unit: 'pz', quantity: 1, weightInfo: '' }; const q = quantityInfo.toLowerCase().trim(); // Match multi-pack patterns like "6 x 1l", "4 x 125g" β†’ total weight const multiMatch = q.match(/(\d+)\s*x\s*([\d.,]+)\s*(ml|l|g|kg|cl)/i); if (multiMatch) { const count = parseInt(multiMatch[1]); let perUnitVal = parseFloat(multiMatch[2].replace(',', '.')); let perUnitUnit = multiMatch[3].toLowerCase(); if (perUnitUnit === 'cl') { perUnitUnit = 'ml'; perUnitVal *= 10; } const totalVal = count * perUnitVal; return { unit: perUnitUnit, quantity: totalVal, weightInfo: quantityInfo }; } // Match single package patterns like "500 g", "1 l", "750 ml", "1.5 kg" const match = q.match(/([\d.,]+)\s*(kg|g|l|ml|cl)/i); if (match) { let unit = match[2].toLowerCase(); let val = parseFloat(match[1].replace(',', '.')); if (unit === 'cl') { unit = 'ml'; val *= 10; } return { unit, quantity: val, weightInfo: quantityInfo }; } return { unit: 'pz', quantity: 1, weightInfo: quantityInfo }; } // Estimate expiry days based on category/product type const EXPIRY_DAYS = { 'latticini': 7, 'carne': 4, 'pesce': 3, 'frutta': 7, 'verdura': 7, 'pasta': 730, 'pane': 4, 'surgelati': 180, 'bevande': 365, 'condimenti': 365, 'snack': 180, 'conserve': 730, 'cereali': 365, 'igiene': 1095, 'pulizia': 1095, 'altro': 180 }; // More specific expiry by product name keywords function estimateExpiryDays(product) { const name = (product.name || '').toLowerCase(); const cat = (product.category || '').toLowerCase(); // Specific product overrides if (/latte\s+(fresco|intero|parzial|scremato)/.test(name)) return 7; if (/latte\s+uht|latte\s+a\s+lunga/.test(name)) return 90; if (/yogurt/.test(name)) return 21; if (/mozzarella|burrata|stracciatella/.test(name)) return 5; if (/formaggio\s+(fresco|ricotta|mascarpone|stracchino|crescenza)/.test(name)) return 10; if (/parmigiano|grana|pecorino|provolone/.test(name)) return 60; if (/prosciutto\s+cotto|mortadella|wurstel/.test(name)) return 7; if (/prosciutto\s+crudo|salame|bresaola|speck/.test(name)) return 30; if (/uova/.test(name)) return 28; if (/pane\s+fresco|pane\s+in\s+cassetta/.test(name)) return 5; if (/pane\s+confezionato|pan\s+carr|pancarrΓ¨/.test(name)) return 14; if (/insalata|rucola|spinaci\s+freschi/.test(name)) return 5; if (/pollo|tacchino|maiale|manzo|vitello/.test(name)) return 3; if (/salmone|tonno\s+fresco|pesce/.test(name) && !/tonno\s+in\s+scatola|tonno\s+rio/.test(name)) return 2; if (/tonno\s+in\s+scatola|tonno\s+rio|sgombro\s+in/.test(name)) return 1095; if (/surgelat|frozen|findus|4\s*salti/.test(name)) return 180; if (/gelato/.test(name)) return 365; if (/succo|spremuta/.test(name)) return 7; if (/birra|vino/.test(name)) return 365; if (/acqua/.test(name)) return 365; if (/biscott|cracker|grissini|fette\s+biscott/.test(name)) return 180; if (/nutella|marmellata|miele/.test(name)) return 365; if (/passata|pelati|pomodor/.test(name)) return 730; if (/olio|aceto/.test(name)) return 548; // Fallback to category for (const [key, days] of Object.entries(EXPIRY_DAYS)) { if (cat.includes(key)) return days; } return 180; // generic default } function formatEstimatedExpiry(days) { if (days <= 7) return `~${days} giorni`; if (days <= 30) return `~${Math.round(days / 7)} settimane`; if (days <= 365) return `~${Math.round(days / 30)} mesi`; return `~${Math.round(days / 365)} anni`; } function addDays(days) { const d = new Date(); d.setDate(d.getDate() + days); return d.toISOString().split('T')[0]; } // Guess location from product name keywords (fallback if no category) function guessLocationFromName(name) { const n = (name || '').toLowerCase(); // Frigo keywords if (/latte|yogurt|formaggio|mozzarella|burro|panna|uova|prosciutto|salame|wurstel|ricotta|mascarpone|gorgonzola|insalata|rucola|spinaci|pollo|manzo|maiale|salmone|tonno fresco|bresaola/.test(n)) return 'frigo'; // Freezer keywords if (/surgel|frozen|gelato|ghiaccioli|bastoncini|findus|4 salti|pizza surgel|verdure surgel|minestrone surg/.test(n)) return 'freezer'; // Dispensa keywords if (/pasta|riso|farina|zucchero|sale|olio|aceto|biscott|cracker|grissini|caffΓ¨|tΓ¨|the |tea |tonno|pelati|passata|legumi|ceci|fagioli|lenticchie|cereali|muesli|marmell|nutella|miele|cioccolat/.test(n)) return 'dispensa'; return null; // unknown } function guessLocation(product) { // 1. Category-based if (product.category) { const cat = product.category.toLowerCase().replace(/^en:/, '').split(',')[0].trim(); // Check our map for (const [key, loc] of Object.entries(CATEGORY_LOCATION)) { if (cat.includes(key)) return loc; } // Open Food Facts categories if (/dairy|lait|cheese|fromage|yoghurt|milk|latticin/i.test(cat)) return 'frigo'; if (/meat|viande|carne|fish|poisson|pesce/i.test(cat)) return 'frigo'; if (/frozen|surgelΓ©|surgel/i.test(cat)) return 'freezer'; if (/fruit|vegetable|verdur|frutta/i.test(cat)) return 'frigo'; if (/beverage|drink|boisson|bevand/i.test(cat)) return 'dispensa'; if (/pasta|cereal|grain|bread|biscuit|snack|sauce|condiment|conserv|can/i.test(cat)) return 'dispensa'; } // 2. Name-based fallback const nameLoc = guessLocationFromName(product.name); if (nameLoc) return nameLoc; // 3. Default return 'dispensa'; } // ===== STATE ===== let currentProduct = null; let currentInventory = []; let currentLocation = ''; let scannerStream = null; let quaggaRunning = false; let aiStream = null; // ===== API HELPER ===== async function api(action, params = {}, method = 'GET', body = null) { let url = `${API_BASE}?action=${action}`; if (method === 'GET') { Object.entries(params).forEach(([k, v]) => { url += `&${encodeURIComponent(k)}=${encodeURIComponent(v)}`; }); } const opts = { method }; if (body) { opts.headers = { 'Content-Type': 'application/json' }; opts.body = JSON.stringify(body); } const res = await fetch(url, opts); return res.json(); } // ===== PAGE NAVIGATION ===== function showPage(pageId, param = null) { // Hide all pages document.querySelectorAll('.page').forEach(p => p.classList.remove('active')); // Show target page const page = document.getElementById(`page-${pageId}`); if (page) page.classList.add('active'); // Update nav document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active')); const navBtn = document.querySelector(`.nav-btn[data-page="${pageId}"]`); if (navBtn) navBtn.classList.add('active'); // Page-specific init switch(pageId) { case 'dashboard': loadDashboard(); break; case 'inventory': if (param !== null) { currentLocation = param; filterLocation(param); } loadInventory(); break; case 'scan': initScanner(); clearQuickNameResults(); break; case 'products': loadAllProducts(); break; case 'shopping': loadShoppingList(); break; case 'ai': initAICamera(); break; } // Stop scanner when leaving scan page if (pageId !== 'scan' && pageId !== 'ai') { stopScanner(); } // Scroll to top window.scrollTo(0, 0); } // ===== DASHBOARD ===== async function loadDashboard() { try { const [summaryData, statsData, invData] = await Promise.all([ api('inventory_summary'), api('stats'), api('inventory_list') ]); // Update stat cards const summary = summaryData.summary || []; let total = 0; ['dispensa', 'frigo', 'freezer'].forEach(loc => { const s = summary.find(x => x.location === loc); const count = s ? s.product_count : 0; document.getElementById(`stat-${loc}`).textContent = count; total += count; }); // Add non-standard locations summary.forEach(s => { if (!['dispensa', 'frigo', 'freezer'].includes(s.location)) { total += s.product_count; } }); document.getElementById('stat-total').textContent = total || summary.reduce((a, s) => a + s.product_count, 0); // Expiring items const expiringSection = document.getElementById('alert-expiring'); const expiringList = document.getElementById('expiring-list'); if (statsData.expiring_soon && statsData.expiring_soon.length > 0) { expiringSection.style.display = 'block'; expiringList.innerHTML = statsData.expiring_soon.map(item => { const days = daysUntilExpiry(item.expiry_date); let badgeText, badgeClass; if (days === 0) { badgeText = 'OGGI'; badgeClass = 'today'; } else if (days === 1) { badgeText = 'Domani'; badgeClass = 'expiring'; } else if (days <= 7) { badgeText = `${days} giorni`; badgeClass = 'expiring'; } else if (days <= 30) { badgeText = `${days}g`; badgeClass = 'expiring-soon'; } else { const m = Math.round(days/30); badgeText = m <= 1 ? `${days}g` : `~${m} mesi`; badgeClass = 'expiring-later'; } return `
${escapeHtml(item.name)} ${item.brand ? `${escapeHtml(item.brand)}` : ''}
${badgeText}
`; }).join(''); } else { expiringSection.style.display = 'none'; } // Expired items const expiredSection = document.getElementById('alert-expired'); const expiredList = document.getElementById('expired-list'); if (statsData.expired && statsData.expired.length > 0) { expiredSection.style.display = 'block'; expiredList.innerHTML = statsData.expired.map(item => { const days = Math.abs(daysUntilExpiry(item.expiry_date)); let daysText; if (days === 0) daysText = 'Oggi'; else if (days === 1) daysText = 'Da ieri'; else daysText = `Da ${days}g`; const safety = getExpiredSafety(item, days); return `
${escapeHtml(item.name)} ${item.brand ? `${escapeHtml(item.brand)}` : ''}
${daysText} ${safety.icon} ${safety.label}
`; }).join(''); } else { expiredSection.style.display = 'none'; } // Full inventory grouped by location, then by category within each location const allItems = invData.inventory || []; const grouped = { dispensa: [], frigo: [], freezer: [], altro: [] }; allItems.forEach(item => { const loc = grouped[item.location] !== undefined ? item.location : 'altro'; grouped[loc].push(item); }); for (const [loc, items] of Object.entries(grouped)) { const section = document.getElementById(`dash-section-${loc}`); const container = document.getElementById(`dash-inv-${loc}`); if (items.length === 0) { section.style.display = 'none'; } else { section.style.display = 'block'; container.innerHTML = renderGroupedByCategory(items, true); } } } catch (err) { console.error('Dashboard load error:', err); } } // Group items by local category and render with category headers function renderGroupedByCategory(items, compact = false) { const catGroups = {}; items.forEach(item => { const localCat = mapToLocalCategory(item.category, item.name); if (!catGroups[localCat]) catGroups[localCat] = []; catGroups[localCat].push(item); }); // Sort categories: use CATEGORY_ICONS key order const catOrder = Object.keys(CATEGORY_ICONS); const sortedCats = Object.keys(catGroups).sort((a, b) => { const ia = catOrder.indexOf(a); const ib = catOrder.indexOf(b); return (ia === -1 ? 999 : ia) - (ib === -1 ? 999 : ib); }); let html = ''; for (const cat of sortedCats) { const catItems = catGroups[cat]; const label = CATEGORY_LABELS[cat] || 'πŸ“¦ Altro'; html += `
${label} ${catItems.length}
`; html += catItems.map(item => compact ? renderDashItem(item) : renderInventoryItem(item)).join(''); } return html; } function renderDashItem(item) { const catIcon = CATEGORY_ICONS[mapToLocalCategory(item.category, item.name)] || 'πŸ“¦'; const days = daysUntilExpiry(item.expiry_date); const isExpired = days < 0; const isExpiring = !isExpired && days <= 7; const qtyDisplay = formatQuantity(item.quantity, item.unit); let expiryLabel = ''; if (item.expiry_date) { if (days < 0) expiryLabel = `⚠️ Scaduto da ${Math.abs(days)}g`; else if (days === 0) expiryLabel = '⚠️ Scade oggi!'; else if (days === 1) expiryLabel = '⏰ Scade domani'; else if (days <= 7) expiryLabel = `⏰ ${days} giorni`; else expiryLabel = formatDate(item.expiry_date); } return `
${item.image_url ? `` : catIcon}
${escapeHtml(item.name)}
${item.brand ? `
${escapeHtml(item.brand)}
` : ''}
${qtyDisplay} ${expiryLabel ? `${expiryLabel}` : ''}
`; } function dashItemTap(inventoryId, productId) { // Load full inventory so modal works api('inventory_list').then(data => { currentInventory = data.inventory || []; showItemDetail(inventoryId, productId); }); } function showAlertItemDetail(inventoryId, productId) { // Load full inventory so modal works (same pattern as dashItemTap) api('inventory_list').then(data => { currentInventory = data.inventory || []; showItemDetail(inventoryId, productId); }); } function formatQuantity(qty, unit) { if (!qty && qty !== 0) return ''; const n = parseFloat(qty); const unitLabels = { 'pz': 'pz', 'kg': 'kg', 'g': 'g', 'l': 'L', 'ml': 'ml', 'conf': 'conf' }; const label = unitLabels[unit] || unit || 'pz'; // Format nicely if (n === Math.floor(n)) return `${Math.floor(n)} ${label}`; return `${n.toFixed(1)} ${label}`; } // ===== INVENTORY ===== async function loadInventory() { try { const data = await api('inventory_list', currentLocation ? { location: currentLocation } : {}); currentInventory = data.inventory || []; renderInventory(currentInventory); } catch (err) { console.error('Inventory load error:', err); } } function renderInventoryItem(item) { const catIcon = CATEGORY_ICONS[mapToLocalCategory(item.category, item.name)] || 'πŸ“¦'; const locInfo = LOCATIONS[item.location] || { icon: 'πŸ“¦', label: item.location }; const days = daysUntilExpiry(item.expiry_date); const isExpired = days < 0; const isExpiring = !isExpired && days <= 7; const qtyDisplay = formatQuantity(item.quantity, item.unit); let expiryBadge = ''; if (item.expiry_date) { let expiryText; if (isExpired) expiryText = `⚠️ Scaduto da ${Math.abs(days)}g`; else if (days === 0) expiryText = '⚠️ Scade oggi!'; else if (days === 1) expiryText = '⏰ Domani'; else if (days <= 7) expiryText = `⏰ ${days} giorni`; else expiryText = formatDate(item.expiry_date); expiryBadge = `${expiryText}`; } return `
${item.image_url ? `` : catIcon}
${escapeHtml(item.name)}
${item.brand ? `
${escapeHtml(item.brand)}
` : ''}
${locInfo.icon} ${locInfo.label} ${qtyDisplay} ${expiryBadge}
`; } function renderInventory(items) { const container = document.getElementById('inventory-list'); if (items.length === 0) { container.innerHTML = '
πŸ“­

Nessun prodotto qui.
Scansiona un prodotto per aggiungerlo!

'; return; } container.innerHTML = renderGroupedByCategory(items, false); } function filterLocation(loc) { currentLocation = loc; document.querySelectorAll('.location-tabs .tab').forEach(t => { t.classList.toggle('active', t.dataset.loc === loc); }); loadInventory(); } function filterInventory() { const q = document.getElementById('inventory-search').value.toLowerCase(); if (!q) { renderInventory(currentInventory); return; } const filtered = currentInventory.filter(i => i.name.toLowerCase().includes(q) || (i.brand && i.brand.toLowerCase().includes(q)) || (i.barcode && i.barcode.includes(q)) ); renderInventory(filtered); } // ===== ITEM DETAIL MODAL ===== function showItemDetail(inventoryId, productId) { const item = currentInventory.find(i => i.id === inventoryId); if (!item) return; const locInfo = LOCATIONS[item.location] || { icon: 'πŸ“¦', label: item.location }; const catIcon = CATEGORY_ICONS[mapToLocalCategory(item.category, item.name)] || 'πŸ“¦'; document.getElementById('modal-content').innerHTML = `
${item.image_url ? `` : `${catIcon}` }

${escapeHtml(item.name)}

${item.brand ? escapeHtml(item.brand) : ''}

`; document.getElementById('modal-overlay').style.display = 'flex'; } function closeModal() { document.getElementById('modal-overlay').style.display = 'none'; } async function quickUse(productId, location) { closeModal(); showLoading(true); try { currentProduct = { id: productId }; // Get product info const data = await api('product_get', { id: productId }); if (data.product) { currentProduct = data.product; // Extract weight_info from notes if available if (!currentProduct.weight_info && currentProduct.notes) { const pesoMatch = currentProduct.notes.match(/Peso:\s*([^Β·]+)/); if (pesoMatch) currentProduct.weight_info = pesoMatch[1].trim(); } } document.getElementById('use-location').value = location; // Mark active location button document.querySelectorAll('#page-use .loc-btn').forEach(b => b.classList.remove('active')); const locBtns = document.querySelectorAll('#page-use .loc-btn'); locBtns.forEach(b => { if (b.textContent.toLowerCase().includes(location)) b.classList.add('active'); }); renderUsePreview(); loadUseInventoryInfo(); showLoading(false); showPage('use'); } catch (err) { showLoading(false); console.error('quickUse error:', err); showToast('Errore nel caricamento del prodotto', 'error'); } } async function deleteInventoryItem(id) { if (confirm('Vuoi davvero rimuovere questo prodotto dall\'inventario?')) { await api('inventory_delete', {}, 'POST', { id }); closeModal(); showToast('Prodotto rimosso', 'success'); loadInventory(); } } function editInventoryItem(id) { const item = currentInventory.find(i => i.id === id); if (!item) { closeModal(); showToast('Prodotto non trovato', 'error'); return; } // Rebuild modal content for editing (don't close and reopen - just replace content) document.getElementById('modal-content').innerHTML = `
${Object.entries(LOCATIONS).map(([k, v]) => ` `).join('')}
`; document.getElementById('modal-overlay').style.display = 'flex'; } async function submitEditInventory(e, id) { e.preventDefault(); const qty = parseFloat(document.getElementById('edit-qty').value); const loc = document.getElementById('edit-loc').value; const expiry = document.getElementById('edit-expiry').value || null; await api('inventory_update', {}, 'POST', { id, quantity: qty, location: loc, expiry_date: expiry }); closeModal(); showToast('Aggiornato!', 'success'); loadInventory(); } // ===== BARCODE SCANNER ===== async function initScanner() { const video = document.getElementById('scanner-video'); const viewport = document.getElementById('scanner-viewport'); try { // Stop any existing stream stopScanner(); const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment', width: { ideal: 1280 }, height: { ideal: 720 } } }); scannerStream = stream; video.srcObject = stream; await video.play(); // Start Quagga for barcode detection startQuagga(video); } catch (err) { console.error('Camera error:', err); document.getElementById('scan-result').style.display = 'block'; document.getElementById('scan-result').innerHTML = `

⚠️ Impossibile accedere alla fotocamera.

Assicurati di usare HTTPS e di aver concesso i permessi della fotocamera.
Puoi inserire il barcode manualmente o usare l'identificazione AI.

`; } } function startQuagga(videoEl) { if (quaggaRunning) return; const canvas = document.getElementById('scanner-canvas'); const ctx = canvas.getContext('2d'); let scanning = true; quaggaRunning = true; let lastDetected = ''; let detectCount = 0; function scanFrame() { if (!scanning || !scannerStream) return; canvas.width = videoEl.videoWidth; canvas.height = videoEl.videoHeight; ctx.drawImage(videoEl, 0, 0); try { Quagga.decodeSingle({ src: canvas.toDataURL('image/jpeg', 0.8), numOfWorkers: 0, inputStream: { size: 800 }, decoder: { readers: [ 'ean_reader', 'ean_8_reader', 'code_128_reader', 'code_39_reader', 'upc_reader', 'upc_e_reader' ] }, locate: true }, function(result) { if (result && result.codeResult) { const code = result.codeResult.code; if (code === lastDetected) { detectCount++; } else { lastDetected = code; detectCount = 1; } // Require 2 consecutive reads for reliability if (detectCount >= 2) { scanning = false; quaggaRunning = false; onBarcodeDetected(code); return; } } if (scanning) { setTimeout(scanFrame, 300); } }); } catch (e) { if (scanning) setTimeout(scanFrame, 500); } } // Start scanning after a small delay setTimeout(scanFrame, 500); } function stopScanner() { quaggaRunning = false; if (scannerStream) { scannerStream.getTracks().forEach(t => t.stop()); scannerStream = null; } const video = document.getElementById('scanner-video'); if (video) video.srcObject = null; // Also stop AI camera if (aiStream) { aiStream.getTracks().forEach(t => t.stop()); aiStream = null; } const aiVideo = document.getElementById('ai-video'); if (aiVideo) aiVideo.srcObject = null; } async function onBarcodeDetected(barcode) { showLoading(true); // Vibrate if available if (navigator.vibrate) navigator.vibrate(100); try { // First check local DB const localResult = await api('search_barcode', { barcode }); if (localResult.found) { currentProduct = localResult.product; // If product was saved with 'pz' but has weight info in notes, fix defaults if (currentProduct.unit === 'pz' && currentProduct.default_quantity <= 1 && currentProduct.notes) { const pesoMatch = currentProduct.notes.match(/Peso:\s*([^Β·]+)/); if (pesoMatch) { const weightStr = pesoMatch[1].trim(); const detected = detectUnitAndQuantity(weightStr); if (detected.unit !== 'pz') { currentProduct.unit = detected.unit; currentProduct.default_quantity = detected.quantity; currentProduct.weight_info = weightStr; // Update product in DB for future scans api('product_save', {}, 'POST', { id: currentProduct.id, barcode: currentProduct.barcode, name: currentProduct.name, brand: currentProduct.brand || '', category: currentProduct.category || '', image_url: currentProduct.image_url || '', unit: detected.unit, default_quantity: detected.quantity, notes: currentProduct.notes, }); } } } // Extract weight_info from notes if available (stored as "Peso: 500 g Β· ...") if (!currentProduct.weight_info && currentProduct.notes) { const pesoMatch = currentProduct.notes.match(/Peso:\s*([^Β·]+)/); if (pesoMatch) currentProduct.weight_info = pesoMatch[1].trim(); } showLoading(false); stopScanner(); showProductAction(); return; } // Lookup in external DB const lookupResult = await api('lookup_barcode', { barcode }); if (lookupResult.found && lookupResult.product) { const p = lookupResult.product; // Detect unit and quantity from quantity_info const detected = detectUnitAndQuantity(p.quantity_info); // Build rich notes with all available info const notesParts = []; if (p.quantity_info) notesParts.push(`Peso: ${p.quantity_info}`); if (p.nutriscore) notesParts.push(`Nutriscore: ${p.nutriscore.toUpperCase()}`); if (p.nova_group) notesParts.push(`NOVA: ${p.nova_group}`); if (p.ecoscore) notesParts.push(`Ecoscore: ${p.ecoscore.toUpperCase()}`); if (p.origin) notesParts.push(`Origine: ${p.origin}`); if (p.labels) notesParts.push(`Etichette: ${p.labels}`); // Save to local DB const saveResult = await api('product_save', {}, 'POST', { barcode: barcode, name: p.name || 'Prodotto sconosciuto', brand: p.brand || '', category: p.category || '', image_url: p.image_url || '', unit: detected.unit, default_quantity: detected.quantity, notes: notesParts.join(' Β· '), }); if (saveResult.id) { currentProduct = { id: saveResult.id, barcode: barcode, name: p.name || 'Prodotto sconosciuto', brand: p.brand || '', category: p.category || '', image_url: p.image_url || '', unit: detected.unit, default_quantity: detected.quantity, weight_info: p.quantity_info || '', nutriscore: p.nutriscore || '', ingredients: p.ingredients || '', allergens: p.allergens || '', conservation: p.conservation || '', origin: p.origin || '', nova_group: p.nova_group || '', ecoscore: p.ecoscore || '', labels: p.labels || '', stores: p.stores || '', }; showLoading(false); stopScanner(); showProductAction(); return; } } // Not found - ask user to add manually showLoading(false); stopScanner(); showToast('Prodotto non trovato. Inseriscilo manualmente.', 'error'); startManualEntry(barcode); } catch (err) { showLoading(false); console.error('Barcode lookup error:', err); showToast('Errore nella ricerca. Riprova.', 'error'); } } function submitManualBarcode() { const input = document.getElementById('manual-barcode-input'); const barcode = (input.value || '').trim(); if (!barcode) { showToast('Inserisci un codice a barre', 'error'); input.focus(); return; } if (!/^\d{4,14}$/.test(barcode)) { showToast('Il codice a barre deve contenere solo numeri (4-14 cifre)', 'error'); input.focus(); return; } stopScanner(); onBarcodeDetected(barcode); } // ===== QUICK NAME ENTRY (for loose/unpackaged products) ===== async function submitQuickName() { const input = document.getElementById('quick-product-name'); const name = (input.value || '').trim(); if (!name || name.length < 2) { showToast('Scrivi almeno 2 caratteri', 'error'); input.focus(); return; } stopScanner(); showLoading(true); try { // Search local products DB const localData = await api('products_search', { q: name }); const localProducts = (localData.products || []).slice(0, 5); showLoading(false); if (localProducts.length > 0) { // Show results to pick from + option to create new showQuickNameResults(name, localProducts); } else { // No local results β€” create new product directly await createQuickProduct(name); } } catch (err) { showLoading(false); console.error('Quick name search error:', err); showToast('Errore nella ricerca', 'error'); } } function showQuickNameResults(searchName, products) { const container = document.querySelector('.quick-name-entry'); // Remove any previous results const oldResults = container.querySelector('.quick-name-results'); if (oldResults) oldResults.remove(); const resultsDiv = document.createElement('div'); resultsDiv.className = 'quick-name-results'; // Existing products products.forEach(p => { const catIcon = CATEGORY_ICONS[mapToLocalCategory(p.category, p.name)] || 'πŸ“¦'; const item = document.createElement('div'); item.className = 'quick-name-result-item'; item.innerHTML = ` ${catIcon}
${escapeHtml(p.name)}
${p.brand ? escapeHtml(p.brand) + ' Β· ' : ''}${p.barcode ? 'πŸ“Š ' + p.barcode : 'Senza barcode'}
`; item.onclick = () => selectQuickProduct(p); resultsDiv.appendChild(item); }); // "Create new" button const newItem = document.createElement('div'); newItem.className = 'quick-name-result-item qnr-new'; newItem.innerHTML = ` βž•
Crea "${escapeHtml(searchName)}"
Nuovo prodotto senza barcode
`; newItem.onclick = () => createQuickProduct(searchName); resultsDiv.appendChild(newItem); container.appendChild(resultsDiv); } function selectQuickProduct(product) { currentProduct = { id: product.id, barcode: product.barcode || '', name: product.name, brand: product.brand || '', category: product.category || '', image_url: product.image_url || '', unit: product.unit || 'pz', default_quantity: product.default_quantity || 1, }; // Extract weight_info from notes if available if (product.notes) { const pesoMatch = product.notes.match(/Peso:\s*([^Β·]+)/); if (pesoMatch) currentProduct.weight_info = pesoMatch[1].trim(); } clearQuickNameResults(); showProductAction(); } async function createQuickProduct(name) { showLoading(true); // Auto-detect category from name const category = guessCategoryFromName(name); try { const result = await api('product_save', {}, 'POST', { name: name, brand: '', category: category, unit: 'pz', default_quantity: 1, }); if (result.success || result.id) { currentProduct = { id: result.id, name: name, brand: '', category: category, unit: 'pz', default_quantity: 1, }; showLoading(false); clearQuickNameResults(); showToast('Prodotto creato!', 'success'); showProductAction(); } else { showLoading(false); showToast(result.error || 'Errore nel salvataggio', 'error'); } } catch (err) { showLoading(false); console.error('Quick product creation error:', err); showToast('Errore di connessione', 'error'); } } function clearQuickNameResults() { const container = document.querySelector('.quick-name-entry'); if (container) { const results = container.querySelector('.quick-name-results'); if (results) results.remove(); } const input = document.getElementById('quick-product-name'); if (input) input.value = ''; } function startManualEntry(barcode = '') { stopScanner(); // Reset form document.getElementById('pf-id').value = ''; document.getElementById('pf-name').value = ''; document.getElementById('pf-brand').value = ''; document.getElementById('pf-category').value = ''; document.getElementById('pf-unit').value = 'pz'; document.getElementById('pf-defqty').value = '1'; document.getElementById('pf-notes').value = ''; document.getElementById('pf-barcode').value = barcode || ''; document.getElementById('pf-image').value = ''; document.getElementById('pf-image-preview').style.display = 'none'; document.getElementById('product-form-title').textContent = 'Nuovo Prodotto'; // Reset manual-edit tracking flags document.getElementById('pf-category').dataset.manuallySet = 'false'; document.getElementById('pf-defqty').dataset.manuallySet = 'false'; // Track if user manually changes the quantity field const qtyInput = document.getElementById('pf-defqty'); qtyInput.removeEventListener('input', markQtyManuallySet); qtyInput.addEventListener('input', markQtyManuallySet); // Auto-detect name β†’ category when typing const nameInput = document.getElementById('pf-name'); nameInput.removeEventListener('input', autoDetectCategory); nameInput.addEventListener('input', autoDetectCategory); showPage('product-form'); } function markQtyManuallySet() { document.getElementById('pf-defqty').dataset.manuallySet = 'true'; } function autoDetectCategory() { const name = document.getElementById('pf-name').value.toLowerCase(); if (name.length < 3) return; const catSelect = document.getElementById('pf-category'); // Don't override if user already manually selected something if (catSelect.dataset.manuallySet === 'true') return; // Keywords β†’ category mapping const keyword2cat = { 'latte': 'latticini', 'yogurt': 'latticini', 'formaggio': 'latticini', 'mozzarella': 'latticini', 'burro': 'latticini', 'panna': 'latticini', 'ricotta': 'latticini', 'mascarpone': 'latticini', 'gorgonzola': 'latticini', 'parmigiano': 'latticini', 'grana': 'latticini', 'burrata': 'latticini', 'stracchino': 'latticini', 'uova': 'latticini', 'pollo': 'carne', 'manzo': 'carne', 'maiale': 'carne', 'vitello': 'carne', 'tacchino': 'carne', 'prosciutto': 'carne', 'salame': 'carne', 'bresaola': 'carne', 'mortadella': 'carne', 'wurstel': 'carne', 'macinato': 'carne', 'speck': 'carne', 'salmone': 'pesce', 'tonno': 'pesce', 'sgombro': 'pesce', 'pesce': 'pesce', 'merluzzo': 'pesce', 'mela': 'frutta', 'mele': 'frutta', 'banana': 'frutta', 'arancia': 'frutta', 'pera': 'frutta', 'fragola': 'frutta', 'uva': 'frutta', 'kiwi': 'frutta', 'limone': 'frutta', 'insalata': 'verdura', 'pomodor': 'verdura', 'zucchin': 'verdura', 'patat': 'verdura', 'cipoll': 'verdura', 'carota': 'verdura', 'spinaci': 'verdura', 'rucola': 'verdura', 'peperoni': 'verdura', 'melanzane': 'verdura', 'broccoli': 'verdura', 'pasta': 'pasta', 'spaghetti': 'pasta', 'penne': 'pasta', 'fusilli': 'pasta', 'riso': 'pasta', 'farina': 'pasta', 'rigatoni': 'pasta', 'farfalle': 'pasta', 'pane': 'pane', 'fette biscottate': 'pane', 'pancarrΓ¨': 'pane', 'pan carrΓ¨': 'pane', 'grissini': 'pane', 'crackers': 'pane', 'cracker': 'pane', 'surgelat': 'surgelati', 'findus': 'surgelati', 'gelato': 'surgelati', 'acqua': 'bevande', 'succo': 'bevande', 'birra': 'bevande', 'vino': 'bevande', 'coca cola': 'bevande', 'aranciata': 'bevande', 'tΓ¨': 'bevande', 'caffΓ¨': 'bevande', 'olio': 'condimenti', 'aceto': 'condimenti', 'sale': 'condimenti', 'pepe': 'condimenti', 'maionese': 'condimenti', 'ketchup': 'condimenti', 'senape': 'condimenti', 'zucchero': 'condimenti', 'biscott': 'snack', 'cioccolat': 'snack', 'nutella': 'snack', 'merendine': 'snack', 'patatine': 'snack', 'caramelle': 'snack', 'pelati': 'conserve', 'passata': 'conserve', 'legumi': 'conserve', 'ceci': 'conserve', 'fagioli': 'conserve', 'lenticchie': 'conserve', 'marmellata': 'conserve', 'miele': 'conserve', 'cereali': 'cereali', 'muesli': 'cereali', 'fiocchi': 'cereali', }; for (const [keyword, cat] of Object.entries(keyword2cat)) { if (name.includes(keyword)) { catSelect.value = cat; onCategoryChange(true); return; } } } function onCategoryChange(fromAutoDetect = false) { const cat = document.getElementById('pf-category').value; const unitSelect = document.getElementById('pf-unit'); const qtyInput = document.getElementById('pf-defqty'); // If user manually changed category via dropdown, don't auto-fill qty/unit if (!fromAutoDetect) { // Mark qty as "set" so future auto-detects won't overwrite either qtyInput.dataset.manuallySet = 'true'; return; } // Auto-detect from name: suggest default unit/qty based on category // BUT only if user hasn't manually changed the quantity field const catDefaults = { 'latticini': { unit: 'pz', qty: 1 }, 'carne': { unit: 'g', qty: 500 }, 'pesce': { unit: 'g', qty: 300 }, 'frutta': { unit: 'kg', qty: 1 }, 'verdura': { unit: 'kg', qty: 0.5 }, 'pasta': { unit: 'g', qty: 500 }, 'pane': { unit: 'pz', qty: 1 }, 'surgelati': { unit: 'g', qty: 450 }, 'bevande': { unit: 'l', qty: 1 }, 'condimenti': { unit: 'pz', qty: 1 }, 'snack': { unit: 'g', qty: 250 }, 'conserve': { unit: 'g', qty: 400 }, 'cereali': { unit: 'g', qty: 500 }, 'igiene': { unit: 'pz', qty: 1 }, 'pulizia': { unit: 'pz', qty: 1 }, }; if (catDefaults[cat]) { // Only auto-fill unit/qty if user hasn't manually touched them if (qtyInput.dataset.manuallySet !== 'true') { unitSelect.value = catDefaults[cat].unit; qtyInput.value = catDefaults[cat].qty; } } } async function submitProduct(e) { e.preventDefault(); showLoading(true); const productData = { id: document.getElementById('pf-id').value || null, name: document.getElementById('pf-name').value, brand: document.getElementById('pf-brand').value, category: document.getElementById('pf-category').value, unit: document.getElementById('pf-unit').value, default_quantity: parseFloat(document.getElementById('pf-defqty').value) || 1, notes: document.getElementById('pf-notes').value, barcode: document.getElementById('pf-barcode').value || null, image_url: document.getElementById('pf-image').value || '', }; try { const result = await api('product_save', {}, 'POST', productData); if (result.success) { currentProduct = { ...productData, id: result.id }; showLoading(false); showToast('Prodotto salvato!', 'success'); showProductAction(); } else { showLoading(false); showToast(result.error || 'Errore nel salvataggio', 'error'); } } catch (err) { showLoading(false); showToast('Errore di connessione', 'error'); } } // ===== PRODUCT ACTION (IN/OUT) ===== function showProductAction() { if (!currentProduct) return; const catIcon = CATEGORY_ICONS[mapToLocalCategory(currentProduct.category, currentProduct.name)] || 'πŸ“¦'; const nutriscoreColors = { a: '#1e8f4e', b: '#60ac0e', c: '#eeae0e', d: '#ff6f1e', e: '#e63e11' }; let detailsHtml = ''; // Weight / quantity info if (currentProduct.weight_info) { detailsHtml += `
βš–οΈ ${escapeHtml(currentProduct.weight_info)}
`; } // Nutriscore badge if (currentProduct.nutriscore) { const ns = currentProduct.nutriscore.toLowerCase(); const nsColor = nutriscoreColors[ns] || '#999'; detailsHtml += `
Nutri-Score ${ns.toUpperCase()}
`; } // NOVA group if (currentProduct.nova_group) { const novaLabels = { '1': 'Non trasformato', '2': 'Ingrediente culinario', '3': 'Trasformato', '4': 'Ultra-trasformato' }; detailsHtml += `
🏭 NOVA ${currentProduct.nova_group}${novaLabels[currentProduct.nova_group] ? ' - ' + novaLabels[currentProduct.nova_group] : ''}
`; } // Ecoscore if (currentProduct.ecoscore) { const es = currentProduct.ecoscore.toLowerCase(); const esColor = nutriscoreColors[es] || '#999'; detailsHtml += `
🌍 Eco-Score ${es.toUpperCase()}
`; } // Origin if (currentProduct.origin) { detailsHtml += `
πŸ“ ${escapeHtml(currentProduct.origin)}
`; } // Labels (bio, DOP, etc.) if (currentProduct.labels) { detailsHtml += `
🏷️ ${escapeHtml(currentProduct.labels)}
`; } // Allergens let allergensHtml = ''; if (currentProduct.allergens) { allergensHtml = `
⚠️ Allergeni: ${escapeHtml(currentProduct.allergens)}
`; } // Ingredients (collapsible) let ingredientsHtml = ''; if (currentProduct.ingredients) { const ingredShort = currentProduct.ingredients.length > 120 ? currentProduct.ingredients.substring(0, 120) + '...' : currentProduct.ingredients; ingredientsHtml = `
πŸ“‹ Ingredienti

${escapeHtml(currentProduct.ingredients)}

`; } // Conservation let conservationHtml = ''; if (currentProduct.conservation) { conservationHtml = `
🧊 ${escapeHtml(currentProduct.conservation)}
`; } document.getElementById('action-product-preview').innerHTML = ` ${currentProduct.image_url ? `` : `${catIcon}` }

${escapeHtml(currentProduct.name)}

${currentProduct.brand ? `${escapeHtml(currentProduct.brand)}` : ''}

${currentProduct.barcode ? `

πŸ“Š ${currentProduct.barcode}

` : ''}
`; // Check if product needs editing (unknown name, missing info) const isUnknown = !currentProduct.name || /sconosciuto|unknown|^$/i.test(currentProduct.name.trim()) || currentProduct.name.trim().length < 2; const needsEdit = isUnknown || !currentProduct.brand; // Edit product info section let editInfoEl = document.getElementById('action-edit-info'); if (!editInfoEl) { editInfoEl = document.createElement('div'); editInfoEl.id = 'action-edit-info'; const preview = document.getElementById('action-product-preview'); preview.parentElement.insertBefore(editInfoEl, preview.nextSibling); } if (needsEdit) { const categoryOptions = Object.entries(CATEGORY_LABELS).map(([key, label]) => `` ).join(''); editInfoEl.innerHTML = `

${isUnknown ? '⚠️ Prodotto non riconosciuto' : '✏️ Completa le informazioni'}

${isUnknown ? 'Inserisci il nome e le informazioni del prodotto' : 'Puoi modificare o completare le info mancanti'}

`; editInfoEl.style.display = 'block'; // Focus name field if unknown if (isUnknown) { setTimeout(() => document.getElementById('edit-action-name')?.focus(), 100); } } else { editInfoEl.style.display = 'none'; editInfoEl.innerHTML = ''; } // Show extra product info section below preview let extraInfoEl = document.getElementById('action-product-details'); if (!extraInfoEl) { const container = document.getElementById('action-product-preview').parentElement; extraInfoEl = document.createElement('div'); extraInfoEl.id = 'action-product-details'; // Insert after preview, before action buttons const actionBtns = document.querySelector('#page-action .action-buttons'); actionBtns.parentElement.insertBefore(extraInfoEl, actionBtns); } if (detailsHtml || allergensHtml || ingredientsHtml || conservationHtml) { extraInfoEl.innerHTML = `
${detailsHtml ? `
${detailsHtml}
` : ''} ${allergensHtml} ${ingredientsHtml} ${conservationHtml}
`; extraInfoEl.style.display = 'block'; } else { extraInfoEl.style.display = 'none'; extraInfoEl.innerHTML = ''; } showPage('action'); } async function saveEditedProductInfo() { const name = (document.getElementById('edit-action-name')?.value || '').trim(); if (!name) { showToast('Inserisci il nome del prodotto', 'error'); document.getElementById('edit-action-name')?.focus(); return; } const brand = (document.getElementById('edit-action-brand')?.value || '').trim(); const category = document.getElementById('edit-action-category')?.value || ''; showLoading(true); try { const result = await api('product_save', {}, 'POST', { id: currentProduct.id, barcode: currentProduct.barcode || null, name: name, brand: brand, category: category || currentProduct.category || '', image_url: currentProduct.image_url || '', unit: currentProduct.unit || 'pz', default_quantity: currentProduct.default_quantity || 1, notes: currentProduct.notes || '', }); showLoading(false); if (result.success) { // Update current product in memory currentProduct.name = name; currentProduct.brand = brand; if (category) currentProduct.category = category; showToast('βœ… Prodotto aggiornato!', 'success'); // Refresh the action page with updated data showProductAction(); } else { showToast(result.error || 'Errore nel salvataggio', 'error'); } } catch (err) { showLoading(false); showToast('Errore di connessione', 'error'); } } // ===== ADD TO INVENTORY ===== function showAddForm() { const catIcon = CATEGORY_ICONS[mapToLocalCategory(currentProduct.category, currentProduct.name)] || 'πŸ“¦'; document.getElementById('add-product-preview').innerHTML = ` ${currentProduct.image_url ? `` : `${catIcon}` }

${escapeHtml(currentProduct.name)}

${currentProduct.brand ? escapeHtml(currentProduct.brand) : ''}

${currentProduct.weight_info ? `

${escapeHtml(currentProduct.weight_info)}

` : ''}
`; // Set unit selector const unit = currentProduct.unit || 'pz'; const unitSelect = document.getElementById('add-unit'); unitSelect.value = unit; document.getElementById('add-quantity').value = currentProduct.default_quantity || 1; document.getElementById('add-quantity').dataset.manuallySet = 'false'; // Track manual edits to quantity in add form const addQtyInput = document.getElementById('add-quantity'); addQtyInput.removeEventListener('input', markAddQtyManuallySet); addQtyInput.addEventListener('input', markAddQtyManuallySet); // Show weight info if product has it const weightInfoEl = document.getElementById('add-weight-info'); if (currentProduct.weight_info) { weightInfoEl.textContent = `πŸ“¦ Confezione: ${currentProduct.weight_info}`; weightInfoEl.style.display = 'block'; } else { weightInfoEl.style.display = 'none'; } // Set qty step based on selected unit updateAddQtyStep(); // Auto-detect location const autoLoc = guessLocation(currentProduct); document.getElementById('add-location').value = autoLoc; // Highlight correct location button document.querySelectorAll('#page-add .loc-btn').forEach(b => b.classList.remove('active')); document.querySelectorAll('#page-add .loc-btn').forEach(b => { const btnText = b.textContent.toLowerCase(); if (btnText.includes(autoLoc)) b.classList.add('active'); }); // Show the purchase-type selector const expirySection = document.getElementById('add-expiry-section'); const estimatedDays = estimateExpiryDays(currentProduct); const estimatedDate = addDays(estimatedDays); const estimateLabel = formatEstimatedExpiry(estimatedDays); expirySection.innerHTML = `
Scadenza stimata: ${estimateLabel} ${formatDate(estimatedDate)}

πŸ“ Puoi modificare la data o scansionarla con la fotocamera

`; showPage('add'); } function onAddUnitChange() { updateAddQtyStep(); // If switching units, suggest a sensible quantity // BUT only if the user hasn't manually changed the quantity in this form const unit = document.getElementById('add-unit').value; const qtyInput = document.getElementById('add-quantity'); if (qtyInput.dataset.manuallySet === 'true') return; // User already edited qty, don't overwrite const currentQty = parseFloat(qtyInput.value) || 1; // Convert between related units if logical if (unit === 'g' && currentQty <= 10) qtyInput.value = currentProduct.weight_info ? parseFloat(currentProduct.weight_info) || 250 : 250; if (unit === 'kg' && currentQty > 100) qtyInput.value = (currentQty / 1000).toFixed(1); if (unit === 'ml' && currentQty <= 10) qtyInput.value = 500; if (unit === 'l' && currentQty > 100) qtyInput.value = (currentQty / 1000).toFixed(1); if (unit === 'pz' && currentQty > 100) qtyInput.value = 1; if (unit === 'conf' && currentQty > 10) qtyInput.value = 1; } function updateAddQtyStep() { const qtyInput = document.getElementById('add-quantity'); const unit = document.getElementById('add-unit').value; qtyInput.step = 'any'; if (unit === 'g' || unit === 'ml') { qtyInput.min = '1'; } else if (unit === 'kg' || unit === 'l') { qtyInput.min = '0.1'; } else { qtyInput.min = '1'; } } function markAddQtyManuallySet() { document.getElementById('add-quantity').dataset.manuallySet = 'true'; } function adjustAddQty(delta) { const qtyInput = document.getElementById('add-quantity'); qtyInput.dataset.manuallySet = 'true'; // +/- buttons count as manual edit const unit = document.getElementById('add-unit').value; let val = parseFloat(qtyInput.value) || 0; let step; if (unit === 'kg' || unit === 'l') { step = val < 1 ? 0.1 : 0.5; } else if (unit === 'g' || unit === 'ml') { step = val < 50 ? 1 : (val < 500 ? 10 : 50); } else { step = 1; } val = Math.max(parseFloat(qtyInput.min) || 0.1, val + delta * step); // Round nicely if (step >= 1) val = Math.round(val); else val = Math.round(val * 10) / 10; qtyInput.value = val; } function selectPurchaseType(btn, type, estimatedDate, estimateLabel) { btn.parentElement.querySelectorAll('.purchase-type-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); const detailDiv = document.getElementById('expiry-detail'); // Save current quantity before switching, so we can preserve it const currentQty = document.getElementById('add-quantity').value; if (type === 'new') { detailDiv.innerHTML = `
Scadenza stimata: ${estimateLabel} ${formatDate(estimatedDate)}

πŸ“ Puoi modificare la data o scansionarla con la fotocamera

`; // Restore quantity - switching purchase type should NOT change it document.getElementById('add-quantity').value = currentQty; } else { detailDiv.innerHTML = `

Inserisci la data di scadenza o scansionala

Quanto Γ¨ rimasto approssimativamente?

`; // DON'T auto-set remaining percentage - keep the quantity the user already entered } } function setRemainingPct(pct) { document.querySelectorAll('.remaining-btn').forEach(b => b.classList.remove('active')); event.target.classList.add('active'); const baseQty = currentProduct.default_quantity || 1; const unit = currentProduct.unit || 'pz'; let adjustedQty; if (unit === 'pz' || unit === 'conf') { adjustedQty = Math.max(1, Math.round(baseQty * pct)); } else { adjustedQty = Math.round(baseQty * pct * 10) / 10; } document.getElementById('add-quantity').value = adjustedQty; } function selectLocation(btn, loc) { btn.parentElement.querySelectorAll('.loc-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); document.getElementById('add-location').value = loc; } async function submitAdd(e) { e.preventDefault(); showLoading(true); try { const selectedUnit = document.getElementById('add-unit').value; const productUnit = currentProduct.unit || 'pz'; const result = await api('inventory_add', {}, 'POST', { product_id: currentProduct.id, quantity: parseFloat(document.getElementById('add-quantity').value) || 1, location: document.getElementById('add-location').value, expiry_date: document.getElementById('add-expiry').value || null, unit: selectedUnit !== productUnit ? selectedUnit : null, }); showLoading(false); if (result.success) { showToast(`βœ… ${currentProduct.name} aggiunto!`, 'success'); showPage('dashboard'); } else { showToast(result.error || 'Errore', 'error'); } } catch (err) { showLoading(false); showToast('Errore di connessione', 'error'); } } // ===== USE FROM INVENTORY ===== function showUseForm() { renderUsePreview(); document.getElementById('use-quantity').value = 1; document.getElementById('use-location').value = 'dispensa'; // Reset location buttons document.querySelectorAll('#page-use .loc-btn').forEach(b => b.classList.remove('active')); document.querySelector('#page-use .loc-btn').classList.add('active'); loadUseInventoryInfo(); showPage('use'); } function renderUsePreview() { const catIcon = CATEGORY_ICONS[mapToLocalCategory(currentProduct?.category, currentProduct?.name)] || 'πŸ“¦'; document.getElementById('use-product-preview').innerHTML = ` ${currentProduct?.image_url ? `` : `${catIcon}` }

${escapeHtml(currentProduct?.name || '')}

${currentProduct?.brand ? escapeHtml(currentProduct.brand) : ''}

`; } async function loadUseInventoryInfo() { try { const data = await api('inventory_list'); const items = (data.inventory || []).filter(i => i.product_id == currentProduct.id); const infoEl = document.getElementById('use-inventory-info'); if (items.length > 0) { infoEl.innerHTML = 'πŸ“¦ Disponibile: ' + items.map(i => { const loc = LOCATIONS[i.location] || { icon: 'πŸ“¦', label: i.location }; return `${loc.icon} ${loc.label}: ${i.quantity} ${i.unit}`; }).join(' Β· '); } else { infoEl.innerHTML = '⚠️ Prodotto non presente nell\'inventario.'; } } catch(e) { console.error(e); } } function selectUseLocation(btn, loc) { btn.parentElement.querySelectorAll('.loc-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); document.getElementById('use-location').value = loc; } async function submitUseAll() { showLoading(true); try { const result = await api('inventory_use', {}, 'POST', { product_id: currentProduct.id, use_all: true, location: document.getElementById('use-location').value, }); showLoading(false); if (result.success) { showToast(`πŸ“€ ${currentProduct.name} terminato!`, 'success'); showPage('dashboard'); } else { showToast(result.error || 'Errore', 'error'); } } catch (err) { showLoading(false); showToast('Errore di connessione', 'error'); } } async function submitUse(e) { e.preventDefault(); showLoading(true); try { const qty = parseFloat(document.getElementById('use-quantity').value) || 1; const result = await api('inventory_use', {}, 'POST', { product_id: currentProduct.id, quantity: qty, location: document.getElementById('use-location').value, }); showLoading(false); if (result.success) { showToast(`πŸ“€ Usato ${qty} di ${currentProduct.name}. Rimasti: ${result.remaining}`, 'success'); showPage('dashboard'); } else { showToast(result.error || 'Errore', 'error'); } } catch (err) { showLoading(false); showToast('Errore di connessione', 'error'); } } // ===== AI IDENTIFICATION ===== async function captureForAI() { stopScanner(); showPage('ai'); } async function initAICamera() { const video = document.getElementById('ai-video'); const captureDiv = document.getElementById('ai-capture'); const previewDiv = document.getElementById('ai-preview'); const captureBtn = document.getElementById('ai-capture-btn'); const retakeBtn = document.getElementById('ai-retake-btn'); const resultDiv = document.getElementById('ai-result'); captureDiv.style.display = 'block'; previewDiv.style.display = 'none'; captureBtn.style.display = 'block'; retakeBtn.style.display = 'none'; resultDiv.style.display = 'none'; try { if (aiStream) { aiStream.getTracks().forEach(t => t.stop()); } aiStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment', width: { ideal: 1280 }, height: { ideal: 720 } } }); video.srcObject = aiStream; await video.play(); } catch (err) { console.error('AI Camera error:', err); showToast('Impossibile accedere alla fotocamera', 'error'); } } function takePhotoForAI() { const video = document.getElementById('ai-video'); const canvas = document.getElementById('ai-canvas'); const img = document.getElementById('ai-image'); canvas.width = video.videoWidth; canvas.height = video.videoHeight; const ctx = canvas.getContext('2d'); ctx.drawImage(video, 0, 0); const dataUrl = canvas.toDataURL('image/jpeg', 0.85); img.src = dataUrl; // Stop camera if (aiStream) { aiStream.getTracks().forEach(t => t.stop()); aiStream = null; } video.srcObject = null; document.getElementById('ai-capture').style.display = 'none'; document.getElementById('ai-preview').style.display = 'block'; document.getElementById('ai-capture-btn').style.display = 'none'; document.getElementById('ai-retake-btn').style.display = 'block'; // Immediately start analysis analyzeWithAI(); } function retakePhotoAI() { document.getElementById('ai-result').style.display = 'none'; initAICamera(); } async function analyzeWithAI() { const resultDiv = document.getElementById('ai-result'); resultDiv.style.display = 'block'; resultDiv.innerHTML = '

πŸ€– Identifico il prodotto...

'; const canvas = document.getElementById('ai-canvas'); const base64 = canvas.toDataURL('image/jpeg', 0.7).split(',')[1]; try { const result = await api('gemini_identify', {}, 'POST', { image: base64 }); if (!result.success) { if (result.error === 'no_api_key') { resultDiv.innerHTML = `

⚠️ Chiave API Gemini non configurata.
Aggiungi GEMINI_API_KEY nel file .env sul server.

`; } else { resultDiv.innerHTML = `

❌ ${escapeHtml(result.error || 'Errore nell\'identificazione')}

`; } return; } const id = result.identified; const matches = result.off_matches || []; let html = `

πŸ€– Prodotto identificato

`; html += `
`; html += `${escapeHtml(id.name)}`; if (id.brand) html += ` - ${escapeHtml(id.brand)}`; if (id.description) html += `

${escapeHtml(id.description)}

`; html += `
`; if (matches.length > 0) { html += `

πŸ“¦ Prodotti corrispondenti

`; html += `
`; matches.forEach((m, idx) => { html += `
`; if (m.image_url) { html += ``; } html += `
`; html += `${escapeHtml(m.name)}`; if (m.brand) html += `
${escapeHtml(m.brand)}`; if (m.quantity_info) html += `
${escapeHtml(m.quantity_info)}`; html += `
`; html += `${m.barcode}`; html += `
`; }); html += `
`; } // Option to save as-is without barcode html += `
`; html += ``; html += `
`; resultDiv.innerHTML = html; // Store data for later use window._aiIdentified = id; window._aiMatches = matches; } catch (err) { console.error('AI identify error:', err); resultDiv.innerHTML = `

❌ Errore di connessione

`; } } async function selectAIMatch(idx) { const match = window._aiMatches[idx]; if (!match) return; showLoading(true); try { // Use the barcode to do a full lookup (gets all details) const localResult = await api('search_barcode', { barcode: match.barcode }); if (localResult.found) { currentProduct = localResult.product; showLoading(false); showProductAction(); return; } // Full lookup via OpenFoodFacts const lookupResult = await api('lookup_barcode', { barcode: match.barcode }); if (lookupResult.found && lookupResult.product) { const p = lookupResult.product; const detected = detectUnitAndQuantity(p.quantity_info); const notesParts = []; if (p.quantity_info) notesParts.push(`Peso: ${p.quantity_info}`); if (p.nutriscore) notesParts.push(`Nutriscore: ${p.nutriscore.toUpperCase()}`); if (p.nova_group) notesParts.push(`NOVA: ${p.nova_group}`); if (p.ecoscore) notesParts.push(`Ecoscore: ${p.ecoscore.toUpperCase()}`); if (p.origin) notesParts.push(`Origine: ${p.origin}`); const saveResult = await api('product_save', {}, 'POST', { barcode: match.barcode, name: p.name || match.name, brand: p.brand || match.brand || '', category: p.category || '', image_url: p.image_url || match.image_url || '', unit: detected.unit, default_quantity: detected.quantity, notes: notesParts.join(' Β· '), }); if (saveResult.id) { currentProduct = { id: saveResult.id, barcode: match.barcode, name: p.name || match.name, brand: p.brand || match.brand || '', category: p.category || '', image_url: p.image_url || match.image_url || '', unit: detected.unit, default_quantity: detected.quantity, weight_info: p.quantity_info || '', }; showLoading(false); showProductAction(); return; } } // Fallback: save with basic info from match const saveResult = await api('product_save', {}, 'POST', { barcode: match.barcode, name: match.name, brand: match.brand || '', category: match.category || '', image_url: match.image_url || '', unit: 'pz', default_quantity: 1, }); if (saveResult.id) { currentProduct = { id: saveResult.id, barcode: match.barcode, name: match.name, brand: match.brand || '', category: match.category || '', image_url: match.image_url || '', unit: 'pz', default_quantity: 1 }; showLoading(false); showProductAction(); } else { showLoading(false); showToast('Errore nel salvataggio', 'error'); } } catch (err) { showLoading(false); console.error('AI match select error:', err); showToast('Errore di connessione', 'error'); } } async function saveAIProductDirect() { const id = window._aiIdentified; if (!id) return; showLoading(true); try { const result = await api('product_save', {}, 'POST', { name: id.name, brand: id.brand || '', category: id.category || '', unit: 'pz', default_quantity: 1, }); if (result.success || result.id) { currentProduct = { id: result.id, name: id.name, brand: id.brand || '', category: id.category || '', unit: 'pz', default_quantity: 1 }; showLoading(false); showToast('Prodotto salvato!', 'success'); showProductAction(); } else { showLoading(false); showToast(result.error || 'Errore nel salvataggio', 'error'); } } catch (err) { showLoading(false); showToast('Errore di connessione', 'error'); } } // ===== ALL PRODUCTS ===== async function loadAllProducts() { try { const data = await api('products_list'); renderProductsList(data.products || []); } catch (err) { console.error(err); } } async function searchAllProducts() { const q = document.getElementById('products-search').value; if (q.length < 2) { loadAllProducts(); return; } const data = await api('products_search', { q }); renderProductsList(data.products || []); } function renderProductsList(products) { const container = document.getElementById('products-list'); if (products.length === 0) { container.innerHTML = '
πŸ“¦

Nessun prodotto nel database.
Scansiona un prodotto per iniziare!

'; return; } container.innerHTML = products.map(p => { const catIcon = CATEGORY_ICONS[mapToLocalCategory(p.category, p.name)] || 'πŸ“¦'; return `
${p.image_url ? `` : catIcon}
${escapeHtml(p.name)}
${p.brand ? `
${escapeHtml(p.brand)}
` : ''}
${p.barcode ? `πŸ“Š ${p.barcode}` : ''} ${catIcon} ${p.category || 'Non categorizzato'}
`; }).join(''); } async function selectProductForAction(productId) { showLoading(true); try { const data = await api('product_get', { id: productId }); if (data.product) { currentProduct = data.product; showLoading(false); showProductAction(); } else { showLoading(false); showToast('Prodotto non trovato', 'error'); } } catch (err) { showLoading(false); showToast('Errore', 'error'); } } // ===== SHOPPING LIST (BRING! INTEGRATION) ===== let shoppingListUUID = ''; let shoppingItems = []; let suggestionItems = []; async function loadShoppingList() { const statusEl = document.getElementById('bring-status'); const currentEl = document.getElementById('shopping-current'); const suggestionsEl = document.getElementById('shopping-suggestions'); statusEl.style.display = 'block'; statusEl.innerHTML = '
Connessione a Bring!...
'; currentEl.style.display = 'none'; suggestionsEl.style.display = 'none'; try { const data = await api('bring_list'); statusEl.style.display = 'none'; if (!data.success) { statusEl.style.display = 'block'; statusEl.innerHTML = `
⚠️ ${escapeHtml(data.error || 'Errore connessione Bring!')}
`; return; } shoppingListUUID = data.listUUID; shoppingItems = data.purchase || []; renderShoppingItems(); currentEl.style.display = 'block'; } catch (err) { console.error('Bring! error:', err); statusEl.style.display = 'block'; statusEl.innerHTML = '
⚠️ Errore di connessione a Bring!
'; } } function renderShoppingItems() { const container = document.getElementById('shopping-items'); const countEl = document.getElementById('shopping-count'); countEl.textContent = shoppingItems.length; if (shoppingItems.length === 0) { container.innerHTML = '
βœ…

Lista della spesa vuota!
Usa il pulsante sotto per generare suggerimenti.

'; return; } container.innerHTML = shoppingItems.map((item, idx) => { const catIcon = CATEGORY_ICONS[guessCategoryFromName(item.name)] || 'πŸ›’'; return `
${catIcon}
${escapeHtml(item.name)}
${item.specification ? `
${escapeHtml(item.specification)}
` : ''}
`; }).join(''); } async function removeBringItem(idx) { const item = shoppingItems[idx]; if (!item) return; try { const data = await api('bring_remove', {}, 'POST', { name: item.name, rawName: item.rawName || '', listUUID: shoppingListUUID }); if (data.success) { shoppingItems.splice(idx, 1); renderShoppingItems(); showToast('Rimosso dalla lista', 'success'); } } catch (err) { showToast('Errore nella rimozione', 'error'); } } async function generateSuggestions() { const btn = document.getElementById('btn-suggest'); const suggestionsEl = document.getElementById('shopping-suggestions'); btn.disabled = true; btn.innerHTML = '
Analisi in corso...'; suggestionsEl.style.display = 'none'; try { const data = await api('bring_suggest', {}, 'POST', {}); btn.disabled = false; btn.innerHTML = 'πŸ€– Suggerisci cosa comprare'; if (!data.success) { showToast(data.error || 'Errore nella generazione', 'error'); return; } suggestionItems = (data.suggestions || []).map(s => ({ ...s, selected: true })); // Show seasonal tip const tipEl = document.getElementById('seasonal-tip'); if (data.seasonal_tip) { tipEl.style.display = 'block'; tipEl.innerHTML = `🌿 ${escapeHtml(data.seasonal_tip)}`; } else { tipEl.style.display = 'none'; } renderSuggestions(); suggestionsEl.style.display = 'block'; document.getElementById('suggestion-actions').style.display = 'block'; // Scroll to suggestions suggestionsEl.scrollIntoView({ behavior: 'smooth', block: 'start' }); } catch (err) { btn.disabled = false; btn.innerHTML = 'πŸ€– Suggerisci cosa comprare'; console.error('Suggestion error:', err); showToast('Errore di connessione', 'error'); } } function renderSuggestions() { const container = document.getElementById('suggestion-items'); const priorityOrder = { 'alta': 0, 'media': 1, 'bassa': 2 }; const sorted = [...suggestionItems].sort((a, b) => (priorityOrder[a.priority] || 2) - (priorityOrder[b.priority] || 2)); container.innerHTML = sorted.map((item, idx) => { const catIcon = CATEGORY_ICONS[item.category] || 'πŸ›’'; const priorityBadge = { 'alta': 'Alta', 'media': 'Media', 'bassa': 'Bassa', }[item.priority] || ''; return `
${item.selected ? 'β˜‘οΈ' : '⬜'}
${catIcon}
${escapeHtml(item.name)}${item.specification ? ` (${escapeHtml(item.specification)})` : ''} ${priorityBadge}
${escapeHtml(item.reason)}
`; }).join(''); updateSuggestionActionBtn(); } function toggleSuggestion(idx) { const priorityOrder = { 'alta': 0, 'media': 1, 'bassa': 2 }; const sorted = [...suggestionItems].sort((a, b) => (priorityOrder[a.priority] || 2) - (priorityOrder[b.priority] || 2)); const actualItem = sorted[idx]; // Find in original array const origIdx = suggestionItems.indexOf(actualItem); if (origIdx >= 0) { suggestionItems[origIdx].selected = !suggestionItems[origIdx].selected; } renderSuggestions(); } function updateSuggestionActionBtn() { const selected = suggestionItems.filter(s => s.selected); const btn = document.querySelector('#suggestion-actions .btn-success'); if (btn) { btn.textContent = `βœ… Aggiungi ${selected.length} prodott${selected.length === 1 ? 'o' : 'i'} a Bring!`; btn.disabled = selected.length === 0; } } async function addSelectedSuggestions() { const selected = suggestionItems.filter(s => s.selected); if (selected.length === 0) { showToast('Seleziona almeno un prodotto', 'error'); return; } const btn = document.querySelector('#suggestion-actions .btn-success'); btn.disabled = true; btn.innerHTML = '
Aggiunta in corso...'; try { const items = selected.map(s => ({ name: s.name, specification: s.specification || '', })); const data = await api('bring_add', {}, 'POST', { items, listUUID: shoppingListUUID }); if (data.success) { showToast(`${data.added} prodott${data.added === 1 ? 'o aggiunto' : 'i aggiunti'} a Bring!`, 'success'); // Refresh list await loadShoppingList(); // Clear suggestions document.getElementById('shopping-suggestions').style.display = 'none'; suggestionItems = []; } else { showToast(data.error || 'Errore', 'error'); } } catch (err) { showToast('Errore di connessione', 'error'); } btn.disabled = false; btn.innerHTML = 'βœ… Aggiungi selezionati a Bring!'; } // ===== UTILITY FUNCTIONS ===== // ===== SCAN EXPIRY DATE WITH CAMERA + GEMINI AI ===== let expiryStream = null; async function scanExpiryWithAI() { // Create modal for camera capture document.getElementById('modal-content').innerHTML = `

Inquadra la data di scadenza stampata sul prodotto

`; document.getElementById('modal-overlay').style.display = 'flex'; // Start camera try { expiryStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment', width: { ideal: 1280 }, height: { ideal: 720 } } }); const video = document.getElementById('expiry-video'); video.srcObject = expiryStream; await video.play(); } catch (err) { console.error('Expiry camera error:', err); document.getElementById('expiry-cam-container').innerHTML = `

⚠️ Impossibile accedere alla fotocamera

`; } } function closeExpiryScanner() { if (expiryStream) { expiryStream.getTracks().forEach(t => t.stop()); expiryStream = null; } closeModal(); } function captureExpiry() { const video = document.getElementById('expiry-video'); const canvas = document.getElementById('expiry-canvas'); const img = document.getElementById('expiry-preview-img'); // Crop to center 50% (matching the 2x zoom view) for better AI accuracy const sw = video.videoWidth / 2; const sh = video.videoHeight / 2; const sx = (video.videoWidth - sw) / 2; const sy = (video.videoHeight - sh) / 2; canvas.width = sw; canvas.height = sh; const ctx = canvas.getContext('2d'); ctx.drawImage(video, sx, sy, sw, sh, 0, 0, sw, sh); const dataUrl = canvas.toDataURL('image/jpeg', 0.85); img.src = dataUrl; // Stop camera if (expiryStream) { expiryStream.getTracks().forEach(t => t.stop()); expiryStream = null; } video.srcObject = null; document.getElementById('expiry-cam-container').style.display = 'none'; document.getElementById('expiry-preview-container').style.display = 'block'; document.getElementById('expiry-capture-btn').style.display = 'none'; document.getElementById('expiry-retake-btn').style.display = 'block'; // Auto-analyze analyzeExpiryImage(dataUrl); } function retakeExpiry() { document.getElementById('expiry-cam-container').style.display = 'block'; document.getElementById('expiry-preview-container').style.display = 'none'; document.getElementById('expiry-capture-btn').style.display = 'block'; document.getElementById('expiry-retake-btn').style.display = 'none'; document.getElementById('expiry-scan-status').style.display = 'none'; // Restart camera navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment', width: { ideal: 1280 }, height: { ideal: 720 } } }).then(stream => { expiryStream = stream; const video = document.getElementById('expiry-video'); video.srcObject = stream; video.play(); }).catch(err => console.error(err)); } async function analyzeExpiryImage(dataUrl) { const statusDiv = document.getElementById('expiry-scan-status'); statusDiv.style.display = 'block'; statusDiv.innerHTML = '

πŸ€– Analisi AI in corso...

'; try { // Remove data:image/jpeg;base64, prefix const base64 = dataUrl.split(',')[1]; const result = await api('gemini_expiry', {}, 'POST', { image: base64 }); if (result.success && result.expiry_date) { // Auto-fill the expiry date const expiryInput = document.getElementById('add-expiry'); if (expiryInput) { expiryInput.value = result.expiry_date; } statusDiv.innerHTML = `

βœ… Data trovata: ${formatDate(result.expiry_date)}

`; // Close modal after delay setTimeout(() => closeExpiryScanner(), 1500); } else if (result.error === 'no_api_key') { statusDiv.innerHTML = `

⚠️ Chiave API Gemini non configurata.
Aggiungi GEMINI_API_KEY nel file .env sul server.

`; } else { statusDiv.innerHTML = `

❌ Non riesco a leggere la data. ${result.raw_text ? '
Letto: ' + escapeHtml(result.raw_text) + '' : ''}

`; } } catch (err) { console.error('Expiry AI error:', err); statusDiv.innerHTML = `

❌ Errore di connessione. Riprova.

`; } } function escapeHtml(str) { if (!str) return ''; const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } function formatDate(dateStr) { if (!dateStr) return ''; const d = new Date(dateStr + 'T00:00:00'); return d.toLocaleDateString('it-IT', { day: '2-digit', month: 'short', year: 'numeric' }); } function formatDateTime(dtStr) { if (!dtStr) return ''; const d = new Date(dtStr.replace(' ', 'T')); return d.toLocaleDateString('it-IT', { day: '2-digit', month: 'short' }) + ' ' + d.toLocaleTimeString('it-IT', { hour: '2-digit', minute: '2-digit' }); } function daysUntilExpiry(dateStr) { if (!dateStr) return Infinity; const expiry = new Date(dateStr + 'T00:00:00'); const today = new Date(); today.setHours(0, 0, 0, 0); return Math.round((expiry - today) / 86400000); } function adjustQty(inputId, delta) { const input = document.getElementById(inputId); let val = parseFloat(input.value) || 0; val = Math.max(0.1, val + delta); input.value = Math.round(val * 10) / 10; } function showLoading(show) { document.getElementById('loading').style.display = show ? 'flex' : 'none'; } function showToast(message, type = '') { const toast = document.getElementById('toast'); toast.textContent = message; toast.className = 'toast show ' + type; setTimeout(() => { toast.className = 'toast'; }, 3000); } // ===== RECIPE GENERATION ===== function getMealType() { const hour = new Date().getHours(); if (hour >= 5 && hour < 11) return 'colazione'; if (hour >= 11 && hour < 16) return 'pranzo'; return 'cena'; } const MEAL_LABELS = { 'colazione': 'β˜€οΈ Colazione', 'pranzo': '🍽️ Pranzo', 'cena': 'πŸŒ™ Cena' }; function openRecipeDialog() { const meal = getMealType(); document.getElementById('recipe-meal-title').textContent = MEAL_LABELS[meal] || '🍳 Ricetta'; document.getElementById('recipe-persons').value = 1; document.getElementById('recipe-ask').style.display = ''; document.getElementById('recipe-loading').style.display = 'none'; document.getElementById('recipe-result').style.display = 'none'; document.getElementById('recipe-overlay').style.display = 'flex'; } function closeRecipeDialog() { document.getElementById('recipe-overlay').style.display = 'none'; } function adjustRecipePersons(delta) { const input = document.getElementById('recipe-persons'); let val = parseInt(input.value) || 1; val = Math.max(1, Math.min(20, val + delta)); input.value = val; } async function generateRecipe() { const meal = getMealType(); const persons = parseInt(document.getElementById('recipe-persons').value) || 1; document.getElementById('recipe-ask').style.display = 'none'; document.getElementById('recipe-loading').style.display = ''; document.getElementById('recipe-result').style.display = 'none'; try { const result = await api('generate_recipe', {}, 'POST', { meal, persons }); if (!result.success) { document.getElementById('recipe-loading').style.display = 'none'; document.getElementById('recipe-ask').style.display = ''; if (result.error === 'no_api_key') { showToast('⚠️ Chiave API Gemini non configurata', 'warning'); } else { showToast(result.error || 'Errore nella generazione', 'error'); } return; } const r = result.recipe; let html = `

${r.title}

`; // Meta tags html += '
'; html += `${MEAL_LABELS[r.meal] || r.meal}`; html += `πŸ‘₯ ${r.persons} pers.`; if (r.prep_time) html += `πŸ”ͺ ${r.prep_time}`; if (r.cook_time) html += `πŸ”₯ ${r.cook_time}`; if (r.tags) r.tags.forEach(t => { html += `${t}`; }); html += '
'; // Expiry note if (r.expiry_note) { html += `
⚠️ ${r.expiry_note}
`; } // Ingredients html += '

🧾 Ingredienti

'; // Steps html += '

πŸ‘¨β€πŸ³ Procedimento

    '; (r.steps || []).forEach(step => { // Remove leading "Passo N:" if present const cleanStep = step.replace(/^Passo\s*\d+\s*:\s*/i, ''); html += `
  1. ${cleanStep}
  2. `; }); html += '
'; // Nutrition note if (r.nutrition_note) { html += `

πŸ’‘ ${r.nutrition_note}

`; } document.getElementById('recipe-content').innerHTML = html; document.getElementById('recipe-loading').style.display = 'none'; document.getElementById('recipe-result').style.display = ''; } catch (err) { console.error('Recipe error:', err); document.getElementById('recipe-loading').style.display = 'none'; document.getElementById('recipe-ask').style.display = ''; showToast('Errore di connessione', 'error'); } } // ===== INITIALIZATION ===== document.addEventListener('DOMContentLoaded', () => { showPage('dashboard'); });