diff --git a/CHANGELOG.md b/CHANGELOG.md index e7acbc2..6bb9652 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.29] - 2026-05-29 + +### Added +- **Buy-cycle consumption prediction** — Products that are never tracked per-use (salt, spices, cleaning supplies, etc.) now use the average time between restocks as a proxy for consumption rate. When a product has ≥ 3 purchase events and no individual `out` events, EverShelf calculates the average buy cycle (`(lastBuy - firstBuy) / (buyCount - 1)`) and estimates how many days of stock remain in the current cycle. The product appears in the smart shopping list with a reason like "Finisce tra ~12gg (ciclo medio 75gg)" before it runs out, rather than only after. These products are now also treated as `isRegular` so all stock-level urgency checks apply correctly. + + ## [1.7.28] - 2026-05-30 ### Fixed diff --git a/api/index.php b/api/index.php index eca9b6a..7b4fb77 100644 --- a/api/index.php +++ b/api/index.php @@ -8813,6 +8813,29 @@ function smartShopping(PDO $db): void { $dailyRate = $effectiveDays < 999 && $totalUsed > 0 ? $totalUsed / $effectiveDays : 0; } + // --- Buy-cycle proxy (for products tracked without individual 'out' events) --- + // Products like salt, spices, cleaning products are never logged per-use. + // When the user buys them again it implicitly means the previous pack ran out. + // If we have ≥ 3 buy events and no (or very few) out events, we estimate + // the average cycle duration = (lastIn - firstIn) / (buyCount - 1) and + // project how many days of stock are likely left in the current cycle. + // estimatedDaysLeft = avgCycleDays − daysSinceLastBuy + // This dailyRate proxy is ONLY used when the regular out-based rate is 0. + $buyCycleDays = null; // avg days per buy cycle + $buyCycleDaysLeft = null; // estimated days remaining in current cycle + if ($dailyRate == 0 && $buyCount >= 3 && $firstIn && $lastIn && $lastIn > $firstIn) { + $buyCycleDays = ($lastIn - $firstIn) / 86400 / ($buyCount - 1); + if ($buyCycleDays >= 7) { // ignore implausible < 1-week cycles + $daysSinceLastBuyFloat = ($now - $lastIn) / 86400; + $buyCycleDaysLeft = max(0, $buyCycleDays - $daysSinceLastBuyFloat); + // Derive a synthetic dailyRate so existing daysLeft / pctLeft logic works naturally + // 1 restock event ≈ consuming 1 "average package" over avgCycleDays + if ($qty > 0 && $buyCycleDays > 0) { + $dailyRate = $qty / max(1, $buyCycleDaysLeft > 0 ? $buyCycleDaysLeft : $buyCycleDays); + } + } + } + // Days of stock remaining $daysLeft = ($dailyRate > 0 && $qty > 0) ? $qty / $dailyRate : ($qty > 0 ? 999 : 0); @@ -8853,7 +8876,9 @@ function smartShopping(PDO $db): void { // Is this a frequently used product? (≥ 1.5 uses/month) $isFrequent = $usesPerMonth >= 1.5; // Is it a regular product? (≥ 0.5 uses/month = at least once every 2 months) - $isRegular = $usesPerMonth >= 0.5; + // Also treat buy-cycle products (≥3 buys, no out events) as regular — they are + // by definition products the user buys periodically. + $isRegular = $usesPerMonth >= 0.5 || ($buyCycleDays !== null && $buyCount >= 3); // Is it recently relevant? (used/bought in last 60 days) $isRecent = $daysSinceLastUse <= 60; @@ -8983,11 +9008,24 @@ function smartShopping(PDO $db): void { $daysLeftDisplay = (int)round($daysLeft); $reasons[] = 'Finisce tra ~' . $daysLeftDisplay . 'gg'; if ($daysLeftDisplay <= 3) { - // Running out within 3 days for a frequent product → high urgency $urgency = 'high'; $score += 70; } elseif ($daysLeftDisplay <= 7) { - // Running out within a week → medium + $urgency = 'medium'; + $score += 45; + } else { + $urgency = 'low'; + $score += 25; + } + } + // Buy-cycle prediction for products not tracked per-use (e.g. salt, spices): + // if daily rate was derived from buy cycles and we have < 21 days left → flag. + if ($urgency === 'none' && $buyCycleDays !== null && $dailyRate > 0 + && $daysLeft <= 21 && $isRegular && !$justRestocked) { + $daysLeftDisplay = (int)round($daysLeft); + $cycleDisplay = (int)round($buyCycleDays); + $reasons[] = 'Finisce tra ~' . $daysLeftDisplay . 'gg (ciclo medio ' . $cycleDisplay . 'gg)'; + if ($daysLeftDisplay <= 7) { $urgency = 'medium'; $score += 45; } else {