feat(routes): implement / for current stock overview and /manage for tablet station with auto-return
Build and Push Fridger Container / Build & Push Fridger Image (push) Successful in 1m21s
Build and Push Fridger Container / Build & Push Fridger Image (push) Successful in 1m21s
This commit is contained in:
+194
-163
@@ -1,11 +1,11 @@
|
||||
/**
|
||||
* Fridger Frontend Application
|
||||
* Tablet-optimized Fridge & Freezer Inventory Manager (No expiration dates)
|
||||
* Main -> Scan -> Manage (Scan/Search -> Add/Remove -> Packet/Amount)
|
||||
* Route / : Current Stock Overview
|
||||
* Route /manage : Tablet Scanner & Manager Station
|
||||
*/
|
||||
|
||||
const state = {
|
||||
currentTab: 'main', // 'main' or 'manage'
|
||||
currentRoute: '/', // '/' or '/manage'
|
||||
items: [],
|
||||
meta: {
|
||||
categories: [],
|
||||
@@ -28,11 +28,19 @@ const state = {
|
||||
isScanning: false
|
||||
};
|
||||
|
||||
// Initialize Application
|
||||
// Initialize Application & Router
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
await loadMetadata();
|
||||
await loadStats();
|
||||
await loadItems();
|
||||
|
||||
// Handle initial browser route
|
||||
handleInitialRoute();
|
||||
|
||||
window.addEventListener('popstate', () => {
|
||||
handleInitialRoute();
|
||||
});
|
||||
|
||||
initLucide();
|
||||
});
|
||||
|
||||
@@ -42,6 +50,56 @@ function initLucide() {
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
// Client-Side Router
|
||||
// ----------------------------------------------------
|
||||
|
||||
function handleInitialRoute() {
|
||||
const path = window.location.pathname;
|
||||
if (path === '/manage' || path.startsWith('/manage')) {
|
||||
setRoute('/manage');
|
||||
} else {
|
||||
setRoute('/');
|
||||
}
|
||||
}
|
||||
|
||||
function navigateTo(path) {
|
||||
window.history.pushState({}, '', path);
|
||||
setRoute(path);
|
||||
}
|
||||
|
||||
function setRoute(path) {
|
||||
state.currentRoute = path === '/manage' ? '/manage' : '/';
|
||||
|
||||
const stockBtn = document.getElementById('nav-stock-btn');
|
||||
const manageBtn = document.getElementById('nav-manage-btn');
|
||||
const stockPage = document.getElementById('stock-view-page');
|
||||
const managePage = document.getElementById('manage-view-page');
|
||||
const subTitle = document.getElementById('page-subtitle');
|
||||
|
||||
const activeClass = 'flex items-center justify-center gap-2 px-3.5 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 inactiveClass = 'flex items-center justify-center gap-2 px-3.5 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]';
|
||||
|
||||
if (state.currentRoute === '/') {
|
||||
stockBtn.className = activeClass;
|
||||
manageBtn.className = inactiveClass;
|
||||
stockPage.classList.remove('hidden');
|
||||
managePage.classList.add('hidden');
|
||||
subTitle.textContent = 'Current Stock';
|
||||
renderStockView();
|
||||
} else {
|
||||
manageBtn.className = activeClass;
|
||||
stockBtn.className = inactiveClass;
|
||||
managePage.classList.remove('hidden');
|
||||
stockPage.classList.add('hidden');
|
||||
subTitle.textContent = 'Tablet Station';
|
||||
resetTabletManageStation();
|
||||
renderTabletProductPicker();
|
||||
}
|
||||
|
||||
initLucide();
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
// API Calls
|
||||
// ----------------------------------------------------
|
||||
@@ -84,49 +142,20 @@ async function loadItems() {
|
||||
const res = await fetch(`/api/items?${params.toString()}`);
|
||||
if (res.ok) {
|
||||
state.items = await res.json();
|
||||
renderApp();
|
||||
renderManageQuickPicker();
|
||||
if (state.currentRoute === '/') {
|
||||
renderStockView();
|
||||
} else {
|
||||
renderTabletProductPicker();
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load items:', err);
|
||||
console.error('Failed to load inventory:', err);
|
||||
showToast('Failed to load inventory', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
// Navigation Tab Switching (Main -> Scan -> Manage)
|
||||
// ----------------------------------------------------
|
||||
|
||||
function switchTab(tab) {
|
||||
state.currentTab = tab;
|
||||
|
||||
const mainBtn = document.getElementById('nav-main-btn');
|
||||
const manageBtn = document.getElementById('nav-manage-btn');
|
||||
const mainView = document.getElementById('main-tab-view');
|
||||
const manageView = document.getElementById('manage-tab-view');
|
||||
|
||||
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;
|
||||
mainView.classList.remove('hidden');
|
||||
manageView.classList.add('hidden');
|
||||
renderApp();
|
||||
} else if (tab === 'manage') {
|
||||
manageBtn.className = activeClasses;
|
||||
mainBtn.className = inactiveClasses;
|
||||
manageView.classList.remove('hidden');
|
||||
mainView.classList.add('hidden');
|
||||
renderManageQuickPicker();
|
||||
}
|
||||
|
||||
initLucide();
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
// Filters & Search (Main Tab)
|
||||
// ROUTE 1: / (CURRENT STOCK DISPLAY)
|
||||
// ----------------------------------------------------
|
||||
|
||||
let searchDebounceTimer = null;
|
||||
@@ -200,22 +229,10 @@ function renderCategoryFilters() {
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
function populateModalSelects() {
|
||||
const catSelect = document.getElementById('modal-category');
|
||||
if (catSelect && state.meta.categories) {
|
||||
catSelect.innerHTML = state.meta.categories.map(c => `
|
||||
<option value="${escapeHtml(c.id)}">${escapeHtml(c.name)}</option>
|
||||
`).join('');
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
// UI Rendering (Main Tab: Fridge / Freezer Cards)
|
||||
// ----------------------------------------------------
|
||||
|
||||
function renderApp() {
|
||||
const emptyState = document.getElementById('empty-state');
|
||||
const container = document.getElementById('fridge-view-content');
|
||||
function renderStockView() {
|
||||
const container = document.getElementById('stock-grid');
|
||||
const emptyState = document.getElementById('empty-stock');
|
||||
if (!container) return;
|
||||
|
||||
if (state.items.length === 0) {
|
||||
emptyState.classList.remove('hidden');
|
||||
@@ -236,16 +253,16 @@ function renderApp() {
|
||||
else if (percent <= 50) progressBarColor = 'bg-amber-500';
|
||||
|
||||
return `
|
||||
<div class="shelf-card rounded-2xl sm:rounded-3xl p-4 sm:p-5 flex flex-col justify-between transition hover:border-teal-500/40 space-y-3.5">
|
||||
<div class="shelf-card rounded-2xl sm:rounded-3xl p-4 sm:p-5 flex flex-col justify-between space-y-3.5">
|
||||
|
||||
<div>
|
||||
<!-- Header: Brand & Status Pill -->
|
||||
<!-- Header: Brand & Status -->
|
||||
<div class="flex items-center justify-between gap-2 mb-2">
|
||||
<span class="text-xs font-bold text-teal-400 bg-teal-500/10 px-2.5 py-1 rounded-lg truncate max-w-[140px]">
|
||||
${escapeHtml(item.brand || item.category)}
|
||||
</span>
|
||||
<span class="text-xs font-bold px-2.5 py-1 rounded-lg flex items-center gap-1.5 shrink-0 ${isOpened ? 'bg-indigo-500/20 text-indigo-300 border border-indigo-500/30' : 'bg-emerald-500/15 text-emerald-300 border border-emerald-500/20'}">
|
||||
<span>${isOpened ? 'Opened / In Use' : 'Full Pack'}</span>
|
||||
<span>${isOpened ? 'Opened' : 'Full Pack'}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -265,7 +282,7 @@ function renderApp() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Grams / Quantity Progress -->
|
||||
<!-- Stock & Grams Indicator -->
|
||||
<div class="mt-3 bg-slate-950/80 rounded-2xl p-3 border border-slate-800/80">
|
||||
<div class="flex items-center justify-between text-xs sm:text-sm mb-1.5">
|
||||
<span class="font-bold text-white flex items-center gap-1">
|
||||
@@ -282,12 +299,6 @@ function renderApp() {
|
||||
</div>
|
||||
</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('');
|
||||
@@ -296,11 +307,31 @@ function renderApp() {
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
// MANAGE TAB: 3-Step Flow (Scan/Search -> Add/Remove -> Packet/Amount)
|
||||
// ROUTE 2: /manage (TABLET STATION WORKFLOW)
|
||||
// ----------------------------------------------------
|
||||
|
||||
function renderManageQuickPicker(filterQuery = '') {
|
||||
const container = document.getElementById('manage-quick-picker');
|
||||
function resetTabletManageStation() {
|
||||
state.manage.selectedItem = null;
|
||||
state.manage.action = null;
|
||||
document.getElementById('tablet-step-select').classList.remove('hidden');
|
||||
document.getElementById('tablet-step-action').classList.add('hidden');
|
||||
document.getElementById('tablet-search-input').value = '';
|
||||
}
|
||||
|
||||
function handleTabletSearch(val) {
|
||||
renderTabletProductPicker(val);
|
||||
|
||||
const clean = val.trim();
|
||||
if (/^\d{8,14}$/.test(clean)) {
|
||||
const found = state.items.find(i => i.barcode === clean);
|
||||
if (found) {
|
||||
forwardToProductAction(found);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderTabletProductPicker(filterQuery = '') {
|
||||
const container = document.getElementById('tablet-product-picker');
|
||||
if (!container) return;
|
||||
|
||||
const query = filterQuery.toLowerCase().trim();
|
||||
@@ -314,7 +345,7 @@ function renderManageQuickPicker(filterQuery = '') {
|
||||
if (matched.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="col-span-full py-4 text-center text-xs text-slate-500">
|
||||
No matching items found. Scan barcode or add as new product!
|
||||
No matching product found. Scan barcode or tap "Add Manually"!
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
@@ -322,8 +353,8 @@ function renderManageQuickPicker(filterQuery = '') {
|
||||
|
||||
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' : ''}"
|
||||
onclick="forwardToProductActionById(${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-[52px]"
|
||||
>
|
||||
${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">
|
||||
@@ -344,90 +375,71 @@ function renderManageQuickPicker(filterQuery = '') {
|
||||
initLucide();
|
||||
}
|
||||
|
||||
function handleManageSearch(val) {
|
||||
renderManageQuickPicker(val);
|
||||
|
||||
const clean = val.trim();
|
||||
if (/^\d{8,14}$/.test(clean)) {
|
||||
const found = state.items.find(i => i.barcode === clean);
|
||||
if (found) {
|
||||
selectManageItem(found);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function selectManageItemById(id) {
|
||||
function forwardToProductActionById(id) {
|
||||
const item = state.items.find(i => i.id === id);
|
||||
if (item) selectManageItem(item);
|
||||
if (item) forwardToProductAction(item);
|
||||
}
|
||||
|
||||
function selectManageItem(item) {
|
||||
// Forwards to the action screen on /manage
|
||||
function forwardToProductAction(item) {
|
||||
state.manage.selectedItem = item;
|
||||
state.manage.action = null; // reset step 2
|
||||
state.manage.action = null; // reset action
|
||||
state.manage.packetCount = 1;
|
||||
state.manage.amountValue = item.quantity;
|
||||
|
||||
document.getElementById('manage-selected-name').textContent = item.name;
|
||||
document.getElementById('manage-selected-details').textContent = `Currently: ${item.quantity} ${item.unit} in stock • 1 Full Pack = ${item.initial_quantity} ${item.unit}`;
|
||||
document.getElementById('manage-pack-size-label').textContent = `${item.initial_quantity} ${item.unit}`;
|
||||
document.getElementById('manage-amount-unit').textContent = item.unit;
|
||||
document.getElementById('manage-amount-val').textContent = item.quantity;
|
||||
// Populate header banner
|
||||
document.getElementById('action-product-name').textContent = item.name;
|
||||
document.getElementById('action-product-sub').textContent = `Current Stock: ${item.quantity} ${item.unit} • 1 Full Pack = ${item.initial_quantity} ${item.unit}`;
|
||||
document.getElementById('action-pack-size-label').textContent = `${item.initial_quantity} ${item.unit}`;
|
||||
document.getElementById('action-amount-unit').textContent = item.unit;
|
||||
document.getElementById('action-amount-val').textContent = item.quantity;
|
||||
|
||||
const slider = document.getElementById('manage-amount-slider');
|
||||
const slider = document.getElementById('action-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');
|
||||
const imgBox = document.getElementById('action-product-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>`;
|
||||
}
|
||||
|
||||
document.getElementById('manage-action-card').classList.remove('hidden');
|
||||
document.getElementById('manage-qty-type-section').classList.add('hidden');
|
||||
|
||||
// Switch view state
|
||||
document.getElementById('tablet-step-select').classList.add('hidden');
|
||||
document.getElementById('tablet-step-action').classList.remove('hidden');
|
||||
document.getElementById('action-qty-section').classList.add('hidden');
|
||||
|
||||
resetActionButtonsStyle();
|
||||
renderManageQuickPicker();
|
||||
initLucide();
|
||||
|
||||
document.getElementById('manage-action-card').scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
document.getElementById('tablet-step-action').scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}
|
||||
|
||||
function clearSelectedManageItem() {
|
||||
state.manage.selectedItem = null;
|
||||
state.manage.action = null;
|
||||
document.getElementById('manage-action-card').classList.add('hidden');
|
||||
renderManageQuickPicker();
|
||||
function cancelActionAndReturnToManage() {
|
||||
resetTabletManageStation();
|
||||
renderTabletProductPicker();
|
||||
}
|
||||
|
||||
function goToManageItem(itemId) {
|
||||
switchTab('manage');
|
||||
selectManageItemById(itemId);
|
||||
}
|
||||
|
||||
function setManageAction(action) {
|
||||
function setActionType(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');
|
||||
const btnAdd = document.getElementById('action-btn-add');
|
||||
const btnRemove = document.getElementById('action-btn-remove');
|
||||
const qtySection = document.getElementById('action-qty-section');
|
||||
const confirmBtn = document.getElementById('action-confirm-btn');
|
||||
const confirmLabel = document.getElementById('action-confirm-label');
|
||||
|
||||
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 Stock`;
|
||||
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-[84px] 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-[84px] bg-slate-950 border-slate-800 text-slate-400 hover:border-slate-700';
|
||||
confirmBtn.className = 'flex-1 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 & Done`;
|
||||
} 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 Stock`;
|
||||
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-[84px] 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-[84px] bg-slate-950 border-slate-800 text-slate-400 hover:border-slate-700';
|
||||
confirmBtn.className = 'flex-1 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 & Done`;
|
||||
}
|
||||
|
||||
qtySection.classList.remove('hidden');
|
||||
@@ -435,19 +447,19 @@ function setManageAction(action) {
|
||||
}
|
||||
|
||||
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;
|
||||
const btnAdd = document.getElementById('action-btn-add');
|
||||
const btnRemove = document.getElementById('action-btn-remove');
|
||||
const def = '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-[84px] bg-slate-950 border-slate-800 text-slate-300 hover:border-slate-700';
|
||||
btnAdd.className = def;
|
||||
btnRemove.className = def;
|
||||
}
|
||||
|
||||
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');
|
||||
function setActionQtyMode(mode) {
|
||||
state.manage.qtyMode = mode;
|
||||
const btnPack = document.getElementById('btn-mode-packet');
|
||||
const btnAmount = document.getElementById('btn-mode-amount');
|
||||
const packControls = document.getElementById('controls-packet');
|
||||
const amountControls = document.getElementById('controls-amount');
|
||||
|
||||
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';
|
||||
@@ -467,35 +479,36 @@ function setQtyMode(mode) {
|
||||
initLucide();
|
||||
}
|
||||
|
||||
function changePacketCount(delta) {
|
||||
function changeActionPackCount(delta) {
|
||||
state.manage.packetCount = Math.max(1, state.manage.packetCount + delta);
|
||||
document.getElementById('manage-pack-count').textContent = state.manage.packetCount;
|
||||
document.getElementById('action-pack-count-val').textContent = state.manage.packetCount;
|
||||
}
|
||||
|
||||
function setPacketCount(num) {
|
||||
function setActionPackCount(num) {
|
||||
state.manage.packetCount = num;
|
||||
document.getElementById('manage-pack-count').textContent = num;
|
||||
document.getElementById('action-pack-count-val').textContent = num;
|
||||
}
|
||||
|
||||
function onManageAmountSlider(val) {
|
||||
function onActionAmountSlider(val) {
|
||||
state.manage.amountValue = parseFloat(val);
|
||||
document.getElementById('manage-amount-val').textContent = state.manage.amountValue;
|
||||
document.getElementById('action-amount-val').textContent = state.manage.amountValue;
|
||||
}
|
||||
|
||||
function applyManageDelta(delta) {
|
||||
function applyActionDelta(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;
|
||||
document.getElementById('action-amount-slider').value = state.manage.amountValue;
|
||||
document.getElementById('action-amount-val').textContent = state.manage.amountValue;
|
||||
}
|
||||
|
||||
function setManageAmountRatio(ratio) {
|
||||
function setActionAmountRatio(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;
|
||||
document.getElementById('action-amount-slider').value = state.manage.amountValue;
|
||||
document.getElementById('action-amount-val').textContent = state.manage.amountValue;
|
||||
}
|
||||
|
||||
async function confirmManageExecution() {
|
||||
// Confirm & finish execution -> Returns right back to /manage ready for next scan!
|
||||
async function confirmAndFinishAction() {
|
||||
const item = state.manage.selectedItem;
|
||||
const action = state.manage.action;
|
||||
const mode = state.manage.qtyMode;
|
||||
@@ -517,14 +530,17 @@ async function confirmManageExecution() {
|
||||
if (res.ok) {
|
||||
const updated = await res.json();
|
||||
playBeepSound();
|
||||
showToast(`✓ ${action === 'add' ? 'Added' : 'Removed'} ${state.manage.packetCount} pack(s) of ${updated.name} (Now: ${updated.quantity} ${updated.unit})`, 'success');
|
||||
clearSelectedManageItem();
|
||||
showToast(`✓ ${action === 'add' ? 'Added' : 'Removed'} ${state.manage.packetCount} pack(s) of ${updated.name}`, 'success');
|
||||
|
||||
// Return to manage station
|
||||
resetTabletManageStation();
|
||||
await loadStats();
|
||||
await loadItems();
|
||||
} else {
|
||||
showToast('Failed to update packet', 'error');
|
||||
}
|
||||
} else {
|
||||
// Amount / Grams mode
|
||||
const newQty = state.manage.amountValue;
|
||||
const res = await fetch(`/api/items/${item.id}/quantity`, {
|
||||
method: 'PATCH',
|
||||
@@ -536,7 +552,9 @@ async function confirmManageExecution() {
|
||||
const updated = await res.json();
|
||||
playBeepSound();
|
||||
showToast(`✓ Set ${updated.name} to ${updated.quantity} ${updated.unit}`, 'success');
|
||||
clearSelectedManageItem();
|
||||
|
||||
// Return to manage station
|
||||
resetTabletManageStation();
|
||||
await loadStats();
|
||||
await loadItems();
|
||||
} else {
|
||||
@@ -544,13 +562,13 @@ async function confirmManageExecution() {
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Execution error:', err);
|
||||
console.error('Action error:', err);
|
||||
showToast('Network error while updating item', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
// Add / Edit Product Modal Logic
|
||||
// Add Product Manually Modal
|
||||
// ----------------------------------------------------
|
||||
|
||||
function openItemModal(itemId = null) {
|
||||
@@ -582,7 +600,7 @@ function openItemModal(itemId = null) {
|
||||
}
|
||||
} else {
|
||||
document.getElementById('modal-item-id').value = '';
|
||||
document.getElementById('item-modal-title').textContent = 'Add Product';
|
||||
document.getElementById('item-modal-title').textContent = 'Add Product Manually';
|
||||
document.getElementById('modal-unit').value = 'g';
|
||||
document.getElementById('modal-quantity').value = '';
|
||||
document.getElementById('modal-initial-quantity').value = '';
|
||||
@@ -679,8 +697,10 @@ async function handleItemFormSubmit(event) {
|
||||
closeItemModal();
|
||||
await loadStats();
|
||||
await loadItems();
|
||||
selectManageItem(saved);
|
||||
switchTab('manage');
|
||||
|
||||
// Forward directly to action screen for this new product on /manage!
|
||||
setRoute('/manage');
|
||||
forwardToProductAction(saved);
|
||||
} else {
|
||||
const err = await res.json();
|
||||
showToast(err.error || 'Failed to save product', 'error');
|
||||
@@ -691,6 +711,15 @@ async function handleItemFormSubmit(event) {
|
||||
}
|
||||
}
|
||||
|
||||
function populateModalSelects() {
|
||||
const catSelect = document.getElementById('modal-category');
|
||||
if (catSelect && state.meta.categories) {
|
||||
catSelect.innerHTML = state.meta.categories.map(c => `
|
||||
<option value="${escapeHtml(c.id)}">${escapeHtml(c.name)}</option>
|
||||
`).join('');
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
// Tablet Live Camera Barcode Scanner
|
||||
// ----------------------------------------------------
|
||||
@@ -758,25 +787,27 @@ async function onBarcodeScannedSuccess(decodedText) {
|
||||
stopCameraScanner();
|
||||
closeScannerModal();
|
||||
|
||||
// If item exists in stock -> forward directly to product action screen!
|
||||
const existing = state.items.find(i => i.barcode === decodedText);
|
||||
if (existing) {
|
||||
switchTab('manage');
|
||||
selectManageItem(existing);
|
||||
setRoute('/manage');
|
||||
forwardToProductAction(existing);
|
||||
showToast(`Scanned: ${existing.name}`, 'success');
|
||||
return;
|
||||
}
|
||||
|
||||
// Not yet in stock -> lookup in Open Food Facts and open modal to confirm
|
||||
try {
|
||||
const res = await fetch(`/api/barcode/${encodeURIComponent(decodedText)}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
openItemModal();
|
||||
applyProductDataToForm(data);
|
||||
showToast(`Scanned: ${data.name}. Click Save to add!`, 'success');
|
||||
showToast(`Scanned: ${data.name}. Click Save to add to stock!`, 'success');
|
||||
} else {
|
||||
openItemModal();
|
||||
document.getElementById('modal-barcode').value = decodedText;
|
||||
showToast(`Scanned barcode: ${decodedText}. Enter details.`, 'info');
|
||||
showToast(`Scanned barcode: ${decodedText}. Enter product name.`, 'info');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
Reference in New Issue
Block a user