// customer-view.js — ES Module // Customer list with filtering, QBO status, email, modal with contact/remarks let customers = []; let filterName = localStorage.getItem('cust_filterName') || ''; let filterQbo = localStorage.getItem('cust_filterQbo') || 'all'; // all | qbo | local // ============================================================ // Data // ============================================================ export async function loadCustomers() { try { const response = await fetch('/api/customers'); customers = await response.json(); // Backward compat: quote/invoice modals use global 'customers' variable window.customers = customers; renderCustomerView(); } catch (error) { console.error('Error loading customers:', error); } } export function getCustomers() { return customers; } // ============================================================ // Filter // ============================================================ function getFilteredCustomers() { let f = [...customers]; if (filterName.trim()) { const s = filterName.toLowerCase(); f = f.filter(c => (c.name || '').toLowerCase().includes(s) || (c.contact || '').toLowerCase().includes(s) || (c.email || '').toLowerCase().includes(s)); } if (filterQbo === 'qbo') f = f.filter(c => c.qbo_id); else if (filterQbo === 'local') f = f.filter(c => !c.qbo_id); f.sort((a, b) => (a.name || '').localeCompare(b.name || '')); return f; } function saveSettings() { localStorage.setItem('cust_filterName', filterName); localStorage.setItem('cust_filterQbo', filterQbo); } // ============================================================ // Render // ============================================================ export function renderCustomerView() { const tbody = document.getElementById('customers-list'); if (!tbody) return; const filtered = getFilteredCustomers(); tbody.innerHTML = filtered.map(customer => { const lines = [customer.line1, customer.line2, customer.line3, customer.line4].filter(Boolean); const cityStateZip = [customer.city, customer.state, customer.zip_code].filter(Boolean).join(' '); let fullAddress = lines.join(', '); if (cityStateZip) fullAddress += (fullAddress ? ', ' : '') + cityStateZip; // QBO Status const qboStatus = customer.qbo_id ? `QBO ✓` : ``; // Contact const contactDisplay = customer.contact ? `(${customer.contact})` : ''; // Email const emailDisplay = customer.email ? `${customer.email}` : ''; return ` ${customer.name} ${qboStatus} ${contactDisplay} ${fullAddress || '—'} ${emailDisplay} ${customer.account_number || '—'} `; }).join(''); if (filtered.length === 0) { tbody.innerHTML = `No customers found.`; } const countEl = document.getElementById('customer-count'); if (countEl) countEl.textContent = filtered.length; updateFilterButtons(); } function updateFilterButtons() { document.querySelectorAll('[data-qbo-filter]').forEach(btn => { const s = btn.getAttribute('data-qbo-filter'); btn.classList.toggle('bg-blue-600', s === filterQbo); btn.classList.toggle('text-white', s === filterQbo); btn.classList.toggle('bg-white', s !== filterQbo); btn.classList.toggle('text-gray-600', s !== filterQbo); }); } // ============================================================ // Toolbar // ============================================================ export function injectToolbar() { const c = document.getElementById('customer-toolbar'); if (!c) return; c.innerHTML = `
0 customers
`; updateFilterButtons(); document.getElementById('customer-filter-name').addEventListener('input', (e) => { filterName = e.target.value; saveSettings(); renderCustomerView(); }); } // ============================================================ // Modal // ============================================================ function ensureModalElement() { let modal = document.getElementById('customer-modal-v2'); if (modal) return; modal = document.createElement('div'); modal.id = 'customer-modal-v2'; modal.className = 'modal fixed inset-0 bg-black bg-opacity-50 z-50 justify-center items-start pt-10 overflow-y-auto'; document.body.appendChild(modal); } export function openModal(customerId = null) { ensureModalElement(); const modal = document.getElementById('customer-modal-v2'); const isEdit = !!customerId; const customer = isEdit ? customers.find(c => c.id === customerId) : null; modal.innerHTML = `

${isEdit ? 'Edit Customer' : 'New Customer'}

`; modal.classList.add('active'); document.getElementById('customer-form-v2').addEventListener('submit', handleSubmit); } export function closeModal() { const modal = document.getElementById('customer-modal-v2'); if (modal) modal.classList.remove('active'); } // ============================================================ // Submit // ============================================================ async function handleSubmit(e) { e.preventDefault(); const data = { name: document.getElementById('cf-name').value, contact: document.getElementById('cf-contact').value || null, line1: document.getElementById('cf-line1').value || null, line2: document.getElementById('cf-line2').value || null, line3: document.getElementById('cf-line3').value || null, line4: document.getElementById('cf-line4').value || null, city: document.getElementById('cf-city').value || null, state: (document.getElementById('cf-state').value || '').toUpperCase() || null, zip_code: document.getElementById('cf-zip').value || null, account_number: document.getElementById('cf-account').value || null, email: document.getElementById('cf-email').value || null, phone: document.getElementById('cf-phone').value || null, phone2: null, taxable: document.getElementById('cf-taxable').checked, remarks: document.getElementById('cf-remarks').value || null }; const customerId = document.getElementById('cf-id').value; const url = customerId ? `/api/customers/${customerId}` : '/api/customers'; const method = customerId ? 'PUT' : 'POST'; try { const response = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); if (response.ok) { closeModal(); await loadCustomers(); } else { const err = await response.json(); alert(`Error: ${err.error || 'Failed to save customer'}`); } } catch (error) { console.error('Error saving customer:', error); alert('Network error saving customer.'); } } // ============================================================ // Actions // ============================================================ export function edit(id) { openModal(id); } export async function remove(id) { const customer = customers.find(c => c.id === id); if (!customer) return; let msg = `Delete customer "${customer.name}"?`; if (customer.qbo_id) msg += '\nThis will also deactivate the customer in QBO.'; if (!confirm(msg)) return; try { const response = await fetch(`/api/customers/${id}`, { method: 'DELETE' }); if (response.ok) await loadCustomers(); else { const err = await response.json(); alert(`Error: ${err.error || 'Failed to delete'}`); } } catch (error) { console.error('Error:', error); alert('Network error.'); } } export async function exportToQbo(id) { const customer = customers.find(c => c.id === id); if (!customer) return; if (!confirm(`Export "${customer.name}" to QuickBooks Online?`)) return; if (typeof showSpinner === 'function') showSpinner('Exporting customer to QBO...'); try { const response = await fetch(`/api/customers/${id}/export-qbo`, { method: 'POST' }); const result = await response.json(); if (response.ok) { alert(`✅ "${result.name}" exported to QBO (ID: ${result.qbo_id}).`); await loadCustomers(); } else { alert(`❌ Error: ${result.error}`); } } catch (error) { alert('Network error.'); } finally { if (typeof hideSpinner === 'function') hideSpinner(); } } export function setQboFilter(val) { filterQbo = val; saveSettings(); renderCustomerView(); } // ============================================================ // Expose // ============================================================ window.customerView = { loadCustomers, renderCustomerView, getCustomers, openModal, closeModal, edit, remove, exportToQbo, setQboFilter }; // Make customers available globally for other modules (quote/invoice dropdowns) window.getCustomers = () => customers;