Smart opened-product expiry: days countdown, edibility, correct sort

PHP getStats() opened section:
- Primary detection: opened_at IS NOT NULL (reliable, set by useFromInventory)
  Fallback: fractional-qty pattern (legacy items)
- Per-item compute opened_expiry = min(opened_at + estimateOpenedExpiryDaysPHP, original_expiry)
  → vacuum_sealed items get 1.5× multiplier
  → always take sooner of 'opened shelf life' vs 'original sealed expiry'
- Add days_to_expiry, opened_expiry, is_edible, has_opened_at to each item
- Filter legacy items (no opened_at) with expiry > 14 days (too much noise)
- Sort by days_to_expiry ASC (soonest/spoiled first) instead of updated_at DESC

JS dashboard opened render:
- Expiry badge:  Scaduto / ⚠️ Scade oggi /  Xgg (urgent≤2, soon≤5, ok>5)
- 🔒 icon added when vacuum_sealed=1
- Spoiled items shown with strikethrough name + muted styling (.alert-item-spoiled)
- Cap display at 10 items; 'e altri N prodotti aperti...' note if more
- Sort comes from server (removed JS openedFraction sort)

CSS:
- .opened-expiry-{ok,soon,urgent,today,spoiled} badge classes
- .alert-item-spoiled strikethrough styling
- .alert-more-note
This commit is contained in:
dadaloop82
2026-04-08 12:30:36 +00:00
parent e8649a87fc
commit 19489a0265
3 changed files with 121 additions and 19 deletions
+28
View File
@@ -4304,6 +4304,34 @@ body {
font-size: 0.7rem;
}
/* Opened expiry badges */
.alert-item-badge.opened-expiry {
font-size: 0.7rem;
font-weight: 700;
border-radius: 20px;
padding: 2px 7px;
}
.opened-expiry-ok { background: #dcfce7; color: #166534; }
.opened-expiry-soon { background: #fef9c3; color: #854d0e; }
.opened-expiry-urgent { background: #fee2e2; color: #991b1b; }
.opened-expiry-today { background: #f97316; color: #fff; }
.opened-expiry-spoiled { background: #1f2937; color: #f9fafb; }
.alert-item-spoiled {
opacity: 0.75;
}
.alert-item-spoiled .alert-item-name {
text-decoration: line-through;
color: var(--text-light);
}
.alert-more-note {
font-size: 0.8rem;
color: var(--text-light);
text-align: center;
padding: 6px 0 2px;
}
.review-hint {
font-size: 0.8rem;
color: #92400e;
+35 -8
View File
@@ -1079,13 +1079,12 @@ async function loadDashboard() {
const openedSection = document.getElementById('alert-opened');
const openedList = document.getElementById('opened-list');
if (statsData.opened && statsData.opened.length > 0) {
// Sort by remaining fraction ascending (least remaining first)
statsData.opened.sort((a, b) => {
const fA = openedFraction(a), fB = openedFraction(b);
return fA - fB;
});
// Sorted server-side by days_to_expiry ASC
openedSection.style.display = 'block';
openedList.innerHTML = statsData.opened.map(item => {
const MAX_SHOWN = 10;
const visible = statsData.opened.slice(0, MAX_SHOWN);
const extra = statsData.opened.length - visible.length;
openedList.innerHTML = visible.map(item => {
const locInfo = LOCATIONS[item.location] || { icon: '📦', label: item.location };
const qty = parseFloat(item.quantity);
const pkgSize = parseFloat(item.default_quantity);
@@ -1116,8 +1115,35 @@ async function loadDashboard() {
qtyText = `${qty}${unitLabel}`;
}
}
// Expiry badge
const days = item.days_to_expiry;
const isEdible = item.is_edible;
let expiryBadge = '';
if (days !== null && days !== undefined) {
let expiryClass, expiryText;
if (!isEdible) {
expiryClass = 'opened-expiry-spoiled';
expiryText = '⛔ Scaduto!';
} else if (days === 0) {
expiryClass = 'opened-expiry-today';
expiryText = '⚠️ Scade oggi!';
} else if (days <= 2) {
expiryClass = 'opened-expiry-urgent';
expiryText = `⏰ Scade fra ${days}gg`;
} else if (days <= 5) {
expiryClass = 'opened-expiry-soon';
expiryText = `⏰ Scade fra ${days}gg`;
} else {
expiryClass = 'opened-expiry-ok';
expiryText = `✅ Ancora ${days}gg`;
}
const vacuumNote = item.vacuum_sealed ? ' 🔒' : '';
expiryBadge = `<span class="alert-item-badge opened-expiry ${expiryClass}">${expiryText}${vacuumNote}</span>`;
}
return `
<div class="alert-item alert-item-clickable" onclick="showAlertItemDetail(${item.id}, ${item.product_id})">
<div class="alert-item alert-item-clickable${!isEdible ? ' alert-item-spoiled' : ''}" onclick="showAlertItemDetail(${item.id}, ${item.product_id})">
<div class="alert-item-info">
<span class="alert-item-name">${escapeHtml(item.name)}</span>
${item.brand ? `<span class="alert-item-brand">${escapeHtml(item.brand)}</span>` : ''}
@@ -1125,9 +1151,10 @@ async function loadDashboard() {
<div class="alert-item-badges">
<span class="alert-item-qty">${locInfo.icon} ${locInfo.label}</span>
<span class="alert-item-badge opened">${qtyText}</span>
${expiryBadge}
</div>
</div>`;
}).join('');
}).join('') + (extra > 0 ? `<div class="alert-more-note">e altri ${extra} prodotti aperti...</div>` : '');
} else {
openedSection.style.display = 'none';
}