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;
|
if ($openedId) $response['opened_id'] = $openedId;
|
||||||
echo json_encode($response);
|
echo json_encode($response);
|
||||||
}
|
}
|
||||||
|
|||||||
+94
-10
@@ -3482,6 +3482,88 @@ function selectUseLocation(btn, loc) {
|
|||||||
document.getElementById('use-location').value = 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) {
|
function showMoveAfterUseModal(product, fromLoc, remaining, openedId) {
|
||||||
const otherLocs = Object.entries(LOCATIONS).filter(([k]) => k !== fromLoc);
|
const otherLocs = Object.entries(LOCATIONS).filter(([k]) => k !== fromLoc);
|
||||||
const locButtons = otherLocs.map(([k, v]) =>
|
const locButtons = otherLocs.map(([k, v]) =>
|
||||||
@@ -3555,7 +3637,8 @@ async function submitUseAll() {
|
|||||||
if (result.added_to_bring) {
|
if (result.added_to_bring) {
|
||||||
setTimeout(() => showToast('🛒 Prodotto finito → aggiunto a Bring!', 'info'), 1500);
|
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 {
|
} else {
|
||||||
showToast(result.error || 'Errore', 'error');
|
showToast(result.error || 'Errore', 'error');
|
||||||
}
|
}
|
||||||
@@ -3595,11 +3678,11 @@ async function submitUse(e) {
|
|||||||
}
|
}
|
||||||
// If there's remaining quantity, offer to move to another location
|
// If there's remaining quantity, offer to move to another location
|
||||||
const usedFrom = document.getElementById('use-location').value;
|
const usedFrom = document.getElementById('use-location').value;
|
||||||
if (result.remaining > 0) {
|
const moveCallback = result.remaining > 0
|
||||||
showMoveAfterUseModal(currentProduct, usedFrom, result.remaining, result.opened_id);
|
? () => showMoveAfterUseModal(currentProduct, usedFrom, result.remaining, result.opened_id)
|
||||||
} else {
|
: () => showPage('dashboard');
|
||||||
showPage('dashboard');
|
// Check low stock → Bring! prompt
|
||||||
}
|
showLowStockBringPrompt(result, moveCallback);
|
||||||
} else {
|
} else {
|
||||||
showToast(result.error || 'Errore', 'error');
|
showToast(result.error || 'Errore', 'error');
|
||||||
}
|
}
|
||||||
@@ -5149,10 +5232,11 @@ async function submitRecipeUse(useAll) {
|
|||||||
setTimeout(() => showToast('🛒 Prodotto finito → aggiunto a Bring!', 'info'), 1500);
|
setTimeout(() => showToast('🛒 Prodotto finito → aggiunto a Bring!', 'info'), 1500);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Offer to move opened package (stays on recipe page, modal over recipe overlay)
|
// Check low stock → Bring! prompt, then offer move
|
||||||
if (result.remaining > 0) {
|
const moveCallback = result.remaining > 0
|
||||||
setTimeout(() => showRecipeMoveModal(productId, location, result.remaining, result.opened_id), 400);
|
? () => setTimeout(() => showRecipeMoveModal(productId, location, result.remaining, result.opened_id), 300)
|
||||||
}
|
: null;
|
||||||
|
setTimeout(() => showLowStockBringPrompt(result, moveCallback), 300);
|
||||||
} else {
|
} else {
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.textContent = '📦 Usa';
|
btn.textContent = '📦 Usa';
|
||||||
|
|||||||
Binary file not shown.
+2
-2
@@ -9,7 +9,7 @@
|
|||||||
<title>Dispensa Manager</title>
|
<title>Dispensa Manager</title>
|
||||||
<link rel="manifest" href="manifest.json">
|
<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="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 -->
|
<!-- QuaggaJS for barcode scanning -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2@1.8.4/dist/quagga.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2@1.8.4/dist/quagga.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
@@ -911,6 +911,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="assets/js/app.js?v=20260316h"></script>
|
<script src="assets/js/app.js?v=20260316i"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user