From 81b80d3bc50118c787fab10dd48bda9a15684292 Mon Sep 17 00:00:00 2001 From: max Date: Sat, 10 Oct 2026 23:59:10 +0200 Subject: [PATCH] feat: automatically remove items from stock when empty (quantity <= 0) --- db.js | 74 ++++++++++++++++++++++++++++++++++++++++++++------- public/app.js | 16 ++++++++--- test.js | 22 ++++++++++----- 3 files changed, 93 insertions(+), 19 deletions(-) diff --git a/db.js b/db.js index c275f72..dc48662 100644 --- a/db.js +++ b/db.js @@ -49,7 +49,7 @@ db.exec(` // Item functions function getAllItems({ search = '', category = '', location = '', status = 'all' } = {}) { - let query = `SELECT * FROM items WHERE 1=1`; + let query = `SELECT * FROM items WHERE quantity > 0`; const params = []; if (search) { @@ -73,7 +73,7 @@ function getAllItems({ search = '', category = '', location = '', status = 'all' } else if (status === 'full') { query += ` AND (is_opened = 0 AND quantity >= initial_quantity)`; } else if (status === 'low') { - query += ` AND (quantity <= (initial_quantity * 0.25) OR quantity = 0)`; + query += ` AND quantity <= (initial_quantity * 0.25)`; } query += ` ORDER BY updated_at DESC, name ASC`; @@ -148,6 +148,21 @@ function updateItem(id, item) { const qty = item.quantity !== undefined ? Number(item.quantity) : existing.quantity; const init_qty = item.initial_quantity !== undefined ? Number(item.initial_quantity) : existing.initial_quantity; + + // If item updated to 0 or less, remove it from stock automatically + if (qty <= 0) { + deleteItem(id); + return { + id, + name: item.name || existing.name, + quantity: 0, + initial_quantity: init_qty, + unit: item.unit || existing.unit, + deleted: true, + is_empty: true + }; + } + const is_opened = item.is_opened !== undefined ? (item.is_opened ? 1 : 0) : existing.is_opened; let opened_at = existing.opened_at; if (is_opened && !existing.is_opened) { @@ -218,8 +233,30 @@ function adjustQuantity(id, { newQuantity, delta, markOpened }) { targetQty = item.quantity + Number(delta); } - // Prevent negative quantity - if (targetQty < 0) targetQty = 0; + // If item is finished/empty (0 or less), automatically remove it from stock + if (targetQty <= 0) { + logActivity({ + item_id: id, + item_name: item.name, + action: 'consume_all', + amount_change: -item.quantity, + unit: item.unit, + note: `Consumed all (${item.quantity} ${item.unit}) - automatically removed from stock` + }); + + const delStmt = db.prepare(`DELETE FROM items WHERE id = ?`); + delStmt.run(id); + + return { + id, + name: item.name, + quantity: 0, + initial_quantity: item.initial_quantity, + unit: item.unit, + deleted: true, + is_empty: true + }; + } const is_opened = markOpened !== undefined ? (markOpened ? 1 : 0) : (item.is_opened || (targetQty < item.initial_quantity ? 1 : 0)); const opened_at = is_opened && !item.opened_at ? new Date().toISOString().slice(0, 10) : item.opened_at; @@ -240,12 +277,10 @@ function adjustQuantity(id, { newQuantity, delta, markOpened }) { logActivity({ item_id: id, item_name: item.name, - action: targetQty === 0 ? 'consume_all' : (diff < 0 ? 'consume_part' : 'restock'), + action: diff < 0 ? 'consume_part' : 'restock', amount_change: diff, unit: item.unit, - note: targetQty === 0 - ? `Consumed all (${item.quantity} ${item.unit})` - : `${diff < 0 ? 'Consumed' : 'Added'} ${Math.abs(diff)} ${item.unit} (remaining: ${targetQty} ${item.unit})` + note: `${diff < 0 ? 'Consumed' : 'Added'} ${Math.abs(diff)} ${item.unit} (remaining: ${targetQty} ${item.unit})` }); return getItemById(id); @@ -287,8 +322,29 @@ function removePacket(id, count = 1) { const removeAmount = packSize * count; let newQty = item.quantity - removeAmount; + // If item is completely removed/empty, remove from stock automatically if (newQty <= 0) { - newQty = 0; + logActivity({ + item_id: id, + item_name: item.name, + action: 'remove_packet', + amount_change: -item.quantity, + unit: item.unit, + note: `Removed all remaining stock (${count} packet(s)) - automatically removed from stock` + }); + + const delStmt = db.prepare(`DELETE FROM items WHERE id = ?`); + delStmt.run(id); + + return { + id, + name: item.name, + quantity: 0, + initial_quantity: item.initial_quantity, + unit: item.unit, + deleted: true, + is_empty: true + }; } const stmt = db.prepare(` diff --git a/public/app.js b/public/app.js index aec0642..0311d62 100644 --- a/public/app.js +++ b/public/app.js @@ -546,9 +546,13 @@ async function confirmAndFinishAction() { if (res.ok) { const updated = await res.json(); playBeepSound(); - showToast(`✓ ${action === 'add' ? 'Added' : 'Removed'} ${state.manage.packetCount} pack(s) of ${updated.name}`, 'success'); + if (updated.deleted || updated.is_empty || updated.quantity <= 0) { + showToast(`✓ Removed empty ${updated.name} from stock`, 'success'); + } else { + showToast(`✓ ${action === 'add' ? 'Added' : 'Removed'} ${state.manage.packetCount} pack(s) of ${updated.name}`, 'success'); + } - // Return to manage station + // Return to manager station resetTabletManageStation(); await loadStats(); await loadItems(); @@ -567,9 +571,13 @@ async function confirmAndFinishAction() { if (res.ok) { const updated = await res.json(); playBeepSound(); - showToast(`✓ Set ${updated.name} to ${updated.quantity} ${updated.unit}`, 'success'); + if (updated.deleted || updated.is_empty || updated.quantity <= 0) { + showToast(`✓ Finished ${updated.name} (0 ${updated.unit}): removed from stock`, 'success'); + } else { + showToast(`✓ Set ${updated.name} to ${updated.quantity} ${updated.unit}`, 'success'); + } - // Return to manage station + // Return to manager station resetTabletManageStation(); await loadStats(); await loadItems(); diff --git a/test.js b/test.js index c0f7aec..b271055 100644 --- a/test.js +++ b/test.js @@ -76,15 +76,25 @@ console.log('Testing absolute set (100g remaining)...'); const updatedAbs = db.adjustQuantity(newItem.id, { newQuantity: 100 }); assert.strictEqual(updatedAbs.quantity, 100); -// Consume remaining -console.log('Testing consume all...'); +// Consume remaining -> should automatically remove from stock +console.log('Testing consume all (auto-remove on empty)...'); const finished = db.adjustQuantity(newItem.id, { newQuantity: 0 }); assert.strictEqual(finished.quantity, 0); +assert.strictEqual(finished.deleted, true, 'Result should indicate item was deleted'); +assert.strictEqual(db.getItemById(newItem.id), undefined, 'Item should be automatically removed from DB when empty'); -// Delete test item -const deleted = db.deleteItem(newItem.id); -assert.strictEqual(deleted, true); -assert.strictEqual(db.getItemById(newItem.id), undefined); +// Test packet removal to 0 -> should automatically remove from stock +console.log('Testing packet removal to 0 (auto-remove on empty)...'); +const autoRemovePack = db.createItem({ + name: 'Test Packet Auto-Remove', + quantity: 250, + initial_quantity: 250, + unit: 'g' +}); +const packFinished = db.removePacket(autoRemovePack.id, 1); +assert.strictEqual(packFinished.quantity, 0); +assert.strictEqual(packFinished.deleted, true); +assert.strictEqual(db.getItemById(autoRemovePack.id), undefined, 'Packet should be automatically removed when 0 left'); // Test stats const stats = db.getStats();