/* ============================================================ LEAFGUARD — APP.JS ============================================================ */ /* ---- PRODUCTS DATA ---- */ const PRODUCTS = [ {id:'AF0001',name:'Citrus Bacteria Clear', primary:'Biological Pesticides', secondary:'Bacillus Fungicides', crops:'Citrus, vegetables, fruit trees', price:165}, {id:'AF0002',name:'Onion-Ginger-Garlic Bacteria Clear', primary:'Biological Pesticides', secondary:'Bacillus Fungicides', crops:'Scallion, ginger, garlic, vegetables', price:155}, {id:'AF0003',name:'Rice Bacteria Clear', primary:'Biological Pesticides', secondary:'Bacillus Fungicides', crops:'Rice, vegetables, fruit trees', price:155}, {id:'AF0004',name:'Cabbage & Kale Anti-rot Agent', primary:'Fertilizers', secondary:'Foliar Fertilizers/Nutrient Solutions', crops:'Cabbage, kale, tomato, pepper', price:175}, {id:'AF0005',name:'Strawberry Bacteria Clear', primary:'Biological Pesticides', secondary:'Bacillus Fungicides', crops:'Strawberry, vegetables, fruit trees', price:160}, {id:'AF0006',name:'Bacteria Clear Universal', primary:'Biological Pesticides', secondary:'Bacillus Fungicides', crops:'Vegetables, fruit trees, melons', price:170}, {id:'AF0007',name:'Chili Pepper Bacteria Clear', primary:'Biological Pesticides', secondary:'Bacillus Fungicides', crops:'Chili pepper, vegetables', price:155}, {id:'AF0008',name:'Cucumber Bacteria Clear', primary:'Biological Pesticides', secondary:'Bacillus Fungicides', crops:'Cucumber, vegetables, melons', price:155}, {id:'AF0012',name:'Root Nematode Clear', primary:'Biological Pesticides', secondary:'Microbial Agents', crops:'General crops — root protection', price:195}, {id:'AF0013',name:'Citrus Water-Soluble Fertilizer', primary:'Fertilizers', secondary:'Secondary Nutrient Water-Soluble Fertilizers', crops:'Citrus, fruit trees, tomato', price:145}, {id:'AF0016',name:'Vigorous Root Growth', primary:'Fertilizers', secondary:'Macronutrient Water-Soluble Fertilizers', crops:'Fruit trees, tomato, pepper, garlic', price:150}, {id:'AF0017',name:'One Spray Green', primary:'Fertilizers', secondary:'Foliar Fertilizers/Nutrient Solutions', crops:'Wheat, corn, rice, apple, tomato', price:130}, {id:'AF0018',name:'Melon Booster', primary:'Fertilizers', secondary:'Secondary Nutrient Water-Soluble Fertilizers', crops:'Cucumber, pumpkin, watermelon, tomato', price:140}, {id:'AF0019',name:'Underground Root & Tuber Enlarger', primary:'Fertilizers', secondary:'Secondary Nutrient Water-Soluble Fertilizers', crops:'Potato, sweet potato, garlic, radish', price:155}, {id:'AF0020',name:'Sweet Potato Baby', primary:'Fertilizers', secondary:'Foliar Fertilizers/Nutrient Solutions', crops:'Sweet potato, strawberry, banana, mango', price:145}, {id:'CP001', name:'1.8% Abamectin', primary:'Chemical Pesticides', secondary:'Insecticides', crops:'Vegetables, fruit trees — mites, miners', price:95}, {id:'CP002', name:'10% Imidacloprid', primary:'Chemical Pesticides', secondary:'Insecticides', crops:'Rice, wheat, vegetables — aphids, whitefly', price:85}, {id:'CP003', name:'10% Thiamethoxam', primary:'Chemical Pesticides', secondary:'Insecticides', crops:'Vegetables, fruit trees — whitefly', price:90}, {id:'CP004', name:'3.2% Emamectin Benzoate', primary:'Chemical Pesticides', secondary:'Insecticides', crops:'Vegetables — caterpillars, moths', price:100}, {id:'CP005', name:'1.8% Cymoxanil Acetate', primary:'Chemical Pesticides', secondary:'Fungicides', crops:'Vegetables, potatoes — downy mildew', price:110}, {id:'CP006', name:'3% Metalaxyl-M', primary:'Chemical Pesticides', secondary:'Fungicides', crops:'Vegetables, tomatoes — root rot', price:120}, {id:'CP007', name:'6% Kasugamycin', primary:'Chemical Pesticides', secondary:'Fungicides', crops:'Rice, vegetables — bacterial blight', price:115}, {id:'CP008', name:'5% Cyazofamid', primary:'Chemical Pesticides', secondary:'Fungicides', crops:'Cucumber, tomato, pepper — downy mildew', price:125}, {id:'CP009', name:'10% Glufosinate-Ammonium', primary:'Chemical Pesticides', secondary:'Herbicides', crops:'General weeds — non-selective', price:75}, {id:'CP010', name:'33% Glyphosate Ammonium', primary:'Chemical Pesticides', secondary:'Herbicides', crops:'Broad spectrum weed control', price:70}, {id:'BP001', name:'Pyraclostrobin-Kairun', primary:'Chemical Pesticides', secondary:'Fungicides', crops:'Fruits, vegetables — powdery mildew', price:135}, {id:'BP002', name:'Benomyl Fungicide', primary:'Chemical Pesticides', secondary:'Fungicides', crops:'Broad spectrum — mildew, gray mold', price:105}, {id:'PGR001',name:'Biostimulant Regulator A', primary:'Plant Growth Regulators', secondary:'Biostimulants/Regulators', crops:'Vegetables, fruit trees — growth', price:160}, {id:'PGR002',name:'Biostimulant Regulator B', primary:'Plant Growth Regulators', secondary:'Biostimulants/Regulators', crops:'Cereals, oilseeds — lodging resistance', price:155}, ]; /* ============================================================ STATE ============================================================ */ let currentPage = 'home'; let cart = []; let chatHistory = []; let cartOpen = false; let currentUser = null; // {id, name, email} let userLocation = null; // {lat, lng, city, country} — set after permission granted let selectedImages = []; // [{ file, url }] — images attached to next chat message (max 4) let shopState = { category:'All', subtype:'All', maxPrice:500, search:'', sort:'name' }; /* ============================================================ HELPERS ============================================================ */ function getInitials(name) { const words = name.split(/\s+/).filter(w => !/^[\d.%]+$/.test(w)); if (!words.length) return name.slice(0,2).toUpperCase(); if (words.length === 1) return words[0].slice(0,2).toUpperCase(); return (words[0][0] + words[1][0]).toUpperCase(); } function catMeta(primary) { if (primary === 'Biological Pesticides') return { bg:'#1a3820', badge:'BIO', cls:'badge-bio' }; if (primary === 'Chemical Pesticides') return { bg:'#3d2010', badge:'CHEM', cls:'badge-chem' }; if (primary === 'Fertilizers') return { bg:'#102040', badge:'FERT', cls:'badge-fert' }; if (primary === 'Plant Growth Regulators') return { bg:'#251535', badge:'PGR', cls:'badge-pgr' }; return { bg:'#1a1a1a', badge:'—', cls:'' }; } function subtypeMatch(secondary, filter) { const s = secondary.toLowerCase(); if (filter === 'Insecticides') return s.includes('insect'); if (filter === 'Fungicides') return s.includes('fungic'); if (filter === 'Herbicides') return s.includes('herbic'); if (filter === 'Foliar Nutrients') return s.includes('foliar'); if (filter === 'Bacillus Bio') return s.includes('bacillus'); return true; } function greeting() { const h = new Date().getHours(); if (h < 12) return 'Good morning'; if (h < 17) return 'Good afternoon'; return 'Good evening'; } /* ============================================================ AUTH ============================================================ */ async function loadUser() { try { const res = await fetch('/api/auth/me'); if (!res.ok) { window.location.href = '/'; return; } currentUser = await res.json(); applyUserToUI(); } catch { window.location.href = '/'; } } function applyUserToUI() { if (!currentUser) return; const first = currentUser.name.split(' ')[0]; // Nav avatar const av = document.getElementById('userAvatar'); if (av) av.textContent = currentUser.name[0].toUpperCase(); const nm = document.getElementById('userNameNav'); if (nm) nm.textContent = first; // Dropdown const dn = document.getElementById('dropdownName'); if (dn) dn.textContent = currentUser.name; const de = document.getElementById('dropdownEmail'); if (de) de.textContent = currentUser.email; // Dashboard greeting refreshDashboard(); } async function handleLogout() { await fetch('/api/auth/logout', { method: 'POST' }); window.location.href = '/'; } function toggleUserDropdown() { document.getElementById('userDropdown').classList.toggle('hidden'); } // Close dropdown when clicking outside document.addEventListener('click', e => { const dd = document.getElementById('userDropdown'); if (!dd) return; if (!e.target.closest('#userBtn') && !e.target.closest('#userDropdown')) { dd.classList.add('hidden'); } }); /* ============================================================ LOCATION — one-time per user account (keyed by user ID) ============================================================ */ function _locDecidedKey() { return currentUser ? `lg_loc_decided_${currentUser.id}` : 'lg_loc_decided'; } function _locDataKey() { return currentUser ? `lg_location_${currentUser.id}` : 'lg_location'; } function showLocationModal() { if (localStorage.getItem(_locDecidedKey())) return; document.getElementById('locationModal').classList.remove('hidden'); } function allowLocation() { document.getElementById('locationModal').classList.add('hidden'); localStorage.setItem(_locDecidedKey(), '1'); navigator.geolocation.getCurrentPosition( pos => { const { latitude: lat, longitude: lng } = pos.coords; fetch(`https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lng}&format=json`) .then(r => r.json()) .then(data => { userLocation = { lat, lng, city: data.address?.city || data.address?.town || data.address?.village || '', country: data.address?.country || '', }; localStorage.setItem(_locDataKey(), JSON.stringify(userLocation)); showLocationBadge(); }) .catch(() => { userLocation = { lat, lng, city: '', country: '' }; localStorage.setItem(_locDataKey(), JSON.stringify(userLocation)); }); }, () => {} ); } function skipLocation() { document.getElementById('locationModal').classList.add('hidden'); localStorage.setItem(_locDecidedKey(), '1'); } function showLocationBadge() { if (!userLocation || !userLocation.city) return; const wrap = document.getElementById('locationBadgeWrap'); const text = document.getElementById('locationText'); if (wrap && text) { text.textContent = userLocation.city + (userLocation.country ? ', ' + userLocation.country : ''); wrap.style.display = ''; } } function loadSavedLocation() { const saved = localStorage.getItem(_locDataKey()); if (saved) { try { userLocation = JSON.parse(saved); showLocationBadge(); } catch {} } } /* ============================================================ NAVIGATION ============================================================ */ function goPage(name) { if (name !== 'orders') stopTrackingPoll(); document.querySelectorAll('.page').forEach(p => p.classList.add('hidden')); const el = document.getElementById('page-' + name); if (el) el.classList.remove('hidden'); const nav = document.getElementById('mainNav'); const darkPages = ['home', 'sim']; nav.classList.toggle('nav--dark', darkPages.includes(name)); nav.classList.toggle('nav--light', !darkPages.includes(name)); document.querySelectorAll('.nav-link').forEach(l => l.classList.toggle('active', l.dataset.page === name) ); currentPage = name; if (name === 'dashboard') refreshDashboard(); if (name === 'shop') renderProducts(); if (name === 'orders') { initOrdersPage(); setTimeout(() => { if (_trackingMap) _trackingMap.invalidateSize(); }, 120); } if (name === 'sim') { const frame = document.getElementById('simFrame'); if (frame && !frame.getAttribute('src')) frame.src = '/simulation.html'; } } /* ============================================================ CART ============================================================ */ function addToCart(productId) { const p = PRODUCTS.find(x => x.id === productId); if (!p) return; const ex = cart.find(i => i.id === productId); if (ex) { ex.qty++; } else { cart.push({id:p.id,name:p.name,primary:p.primary,price:p.price,qty:1}); } updateCartBadge(); renderCart(); renderProducts(); refreshDashboard(); showCartToast(p.name); if (!cartOpen) toggleCart(); } function showCartToast(name) { let t = document.getElementById('cartToast'); if (!t) { t = document.createElement('div'); t.id = 'cartToast'; document.body.appendChild(t); } t.textContent = (typeof window.t === 'function' ? window.t('added_to_cart') : '✓ Added to cart: ') + name; t.className = 'cart-toast show'; clearTimeout(t._timer); t._timer = setTimeout(() => t.classList.remove('show'), 2500); } function removeFromCart(id) { cart = cart.filter(i => i.id !== id); updateCartBadge(); renderCart(); renderProducts(); refreshDashboard(); } function updateCartQty(id, delta) { const item = cart.find(i => i.id === id); if (!item) return; item.qty = Math.max(0, item.qty + delta); if (item.qty === 0) removeFromCart(id); else { updateCartBadge(); renderCart(); refreshDashboard(); } } function updateCartBadge() { const n = cart.reduce((s,i) => s+i.qty, 0); document.getElementById('cartBadge').textContent = n; } function toggleCart() { cartOpen = !cartOpen; document.getElementById('cartDrawer').classList.toggle('open', cartOpen); document.getElementById('cartOverlay').classList.toggle('open', cartOpen); if (cartOpen) renderCart(); } function cartSubtotal() { return cart.reduce((s,i) => s+i.price*i.qty, 0); } function renderCart() { const itemsEl = document.getElementById('cartItems'); const footerEl = document.getElementById('cartFooter'); if (!cart.length) { itemsEl.innerHTML = `
${t('cart_empty_title')}
`; footerEl.innerHTML = ''; return; } itemsEl.innerHTML = cart.map(item => { const m = catMeta(item.primary); return `
${getInitials(item.name)}
${item.name}
${item.primary}
${item.qty}
SAR ${(item.price*item.qty).toLocaleString()}
`; }).join(''); const sub = cartSubtotal(); const shipping = 15; const vat = Math.round(sub * 0.15); const total = sub + shipping + vat; footerEl.innerHTML = `
${t('cart_subtotal')}SAR ${sub.toLocaleString()}
${t('cart_shipping')}SAR ${shipping}
${t('cart_vat')}SAR ${vat.toLocaleString()}
${t('cart_grand_total')}SAR ${total.toLocaleString()}
`; } /* ============================================================ SHOP ============================================================ */ function getFilteredProducts() { return PRODUCTS .filter(p => { if (shopState.category !== 'All' && p.primary !== shopState.category) return false; if (shopState.subtype !== 'All' && !subtypeMatch(p.secondary, shopState.subtype)) return false; if (p.price > shopState.maxPrice) return false; if (shopState.search) { const q = shopState.search.toLowerCase(); if (!p.name.toLowerCase().includes(q) && !p.crops.toLowerCase().includes(q)) return false; } return true; }) .sort((a,b) => { if (shopState.sort === 'price-asc') return a.price - b.price; if (shopState.sort === 'price-desc') return b.price - a.price; if (shopState.sort === 'type') return a.primary.localeCompare(b.primary); return a.name.localeCompare(b.name); }); } function renderProducts() { const grid = document.getElementById('productGrid'); if (!grid) return; const list = getFilteredProducts(); if (!list.length) { grid.innerHTML = '
No products match your filters
'; return; } grid.innerHTML = list.map(p => { const m = catMeta(p.primary); const inCart = cart.some(i => i.id === p.id); return `
${getInitials(p.name)} ${m.badge}
${p.name}
${p.primary}
`; }).join(''); } function initShopFilters() { document.getElementById('filterCategory').addEventListener('click', e => { const li = e.target.closest('.filter-item'); if (!li) return; document.querySelectorAll('#filterCategory .filter-item').forEach(x => x.classList.remove('active')); li.classList.add('active'); shopState.category = li.dataset.cat; renderProducts(); }); document.getElementById('filterSubtype').addEventListener('click', e => { const li = e.target.closest('.filter-item'); if (!li) return; document.querySelectorAll('#filterSubtype .filter-item').forEach(x => x.classList.remove('active')); li.classList.add('active'); shopState.subtype = li.dataset.sub; renderProducts(); }); document.getElementById('priceSlider').addEventListener('input', e => { shopState.maxPrice = parseInt(e.target.value); document.getElementById('priceMax').textContent = e.target.value; renderProducts(); }); document.getElementById('searchInput').addEventListener('input', e => { shopState.search = e.target.value.trim(); renderProducts(); }); document.getElementById('sortSelect').addEventListener('change', e => { shopState.sort = e.target.value; renderProducts(); }); } /* ============================================================ AI ADVISOR CHAT — uses /api/chat (LeafGuardAgent backend) ============================================================ */ function escHtml(t) { return t.replace(/&/g,'&').replace(//g,'>'); } function inlineFormat(escaped) { return escaped.replace(/\*\*(.+?)\*\*/g, '$1'); } function formatBotMsg(text) { const lines = text.split('\n'); let html = ''; let inUl = false; let inOl = false; function closeList() { if (inUl) { html += ''; inUl = false; } if (inOl) { html += ''; inOl = false; } } for (const raw of lines) { // ### / ## / # heading const hm = raw.match(/^#{1,3}\s+(.+)$/); if (hm) { closeList(); html += `
${inlineFormat(escHtml(hm[1]))}
`; continue; } // Numbered list: 1. item const nm = raw.match(/^(\d+)\.\s+(.+)$/); if (nm) { if (inUl) { html += ''; inUl = false; } if (!inOl) { html += '
    '; inOl = true; } html += `
  1. ${inlineFormat(escHtml(nm[2]))}
  2. `; continue; } // Bullet: - item or * item const bm = raw.match(/^[\*\-]\s+(.+)$/); if (bm) { if (inOl) { html += '
'; inOl = false; } if (!inUl) { html += '