feat: shopping list pantry hints, barcode multi-API fallback (OPF/beauty/Gemini), README disclaimer

- Shopping list: each item now shows 'Hai già Xg in dispensa' for same-family inventory stock
  - Lazy-loads inventory once per shopping page visit (_getShoppingInventoryCache)
  - Matches by first significant token (same logic as related-stock on action page)
  - Green hint below item badges, dark-mode aware (.shopping-pantry-hint)
- Barcode lookup: added Open Products Facts + Open Beauty Facts as step 3;
  Gemini AI (_barcodeLookupGemini) as final step 4 fallback
- Added stockForName PHP endpoint (stock_for_name action) for future use
- Restored missing function signatures for _offFetchProduct() and saveProduct()
  that were accidentally lost when stockForName was added in a previous session
- Translation: added shopping.pantry_hint in it/en/de
This commit is contained in:
dadaloop82
2026-05-23 09:53:17 +00:00
parent 561c6e9809
commit 6a41b53174
6 changed files with 236 additions and 7 deletions
+51 -1
View File
@@ -3669,7 +3669,10 @@ function showPage(pageId, param = null) {
}
break;
case 'products': loadAllProducts(); break;
case 'shopping': loadShoppingList(); break;
case 'shopping':
_shoppingInventoryCache = null; // invalidate so hints use fresh data
loadShoppingList();
break;
case 'recipe': loadRecipeArchive(); break;
case 'log': loadLog(); break;
case 'ai': initAICamera(); break;
@@ -10155,6 +10158,20 @@ let shoppingItems = [];
let suggestionItems = [];
let _spesaScanTarget = null; // { name, rawName, idx } when tapping item to scan
// Inventory cache for "already at home" hints in the shopping list.
// Loaded once per shopping page visit and reused for all item hints.
let _shoppingInventoryCache = null;
async function _getShoppingInventoryCache() {
if (_shoppingInventoryCache !== null) return _shoppingInventoryCache;
try {
const data = await api('inventory_list');
_shoppingInventoryCache = data.inventory || [];
} catch(e) {
_shoppingInventoryCache = [];
}
return _shoppingInventoryCache;
}
// ===== SHOPPING TABS =====
function switchShoppingTab(tab) {
document.querySelectorAll('.shopping-tab').forEach(b => b.classList.remove('active'));
@@ -11594,6 +11611,39 @@ async function renderShoppingItems() {
container.innerHTML = html;
// ── PANTRY HINTS: show "already at home: X" for each shopping item ──────
// Load inventory once, then decorate all items asynchronously.
_getShoppingInventoryCache().then(invItems => {
for (const { item, idx } of enriched) {
const firstTok = (_nameTokens(item.name)[0] || '').toLowerCase();
if (!firstTok) continue;
const matches = invItems.filter(i => {
const iFirst = (_nameTokens(i.name || '')[0] || '').toLowerCase();
return iFirst === firstTok && parseFloat(i.quantity) > 0;
});
if (matches.length === 0) continue;
// Group by unit and sum
const byUnit = {};
for (const m of matches) {
const u = m.unit || 'pz';
byUnit[u] = (byUnit[u] || 0) + parseFloat(m.quantity);
}
const hintText = Object.entries(byUnit)
.map(([u, q]) => `${Math.round(q * 10) / 10} ${u}`)
.join(', ');
const itemEl = document.getElementById(`shop-item-${idx}`);
if (!itemEl) continue;
const infoEl = itemEl.querySelector('.shopping-item-info');
if (!infoEl) continue;
// Don't duplicate
if (infoEl.querySelector('.shopping-pantry-hint')) continue;
const hintEl = document.createElement('div');
hintEl.className = 'shopping-pantry-hint';
hintEl.textContent = t('shopping.pantry_hint').replace('{qty}', hintText);
infoEl.appendChild(hintEl);
}
});
// Trigger async price loading if enabled
const s2 = getSettings();
if (s2.price_enabled && shoppingItems.length > 0) {