// Customer management functionality - Tailwind version let currentPage = 0; let currentSearch = ''; let isEditing = false; let editingCustomerId = null; let selectedCustomerIds = new Set(); let customerCompactMode = false; // Local debounce fallback to avoid dependency on main.js function _localDebounce(func, wait) { let timeout; return function() { const context = this, args = arguments; clearTimeout(timeout); timeout = setTimeout(() => func.apply(context, args), wait); }; } // Enhanced table display function function displayCustomers(customers) { const tbody = document.getElementById('customersTableBody'); const emptyState = document.getElementById('emptyState'); tbody.innerHTML = ''; if (!customers || customers.length === 0) { emptyState.classList.remove('hidden'); return; } emptyState.classList.add('hidden'); // Selection removed // Build highlight function based on currentSearch tokens const tokens = (window.highlightUtils && typeof window.highlightUtils.buildTokens === 'function') ? window.highlightUtils.buildTokens((currentSearch || '').trim()) : []; function highlightText(text) { if (!text) return ''; if (!window.highlightUtils || typeof window.highlightUtils.highlight !== 'function' || tokens.length === 0) { return escapeHtml(String(text)); } // Use safe highlighter that computes ranges, then transform to styled const strongHtml = window.highlightUtils.highlight(String(text), tokens); return strongHtml .replace(//g, '') .replace(/<\/strong>/g, ''); } customers.forEach(customer => { const phones = Array.isArray(customer.phone_numbers) ? customer.phone_numbers : []; const primaryPhone = phones.length > 0 ? (phones[0].phone || '') : ''; const phoneCount = phones.length; const phoneHtml = `${highlightText(primaryPhone)}${phoneCount > 1 ? ` (+${phoneCount - 1} more)` : ''}`; const row = document.createElement('tr'); row.className = 'group odd:bg-neutral-50 dark:odd:bg-neutral-800/50 border-b border-neutral-100 dark:border-neutral-700/50 hover:bg-primary-50 dark:hover:bg-primary-900/20 transition-colors'; // Store customer ID as data attribute to avoid escaping issues in onclick row.dataset.customerId = customer.id; // Build clean, simple row structure with clickable rows (no inline onclick to avoid backslash issues) const pad = customerCompactMode ? 'px-3 py-2' : 'px-6 py-4'; row.innerHTML = `
${highlightText(customer.id || '')}
${highlightText(formatFullName(customer))}
${customerCompactMode ? '' : (customer.title ? `
${highlightText(customer.title)}
` : '')} ${customer.group ? `${highlightText(customer.group)}` : '-'}
${highlightText(formatCityState(customer))}
${customerCompactMode ? '' : (customer.a1 ? `
${highlightText(customer.a1)}
` : '')}
${phoneHtml}
${customer.email ? `${highlightText(customer.email)}` : '-'}
`; // Add event listeners using the stored customer ID (no escaping issues) const customerCells = row.querySelectorAll('.customer-cell'); customerCells.forEach(cell => { cell.addEventListener('click', () => viewCustomer(customer.id)); }); const viewBtn = row.querySelector('.view-customer-btn'); const editBtn = row.querySelector('.edit-customer-btn'); viewBtn.addEventListener('click', (e) => { e.stopPropagation(); viewCustomer(customer.id); }); editBtn.addEventListener('click', (e) => { e.stopPropagation(); editCustomer(customer.id); }); tbody.appendChild(row); }); // No select-all } // Helper functions function getInitials(customer) { const first = (customer.first || '').trim(); const last = (customer.last || '').trim(); if (first && last) { return (first.charAt(0) + last.charAt(0)).toUpperCase(); } else if (last) { return last.charAt(0).toUpperCase(); } else if (first) { return first.charAt(0).toUpperCase(); } else { return '?'; } } function formatFullName(customer) { const parts = []; if (customer.prefix) parts.push(customer.prefix.trim()); if (customer.first) parts.push(customer.first.trim()); if (customer.middle) parts.push(customer.middle.trim()); if (customer.last) parts.push(customer.last.trim()); if (customer.suffix) parts.push(customer.suffix.trim()); return parts.join(' ') || 'Unknown'; } function formatCityState(customer) { const parts = []; if (customer.city) parts.push(customer.city.trim()); if (customer.abrev) parts.push(customer.abrev.trim()); return parts.join(', ') || '-'; } function formatPrimaryPhone(phones) { if (!phones || phones.length === 0) { return '-'; } const primary = phones[0]; const count = phones.length; if (count === 1) { return escapeHtml(primary.phone || ''); } else { return escapeHtml(primary.phone || '') + ` (+${count - 1} more)`; } } function escapeHtml(str) { if (!str && str !== 0) return ''; return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } // Enhanced alert function function showAlert(message, type = 'info') { if (window.alerts && typeof window.alerts.show === 'function') { window.alerts.show(message, type); return; } // Fallback alert(String(message)); } // Modal management functions function showAddCustomerModal() { isEditing = false; editingCustomerId = null; document.getElementById('customerModalLabel').textContent = 'Add New Customer'; document.getElementById('deleteCustomerBtn').classList.add('hidden'); clearCustomerForm(); document.getElementById('customerModal').classList.remove('hidden'); } function closeCustomerModal() { document.getElementById('customerModal').classList.add('hidden'); } function showEditCustomerModal() { document.getElementById('customerModalLabel').textContent = 'Edit Customer'; document.getElementById('deleteCustomerBtn').classList.remove('hidden'); document.getElementById('customerModal').classList.remove('hidden'); } // Customer detail functions async function viewCustomer(customerId) { try { // URL encode the customer ID to handle special characters like backslashes and spaces const encodedCustomerId = encodeURIComponent(customerId); const response = await window.http.wrappedFetch(`/api/customers/${encodedCustomerId}`); if (!response.ok) throw new Error('Failed to load customer details'); const customer = await response.json(); showCustomerDetailsModal(customer); } catch (error) { console.error('Error loading customer:', error); showAlert(`Error loading customer: ${error.message}`, 'danger'); } } function showCustomerDetailsModal(customer) { // Create modal content const modalContent = `

