Add 'Da revisionare' dashboard section for suspicious quantities
- Flags inventory items with abnormally small or large quantities
- Per-unit thresholds (pz/conf, g, kg, ml, l)
- Confirm button (✓) to mark as correct (stored in localStorage)
- Edit button (✏️) opens existing edit modal
- Smooth dismiss animation on confirm
- Amber-themed styling to distinguish from expiry alerts
This commit is contained in:
@@ -2579,6 +2579,127 @@ body {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
/* ===== REVIEW SECTION ===== */
|
||||
.alert-review {
|
||||
background: #fffbeb;
|
||||
border-color: #f59e0b;
|
||||
}
|
||||
|
||||
.alert-review h3 {
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.review-hint {
|
||||
font-size: 0.8rem;
|
||||
color: #92400e;
|
||||
margin: -4px 0 10px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.review-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
background: rgba(255,255,255,0.7);
|
||||
border-radius: var(--radius-sm);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.review-item-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.review-item-icon {
|
||||
font-size: 1.5rem;
|
||||
flex-shrink: 0;
|
||||
width: 36px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.review-item-icon img {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.review-item-text {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.review-item-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.review-item-meta {
|
||||
font-size: 0.75rem;
|
||||
color: #78716c;
|
||||
}
|
||||
|
||||
.review-warn {
|
||||
color: #dc2626;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.review-item-qty {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.review-qty-value {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 800;
|
||||
color: #dc2626;
|
||||
background: #fee2e2;
|
||||
padding: 3px 10px;
|
||||
border-radius: 16px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.review-item-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.btn-review {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-review-ok {
|
||||
background: #d1fae5;
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.btn-review-ok:active {
|
||||
background: #a7f3d0;
|
||||
}
|
||||
|
||||
.btn-review-edit {
|
||||
background: #e0e7ff;
|
||||
color: #4338ca;
|
||||
}
|
||||
|
||||
.btn-review-edit:active {
|
||||
background: #c7d2fe;
|
||||
}
|
||||
|
||||
/* ===== ALERT QUANTITY BADGES ===== */
|
||||
.alert-item-qty {
|
||||
font-size: 0.78rem;
|
||||
|
||||
@@ -619,11 +619,117 @@ async function loadDashboard() {
|
||||
expiredSection.style.display = 'none';
|
||||
}
|
||||
|
||||
// Review suspicious quantities
|
||||
loadReviewItems();
|
||||
|
||||
} catch (err) {
|
||||
console.error('Dashboard load error:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// === SUSPICIOUS QUANTITY REVIEW ===
|
||||
const QTY_THRESHOLDS = {
|
||||
'pz': { min: 0.3, max: 50 },
|
||||
'conf': { min: 0.3, max: 50 },
|
||||
'g': { min: 3, max: 10000 },
|
||||
'kg': { min: 0.005, max: 50 },
|
||||
'ml': { min: 3, max: 10000 },
|
||||
'l': { min: 0.005, max: 50 },
|
||||
};
|
||||
|
||||
function isSuspiciousQty(qty, unit) {
|
||||
const n = parseFloat(qty);
|
||||
if (isNaN(n) || n <= 0) return false;
|
||||
const t = QTY_THRESHOLDS[unit] || QTY_THRESHOLDS['pz'];
|
||||
return n < t.min || n > t.max;
|
||||
}
|
||||
|
||||
function getReviewConfirmed() {
|
||||
try { return JSON.parse(localStorage.getItem('review_confirmed') || '{}'); } catch(e) { return {}; }
|
||||
}
|
||||
|
||||
function setReviewConfirmed(inventoryId) {
|
||||
const c = getReviewConfirmed();
|
||||
c[inventoryId] = Date.now();
|
||||
localStorage.setItem('review_confirmed', JSON.stringify(c));
|
||||
}
|
||||
|
||||
async function loadReviewItems() {
|
||||
const section = document.getElementById('alert-review');
|
||||
const list = document.getElementById('review-list');
|
||||
try {
|
||||
const data = await api('inventory_list');
|
||||
const items = data.inventory || [];
|
||||
const confirmed = getReviewConfirmed();
|
||||
|
||||
const suspicious = items.filter(item => {
|
||||
if (confirmed[item.id]) return false;
|
||||
return isSuspiciousQty(item.quantity, item.unit);
|
||||
});
|
||||
|
||||
if (suspicious.length === 0) {
|
||||
section.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
section.style.display = 'block';
|
||||
list.innerHTML = suspicious.map(item => {
|
||||
const catIcon = CATEGORY_ICONS[mapToLocalCategory(item.category, item.name)] || '📦';
|
||||
const qtyDisplay = formatQuantity(item.quantity, item.unit);
|
||||
const locInfo = LOCATIONS[item.location] || { icon: '📦', label: item.location };
|
||||
const t = QTY_THRESHOLDS[item.unit] || QTY_THRESHOLDS['pz'];
|
||||
const isTooSmall = parseFloat(item.quantity) < t.min;
|
||||
const warning = isTooSmall ? '⬇️ Troppo poco' : '⬆️ Troppo';
|
||||
|
||||
return `
|
||||
<div class="review-item" id="review-item-${item.id}">
|
||||
<div class="review-item-info">
|
||||
<span class="review-item-icon">${item.image_url ? `<img src="${escapeHtml(item.image_url)}" alt="">` : catIcon}</span>
|
||||
<div class="review-item-text">
|
||||
<div class="review-item-name">${escapeHtml(item.name)}</div>
|
||||
<div class="review-item-meta">${locInfo.icon} ${locInfo.label} · <span class="review-warn">${warning}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="review-item-qty">
|
||||
<span class="review-qty-value">${qtyDisplay}</span>
|
||||
</div>
|
||||
<div class="review-item-actions">
|
||||
<button class="btn-review btn-review-ok" onclick="confirmReviewItem(${item.id})" title="È corretto">✓</button>
|
||||
<button class="btn-review btn-review-edit" onclick="editReviewItem(${item.id}, ${item.product_id})" title="Modifica">✏️</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
} catch(e) {
|
||||
section.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function confirmReviewItem(inventoryId) {
|
||||
setReviewConfirmed(inventoryId);
|
||||
const el = document.getElementById(`review-item-${inventoryId}`);
|
||||
if (el) {
|
||||
el.style.transition = 'opacity 0.3s, transform 0.3s';
|
||||
el.style.opacity = '0';
|
||||
el.style.transform = 'translateX(60px)';
|
||||
setTimeout(() => {
|
||||
el.remove();
|
||||
// Hide section if empty
|
||||
const list = document.getElementById('review-list');
|
||||
if (!list.children.length) {
|
||||
document.getElementById('alert-review').style.display = 'none';
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
showToast('✓ Quantità confermata', 'success');
|
||||
}
|
||||
|
||||
function editReviewItem(inventoryId, productId) {
|
||||
api('inventory_list').then(data => {
|
||||
currentInventory = data.inventory || [];
|
||||
showItemDetail(inventoryId, productId);
|
||||
});
|
||||
}
|
||||
|
||||
// Group items by local category and render with category headers
|
||||
function renderGroupedByCategory(items, compact = false) {
|
||||
const catGroups = {};
|
||||
|
||||
Binary file not shown.
@@ -64,6 +64,13 @@
|
||||
<div id="expiring-list"></div>
|
||||
</div>
|
||||
|
||||
<!-- Review suspicious quantities -->
|
||||
<div class="alert-section alert-review" id="alert-review" style="display:none">
|
||||
<h3>🔍 Da revisionare</h3>
|
||||
<p class="review-hint">Quantità che sembrano anomale. Conferma se corrette o modifica.</p>
|
||||
<div id="review-list"></div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<!-- ===== INVENTORY LIST ===== -->
|
||||
|
||||
Reference in New Issue
Block a user