From 9ebfd9b8c3b975e0e3cd2c1468cbd409b914d66b Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Mon, 23 Feb 2026 14:09:52 -0600 Subject: [PATCH] customer module + features --- public/app.js | 176 +----------------- public/customer-view.js | 399 ++++++++++++++++++++++++++++++++++++++++ public/index.html | 135 +++----------- server.js | 125 ++++++------- 4 files changed, 482 insertions(+), 353 deletions(-) create mode 100644 public/customer-view.js diff --git a/public/app.js b/public/app.js index 549eb25..8097309 100644 --- a/public/app.js +++ b/public/app.js @@ -1,5 +1,5 @@ // Global state -let customers = []; +let customers = []; // shared, updated by customer-view.js let quotes = []; let invoices = []; let currentQuoteId = null; @@ -204,50 +204,8 @@ async function uploadLogo() { } } -// Customer Management -async function loadCustomers() { - try { - const response = await fetch('/api/customers'); - customers = await response.json(); - renderCustomers(); - } catch (error) { - console.error('Error loading customers:', error); - alert('Error loading customers'); - } -} -// ===================================================== -// 1. renderCustomers() — ERSETZE komplett -// Zeigt QBO-Status und Export-Button in der Kundenliste -// ===================================================== -function renderCustomers() { - const tbody = document.getElementById('customers-list'); - tbody.innerHTML = customers.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 ✓` - : ``; - - return ` - - - ${customer.name} ${qboStatus} - - ${fullAddress || '-'} - ${customer.account_number || '-'} - - - - - `; - }).join(''); -} // --- 2. Credits async laden --- async function loadCustomerCredits() { @@ -406,139 +364,7 @@ async function submitDownpayment(customerId, customerQboId) { if (typeof hideSpinner === 'function') hideSpinner(); } } -function openCustomerModal(customerId = null) { - currentCustomerId = customerId; - const modal = document.getElementById('customer-modal'); - const title = document.getElementById('customer-modal-title'); - - if (customerId) { - title.textContent = 'Edit Customer'; - const customer = customers.find(c => c.id === customerId); - - document.getElementById('customer-id').value = customer.id; - document.getElementById('customer-name').value = customer.name; - - // Neue Felder befüllen - document.getElementById('customer-line1').value = customer.line1 || ''; - document.getElementById('customer-line2').value = customer.line2 || ''; - document.getElementById('customer-line3').value = customer.line3 || ''; - document.getElementById('customer-line4').value = customer.line4 || ''; - - document.getElementById('customer-city').value = customer.city || ''; - document.getElementById('customer-state').value = customer.state || ''; - document.getElementById('customer-zip').value = customer.zip_code || ''; - document.getElementById('customer-account').value = customer.account_number || ''; - document.getElementById('customer-email').value = customer.email || ''; - document.getElementById('customer-phone').value = customer.phone || ''; - - document.getElementById('customer-taxable').checked = customer.taxable !== false; - } else { - title.textContent = 'New Customer'; - document.getElementById('customer-form').reset(); - document.getElementById('customer-id').value = ''; - document.getElementById('customer-taxable').checked = true; - } - - modal.classList.add('active'); -} -function closeCustomerModal() { - document.getElementById('customer-modal').classList.remove('active'); - currentCustomerId = null; -} - -async function handleCustomerSubmit(e) { - e.preventDefault(); - - const data = { - name: document.getElementById('customer-name').value, - - // Neue Felder auslesen - line1: document.getElementById('customer-line1').value, - line2: document.getElementById('customer-line2').value, - line3: document.getElementById('customer-line3').value, - line4: document.getElementById('customer-line4').value, - - city: document.getElementById('customer-city').value, - state: document.getElementById('customer-state').value.toUpperCase(), - zip_code: document.getElementById('customer-zip').value, - account_number: document.getElementById('customer-account').value, - email: document.getElementById('customer-email')?.value || '', - phone: document.getElementById('customer-phone')?.value || '', - phone2: '', // Erstmal leer lassen, falls kein Feld im Formular ist - taxable: document.getElementById('customer-taxable')?.checked ?? true - }; - - try { - const customerId = document.getElementById('customer-id').value; - const url = customerId ? `/api/customers/${customerId}` : '/api/customers'; - const method = customerId ? 'PUT' : 'POST'; - - const response = await fetch(url, { - method, - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data) - }); - - if (response.ok) { - closeCustomerModal(); - loadCustomers(); - } else { - alert('Error saving customer'); - } - } catch (error) { - console.error('Error:', error); - alert('Error saving customer'); - } -} - -async function editCustomer(id) { - openCustomerModal(id); -} - -async function deleteCustomer(id) { - if (!confirm('Are you sure you want to delete this customer?')) return; - - try { - const response = await fetch(`/api/customers/${id}`, { method: 'DELETE' }); - if (response.ok) { - loadCustomers(); - } else { - alert('Error deleting customer'); - } - } catch (error) { - console.error('Error:', error); - alert('Error deleting customer'); - } -} -async function exportCustomerToQbo(customerId) { - const customer = customers.find(c => c.id === customerId); - if (!customer) return; - - if (!confirm(`Kunde "${customer.name}" nach QuickBooks Online exportieren?`)) return; - - showSpinner('Exportiere Kunde nach QBO...'); - - try { - const response = await fetch(`/api/customers/${customerId}/export-qbo`, { method: 'POST' }); - const result = await response.json(); - - if (response.ok) { - alert(`✅ Kunde "${result.name}" erfolgreich in QBO erstellt (ID: ${result.qbo_id}).`); - // Kunden-Liste neu laden - const custResponse = await fetch('/api/customers'); - customers = await custResponse.json(); - renderCustomers(); - } else { - alert(`❌ Fehler: ${result.error}`); - } - } catch (error) { - console.error('Error exporting customer:', error); - alert('Netzwerkfehler beim Export.'); - } finally { - hideSpinner(); - } -} // Quote Management async function loadQuotes() { try { diff --git a/public/customer-view.js b/public/customer-view.js new file mode 100644 index 0000000..fbd5534 --- /dev/null +++ b/public/customer-view.js @@ -0,0 +1,399 @@ +// 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(); + 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; \ No newline at end of file diff --git a/public/index.html b/public/index.html index c693081..0bb2dbf 100644 --- a/public/index.html +++ b/public/index.html @@ -20,7 +20,7 @@
-