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:
+85
-6
@@ -94,6 +94,33 @@ body {
|
||||
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 ===== */
|
||||
.app-content {
|
||||
max-width: 600px;
|
||||
@@ -196,13 +223,16 @@ body {
|
||||
background: #fef3c7;
|
||||
border: 2px solid var(--warning);
|
||||
border-radius: var(--radius);
|
||||
padding: 14px;
|
||||
padding: 16px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.alert-section h3 {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 8px;
|
||||
font-size: 1.05rem;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
@@ -214,13 +244,60 @@ body {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px solid rgba(0,0,0,0.1);
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 6px;
|
||||
background: rgba(255,255,255,0.7);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.9rem;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.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 ===== */
|
||||
@@ -943,6 +1020,7 @@ body {
|
||||
font-weight: 600;
|
||||
z-index: 1000;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: all 0.3s;
|
||||
max-width: 90vw;
|
||||
text-align: center;
|
||||
@@ -952,6 +1030,7 @@ body {
|
||||
.toast.show {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.toast.success {
|
||||
|
||||
+87
-14
@@ -239,12 +239,21 @@ async function loadDashboard() {
|
||||
const expiringList = document.getElementById('expiring-list');
|
||||
if (statsData.expiring_soon && statsData.expiring_soon.length > 0) {
|
||||
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">
|
||||
<span>${item.name}${item.brand ? ' - ' + item.brand : ''}</span>
|
||||
<span>${formatDate(item.expiry_date)}</span>
|
||||
<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>` : ''}
|
||||
</div>
|
||||
`).join('');
|
||||
<span class="alert-item-badge ${badgeClass}">${badgeText}</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
} else {
|
||||
expiringSection.style.display = 'none';
|
||||
}
|
||||
@@ -254,12 +263,21 @@ async function loadDashboard() {
|
||||
const expiredList = document.getElementById('expired-list');
|
||||
if (statsData.expired && statsData.expired.length > 0) {
|
||||
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">
|
||||
<span>${item.name}${item.brand ? ' - ' + item.brand : ''}</span>
|
||||
<span>${formatDate(item.expiry_date)}</span>
|
||||
<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>` : ''}
|
||||
</div>
|
||||
`).join('');
|
||||
<span class="alert-item-badge expired">${badgeText}</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
} else {
|
||||
expiredSection.style.display = 'none';
|
||||
}
|
||||
@@ -290,10 +308,20 @@ async function loadDashboard() {
|
||||
|
||||
function renderDashItem(item) {
|
||||
const catIcon = CATEGORY_ICONS[item.category] || '📦';
|
||||
const isExpired = item.expiry_date && new Date(item.expiry_date) < new Date();
|
||||
const isExpiring = item.expiry_date && !isExpired && new Date(item.expiry_date) <= new Date(Date.now() + 7 * 86400000);
|
||||
const days = daysUntilExpiry(item.expiry_date);
|
||||
const isExpired = days < 0;
|
||||
const isExpiring = !isExpired && days <= 7;
|
||||
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 `
|
||||
<div class="inventory-item compact-item" onclick="dashItemTap(${item.id}, ${item.product_id})">
|
||||
<div class="inv-image">
|
||||
@@ -305,7 +333,7 @@ function renderDashItem(item) {
|
||||
</div>
|
||||
<div class="inv-qty-right">
|
||||
<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>`;
|
||||
}
|
||||
@@ -348,10 +376,22 @@ function renderInventory(items) {
|
||||
container.innerHTML = items.map(item => {
|
||||
const catIcon = CATEGORY_ICONS[item.category] || '📦';
|
||||
const locInfo = LOCATIONS[item.location] || { icon: '📦', label: item.location };
|
||||
const isExpired = item.expiry_date && new Date(item.expiry_date) < new Date();
|
||||
const isExpiring = item.expiry_date && !isExpired && new Date(item.expiry_date) <= new Date(Date.now() + 7 * 86400000);
|
||||
const days = daysUntilExpiry(item.expiry_date);
|
||||
const isExpired = days < 0;
|
||||
const isExpiring = !isExpired && days <= 7;
|
||||
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 `
|
||||
<div class="inventory-item" onclick="showItemDetail(${item.id}, ${item.product_id})">
|
||||
<div class="inv-image">
|
||||
@@ -363,7 +403,7 @@ function renderInventory(items) {
|
||||
<div class="inv-meta">
|
||||
<span class="inv-badge badge-location">${locInfo.icon} ${locInfo.label}</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>`;
|
||||
@@ -680,6 +720,31 @@ async function onBarcodeDetected(barcode) {
|
||||
const localResult = await api('search_barcode', { barcode });
|
||||
if (localResult.found) {
|
||||
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 · ...")
|
||||
if (!currentProduct.weight_info && currentProduct.notes) {
|
||||
const pesoMatch = currentProduct.notes.match(/Peso:\s*([^·]+)/);
|
||||
@@ -1765,6 +1830,14 @@ function formatDateTime(dtStr) {
|
||||
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) {
|
||||
const input = document.getElementById(inputId);
|
||||
let val = parseFloat(input.value) || 0;
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
<header class="app-header">
|
||||
<div class="header-content">
|
||||
<h1 class="header-title" onclick="showPage('dashboard')">🏠 Dispensa</h1>
|
||||
<button class="header-scan-btn" onclick="showPage('scan')" title="Scansiona prodotto">
|
||||
📷
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user