feat(freezer): remove expiration tracking for freezer compatibility and clean stock management
Build and Push Fridger Container / Build & Push Fridger Image (push) Successful in 1m20s
Build and Push Fridger Container / Build & Push Fridger Image (push) Successful in 1m20s
This commit is contained in:
+16
-104
@@ -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() {
|
||||
<div class="shelf-card rounded-2xl sm:rounded-3xl p-4 sm:p-5 flex flex-col justify-between transition hover:border-teal-500/40 space-y-3.5">
|
||||
|
||||
<div>
|
||||
<!-- Header: Brand & Expiry -->
|
||||
<!-- Header: Brand & Status Pill -->
|
||||
<div class="flex items-center justify-between gap-2 mb-2">
|
||||
<span class="text-xs font-bold text-teal-400 bg-teal-500/10 px-2.5 py-1 rounded-lg truncate max-w-[140px]">
|
||||
${escapeHtml(item.brand || item.category)}
|
||||
</span>
|
||||
<span class="text-xs font-bold px-2.5 py-1 rounded-lg flex items-center gap-1.5 shrink-0 ${expiryInfo.badgeClass}">
|
||||
<i data-lucide="${expiryInfo.icon}" class="w-3.5 h-3.5"></i>
|
||||
<span>${expiryInfo.label}</span>
|
||||
<span class="text-xs font-bold px-2.5 py-1 rounded-lg flex items-center gap-1.5 shrink-0 ${isOpened ? 'bg-indigo-500/20 text-indigo-300 border border-indigo-500/30' : 'bg-emerald-500/15 text-emerald-300 border border-emerald-500/20'}">
|
||||
<span>${isOpened ? 'Opened / In Use' : 'Full Pack'}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -314,7 +314,7 @@ function renderManageQuickPicker(filterQuery = '') {
|
||||
if (matched.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="col-span-full py-4 text-center text-xs text-slate-500">
|
||||
No matching fridge items found. Scan barcode or add as new product!
|
||||
No matching items found. Scan barcode or add as new product!
|
||||
</div>
|
||||
`;
|
||||
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 = `<i data-lucide="${getCategoryIcon(item.category)}" class="w-7 h-7 text-teal-400"></i>`;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user