feat(manage): implement interactive Scan/Search -> Add/Remove -> Packet/Amount workflow
Build and Push Fridger Container / Build & Push Fridger Image (push) Successful in 1m27s
Build and Push Fridger Container / Build & Push Fridger Image (push) Successful in 1m27s
This commit is contained in:
+294
-295
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Fridger Frontend Application
|
||||
* Tablet-optimized UI: Main -> Scan -> Manage navigation and 3-button product management.
|
||||
* Tablet-optimized: Main -> Scan -> Manage (Scan/Search -> Add/Remove -> Packet/Amount)
|
||||
*/
|
||||
|
||||
const state = {
|
||||
@@ -16,8 +16,13 @@ const state = {
|
||||
category: 'All',
|
||||
status: 'all'
|
||||
},
|
||||
activeGramsItem: null,
|
||||
activeGramsValue: 0,
|
||||
manage: {
|
||||
selectedItem: null,
|
||||
action: null, // 'add' or 'remove'
|
||||
qtyMode: 'packet', // 'packet' or 'amount'
|
||||
packetCount: 1,
|
||||
amountValue: 0
|
||||
},
|
||||
html5QrCode: null,
|
||||
isScanning: false
|
||||
};
|
||||
@@ -79,6 +84,7 @@ async function loadItems() {
|
||||
if (res.ok) {
|
||||
state.items = await res.json();
|
||||
renderApp();
|
||||
renderManageQuickPicker();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load items:', err);
|
||||
@@ -95,27 +101,33 @@ function switchTab(tab) {
|
||||
|
||||
const mainBtn = document.getElementById('nav-main-btn');
|
||||
const manageBtn = document.getElementById('nav-manage-btn');
|
||||
const viewContainer = document.getElementById('view-tab-container');
|
||||
const manageContainer = document.getElementById('manage-tab-container');
|
||||
const mainView = document.getElementById('main-tab-view');
|
||||
const manageView = document.getElementById('manage-tab-view');
|
||||
|
||||
const activeClasses = 'flex items-center justify-center gap-2 px-3 sm:px-6 py-2.5 sm:py-3 rounded-xl text-xs sm:text-sm font-bold transition-all bg-teal-500 text-white shadow-md active:scale-95 touch-manipulation min-h-[44px]';
|
||||
const inactiveClasses = 'flex items-center justify-center gap-2 px-3 sm:px-6 py-2.5 sm:py-3 rounded-xl text-xs sm:text-sm font-bold transition-all text-slate-300 hover:text-white hover:bg-slate-800/80 active:scale-95 touch-manipulation min-h-[44px]';
|
||||
const activeClasses = 'flex items-center justify-center gap-2 px-4 sm:px-7 py-2.5 sm:py-3 rounded-xl text-xs sm:text-sm font-bold transition-all bg-teal-500 text-white shadow-md active:scale-95 touch-manipulation min-h-[44px]';
|
||||
const inactiveClasses = 'flex items-center justify-center gap-2 px-4 sm:px-7 py-2.5 sm:py-3 rounded-xl text-xs sm:text-sm font-bold transition-all text-slate-300 hover:text-white hover:bg-slate-800/80 active:scale-95 touch-manipulation min-h-[44px]';
|
||||
|
||||
if (tab === 'main') {
|
||||
mainBtn.className = activeClasses;
|
||||
manageBtn.className = inactiveClasses;
|
||||
viewContainer.classList.remove('hidden');
|
||||
manageContainer.classList.add('hidden');
|
||||
mainView.classList.remove('hidden');
|
||||
manageView.classList.add('hidden');
|
||||
renderApp();
|
||||
} else if (tab === 'manage') {
|
||||
manageBtn.className = activeClasses;
|
||||
mainBtn.className = inactiveClasses;
|
||||
manageContainer.classList.remove('hidden');
|
||||
viewContainer.classList.add('hidden');
|
||||
manageView.classList.remove('hidden');
|
||||
mainView.classList.add('hidden');
|
||||
renderManageQuickPicker();
|
||||
}
|
||||
|
||||
renderApp();
|
||||
initLucide();
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
// Filters & Search (Main Tab)
|
||||
// ----------------------------------------------------
|
||||
|
||||
let searchDebounceTimer = null;
|
||||
function handleSearch(val) {
|
||||
state.filters.search = val.trim();
|
||||
@@ -197,35 +209,21 @@ function populateModalSelects() {
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
// UI Rendering
|
||||
// UI Rendering (Main Tab: Fridge Cards)
|
||||
// ----------------------------------------------------
|
||||
|
||||
function renderApp() {
|
||||
const emptyState = document.getElementById('empty-state');
|
||||
const container = document.getElementById('fridge-view-content');
|
||||
|
||||
if (state.items.length === 0) {
|
||||
emptyState.classList.remove('hidden');
|
||||
document.getElementById('fridge-view-content').innerHTML = '';
|
||||
document.getElementById('manage-items-list').innerHTML = '';
|
||||
container.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
emptyState.classList.add('hidden');
|
||||
|
||||
if (state.currentTab === 'main') {
|
||||
renderFridgeView();
|
||||
} else {
|
||||
renderManageView();
|
||||
}
|
||||
|
||||
initLucide();
|
||||
}
|
||||
|
||||
// MAIN TAB: Fridge View (Tablet-optimized cards)
|
||||
function renderFridgeView() {
|
||||
const container = document.getElementById('fridge-view-content');
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = state.items.map(item => {
|
||||
const expiryInfo = getExpiryStatus(item.expiry_date);
|
||||
const percent = item.initial_quantity > 0
|
||||
@@ -284,282 +282,288 @@ function renderFridgeView() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3 Quick Tablet Action Buttons -->
|
||||
<div class="grid grid-cols-3 gap-2 pt-2 border-t border-slate-800/80">
|
||||
<button onclick="addWholePacket(${item.id})" class="flex items-center justify-center gap-1.5 py-2.5 px-2 rounded-xl bg-emerald-500/15 hover:bg-emerald-500/25 text-emerald-300 border border-emerald-500/30 text-xs sm:text-sm font-bold transition active:scale-95 touch-manipulation min-h-[42px]" title="Add 1 whole packet">
|
||||
<i data-lucide="plus" class="w-4 h-4"></i>
|
||||
<span>Add</span>
|
||||
</button>
|
||||
<button onclick="removeWholePacket(${item.id})" class="flex items-center justify-center gap-1.5 py-2.5 px-2 rounded-xl bg-rose-500/15 hover:bg-rose-500/25 text-rose-300 border border-rose-500/30 text-xs sm:text-sm font-bold transition active:scale-95 touch-manipulation min-h-[42px]" title="Remove 1 whole packet">
|
||||
<i data-lucide="minus" class="w-4 h-4"></i>
|
||||
<span>Remove</span>
|
||||
</button>
|
||||
<button onclick="openGramsModal(${item.id})" class="flex items-center justify-center gap-1.5 py-2.5 px-2 rounded-xl bg-teal-500/20 hover:bg-teal-500/30 text-teal-300 border border-teal-500/40 text-xs sm:text-sm font-bold transition active:scale-95 touch-manipulation min-h-[42px]" title="Manage grams remaining">
|
||||
<i data-lucide="sliders" class="w-4 h-4"></i>
|
||||
<span>Manage</span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Touch Manage Button -->
|
||||
<button onclick="goToManageItem(${item.id})" class="w-full flex items-center justify-center gap-2 py-3 px-3 rounded-2xl bg-teal-500/15 hover:bg-teal-500/25 text-teal-300 border border-teal-500/40 text-xs sm:text-sm font-bold transition active:scale-95 touch-manipulation min-h-[44px]" title="Manage this item">
|
||||
<i data-lucide="sliders-horizontal" class="w-4 h-4"></i>
|
||||
<span>Manage Item</span>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// MANAGE TAB: Manage Products Interface (Optimized for Tablet with Large Controls)
|
||||
function renderManageView() {
|
||||
const container = document.getElementById('manage-items-list');
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = state.items.map(item => {
|
||||
const expiryInfo = getExpiryStatus(item.expiry_date);
|
||||
const percent = item.initial_quantity > 0
|
||||
? Math.min(100, Math.max(0, Math.round((item.quantity / item.initial_quantity) * 100)))
|
||||
: 100;
|
||||
|
||||
return `
|
||||
<div class="bg-slate-900 border border-slate-800 rounded-2xl sm:rounded-3xl p-4 sm:p-5 shadow-lg transition hover:border-slate-700 space-y-4">
|
||||
|
||||
<div class="flex flex-col tablet:flex-row items-start tablet:items-center justify-between gap-4">
|
||||
|
||||
<!-- Product info -->
|
||||
<div class="flex items-center gap-3.5 min-w-0 flex-1">
|
||||
${item.image_url ? `
|
||||
<img src="${escapeHtml(item.image_url)}" alt="${escapeHtml(item.name)}" class="w-14 h-14 object-contain rounded-2xl bg-white/5 p-1 border border-slate-800 shrink-0">
|
||||
` : `
|
||||
<div class="w-14 h-14 rounded-2xl bg-slate-800 border border-slate-700 text-teal-400 flex items-center justify-center shrink-0">
|
||||
<i data-lucide="${getCategoryIcon(item.category)}" class="w-7 h-7"></i>
|
||||
</div>
|
||||
`}
|
||||
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<h4 class="font-extrabold text-white text-base sm:text-lg truncate">${escapeHtml(item.name)}</h4>
|
||||
${item.brand ? `<span class="text-xs px-2 py-0.5 rounded-lg bg-slate-800 text-slate-300 font-medium">${escapeHtml(item.brand)}</span>` : ''}
|
||||
</div>
|
||||
<div class="text-xs sm:text-sm text-slate-400 flex items-center gap-2.5 mt-1 flex-wrap">
|
||||
<span class="font-black text-teal-300 text-sm sm:text-base">${item.quantity} ${item.unit}</span>
|
||||
<span>(Pack size: ${item.initial_quantity} ${item.unit})</span>
|
||||
<span>•</span>
|
||||
<span class="${expiryInfo.badgeClass} px-2 py-0.5 rounded-lg font-bold text-xs">${expiryInfo.label}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- The 3 Core Tablet Action Buttons: Add, Remove, Manage Product Grams -->
|
||||
<div class="flex items-center gap-2 w-full tablet:w-auto justify-end flex-wrap">
|
||||
|
||||
<!-- 1. Add Whole Packet -->
|
||||
<button
|
||||
onclick="addWholePacket(${item.id})"
|
||||
class="flex-1 tablet:flex-initial px-4 py-3 rounded-2xl bg-emerald-500/15 hover:bg-emerald-500/25 text-emerald-300 border border-emerald-500/30 text-xs sm:text-sm font-bold flex items-center justify-center gap-1.5 transition active:scale-95 touch-manipulation min-h-[46px]"
|
||||
title="Add 1 Whole Packet"
|
||||
>
|
||||
<i data-lucide="plus" class="w-4 h-4"></i>
|
||||
<span>Add</span>
|
||||
</button>
|
||||
|
||||
<!-- 2. Remove Whole Packet -->
|
||||
<button
|
||||
onclick="removeWholePacket(${item.id})"
|
||||
class="flex-1 tablet:flex-initial px-4 py-3 rounded-2xl bg-rose-500/15 hover:bg-rose-500/25 text-rose-300 border border-rose-500/30 text-xs sm:text-sm font-bold flex items-center justify-center gap-1.5 transition active:scale-95 touch-manipulation min-h-[46px]"
|
||||
title="Remove 1 Whole Packet"
|
||||
>
|
||||
<i data-lucide="minus" class="w-4 h-4"></i>
|
||||
<span>Remove</span>
|
||||
</button>
|
||||
|
||||
<!-- 3. Manage Grams / Partial -->
|
||||
<button
|
||||
onclick="openGramsModal(${item.id})"
|
||||
class="flex-1 tablet:flex-initial px-5 py-3 rounded-2xl bg-teal-500 hover:bg-teal-400 text-white text-xs sm:text-sm font-bold flex items-center justify-center gap-1.5 shadow-md shadow-teal-500/20 transition active:scale-95 touch-manipulation min-h-[46px]"
|
||||
title="Manage product grams"
|
||||
>
|
||||
<i data-lucide="sliders" class="w-4 h-4"></i>
|
||||
<span>Manage</span>
|
||||
</button>
|
||||
|
||||
<!-- Secondary: Edit / Delete -->
|
||||
<button onclick="openItemModal(${item.id})" class="p-3 rounded-2xl bg-slate-800 hover:bg-slate-700 text-slate-300 hover:text-white border border-slate-700 min-h-[46px] min-w-[46px] flex items-center justify-center touch-manipulation" title="Edit details">
|
||||
<i data-lucide="pencil" class="w-4 h-4"></i>
|
||||
</button>
|
||||
<button onclick="confirmDeleteItem(${item.id}, '${escapeHtml(item.name)}')" class="p-3 rounded-2xl bg-slate-800 hover:bg-rose-900/40 text-slate-400 hover:text-rose-300 border border-slate-700 min-h-[46px] min-w-[46px] flex items-center justify-center touch-manipulation" title="Delete">
|
||||
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
// 3 Primary Action Handlers
|
||||
// ----------------------------------------------------
|
||||
|
||||
async function addWholePacket(itemId) {
|
||||
try {
|
||||
const res = await fetch(`/api/items/${itemId}/add-packet`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ count: 1 })
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
const updated = await res.json();
|
||||
showToast(`+1 Pack of ${updated.name} (Total: ${updated.quantity} ${updated.unit})`, 'success');
|
||||
await loadStats();
|
||||
await loadItems();
|
||||
} else {
|
||||
showToast('Failed to add packet', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
showToast('Network error', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function removeWholePacket(itemId) {
|
||||
try {
|
||||
const res = await fetch(`/api/items/${itemId}/remove-packet`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ count: 1 })
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
const updated = await res.json();
|
||||
showToast(`-1 Pack of ${updated.name} (Remaining: ${updated.quantity} ${updated.unit})`, 'info');
|
||||
await loadStats();
|
||||
await loadItems();
|
||||
} else {
|
||||
showToast('Failed to remove packet', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
showToast('Network error', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
// Manage Grams Modal Logic
|
||||
// ----------------------------------------------------
|
||||
|
||||
function openGramsModal(itemId) {
|
||||
const item = state.items.find(i => i.id === itemId);
|
||||
if (!item) return;
|
||||
|
||||
state.activeGramsItem = item;
|
||||
state.activeGramsValue = item.quantity;
|
||||
|
||||
document.getElementById('grams-modal-title').textContent = item.name;
|
||||
document.getElementById('grams-modal-sub').textContent = `1 Full Packet = ${item.initial_quantity} ${item.unit}`;
|
||||
document.getElementById('grams-modal-unit').textContent = item.unit;
|
||||
document.getElementById('grams-current-val').textContent = item.quantity;
|
||||
document.getElementById('grams-exact-input').value = item.quantity;
|
||||
|
||||
const slider = document.getElementById('grams-slider');
|
||||
slider.max = Math.max(item.initial_quantity * 2, item.quantity, 100);
|
||||
slider.step = (item.unit === 'g' || item.unit === 'ml') ? '5' : '1';
|
||||
slider.value = item.quantity;
|
||||
|
||||
updateGramsGauge();
|
||||
document.getElementById('grams-modal').classList.remove('hidden');
|
||||
initLucide();
|
||||
}
|
||||
|
||||
function closeGramsModal() {
|
||||
document.getElementById('grams-modal').classList.add('hidden');
|
||||
state.activeGramsItem = null;
|
||||
// ----------------------------------------------------
|
||||
// MANAGE TAB: 3-Step Flow (Scan/Search -> Add/Remove -> Packet/Amount)
|
||||
// ----------------------------------------------------
|
||||
|
||||
function renderManageQuickPicker(filterQuery = '') {
|
||||
const container = document.getElementById('manage-quick-picker');
|
||||
if (!container) return;
|
||||
|
||||
const query = filterQuery.toLowerCase().trim();
|
||||
const matched = state.items.filter(i => {
|
||||
if (!query) return true;
|
||||
return (i.name && i.name.toLowerCase().includes(query)) ||
|
||||
(i.brand && i.brand.toLowerCase().includes(query)) ||
|
||||
(i.barcode && i.barcode.includes(query));
|
||||
});
|
||||
|
||||
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!
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = matched.map(item => `
|
||||
<button
|
||||
onclick="selectManageItemById(${item.id})"
|
||||
class="p-3 rounded-2xl bg-slate-950 hover:bg-slate-800/80 border border-slate-800/90 text-left flex items-center gap-3 transition active:scale-95 touch-manipulation min-h-[50px] ${state.manage.selectedItem?.id === item.id ? 'ring-2 ring-teal-500 border-teal-500' : ''}"
|
||||
>
|
||||
${item.image_url ? `
|
||||
<img src="${escapeHtml(item.image_url)}" class="w-9 h-9 object-contain rounded-xl bg-white/5 p-0.5 shrink-0">
|
||||
` : `
|
||||
<div class="w-9 h-9 rounded-xl bg-slate-800 text-teal-400 flex items-center justify-center shrink-0">
|
||||
<i data-lucide="${getCategoryIcon(item.category)}" class="w-4 h-4"></i>
|
||||
</div>
|
||||
`}
|
||||
<div class="min-w-0 flex-1">
|
||||
<h5 class="text-xs sm:text-sm font-bold text-white truncate">${escapeHtml(item.name)}</h5>
|
||||
<div class="text-[11px] text-teal-300/90 font-medium">
|
||||
${item.quantity} ${item.unit} <span class="text-slate-500 font-normal">(${item.initial_quantity}${item.unit} pack)</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
`).join('');
|
||||
|
||||
initLucide();
|
||||
}
|
||||
|
||||
function onGramsSliderChange(val) {
|
||||
state.activeGramsValue = parseFloat(val);
|
||||
document.getElementById('grams-exact-input').value = state.activeGramsValue;
|
||||
updateGramsGauge();
|
||||
}
|
||||
|
||||
function onGramsExactInput(val) {
|
||||
const num = parseFloat(val);
|
||||
if (!isNaN(num) && num >= 0) {
|
||||
state.activeGramsValue = num;
|
||||
document.getElementById('grams-slider').value = num;
|
||||
updateGramsGauge();
|
||||
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);
|
||||
if (found) {
|
||||
selectManageItem(found);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function applyGramsDelta(delta) {
|
||||
state.activeGramsValue = Math.max(0, state.activeGramsValue + delta);
|
||||
document.getElementById('grams-slider').value = state.activeGramsValue;
|
||||
document.getElementById('grams-exact-input').value = state.activeGramsValue;
|
||||
updateGramsGauge();
|
||||
function selectManageItemById(id) {
|
||||
const item = state.items.find(i => i.id === id);
|
||||
if (item) selectManageItem(item);
|
||||
}
|
||||
|
||||
function setGramsPercent(ratio) {
|
||||
if (!state.activeGramsItem) return;
|
||||
state.activeGramsValue = Math.round(state.activeGramsItem.initial_quantity * ratio);
|
||||
document.getElementById('grams-slider').value = state.activeGramsValue;
|
||||
document.getElementById('grams-exact-input').value = state.activeGramsValue;
|
||||
updateGramsGauge();
|
||||
}
|
||||
function selectManageItem(item) {
|
||||
state.manage.selectedItem = item;
|
||||
state.manage.action = null; // reset step 2
|
||||
state.manage.packetCount = 1;
|
||||
state.manage.amountValue = item.quantity;
|
||||
|
||||
function updateGramsGauge() {
|
||||
document.getElementById('grams-current-val').textContent = state.activeGramsValue;
|
||||
if (state.activeGramsItem && state.activeGramsItem.initial_quantity > 0) {
|
||||
const pct = Math.round((state.activeGramsValue / state.activeGramsItem.initial_quantity) * 100);
|
||||
document.getElementById('grams-percent-text').textContent = `${pct}% remaining of a full ${state.activeGramsItem.initial_quantity}${state.activeGramsItem.unit} packet`;
|
||||
// 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-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;
|
||||
|
||||
const slider = document.getElementById('manage-amount-slider');
|
||||
slider.max = Math.max(item.initial_quantity * 2, item.quantity + item.initial_quantity, 100);
|
||||
slider.value = item.quantity;
|
||||
slider.step = (item.unit === 'g' || item.unit === 'ml') ? '5' : '1';
|
||||
|
||||
const imgBox = document.getElementById('manage-selected-img');
|
||||
if (item.image_url) {
|
||||
imgBox.innerHTML = `<img src="${escapeHtml(item.image_url)}" class="w-full h-full object-contain p-1">`;
|
||||
} else {
|
||||
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' });
|
||||
}
|
||||
|
||||
async function saveGramsAdjustment() {
|
||||
if (!state.activeGramsItem) return;
|
||||
const itemId = state.activeGramsItem.id;
|
||||
const newQty = state.activeGramsValue;
|
||||
function clearSelectedManageItem() {
|
||||
state.manage.selectedItem = null;
|
||||
state.manage.action = null;
|
||||
document.getElementById('manage-action-card').classList.add('hidden');
|
||||
renderManageQuickPicker();
|
||||
}
|
||||
|
||||
function goToManageItem(itemId) {
|
||||
switchTab('manage');
|
||||
selectManageItemById(itemId);
|
||||
}
|
||||
|
||||
function setManageAction(action) {
|
||||
state.manage.action = action; // 'add' or 'remove'
|
||||
const btnAdd = document.getElementById('manage-btn-add');
|
||||
const btnRemove = document.getElementById('manage-btn-remove');
|
||||
const qtySection = document.getElementById('manage-qty-type-section');
|
||||
const confirmBtn = document.getElementById('manage-confirm-btn');
|
||||
const confirmLabel = document.getElementById('manage-confirm-label');
|
||||
const step3Title = document.getElementById('manage-step3-title');
|
||||
|
||||
if (action === 'add') {
|
||||
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-emerald-500/20 border-emerald-500 text-white shadow-lg shadow-emerald-500/20 ring-2 ring-emerald-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-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`;
|
||||
} 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`;
|
||||
}
|
||||
|
||||
qtySection.classList.remove('hidden');
|
||||
initLucide();
|
||||
}
|
||||
|
||||
function resetActionButtonsStyle() {
|
||||
const btnAdd = document.getElementById('manage-btn-add');
|
||||
const btnRemove = document.getElementById('manage-btn-remove');
|
||||
const defClasses = '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-slate-700';
|
||||
btnAdd.className = defClasses;
|
||||
btnRemove.className = defClasses;
|
||||
}
|
||||
|
||||
function setQtyMode(mode) {
|
||||
state.manage.qtyMode = mode; // 'packet' or 'amount'
|
||||
const btnPack = document.getElementById('qty-mode-packet-btn');
|
||||
const btnAmount = document.getElementById('qty-mode-amount-btn');
|
||||
const packControls = document.getElementById('qty-packet-controls');
|
||||
const amountControls = document.getElementById('qty-amount-controls');
|
||||
|
||||
const active = 'py-3 px-3 rounded-xl font-bold text-xs sm:text-sm transition bg-teal-500 text-white shadow-md touch-manipulation flex items-center justify-center gap-2';
|
||||
const inactive = 'py-3 px-3 rounded-xl font-bold text-xs sm:text-sm transition text-slate-300 hover:text-white touch-manipulation flex items-center justify-center gap-2';
|
||||
|
||||
if (mode === 'packet') {
|
||||
btnPack.className = active;
|
||||
btnAmount.className = inactive;
|
||||
packControls.classList.remove('hidden');
|
||||
amountControls.classList.add('hidden');
|
||||
} else {
|
||||
btnAmount.className = active;
|
||||
btnPack.className = inactive;
|
||||
amountControls.classList.remove('hidden');
|
||||
packControls.classList.add('hidden');
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function setPacketCount(num) {
|
||||
state.manage.packetCount = 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;
|
||||
}
|
||||
|
||||
function applyManageDelta(delta) {
|
||||
state.manage.amountValue = Math.max(0, state.manage.amountValue + delta);
|
||||
document.getElementById('manage-amount-slider').value = state.manage.amountValue;
|
||||
document.getElementById('manage-amount-val').textContent = state.manage.amountValue;
|
||||
}
|
||||
|
||||
function setManageAmountRatio(ratio) {
|
||||
if (!state.manage.selectedItem) return;
|
||||
state.manage.amountValue = Math.round(state.manage.selectedItem.initial_quantity * ratio);
|
||||
document.getElementById('manage-amount-slider').value = state.manage.amountValue;
|
||||
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;
|
||||
const mode = state.manage.qtyMode;
|
||||
|
||||
if (!item || !action) {
|
||||
showToast('Please select Add or Remove action first', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/items/${itemId}/quantity`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ newQuantity: newQty, markOpened: true })
|
||||
});
|
||||
if (mode === 'packet') {
|
||||
const endpoint = action === 'add' ? `/api/items/${item.id}/add-packet` : `/api/items/${item.id}/remove-packet`;
|
||||
const res = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ count: state.manage.packetCount })
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
showToast(`Updated to ${newQty} ${state.activeGramsItem.unit}`, 'success');
|
||||
closeGramsModal();
|
||||
await loadStats();
|
||||
await loadItems();
|
||||
} else {
|
||||
showToast('Failed to update quantity', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Save grams error:', err);
|
||||
showToast('Network error while updating quantity', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function consumeAllAndRemove() {
|
||||
if (!state.activeGramsItem) return;
|
||||
const itemId = state.activeGramsItem.id;
|
||||
|
||||
if (confirm(`Remove "${state.activeGramsItem.name}" from fridge?`)) {
|
||||
try {
|
||||
const res = await fetch(`/api/items/${itemId}`, { method: 'DELETE' });
|
||||
if (res.ok) {
|
||||
showToast('Item removed', 'info');
|
||||
closeGramsModal();
|
||||
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');
|
||||
clearSelectedManageItem();
|
||||
await loadStats();
|
||||
await loadItems();
|
||||
} else {
|
||||
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',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ newQuantity: newQty, markOpened: true })
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
const updated = await res.json();
|
||||
playBeepSound();
|
||||
showToast(`✓ Set ${updated.name} to ${updated.quantity} ${updated.unit}`, 'success');
|
||||
clearSelectedManageItem();
|
||||
await loadStats();
|
||||
await loadItems();
|
||||
} else {
|
||||
showToast('Failed to update amount', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Execution error:', err);
|
||||
showToast('Network error while updating item', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
// Item Add / Edit Modal
|
||||
// Add / Edit Product Modal Logic
|
||||
// ----------------------------------------------------
|
||||
|
||||
function openItemModal(itemId = null) {
|
||||
@@ -690,10 +694,14 @@ async function handleItemFormSubmit(event) {
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
const saved = await res.json();
|
||||
showToast(itemId ? 'Product updated' : 'Product added to fridge', 'success');
|
||||
closeItemModal();
|
||||
await loadStats();
|
||||
await loadItems();
|
||||
// Auto-select in manage tab
|
||||
selectManageItem(saved);
|
||||
switchTab('manage');
|
||||
} else {
|
||||
const err = await res.json();
|
||||
showToast(err.error || 'Failed to save product', 'error');
|
||||
@@ -704,22 +712,6 @@ async function handleItemFormSubmit(event) {
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmDeleteItem(itemId, name) {
|
||||
if (confirm(`Remove "${name}" from fridge?`)) {
|
||||
try {
|
||||
const res = await fetch(`/api/items/${itemId}`, { method: 'DELETE' });
|
||||
if (res.ok) {
|
||||
showToast(`Removed "${name}"`, 'info');
|
||||
await loadStats();
|
||||
await loadItems();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
showToast('Failed to delete product', 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
// Tablet Live Camera Barcode Scanner
|
||||
// ----------------------------------------------------
|
||||
@@ -785,17 +777,25 @@ async function stopCameraScanner() {
|
||||
async function onBarcodeScannedSuccess(decodedText) {
|
||||
playBeepSound();
|
||||
stopCameraScanner();
|
||||
document.getElementById('scanner-status').textContent = `Scanned ${decodedText}!`;
|
||||
closeScannerModal();
|
||||
|
||||
// Check if item exists in fridge
|
||||
const existing = state.items.find(i => i.barcode === decodedText);
|
||||
if (existing) {
|
||||
switchTab('manage');
|
||||
selectManageItem(existing);
|
||||
showToast(`Scanned: ${existing.name}`, 'success');
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise fetch from API / Open Food Facts
|
||||
try {
|
||||
const res = await fetch(`/api/barcode/${encodeURIComponent(decodedText)}`);
|
||||
closeScannerModal();
|
||||
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
openItemModal();
|
||||
applyProductDataToForm(data);
|
||||
showToast(`Scanned: ${data.name}`, 'success');
|
||||
showToast(`Scanned: ${data.name}. Click Save to add!`, 'success');
|
||||
} else {
|
||||
openItemModal();
|
||||
document.getElementById('modal-barcode').value = decodedText;
|
||||
@@ -803,7 +803,6 @@ async function onBarcodeScannedSuccess(decodedText) {
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
closeScannerModal();
|
||||
openItemModal();
|
||||
document.getElementById('modal-barcode').value = decodedText;
|
||||
}
|
||||
|
||||
+237
-148
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>Fridger - Smart Fridge Manager</title>
|
||||
<title>Fridger - Tablet Fridge Manager</title>
|
||||
|
||||
<!-- Tailwind CSS -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
@@ -18,9 +18,6 @@
|
||||
500: '#14b8a6',
|
||||
600: '#0d9488',
|
||||
}
|
||||
},
|
||||
screens: {
|
||||
'tablet': '640px',
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,74 +34,74 @@
|
||||
</head>
|
||||
<body class="h-full flex flex-col bg-slate-950 text-slate-100 antialiased selection:bg-teal-500 selection:text-white select-none">
|
||||
|
||||
<!-- TOP TABLET APP BAR -->
|
||||
<!-- TOP TABLET HEADER (MAIN -> SCAN -> MANAGE) -->
|
||||
<header class="sticky top-0 z-30 bg-slate-900/95 backdrop-blur-md border-b border-slate-800 shadow-md">
|
||||
<div class="max-w-5xl mx-auto px-3 sm:px-6">
|
||||
<div class="max-w-4xl mx-auto px-3 sm:px-6">
|
||||
<div class="flex items-center justify-between h-16 sm:h-20 gap-2">
|
||||
|
||||
<!-- Brand Logo (Compact & Clean) -->
|
||||
<!-- Logo -->
|
||||
<div class="flex items-center gap-2.5 shrink-0">
|
||||
<div class="w-10 h-10 sm:w-11 sm:h-11 rounded-2xl bg-gradient-to-tr from-teal-500 to-cyan-400 flex items-center justify-center shadow-lg shadow-teal-500/20 text-white font-black text-xl">
|
||||
🧊
|
||||
</div>
|
||||
<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>
|
||||
<p class="text-[11px] text-slate-400 font-medium">Smart Fridge</p>
|
||||
<p class="text-[11px] text-slate-400 font-medium">Tablet Manager</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3 MAIN TABLET BUTTONS: 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">
|
||||
|
||||
<!-- 1. MAIN (Fridge View) -->
|
||||
<!-- 1. MAIN (What's in the Fridge) -->
|
||||
<button
|
||||
id="nav-main-btn"
|
||||
onclick="switchTab('main')"
|
||||
class="flex items-center justify-center gap-2 px-3 sm:px-6 py-2.5 sm:py-3 rounded-xl text-xs sm:text-sm font-bold transition-all bg-teal-500 text-white shadow-md active:scale-95 touch-manipulation min-h-[44px]"
|
||||
class="flex items-center justify-center gap-2 px-4 sm:px-7 py-2.5 sm:py-3 rounded-xl text-xs sm:text-sm font-bold transition-all bg-teal-500 text-white shadow-md active:scale-95 touch-manipulation min-h-[44px]"
|
||||
>
|
||||
<i data-lucide="home" class="w-4 h-4 sm:w-5 sm:h-5"></i>
|
||||
<span>Main</span>
|
||||
</button>
|
||||
|
||||
<!-- 2. SCAN (Live Barcode Camera) -->
|
||||
<!-- 2. SCAN (Live Camera Barcode) -->
|
||||
<button
|
||||
id="nav-scan-btn"
|
||||
onclick="openScannerModal()"
|
||||
class="flex items-center justify-center gap-2 px-3 sm:px-6 py-2.5 sm:py-3 rounded-xl text-xs sm:text-sm font-bold transition-all text-slate-300 hover:text-white hover:bg-slate-800/80 active:scale-95 touch-manipulation min-h-[44px]"
|
||||
class="flex items-center justify-center gap-2 px-4 sm:px-7 py-2.5 sm:py-3 rounded-xl text-xs sm:text-sm font-bold transition-all text-slate-300 hover:text-white hover:bg-slate-800/80 active:scale-95 touch-manipulation min-h-[44px]"
|
||||
>
|
||||
<i data-lucide="scan-barcode" class="w-4 h-4 sm:w-5 sm:h-5 text-teal-400"></i>
|
||||
<span>Scan</span>
|
||||
</button>
|
||||
|
||||
<!-- 3. MANAGE (Manage Products) -->
|
||||
<!-- 3. MANAGE (Scan/Search -> Add/Remove -> Packet/Amount) -->
|
||||
<button
|
||||
id="nav-manage-btn"
|
||||
onclick="switchTab('manage')"
|
||||
class="flex items-center justify-center gap-2 px-3 sm:px-6 py-2.5 sm:py-3 rounded-xl text-xs sm:text-sm font-bold transition-all text-slate-300 hover:text-white hover:bg-slate-800/80 active:scale-95 touch-manipulation min-h-[44px]"
|
||||
class="flex items-center justify-center gap-2 px-4 sm:px-7 py-2.5 sm:py-3 rounded-xl text-xs sm:text-sm font-bold transition-all text-slate-300 hover:text-white hover:bg-slate-800/80 active:scale-95 touch-manipulation min-h-[44px]"
|
||||
>
|
||||
<i data-lucide="package" class="w-4 h-4 sm:w-5 sm:h-5"></i>
|
||||
<i data-lucide="sliders-horizontal" class="w-4 h-4 sm:w-5 sm:h-5"></i>
|
||||
<span>Manage</span>
|
||||
</button>
|
||||
|
||||
</nav>
|
||||
|
||||
<!-- Quick Add Item Button -->
|
||||
<!-- Quick Add New Product -->
|
||||
<button
|
||||
onclick="openItemModal()"
|
||||
class="flex items-center justify-center gap-1.5 px-3 sm:px-4 py-2.5 sm:py-3 rounded-xl bg-slate-800 hover:bg-slate-700 border border-slate-700 text-slate-200 text-xs sm:text-sm font-semibold transition active:scale-95 min-h-[44px] touch-manipulation shrink-0"
|
||||
title="Add Product Manually"
|
||||
title="New Product"
|
||||
>
|
||||
<i data-lucide="plus" class="w-4 h-4"></i>
|
||||
<span class="hidden sm:inline">Add</span>
|
||||
<span class="hidden sm:inline">New</span>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- TABLET QUICK STATS BAR -->
|
||||
<!-- QUICK STATS BAR -->
|
||||
<section class="bg-slate-900/50 border-b border-slate-800/60 py-2 sm:py-2.5">
|
||||
<div class="max-w-5xl mx-auto px-3 sm:px-6">
|
||||
<div class="max-w-4xl mx-auto px-3 sm:px-6">
|
||||
<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">
|
||||
<span class="w-2.5 h-2.5 rounded-full bg-teal-400"></span>
|
||||
@@ -126,154 +123,252 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- SEARCH & FILTER BAR -->
|
||||
<section class="max-w-5xl mx-auto px-3 sm:px-6 pt-3 sm:pt-4 pb-2 w-full space-y-2.5">
|
||||
<div class="flex flex-col sm:flex-row gap-2.5 items-stretch sm:items-center justify-between">
|
||||
|
||||
<!-- Search Input (Large tablet hit area) -->
|
||||
<div class="relative flex-1">
|
||||
<i data-lucide="search" class="w-4 h-4 text-slate-400 absolute left-3.5 top-1/2 -translate-y-1/2"></i>
|
||||
<input
|
||||
type="text"
|
||||
id="search-input"
|
||||
placeholder="Search items by name, barcode or brand..."
|
||||
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"
|
||||
>
|
||||
<button id="clear-search" onclick="clearSearch()" class="hidden absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-white p-1">
|
||||
<i data-lucide="x" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Quick Status Filters -->
|
||||
<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('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('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('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('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>
|
||||
|
||||
<!-- Category Filter Bar -->
|
||||
<div id="category-filter-bar" class="flex items-center gap-1.5 overflow-x-auto pb-1 scrollbar-none text-xs sm:text-sm font-medium">
|
||||
<!-- Populated dynamically -->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- MAIN VIEWPORT -->
|
||||
<main class="flex-1 max-w-5xl mx-auto px-3 sm:px-6 py-3 w-full">
|
||||
<!-- MAIN TAB CONTENT (FRIDGE 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">
|
||||
|
||||
<!-- 1. MAIN TAB: FRIDGE VIEW (Visual touch-friendly grid) -->
|
||||
<div id="view-tab-container">
|
||||
<div id="fridge-view-content" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3.5 sm:gap-4">
|
||||
<!-- Rendered items -->
|
||||
<!-- Search & Filter Ribbon in Main tab -->
|
||||
<div class="space-y-2.5">
|
||||
<div class="flex flex-col sm:flex-row gap-2.5 items-stretch sm:items-center justify-between">
|
||||
<div class="relative flex-1">
|
||||
<i data-lucide="search" class="w-4 h-4 text-slate-400 absolute left-3.5 top-1/2 -translate-y-1/2"></i>
|
||||
<input
|
||||
type="text"
|
||||
id="search-input"
|
||||
placeholder="Search items in fridge..."
|
||||
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"
|
||||
>
|
||||
<button id="clear-search" onclick="clearSearch()" class="hidden absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-white p-1">
|
||||
<i data-lucide="x" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<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('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('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('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('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 id="category-filter-bar" class="flex items-center gap-1.5 overflow-x-auto pb-1 scrollbar-none text-xs sm:text-sm font-medium">
|
||||
<!-- Injected by JS -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. MANAGE TAB: MANAGE PRODUCTS (Large tablet touch actions: Add Pack, Remove Pack, Manage Grams) -->
|
||||
<div id="manage-tab-container" class="hidden space-y-3">
|
||||
<div id="manage-items-list" class="space-y-3">
|
||||
<!-- Rendered management rows -->
|
||||
</div>
|
||||
<!-- Fridge Cards Grid -->
|
||||
<div id="fridge-view-content" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3.5 sm:gap-4 pt-2">
|
||||
<!-- Rendered items -->
|
||||
</div>
|
||||
|
||||
<!-- EMPTY STATE -->
|
||||
<!-- Empty State -->
|
||||
<div id="empty-state" class="hidden text-center py-16 px-4">
|
||||
<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>
|
||||
<h3 class="text-lg font-bold text-white mb-1">Your fridge 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 and grams.</p>
|
||||
<div class="flex items-center justify-center gap-3">
|
||||
<button onclick="openScannerModal()" class="px-5 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">
|
||||
<i data-lucide="scan-barcode" class="w-5 h-5"></i>
|
||||
<span>Scan Barcode</span>
|
||||
</button>
|
||||
<button onclick="seedDemoData()" class="px-4 py-3 rounded-xl bg-slate-800 hover:bg-slate-700 border border-slate-700 text-slate-300 text-sm font-medium active:scale-95 touch-manipulation">
|
||||
Load Sample Fridge
|
||||
</button>
|
||||
</div>
|
||||
<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>
|
||||
<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>
|
||||
<span>Go to Manage</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<!-- MANAGE GRAMS / WEIGHT ADJUSTMENT MODAL (Optimized for Tablet Touch) -->
|
||||
<div id="grams-modal" class="fixed inset-0 z-50 bg-black/80 backdrop-blur-sm hidden flex items-center justify-center p-3 sm:p-4">
|
||||
<div class="bg-slate-900 border border-slate-800 rounded-3xl max-w-md w-full p-5 sm:p-6 shadow-2xl space-y-5 animate-scale-up">
|
||||
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<span class="text-xs uppercase tracking-wider font-bold text-teal-400">Manage Grams / Amount</span>
|
||||
<h3 id="grams-modal-title" class="text-lg sm:text-xl font-extrabold text-white mt-0.5">Item Name</h3>
|
||||
<p id="grams-modal-sub" class="text-xs sm:text-sm text-slate-400">1 Full Packet = 250g</p>
|
||||
<!-- MANAGE TAB CONTENT (INTERACTIVE STEP FLOW: SCAN/SEARCH -> ADD/REMOVE -> PACKET/AMOUNT) -->
|
||||
<main id="manage-tab-view" class="hidden flex-1 max-w-4xl mx-auto px-3 sm:px-6 py-4 w-full space-y-5">
|
||||
|
||||
<!-- STEP 1: SCAN OR SEARCH / SELECT PRODUCT -->
|
||||
<div class="bg-slate-900 border border-slate-800 rounded-3xl p-4 sm:p-6 shadow-xl space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<div class="w-7 h-7 rounded-xl bg-teal-500/20 text-teal-300 flex items-center justify-center text-xs font-black">1</div>
|
||||
<h2 class="text-base sm:text-lg font-extrabold text-white">Scan Barcode or Search Product</h2>
|
||||
</div>
|
||||
<button onclick="closeGramsModal()" class="text-slate-400 hover:text-white p-2 rounded-xl bg-slate-800/60 touch-manipulation">
|
||||
<i data-lucide="x" class="w-5 h-5"></i>
|
||||
<button onclick="openItemModal()" class="text-xs text-teal-400 hover:text-teal-300 font-semibold flex items-center gap-1 touch-manipulation">
|
||||
<i data-lucide="plus-circle" class="w-4 h-4"></i>
|
||||
<span>Add New</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Live Remaining Big Gauge & Slider -->
|
||||
<div class="bg-slate-950 p-4 sm:p-5 rounded-2xl border border-slate-800/90 space-y-4 text-center">
|
||||
<div>
|
||||
<div class="text-4xl sm:text-5xl font-black text-white flex items-center justify-center gap-1.5">
|
||||
<span id="grams-current-val">250</span>
|
||||
<span id="grams-modal-unit" class="text-base sm:text-lg font-normal text-teal-400">g</span>
|
||||
</div>
|
||||
<div id="grams-percent-text" class="text-xs sm:text-sm text-slate-400 mt-1 font-medium">Remaining in open pack</div>
|
||||
</div>
|
||||
|
||||
<!-- Big touch-friendly slider -->
|
||||
<input
|
||||
type="range"
|
||||
id="grams-slider"
|
||||
min="0"
|
||||
max="500"
|
||||
step="5"
|
||||
value="250"
|
||||
oninput="onGramsSliderChange(this.value)"
|
||||
class="w-full accent-teal-500 cursor-pointer h-3.5 bg-slate-800 rounded-xl"
|
||||
<!-- Live Scan & Search Big Controls -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
<!-- Big Camera Scan Button -->
|
||||
<button
|
||||
onclick="openScannerModal()"
|
||||
class="sm:col-span-1 p-4 rounded-2xl bg-gradient-to-r from-teal-500 to-emerald-600 hover:from-teal-400 hover:to-emerald-500 text-white font-extrabold flex items-center justify-center gap-3 shadow-lg shadow-teal-500/25 active:scale-95 touch-manipulation min-h-[56px]"
|
||||
>
|
||||
<i data-lucide="camera" class="w-6 h-6"></i>
|
||||
<span class="text-sm sm:text-base">Scan Barcode</span>
|
||||
</button>
|
||||
|
||||
<!-- Large Tablet Touch Quick Buttons -->
|
||||
<div class="grid grid-cols-3 sm:grid-cols-6 gap-2 pt-1">
|
||||
<button onclick="applyGramsDelta(-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 active:scale-95 touch-manipulation">-10</button>
|
||||
<button onclick="applyGramsDelta(-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 active:scale-95 touch-manipulation">-25</button>
|
||||
<button onclick="applyGramsDelta(-50)" 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 active:scale-95 touch-manipulation">-50</button>
|
||||
<button onclick="applyGramsDelta(-100)" 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 active:scale-95 touch-manipulation">-100</button>
|
||||
<button onclick="setGramsPercent(0.5)" 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 active:scale-95 touch-manipulation">Half</button>
|
||||
<button onclick="setGramsPercent(0)" class="py-2.5 rounded-xl bg-rose-950/40 hover:bg-rose-900 text-xs sm:text-sm text-rose-300 border border-rose-800 font-bold active:scale-95 touch-manipulation">Empty (0)</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Exact Numeric Input -->
|
||||
<div class="flex items-center gap-3">
|
||||
<label class="text-xs sm:text-sm text-slate-300 font-medium shrink-0">Exact amount:</label>
|
||||
<div class="relative flex-1">
|
||||
<!-- Search / Barcode Input -->
|
||||
<div class="sm:col-span-2 relative flex items-center">
|
||||
<i data-lucide="search" class="w-5 h-5 text-slate-400 absolute left-4"></i>
|
||||
<input
|
||||
type="number"
|
||||
id="grams-exact-input"
|
||||
min="0"
|
||||
step="any"
|
||||
oninput="onGramsExactInput(this.value)"
|
||||
class="w-full bg-slate-950 border border-slate-800 rounded-xl px-3.5 py-2.5 text-base sm:text-lg font-bold text-white focus:border-teal-500 outline-none"
|
||||
type="text"
|
||||
id="manage-search-input"
|
||||
placeholder="Type name, brand, or barcode number..."
|
||||
oninput="handleManageSearch(this.value)"
|
||||
class="w-full bg-slate-950 border border-slate-800 focus:border-teal-500 rounded-2xl pl-12 pr-4 py-3.5 text-sm sm:text-base text-white placeholder-slate-500 outline-none transition min-h-[56px]"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between pt-2">
|
||||
<button onclick="consumeAllAndRemove()" class="text-xs sm:text-sm text-rose-400 hover:text-rose-300 flex items-center gap-1.5 p-2 rounded-xl hover:bg-rose-500/10 touch-manipulation">
|
||||
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
||||
<span>Remove Item</span>
|
||||
</button>
|
||||
<div class="flex items-center gap-2">
|
||||
<button onclick="closeGramsModal()" class="px-4 py-3 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-300 text-xs sm:text-sm font-medium touch-manipulation">Cancel</button>
|
||||
<button onclick="saveGramsAdjustment()" class="px-6 py-3 rounded-xl bg-teal-500 hover:bg-teal-400 text-white text-xs sm:text-sm font-bold shadow-lg shadow-teal-500/20 active:scale-95 touch-manipulation">Save Grams</button>
|
||||
<!-- Quick Product Pick List -->
|
||||
<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>
|
||||
<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 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- STEP 2 & 3: ACTION SELECTION (ADD / REMOVE) & QUANTITY TYPE (PACKET / AMOUNT) -->
|
||||
<div id="manage-action-card" class="hidden bg-slate-900 border border-slate-700/80 rounded-3xl p-5 sm:p-6 shadow-2xl space-y-6 animate-scale-up">
|
||||
|
||||
<!-- Selected Product Banner -->
|
||||
<div class="bg-slate-950/80 p-4 rounded-2xl border border-slate-800 flex items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-3.5 min-w-0">
|
||||
<div id="manage-selected-img" class="w-14 h-14 rounded-xl bg-slate-800 flex items-center justify-center text-teal-400 shrink-0 overflow-hidden">
|
||||
<i data-lucide="package" class="w-7 h-7"></i>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button onclick="clearSelectedManageItem()" class="text-slate-400 hover:text-white p-2 rounded-xl bg-slate-800/80 touch-manipulation">
|
||||
<i data-lucide="x" class="w-5 h-5"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- STEP 2: CHOOSE ADD OR REMOVE -->
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-7 h-7 rounded-xl bg-teal-500/20 text-teal-300 flex items-center justify-center text-xs font-black">2</div>
|
||||
<h4 class="text-sm sm:text-base font-bold text-white">Choose Action</h4>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-3 sm:gap-4">
|
||||
<button
|
||||
id="manage-btn-add"
|
||||
onclick="setManageAction('add')"
|
||||
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>
|
||||
<span class="text-sm sm:text-base">➕ Add to Fridge</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
id="manage-btn-remove"
|
||||
onclick="setManageAction('remove')"
|
||||
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>
|
||||
<span class="text-sm sm:text-base">➖ Remove from Fridge</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- STEP 3: CHOOSE QUANTITY TYPE (WHOLE PACKET OR SPECIFIC GRAMS/AMOUNT) -->
|
||||
<div id="manage-qty-type-section" class="hidden space-y-4 pt-2 border-t border-slate-800">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-7 h-7 rounded-xl bg-teal-500/20 text-teal-300 flex items-center justify-center text-xs font-black">3</div>
|
||||
<h4 id="manage-step3-title" class="text-sm sm:text-base font-bold text-white">How much to Add / Remove?</h4>
|
||||
</div>
|
||||
|
||||
<!-- Mode Toggle: Whole Packet vs Specific Grams -->
|
||||
<div class="grid grid-cols-2 gap-2 p-1.5 bg-slate-950 rounded-2xl border border-slate-800">
|
||||
<button
|
||||
id="qty-mode-packet-btn"
|
||||
onclick="setQtyMode('packet')"
|
||||
class="py-3 px-3 rounded-xl font-bold text-xs sm:text-sm transition bg-teal-500 text-white shadow-md touch-manipulation flex items-center justify-center gap-2"
|
||||
>
|
||||
<i data-lucide="package" class="w-4 h-4"></i>
|
||||
<span>Whole Packet(s)</span>
|
||||
</button>
|
||||
<button
|
||||
id="qty-mode-amount-btn"
|
||||
onclick="setQtyMode('amount')"
|
||||
class="py-3 px-3 rounded-xl font-bold text-xs sm:text-sm transition text-slate-300 hover:text-white touch-manipulation flex items-center justify-center gap-2"
|
||||
>
|
||||
<i data-lucide="scale" class="w-4 h-4"></i>
|
||||
<span>Specific Grams / Amount</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Option A: Whole Packet Controls -->
|
||||
<div id="qty-packet-controls" class="bg-slate-950 p-4 sm:p-5 rounded-2xl border border-slate-800 space-y-4 text-center">
|
||||
<div class="text-xs sm:text-sm text-slate-400 font-medium">
|
||||
1 Packet = <strong id="manage-pack-size-label" class="text-teal-300 font-bold">250g</strong>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-center gap-4">
|
||||
<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 class="text-3xl sm:text-4xl font-black text-white min-w-[90px]">
|
||||
<span id="manage-pack-count">1</span> <span class="text-sm font-normal text-slate-400">pack</span>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<!-- Quick Chips -->
|
||||
<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(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(3)" 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">3 Packs</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Option B: Specific Grams Controls (Slider & Quick Chips) -->
|
||||
<div id="qty-amount-controls" class="hidden bg-slate-950 p-4 sm:p-5 rounded-2xl border border-slate-800 space-y-4 text-center">
|
||||
<div>
|
||||
<div class="text-3xl sm:text-4xl font-black text-white flex items-center justify-center gap-1.5">
|
||||
<span id="manage-amount-val">150</span>
|
||||
<span id="manage-amount-unit" class="text-base font-normal text-teal-400">g</span>
|
||||
</div>
|
||||
<div id="manage-amount-subtext" class="text-xs text-slate-400 mt-1 font-medium">New remaining amount in fridge</div>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="range"
|
||||
id="manage-amount-slider"
|
||||
min="0"
|
||||
max="500"
|
||||
step="5"
|
||||
value="150"
|
||||
oninput="onManageAmountSlider(this.value)"
|
||||
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">
|
||||
<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(-50)" 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">-50</button>
|
||||
<button onclick="applyManageDelta(-100)" 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">-100</button>
|
||||
<button onclick="setManageAmountRatio(0.5)" 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">Half</button>
|
||||
<button onclick="setManageAmountRatio(0)" class="py-2.5 rounded-xl bg-rose-950/40 hover:bg-rose-900 text-xs sm:text-sm text-rose-300 border border-rose-800 font-bold touch-manipulation">0 (Empty)</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Big Confirm Button -->
|
||||
<button
|
||||
id="manage-confirm-btn"
|
||||
onclick="confirmManageExecution()"
|
||||
class="w-full py-4 rounded-2xl bg-teal-500 hover:bg-teal-400 text-white font-black text-base sm:text-lg shadow-xl shadow-teal-500/25 active:scale-95 transition-all touch-manipulation flex items-center justify-center gap-2"
|
||||
>
|
||||
<i data-lucide="check" class="w-6 h-6"></i>
|
||||
<span id="manage-confirm-label">Confirm Update</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<!-- ADD / EDIT ITEM MODAL -->
|
||||
<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">
|
||||
@@ -312,7 +407,6 @@
|
||||
<input type="hidden" id="modal-item-id">
|
||||
<input type="hidden" id="modal-image-url">
|
||||
|
||||
<!-- Image Preview if available -->
|
||||
<div id="modal-image-container" class="hidden items-center gap-3 p-2.5 bg-slate-950 rounded-2xl border border-slate-800">
|
||||
<img id="modal-image-preview" src="" alt="Product" class="w-12 h-12 object-contain rounded-xl bg-white/5">
|
||||
<div class="flex-1 min-w-0">
|
||||
@@ -321,7 +415,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Name & Brand -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-2.5">
|
||||
<div class="sm:col-span-2">
|
||||
<label class="block text-slate-300 font-semibold mb-1">Product Name *</label>
|
||||
@@ -333,7 +426,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quantity & Unit -->
|
||||
<div class="grid grid-cols-3 gap-2.5">
|
||||
<div>
|
||||
<label class="block text-slate-300 font-semibold mb-1">Current Amount *</label>
|
||||
@@ -357,7 +449,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Category -->
|
||||
<div>
|
||||
<label class="block text-slate-300 font-semibold mb-1">Category</label>
|
||||
<select id="modal-category" 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">
|
||||
@@ -365,7 +456,6 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Expiry Date -->
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<label class="block text-slate-300 font-semibold">Expiration Date</label>
|
||||
@@ -382,10 +472,9 @@
|
||||
<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>
|
||||
|
||||
<!-- Notes -->
|
||||
<div>
|
||||
<label class="block text-slate-300 font-semibold mb-1">Notes (Optional)</label>
|
||||
<input type="text" id="modal-notes" placeholder="e.g. For pasta 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. 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">
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end gap-3 pt-3 border-t border-slate-800">
|
||||
@@ -446,7 +535,7 @@
|
||||
<!-- TOAST NOTIFICATION CONTAINER -->
|
||||
<div id="toast-container" class="fixed bottom-5 right-5 z-50 flex flex-col gap-2 max-w-sm pointer-events-none"></div>
|
||||
|
||||
<!-- MAIN CLIENT SCRIPT -->
|
||||
<!-- MAIN SCRIPT -->
|
||||
<script src="/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user