Fix: any-token product family grouping + auto timer reset on cache change
Root cause: autoAddCriticalItems used stale in-memory cache (old critical items) and re-added items to Bring right after manual removal, because on_bring was now false but urgency was still 'critical' in the old cache. PHP smartShopping(): - Rename stockByFirstToken → stockByAnyToken (indexes ALL significant tokens) - 'Passata di pomodoro' depleted + 'Polpa di pomodoro' in stock → share token 'pomodoro' → passata no longer flagged as critical (COVERS: passata/polpa/pelato and any future tomato product variant) - Same logic: 'aglio'/'aglio rosso', 'latte'/'latte di montagna', etc. JS loadSmartShopping(): - When critical item set changes (items added OR removed), immediately reset _autoAddedCriticalTs and _bringCleanupTs so next shopping load uses fresh data instead of debounced old data JS cleanupObsoleteBringItems(): - Use any-token matching (like PHP) for both stockByAnyToken and urgentSmartByToken → 'Passata di pomodoro' in Bring, 'polpa' in stock → share 'pomodoro' → removed
This commit is contained in:
+18
-12
@@ -2631,17 +2631,18 @@ function smartShopping(PDO $db): void {
|
||||
}
|
||||
} catch (Exception $e) { /* ignore */ }
|
||||
|
||||
// 4b. Build stockByFirstToken: first-significant-token → total in-stock qty.
|
||||
// Used to skip depleted products that have an equivalent product in stock
|
||||
// (e.g. "Aglio rosso" depleted but "Aglio" has 3 pz → don't re-add Aglio rosso to list)
|
||||
$stockByFirstToken = [];
|
||||
// 4b. Build stockByAnyToken: every significant token of in-stock products → total qty.
|
||||
// Used to skip depleted products covered by any equivalent in-stock product.
|
||||
// Any-token (not just first) groups product families:
|
||||
// 'Passata di pomodoro' + 'Polpa di pomodoro' + 'Pelato Cirio' all share 'pomodoro'
|
||||
// 'Aglio rosso' + 'Aglio' share 'aglio'
|
||||
// 'Latte di Montagna' + 'Latte Parzialmente Scremato' share 'latte'
|
||||
$stockByAnyToken = [];
|
||||
foreach ($products as $pStock) {
|
||||
$qty = isset($inventory[$pStock['id']]) ? (float)$inventory[$pStock['id']]['total_qty'] : 0;
|
||||
if ($qty <= 0) continue;
|
||||
$toks = $nameTokens($pStock['name']);
|
||||
if (!empty($toks)) {
|
||||
$tok = $toks[0];
|
||||
$stockByFirstToken[$tok] = ($stockByFirstToken[$tok] ?? 0) + $qty;
|
||||
foreach ($nameTokens($pStock['name']) as $tok) {
|
||||
$stockByAnyToken[$tok] = ($stockByAnyToken[$tok] ?? 0) + $qty;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2721,11 +2722,16 @@ function smartShopping(PDO $db): void {
|
||||
|
||||
// Out of stock
|
||||
if ($qty <= 0) {
|
||||
// If another product with the same first-token has stock, this depletion is covered
|
||||
// (e.g. "Aglio rosso" depleted but "Aglio" has 3 pz → skip "Aglio rosso")
|
||||
// If ANY significant token of this depleted product also appears in an in-stock product,
|
||||
// the user's need is already covered — skip flagging it.
|
||||
// Examples: 'Passata di pomodoro' depleted, 'Polpa di pomodoro' in stock → share 'pomodoro' → skip
|
||||
// 'Aglio rosso' depleted, 'Aglio' in stock → share 'aglio' → skip
|
||||
$pToks = $nameTokens($p['name']);
|
||||
$pFirst = $pToks[0] ?? null;
|
||||
if ($pFirst && ($stockByFirstToken[$pFirst] ?? 0) > 0) continue;
|
||||
$coveredByEquivalent = false;
|
||||
foreach ($pToks as $tok) {
|
||||
if (($stockByAnyToken[$tok] ?? 0) > 0) { $coveredByEquivalent = true; break; }
|
||||
}
|
||||
if ($coveredByEquivalent) continue;
|
||||
|
||||
if ($isFrequent && $isRecent && $buyCount >= 2) {
|
||||
// Frequently used, recently active, AND bought multiple times → critical
|
||||
|
||||
+33
-20
@@ -5080,42 +5080,43 @@ async function cleanupObsoleteBringItems() {
|
||||
invItems = res.inventory || [];
|
||||
} catch (e) { return; }
|
||||
|
||||
// Build: first-significant-token → total in-stock qty across all products with that token
|
||||
const stockByFirstToken = new Map();
|
||||
// Build: every significant token of in-stock products → total qty
|
||||
// Any-token matching groups product families:
|
||||
// 'Passata di pomodoro' + 'Polpa di pomodoro' share 'pomodoro' → same need
|
||||
const stockByAnyToken = new Map();
|
||||
for (const inv of invItems) {
|
||||
const qty = parseFloat(inv.quantity || 0);
|
||||
if (qty <= 0) continue;
|
||||
const tok = _nameTokens(inv.name || '')[0];
|
||||
if (tok) stockByFirstToken.set(tok, (stockByFirstToken.get(tok) || 0) + qty);
|
||||
for (const tok of _nameTokens(inv.name || '')) {
|
||||
stockByAnyToken.set(tok, (stockByAnyToken.get(tok) || 0) + qty);
|
||||
}
|
||||
}
|
||||
|
||||
// Build: first-significant-token → smart item (critical/high only)
|
||||
// This tells us which tokens still urgently need buying
|
||||
// Build: any matching token → smart item (critical/high only)
|
||||
const urgentSmartByToken = new Map();
|
||||
for (const si of smartShoppingItems) {
|
||||
if (si.urgency !== 'critical' && si.urgency !== 'high') continue;
|
||||
const tok = _nameTokens(si.name)[0];
|
||||
if (tok) urgentSmartByToken.set(tok, si);
|
||||
for (const tok of _nameTokens(si.name)) {
|
||||
if (!urgentSmartByToken.has(tok)) urgentSmartByToken.set(tok, si);
|
||||
}
|
||||
}
|
||||
|
||||
const toRemove = [];
|
||||
for (const item of shoppingItems) {
|
||||
const itemFirst = _nameTokens(item.name)[0];
|
||||
const stockQty = (itemFirst && stockByFirstToken.get(itemFirst)) || 0;
|
||||
// Check if any significant token of this Bring item has stock in inventory
|
||||
const itemTokens = _nameTokens(item.name);
|
||||
const stockQty = itemTokens.reduce((sum, tok) => sum + (stockByAnyToken.get(tok) || 0), 0);
|
||||
|
||||
// No stock of anything similar → nothing to remove
|
||||
// No inventory stock for any related product → nothing to remove
|
||||
if (stockQty <= 0) continue;
|
||||
|
||||
// Check if smart shopping flags something with this token as urgently needed
|
||||
const urgSi = itemFirst && urgentSmartByToken.get(itemFirst);
|
||||
// Check if smart shopping flags something with a matching token as urgently needed
|
||||
const urgSi = itemTokens.map(tok => urgentSmartByToken.get(tok)).find(Boolean);
|
||||
if (urgSi) {
|
||||
// Smart shopping says this type is urgent.
|
||||
// BUT: if the flagged product is COMPLETELY depleted (qty=0) yet we have
|
||||
// a different in-stock product with the same root token (e.g. "Aglio rosso"
|
||||
// depleted but "Aglio" has 3 pz), the user's need is already covered → remove.
|
||||
// If the flagged product still has some qty but is running low → keep (genuine need).
|
||||
if (urgSi.current_qty > 0) continue; // genuinely running low → keep in list
|
||||
// depleted variant but equivalent in stock → fall through to remove
|
||||
// Smart says something with this root token is urgent.
|
||||
// If the flagged product still has qty > 0, it's genuinely running low → keep.
|
||||
// If depleted (qty=0) but we have equivalent stock via another token → remove.
|
||||
if (urgSi.current_qty > 0) continue;
|
||||
}
|
||||
|
||||
toRemove.push(item);
|
||||
@@ -5307,8 +5308,20 @@ async function loadSmartShopping() {
|
||||
try {
|
||||
const data = await api('smart_shopping');
|
||||
if (data.success && data.items && data.items.length > 0) {
|
||||
const prevCriticalNames = new Set(
|
||||
smartShoppingItems.filter(i => i.urgency === 'critical').map(i => i.name)
|
||||
);
|
||||
smartShoppingItems = data.items;
|
||||
_smartShoppingLastFetch = Date.now();
|
||||
// If the set of critical items changed, reset autoAdd/cleanup timers so
|
||||
// they run with fresh data on next shopping page load
|
||||
const newCriticalNames = new Set(data.items.filter(i => i.urgency === 'critical').map(i => i.name));
|
||||
const criticalChanged = [...prevCriticalNames].some(n => !newCriticalNames.has(n)) ||
|
||||
[...newCriticalNames].some(n => !prevCriticalNames.has(n));
|
||||
if (criticalChanged) {
|
||||
localStorage.removeItem('_autoAddedCriticalTs');
|
||||
localStorage.removeItem('_bringCleanupTs');
|
||||
}
|
||||
renderSmartShopping();
|
||||
_renderSmartLastUpdate();
|
||||
_updateSmartUrgencyBadge();
|
||||
|
||||
Reference in New Issue
Block a user