From b4b68d657918893eaa248768f6973dd36ffd0300 Mon Sep 17 00:00:00 2001 From: dadaloop82 Date: Mon, 16 Mar 2026 14:59:55 +0000 Subject: [PATCH] 20260316i: Low-stock Bring prompt after use - API returns total_remaining, product_name, unit, default_qty after use - isLowStock() detects when inventory is running low (pz<=2, conf<=1, weight/vol<=25% of default) - After using a product, if low stock detected, shows prompt asking to add to Bring shopping list - Works in main USE form, USE ALL, and recipe ingredient USE - Prompt chains properly: low-stock then move-after-use (if applicable) - Skips prompt when product is fully depleted (already auto-added to Bring) --- api/index.php | 19 ++++++++- assets/js/app.js | 104 ++++++++++++++++++++++++++++++++++++++++++----- data/dispensa.db | Bin 184320 -> 184320 bytes index.html | 4 +- 4 files changed, 114 insertions(+), 13 deletions(-) diff --git a/api/index.php b/api/index.php index 427423f..a4c3351 100644 --- a/api/index.php +++ b/api/index.php @@ -764,7 +764,24 @@ function useFromInventory(PDO $db): void { } } - $response = ['success' => true, 'remaining' => $remaining, 'added_to_bring' => $addedToBring]; + // Calculate total remaining across ALL locations + $stmt = $db->prepare("SELECT SUM(quantity) as total FROM inventory WHERE product_id = ? AND quantity > 0"); + $stmt->execute([$productId]); + $totalRemaining = round((float)($stmt->fetchColumn() ?: 0), 6); + + // Get product info for low-stock prompt + $stmt = $db->prepare("SELECT name, unit, default_quantity, package_unit FROM products WHERE id = ?"); + $stmt->execute([$productId]); + $prodInfo = $stmt->fetch(); + + $response = ['success' => true, 'remaining' => $remaining, 'added_to_bring' => $addedToBring, + 'total_remaining' => $totalRemaining]; + if ($prodInfo) { + $response['product_name'] = $prodInfo['name']; + $response['product_unit'] = $prodInfo['unit']; + $response['product_default_qty'] = (float)($prodInfo['default_quantity'] ?: 0); + $response['product_package_unit'] = $prodInfo['package_unit'] ?: ''; + } if ($openedId) $response['opened_id'] = $openedId; echo json_encode($response); } diff --git a/assets/js/app.js b/assets/js/app.js index 159409b..06a77e4 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -3482,6 +3482,88 @@ function selectUseLocation(btn, loc) { document.getElementById('use-location').value = loc; } +// ===== LOW STOCK → BRING! PROMPT ===== +function isLowStock(totalRemaining, unit, defaultQty) { + if (totalRemaining <= 0) return false; // already fully depleted → auto-added + if (unit === 'pz') return totalRemaining <= 2; + if (unit === 'conf') return totalRemaining <= 1; + // Weight/volume: use percentage of default_qty or fixed threshold + if (defaultQty > 0) return totalRemaining <= defaultQty * 0.25; + // Fallback fixed thresholds + if (unit === 'g' || unit === 'ml') return totalRemaining <= 100; + if (unit === 'kg' || unit === 'l') return totalRemaining <= 0.15; + return false; +} + +function showLowStockBringPrompt(result, afterCallback) { + const name = result.product_name || currentProduct?.name || ''; + const unit = result.product_unit || currentProduct?.unit || 'pz'; + const defaultQty = result.product_default_qty || parseFloat(currentProduct?.default_quantity) || 0; + const totalRemaining = result.total_remaining; + + if (!isLowStock(totalRemaining, unit, defaultQty)) { + if (afterCallback) afterCallback(); + return; + } + + // Format remaining for display + let remainLabel = ''; + if (unit === 'conf' && result.product_package_unit) { + const subTotal = Math.round(totalRemaining * defaultQty); + remainLabel = `${subTotal}${result.product_package_unit}`; + } else { + const unitLabels = { pz: 'pz', g: 'g', kg: 'kg', ml: 'ml', l: 'L', conf: 'conf' }; + remainLabel = `${Number.isInteger(totalRemaining) ? totalRemaining : totalRemaining.toFixed(1)} ${unitLabels[unit] || unit}`; + } + + // Store callback for after user responds + window._lowStockAfterCallback = afterCallback; + + document.getElementById('modal-content').innerHTML = ` + +
+

${escapeHtml(name)} sta per finire — rimangono solo ${remainLabel}.

+

Vuoi aggiungerlo alla lista della spesa?

