feat: predict expiry date from product history when adding items

- PHP: new 'expiry_history' action computes avg shelf life (expiry_date - added_at)
  from inventory table for the same product_id (last 730 days, valid entries only)
- JS: _fetchExpiryHistoryAndUpdate() fires async after showAddForm() renders
  and replaces the rule-based estimate with the historical average if available
- Labeled with '📊 storico' badge on the estimate line (tooltip shows sample count)
- recalculateAddExpiry() and selectPurchaseType('new') both honour window._historyExpiryDays
- Vacuum-sealed multiplier still applied on top of historical base
- Falls back silently to rule-based estimateExpiryDays when no history exists
This commit is contained in:
dadaloop82
2026-04-06 09:09:04 +00:00
parent 568cc1e6fa
commit 50da545c72
3 changed files with 88 additions and 7 deletions
+11
View File
@@ -2531,6 +2531,17 @@ body {
font-weight: 600;
}
.history-badge {
font-size: 0.75rem;
background: var(--primary);
color: #fff;
border-radius: 10px;
padding: 1px 7px;
vertical-align: middle;
font-weight: 500;
cursor: default;
}
.form-hint {
font-size: 0.8rem;
color: var(--text-muted);
+43 -7
View File
@@ -3231,6 +3231,9 @@ function showAddForm() {
vacuumCb.checked = false;
document.getElementById('add-vacuum-hint').style.display = 'none';
}
// Reset historical expiry for this product; will be fetched async
window._historyExpiryDays = null;
window._historyExpiryCount = 0;
// Store base expiry for vacuum recalculation
window._addBaseExpiryDays = estimatedDays;
@@ -3258,6 +3261,10 @@ function showAddForm() {
`;
showPage('add');
// After rendering, fetch history-based expiry prediction
if (currentProduct && currentProduct.id) {
_fetchExpiryHistoryAndUpdate(currentProduct.id);
}
}
function toggleVacuumSealed() {
@@ -3277,16 +3284,17 @@ function recalculateAddExpiry() {
const loc = document.getElementById('add-location')?.value || '';
const isVacuum = document.getElementById('add-vacuum-sealed')?.checked;
let days = estimateExpiryDays(currentProduct, loc);
if (isVacuum) days = getVacuumExpiryDays(days);
const baseDays = window._historyExpiryDays ?? estimateExpiryDays(currentProduct, loc);
let days = isVacuum ? getVacuumExpiryDays(baseDays) : baseDays;
window._addBaseExpiryDays = estimateExpiryDays(currentProduct, loc);
window._addBaseExpiryDays = baseDays;
const newDate = addDays(days);
const newLabel = formatEstimatedExpiry(days);
let suffix = '';
if (loc === 'freezer' && isVacuum) suffix = ' (freezer + sotto vuoto)';
if (window._historyExpiryDays) suffix = ' (da storico)';
else if (loc === 'freezer' && isVacuum) suffix = ' (freezer + sotto vuoto)';
else if (loc === 'freezer') suffix = ' (freezer)';
else if (isVacuum) suffix = ' (sotto vuoto)';
@@ -3298,6 +3306,33 @@ function recalculateAddExpiry() {
if (dateEl) dateEl.textContent = formatDate(newDate);
}
async function _fetchExpiryHistoryAndUpdate(productId) {
try {
const res = await fetch(`api/index.php?action=expiry_history&product_id=${encodeURIComponent(productId)}`);
const data = await res.json();
if (data.avg_days && data.avg_days > 0 && data.count >= 1) {
window._historyExpiryDays = data.avg_days;
window._historyExpiryCount = data.count;
// Update the displayed date and label
const loc = document.getElementById('add-location')?.value || '';
const isVacuum = document.getElementById('add-vacuum-sealed')?.checked;
let days = isVacuum ? getVacuumExpiryDays(data.avg_days) : data.avg_days;
const newDate = addDays(days);
const newLabel = formatEstimatedExpiry(days);
const suffix = ` <span class="history-badge" title="Media da ${data.count} insertiment${data.count === 1 ? 'o' : 'i'} precedent${data.count === 1 ? 'e' : 'i'}">📊 storico</span>`;
const expiryInput = document.getElementById('add-expiry');
const estimateEl = document.querySelector('.expiry-estimate-label');
const dateEl = document.querySelector('.expiry-estimate-date');
if (expiryInput) expiryInput.value = newDate;
if (estimateEl) estimateEl.innerHTML = `Scadenza stimata: <strong>${newLabel}${suffix}</strong>`;
if (dateEl) dateEl.textContent = formatDate(newDate);
window._addBaseExpiryDays = data.avg_days;
}
} catch (e) {
// silently fall back to rule-based estimate
}
}
function getVacuumExpiryDays(baseDays) {
// Vacuum sealing extends shelf life significantly
if (baseDays <= 7) return Math.round(baseDays * 3); // very fresh: 3x (e.g., 3→9, 7→21)
@@ -3392,12 +3427,13 @@ function selectPurchaseType(btn, type) {
// Recalculate fresh expiry based on current location/vacuum
const loc = document.getElementById('add-location')?.value || '';
const isVacuum = document.getElementById('add-vacuum-sealed')?.checked;
let days = estimateExpiryDays(currentProduct, loc);
if (isVacuum) days = getVacuumExpiryDays(days);
const baseDays = window._historyExpiryDays ?? estimateExpiryDays(currentProduct, loc);
let days = isVacuum ? getVacuumExpiryDays(baseDays) : baseDays;
const estimatedDate = addDays(days);
const estimateLabel = formatEstimatedExpiry(days);
let suffix = '';
if (loc === 'freezer' && isVacuum) suffix = ' (freezer + sotto vuoto)';
if (window._historyExpiryDays) suffix = ` <span class="history-badge" title="Media da ${window._historyExpiryCount} inserimento/i precedente/i">📊 storico</span>`;
else if (loc === 'freezer' && isVacuum) suffix = ' (freezer + sotto vuoto)';
else if (loc === 'freezer') suffix = ' (freezer)';
else if (isVacuum) suffix = ' (sotto vuoto)';