// State Management const state = { inventory: [], stats: { totalItems: 0, expiringSoon: 0, wasteAvoided: 0, moneySaved: 0 }, useMock: true }; // Mock Data (Fallback) const MOCK_INVENTORY = [ { id: 1, name: 'Milk (Mock)', quantity: '1L', expiryDate: '2023-10-30', category: 'Dairy', price: 2.50 }, { id: 2, name: 'Spinach (Mock)', quantity: '1 bag', expiryDate: '2023-10-26', category: 'Vegetables', price: 3.00 }, ]; const MOCK_RECIPES = [ { title: 'Mock Chicken Stir-fry', image: 'https://via.placeholder.com/300', ingredients: 'Chicken, Veggies' } ]; // Initialization // Initialization document.addEventListener('DOMContentLoaded', () => { // Check if backend is reachable or just load UI state.useMock = false; // We assume Python backend is primary now fetchInventory(); setupFormListeners(); // Hide N8N settings if present (optional UX polish) const settingsSection = document.getElementById('settings-section'); if (settingsSection) { // We can inject a message or hide legacy N8N inputs here if we wanted // For now, we just leave it as is } }); // Navigation function showSection(sectionId) { const sections = ['dashboard', 'inventory', 'recipes', 'settings']; sections.forEach(id => document.getElementById(`${id}-section`).classList.add('hidden')); document.getElementById(`${sectionId}-section`).classList.remove('hidden'); document.querySelectorAll('nav button').forEach(btn => btn.classList.remove('active')); if (sectionId === 'dashboard' || sectionId === 'inventory') { if (!state.useMock) fetchInventory(); else updateUI(); } } // Data Handling - Python API async function apiRequest(endpoint, method = 'GET', data = null) { // If endpoint doesn't start with /, add it const path = endpoint.startsWith('/') ? endpoint : `/${endpoint}`; const url = `/api${path}`; const options = { method: method, headers: { 'Content-Type': 'application/json' } }; if (data) { options.body = JSON.stringify(data); } const response = await fetch(url, options); if (!response.ok) { throw new Error(`Server Error: ${response.status}`); } return await response.json(); } async function fetchInventory() { if (state.useMock) { updateUI(); return; } // Keep mock fallback for initial load if no URL was saved try { const data = await apiRequest('/get-items'); state.inventory = data.items || []; updateUI(); } catch (error) { console.error('API Error:', error); showToast('Backend not running? ' + error.message, 'error'); // Fallback to mock data if API fails if (state.inventory.length === 0) { state.inventory = MOCK_INVENTORY; state.useMock = true; // Switch to mock mode updateUI(); } } } async function addItem(itemData) { if (state.useMock) { state.inventory.push({ ...itemData, id: Date.now(), expiryDate: itemData.expiryDate || '2023-12-01' }); // Add ID for mock updateUI(); showToast('Item added (Mock)!', 'success'); return; } try { const result = await apiRequest('/add-item', 'POST', itemData); if (result.success) { showToast(`Item added! Expiry: ${result.expiryDate}`, 'success'); fetchInventory(); } else { showToast('Failed to add item: ' + (result.message || 'Unknown error'), 'error'); } } catch (error) { console.error('Add Item Error:', error); showToast('Failed to add item.', 'error'); } } async function getRecipeSuggestions() { const container = document.getElementById('recipes-container'); container.innerHTML = '
'; if (state.useMock) { await new Promise(r => setTimeout(r, 1000)); renderRecipes(MOCK_RECIPES); return; } try { const recipes = await apiRequest('/suggest-recipes', 'POST', {}); renderRecipes(recipes); } catch (e) { console.error(e); container.innerHTML = '

Error fetching recipes. Showing mock recipes.

