feat(ui): add dedicated Filters tab with scrollable categories and pack state cards
Build and Push Fridger Container / Build & Push Fridger Image (push) Successful in 1m23s

This commit is contained in:
2026-10-11 00:00:59 +02:00
parent 81b80d3bc5
commit 3d89aab10e
2 changed files with 360 additions and 54 deletions
+189 -33
View File
@@ -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 = `
<button onclick="setCategoryFilter('All')" class="px-3.5 py-2 rounded-xl border whitespace-nowrap transition text-xs sm:text-sm min-h-[38px] touch-manipulation ${
state.filters.category === 'All'
? 'bg-teal-500/20 text-teal-300 border-teal-500/50 font-bold'
: 'bg-slate-900 text-slate-400 border-slate-800 hover:text-slate-200'
}">
All Categories
<!-- All Categories -->
<button
onclick="setCategoryFilter('All')"
data-filter-cat="All"
class="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"
>
<div class="w-10 h-10 rounded-xl bg-teal-500/20 text-teal-300 flex items-center justify-center shrink-0">
<i data-lucide="layout-grid" class="w-5 h-5"></i>
</div>
<div class="min-w-0 flex-1">
<div class="flex items-center justify-between gap-1">
<h4 class="text-xs sm:text-sm font-extrabold text-white truncate">All Categories</h4>
<span id="cat-count-all" class="text-[11px] font-bold px-1.5 py-0.5 rounded-md bg-slate-800 text-slate-400">0</span>
</div>
<p class="text-[10px] text-slate-400 truncate">View everything</p>
</div>
</button>
`;
state.meta.categories.forEach(cat => {
const isSelected = state.filters.category === cat.id;
html += `
<button onclick="setCategoryFilter('${escapeHtml(cat.id)}')" class="px-3.5 py-2 rounded-xl border whitespace-nowrap transition text-xs sm:text-sm min-h-[38px] touch-manipulation ${
isSelected
? 'bg-teal-500/20 text-teal-300 border-teal-500/50 font-bold'
: 'bg-slate-900 text-slate-400 border-slate-800 hover:text-slate-200'
}">
${escapeHtml(cat.name)}
<button
onclick="setCategoryFilter('${escapeHtml(cat.id)}')"
data-filter-cat="${escapeHtml(cat.id)}"
class="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"
>
<div class="w-10 h-10 rounded-xl bg-slate-800 text-teal-400 flex items-center justify-center shrink-0">
<i data-lucide="${escapeHtml(cat.icon || 'package')}" class="w-5 h-5"></i>
</div>
<div class="min-w-0 flex-1">
<div class="flex items-center justify-between gap-1">
<h4 class="text-xs sm:text-sm font-extrabold text-white truncate">${escapeHtml(cat.name)}</h4>
<span id="cat-count-${escapeHtml(cat.id.replace(/[^a-zA-Z0-9]/g, ''))}" class="text-[11px] font-bold px-1.5 py-0.5 rounded-md bg-slate-800 text-slate-400">0</span>
</div>
<p class="text-[10px] text-slate-400 truncate">${escapeHtml(cat.id)}</p>
</div>
</button>
`;
});
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() {