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:
@@ -68,30 +68,15 @@ function getAllItems({ search = '', category = '', location = '', status = 'all'
|
|||||||
params.push(location);
|
params.push(location);
|
||||||
}
|
}
|
||||||
|
|
||||||
const today = new Date().toISOString().slice(0, 10);
|
if (status === 'opened') {
|
||||||
|
query += ` AND (is_opened = 1 OR quantity < initial_quantity)`;
|
||||||
if (status === 'expired') {
|
} else if (status === 'full') {
|
||||||
query += ` AND expiry_date IS NOT NULL AND expiry_date != '' AND expiry_date < ?`;
|
query += ` AND (is_opened = 0 AND quantity >= initial_quantity)`;
|
||||||
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);
|
|
||||||
} else if (status === 'low') {
|
} else if (status === 'low') {
|
||||||
query += ` AND (quantity <= (initial_quantity * 0.25) OR quantity = 0)`;
|
query += ` AND (quantity <= (initial_quantity * 0.25) OR quantity = 0)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
query += ` ORDER BY
|
query += ` ORDER BY updated_at DESC, name ASC`;
|
||||||
CASE
|
|
||||||
WHEN expiry_date IS NULL OR expiry_date = '' THEN 1
|
|
||||||
ELSE 0
|
|
||||||
END,
|
|
||||||
expiry_date ASC,
|
|
||||||
updated_at DESC`;
|
|
||||||
|
|
||||||
const stmt = db.prepare(query);
|
const stmt = db.prepare(query);
|
||||||
return stmt.all(...params);
|
return stmt.all(...params);
|
||||||
@@ -362,27 +347,21 @@ function getActivityLogs(limit = 50) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getStats() {
|
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 totalStmt = db.prepare(`SELECT COUNT(*) as count FROM items`);
|
||||||
const total = totalStmt.get().count;
|
const total = totalStmt.get().count;
|
||||||
|
|
||||||
const expiredStmt = db.prepare(`
|
const openedStmt = db.prepare(`
|
||||||
SELECT COUNT(*) as count FROM items
|
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 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(`
|
const lowStmt = db.prepare(`
|
||||||
SELECT COUNT(*) as count FROM items
|
SELECT COUNT(*) as count FROM items
|
||||||
WHERE quantity <= (initial_quantity * 0.25) AND quantity > 0
|
WHERE quantity <= (initial_quantity * 0.25) AND quantity > 0
|
||||||
@@ -391,11 +370,9 @@ function getStats() {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
total,
|
total,
|
||||||
expired,
|
full,
|
||||||
expiringSoon,
|
|
||||||
opened,
|
opened,
|
||||||
low,
|
low
|
||||||
fresh: total - expired - expiringSoon
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+16
-104
@@ -1,6 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Fridger Frontend Application
|
* 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 = {
|
const state = {
|
||||||
@@ -64,9 +65,9 @@ async function loadStats() {
|
|||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const stats = await res.json();
|
const stats = await res.json();
|
||||||
document.getElementById('stat-total').textContent = stats.total || 0;
|
document.getElementById('stat-total').textContent = stats.total || 0;
|
||||||
document.getElementById('stat-fresh').textContent = stats.fresh || 0;
|
document.getElementById('stat-full').textContent = stats.full || 0;
|
||||||
document.getElementById('stat-expiring').textContent = stats.expiringSoon || 0;
|
|
||||||
document.getElementById('stat-opened').textContent = stats.opened || 0;
|
document.getElementById('stat-opened').textContent = stats.opened || 0;
|
||||||
|
document.getElementById('stat-low').textContent = stats.low || 0;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to load stats:', err);
|
console.error('Failed to load stats:', err);
|
||||||
@@ -88,7 +89,7 @@ async function loadItems() {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to load items:', 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() {
|
function renderApp() {
|
||||||
@@ -225,7 +226,7 @@ function renderApp() {
|
|||||||
emptyState.classList.add('hidden');
|
emptyState.classList.add('hidden');
|
||||||
|
|
||||||
container.innerHTML = state.items.map(item => {
|
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
|
const percent = item.initial_quantity > 0
|
||||||
? Math.min(100, Math.max(0, Math.round((item.quantity / item.initial_quantity) * 100)))
|
? Math.min(100, Math.max(0, Math.round((item.quantity / item.initial_quantity) * 100)))
|
||||||
: 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 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>
|
<div>
|
||||||
<!-- Header: Brand & Expiry -->
|
<!-- Header: Brand & Status Pill -->
|
||||||
<div class="flex items-center justify-between gap-2 mb-2">
|
<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]">
|
<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)}
|
${escapeHtml(item.brand || item.category)}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-xs font-bold px-2.5 py-1 rounded-lg flex items-center gap-1.5 shrink-0 ${expiryInfo.badgeClass}">
|
<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'}">
|
||||||
<i data-lucide="${expiryInfo.icon}" class="w-3.5 h-3.5"></i>
|
<span>${isOpened ? 'Opened / In Use' : 'Full Pack'}</span>
|
||||||
<span>${expiryInfo.label}</span>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -314,7 +314,7 @@ function renderManageQuickPicker(filterQuery = '') {
|
|||||||
if (matched.length === 0) {
|
if (matched.length === 0) {
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="col-span-full py-4 text-center text-xs text-slate-500">
|
<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>
|
</div>
|
||||||
`;
|
`;
|
||||||
return;
|
return;
|
||||||
@@ -347,7 +347,6 @@ function renderManageQuickPicker(filterQuery = '') {
|
|||||||
function handleManageSearch(val) {
|
function handleManageSearch(val) {
|
||||||
renderManageQuickPicker(val);
|
renderManageQuickPicker(val);
|
||||||
|
|
||||||
// If barcode entered directly (e.g. 8-14 digits) and matches an item
|
|
||||||
const clean = val.trim();
|
const clean = val.trim();
|
||||||
if (/^\d{8,14}$/.test(clean)) {
|
if (/^\d{8,14}$/.test(clean)) {
|
||||||
const found = state.items.find(i => i.barcode === clean);
|
const found = state.items.find(i => i.barcode === clean);
|
||||||
@@ -368,9 +367,8 @@ function selectManageItem(item) {
|
|||||||
state.manage.packetCount = 1;
|
state.manage.packetCount = 1;
|
||||||
state.manage.amountValue = item.quantity;
|
state.manage.amountValue = item.quantity;
|
||||||
|
|
||||||
// Populate Selected Product Banner
|
|
||||||
document.getElementById('manage-selected-name').textContent = item.name;
|
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-pack-size-label').textContent = `${item.initial_quantity} ${item.unit}`;
|
||||||
document.getElementById('manage-amount-unit').textContent = item.unit;
|
document.getElementById('manage-amount-unit').textContent = item.unit;
|
||||||
document.getElementById('manage-amount-val').textContent = item.quantity;
|
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>`;
|
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-action-card').classList.remove('hidden');
|
||||||
document.getElementById('manage-qty-type-section').classList.add('hidden');
|
document.getElementById('manage-qty-type-section').classList.add('hidden');
|
||||||
|
|
||||||
// Reset Action Button Styles
|
|
||||||
resetActionButtonsStyle();
|
resetActionButtonsStyle();
|
||||||
|
|
||||||
renderManageQuickPicker();
|
renderManageQuickPicker();
|
||||||
initLucide();
|
initLucide();
|
||||||
|
|
||||||
// Scroll smoothly to action card on tablet
|
|
||||||
document.getElementById('manage-action-card').scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
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';
|
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?`;
|
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';
|
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 {
|
} 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';
|
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';
|
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?`;
|
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';
|
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');
|
qtySection.classList.remove('hidden');
|
||||||
@@ -473,7 +467,6 @@ function setQtyMode(mode) {
|
|||||||
initLucide();
|
initLucide();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Packet count stepper
|
|
||||||
function changePacketCount(delta) {
|
function changePacketCount(delta) {
|
||||||
state.manage.packetCount = Math.max(1, state.manage.packetCount + delta);
|
state.manage.packetCount = Math.max(1, state.manage.packetCount + delta);
|
||||||
document.getElementById('manage-pack-count').textContent = state.manage.packetCount;
|
document.getElementById('manage-pack-count').textContent = state.manage.packetCount;
|
||||||
@@ -484,7 +477,6 @@ function setPacketCount(num) {
|
|||||||
document.getElementById('manage-pack-count').textContent = num;
|
document.getElementById('manage-pack-count').textContent = num;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Specific amount slider & delta
|
|
||||||
function onManageAmountSlider(val) {
|
function onManageAmountSlider(val) {
|
||||||
state.manage.amountValue = parseFloat(val);
|
state.manage.amountValue = parseFloat(val);
|
||||||
document.getElementById('manage-amount-val').textContent = state.manage.amountValue;
|
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;
|
document.getElementById('manage-amount-val').textContent = state.manage.amountValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------
|
|
||||||
// EXECUTION: Commit Add / Remove
|
|
||||||
// ----------------------------------------------------
|
|
||||||
|
|
||||||
async function confirmManageExecution() {
|
async function confirmManageExecution() {
|
||||||
const item = state.manage.selectedItem;
|
const item = state.manage.selectedItem;
|
||||||
const action = state.manage.action;
|
const action = state.manage.action;
|
||||||
@@ -529,7 +517,7 @@ async function confirmManageExecution() {
|
|||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const updated = await res.json();
|
const updated = await res.json();
|
||||||
playBeepSound();
|
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();
|
clearSelectedManageItem();
|
||||||
await loadStats();
|
await loadStats();
|
||||||
await loadItems();
|
await loadItems();
|
||||||
@@ -537,7 +525,6 @@ async function confirmManageExecution() {
|
|||||||
showToast('Failed to update packet', 'error');
|
showToast('Failed to update packet', 'error');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Specific Grams / Amount mode
|
|
||||||
const newQty = state.manage.amountValue;
|
const newQty = state.manage.amountValue;
|
||||||
const res = await fetch(`/api/items/${item.id}/quantity`, {
|
const res = await fetch(`/api/items/${item.id}/quantity`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
@@ -585,7 +572,6 @@ function openItemModal(itemId = null) {
|
|||||||
document.getElementById('modal-initial-quantity').value = item.initial_quantity || item.quantity;
|
document.getElementById('modal-initial-quantity').value = item.initial_quantity || item.quantity;
|
||||||
document.getElementById('modal-unit').value = item.unit || 'g';
|
document.getElementById('modal-unit').value = item.unit || 'g';
|
||||||
document.getElementById('modal-category').value = item.category || 'General';
|
document.getElementById('modal-category').value = item.category || 'General';
|
||||||
document.getElementById('modal-expiry').value = item.expiry_date || '';
|
|
||||||
document.getElementById('modal-notes').value = item.notes || '';
|
document.getElementById('modal-notes').value = item.notes || '';
|
||||||
|
|
||||||
if (item.image_url) {
|
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) {
|
async function handleItemFormSubmit(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
@@ -678,7 +659,6 @@ async function handleItemFormSubmit(event) {
|
|||||||
initial_quantity: initQty,
|
initial_quantity: initQty,
|
||||||
unit: document.getElementById('modal-unit').value,
|
unit: document.getElementById('modal-unit').value,
|
||||||
category: document.getElementById('modal-category').value,
|
category: document.getElementById('modal-category').value,
|
||||||
expiry_date: document.getElementById('modal-expiry').value || null,
|
|
||||||
notes: document.getElementById('modal-notes').value.trim(),
|
notes: document.getElementById('modal-notes').value.trim(),
|
||||||
image_url: document.getElementById('modal-image-url').value || null
|
image_url: document.getElementById('modal-image-url').value || null
|
||||||
};
|
};
|
||||||
@@ -695,11 +675,10 @@ async function handleItemFormSubmit(event) {
|
|||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const saved = await res.json();
|
const saved = await res.json();
|
||||||
showToast(itemId ? 'Product updated' : 'Product added to fridge', 'success');
|
showToast(itemId ? 'Product updated' : 'Product added', 'success');
|
||||||
closeItemModal();
|
closeItemModal();
|
||||||
await loadStats();
|
await loadStats();
|
||||||
await loadItems();
|
await loadItems();
|
||||||
// Auto-select in manage tab
|
|
||||||
selectManageItem(saved);
|
selectManageItem(saved);
|
||||||
switchTab('manage');
|
switchTab('manage');
|
||||||
} else {
|
} else {
|
||||||
@@ -779,7 +758,6 @@ async function onBarcodeScannedSuccess(decodedText) {
|
|||||||
stopCameraScanner();
|
stopCameraScanner();
|
||||||
closeScannerModal();
|
closeScannerModal();
|
||||||
|
|
||||||
// Check if item exists in fridge
|
|
||||||
const existing = state.items.find(i => i.barcode === decodedText);
|
const existing = state.items.find(i => i.barcode === decodedText);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
switchTab('manage');
|
switchTab('manage');
|
||||||
@@ -788,7 +766,6 @@ async function onBarcodeScannedSuccess(decodedText) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise fetch from API / Open Food Facts
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/barcode/${encodeURIComponent(decodedText)}`);
|
const res = await fetch(`/api/barcode/${encodeURIComponent(decodedText)}`);
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
@@ -832,58 +809,6 @@ function playBeepSound() {
|
|||||||
} catch (e) {}
|
} 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) {
|
function getCategoryIcon(category) {
|
||||||
const cat = state.meta.categories.find(c => c.id === category);
|
const cat = state.meta.categories.find(c => c.id === category);
|
||||||
return cat ? cat.icon : 'package';
|
return cat ? cat.icon : 'package';
|
||||||
@@ -924,16 +849,3 @@ function showToast(message, type = 'info') {
|
|||||||
setTimeout(() => toast.remove(), 300);
|
setTimeout(() => toast.remove(), 300);
|
||||||
}, 3000);
|
}, 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+28
-47
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
<title>Fridger - Tablet Fridge Manager</title>
|
<title>Fridger - Inventory & Stock Manager</title>
|
||||||
|
|
||||||
<!-- Tailwind CSS -->
|
<!-- Tailwind CSS -->
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
@@ -46,14 +46,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="hidden xs:block">
|
<div class="hidden xs:block">
|
||||||
<h1 class="font-extrabold text-lg sm:text-xl leading-none bg-gradient-to-r from-teal-300 to-cyan-300 bg-clip-text text-transparent">Fridger</h1>
|
<h1 class="font-extrabold text-lg sm:text-xl leading-none bg-gradient-to-r from-teal-300 to-cyan-300 bg-clip-text text-transparent">Fridger</h1>
|
||||||
<p class="text-[11px] text-slate-400 font-medium">Tablet Manager</p>
|
<p class="text-[11px] text-slate-400 font-medium">Fridge & Freezer</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 3 MAIN TABS: MAIN -> SCAN -> MANAGE -->
|
<!-- 3 MAIN TABS: MAIN -> SCAN -> MANAGE -->
|
||||||
<nav class="flex items-center bg-slate-950 p-1.5 rounded-2xl border border-slate-800 shadow-inner gap-1">
|
<nav class="flex items-center bg-slate-950 p-1.5 rounded-2xl border border-slate-800 shadow-inner gap-1">
|
||||||
|
|
||||||
<!-- 1. MAIN (What's in the Fridge) -->
|
<!-- 1. MAIN (What's in the Fridge / Freezer) -->
|
||||||
<button
|
<button
|
||||||
id="nav-main-btn"
|
id="nav-main-btn"
|
||||||
onclick="switchTab('main')"
|
onclick="switchTab('main')"
|
||||||
@@ -105,25 +105,25 @@
|
|||||||
<div class="flex items-center justify-between text-xs sm:text-sm text-slate-400 overflow-x-auto gap-4 scrollbar-none">
|
<div class="flex items-center justify-between text-xs sm:text-sm text-slate-400 overflow-x-auto gap-4 scrollbar-none">
|
||||||
<div class="flex items-center gap-2 shrink-0">
|
<div class="flex items-center gap-2 shrink-0">
|
||||||
<span class="w-2.5 h-2.5 rounded-full bg-teal-400"></span>
|
<span class="w-2.5 h-2.5 rounded-full bg-teal-400"></span>
|
||||||
<span>In Fridge: <strong id="stat-total" class="text-white font-black text-sm sm:text-base">0</strong></span>
|
<span>In Stock: <strong id="stat-total" class="text-white font-black text-sm sm:text-base">0</strong> items</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2 shrink-0">
|
<div class="flex items-center gap-2 shrink-0">
|
||||||
<span class="w-2.5 h-2.5 rounded-full bg-emerald-400"></span>
|
<span class="w-2.5 h-2.5 rounded-full bg-emerald-400"></span>
|
||||||
<span>Fresh: <strong id="stat-fresh" class="text-emerald-300 font-bold">0</strong></span>
|
<span>Full Packs: <strong id="stat-full" class="text-emerald-300 font-bold">0</strong></span>
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2 shrink-0">
|
|
||||||
<span class="w-2.5 h-2.5 rounded-full bg-amber-400"></span>
|
|
||||||
<span>Expiring Soon: <strong id="stat-expiring" class="text-amber-300 font-bold">0</strong></span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2 shrink-0">
|
<div class="flex items-center gap-2 shrink-0">
|
||||||
<span class="w-2.5 h-2.5 rounded-full bg-indigo-400"></span>
|
<span class="w-2.5 h-2.5 rounded-full bg-indigo-400"></span>
|
||||||
<span>Opened: <strong id="stat-opened" class="text-indigo-300 font-bold">0</strong></span>
|
<span>In Use / Partial: <strong id="stat-opened" class="text-indigo-300 font-bold">0</strong></span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 shrink-0">
|
||||||
|
<span class="w-2.5 h-2.5 rounded-full bg-amber-400"></span>
|
||||||
|
<span>Low (≤25%): <strong id="stat-low" class="text-amber-300 font-bold">0</strong></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- MAIN TAB CONTENT (FRIDGE VIEW) -->
|
<!-- MAIN TAB CONTENT (FRIDGE / FREEZER VIEW) -->
|
||||||
<main id="main-tab-view" class="flex-1 max-w-4xl mx-auto px-3 sm:px-6 py-4 w-full space-y-3">
|
<main id="main-tab-view" class="flex-1 max-w-4xl mx-auto px-3 sm:px-6 py-4 w-full space-y-3">
|
||||||
|
|
||||||
<!-- Search & Filter Ribbon in Main tab -->
|
<!-- Search & Filter Ribbon in Main tab -->
|
||||||
@@ -134,7 +134,7 @@
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="search-input"
|
id="search-input"
|
||||||
placeholder="Search items in fridge..."
|
placeholder="Search items in fridge or freezer..."
|
||||||
oninput="handleSearch(this.value)"
|
oninput="handleSearch(this.value)"
|
||||||
class="w-full bg-slate-900 border border-slate-800 focus:border-teal-500 rounded-xl pl-9 pr-9 py-2.5 text-sm sm:text-base text-slate-100 placeholder-slate-500 outline-none transition"
|
class="w-full bg-slate-900 border border-slate-800 focus:border-teal-500 rounded-xl pl-9 pr-9 py-2.5 text-sm sm:text-base text-slate-100 placeholder-slate-500 outline-none transition"
|
||||||
>
|
>
|
||||||
@@ -145,10 +145,9 @@
|
|||||||
|
|
||||||
<div class="flex items-center gap-1.5 overflow-x-auto pb-0.5 text-xs sm:text-sm font-medium scrollbar-none">
|
<div class="flex items-center gap-1.5 overflow-x-auto pb-0.5 text-xs sm:text-sm font-medium scrollbar-none">
|
||||||
<button onclick="setStatusFilter('all')" data-status-btn="all" class="status-chip active px-3.5 py-2 rounded-xl bg-slate-800 border border-slate-700 text-white font-bold min-h-[38px] touch-manipulation">All</button>
|
<button onclick="setStatusFilter('all')" data-status-btn="all" class="status-chip active px-3.5 py-2 rounded-xl bg-slate-800 border border-slate-700 text-white font-bold min-h-[38px] touch-manipulation">All</button>
|
||||||
<button onclick="setStatusFilter('fresh')" data-status-btn="fresh" class="status-chip px-3.5 py-2 rounded-xl bg-slate-900 border border-slate-800 text-slate-400 hover:text-slate-200 min-h-[38px] touch-manipulation">Fresh</button>
|
<button onclick="setStatusFilter('full')" data-status-btn="full" class="status-chip px-3.5 py-2 rounded-xl bg-slate-900 border border-slate-800 text-slate-400 hover:text-slate-200 min-h-[38px] touch-manipulation">Full Packs</button>
|
||||||
<button onclick="setStatusFilter('expiring_soon')" data-status-btn="expiring_soon" class="status-chip px-3.5 py-2 rounded-xl bg-slate-900 border border-slate-800 text-amber-400 hover:text-amber-300 min-h-[38px] touch-manipulation">Expiring</button>
|
<button onclick="setStatusFilter('opened')" data-status-btn="opened" class="status-chip px-3.5 py-2 rounded-xl bg-slate-900 border border-slate-800 text-indigo-400 hover:text-indigo-300 min-h-[38px] touch-manipulation">In Use / Partial</button>
|
||||||
<button onclick="setStatusFilter('low')" data-status-btn="low" class="status-chip px-3.5 py-2 rounded-xl bg-slate-900 border border-slate-800 text-indigo-400 hover:text-indigo-300 min-h-[38px] touch-manipulation">≤25%</button>
|
<button onclick="setStatusFilter('low')" data-status-btn="low" class="status-chip px-3.5 py-2 rounded-xl bg-slate-900 border border-slate-800 text-amber-400 hover:text-amber-300 min-h-[38px] touch-manipulation">≤25% Low</button>
|
||||||
<button onclick="setStatusFilter('expired')" data-status-btn="expired" class="status-chip px-3.5 py-2 rounded-xl bg-slate-900 border border-slate-800 text-rose-400 hover:text-rose-300 min-h-[38px] touch-manipulation">Expired</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -167,8 +166,8 @@
|
|||||||
<div class="w-16 h-16 sm:w-20 sm:h-20 rounded-2xl bg-slate-900 text-slate-500 flex items-center justify-center mx-auto mb-3 border border-slate-800 text-3xl">
|
<div class="w-16 h-16 sm:w-20 sm:h-20 rounded-2xl bg-slate-900 text-slate-500 flex items-center justify-center mx-auto mb-3 border border-slate-800 text-3xl">
|
||||||
🧊
|
🧊
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-lg font-bold text-white mb-1">Your fridge is empty</h3>
|
<h3 class="text-lg font-bold text-white mb-1">Your inventory is empty</h3>
|
||||||
<p class="text-xs sm:text-sm text-slate-400 max-w-xs mx-auto mb-6">Scan a barcode or add an item to track quantities.</p>
|
<p class="text-xs sm:text-sm text-slate-400 max-w-xs mx-auto mb-6">Scan a barcode or add an item to track what you have in stock.</p>
|
||||||
<button onclick="switchTab('manage')" class="px-6 py-3 rounded-xl bg-teal-500 hover:bg-teal-400 text-white text-sm font-bold flex items-center gap-2 shadow-lg shadow-teal-500/20 active:scale-95 touch-manipulation mx-auto">
|
<button onclick="switchTab('manage')" class="px-6 py-3 rounded-xl bg-teal-500 hover:bg-teal-400 text-white text-sm font-bold flex items-center gap-2 shadow-lg shadow-teal-500/20 active:scale-95 touch-manipulation mx-auto">
|
||||||
<i data-lucide="sliders-horizontal" class="w-5 h-5"></i>
|
<i data-lucide="sliders-horizontal" class="w-5 h-5"></i>
|
||||||
<span>Go to Manage</span>
|
<span>Go to Manage</span>
|
||||||
@@ -219,7 +218,7 @@
|
|||||||
|
|
||||||
<!-- Quick Product Pick List -->
|
<!-- Quick Product Pick List -->
|
||||||
<div class="space-y-1.5 pt-2">
|
<div class="space-y-1.5 pt-2">
|
||||||
<label class="text-xs font-bold text-slate-400 uppercase tracking-wider block">Or tap an item from your fridge:</label>
|
<label class="text-xs font-bold text-slate-400 uppercase tracking-wider block">Or tap an item from stock:</label>
|
||||||
<div id="manage-quick-picker" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2.5 max-h-48 overflow-y-auto p-1 scrollbar-none">
|
<div id="manage-quick-picker" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2.5 max-h-48 overflow-y-auto p-1 scrollbar-none">
|
||||||
<!-- Rendered dynamically -->
|
<!-- Rendered dynamically -->
|
||||||
</div>
|
</div>
|
||||||
@@ -237,7 +236,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<h3 id="manage-selected-name" class="font-extrabold text-white text-base sm:text-lg truncate">Selected Product</h3>
|
<h3 id="manage-selected-name" class="font-extrabold text-white text-base sm:text-lg truncate">Selected Product</h3>
|
||||||
<p id="manage-selected-details" class="text-xs sm:text-sm text-slate-400 mt-0.5">Currently: 250g in fridge • 1 Pack = 250g</p>
|
<p id="manage-selected-details" class="text-xs sm:text-sm text-slate-400 mt-0.5">Currently: 250g in stock • 1 Pack = 250g</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -260,7 +259,7 @@
|
|||||||
class="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-300 hover:border-emerald-500/60 hover:text-white"
|
class="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-300 hover:border-emerald-500/60 hover:text-white"
|
||||||
>
|
>
|
||||||
<i data-lucide="plus-circle" class="w-7 h-7 text-emerald-400"></i>
|
<i data-lucide="plus-circle" class="w-7 h-7 text-emerald-400"></i>
|
||||||
<span class="text-sm sm:text-base">➕ Add to Fridge</span>
|
<span class="text-sm sm:text-base">➕ Add to Stock</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -269,7 +268,7 @@
|
|||||||
class="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-300 hover:border-rose-500/60 hover:text-white"
|
class="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-300 hover:border-rose-500/60 hover:text-white"
|
||||||
>
|
>
|
||||||
<i data-lucide="minus-circle" class="w-7 h-7 text-rose-400"></i>
|
<i data-lucide="minus-circle" class="w-7 h-7 text-rose-400"></i>
|
||||||
<span class="text-sm sm:text-base">➖ Remove from Fridge</span>
|
<span class="text-sm sm:text-base">➖ Remove from Stock</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -315,7 +314,6 @@
|
|||||||
<button onclick="changePacketCount(1)" class="w-12 h-12 rounded-2xl bg-slate-800 hover:bg-slate-700 text-white font-extrabold text-2xl flex items-center justify-center active:scale-95 touch-manipulation border border-slate-700">+</button>
|
<button onclick="changePacketCount(1)" class="w-12 h-12 rounded-2xl bg-slate-800 hover:bg-slate-700 text-white font-extrabold text-2xl flex items-center justify-center active:scale-95 touch-manipulation border border-slate-700">+</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Quick Chips -->
|
|
||||||
<div class="flex items-center justify-center gap-2">
|
<div class="flex items-center justify-center gap-2">
|
||||||
<button onclick="setPacketCount(1)" class="px-3.5 py-1.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-xs text-slate-200 border border-slate-700 font-bold">1 Pack</button>
|
<button onclick="setPacketCount(1)" class="px-3.5 py-1.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-xs text-slate-200 border border-slate-700 font-bold">1 Pack</button>
|
||||||
<button onclick="setPacketCount(2)" class="px-3.5 py-1.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-xs text-slate-200 border border-slate-700 font-bold">2 Packs</button>
|
<button onclick="setPacketCount(2)" class="px-3.5 py-1.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-xs text-slate-200 border border-slate-700 font-bold">2 Packs</button>
|
||||||
@@ -330,7 +328,7 @@
|
|||||||
<span id="manage-amount-val">150</span>
|
<span id="manage-amount-val">150</span>
|
||||||
<span id="manage-amount-unit" class="text-base font-normal text-teal-400">g</span>
|
<span id="manage-amount-unit" class="text-base font-normal text-teal-400">g</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="manage-amount-subtext" class="text-xs text-slate-400 mt-1 font-medium">New remaining amount in fridge</div>
|
<div id="manage-amount-subtext" class="text-xs text-slate-400 mt-1 font-medium">New remaining amount in stock</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
@@ -344,7 +342,6 @@
|
|||||||
class="w-full accent-teal-500 cursor-pointer h-3 bg-slate-800 rounded-xl"
|
class="w-full accent-teal-500 cursor-pointer h-3 bg-slate-800 rounded-xl"
|
||||||
>
|
>
|
||||||
|
|
||||||
<!-- Tablet Quick Delta Chips -->
|
|
||||||
<div class="grid grid-cols-3 sm:grid-cols-6 gap-2">
|
<div class="grid grid-cols-3 sm:grid-cols-6 gap-2">
|
||||||
<button onclick="applyManageDelta(-10)" class="py-2.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-xs sm:text-sm text-slate-200 border border-slate-700 font-bold touch-manipulation">-10</button>
|
<button onclick="applyManageDelta(-10)" class="py-2.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-xs sm:text-sm text-slate-200 border border-slate-700 font-bold touch-manipulation">-10</button>
|
||||||
<button onclick="applyManageDelta(-25)" class="py-2.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-xs sm:text-sm text-slate-200 border border-slate-700 font-bold touch-manipulation">-25</button>
|
<button onclick="applyManageDelta(-25)" class="py-2.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-xs sm:text-sm text-slate-200 border border-slate-700 font-bold touch-manipulation">-25</button>
|
||||||
@@ -370,7 +367,7 @@
|
|||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- ADD / EDIT ITEM MODAL -->
|
<!-- ADD / EDIT ITEM MODAL (NO EXPIRY REQUIRED) -->
|
||||||
<div id="item-modal" class="fixed inset-0 z-50 bg-black/80 backdrop-blur-sm hidden flex items-center justify-center p-3 sm:p-4 overflow-y-auto">
|
<div id="item-modal" class="fixed inset-0 z-50 bg-black/80 backdrop-blur-sm hidden flex items-center justify-center p-3 sm:p-4 overflow-y-auto">
|
||||||
<div class="bg-slate-900 border border-slate-800 rounded-3xl max-w-lg w-full p-5 sm:p-6 shadow-2xl space-y-4 my-8 animate-scale-up">
|
<div class="bg-slate-900 border border-slate-800 rounded-3xl max-w-lg w-full p-5 sm:p-6 shadow-2xl space-y-4 my-8 animate-scale-up">
|
||||||
|
|
||||||
@@ -418,22 +415,22 @@
|
|||||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-2.5">
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-2.5">
|
||||||
<div class="sm:col-span-2">
|
<div class="sm:col-span-2">
|
||||||
<label class="block text-slate-300 font-semibold mb-1">Product Name *</label>
|
<label class="block text-slate-300 font-semibold mb-1">Product Name *</label>
|
||||||
<input type="text" id="modal-name" required placeholder="e.g. Gouda Cheese" class="w-full bg-slate-950 border border-slate-800 focus:border-teal-500 rounded-xl px-3.5 py-2.5 text-white outline-none">
|
<input type="text" id="modal-name" required placeholder="e.g. Frozen Peas" class="w-full bg-slate-950 border border-slate-800 focus:border-teal-500 rounded-xl px-3.5 py-2.5 text-white outline-none">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-slate-300 font-semibold mb-1">Brand</label>
|
<label class="block text-slate-300 font-semibold mb-1">Brand</label>
|
||||||
<input type="text" id="modal-brand" placeholder="e.g. Milbona" class="w-full bg-slate-950 border border-slate-800 focus:border-teal-500 rounded-xl px-3.5 py-2.5 text-white outline-none">
|
<input type="text" id="modal-brand" placeholder="e.g. Iglo" class="w-full bg-slate-950 border border-slate-800 focus:border-teal-500 rounded-xl px-3.5 py-2.5 text-white outline-none">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-3 gap-2.5">
|
<div class="grid grid-cols-3 gap-2.5">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-slate-300 font-semibold mb-1">Current Amount *</label>
|
<label class="block text-slate-300 font-semibold mb-1">Current Amount *</label>
|
||||||
<input type="number" id="modal-quantity" step="any" min="0" required placeholder="250" class="w-full bg-slate-950 border border-slate-800 focus:border-teal-500 rounded-xl px-3.5 py-2.5 text-white outline-none">
|
<input type="number" id="modal-quantity" step="any" min="0" required placeholder="450" class="w-full bg-slate-950 border border-slate-800 focus:border-teal-500 rounded-xl px-3.5 py-2.5 text-white outline-none">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-slate-300 font-semibold mb-1">1 Full Pack Size *</label>
|
<label class="block text-slate-300 font-semibold mb-1">1 Full Pack Size *</label>
|
||||||
<input type="number" id="modal-initial-quantity" step="any" min="0" required placeholder="250" class="w-full bg-slate-950 border border-slate-800 focus:border-teal-500 rounded-xl px-3.5 py-2.5 text-white outline-none">
|
<input type="number" id="modal-initial-quantity" step="any" min="0" required placeholder="450" class="w-full bg-slate-950 border border-slate-800 focus:border-teal-500 rounded-xl px-3.5 py-2.5 text-white outline-none">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-slate-300 font-semibold mb-1">Unit</label>
|
<label class="block text-slate-300 font-semibold mb-1">Unit</label>
|
||||||
@@ -456,25 +453,9 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
|
||||||
<div class="flex items-center justify-between mb-1">
|
|
||||||
<label class="block text-slate-300 font-semibold">Expiration Date</label>
|
|
||||||
<div class="flex items-center gap-2 text-xs">
|
|
||||||
<button type="button" onclick="setModalExpiryDays(3)" class="text-slate-400 hover:text-teal-400 font-medium">+3d</button>
|
|
||||||
<span>•</span>
|
|
||||||
<button type="button" onclick="setModalExpiryDays(7)" class="text-slate-400 hover:text-teal-400 font-medium">+1w</button>
|
|
||||||
<span>•</span>
|
|
||||||
<button type="button" onclick="setModalExpiryDays(14)" class="text-slate-400 hover:text-teal-400 font-medium">+2w</button>
|
|
||||||
<span>•</span>
|
|
||||||
<button type="button" onclick="setModalExpiryDays(30)" class="text-slate-400 hover:text-teal-400 font-medium">+1m</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<input type="date" id="modal-expiry" class="w-full bg-slate-950 border border-slate-800 focus:border-teal-500 rounded-xl px-3.5 py-2.5 text-white outline-none">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-slate-300 font-semibold mb-1">Notes (Optional)</label>
|
<label class="block text-slate-300 font-semibold mb-1">Notes (Optional)</label>
|
||||||
<input type="text" id="modal-notes" placeholder="e.g. For dinner" class="w-full bg-slate-950 border border-slate-800 focus:border-teal-500 rounded-xl px-3.5 py-2.5 text-white outline-none">
|
<input type="text" id="modal-notes" placeholder="e.g. In top freezer basket" class="w-full bg-slate-950 border border-slate-800 focus:border-teal-500 rounded-xl px-3.5 py-2.5 text-white outline-none">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center justify-end gap-3 pt-3 border-t border-slate-800">
|
<div class="flex items-center justify-end gap-3 pt-3 border-t border-slate-800">
|
||||||
|
|||||||
@@ -89,7 +89,8 @@ assert.strictEqual(db.getItemById(newItem.id), undefined);
|
|||||||
// Test stats
|
// Test stats
|
||||||
const stats = db.getStats();
|
const stats = db.getStats();
|
||||||
assert(typeof stats.total === 'number');
|
assert(typeof stats.total === 'number');
|
||||||
assert(typeof stats.fresh === 'number');
|
assert(typeof stats.full === 'number');
|
||||||
|
assert(typeof stats.opened === 'number');
|
||||||
|
|
||||||
console.log('✓ All database operations passed!');
|
console.log('✓ All database operations passed!');
|
||||||
console.log('🎉 All backend tests completed successfully!');
|
console.log('🎉 All backend tests completed successfully!');
|
||||||
|
|||||||
Reference in New Issue
Block a user