diff --git a/api/index.php b/api/index.php index 27020f7..7f77756 100644 --- a/api/index.php +++ b/api/index.php @@ -1157,23 +1157,61 @@ function deleteInventory(PDO $db): void { } /** - * Returns products whose entire inventory is at quantity = 0 - * (auto-set when stock ran out, pending user confirmation to permanently remove). + * Returns products whose entire inventory is at quantity = 0 AND whose + * transaction balance (total_in - total_out) is still significantly positive — + * meaning the system suspects the product ran out prematurely (scale drift, + * missed registration, etc.). + * + * Products where the balance is at/near zero are legitimately finished by the + * user; those rows are silently deleted here (no banner needed). */ function getFinishedItems(PDO $db): void { $rows = $db->query(" SELECT p.id AS product_id, p.name, p.brand, p.unit, p.default_quantity, p.package_unit, p.image_url, MIN(i.location) AS location, - MAX(i.updated_at) AS updated_at + MAX(i.updated_at) AS updated_at, + COALESCE(SUM(CASE WHEN t.type = 'in' AND t.undone = 0 THEN t.quantity ELSE 0 END), 0) AS total_in, + COALESCE(SUM(CASE WHEN t.type IN ('out','waste') AND t.undone = 0 THEN t.quantity ELSE 0 END), 0) AS total_out FROM products p JOIN inventory i ON i.product_id = p.id + LEFT JOIN transactions t ON t.product_id = p.id WHERE NOT EXISTS ( SELECT 1 FROM inventory i2 WHERE i2.product_id = p.id AND i2.quantity > 0 ) GROUP BY p.id ORDER BY MAX(i.updated_at) DESC ")->fetchAll(PDO::FETCH_ASSOC); - echo json_encode(['success' => true, 'finished' => $rows], JSON_UNESCAPED_UNICODE); + + // Per-unit threshold: residue below this is considered normal rounding/finish + $thresholds = ['g' => 20, 'ml' => 20, 'kg' => 0.02, 'l' => 0.02, 'conf' => 0.1, 'pz' => 0.5]; + + $suspicious = []; + foreach ($rows as $r) { + $expected = (float)$r['total_in'] - (float)$r['total_out']; + $threshold = $thresholds[$r['unit']] ?? 0.5; + + if ($expected > $threshold) { + // Transaction balance says stock should remain — show banner + $suspicious[] = [ + 'product_id' => (int)$r['product_id'], + 'name' => $r['name'], + 'brand' => $r['brand'], + 'unit' => $r['unit'], + 'default_quantity' => $r['default_quantity'], + 'package_unit' => $r['package_unit'], + 'image_url' => $r['image_url'], + 'location' => $r['location'], + 'updated_at' => $r['updated_at'], + 'expected_qty' => round($expected, 3), + ]; + } else { + // Legitimately finished — delete silently, no banner + $db->prepare("DELETE FROM inventory WHERE product_id = ? AND quantity = 0") + ->execute([$r['product_id']]); + } + } + + echo json_encode(['success' => true, 'finished' => $suspicious], JSON_UNESCAPED_UNICODE); } /** diff --git a/assets/js/app.js b/assets/js/app.js index 88e9e5e..3415783 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -2557,7 +2557,8 @@ function renderBannerItem() { banner.className = 'alert-banner banner-finished'; iconEl.textContent = '📦'; titleEl.textContent = `${fin.name}${fin.brand ? ' (' + fin.brand + ')' : ''} — ${t('dashboard.banner_finished_title')}`; - detailEl.textContent = t('dashboard.banner_finished_detail', { name: fin.name }); + const expectedText = fin.expected_qty ? ` Secondo le registrazioni dovresti averne ancora ${fin.expected_qty} ${fin.unit}.` : ''; + detailEl.innerHTML = `L'inventario segna zero, ma i movimenti registrati dicono che non dovrebbe essere finito.${expectedText} Puoi controllare?`; let btns = ``; btns += ``; actionsEl.innerHTML = btns;