Banner: suppress low-qty alert when sibling product entries exist elsewhere

A partially-used fridge entry (e.g. 191 ml of milk) triggered a
'suspiciously low quantity' banner even when sealed packages of the
same product were present in another location (e.g. pantry).

Fix: before pushing a low-qty review alert, group all inventory rows
by product key (barcode, or name+brand fallback). If any sibling entry
for the same product has qty > 0 in a different row, skip the alert.
High-qty and suspicious package-size alerts are unaffected.
This commit is contained in:
dadaloop82
2026-04-30 05:28:43 +00:00
parent 8359b14931
commit 4e583127dd
3 changed files with 34 additions and 9 deletions
+3
View File
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] - 2026-04-30
### Fixed
- **Low-qty banner false positive** — A "suspiciously low quantity" review alert is now suppressed for a partially-used inventory entry when one or more sibling entries for the same product (identified by barcode, or name+brand as fallback) exist in other locations with stock > 0. Prevents noise like "191 ml of milk" when 11 sealed packages are stored in the pantry.
### Changed
- **Non-alarmist expired banner** — Banner icon, CSS class, and title suffix now adapt to the `getExpiredSafety()` level:
- `ok` (long-life products, freezer within margin): green banner, ✅ icon, "— Scaduto (ancora ok)"
+1
View File
@@ -14,6 +14,7 @@
## 🌍 Recent Updates
- **Smarter low-quantity alerts** — The "suspiciously low quantity" banner is no longer raised for a partially-used entry (e.g. 191 ml of milk in the fridge) when the same product has stock in another location (e.g. 11 sealed packages in the pantry). Sibling entries are detected by barcode or name+brand.
- **Non-alarmist expired banner** — The expired-product banner now adapts its icon, colour, and title to the actual safety level: green ✅ for long-life products that are still safe, amber 👀 for items that should be checked, and the original red 🚫 only for genuinely dangerous items (raw meat, dairy, fish). Low-risk products like canned tomatoes or pasta are no longer shown with a scary red banner.
- Recipe and meal-plan labels now resolve at runtime from translations, preventing raw placeholders like `meal_types.*` and `meal_plan_types.*` from appearing in the UI.
- Recipe generation now receives the selected app language (`it`/`en`/`de`) and enforces localized output in both streaming and non-streaming API flows.
+30 -9
View File
@@ -2726,18 +2726,39 @@ async function loadBannerAlerts() {
});
// 2. Suspicious quantities ("expiring soon" shown only in dashboard sections, not in banner)
// Group items by product identity to detect sibling entries in other locations.
// A "low quantity" alert is suppressed when other stock of the same product exists
// (e.g. 191 ml of milk in the fridge is fine if there are 11 sealed packages in the pantry).
const _productKey = item => item.barcode || `${item.name}||${item.brand || ''}`;
const _productGroups = {};
items.forEach(item => {
const k = _productKey(item);
if (!_productGroups[k]) _productGroups[k] = [];
_productGroups[k].push(item);
});
items.forEach(item => {
if (confirmed[item.id]) return;
if (isSuspiciousQty(item.quantity, item.unit) || isSuspiciousDefaultQty(item.default_quantity, item.unit, item.package_unit)) {
const t_ = QTY_THRESHOLDS[item.unit] || QTY_THRESHOLDS['pz'];
const suspQty = isSuspiciousQty(item.quantity, item.unit);
const suspDq = isSuspiciousDefaultQty(item.default_quantity, item.unit, item.package_unit);
let warning;
if (suspDq && !suspQty) warning = '📦 Conf. sospetta';
else if (parseFloat(item.quantity) < t_.min) warning = '⬇️ Troppo poco';
else warning = '⬆️ Troppo';
_bannerQueue.push({ type: 'review', data: { ...item, warning } });
const t_ = QTY_THRESHOLDS[item.unit] || QTY_THRESHOLDS['pz'];
const qty = parseFloat(item.quantity);
const isLow = !isNaN(qty) && qty > 0 && qty < t_.min;
const isHigh = !isNaN(qty) && qty > t_.max;
const suspDq = isSuspiciousDefaultQty(item.default_quantity, item.unit, item.package_unit);
if (!isLow && !isHigh && !suspDq) return;
// Suppress low-qty warning when sibling entries for the same product exist
// in other locations — the user is simply tracking a partial/opened unit.
if (isLow && !isHigh && !suspDq) {
const siblings = (_productGroups[_productKey(item)] || []).filter(s => s.id !== item.id && parseFloat(s.quantity) > 0);
if (siblings.length > 0) return;
}
let warning;
if (suspDq && !isLow && !isHigh) warning = '📦 Conf. sospetta';
else if (isLow) warning = '⬇️ Troppo poco';
else warning = '⬆️ Troppo';
_bannerQueue.push({ type: 'review', data: { ...item, warning } });
});
// 4. Consumption predictions that don't match actual quantity