tayyab / app.js
mabdullahsibghatullah123's picture
Upload 17 files
5ae7c8f verified
// 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 = '<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); // 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 = `<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.`;
}
}
// 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 = '<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);
});
}
// 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');
});
}