Priorità verdura/frutta aperta in ricette + stime corrette

Stime opened (PHP+JS):
- Avocado -> 2d; fragole/banane/pesca/mango -> 2d
- Mela/pera/kiwi/ananas/melone/uva -> 3d
- Zucchina/melanzana/pomodoro/peperone -> 3d (era 7d)
- Broccolo/cavolfiore/sedano/finocchio -> 3d (era 7d)
- Cipolla/cipollotto/scalogno/porro -> 4d (era 7d)
- Carota -> 5d (era 7d); Aglio -> 10d
- Insalata/rucola/spinaci -> 2d (era 4d)

Recipe generator (index.php):
- SQL include i.opened_at
- getItemPriority: rileva opened_at su QUALSIASI unità (non solo conf)
- Elementi [APERTO] con scadenza <=5gg promossi in 'fortemente consigliati'
- Label '📦 PRODOTTI APERTI' aggiornata
- [APERTO] mostrato nel testo ingredienti del prompt AI
- Stesso fix nel contesto chat (opened_at + [APERTO] label)

DB migration: ricalcolate scadenze aperti con nuove stime
This commit is contained in:
dadaloop82
2026-04-09 12:56:10 +00:00
parent d690ad826c
commit 6531765921
3 changed files with 58 additions and 21 deletions
+16 -6
View File
@@ -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;
+26 -9
View File
@@ -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 = [
+16 -6
View File
@@ -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;