Compare commits

...

6 Commits

Author SHA1 Message Date
dadaloop82 ff1175451a release: v1.7.32 2026-05-29 06:54:42 +00:00
dadaloop82 42630c3e3e feat: smarter expiry-to-shopping-list logic
- Extend isExpiringSoon threshold: 3d -> 7d
- Expired items: add isRegular/buyCount>=2 guard so one-off
  expired products don't appear in shopping list (expiry
  banner already covers them)
- Expiring-soon block: require isRegular for 7-day window;
  add 'willExpireBeforeUsed' check (daysLeft > daysToExpiry);
  new reason string 'Scade in Ngg — ricompra' when stock is
  adequate but won't be consumed in time
2026-05-29 06:54:40 +00:00
dadaloop82 637eaa20d6 docs: version badge 1.7.31 2026-05-29 06:48:52 +00:00
dadaloop82 5e307f79b8 docs: update version badge to v1.7.31 2026-05-29 06:48:50 +00:00
dadaloop82 a6478b20e1 release: v1.7.31 2026-05-29 06:46:40 +00:00
dadaloop82 223457bbdf fix: addToInventory creates new row when all existing rows are opened
When adding a new pack of a product that already has an opened row
in inventory (opened_at IS NOT NULL), the previous code merged the
new stock into the opened row, corrupting opened_at tracking and
hiding the second pack from the anomaly model.

Now: search only for sealed rows (opened_at IS NULL) to merge into.
If only opened rows exist, INSERT a new sealed row instead.
2026-05-29 06:46:37 +00:00
3 changed files with 55 additions and 17 deletions
+12
View File
@@ -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.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
- **New pack merges into opened pack on add** — `addToInventory` was looking for ANY existing row for the same product+location and adding the new quantity to it. This caused a newly purchased sealed pack to be silently merged with an already-opened pack, collapsing two physically distinct containers into one row and corrupting the `opened_at` timestamp. The fix now searches only for a **sealed** (unopened) row (`opened_at IS NULL`) to merge into. If only opened rows exist, a new sealed row is created instead — keeping the two packs separate and allowing the anomaly model and shelf-life tracker to work correctly.
## [1.7.30] - 2026-05-29
### Fixed
+1 -1
View File
@@ -25,7 +25,7 @@
[![SQLite](https://img.shields.io/badge/SQLite-3-blue.svg)](https://www.sqlite.org/)
[![Docker](https://img.shields.io/badge/Docker-Ready-2496ED.svg)](Dockerfile)
[![i18n](https://img.shields.io/badge/i18n-IT%20%7C%20EN%20%7C%20DE%20%7C%20FR%20%7C%20ES-orange.svg)](translations/)
[![Version](https://img.shields.io/badge/version-1.7.25-brightgreen.svg)](CHANGELOG.md)
[![Version](https://img.shields.io/badge/version-1.7.31-brightgreen.svg)](CHANGELOG.md)
[![GitHub stars](https://img.shields.io/github/stars/dadaloop82/EverShelf?style=social)](https://github.com/dadaloop82/EverShelf/stargazers)
[![Last commit](https://img.shields.io/github/last-commit/dadaloop82/EverShelf/main)](https://github.com/dadaloop82/EverShelf/commits/main)
[![Contributors](https://img.shields.io/github/contributors/dadaloop82/EverShelf)](https://github.com/dadaloop82/EverShelf/graphs/contributors)
+41 -15
View File
@@ -2634,19 +2634,26 @@ function addToInventory(PDO $db): void {
$vacuumSealed = (int)($input['vacuum_sealed'] ?? 0);
// Check if product already exists in this location
$stmt = $db->prepare("SELECT id, quantity FROM inventory WHERE product_id = ? AND location = ?");
// Check if a SEALED (not yet opened) row exists for this product+location.
// We merge new stock into a sealed row only — never into an already-opened
// pack, because that would conflate two physically distinct containers and
// corrupt the opened_at timestamp tracking.
$stmt = $db->prepare("
SELECT id, quantity FROM inventory
WHERE product_id = ? AND location = ? AND opened_at IS NULL
ORDER BY added_at ASC LIMIT 1
");
$stmt->execute([$productId, $location]);
$existing = $stmt->fetch();
if ($existing) {
// Update quantity
// Merge into the existing sealed row
$newQty = $existing['quantity'] + $quantity;
$stmt = $db->prepare("UPDATE inventory SET quantity = ?, expiry_date = COALESCE(?, expiry_date), vacuum_sealed = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?");
$stmt->execute([$newQty, $expiry, $vacuumSealed, $existing['id']]);
} else {
$newQty = $quantity;
// Insert new inventory entry
// All existing rows (if any) are opened packs — insert a new sealed row
$stmt = $db->prepare("INSERT INTO inventory (product_id, location, quantity, expiry_date, vacuum_sealed) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$productId, $location, $quantity, $expiry, $vacuumSealed]);
}
@@ -8861,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;
@@ -8994,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