diff --git a/public/app.js b/public/app.js index 0311d62..6c2c673 100644 --- a/public/app.js +++ b/public/app.js @@ -5,7 +5,8 @@ */ const state = { - currentRoute: '/', // '/' or '/manage' + currentRoute: '/', // '/', '/filters', or '/manage' + allItems: [], items: [], meta: { categories: [], @@ -58,6 +59,8 @@ function handleInitialRoute() { const path = window.location.pathname; if (path === '/manage' || path.startsWith('/manage')) { setRoute('/manage'); + } else if (path === '/filters' || path.startsWith('/filters')) { + setRoute('/filters'); } else { setRoute('/'); } @@ -69,34 +72,54 @@ function navigateTo(path) { } function setRoute(path) { - state.currentRoute = path === '/manage' ? '/manage' : '/'; + if (path === '/manage' || path.startsWith('/manage')) { + state.currentRoute = '/manage'; + } else if (path === '/filters' || path.startsWith('/filters')) { + state.currentRoute = '/filters'; + } else { + state.currentRoute = '/'; + } const stockBtn = document.getElementById('nav-stock-btn'); + const filtersBtn = document.getElementById('nav-filters-btn'); const manageBtn = document.getElementById('nav-manage-btn'); + const stockPage = document.getElementById('stock-view-page'); + const filtersPage = document.getElementById('filters-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]'; + const activeClass = 'flex items-center justify-center gap-1.5 sm:gap-2 px-3 sm:px-5 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 = 'relative flex items-center justify-center gap-1.5 sm:gap-2 px-3 sm:px-5 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]'; + + // Hide all pages first + stockPage.classList.add('hidden'); + filtersPage.classList.add('hidden'); + managePage.classList.add('hidden'); + + stockBtn.className = inactiveClass; + filtersBtn.className = inactiveClass; + manageBtn.className = inactiveClass; if (state.currentRoute === '/') { stockBtn.className = activeClass; - manageBtn.className = inactiveClass; stockPage.classList.remove('hidden'); - managePage.classList.add('hidden'); subTitle.textContent = 'Current Stock'; renderStockView(); + } else if (state.currentRoute === '/filters') { + filtersBtn.className = activeClass; + filtersPage.classList.remove('hidden'); + subTitle.textContent = 'Filter Stock'; + renderFilterTabView(); } else { manageBtn.className = activeClass; - stockBtn.className = inactiveClass; managePage.classList.remove('hidden'); - stockPage.classList.add('hidden'); subTitle.textContent = 'Manager'; resetTabletManageStation(); renderTabletProductPicker(''); } + updateFilterUIState(); initLucide(); } @@ -109,7 +132,7 @@ async function loadMetadata() { const res = await fetch('/api/meta'); if (res.ok) { state.meta = await res.json(); - renderCategoryFilters(); + renderCategoryFilterCards(); populateModalSelects(); } } catch (err) { @@ -134,6 +157,12 @@ async function loadStats() { async function loadItems() { try { + // Fetch all items to accurately compute category & status counts + const resAll = await fetch('/api/items'); + if (resAll.ok) { + state.allItems = await resAll.json(); + } + const params = new URLSearchParams(); if (state.filters.search) params.append('search', state.filters.search); if (state.filters.category && state.filters.category !== 'All') params.append('category', state.filters.category); @@ -144,11 +173,14 @@ async function loadItems() { state.items = await res.json(); if (state.currentRoute === '/') { renderStockView(); + } else if (state.currentRoute === '/filters') { + renderFilterTabView(); } else { const query = document.getElementById('tablet-search-input')?.value || ''; renderTabletProductPicker(query); } } + updateFilterUIState(); } catch (err) { console.error('Failed to load inventory:', err); showToast('Failed to load inventory', 'error'); @@ -182,52 +214,176 @@ function clearSearch() { loadItems(); } +// ---------------------------------------------------- +// ROUTE 2: /filters (DEDICATED FILTERS TAB) +// ---------------------------------------------------- + function setStatusFilter(status) { state.filters.status = status; - document.querySelectorAll('[data-status-btn]').forEach(btn => { - if (btn.getAttribute('data-status-btn') === status) { - btn.className = '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'; - } else { - btn.className = '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'; - } - }); + updateFilterUIState(); loadItems(); } function setCategoryFilter(category) { state.filters.category = category; - renderCategoryFilters(); + updateFilterUIState(); loadItems(); } -function renderCategoryFilters() { - const container = document.getElementById('category-filter-bar'); - if (!container) return; +function resetAllFilters() { + state.filters.category = 'All'; + state.filters.status = 'all'; + state.filters.search = ''; + const searchInput = document.getElementById('search-input'); + if (searchInput) searchInput.value = ''; + const clearBtn = document.getElementById('clear-search'); + if (clearBtn) clearBtn.classList.add('hidden'); + + updateFilterUIState(); + loadItems(); + showToast('Filters reset to All', 'info'); +} + +function renderCategoryFilterCards() { + const container = document.getElementById('filter-categories-container'); + if (!container || !state.meta.categories) return; let html = ` - `; state.meta.categories.forEach(cat => { - const isSelected = state.filters.category === cat.id; html += ` - `; }); container.innerHTML = html; + initLucide(); +} + +function renderFilterTabView() { + updateFilterUIState(); +} + +function updateFilterUIState() { + // 1. Update Pack State cards highlight & counts + const allItems = state.allItems || []; + const fullCount = allItems.filter(i => !i.is_opened && i.quantity >= i.initial_quantity).length; + const openedCount = allItems.filter(i => i.is_opened || i.quantity < i.initial_quantity).length; + const lowCount = allItems.filter(i => i.quantity <= (i.initial_quantity * 0.25)).length; + + const countAllEl = document.getElementById('filter-count-all'); + const countFullEl = document.getElementById('filter-count-full'); + const countOpenedEl = document.getElementById('filter-count-opened'); + const countLowEl = document.getElementById('filter-count-low'); + const countTotalEl = document.getElementById('filter-total-count'); + + if (countAllEl) countAllEl.textContent = allItems.length; + if (countFullEl) countFullEl.textContent = fullCount; + if (countOpenedEl) countOpenedEl.textContent = openedCount; + if (countLowEl) countLowEl.textContent = lowCount; + if (countTotalEl) countTotalEl.textContent = state.items.length; + + // Highlight status cards + document.querySelectorAll('[data-filter-status]').forEach(card => { + const status = card.getAttribute('data-filter-status'); + const isSelected = state.filters.status === status; + if (isSelected) { + card.className = 'filter-status-card flex-1 min-w-[140px] sm:min-w-[160px] p-4 rounded-2xl border-2 text-left flex flex-col justify-between space-y-3 transition-all active:scale-95 touch-manipulation bg-teal-500/20 border-teal-500 text-white shadow-lg shadow-teal-500/20 ring-2 ring-teal-500/30'; + } else { + card.className = 'filter-status-card flex-1 min-w-[140px] sm:min-w-[160px] p-4 rounded-2xl border text-left flex flex-col justify-between space-y-3 transition-all active:scale-95 touch-manipulation bg-slate-900 border-slate-800 text-slate-300 hover:border-slate-700'; + } + }); + + // 2. Update Category cards counts & highlight + const catAllCountEl = document.getElementById('cat-count-all'); + if (catAllCountEl) catAllCountEl.textContent = allItems.length; + + if (state.meta.categories) { + state.meta.categories.forEach(cat => { + const sanitized = cat.id.replace(/[^a-zA-Z0-9]/g, ''); + const countEl = document.getElementById(`cat-count-${sanitized}`); + if (countEl) { + const c = allItems.filter(i => i.category === cat.id).length; + countEl.textContent = c; + } + }); + } + + document.querySelectorAll('[data-filter-cat]').forEach(card => { + const cat = card.getAttribute('data-filter-cat'); + const isSelected = state.filters.category === cat; + if (isSelected) { + card.className = 'filter-cat-card p-3 sm:p-4 rounded-2xl border-2 text-left flex items-center gap-3 transition-all active:scale-95 touch-manipulation min-h-[64px] bg-teal-500/20 border-teal-500 text-white shadow-lg shadow-teal-500/20 ring-2 ring-teal-500/30'; + } else { + card.className = 'filter-cat-card p-3 sm:p-4 rounded-2xl border text-left flex items-center gap-3 transition-all active:scale-95 touch-manipulation min-h-[64px] bg-slate-900 border-slate-800 text-slate-300 hover:border-slate-700'; + } + }); + + // 3. Top Navigation Badge + const navBadge = document.getElementById('nav-filter-badge'); + let activeFilterCount = 0; + if (state.filters.category !== 'All') activeFilterCount++; + if (state.filters.status !== 'all') activeFilterCount++; + + if (navBadge) { + if (activeFilterCount > 0) { + navBadge.textContent = activeFilterCount; + navBadge.classList.remove('hidden'); + } else { + navBadge.classList.add('hidden'); + } + } + + // 4. Stock page active filter banner + const banner = document.getElementById('active-filter-indicator'); + const bannerText = document.getElementById('active-filter-text'); + if (banner && bannerText) { + if (activeFilterCount > 0) { + const parts = []; + if (state.filters.category !== 'All') parts.push(`Category: ${state.filters.category}`); + if (state.filters.status !== 'all') { + const labels = { full: 'Full Packs', opened: 'Opened / In-Use', low: 'Low Stock' }; + parts.push(`Status: ${labels[state.filters.status] || state.filters.status}`); + } + bannerText.textContent = `Filtered by: ${parts.join(' • ')} (${state.items.length} items)`; + banner.classList.remove('hidden'); + } else { + banner.classList.add('hidden'); + } + } } function renderStockView() { diff --git a/public/index.html b/public/index.html index 198d210..bc96319 100644 --- a/public/index.html +++ b/public/index.html @@ -56,17 +56,28 @@ - + + + + - -
- - - - -
+ -
- + +
-
+
@@ -148,12 +163,147 @@ 🧊

No items in stock

-

Use Manager to scan or add items.

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