diff --git a/api/index.php b/api/index.php
index c1f1390..9098fbe 100644
--- a/api/index.php
+++ b/api/index.php
@@ -1634,16 +1634,19 @@ function getConsumptionPredictions(PDO $db): void {
}
$predictions[] = [
- 'inventory_id' => (int)$item['inventory_id'],
- 'product_id' => (int)$item['product_id'],
- 'name' => $item['name'],
- 'brand' => $item['brand'],
- 'location' => $item['location'],
- 'unit' => $displayUnit,
- 'expected_qty' => $expDisplay,
- 'actual_qty' => $actDisplay,
- 'daily_rate' => round($dailyRate, 3),
- 'deviation_pct'=> round($pctDev * 100),
+ 'inventory_id' => (int)$item['inventory_id'],
+ 'product_id' => (int)$item['product_id'],
+ 'name' => $item['name'],
+ 'brand' => $item['brand'],
+ 'location' => $item['location'],
+ 'unit' => $displayUnit,
+ 'expected_qty' => $expDisplay,
+ 'actual_qty' => $actDisplay,
+ 'daily_rate' => round($dailyRate, 3),
+ 'deviation_pct' => round($pctDev * 100),
+ 'days_since_restock' => (int)round($daysSinceRestock),
+ 'direction' => $actualQty > $expectedQty ? 'more' : 'less',
+ 'tx_count' => count($rows),
];
}
}
diff --git a/assets/js/app.js b/assets/js/app.js
index 7400292..44c6271 100644
--- a/assets/js/app.js
+++ b/assets/js/app.js
@@ -2371,16 +2371,7 @@ async function loadBannerAlerts() {
_bannerQueue.push({ type: 'expired', data: { ...item, days_expired: Math.abs(days) } });
});
- // 2. Products expiring very soon (today, tomorrow, within 3 days)
- items.forEach(item => {
- if (!item.expiry_date) return;
- const days = daysUntilExpiry(item.expiry_date);
- if (days < 0 || days > 3) return;
- if (confirmed['exps_' + item.id]) return;
- _bannerQueue.push({ type: 'expiring', data: { ...item, days_left: days } });
- });
-
- // 3. Suspicious quantities
+ // 2. Suspicious quantities ("expiring soon" shown only in dashboard sections, not in banner)
items.forEach(item => {
if (confirmed[item.id]) return;
if (isSuspiciousQty(item.quantity, item.unit) || isSuspiciousDefaultQty(item.default_quantity, item.unit, item.package_unit)) {
@@ -2433,7 +2424,7 @@ async function loadBannerAlerts() {
*
* Priority tiers:
* 1000+ : expired (longer ago = higher)
- * 500-999: expiring today/tomorrow/soon (sooner = higher)
+ * 500-799: anomalies (data discrepancies)
* 200-499: suspicious quantities (low stock > high stock > package)
* 100-199: consumption predictions (higher deviation% = higher)
*/
@@ -2444,11 +2435,6 @@ function _bannerPriority(entry) {
// Expired longer = more urgent; base 1000 + days (capped)
return 1000 + Math.min(d, 500);
}
- case 'expiring': {
- const d = entry.data.days_left ?? 3;
- // Today=999, tomorrow=998, 2d=997, 3d=996
- return 999 - d;
- }
case 'review': {
const w = entry.data.warning || '';
// Low stock is more urgent than too-much
@@ -2487,77 +2473,89 @@ function renderBannerItem() {
if (entry.type === 'expired') {
const item = entry.data;
const qtyDisplay = formatQuantity(item.quantity, item.unit, item.default_quantity, item.package_unit);
- const daysText = item.days_expired === 0 ? t('dashboard.banner_expired_today') : t('dashboard.banner_expired_days', { days: item.days_expired });
+ const daysText = item.days_expired === 0 ? 'Scaduto oggi'
+ : `Scaduto da ${item.days_expired} ${item.days_expired === 1 ? 'giorno' : 'giorni'}`;
banner.className = 'alert-banner banner-expired';
iconEl.textContent = '🚫';
- titleEl.textContent = `${t('dashboard.banner_expired_title')}: ${item.name}${item.brand ? ' (' + item.brand + ')' : ''}`;
- detailEl.textContent = `${daysText} · ${qtyDisplay}`;
- let btns = ``;
- btns += ``;
- btns += ``;
- btns += ``;
- actionsEl.innerHTML = btns;
-
- } else if (entry.type === 'expiring') {
- const item = entry.data;
- const qtyDisplay = formatQuantity(item.quantity, item.unit, item.default_quantity, item.package_unit);
- let urgencyText;
- if (item.days_left === 0) urgencyText = t('dashboard.banner_expiring_today');
- else if (item.days_left === 1) urgencyText = t('dashboard.banner_expiring_tomorrow');
- else urgencyText = t('dashboard.banner_expiring_days', { days: item.days_left });
- banner.className = 'alert-banner banner-expiring';
- iconEl.textContent = '⏰';
- titleEl.textContent = `${t('dashboard.banner_expiring_title')}: ${item.name}${item.brand ? ' (' + item.brand + ')' : ''}`;
- detailEl.textContent = `${urgencyText} · ${qtyDisplay}`;
- let btns = ``;
- btns += ``;
- btns += ``;
+ titleEl.textContent = `${item.name}${item.brand ? ' (' + item.brand + ')' : ''} — Scaduto!`;
+ detailEl.innerHTML = `${daysText} · hai ancora ${qtyDisplay}. Usalo subito o buttalo.`;
+ let btns = ``;
+ btns += ``;
+ btns += ``;
+ btns += ``;
actionsEl.innerHTML = btns;
} else if (entry.type === 'review') {
const item = entry.data;
const qtyDisplay = formatQuantity(item.quantity, item.unit, item.default_quantity, item.package_unit);
+ const suspDq = isSuspiciousDefaultQty(item.default_quantity, item.unit, item.package_unit);
+ const suspQty = isSuspiciousQty(item.quantity, item.unit);
+ const t_ = QTY_THRESHOLDS[item.unit] || QTY_THRESHOLDS['pz'];
banner.className = 'alert-banner';
iconEl.textContent = '⚠️';
- titleEl.textContent = `${t('dashboard.banner_review_title')}: ${item.name}${item.brand ? ' (' + item.brand + ')' : ''}`;
- detailEl.textContent = `${item.warning} · ${qtyDisplay}`;
- let btns = ``;
- btns += ``;
+ let titleText, detailText;
+ if (suspDq && !suspQty) {
+ titleText = `Confezione insolita: ${item.name}${item.brand ? ' (' + item.brand + ')' : ''}`;
+ detailText = `Hai impostato una confezione da ${item.default_quantity} ${item.package_unit} — la dimensione sembra molto alta. Controlla se è corretta o modifica.`;
+ } else if (parseFloat(item.quantity) < t_.min) {
+ titleText = `Quantità molto bassa: ${item.name}${item.brand ? ' (' + item.brand + ')' : ''}`;
+ detailText = `Hai solo ${qtyDisplay} in inventario — sembra poco, potrebbe essere un errore di inserimento. Conferma se è corretto.`;
+ } else {
+ titleText = `Quantità insolitamente alta: ${item.name}${item.brand ? ' (' + item.brand + ')' : ''}`;
+ detailText = `Hai ${qtyDisplay} in inventario — la cifra sembra molto alta. Conferma se è corretto o correggi.`;
+ }
+ titleEl.textContent = titleText;
+ detailEl.textContent = detailText;
+ let btns = ``;
+ btns += ``;
if (hasScale) {
- btns += ``;
+ btns += ``;
}
actionsEl.innerHTML = btns;
} else if (entry.type === 'prediction') {
const pred = entry.data;
+ const dir = pred.direction || 'less';
+ const dailyRate = parseFloat(pred.daily_rate) || 0;
+ const daysSince = parseInt(pred.days_since_restock) || 0;
banner.className = 'alert-banner banner-prediction';
iconEl.textContent = '📊';
- titleEl.textContent = `${t('dashboard.banner_prediction_title')}: ${pred.name}${pred.brand ? ' (' + pred.brand + ')' : ''}`;
- const expTxt = t('prediction.expected_qty').replace('{expected}', pred.expected_qty).replace('{unit}', pred.unit);
- const actTxt = t('prediction.actual_qty').replace('{actual}', pred.actual_qty).replace('{unit}', pred.unit);
- detailEl.innerHTML = `${expTxt} · ${actTxt}
${t('prediction.check_suggestion')}`;
- let btns = ``;
- btns += ``;
+ titleEl.textContent = `Consumo anomalo: ${pred.name}${pred.brand ? ' (' + pred.brand + ')' : ''}`;
+ let rateText = '';
+ if (dailyRate > 0) {
+ rateText = dailyRate >= 1
+ ? `Media ~${Math.round(dailyRate)} ${pred.unit}/giorno`
+ : `Media ~${Math.round(dailyRate * 7)} ${pred.unit}/settimana`;
+ }
+ const timeText = daysSince > 0 ? ` — ${daysSince} giorni fa hai rifornito` : '';
+ let diffText;
+ if (dir === 'more') {
+ diffText = `mi aspettavo ${pred.expected_qty} ${pred.unit}${timeText}, ne hai invece ${pred.actual_qty} ${pred.unit}. Hai aggiunto scorte senza registrarle?`;
+ } else {
+ diffText = `mi aspettavo ${pred.expected_qty} ${pred.unit}${timeText}, ne hai solo ${pred.actual_qty} ${pred.unit}. Hai consumato di più del solito?`;
+ }
+ detailEl.innerHTML = rateText ? `${rateText}: ${diffText}` : diffText.charAt(0).toUpperCase() + diffText.slice(1);
+ let btns = ``;
+ btns += ``;
if (hasScale) {
- btns += ``;
+ btns += ``;
}
actionsEl.innerHTML = btns;
} else if (entry.type === 'anomaly') {
const an = entry.data;
- const diffAbs = Math.abs(an.diff);
- const diffDisplay = `${diffAbs} ${an.unit}`;
const isPhantom = an.direction === 'phantom';
banner.className = 'alert-banner banner-anomaly';
iconEl.textContent = '🔍';
- titleEl.textContent = `Anomalia inventario: ${an.name}${an.brand ? ' (' + an.brand + ')' : ''}`;
if (isPhantom) {
- detailEl.innerHTML = `Inventario: ${an.inv_qty}${an.unit} ma le transazioni ne giustificano solo ${an.expected_qty}${an.unit} (+${diffDisplay} fantasma)`;
+ titleEl.textContent = `${an.name} — hai più scorte del previsto`;
+ detailEl.innerHTML = `L'inventario segna ${an.inv_qty} ${an.unit}, ma in base alle entrate e uscite registrate ne dovresti avere solo ${an.expected_qty} ${an.unit}. Hai aggiunto scorte o corretto la quantità manualmente senza registrarlo?`;
} else {
- detailEl.innerHTML = `Le transazioni indicano ${an.expected_qty}${an.unit} ma l'inventario ha solo ${an.inv_qty}${an.unit} (mancano ${diffDisplay})`;
+ titleEl.textContent = `${an.name} — hai meno scorte del previsto`;
+ detailEl.innerHTML = `In base alle operazioni registrate dovresti avere ${an.expected_qty} ${an.unit} di ${an.name}, ma l'inventario mostra solo ${an.inv_qty} ${an.unit}. Hai prelevato senza registrarlo?`;
}
- let btns = ``;
- btns += ``;
+ let btns = ``;
+ btns += ``;
actionsEl.innerHTML = btns;
}
@@ -2603,7 +2601,7 @@ function confirmBannerPrediction() {
const entry = _bannerQueue[_bannerIndex];
if (!entry || entry.type !== 'prediction') return;
setReviewConfirmed('pred_' + entry.data.inventory_id);
- showToast(t('toast.quantity_confirmed'), 'success');
+ showToast('✅ Confermato — il sistema ricalcolerà le previsioni dalle prossime registrazioni', 'success');
dismissBannerItem();
}