Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 12c6a8977a | |||
| c7a69d8379 | |||
| c7f3c95d75 | |||
| a6f90a07e5 |
@@ -11,6 +11,18 @@ 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.30] - 2026-05-29
|
||||
|
||||
### Fixed
|
||||
- **False consumption anomaly with multi-row stock** — The anomaly detection banner was evaluating each inventory row in isolation. Products split across multiple rows (e.g. one opened pack with 1 pz + one sealed pack with 6 pz) incorrectly triggered a "consumed faster than expected" warning because only the opened row (1 pz) was compared against the model. The check now aggregates the total quantity across all rows for the same product before deciding to flag an anomaly. If the combined total ≥ expected remaining, the anomaly is suppressed.
|
||||
|
||||
|
||||
## [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
|
||||
|
||||
+59
-3
@@ -4155,6 +4155,24 @@ function getConsumptionPredictions(PDO $db): void {
|
||||
$expectedQty = max(0, $baselineQty - ($dailyRate * $daysSinceRestock));
|
||||
$actualQty = floatval($item['quantity']);
|
||||
|
||||
// Aggregate total stock for this product across ALL inventory rows.
|
||||
// A product may be split into multiple rows (e.g. one opened pack + one
|
||||
// sealed pack at a different location). The opened row alone may look
|
||||
// depleted while the total is healthy — do not flag in that case.
|
||||
$totalQtyStmt = $db->prepare("
|
||||
SELECT COALESCE(SUM(quantity), 0)
|
||||
FROM inventory
|
||||
WHERE product_id = ? AND quantity > 0
|
||||
");
|
||||
$totalQtyStmt->execute([$pid]);
|
||||
$totalQtyAllRows = floatval($totalQtyStmt->fetchColumn() ?: 0);
|
||||
// If the aggregate total is above the expected remaining, the "depletion"
|
||||
// is just stock spread across rows — suppress the anomaly.
|
||||
if ($totalQtyAllRows >= $expectedQty) continue;
|
||||
// Use the aggregate total as the visible actual qty so the banner shows
|
||||
// the real combined stock, not just the single opened row.
|
||||
$actualQty = $totalQtyAllRows;
|
||||
|
||||
// Need at least some post-restock usage observations before warning.
|
||||
if ($txSinceRestock < 2) continue;
|
||||
|
||||
@@ -8813,6 +8831,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 +8894,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 +9026,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 {
|
||||
|
||||
Reference in New Issue
Block a user