From 7b24efc977297d1d9e87b19b210010c60319b6bb Mon Sep 17 00:00:00 2001 From: max Date: Sat, 10 Oct 2026 23:51:57 +0200 Subject: [PATCH] feat(freezer): remove expiration tracking for freezer compatibility and clean stock management --- db.js | 53 ++++++-------------- public/app.js | 120 +++++++--------------------------------------- public/index.html | 75 +++++++++++------------------ test.js | 3 +- 4 files changed, 61 insertions(+), 190 deletions(-) diff --git a/db.js b/db.js index 4faedbc..c275f72 100644 --- a/db.js +++ b/db.js @@ -68,30 +68,15 @@ function getAllItems({ search = '', category = '', location = '', status = 'all' params.push(location); } - const today = new Date().toISOString().slice(0, 10); - - if (status === 'expired') { - query += ` AND expiry_date IS NOT NULL AND expiry_date != '' AND expiry_date < ?`; - params.push(today); - } else if (status === 'expiring_soon') { - const next3Days = new Date(Date.now() + 3 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10); - query += ` AND expiry_date IS NOT NULL AND expiry_date != '' AND expiry_date >= ? AND expiry_date <= ?`; - params.push(today, next3Days); - } else if (status === 'fresh') { - const next3Days = new Date(Date.now() + 3 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10); - query += ` AND (expiry_date IS NULL OR expiry_date = '' OR expiry_date > ?)`; - params.push(next3Days); + if (status === 'opened') { + query += ` AND (is_opened = 1 OR quantity < initial_quantity)`; + } else if (status === 'full') { + query += ` AND (is_opened = 0 AND quantity >= initial_quantity)`; } else if (status === 'low') { query += ` AND (quantity <= (initial_quantity * 0.25) OR quantity = 0)`; } - query += ` ORDER BY - CASE - WHEN expiry_date IS NULL OR expiry_date = '' THEN 1 - ELSE 0 - END, - expiry_date ASC, - updated_at DESC`; + query += ` ORDER BY updated_at DESC, name ASC`; const stmt = db.prepare(query); return stmt.all(...params); @@ -362,27 +347,21 @@ function getActivityLogs(limit = 50) { } function getStats() { - const today = new Date().toISOString().slice(0, 10); - const next3Days = new Date(Date.now() + 3 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10); - const totalStmt = db.prepare(`SELECT COUNT(*) as count FROM items`); const total = totalStmt.get().count; - const expiredStmt = db.prepare(` + const openedStmt = db.prepare(` SELECT COUNT(*) as count FROM items - WHERE expiry_date IS NOT NULL AND expiry_date != '' AND expiry_date < ? + WHERE is_opened = 1 OR quantity < initial_quantity `); - const expired = expiredStmt.get(today).count; - - const expiringSoonStmt = db.prepare(` - SELECT COUNT(*) as count FROM items - WHERE expiry_date IS NOT NULL AND expiry_date != '' AND expiry_date >= ? AND expiry_date <= ? - `); - const expiringSoon = expiringSoonStmt.get(today, next3Days).count; - - const openedStmt = db.prepare(`SELECT COUNT(*) as count FROM items WHERE is_opened = 1`); const opened = openedStmt.get().count; + const fullStmt = db.prepare(` + SELECT COUNT(*) as count FROM items + WHERE is_opened = 0 AND quantity >= initial_quantity + `); + const full = fullStmt.get().count; + const lowStmt = db.prepare(` SELECT COUNT(*) as count FROM items WHERE quantity <= (initial_quantity * 0.25) AND quantity > 0 @@ -391,11 +370,9 @@ function getStats() { return { total, - expired, - expiringSoon, + full, opened, - low, - fresh: total - expired - expiringSoon + low }; } diff --git a/public/app.js b/public/app.js index 0860135..a1be8f9 100644 --- a/public/app.js +++ b/public/app.js @@ -1,6 +1,7 @@ /** * Fridger Frontend Application - * Tablet-optimized: Main -> Scan -> Manage (Scan/Search -> Add/Remove -> Packet/Amount) + * Tablet-optimized Fridge & Freezer Inventory Manager (No expiration dates) + * Main -> Scan -> Manage (Scan/Search -> Add/Remove -> Packet/Amount) */ const state = { @@ -64,9 +65,9 @@ async function loadStats() { if (res.ok) { const stats = await res.json(); document.getElementById('stat-total').textContent = stats.total || 0; - document.getElementById('stat-fresh').textContent = stats.fresh || 0; - document.getElementById('stat-expiring').textContent = stats.expiringSoon || 0; + document.getElementById('stat-full').textContent = stats.full || 0; document.getElementById('stat-opened').textContent = stats.opened || 0; + document.getElementById('stat-low').textContent = stats.low || 0; } } catch (err) { console.error('Failed to load stats:', err); @@ -88,7 +89,7 @@ async function loadItems() { } } catch (err) { console.error('Failed to load items:', err); - showToast('Failed to load fridge items', 'error'); + showToast('Failed to load inventory', 'error'); } } @@ -209,7 +210,7 @@ function populateModalSelects() { } // ---------------------------------------------------- -// UI Rendering (Main Tab: Fridge Cards) +// UI Rendering (Main Tab: Fridge / Freezer Cards) // ---------------------------------------------------- function renderApp() { @@ -225,7 +226,7 @@ function renderApp() { emptyState.classList.add('hidden'); container.innerHTML = state.items.map(item => { - const expiryInfo = getExpiryStatus(item.expiry_date); + const isOpened = item.is_opened || item.quantity < item.initial_quantity; const percent = item.initial_quantity > 0 ? Math.min(100, Math.max(0, Math.round((item.quantity / item.initial_quantity) * 100))) : 100; @@ -238,14 +239,13 @@ function renderApp() {
- +
${escapeHtml(item.brand || item.category)} - - - ${expiryInfo.label} + + ${isOpened ? 'Opened / In Use' : 'Full Pack'}
@@ -314,7 +314,7 @@ function renderManageQuickPicker(filterQuery = '') { if (matched.length === 0) { container.innerHTML = `
- No matching fridge items found. Scan barcode or add as new product! + No matching items found. Scan barcode or add as new product!
`; return; @@ -347,7 +347,6 @@ function renderManageQuickPicker(filterQuery = '') { function handleManageSearch(val) { renderManageQuickPicker(val); - // If barcode entered directly (e.g. 8-14 digits) and matches an item const clean = val.trim(); if (/^\d{8,14}$/.test(clean)) { const found = state.items.find(i => i.barcode === clean); @@ -368,9 +367,8 @@ function selectManageItem(item) { state.manage.packetCount = 1; state.manage.amountValue = item.quantity; - // Populate Selected Product Banner document.getElementById('manage-selected-name').textContent = item.name; - document.getElementById('manage-selected-details').textContent = `Currently: ${item.quantity} ${item.unit} in fridge • 1 Full Pack = ${item.initial_quantity} ${item.unit}`; + document.getElementById('manage-selected-details').textContent = `Currently: ${item.quantity} ${item.unit} in stock • 1 Full Pack = ${item.initial_quantity} ${item.unit}`; document.getElementById('manage-pack-size-label').textContent = `${item.initial_quantity} ${item.unit}`; document.getElementById('manage-amount-unit').textContent = item.unit; document.getElementById('manage-amount-val').textContent = item.quantity; @@ -387,17 +385,13 @@ function selectManageItem(item) { imgBox.innerHTML = ``; } - // Reveal Action Card document.getElementById('manage-action-card').classList.remove('hidden'); document.getElementById('manage-qty-type-section').classList.add('hidden'); - // Reset Action Button Styles resetActionButtonsStyle(); - renderManageQuickPicker(); initLucide(); - // Scroll smoothly to action card on tablet document.getElementById('manage-action-card').scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } @@ -427,13 +421,13 @@ function setManageAction(action) { btnRemove.className = 'p-4 sm:p-5 rounded-2xl border-2 text-center font-extrabold transition-all active:scale-95 touch-manipulation flex flex-col items-center justify-center gap-2 min-h-[80px] bg-slate-950 border-slate-800 text-slate-400 hover:border-slate-700'; step3Title.textContent = `How much ${state.manage.selectedItem.name} to ADD?`; confirmBtn.className = 'w-full py-4 rounded-2xl bg-emerald-500 hover:bg-emerald-400 text-white font-black text-base sm:text-lg shadow-xl shadow-emerald-500/25 active:scale-95 transition-all touch-manipulation flex items-center justify-center gap-2'; - confirmLabel.textContent = `Confirm Add to Fridge`; + confirmLabel.textContent = `Confirm Add to Stock`; } else { btnRemove.className = 'p-4 sm:p-5 rounded-2xl border-2 text-center font-extrabold transition-all active:scale-95 touch-manipulation flex flex-col items-center justify-center gap-2 min-h-[80px] bg-rose-500/20 border-rose-500 text-white shadow-lg shadow-rose-500/20 ring-2 ring-rose-500/40'; btnAdd.className = 'p-4 sm:p-5 rounded-2xl border-2 text-center font-extrabold transition-all active:scale-95 touch-manipulation flex flex-col items-center justify-center gap-2 min-h-[80px] bg-slate-950 border-slate-800 text-slate-400 hover:border-slate-700'; step3Title.textContent = `How much ${state.manage.selectedItem.name} to REMOVE?`; confirmBtn.className = 'w-full py-4 rounded-2xl bg-rose-500 hover:bg-rose-400 text-white font-black text-base sm:text-lg shadow-xl shadow-rose-500/25 active:scale-95 transition-all touch-manipulation flex items-center justify-center gap-2'; - confirmLabel.textContent = `Confirm Remove from Fridge`; + confirmLabel.textContent = `Confirm Remove from Stock`; } qtySection.classList.remove('hidden'); @@ -473,7 +467,6 @@ function setQtyMode(mode) { initLucide(); } -// Packet count stepper function changePacketCount(delta) { state.manage.packetCount = Math.max(1, state.manage.packetCount + delta); document.getElementById('manage-pack-count').textContent = state.manage.packetCount; @@ -484,7 +477,6 @@ function setPacketCount(num) { document.getElementById('manage-pack-count').textContent = num; } -// Specific amount slider & delta function onManageAmountSlider(val) { state.manage.amountValue = parseFloat(val); document.getElementById('manage-amount-val').textContent = state.manage.amountValue; @@ -503,10 +495,6 @@ function setManageAmountRatio(ratio) { document.getElementById('manage-amount-val').textContent = state.manage.amountValue; } -// ---------------------------------------------------- -// EXECUTION: Commit Add / Remove -// ---------------------------------------------------- - async function confirmManageExecution() { const item = state.manage.selectedItem; const action = state.manage.action; @@ -529,7 +517,7 @@ async function confirmManageExecution() { if (res.ok) { const updated = await res.json(); playBeepSound(); - showToast(`✓ ${action === 'add' ? 'Added' : 'Removed'} ${state.manage.packetCount} packet(s) of ${updated.name} (Now: ${updated.quantity} ${updated.unit})`, 'success'); + showToast(`✓ ${action === 'add' ? 'Added' : 'Removed'} ${state.manage.packetCount} pack(s) of ${updated.name} (Now: ${updated.quantity} ${updated.unit})`, 'success'); clearSelectedManageItem(); await loadStats(); await loadItems(); @@ -537,7 +525,6 @@ async function confirmManageExecution() { showToast('Failed to update packet', 'error'); } } else { - // Specific Grams / Amount mode const newQty = state.manage.amountValue; const res = await fetch(`/api/items/${item.id}/quantity`, { method: 'PATCH', @@ -585,7 +572,6 @@ function openItemModal(itemId = null) { document.getElementById('modal-initial-quantity').value = item.initial_quantity || item.quantity; document.getElementById('modal-unit').value = item.unit || 'g'; document.getElementById('modal-category').value = item.category || 'General'; - document.getElementById('modal-expiry').value = item.expiry_date || ''; document.getElementById('modal-notes').value = item.notes || ''; if (item.image_url) { @@ -658,11 +644,6 @@ function applyProductDataToForm(data) { } } -function setModalExpiryDays(days) { - const date = new Date(Date.now() + days * 24 * 60 * 60 * 1000); - document.getElementById('modal-expiry').value = date.toISOString().slice(0, 10); -} - async function handleItemFormSubmit(event) { event.preventDefault(); @@ -678,7 +659,6 @@ async function handleItemFormSubmit(event) { initial_quantity: initQty, unit: document.getElementById('modal-unit').value, category: document.getElementById('modal-category').value, - expiry_date: document.getElementById('modal-expiry').value || null, notes: document.getElementById('modal-notes').value.trim(), image_url: document.getElementById('modal-image-url').value || null }; @@ -695,11 +675,10 @@ async function handleItemFormSubmit(event) { if (res.ok) { const saved = await res.json(); - showToast(itemId ? 'Product updated' : 'Product added to fridge', 'success'); + showToast(itemId ? 'Product updated' : 'Product added', 'success'); closeItemModal(); await loadStats(); await loadItems(); - // Auto-select in manage tab selectManageItem(saved); switchTab('manage'); } else { @@ -779,7 +758,6 @@ async function onBarcodeScannedSuccess(decodedText) { stopCameraScanner(); closeScannerModal(); - // Check if item exists in fridge const existing = state.items.find(i => i.barcode === decodedText); if (existing) { switchTab('manage'); @@ -788,7 +766,6 @@ async function onBarcodeScannedSuccess(decodedText) { return; } - // Otherwise fetch from API / Open Food Facts try { const res = await fetch(`/api/barcode/${encodeURIComponent(decodedText)}`); if (res.ok) { @@ -832,58 +809,6 @@ function playBeepSound() { } catch (e) {} } -// ---------------------------------------------------- -// Helpers -// ---------------------------------------------------- - -function getExpiryStatus(expiryDateStr) { - if (!expiryDateStr) { - return { label: 'No Expiry', badgeClass: 'bg-slate-800 text-slate-400', icon: 'calendar' }; - } - - const today = new Date(); - today.setHours(0, 0, 0, 0); - - const expDate = new Date(expiryDateStr); - expDate.setHours(0, 0, 0, 0); - - const diffDays = Math.round((expDate - today) / (1000 * 60 * 60 * 24)); - - if (diffDays < 0) { - return { - label: `Expired ${Math.abs(diffDays)}d ago`, - badgeClass: 'bg-rose-500/20 text-rose-300 border border-rose-500/30', - icon: 'alert-triangle' - }; - } - if (diffDays === 0) { - return { - label: 'Expires Today!', - badgeClass: 'bg-rose-500/20 text-rose-300 border border-rose-500/30 font-bold', - icon: 'alert-circle' - }; - } - if (diffDays === 1) { - return { - label: 'Expires Tomorrow', - badgeClass: 'bg-amber-500/20 text-amber-300 border border-amber-500/30', - icon: 'clock' - }; - } - if (diffDays <= 3) { - return { - label: `${diffDays} days left`, - badgeClass: 'bg-amber-500/20 text-amber-300 border border-amber-500/30', - icon: 'clock' - }; - } - return { - label: `${diffDays} days left`, - badgeClass: 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20', - icon: 'calendar-check' - }; -} - function getCategoryIcon(category) { const cat = state.meta.categories.find(c => c.id === category); return cat ? cat.icon : 'package'; @@ -924,16 +849,3 @@ function showToast(message, type = 'info') { setTimeout(() => toast.remove(), 300); }, 3000); } - -async function seedDemoData() { - try { - const res = await fetch('/api/seed?force=true', { method: 'POST' }); - if (res.ok) { - showToast('Loaded sample items!', 'success'); - await loadStats(); - await loadItems(); - } - } catch (err) { - console.error(err); - } -} diff --git a/public/index.html b/public/index.html index a474a24..d9b8e51 100644 --- a/public/index.html +++ b/public/index.html @@ -3,7 +3,7 @@ - Fridger - Tablet Fridge Manager + Fridger - Inventory & Stock Manager @@ -46,14 +46,14 @@