feat(manage): implement interactive Scan/Search -> Add/Remove -> Packet/Amount workflow
Build and Push Fridger Container / Build & Push Fridger Image (push) Successful in 1m27s

This commit is contained in:
2026-10-10 23:48:28 +02:00
parent 3095443420
commit b73d3b6250
2 changed files with 531 additions and 443 deletions
+294 -295
View File
@@ -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>&bull;</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;
}