Customer Details

${escapeHtml(formatFullName(customer))}

${escapeHtml(customer.id || '')} ${customer.group ? `${escapeHtml(customer.group)}` : ''} ${customer.title ? `${escapeHtml(customer.title)}` : ''}

Contact Details

${customer.email ? ` ` : `
No email provided
`} ${formatCustomerPhonesCard(customer.phone_numbers || [])}

Address

${formatCustomerAddressCard(customer)}

Additional Info

${customer.dob ? `
Date of Birth: ${escapeHtml(customer.dob)}
` : ''} ${customer.legal_status ? `
Legal Status: ${escapeHtml(customer.legal_status)}
` : ''} ${customer.ss_number ? `
SSN: ${escapeHtml(customer.ss_number)}
` : ''} ${!customer.dob && !customer.legal_status && !customer.ss_number ? `

No additional information available

` : ''}
${customer.memo ? `

Notes

${escapeHtml(customer.memo)}
` : ''}
`; // Add modal to body document.body.insertAdjacentHTML('beforeend', modalContent); } function closeCustomerDetailsModal() { const modal = document.getElementById('customerDetailsModal'); if (modal) { modal.remove(); } } function formatCustomerAddress(customer) { const parts = []; if (customer.a1) parts.push(`
${escapeHtml(customer.a1)}
`); if (customer.a2) parts.push(`
${escapeHtml(customer.a2)}
`); if (customer.a3) parts.push(`
${escapeHtml(customer.a3)}
`); const cityState = formatCityState(customer); if (cityState !== '-') { parts.push(`
${escapeHtml(cityState)}
`); } if (customer.zip) { parts.push(`
${escapeHtml(customer.zip)}
`); } return parts.length > 0 ? `
Address:
${parts.join('')}
` : ''; } function formatCustomerPhones(phones) { if (!phones || phones.length === 0) { return '
Phone: None
'; } const phoneList = phones.map(phone => `
${phone.location ? `${escapeHtml(phone.location)}: ` : ''}${escapeHtml(phone.phone)}
` ).join(''); return `
Phone${phones.length > 1 ? 's' : ''}:${phoneList}
`; } function formatCustomerAddressLarge(customer) { const addressParts = []; if (customer.a1) addressParts.push(escapeHtml(customer.a1)); if (customer.a2) addressParts.push(escapeHtml(customer.a2)); if (customer.a3) addressParts.push(escapeHtml(customer.a3)); const cityState = formatCityState(customer); if (cityState !== '-') addressParts.push(escapeHtml(cityState)); if (customer.zip) addressParts.push(escapeHtml(customer.zip)); if (addressParts.length === 0) { return '
Address: Not provided
'; } return `
Address:
${addressParts.map(part => `
${part}
`).join('')}
`; } function formatCustomerPhonesLarge(phones) { if (!phones || phones.length === 0) { return '
Phone: None
'; } const phoneList = phones.map(phone => `
${escapeHtml(phone.phone)} ${phone.location ? `${escapeHtml(phone.location)}` : ''}
` ).join(''); return `
Phone${phones.length > 1 ? 's' : ''}:
${phoneList}
`; } function formatCustomerPhonesCard(phones) { if (!phones || phones.length === 0) { return `
No phone numbers
`; } return phones.map(phone => `
${escapeHtml(phone.phone)} ${phone.location ? `${escapeHtml(phone.location)}` : ''}
`).join(''); } function formatCustomerAddressCard(customer) { const addressParts = []; if (customer.a1) addressParts.push(escapeHtml(customer.a1)); if (customer.a2) addressParts.push(escapeHtml(customer.a2)); if (customer.a3) addressParts.push(escapeHtml(customer.a3)); const cityState = formatCityState(customer); if (cityState !== '-') addressParts.push(escapeHtml(cityState)); if (customer.zip) addressParts.push(escapeHtml(customer.zip)); if (addressParts.length === 0) { return `

