From 56eed473c7916e048798ed2c8250e387785d4c85 Mon Sep 17 00:00:00 2001 From: max Date: Sat, 10 Oct 2026 23:55:00 +0200 Subject: [PATCH] feat(routes): implement / for current stock overview and /manage for tablet station with auto-return --- public/app.js | 357 ++++++++++++++------------- public/index.html | 599 +++++++++++++++++++++++----------------------- 2 files changed, 493 insertions(+), 463 deletions(-) diff --git a/public/app.js b/public/app.js index a1be8f9..9f05cb9 100644 --- a/public/app.js +++ b/public/app.js @@ -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 => ` - - `).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 ` -
+
- +
${escapeHtml(item.brand || item.category)} - ${isOpened ? 'Opened / In Use' : 'Full Pack'} + ${isOpened ? 'Opened' : 'Full Pack'}
@@ -265,7 +282,7 @@ function renderApp() {
- +
@@ -282,12 +299,6 @@ function renderApp() {
- - -
`; }).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 = `
- No matching items found. Scan barcode or add as new product! + No matching product found. Scan barcode or tap "Add Manually"!
`; return; @@ -322,8 +353,8 @@ function renderManageQuickPicker(filterQuery = '') { container.innerHTML = matched.map(item => ` - - - - + - - - - - + - -
-
-
-
- - In Stock: 0 items -
-
- - Full Packs: 0 -
-
- - In Use / Partial: 0 -
-
- - Low (≤25%): 0 -
-
-
-
- - -
+ + + +
- -
-
-
- - - + +
+
+
+
+ + Total Stock: 0 items +
+
+ + Full Packs: 0 +
+
+ + Opened / Partial: 0 +
+
+ + Low (≤25%): 0 +
+
+
+
+ + +
+
+
+
+ + + +
+ +
+ + + + +
-
- - - - +
+
-
- + +
+
-
- -
- -
- - - - -
- - -
- - -
-
-
-
1
-

Scan Barcode or Search Product

+ + +
- -
- - +
- -
- - -
-
- -
- -
- -
-
-
- - - + +
+ + + +