'; setTimeout(() => renderRecipes(MOCK_RECIPES), 1000); // Fallback to mock recipes } } // Settings & Testing function saveSettings() { const url = document.getElementById('config-webhook-url').value; updateConfig(url); showToast('Settings Saved! Reloading...', 'success'); setTimeout(() => window.location.reload(), 1000); } async function testConnection() { const log = document.getElementById('connection-log'); log.style.display = 'block'; log.innerHTML = 'Testing connection...'; // Explicitly testing 'get_items' action which matches the Switch node logic const payload = { action: 'get_items' }; const url = document.getElementById('config-webhook-url').value; try { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (response.ok) { const data = await response.json(); log.innerHTML = `SUCCESS: Connected!
Response: ${JSON.stringify(data).slice(0, 50)}...`; } else { log.innerHTML = `ERROR: Server returned ${response.status}`; } } catch (e) { log.innerHTML = `FAIL: ${e.name} - ${e.message}
Possible causes: CORS, Network, or Wrong URL.`; } } // UI Helpers function showToast(msg, type = 'info') { // Simple alert replacement const div = document.createElement('div'); div.innerText = msg; div.style.position = 'fixed'; div.style.bottom = '20px'; div.style.right = '20px'; div.style.background = type === 'error' ? '#ef4444' : '#10b981'; div.style.color = 'white'; div.style.padding = '12px 24px'; div.style.borderRadius = '8px'; div.style.zIndex = '1000'; div.style.animation = 'fadeInUp 0.3s ease'; document.body.appendChild(div); setTimeout(() => div.remove(), 3000); } function renderRecipes(recipes) { const container = document.getElementById('recipes-container'); container.innerHTML = ''; if (!recipes || recipes.length === 0) { container.innerHTML = '

No recipes found.

'; return; } recipes.forEach(recipe => { const div = document.createElement('div'); div.className = 'recipe-card'; div.innerHTML = ` ${recipe.title}
${recipe.title}
Ingredients: ${recipe.ingredients || 'Various'}
`; container.appendChild(div); }); } function calculateStats() { const today = new Date(); const upcoming = new Date(); upcoming.setDate(today.getDate() + 3); let expiringSoonCount = 0; state.inventory.forEach(item => { const expiry = new Date(item.expiryDate); if (expiry <= upcoming && expiry >= today) { expiringSoonCount++; } }); state.stats.totalItems = state.inventory.length; state.stats.expiringSoon = expiringSoonCount; state.stats.wasteAvoided = (state.inventory.length * 0.1).toFixed(1) + 'kg'; state.stats.moneySaved = '$' + (state.inventory.length * 2.5).toFixed(2); } function updateUI() { calculateStats(); document.getElementById('total-items').innerText = state.stats.totalItems; document.getElementById('expiring-soon').innerText = state.stats.expiringSoon; document.getElementById('waste-avoided').innerText = state.stats.wasteAvoided; document.getElementById('money-saved').innerText = state.stats.moneySaved; const list = document.getElementById('inventory-list-ul'); list.innerHTML = ''; state.inventory.forEach(item => { const li = document.createElement('li'); li.className = `inventory-item ${getExpiryClass(item.expiryDate)}`; li.innerHTML = `

${item.name}

${item.quantity || '-'}${item.category || 'Other'}
${formatDate(item.expiryDate)}
`; list.appendChild(li); }); generateDashboardAlerts(); } function getExpiryClass(dateStr) { if (!dateStr) return 'safe'; const today = new Date(); const expiry = new Date(dateStr); const diffDays = Math.ceil((expiry - today) / (1000 * 60 * 60 * 24)); if (diffDays < 0) return 'expired'; if (diffDays <= 3) return 'soon'; return 'safe'; } function formatDate(dateStr) { if (!dateStr) return 'Unknown'; return new Date(dateStr).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); } function generateDashboardAlerts() { const container = document.getElementById('alerts-container'); container.innerHTML = ''; const expiringItems = state.inventory.filter(i => getExpiryClass(i.expiryDate) === 'soon'); if (expiringItems.length === 0) { container.innerHTML = '

No urgent alerts. Good job!

'; return; } expiringItems.forEach(item => { const div = document.createElement('div'); div.style.marginBottom = '10px'; div.style.padding = '10px'; div.style.background = 'rgba(245, 158, 11, 0.1)'; div.style.borderRadius = '8px'; div.style.borderLeft = '3px solid var(--accent)'; div.innerHTML = `${item.name} is expiring soon!`; container.appendChild(div); }); } // Event Listeners function setupFormListeners() { document.getElementById('quick-add-form').addEventListener('submit', (e) => { e.preventDefault(); const name = document.getElementById('quick-name').value; const date = document.getElementById('quick-date').value; addItem({ name, quantity: '1 unit', expiryDate: date, category: 'Uncategorized' }); document.getElementById('quick-add-form').reset(); }); document.getElementById('add-item-form').addEventListener('submit', (e) => { e.preventDefault(); const formData = new FormData(e.target); addItem(Object.fromEntries(formData.entries())); e.target.reset(); showSection('inventory'); }); }