No address provided

`; } return `
${addressParts.map(part => `
${part}
`).join('')}
`; } async function editCustomer(customerId) { try { // URL encode the customer ID to handle special characters like backslashes and spaces const encodedCustomerId = encodeURIComponent(customerId); const response = await window.http.wrappedFetch(`/api/customers/${encodedCustomerId}`); if (!response.ok) throw new Error('Failed to load customer details'); const customer = await response.json(); populateEditForm(customer); showEditCustomerModal(); } catch (error) { console.error('Error loading customer for edit:', error); showAlert(`Error loading customer: ${error.message}`, 'danger'); } } function populateEditForm(customer) { isEditing = true; editingCustomerId = customer.id; // Populate form fields document.getElementById('customerId').value = customer.id || ''; document.getElementById('last').value = customer.last || ''; document.getElementById('first').value = customer.first || ''; document.getElementById('middle').value = customer.middle || ''; document.getElementById('prefix').value = customer.prefix || ''; document.getElementById('suffix').value = customer.suffix || ''; document.getElementById('title').value = customer.title || ''; document.getElementById('group').value = customer.group || ''; document.getElementById('a1').value = customer.a1 || ''; document.getElementById('a2').value = customer.a2 || ''; document.getElementById('a3').value = customer.a3 || ''; document.getElementById('city').value = customer.city || ''; document.getElementById('abrev').value = customer.abrev || ''; document.getElementById('zip').value = customer.zip || ''; document.getElementById('email').value = customer.email || ''; document.getElementById('dob').value = customer.dob || ''; document.getElementById('ss_number').value = customer.ss_number || ''; document.getElementById('legal_status').value = customer.legal_status || ''; document.getElementById('memo').value = customer.memo || ''; // Populate phone numbers populatePhoneNumbers(customer.phone_numbers || []); // Make customer ID readonly for editing document.getElementById('customerId').readOnly = true; } async function saveCustomer() { try { // Gather form data const customerData = { last: document.getElementById('last').value, first: document.getElementById('first').value || null, middle: document.getElementById('middle').value || null, prefix: document.getElementById('prefix').value || null, suffix: document.getElementById('suffix').value || null, title: document.getElementById('title').value || null, group: document.getElementById('group').value || null, a1: document.getElementById('a1').value || null, a2: document.getElementById('a2').value || null, a3: document.getElementById('a3').value || null, city: document.getElementById('city').value || null, abrev: document.getElementById('abrev').value || null, zip: document.getElementById('zip').value || null, email: document.getElementById('email').value || null, dob: document.getElementById('dob').value || null, ss_number: document.getElementById('ss_number').value || null, legal_status: document.getElementById('legal_status').value || null, memo: document.getElementById('memo').value || null }; // Validate required fields if (!customerData.last) { showAlert('Last name/Company is required', 'danger'); return; } // Double confirm for edits if (isEditing) { const confirmMessage = `Are you sure you want to update customer "${customerData.last}"? This will permanently modify the customer record.`; const firstConfirm = confirm(confirmMessage); if (!firstConfirm) { return; // User cancelled } // Second confirmation const secondConfirm = confirm('Please confirm again: Do you really want to save these changes? This action cannot be undone.'); if (!secondConfirm) { return; // User cancelled on second confirmation } } let response; if (isEditing) { // Update existing customer const encodedCustomerId = encodeURIComponent(editingCustomerId); response = await window.http.wrappedFetch(`/api/customers/${encodedCustomerId}`, { method: 'PUT', body: JSON.stringify(customerData) }); } else { // Create new customer customerData.id = document.getElementById('customerId').value; if (!customerData.id) { showAlert('Customer ID is required', 'danger'); return; } response = await window.http.wrappedFetch('/api/customers/', { method: 'POST', body: JSON.stringify(customerData) }); } if (!response.ok) { const error = await response.json(); throw new Error(error.detail || 'Failed to save customer'); } showAlert(isEditing ? 'Customer updated successfully!' : 'Customer created successfully!', 'success'); closeCustomerModal(); loadCustomers(); // Refresh the customer list } catch (error) { console.error('Error saving customer:', error); showAlert(`Error saving customer: ${error.message}`, 'danger'); } } function deleteCustomer() { showAlert('Delete customer feature coming soon...', 'info'); } function validateCustomerId() { // Placeholder } function populatePhoneNumbers(phones) { const phoneList = document.getElementById('phoneList'); phoneList.innerHTML = ''; phones.forEach((phone, index) => { const phoneDiv = document.createElement('div'); phoneDiv.className = 'flex items-center space-x-2'; phoneDiv.innerHTML = `
`; phoneList.appendChild(phoneDiv); }); } function clearCustomerForm() { isEditing = false; editingCustomerId = null; // Clear all form fields document.getElementById('customerId').value = ''; document.getElementById('customerId').readOnly = false; document.getElementById('last').value = ''; document.getElementById('first').value = ''; document.getElementById('middle').value = ''; document.getElementById('prefix').value = ''; document.getElementById('suffix').value = ''; document.getElementById('title').value = ''; document.getElementById('group').value = ''; document.getElementById('a1').value = ''; document.getElementById('a2').value = ''; document.getElementById('a3').value = ''; document.getElementById('city').value = ''; document.getElementById('abrev').value = ''; document.getElementById('zip').value = ''; document.getElementById('email').value = ''; document.getElementById('dob').value = ''; document.getElementById('ss_number').value = ''; document.getElementById('legal_status').value = ''; document.getElementById('memo').value = ''; // Clear phone numbers document.getElementById('phoneList').innerHTML = ''; } function removePhoneNumber(index) { const phoneList = document.getElementById('phoneList'); const phoneInputs = phoneList.children; if (phoneInputs[index]) { phoneInputs[index].remove(); // Re-index remaining phone inputs Array.from(phoneInputs).forEach((phoneDiv, newIndex) => { const inputs = phoneDiv.querySelectorAll('input'); inputs.forEach(input => { input.setAttribute('data-phone-index', newIndex); }); const button = phoneDiv.querySelector('button'); if (button) { button.setAttribute('onclick', `removePhoneNumber(${newIndex})`); } }); } } function addPhoneNumber() { const phoneList = document.getElementById('phoneList'); const currentCount = phoneList.children.length; const phoneDiv = document.createElement('div'); phoneDiv.className = 'flex items-center space-x-2'; phoneDiv.innerHTML = `
`; phoneList.appendChild(phoneDiv); } // Make functions globally available window.showAddCustomerModal = showAddCustomerModal; window.closeCustomerModal = closeCustomerModal; window.showEditCustomerModal = showEditCustomerModal; window.displayCustomers = displayCustomers; window.showAlert = showAlert; window.editCustomer = editCustomer; window.viewCustomer = viewCustomer; window.saveCustomer = saveCustomer; window.deleteCustomer = deleteCustomer; window.validateCustomerId = validateCustomerId; window.closeCustomerDetailsModal = closeCustomerDetailsModal; window.populateEditForm = populateEditForm; window.populatePhoneNumbers = populatePhoneNumbers; window.clearCustomerForm = clearCustomerForm; window.addPhoneNumber = addPhoneNumber; window.removePhoneNumber = removePhoneNumber; window.updateRowSelectionClass = updateRowSelectionClass; window.syncSelectAllCheckbox = syncSelectAllCheckbox; window.enhanceCustomerTableRows = enhanceCustomerTableRows; window.initializeCustomerListEnhancer = initializeCustomerListEnhancer; // Enhance existing rows (useful for phone search results or server-rendered content) function enhanceCustomerTableRows() { const tbody = document.getElementById('customersTableBody'); if (!tbody) return; // Load persisted selection let savedSet = new Set(); try { const saved = JSON.parse(localStorage.getItem('customers.selectedIds') || '[]'); savedSet = new Set(Array.isArray(saved) ? saved : []); } catch (_) {} Array.from(tbody.querySelectorAll('tr')).forEach(row => { const id = row.dataset && row.dataset.customerId ? row.dataset.customerId : null; if (!id) return; let firstCell = row.children[0]; const hasCheckbox = firstCell && firstCell.querySelector && firstCell.querySelector('.customer-row-select'); if (!hasCheckbox) { const cell = document.createElement('td'); cell.className = `px-4 ${customerCompactMode ? 'py-2' : 'py-4'} text-center align-middle`; cell.innerHTML = ``; row.insertBefore(cell, row.firstChild); firstCell = cell; } const checkbox = row.querySelector('.customer-row-select'); if (checkbox) { checkbox.checked = savedSet.has(id); updateRowSelectionClass(row, checkbox.checked); if (!checkbox._enhanced) { checkbox.addEventListener('change', () => { if (checkbox.checked) { selectedCustomerIds.add(id); } else { selectedCustomerIds.delete(id); } saveSelectedIds(); updateRowSelectionClass(row, checkbox.checked); syncSelectAllCheckbox(); }); checkbox._enhanced = true; } } }); syncSelectAllCheckbox(); } function initializeCustomerListEnhancer() { const tbody = document.getElementById('customersTableBody'); if (!tbody || window._customerListObserver) return; const debouncedEnhance = (typeof window.debounce === 'function' ? window.debounce : _localDebounce)(() => enhanceCustomerTableRows(), 10); const observer = new MutationObserver(() => debouncedEnhance()); observer.observe(tbody, { childList: true, subtree: false }); window._customerListObserver = observer; // Initial pass enhanceCustomerTableRows(); } // Selection helpers function saveSelectedIds() { try { localStorage.setItem('customers.selectedIds', JSON.stringify(Array.from(selectedCustomerIds))); } catch (_) {} } function updateRowSelectionClass(row, selected) { row.classList.toggle('bg-blue-50', selected); row.classList.toggle('dark:bg-blue-900/30', selected); } function syncSelectAllCheckbox() { const headerCb = document.getElementById('selectAllCustomers'); if (!headerCb) return; const checkboxes = Array.from(document.querySelectorAll('#customersTableBody .customer-row-select')); if (checkboxes.length === 0) { headerCb.checked = false; headerCb.indeterminate = false; return; } const checkedCount = checkboxes.filter(cb => cb.checked).length; headerCb.checked = checkedCount === checkboxes.length; headerCb.indeterminate = checkedCount > 0 && checkedCount < checkboxes.length; } // Compact mode helpers function initializeCustomerListState() { try { customerCompactMode = localStorage.getItem('customers.compactMode') === '1'; } catch (_) { customerCompactMode = false; } updateCompactModeButton(); } function toggleCompactMode() { customerCompactMode = !customerCompactMode; try { localStorage.setItem('customers.compactMode', customerCompactMode ? '1' : '0'); } catch (_) {} updateCompactModeButton(); // Re-render current page with current search loadCustomers(currentPage, currentSearch); } function updateCompactModeButton() { const btn = document.getElementById('toggleCompactMode'); if (btn) { btn.textContent = `Compact: ${customerCompactMode ? 'On' : 'Off'}`; } } function onSelectAllChange(checked) { const checkboxes = Array.from(document.querySelectorAll('#customersTableBody .customer-row-select')); checkboxes.forEach(cb => { cb.checked = checked; const row = cb.closest('tr'); const id = cb.dataset.id; if (checked) { selectedCustomerIds.add(id); } else { selectedCustomerIds.delete(id); } updateRowSelectionClass(row, checked); }); saveSelectedIds(); syncSelectAllCheckbox(); } // Expose helpers window.initializeCustomerListState = initializeCustomerListState; window.toggleCompactMode = toggleCompactMode; window.onSelectAllChange = onSelectAllChange;