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)
This commit is contained in:
+18
-1
@@ -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);
|
||||
}
|
||||
|
||||
+94
-10
@@ -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 = `
|
||||
<div class="modal-header">
|
||||
<h3>⚠️ Sta per finire!</h3>
|
||||
<button class="modal-close" onclick="closeLowStockPrompt()">✕</button>
|
||||
</div>
|
||||
<div style="padding:0 16px 16px">
|
||||
<p style="margin-bottom:12px"><strong>${escapeHtml(name)}</strong> sta per finire — rimangono solo <strong>${remainLabel}</strong>.</p>
|
||||
<p style="margin-bottom:16px">Vuoi aggiungerlo alla lista della spesa?</p>
|
||||
<button type="button" class="btn btn-large btn-success full-width" onclick="addLowStockToBring('${escapeHtml(name).replace(/'/g, "\\'")}')">
|
||||
🛒 Sì, aggiungi a Bring!
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary full-width" style="margin-top:8px" onclick="closeLowStockPrompt()">
|
||||
No, per ora va bene
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
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';
|
||||
|
||||
Binary file not shown.
+2
-2
@@ -9,7 +9,7 @@
|
||||
<title>Dispensa Manager</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🏠</text></svg>">
|
||||
<link rel="stylesheet" href="assets/css/style.css?v=20260316h">
|
||||
<link rel="stylesheet" href="assets/css/style.css?v=20260316i">
|
||||
<!-- QuaggaJS for barcode scanning -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2@1.8.4/dist/quagga.min.js"></script>
|
||||
</head>
|
||||
@@ -911,6 +911,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="assets/js/app.js?v=20260316h"></script>
|
||||
<script src="assets/js/app.js?v=20260316i"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user