diff --git a/CHANGELOG.md b/CHANGELOG.md index 42b96d5..a06df13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Recipe scraps tips** — During cooking steps, detect "waste" generated (peels, cores, bones, eggshells, coffee grounds, citrus zest, etc.) and surface AI-powered tips on how to reuse them (compost, natural cleaner, broth, candied peel, etc.). Could be shown as an optional collapsible hint card below the step that generates the scrap. +## [1.7.32] - 2026-05-29 + +### Changed +- **Smarter expiry u2192 shopping list logic** — The "expiring soon" threshold is now 7 days (was 3), giving enough time to plan the next shopping trip. Items expiring soon are only flagged for restocking when the user is a **regular buyer** (`isRegular`) and either stock is low (<50%) or the consumption rate predicts the item will expire before being used. Non-regular products keep the old 3-day safety-net. Expired items are now only added to the shopping list when `isRegular || buyCount >= 2` — products that expired unused without ever being a staple no longer pollute the list; the expiry banner handles them. + + ## [1.7.31] - 2026-05-29 ### Fixed diff --git a/api/index.php b/api/index.php index 0b1c43c..e80aae0 100644 --- a/api/index.php +++ b/api/index.php @@ -8868,7 +8868,9 @@ function smartShopping(PDO $db): void { $expiryDate = $inv ? $inv['nearest_expiry'] : null; $daysToExpiry = $expiryDate ? (strtotime($expiryDate) - $now) / 86400 : 999; $isExpired = $daysToExpiry < 0; - $isExpiringSoon = !$isExpired && $daysToExpiry <= 3; + // 7-day warning window: enough to plan the next shopping trip. + // The tighter 3-day threshold was often too late for staple products. + $isExpiringSoon = !$isExpired && $daysToExpiry <= 7; // Fresh (non-expired) quantity — used for suppression when only part of stock is expired $freshQty = $inv ? (float)($inv['fresh_qty'] ?? $qty) : 0; @@ -9001,31 +9003,48 @@ function smartShopping(PDO $db): void { } } - // Expiring soon or expired (needs replacement) — valid regardless of frequency + // Expiring soon or expired (needs replacement) if ($isExpired && $qty > 0) { // Check if the product's shopping_name FAMILY has adequate FRESH stock // from other (non-expired) products. If so, no need to buy more. $sNameKey = strtolower(trim($p['shopping_name'] ?? '')); $familyFreshQty = $sNameKey !== '' ? ($freshStockByShoppingName[$sNameKey] ?? 0) : 0; - // Subtract this product's own qty (it is expired, so fresh_qty=0 for it anyway) $refQtyLocal = $refQty > 0 ? $refQty : 1; $familyFreshPct = min(200, ($familyFreshQty / $refQtyLocal) * 100); if (($justRestocked && $freshPctLeft >= 50) || $familyFreshPct >= 50) { // Fresh stock from this product or same-family products is adequate. // The expired batch will show in the dashboard expiry banner — don't add to shopping list. - } else { + } elseif ($isRegular || $buyCount >= 2) { + // Only suggest restocking if this is a product the user buys regularly. + // If it expired without ever being a staple, the expiry banner is enough. $urgency = 'critical'; $reasons[] = 'Scaduto!'; $score += 90; } - } elseif ($isExpiringSoon && $qty > 0 && $pctLeft < 50) { - // Only flag "expiring soon" if stock is also low (<50%). If you have plenty of - // stock (e.g. just bought fresh produce that naturally expires in 3 days), the - // shopping list is not the right place — the expiry banner handles it. - if ($urgency === 'none') $urgency = 'medium'; - $reasons[] = 'Scade tra ' . max(0, round($daysToExpiry)) . 'gg'; - $score += 40; + // else: one-off product expired unused → expiry banner handles it, no shopping noise + } elseif ($isExpiringSoon && $qty > 0) { + // Flag if: + // (a) regular consumer + stock low (<50%) → needs restock soon + // (b) regular consumer + will expire before finishing it + // (daysLeft based on consumption rate > days to expiry) + // (c) non-regular + within 3 days + low stock → minimal safety net + $willExpireBeforeUsed = $dailyRate > 0 && $daysToExpiry < $daysLeft; + if ($isRegular && ($pctLeft < 50 || $willExpireBeforeUsed)) { + if ($urgency === 'none') $urgency = 'medium'; + if ($willExpireBeforeUsed && $pctLeft >= 50) { + // Has stock but won't finish it in time → buy fresh and use this one now + $reasons[] = 'Scade in ' . max(1, round($daysToExpiry)) . 'gg — ricompra'; + } else { + $reasons[] = 'Scade in ' . max(1, round($daysToExpiry)) . 'gg'; + } + $score += 40; + } elseif (!$isRegular && $daysToExpiry <= 3 && $pctLeft < 50) { + // Non-regular product: only flag when very close and running low + if ($urgency === 'none') $urgency = 'low'; + $reasons[] = 'Scade in ' . max(1, round($daysToExpiry)) . 'gg'; + $score += 20; + } } // Frequently used but stock getting low (predictive) — scale urgency by imminence