diff --git a/api/database.php b/api/database.php index 016a9d5..96377bb 100644 --- a/api/database.php +++ b/api/database.php @@ -273,15 +273,25 @@ function estimateOpenedExpiryDaysPHP(string $name, string $category, string $loc if (preg_match('/\b(pollo|tacchino|maiale|manzo|vitello|agnello)\b/', $n)) return 2; if (preg_match('/salmone|tonno\s+fresco|pesce(?!\s+in)/', $n)) return 2; if (preg_match('/\b(passata|pelati|polpa|sugo|salsa\s+di\s+pomodoro)\b/', $n)) return 5; - if (preg_match('/insalata|rucola|spinaci|lattuga/', $n)) return 4; - if (preg_match('/\b(succo|spremuta)\b/', $n)) return 5; - if (preg_match('/\blimone\b/', $n)) return 14; + if (preg_match('/insalata|rucola|spinaci|lattuga|crescione|germogli/', $n)) return 2; + if (preg_match('/\b(succo|spremuta)\b/', $n)) return 3; if (preg_match('/\b(birra|beer)\b/', $n)) return 3; if (preg_match('/\bvino\b/', $n)) return 5; if (preg_match('/tonno\s+in\s+scatola|tonno\s+rio|sgombro\s+in/', $n)) return 4; - if (preg_match('/\b(banana|banane|mela|pera|pesca|albicocca|ciliegia|uva|fragola|lampone|kiwi)\b/', $n)) return 10; - if (preg_match('/\b(arancia|mandarino|pompelmo|clementina)\b/', $n)) return 14; - if (preg_match('/\b(carota|zucchina|peperone|melanzana|broccolo|cavolfiore|sedano|finocchio)\b/', $n)) return 7; + // Fruit opened/cut in fridge — much shorter than sealed + if (preg_match('/\bavocado\b/', $n)) return 2; + if (preg_match('/\b(banana|banane|fragola|lampone|pesca|albicocca|ciliegia|mango|papaya)\b/', $n)) return 2; + if (preg_match('/\b(mela|pera|nettarina|prugna|kiwi|ananas|uva|melone|anguria)\b/', $n)) return 3; + if (preg_match('/\b(arancia|mandarino|pompelmo|clementina|limone)\b/', $n)) return 3; // cut citrus + // Vegetables opened/cut in fridge + if (preg_match('/\b(zucchina|zucchine|melanzana|pomodor)\b/', $n)) return 3; + if (preg_match('/\b(peperone|peperoni)\b/', $n)) return 3; + if (preg_match('/\b(broccolo|broccoli|cavolfiore|cavolo)\b/', $n)) return 3; + if (preg_match('/\bsedano\b|\bfinocchio\b/', $n)) return 3; + if (preg_match('/\b(cipolla|cipolle|cipollotto|scalogno|porro)\b/', $n)) return 4; + if (preg_match('/\b(carota|carote)\b/', $n)) return 5; + if (preg_match('/\b(patata|patate|tubero)\b/', $n)) return 3; // cooked/cut potato + if (preg_match('/\baglio\b/', $n)) return 10; // ── G: Fridge condiments — medium shelf-life ───────────────────────── if (preg_match('/maionese|mayo|mayon/', $n)) return 90; diff --git a/api/index.php b/api/index.php index 6407e9d..3fb9a8d 100644 --- a/api/index.php +++ b/api/index.php @@ -1364,7 +1364,7 @@ function geminiChat(PDO $db): void { // Fetch inventory context $stmt = $db->query(" - SELECT p.name, p.brand, p.category, i.quantity, p.unit, p.default_quantity, p.package_unit, i.location, i.expiry_date, + SELECT p.name, p.brand, p.category, i.quantity, p.unit, p.default_quantity, p.package_unit, i.location, i.expiry_date, i.opened_at, CASE WHEN i.expiry_date IS NOT NULL THEN julianday(i.expiry_date) - julianday('now') ELSE 999 END AS days_left FROM inventory i JOIN products p ON p.id = i.product_id @@ -1381,6 +1381,9 @@ function geminiChat(PDO $db): void { if ($item['unit'] === 'conf' && !empty($item['package_unit']) && $item['default_quantity'] > 0) { $line .= " (da {$item['default_quantity']} {$item['package_unit']} ciascuna)"; } + $isOpen = !empty($item['opened_at']) || + (floatval($item['quantity']) > 0 && floatval($item['quantity']) < 1 && $item['unit'] === 'conf'); + if ($isOpen) $line .= ' [APERTO]'; if ($item['expiry_date']) { $daysLeft = intval($item['days_left']); if ($daysLeft < 0) { @@ -1527,7 +1530,7 @@ function generateRecipe(PDO $db): void { // Fetch all inventory items with expiry info $stmt = $db->query(" - SELECT p.id AS product_id, p.name, p.brand, p.category, i.quantity, p.unit, p.default_quantity, p.package_unit, i.location, i.expiry_date, + SELECT p.id AS product_id, p.name, p.brand, p.category, i.quantity, p.unit, p.default_quantity, p.package_unit, i.location, i.expiry_date, i.opened_at, CASE WHEN i.expiry_date IS NOT NULL THEN julianday(i.expiry_date) - julianday('now') ELSE 999 END AS days_left FROM inventory i JOIN products p ON p.id = i.product_id @@ -1542,10 +1545,14 @@ function generateRecipe(PDO $db): void { } // Helper to compute priority group for an item: - // 1=scaduto, 2=scadenza imminente ≤3gg, 3=scadenza ravvicinata ≤7gg, 4=scadenza lontana, 5=confezione aperta, 6=chiuso + // 1=scaduto, 2=scadenza imminente ≤3gg, 3=scadenza ravvicinata ≤7gg, + // 4=scadenza lontana, 5=aperto (opened_at set o conf parziale), 6=chiuso $getItemPriority = function($item) { $daysLeft = floatval($item['days_left']); - $isOpen = (floatval($item['quantity']) > 0 && floatval($item['quantity']) < 1 && $item['unit'] === 'conf'); + // "Aperto" = opened_at è impostato (frutta/verdura/qualsiasi cosa usata parzialmente) + // OPPURE confezione parzialmente usata (qty < 1 conf) + $isOpen = !empty($item['opened_at']) || + (floatval($item['quantity']) > 0 && floatval($item['quantity']) < 1 && $item['unit'] === 'conf'); if (!empty($item['expiry_date']) && $daysLeft < 0) return 1; if (!empty($item['expiry_date']) && $daysLeft <= 3) return 2; if (!empty($item['expiry_date']) && $daysLeft <= 7) return 3; @@ -1568,7 +1575,7 @@ function generateRecipe(PDO $db): void { 2 => '🔴 SCADENZA IMMINENTE (entro 3 giorni - USA PER PRIMI)', 3 => '🟠 SCADENZA RAVVICINATA (entro 7 giorni)', 4 => '🟡 ALTRI PRODOTTI CON SCADENZA', - 5 => '📦 CONFEZIONI APERTE (da consumare prima dei chiusi)', + 5 => '📦 PRODOTTI APERTI (già aperti/tagliati — da consumare prima delle confezioni chiuse)', 6 => '🟢 ALTRI PRODOTTI', ]; $priorityGroups = []; @@ -1591,8 +1598,10 @@ function generateRecipe(PDO $db): void { $line .= " [FRIGO]"; } $qty = floatval($item['quantity']); - if ($qty > 0 && $qty < 1 && $item['unit'] === 'conf') { - $line .= " [APERTO]"; + $isOpen = !empty($item['opened_at']) || + ($qty > 0 && $qty < 1 && $item['unit'] === 'conf'); + if ($isOpen) { + $line .= ' [APERTO]'; } $line .= " (in {$item['location']})"; @@ -1619,11 +1628,19 @@ function generateRecipe(PDO $db): void { foreach ($items as $item) { $g = $getItemPriority($item); $daysLeft = floatval($item['days_left']); - $label = $item['name'] . ($item['brand'] ? " ({$item['brand']})" : '') . " — scade: {$item['expiry_date']}"; + $isOpen = !empty($item['opened_at']) || + (floatval($item['quantity']) > 0 && floatval($item['quantity']) < 1 && $item['unit'] === 'conf'); + $expiryNote = !empty($item['expiry_date']) ? " — scade: {$item['expiry_date']}" : ''; + $openNote = $isOpen ? ' [APERTO]' : ''; + $label = $item['name'] . ($item['brand'] ? " ({$item['brand']})" : '') . $openNote . $expiryNote; if ($g === 1 || ($g === 2 && $daysLeft <= 1)) { $mandatoryItems[] = $label; } elseif ($g === 2) { $recommendedItems[] = $label; + } elseif ($isOpen && $daysLeft <= 5 && $daysLeft >= 0) { + // Opened items expiring within 5 days but not already in mandatory/recommended + // → bump to "strongly recommended" so the AI knows to use them + $recommendedItems[] = $label; } } @@ -1632,7 +1649,7 @@ function generateRecipe(PDO $db): void { $mustUseText .= "\n\n⚠️⚠️⚠️ INGREDIENTI OBBLIGATORI (SCADUTI O IN SCADENZA OGGI/DOMANI) ⚠️⚠️⚠️\nLa ricetta DEVE usare ALMENO uno (meglio se tutti) di questi ingredienti come ingrediente PRINCIPALE. Non sono opzionali!\n" . implode("\n", array_map(fn($n) => "→ $n", $mandatoryItems)); } if (!empty($recommendedItems)) { - $mustUseText .= "\n\n🔶 INGREDIENTI FORTEMENTE CONSIGLIATI (scadono tra 2-3 giorni)\nCerca di includerli se la ricetta lo permette, ma non è obbligatorio se non si abbinano bene:\n" . implode("\n", array_map(fn($n) => "· $n", $recommendedItems)); + $mustUseText .= "\n\n🔶 INGREDIENTI FORTEMENTE CONSIGLIATI (aperti e/o in scadenza a breve)\nSono già aperti e/o scadono presto — includi più di questi possibile nella ricetta:\n" . implode("\n", array_map(fn($n) => "· $n", $recommendedItems)); } $mealLabels = [ diff --git a/assets/js/app.js b/assets/js/app.js index cd69c90..d58f1bc 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -447,15 +447,25 @@ function estimateOpenedExpiryDays(product, location) { if (/\b(pollo|tacchino|maiale|manzo|vitello|agnello)\b/.test(name)) return 2; if (/salmone|tonno\s+fresco|pesce(?!\s+in)/.test(name)) return 2; if (/\b(passata|pelati|polpa|sugo|salsa\s+di\s+pomodoro)\b/.test(name)) return 5; - if (/insalata|rucola|spinaci|lattuga/.test(name)) return 4; - if (/\b(succo|spremuta)\b/.test(name)) return 5; - if (/\blimone\b/.test(name)) return 14; + if (/insalata|rucola|spinaci|lattuga|crescione|germogli/.test(name)) return 2; + if (/\b(succo|spremuta)\b/.test(name)) return 3; if (/\b(birra|beer)\b/.test(name)) return 3; if (/\bvino\b/.test(name)) return 5; if (/tonno\s+in\s+scatola|tonno\s+rio|sgombro\s+in/.test(name)) return 4; - if (/\b(banana|banane|mela|pera|pesca|albicocca|ciliegia|uva|fragola|lampone|kiwi)\b/.test(name)) return 10; - if (/\b(arancia|mandarino|pompelmo|clementina)\b/.test(name)) return 14; - if (/\b(carota|zucchina|peperone|melanzana|broccolo|cavolfiore|sedano|finocchio)\b/.test(name)) return 7; + // Fruit opened/cut in fridge + if (/\bavocado\b/.test(name)) return 2; + if (/\b(banana|banane|fragola|lampone|pesca|albicocca|ciliegia|mango|papaya)\b/.test(name)) return 2; + if (/\b(mela|pera|nettarina|prugna|kiwi|ananas|uva|melone|anguria)\b/.test(name)) return 3; + if (/\b(arancia|mandarino|pompelmo|clementina|limone)\b/.test(name)) return 3; + // Vegetables opened/cut in fridge + if (/\b(zucchina|zucchine|melanzana|pomodor)\b/.test(name)) return 3; + if (/\b(peperone|peperoni)\b/.test(name)) return 3; + if (/\b(broccolo|broccoli|cavolfiore|cavolo)\b/.test(name)) return 3; + if (/\bsedano\b|\bfinocchio\b/.test(name)) return 3; + if (/\b(cipolla|cipolle|cipollotto|scalogno|porro)\b/.test(name)) return 4; + if (/\b(carota|carote)\b/.test(name)) return 5; + if (/\b(patata|patate|tubero)\b/.test(name)) return 3; + if (/\baglio\b/.test(name)) return 10; // ── G: Fridge condiments ───────────────────────────────────────────── if (/maionese|mayo|mayon/.test(name)) return 90;