|
|
| const state = {
|
| inventory: [],
|
| stats: {
|
| totalItems: 0,
|
| expiringSoon: 0,
|
| wasteAvoided: 0,
|
| moneySaved: 0
|
| },
|
| useMock: true
|
| };
|
|
|
|
|
| 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' }
|
| ];
|
|
|
|
|
|
|
| document.addEventListener('DOMContentLoaded', () => {
|
|
|
| state.useMock = false;
|
| fetchInventory();
|
| setupFormListeners();
|
|
|
|
|
| const settingsSection = document.getElementById('settings-section');
|
| if (settingsSection) {
|
|
|
|
|
| }
|
| });
|
|
|
|
|
| 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();
|
| }
|
| }
|
|
|
|
|
| async function apiRequest(endpoint, method = 'GET', data = null) {
|
|
|
| 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; }
|
|
|
| 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');
|
|
|
| if (state.inventory.length === 0) {
|
| state.inventory = MOCK_INVENTORY;
|
| state.useMock = true;
|
| updateUI();
|
| }
|
| }
|
| }
|
|
|
| async function addItem(itemData) {
|
| if (state.useMock) {
|
| state.inventory.push({ ...itemData, id: Date.now(), expiryDate: itemData.expiryDate || '2023-12-01' });
|
| 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 = '<div class="loader"></div>';
|
|
|
| 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 = '<p class="text-muted">Error fetching recipes. Showing mock recipes.</p>';
|
| setTimeout(() => renderRecipes(MOCK_RECIPES), 1000);
|
| }
|
| }
|
|
|
|
|
| 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...';
|
|
|
|
|
| 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 = `<span style="color: #10b981;">SUCCESS: Connected!</span><br>Response: ${JSON.stringify(data).slice(0, 50)}...`;
|
| } else {
|
| log.innerHTML = `<span style="color: #ef4444;">ERROR: Server returned ${response.status}</span>`;
|
| }
|
| } catch (e) {
|
| log.innerHTML = `<span style="color: #ef4444;">FAIL: ${e.name} - ${e.message}</span><br>Possible causes: CORS, Network, or Wrong URL.`;
|
| }
|
| }
|
|
|
|
|
| function showToast(msg, type = 'info') {
|
|
|
| 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 = '<p class="text-muted">No recipes found.</p>';
|
| return;
|
| }
|
| recipes.forEach(recipe => {
|
| const div = document.createElement('div');
|
| div.className = 'recipe-card';
|
| div.innerHTML = `
|
| <img src="${recipe.image || 'https://via.placeholder.com/300'}" alt="${recipe.title}">
|
| <div class="recipe-content">
|
| <div class="recipe-title">${recipe.title}</div>
|
| <div class="recipe-ingredients">Ingredients: ${recipe.ingredients || 'Various'}</div>
|
| </div>
|
| `;
|
| 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 = `
|
| <div class="item-info">
|
| <h4>${item.name}</h4>
|
| <div class="item-meta"><span>${item.quantity || '-'}</span> • <span>${item.category || 'Other'}</span></div>
|
| </div>
|
| <div class="item-status">
|
| <span class="expiry-badge ${getExpiryClass(item.expiryDate)}">${formatDate(item.expiryDate)}</span>
|
| </div>
|
| `;
|
| 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 = '<p class="text-muted">No urgent alerts. Good job!</p>';
|
| 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 = `<strong>${item.name}</strong> is expiring soon!`;
|
| container.appendChild(div);
|
| });
|
| }
|
|
|
|
|
| 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');
|
| });
|
| }
|
|
|