diff --git a/api.php b/api.php index 4d4436e..8319ddb 100644 --- a/api.php +++ b/api.php @@ -229,24 +229,26 @@ switch ($action) { $name = sanitize($input['name'] ?? ''); $container = sanitize($input['container'] ?? 'glass-bail-medium', 50); + $category = sanitize($input['category'] ?? 'other', 50); $outOfStock = (bool)($input['outOfStock'] ?? false); if (empty($name)) { echo json_encode(['success' => false, 'error' => 'Name required']); break; } - + $inventory = loadInventory(); $newItem = [ 'id' => getNextId($inventory), 'name' => $name, 'container' => $container, + 'category' => $category, 'outOfStock' => $outOfStock ]; - + $inventory[] = $newItem; saveInventory($inventory); - + echo json_encode(['success' => true, 'item' => $newItem]); break; @@ -259,31 +261,33 @@ switch ($action) { $id = (int)($input['id'] ?? 0); $name = sanitize($input['name'] ?? ''); $container = sanitize($input['container'] ?? '', 50); + $category = sanitize($input['category'] ?? '', 50); $outOfStock = (bool)($input['outOfStock'] ?? false); if ($id <= 0) { echo json_encode(['success' => false, 'error' => 'Invalid ID']); break; } - + $inventory = loadInventory(); $found = false; - + foreach ($inventory as &$item) { if ($item['id'] === $id) { if (!empty($name)) $item['name'] = $name; if (!empty($container)) $item['container'] = $container; + if (!empty($category)) $item['category'] = $category; $item['outOfStock'] = $outOfStock; $found = true; break; } } - + if (!$found) { echo json_encode(['success' => false, 'error' => 'Item not found']); break; } - + saveInventory($inventory); echo json_encode(['success' => true]); break; diff --git a/categories.json b/categories.json new file mode 100644 index 0000000..50a32be --- /dev/null +++ b/categories.json @@ -0,0 +1,26 @@ +{ + "flours": { + "name": "Flours & Starches", + "color": "#8B4513" + }, + "spices": { + "name": "Spices & Seasonings", + "color": "#CD853F" + }, + "pasta": { + "name": "Pasta & Grains", + "color": "#DAA520" + }, + "baking": { + "name": "Baking", + "color": "#D2691E" + }, + "produce": { + "name": "Beans & Dried Produce", + "color": "#556B2F" + }, + "other": { + "name": "Other", + "color": "#708090" + } +} diff --git a/deploy b/deploy index 5b3df9e..c39eabe 100755 --- a/deploy +++ b/deploy @@ -6,7 +6,7 @@ DIR=/var/www/my_webapp__2/www # Deploy code files rsync -avz --no-t --no-p --delete \ --exclude 'data/' \ - index.html api.php containers.json og-image.png ${HOST}:${DIR} + index.html api.php containers.json categories.json og-image.png ${HOST}:${DIR} # Deploy data directory protection scp data/index.php ${HOST}:${DIR}/data/index.php 2>/dev/null || true diff --git a/index.html b/index.html index f7ff57d..bdb0e3d 100644 --- a/index.html +++ b/index.html @@ -207,6 +207,14 @@ border-radius: 50%; } + .category-tag { + font-size: 0.7rem; + padding: 2px 8px; + border-radius: 10px; + margin-left: 8px; + font-weight: 500; + } + .status-badge { padding: 4px 10px; font-size: 0.75rem; @@ -688,13 +696,13 @@ - +
+ + +
@@ -821,11 +835,15 @@ let containerTypes = {}; let containerCategories = []; + // Item categories - loaded from categories.json + let itemCategories = {}; + let inventory = []; let isUnlocked = false; let currentPin = null; let currentFilter = 'all'; let containerFilter = null; + let categoryFilter = null; let hasPinSet = false; // API calls @@ -909,10 +927,73 @@ }); } + // Load item categories from JSON file + async function loadCategories() { + try { + const response = await fetch('categories.json'); + itemCategories = await response.json(); + buildCategoryDropdown(); + buildCategoryTabs(); + } catch (error) { + console.error('Error loading categories:', error); + } + } + + // Build the category dropdown + function buildCategoryDropdown() { + const select = document.getElementById('itemCategory'); + select.replaceChildren(); + Object.entries(itemCategories).forEach(([key, cat]) => { + const option = document.createElement('option'); + option.value = key; + option.textContent = cat.name; + select.appendChild(option); + }); + } + + // Build category filter tabs + function buildCategoryTabs() { + const navTabs = document.getElementById('navTabs'); + // Remove old category tabs if they exist + navTabs.querySelectorAll('.nav-tab[data-category]').forEach(tab => tab.remove()); + + // Add category tabs after "Out of Stock" tab, sorted alphabetically + const outTab = navTabs.querySelector('[data-filter="out"]'); + const sortedCategories = Object.entries(itemCategories) + .sort((a, b) => a[1].name.localeCompare(b[1].name)) + .reverse(); // Reverse so insertAdjacentElement builds correct order + sortedCategories.forEach(([key, cat]) => { + const tab = document.createElement('button'); + tab.className = 'nav-tab'; + tab.dataset.category = key; + tab.textContent = cat.name; + tab.style.borderLeft = `3px solid ${cat.color}`; + tab.setAttribute('role', 'tab'); + tab.setAttribute('aria-selected', 'false'); + tab.addEventListener('click', () => { + // Clear other active states and aria-selected + navTabs.querySelectorAll('.nav-tab').forEach(t => { + t.classList.remove('active'); + t.setAttribute('aria-selected', 'false'); + }); + tab.classList.add('active'); + tab.setAttribute('aria-selected', 'true'); + currentFilter = 'category'; + categoryFilter = key; + document.getElementById('inventoryList').style.display = 'block'; + document.getElementById('containerLegend').style.display = 'grid'; + document.getElementById('qrSection').style.display = 'none'; + renderInventory(); + }); + outTab.insertAdjacentElement('afterend', tab); + }); + } + // Initialize async function init() { try { await loadContainers(); + await loadCategories(); const result = await apiCall('get'); inventory = result.inventory || []; @@ -987,14 +1068,14 @@ function renderInventory() { const list = document.getElementById('inventoryList'); const searchTerm = document.getElementById('searchInput').value.toLowerCase(); - + let filtered = inventory.filter(item => { const matchesSearch = item.name.toLowerCase().includes(searchTerm); const matchesFilter = currentFilter === 'all' || currentFilter === 'recent' || (currentFilter === 'in-stock' && !item.outOfStock) || (currentFilter === 'out' && item.outOfStock) || - (currentFilter === 'spices' && item.container.startsWith('spice-')); + (currentFilter === 'category' && item.category === categoryFilter); const matchesContainer = !containerFilter || item.container === containerFilter; return matchesSearch && matchesFilter && matchesContainer; }); @@ -1024,20 +1105,23 @@ return; } - list.innerHTML = filtered.map(item => ` -
+ list.innerHTML = filtered.map(item => { + const cat = itemCategories[item.category]; + return ` +
${escapeHtml(item.name)}
${containerTypes[item.container] ? `${containerTypes[item.container].category}: ${containerTypes[item.container].name}` : 'Unknown'} + ${cat ? `${cat.name}` : ''}
${item.outOfStock ? 'Out' : 'In Stock'}
- `).join(''); + `}).join(''); } function escapeHtml(text) { @@ -1113,14 +1197,19 @@ function setupEventListeners() { document.getElementById('searchInput').addEventListener('input', renderInventory); - document.querySelectorAll('.nav-tab').forEach(tab => { + document.querySelectorAll('.nav-tab[data-filter]').forEach(tab => { tab.addEventListener('click', () => { - document.querySelectorAll('.nav-tab').forEach(t => t.classList.remove('active')); + document.querySelectorAll('.nav-tab').forEach(t => { + t.classList.remove('active'); + t.setAttribute('aria-selected', 'false'); + }); tab.classList.add('active'); - + tab.setAttribute('aria-selected', 'true'); + const filter = tab.dataset.filter; currentFilter = filter; - + categoryFilter = null; // Clear category filter when switching to non-category tabs + if (filter === 'qr') { document.getElementById('inventoryList').style.display = 'none'; document.getElementById('containerLegend').style.display = 'none'; @@ -1342,10 +1431,11 @@ toggleLock(); return; } - + document.getElementById('itemModalTitle').textContent = 'Add Item'; document.getElementById('itemName').value = ''; document.getElementById('itemContainer').selectedIndex = 0; + document.getElementById('itemCategory').value = 'other'; document.getElementById('itemOutOfStock').checked = false; document.getElementById('editingItemId').value = ''; document.getElementById('deleteBtn').style.display = 'none'; @@ -1357,13 +1447,14 @@ if (!isUnlocked) { return; // View only mode } - + const item = inventory.find(i => i.id === id); if (!item) return; - + document.getElementById('itemModalTitle').textContent = 'Edit Item'; document.getElementById('itemName').value = item.name; document.getElementById('itemContainer').value = item.container; + document.getElementById('itemCategory').value = item.category || 'other'; document.getElementById('itemOutOfStock').checked = item.outOfStock; document.getElementById('editingItemId').value = id; document.getElementById('deleteBtn').style.display = 'block'; @@ -1373,9 +1464,10 @@ async function saveItem() { const name = document.getElementById('itemName').value.trim(); const container = document.getElementById('itemContainer').value; + const category = document.getElementById('itemCategory').value; const outOfStock = document.getElementById('itemOutOfStock').checked; const editingId = document.getElementById('editingItemId').value; - + if (!name) { showToast('Please enter an item name'); return; @@ -1401,6 +1493,7 @@ id: parseInt(editingId), name, container, + category, outOfStock, pin: currentPin }); @@ -1409,13 +1502,14 @@ if (item) { item.name = name; item.container = container; + item.category = category; item.outOfStock = outOfStock; } showToast('Item updated'); } else { // Add new - const result = await apiCall('add', { name, container, outOfStock, pin: currentPin }); - + const result = await apiCall('add', { name, container, category, outOfStock, pin: currentPin }); + if (result.success && result.item) { inventory.push(result.item); }