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:
@@ -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
@@ -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)';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user