Fix overlay blocking nav, add header scan button, fix qty defaults, improve expiry UI

- Toast: add pointer-events:none when hidden to prevent invisible overlay blocking bottom nav
- Header: add prominent camera/scan button (📷) in top-right corner with pulse animation
- Product defaults: auto-fix products saved with 'pz/1' that have weight info in notes (re-detects unit/qty from Peso field and updates DB)
- Expiry sections: show relative days ('3 giorni', 'Domani', 'Scaduto da 5 giorni') instead of absolute dates
- Inventory list + dashboard items: use relative expiry labels
- New CSS: alert-item cards with badges, expiring/expired color-coded badges
- Added daysUntilExpiry() utility function
This commit is contained in:
dadaloop82
2026-03-10 11:53:44 +00:00
parent 0104b422e4
commit 26cc00d994
3 changed files with 177 additions and 22 deletions
+85 -6
View File
@@ -94,6 +94,33 @@ body {
background: rgba(255,255,255,0.4); background: rgba(255,255,255,0.4);
} }
.header-scan-btn {
background: rgba(255,255,255,0.25);
border: 2px solid rgba(255,255,255,0.5);
color: white;
font-size: 1.5rem;
width: 48px;
height: 48px;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
animation: pulse-scan 2s ease-in-out infinite;
}
.header-scan-btn:active {
transform: scale(0.9);
background: rgba(255,255,255,0.4);
}
@keyframes pulse-scan {
0%, 100% { box-shadow: 0 2px 8px rgba(0,0,0,0.2); }
50% { box-shadow: 0 2px 16px rgba(255,255,255,0.4); }
}
/* ===== MAIN CONTENT ===== */ /* ===== MAIN CONTENT ===== */
.app-content { .app-content {
max-width: 600px; max-width: 600px;
@@ -196,13 +223,16 @@ body {
background: #fef3c7; background: #fef3c7;
border: 2px solid var(--warning); border: 2px solid var(--warning);
border-radius: var(--radius); border-radius: var(--radius);
padding: 14px; padding: 16px;
margin-bottom: 12px; margin-bottom: 12px;
} }
.alert-section h3 { .alert-section h3 {
font-size: 1rem; font-size: 1.05rem;
margin-bottom: 8px; margin-bottom: 10px;
display: flex;
align-items: center;
gap: 6px;
} }
.alert-danger { .alert-danger {
@@ -214,13 +244,60 @@ body {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 6px 0; padding: 10px 12px;
border-bottom: 1px solid rgba(0,0,0,0.1); margin-bottom: 6px;
background: rgba(255,255,255,0.7);
border-radius: var(--radius-sm);
font-size: 0.9rem; font-size: 0.9rem;
gap: 8px;
} }
.alert-item:last-child { .alert-item:last-child {
border-bottom: none; margin-bottom: 0;
}
.alert-item-info {
display: flex;
flex-direction: column;
gap: 2px;
flex: 1;
min-width: 0;
}
.alert-item-name {
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.alert-item-brand {
font-size: 0.8rem;
color: var(--text-light);
}
.alert-item-badge {
font-size: 0.8rem;
font-weight: 700;
padding: 4px 10px;
border-radius: 20px;
white-space: nowrap;
flex-shrink: 0;
}
.alert-item-badge.expiring {
background: var(--warning);
color: #fff;
}
.alert-item-badge.expired {
background: var(--danger);
color: #fff;
}
.alert-item-badge.today {
background: var(--danger-light);
color: #fff;
} }
/* ===== SECTION CARD ===== */ /* ===== SECTION CARD ===== */
@@ -943,6 +1020,7 @@ body {
font-weight: 600; font-weight: 600;
z-index: 1000; z-index: 1000;
opacity: 0; opacity: 0;
pointer-events: none;
transition: all 0.3s; transition: all 0.3s;
max-width: 90vw; max-width: 90vw;
text-align: center; text-align: center;
@@ -952,6 +1030,7 @@ body {
.toast.show { .toast.show {
opacity: 1; opacity: 1;
transform: translateX(-50%) translateY(0); transform: translateX(-50%) translateY(0);
pointer-events: auto;
} }
.toast.success { .toast.success {
+89 -16
View File
@@ -239,12 +239,21 @@ async function loadDashboard() {
const expiringList = document.getElementById('expiring-list'); const expiringList = document.getElementById('expiring-list');
if (statsData.expiring_soon && statsData.expiring_soon.length > 0) { if (statsData.expiring_soon && statsData.expiring_soon.length > 0) {
expiringSection.style.display = 'block'; expiringSection.style.display = 'block';
expiringList.innerHTML = statsData.expiring_soon.map(item => ` expiringList.innerHTML = statsData.expiring_soon.map(item => {
const days = daysUntilExpiry(item.expiry_date);
let badgeText, badgeClass;
if (days === 0) { badgeText = 'OGGI'; badgeClass = 'today'; }
else if (days === 1) { badgeText = 'Domani'; badgeClass = 'expiring'; }
else { badgeText = `${days} giorni`; badgeClass = 'expiring'; }
return `
<div class="alert-item"> <div class="alert-item">
<span>${item.name}${item.brand ? ' - ' + item.brand : ''}</span> <div class="alert-item-info">
<span>${formatDate(item.expiry_date)}</span> <span class="alert-item-name">${escapeHtml(item.name)}</span>
</div> ${item.brand ? `<span class="alert-item-brand">${escapeHtml(item.brand)}</span>` : ''}
`).join(''); </div>
<span class="alert-item-badge ${badgeClass}">${badgeText}</span>
</div>`;
}).join('');
} else { } else {
expiringSection.style.display = 'none'; expiringSection.style.display = 'none';
} }
@@ -254,12 +263,21 @@ async function loadDashboard() {
const expiredList = document.getElementById('expired-list'); const expiredList = document.getElementById('expired-list');
if (statsData.expired && statsData.expired.length > 0) { if (statsData.expired && statsData.expired.length > 0) {
expiredSection.style.display = 'block'; expiredSection.style.display = 'block';
expiredList.innerHTML = statsData.expired.map(item => ` expiredList.innerHTML = statsData.expired.map(item => {
const days = Math.abs(daysUntilExpiry(item.expiry_date));
let badgeText;
if (days === 0) badgeText = 'Oggi';
else if (days === 1) badgeText = 'Da ieri';
else badgeText = `Da ${days} giorni`;
return `
<div class="alert-item"> <div class="alert-item">
<span>${item.name}${item.brand ? ' - ' + item.brand : ''}</span> <div class="alert-item-info">
<span>${formatDate(item.expiry_date)}</span> <span class="alert-item-name">${escapeHtml(item.name)}</span>
</div> ${item.brand ? `<span class="alert-item-brand">${escapeHtml(item.brand)}</span>` : ''}
`).join(''); </div>
<span class="alert-item-badge expired">${badgeText}</span>
</div>`;
}).join('');
} else { } else {
expiredSection.style.display = 'none'; expiredSection.style.display = 'none';
} }
@@ -290,10 +308,20 @@ async function loadDashboard() {
function renderDashItem(item) { function renderDashItem(item) {
const catIcon = CATEGORY_ICONS[item.category] || '📦'; const catIcon = CATEGORY_ICONS[item.category] || '📦';
const isExpired = item.expiry_date && new Date(item.expiry_date) < new Date(); const days = daysUntilExpiry(item.expiry_date);
const isExpiring = item.expiry_date && !isExpired && new Date(item.expiry_date) <= new Date(Date.now() + 7 * 86400000); const isExpired = days < 0;
const isExpiring = !isExpired && days <= 7;
const qtyDisplay = formatQuantity(item.quantity, item.unit); const qtyDisplay = formatQuantity(item.quantity, item.unit);
let expiryLabel = '';
if (item.expiry_date) {
if (days < 0) expiryLabel = `⚠️ Scaduto da ${Math.abs(days)}g`;
else if (days === 0) expiryLabel = '⚠️ Scade oggi!';
else if (days === 1) expiryLabel = '⏰ Scade domani';
else if (days <= 7) expiryLabel = `${days} giorni`;
else expiryLabel = formatDate(item.expiry_date);
}
return ` return `
<div class="inventory-item compact-item" onclick="dashItemTap(${item.id}, ${item.product_id})"> <div class="inventory-item compact-item" onclick="dashItemTap(${item.id}, ${item.product_id})">
<div class="inv-image"> <div class="inv-image">
@@ -305,7 +333,7 @@ function renderDashItem(item) {
</div> </div>
<div class="inv-qty-right"> <div class="inv-qty-right">
<span class="inv-qty-value">${qtyDisplay}</span> <span class="inv-qty-value">${qtyDisplay}</span>
${item.expiry_date ? `<span class="inv-expiry-small ${isExpired ? 'expired' : isExpiring ? 'expiring' : ''}">${isExpired ? '⚠️' : ''} ${formatDate(item.expiry_date)}</span>` : ''} ${expiryLabel ? `<span class="inv-expiry-small ${isExpired ? 'expired' : isExpiring ? 'expiring' : ''}">${expiryLabel}</span>` : ''}
</div> </div>
</div>`; </div>`;
} }
@@ -348,10 +376,22 @@ function renderInventory(items) {
container.innerHTML = items.map(item => { container.innerHTML = items.map(item => {
const catIcon = CATEGORY_ICONS[item.category] || '📦'; const catIcon = CATEGORY_ICONS[item.category] || '📦';
const locInfo = LOCATIONS[item.location] || { icon: '📦', label: item.location }; const locInfo = LOCATIONS[item.location] || { icon: '📦', label: item.location };
const isExpired = item.expiry_date && new Date(item.expiry_date) < new Date(); const days = daysUntilExpiry(item.expiry_date);
const isExpiring = item.expiry_date && !isExpired && new Date(item.expiry_date) <= new Date(Date.now() + 7 * 86400000); const isExpired = days < 0;
const isExpiring = !isExpired && days <= 7;
const qtyDisplay = formatQuantity(item.quantity, item.unit); const qtyDisplay = formatQuantity(item.quantity, item.unit);
let expiryBadge = '';
if (item.expiry_date) {
let expiryText;
if (isExpired) expiryText = `⚠️ Scaduto da ${Math.abs(days)}g`;
else if (days === 0) expiryText = '⚠️ Scade oggi!';
else if (days === 1) expiryText = '⏰ Domani';
else if (days <= 7) expiryText = `${days} giorni`;
else expiryText = formatDate(item.expiry_date);
expiryBadge = `<span class="inv-badge ${isExpired ? 'badge-expired' : isExpiring ? 'badge-expiry' : ''}">${expiryText}</span>`;
}
return ` return `
<div class="inventory-item" onclick="showItemDetail(${item.id}, ${item.product_id})"> <div class="inventory-item" onclick="showItemDetail(${item.id}, ${item.product_id})">
<div class="inv-image"> <div class="inv-image">
@@ -363,7 +403,7 @@ function renderInventory(items) {
<div class="inv-meta"> <div class="inv-meta">
<span class="inv-badge badge-location">${locInfo.icon} ${locInfo.label}</span> <span class="inv-badge badge-location">${locInfo.icon} ${locInfo.label}</span>
<span class="inv-badge badge-qty">${qtyDisplay}</span> <span class="inv-badge badge-qty">${qtyDisplay}</span>
${item.expiry_date ? `<span class="inv-badge ${isExpired ? 'badge-expired' : isExpiring ? 'badge-expiry' : ''}">${isExpired ? '⚠️ ' : ''}${formatDate(item.expiry_date)}</span>` : ''} ${expiryBadge}
</div> </div>
</div> </div>
</div>`; </div>`;
@@ -680,6 +720,31 @@ async function onBarcodeDetected(barcode) {
const localResult = await api('search_barcode', { barcode }); const localResult = await api('search_barcode', { barcode });
if (localResult.found) { if (localResult.found) {
currentProduct = localResult.product; currentProduct = localResult.product;
// If product was saved with 'pz' but has weight info in notes, fix defaults
if (currentProduct.unit === 'pz' && currentProduct.default_quantity <= 1 && currentProduct.notes) {
const pesoMatch = currentProduct.notes.match(/Peso:\s*([^·]+)/);
if (pesoMatch) {
const weightStr = pesoMatch[1].trim();
const detected = detectUnitAndQuantity(weightStr);
if (detected.unit !== 'pz') {
currentProduct.unit = detected.unit;
currentProduct.default_quantity = detected.quantity;
currentProduct.weight_info = weightStr;
// Update product in DB for future scans
api('product_save', {}, 'POST', {
id: currentProduct.id,
barcode: currentProduct.barcode,
name: currentProduct.name,
brand: currentProduct.brand || '',
category: currentProduct.category || '',
image_url: currentProduct.image_url || '',
unit: detected.unit,
default_quantity: detected.quantity,
notes: currentProduct.notes,
});
}
}
}
// Extract weight_info from notes if available (stored as "Peso: 500 g · ...") // Extract weight_info from notes if available (stored as "Peso: 500 g · ...")
if (!currentProduct.weight_info && currentProduct.notes) { if (!currentProduct.weight_info && currentProduct.notes) {
const pesoMatch = currentProduct.notes.match(/Peso:\s*([^·]+)/); const pesoMatch = currentProduct.notes.match(/Peso:\s*([^·]+)/);
@@ -1765,6 +1830,14 @@ function formatDateTime(dtStr) {
d.toLocaleTimeString('it-IT', { hour: '2-digit', minute: '2-digit' }); d.toLocaleTimeString('it-IT', { hour: '2-digit', minute: '2-digit' });
} }
function daysUntilExpiry(dateStr) {
if (!dateStr) return Infinity;
const expiry = new Date(dateStr + 'T00:00:00');
const today = new Date();
today.setHours(0, 0, 0, 0);
return Math.round((expiry - today) / 86400000);
}
function adjustQty(inputId, delta) { function adjustQty(inputId, delta) {
const input = document.getElementById(inputId); const input = document.getElementById(inputId);
let val = parseFloat(input.value) || 0; let val = parseFloat(input.value) || 0;
+3
View File
@@ -19,6 +19,9 @@
<header class="app-header"> <header class="app-header">
<div class="header-content"> <div class="header-content">
<h1 class="header-title" onclick="showPage('dashboard')">🏠 Dispensa</h1> <h1 class="header-title" onclick="showPage('dashboard')">🏠 Dispensa</h1>
<button class="header-scan-btn" onclick="showPage('scan')" title="Scansiona prodotto">
📷
</button>
</div> </div>
</header> </header>