+ + +
+ `; + document.getElementById('modal-overlay').style.display = 'flex'; +} + +async function addLowStockToBring(productName) { + closeModal(); + try { + const payload = { items: [{ name: productName }] }; + if (shoppingListUUID) payload.listUUID = shoppingListUUID; + const data = await api('bring_add', {}, 'POST', payload); + if (data.success && data.added > 0) { + showToast('🛒 Aggiunto alla lista della spesa!', 'success'); + } else if (data.success && data.skipped > 0) { + showToast('ℹ️ Già nella lista della spesa', 'info'); + } + } catch (e) { + showToast('Errore nell\'aggiunta a Bring!', 'error'); + } + const cb = window._lowStockAfterCallback; + window._lowStockAfterCallback = null; + if (cb) cb(); +} + +function closeLowStockPrompt() { + closeModal(); + const cb = window._lowStockAfterCallback; + window._lowStockAfterCallback = null; + if (cb) cb(); +} + function showMoveAfterUseModal(product, fromLoc, remaining, openedId) { const otherLocs = Object.entries(LOCATIONS).filter(([k]) => k !== fromLoc); const locButtons = otherLocs.map(([k, v]) => @@ -3555,7 +3637,8 @@ async function submitUseAll() { if (result.added_to_bring) { setTimeout(() => showToast('🛒 Prodotto finito → aggiunto a Bring!', 'info'), 1500); } - showPage('dashboard'); + // Check low stock (product may exist at other locations) + showLowStockBringPrompt(result, () => showPage('dashboard')); } else { showToast(result.error || 'Errore', 'error'); } @@ -3595,11 +3678,11 @@ async function submitUse(e) { } // If there's remaining quantity, offer to move to another location const usedFrom = document.getElementById('use-location').value; - if (result.remaining > 0) { - showMoveAfterUseModal(currentProduct, usedFrom, result.remaining, result.opened_id); - } else { - showPage('dashboard'); - } + const moveCallback = result.remaining > 0 + ? () => showMoveAfterUseModal(currentProduct, usedFrom, result.remaining, result.opened_id) + : () => showPage('dashboard'); + // Check low stock → Bring! prompt + showLowStockBringPrompt(result, moveCallback); } else { showToast(result.error || 'Errore', 'error'); } @@ -5149,10 +5232,11 @@ async function submitRecipeUse(useAll) { setTimeout(() => showToast('🛒 Prodotto finito → aggiunto a Bring!', 'info'), 1500); } - // Offer to move opened package (stays on recipe page, modal over recipe overlay) - if (result.remaining > 0) { - setTimeout(() => showRecipeMoveModal(productId, location, result.remaining, result.opened_id), 400); - } + // Check low stock → Bring! prompt, then offer move + const moveCallback = result.remaining > 0 + ? () => setTimeout(() => showRecipeMoveModal(productId, location, result.remaining, result.opened_id), 300) + : null; + setTimeout(() => showLowStockBringPrompt(result, moveCallback), 300); } else { btn.disabled = false; btn.textContent = '📦 Usa'; diff --git a/data/dispensa.db b/data/dispensa.db index fd857527adc72c0b113014576c5003ae24d5425f..86a1119ec77a685093939fb80fa17fa82dcce9dc 100644 GIT binary patch delta 941 zcmZvaT}TvB6vyw!&exrtnYA7iVomX*789MF-AxC@gd#GcP0J(<`mkgvHQLGu&0WGM zNGM0!LW->ixdwt|#o^~|U2H(YIL-Ty5a2qfH z06TWVhCuM#spjBuCx_GF>GOfcmLhHX)=NQj0l`3$C-$Jr8-R9gGEqQzi&f9|7+zXo zJK#59?Bn!SQw=5@ZEFC_^wNX`%!_l4`{ znz#b?EBlyjWOJC$OdnIrY@o;KoAgc^P;aOZwVSfrKG?c!RW=Hb;x??J1R6q3$OZb~ zQQP@ZmgoU}jEZL)aOH{8?S!7Dclw_+ju;?y!`wELoBUeSZ z*=dk&)#EX4W%C-@YLPIbcM#^2BvGom<=#)Q(`Nj$QVVnCdOw8YtUg5G3#>mAVf`Zn z9D3R@_*1&$7ludSiVu!N{v_5a_*9QuFIs}}I^2ofI^#zW5_}i8;|9DJyN!irp~zSb zk~&tsgP9Dhprl=;P+o1y;{((G3vSg~35cf(9@9FQT1PM)fyoqsHRqv}fC=4jQ??D4 zQ*#j}gj($M6Z#5WO>Z%_GiTVXqx?DH!z_SlT)LnH1I^adQtWz_+;FWyZM3MCix<|% zJie{rLy@WlnAu@X-0CMUe9+811wt)G~0EBtIdMQ aDJZhRf#~Yf|Hn3)d=W~C{{A|W_xuH?`0?@p delta 4202 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 diff --git a/index.html b/index.html index d46c342..53fbdce 100644 --- a/index.html +++ b/index.html @@ -9,7 +9,7 @@ Dispensa Manager - + @@ -911,6 +911,6 @@ - +