// 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;