feat: keep qty=0 instead of auto-delete, ask user to confirm via banner

- useFromInventory: replace DELETE with UPDATE qty=0 when stock hits 0
  (both normal path and use-all-locations path)
- listInventory: add WHERE quantity > 0 so qty=0 rows are invisible in
  the regular inventory list
- New API actions: inventory_finished_items (query) and
  inventory_confirm_finished (delete after user confirms)
- Banner: new 'finished' type (priority 600, above anomalies)
  Shows: '{name} — è finito?' with two buttons
  'Sì, è finito' → permanently deletes the qty=0 row
  'No, ne ho ancora' → navigates to add-inventory form
- i18n: banner_finished_* and toast.product_finished_confirmed (it/en/de)
- DB migration: restored 75 auto-deleted products (last 30 days) as
  qty=0 inventory rows so they appear in the banner queue
This commit is contained in:
dadaloop82
2026-04-27 05:41:38 +00:00
parent 37299e60c9
commit 5df0be1661
5 changed files with 118 additions and 7 deletions
+55 -1
View File
@@ -2354,10 +2354,11 @@ async function loadBannerAlerts() {
if (!banner) { console.warn('[Banner] #alert-banner not found'); return; }
try {
const [invData, predData, anomalyData] = await Promise.all([
const [invData, predData, anomalyData, finishedData] = await Promise.all([
api('inventory_list'),
api('consumption_predictions').catch(err => { console.warn('[Banner] predictions fetch failed:', err); return { predictions: [] }; }),
api('inventory_anomalies').catch(err => { console.warn('[Banner] anomalies fetch failed:', err); return { anomalies: [] }; }),
api('inventory_finished_items').catch(err => { console.warn('[Banner] finished_items fetch failed:', err); return { finished: [] }; }),
]);
const items = invData.inventory || [];
const confirmed = getReviewConfirmed();
@@ -2400,6 +2401,13 @@ async function loadBannerAlerts() {
_bannerQueue.push({ type: 'anomaly', data: an });
});
// 6. Finished products: inventory hit 0, waiting for user confirmation
const finished = finishedData.finished || [];
finished.forEach(fin => {
if (confirmed['fin_' + fin.product_id]) return;
_bannerQueue.push({ type: 'finished', data: fin });
});
// Sort by priority (highest first)
_bannerQueue.sort((a, b) => _bannerPriority(b) - _bannerPriority(a));
@@ -2451,6 +2459,8 @@ function _bannerPriority(entry) {
// Phantom (inflated qty) = 250, Missing = 260 (slightly higher, means data is clearly wrong)
return entry.data.direction === 'missing' ? 260 : 250;
}
case 'finished':
return 600; // product ran out — confirm before removing from DB
default:
return 0;
}
@@ -2542,6 +2552,16 @@ function renderBannerItem() {
}
actionsEl.innerHTML = btns;
} else if (entry.type === 'finished') {
const fin = entry.data;
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 });
let btns = `<button class="btn-banner btn-banner-ok" onclick="confirmBannerFinished()">${t('dashboard.banner_finished_action_yes')}</button>`;
btns += `<button class="btn-banner btn-banner-edit" onclick="notFinishedBannerAction()">${t('dashboard.banner_finished_action_no')}</button>`;
actionsEl.innerHTML = btns;
} else if (entry.type === 'anomaly') {
const an = entry.data;
const isPhantom = an.direction === 'phantom';
@@ -2699,6 +2719,40 @@ function dismissBannerExpiring() {
dismissBannerItem();
}
async function confirmBannerFinished() {
const entry = _bannerQueue[_bannerIndex];
if (!entry || entry.type !== 'finished') return;
const productId = entry.data.product_id;
try {
await api('inventory_confirm_finished', {}, 'POST', { product_id: productId });
} catch(e) {}
setReviewConfirmed('fin_' + productId);
showToast(t('toast.product_finished_confirmed'), 'success');
dismissBannerItem();
}
async function notFinishedBannerAction() {
const entry = _bannerQueue[_bannerIndex];
if (!entry || entry.type !== 'finished') return;
const productId = entry.data.product_id;
// Remove from this session's queue (will re-appear next load if still at qty=0)
dismissBannerItem();
showLoading(true);
try {
const data = await api('product_get', { id: productId });
showLoading(false);
if (data.product) {
currentProduct = data.product;
showAddForm();
} else {
showToast(t('error.not_found'), 'error');
}
} catch(e) {
showLoading(false);
showToast(t('error.connection'), 'error');
}
}
// --- Banner swipe navigation ---
let _bannerTouchStartX = 0;
let _bannerTouchStartY = 0;