// Customer management functionality - Tailwind version let currentPage = 0; let currentSearch = ''; let isEditing = false; let editingCustomerId = null; // Helper function for authenticated API calls function getAuthHeaders() { const token = localStorage.getItem('auth_token'); return { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }; } // 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'); } // 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'); customers.forEach(customer => { const row = document.createElement('tr'); row.className = 'hover:bg-neutral-50 dark:hover:bg-neutral-800/50 transition-colors duration-150'; row.innerHTML = `
${customer.id}
${getInitials(customer)}
${formatName(customer)}
${customer.title ? `
${customer.title}
` : ''}
${customer.group ? `${customer.group}` : '-'} ${formatLocation(customer)} ${formatPhones(customer.phone_numbers)} ${customer.email ? `${customer.email}` : '-'}
`; tbody.appendChild(row); }); } // Helper functions function getInitials(customer) { const first = customer.first || ''; const last = customer.last || ''; return (first.charAt(0) + last.charAt(0)).toUpperCase(); } function formatName(customer) { const parts = []; if (customer.prefix) parts.push(customer.prefix); if (customer.first) parts.push(customer.first); if (customer.middle) parts.push(customer.middle); parts.push(customer.last); if (customer.suffix) parts.push(customer.suffix); return parts.join(' '); } function formatLocation(customer) { const parts = []; if (customer.city) parts.push(customer.city); if (customer.abrev) parts.push(customer.abrev); return parts.join(', ') || '-'; } function formatPhones(phones) { if (!phones || phones.length === 0) return '-'; return phones.map(p => `
${p.location || 'Phone'}: ${p.phone}
` ).join(''); } // Enhanced pagination function updatePagination(currentPage, totalPages) { const pagination = document.getElementById('pagination'); pagination.innerHTML = ''; if (totalPages <= 1) return; // Previous button const prevButton = document.createElement('button'); prevButton.className = `px-3 py-2 text-sm rounded-lg transition-colors duration-200 ${ currentPage === 0 ? 'bg-neutral-100 dark:bg-neutral-800 text-neutral-400 dark:text-neutral-500 cursor-not-allowed' : 'bg-white dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-50 dark:hover:bg-neutral-600 border border-neutral-200 dark:border-neutral-600' }`; prevButton.innerHTML = ''; prevButton.disabled = currentPage === 0; prevButton.onclick = () => currentPage > 0 && loadCustomers(currentPage - 1, currentSearch); pagination.appendChild(prevButton); // Page numbers const startPage = Math.max(0, currentPage - 2); const endPage = Math.min(totalPages - 1, currentPage + 2); for (let i = startPage; i <= endPage; i++) { const pageButton = document.createElement('button'); pageButton.className = `px-3 py-2 text-sm rounded-lg transition-colors duration-200 ${ i === currentPage ? 'bg-primary-600 text-white' : 'bg-white dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-50 dark:hover:bg-neutral-600 border border-neutral-200 dark:border-neutral-600' }`; pageButton.textContent = i + 1; pageButton.onclick = () => loadCustomers(i, currentSearch); pagination.appendChild(pageButton); } // Next button const nextButton = document.createElement('button'); nextButton.className = `px-3 py-2 text-sm rounded-lg transition-colors duration-200 ${ currentPage === totalPages - 1 ? 'bg-neutral-100 dark:bg-neutral-800 text-neutral-400 dark:text-neutral-500 cursor-not-allowed' : 'bg-white dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-50 dark:hover:bg-neutral-600 border border-neutral-200 dark:border-neutral-600' }`; nextButton.innerHTML = ''; nextButton.disabled = currentPage === totalPages - 1; nextButton.onclick = () => currentPage < totalPages - 1 && loadCustomers(currentPage + 1, currentSearch); pagination.appendChild(nextButton); } // Enhanced alert function (delegates to shared alerts utility) function showAlert(message, type = 'info') { if (window.alerts && typeof window.alerts.show === 'function') { window.alerts.show(message, type); return; } // Fallback alert(String(message)); } // Close modal when clicking outside document.addEventListener('click', function(event) { const modal = document.getElementById('customerModal'); if (event.target === modal) { closeCustomerModal(); } }); // Handle escape key for modal document.addEventListener('keydown', function(event) { if (event.key === 'Escape') { closeCustomerModal(); } }); // Make functions globally available for backwards compatibility window.showAddCustomerModal = showAddCustomerModal; window.closeCustomerModal = closeCustomerModal; window.showEditCustomerModal = showEditCustomerModal; window.displayCustomers = displayCustomers; window.showAlert = showAlert; // Form handling functions function clearCustomerForm() { document.getElementById('customerId').value = ''; document.getElementById('customerId').disabled = 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 = ''; document.getElementById('phoneList').innerHTML = ''; window.currentPhones = []; // Track phones with {id, location, phone, action: 'add'|'update'|'delete'|'none'} } async function populateCustomerForm(customer) { document.getElementById('customerId').value = customer.id; document.getElementById('customerId').disabled = true; 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 ? new Date(customer.dob).toISOString().split('T')[0] : ''; document.getElementById('ss_number').value = customer.ss_number || ''; document.getElementById('legal_status').value = customer.legal_status || ''; document.getElementById('memo').value = customer.memo || ''; // Populate phones const phoneList = document.getElementById('phoneList'); phoneList.innerHTML = ''; window.currentPhones = customer.phone_numbers.map(p => ({...p, action: 'none'})); window.currentPhones.forEach((phone, index) => addPhoneRow(index, phone)); } function addPhoneRow(index, phone = {location: '', phone: '', action: 'add'}) { const phoneList = document.getElementById('phoneList'); const row = document.createElement('div'); row.className = 'flex items-end gap-4 mb-2'; row.dataset.index = index; row.innerHTML = `
`; phoneList.appendChild(row); // Event listeners for changes row.querySelector('.phone-location').addEventListener('input', () => updatePhone(index)); row.querySelector('.phone-number').addEventListener('input', () => updatePhone(index)); row.querySelector('.remove-phone').addEventListener('click', () => removePhone(index)); } function updatePhone(index) { const row = document.querySelector(`[data-index="${index}"]`); const location = row.querySelector('.phone-location').value; const phone = row.querySelector('.phone-number').value; const current = window.currentPhones[index]; if (current) { current.location = location; current.phone = phone; if (current.action === 'none') current.action = 'update'; } } function removePhone(index) { const row = document.querySelector(`[data-index="${index}"]`); row.remove(); const phone = window.currentPhones[index]; if (phone.id) { phone.action = 'delete'; } else { window.currentPhones.splice(index, 1); } // Re-index rows document.querySelectorAll('#phoneList > div').forEach((r, i) => { r.dataset.index = i; }); } document.getElementById('addPhoneBtn').addEventListener('click', () => { const index = window.currentPhones.length; window.currentPhones.push({location: '', phone: '', action: 'add'}); addPhoneRow(index); }); // Validation async function validateForm() { let isValid = true; const requiredFields = ['customerId', 'last']; requiredFields.forEach(id => { const input = document.getElementById(id); if (!input.value.trim()) { isValid = false; input.classList.add('border-danger-500'); } else { input.classList.remove('border-danger-500'); } }); // Email validation const email = document.getElementById('email'); if (email.value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value)) { isValid = false; email.classList.add('border-danger-500'); } else { email.classList.remove('border-danger-500'); } // Check unique ID for create if (!isEditing) { try { const response = await fetch(`/api/customers/${document.getElementById('customerId').value}`); if (response.ok) { isValid = false; showAlert('Customer ID already exists', 'danger'); document.getElementById('customerId').classList.add('border-danger-500'); } } catch (error) {} } return isValid; } // Save customer async function saveCustomer() { if (!await validateForm()) return; const customerData = { id: document.getElementById('customerId').value, last: document.getElementById('last').value, first: document.getElementById('first').value, middle: document.getElementById('middle').value, prefix: document.getElementById('prefix').value, suffix: document.getElementById('suffix').value, title: document.getElementById('title').value, group: document.getElementById('group').value, a1: document.getElementById('a1').value, a2: document.getElementById('a2').value, a3: document.getElementById('a3').value, city: document.getElementById('city').value, abrev: document.getElementById('abrev').value, zip: document.getElementById('zip').value, email: document.getElementById('email').value, dob: document.getElementById('dob').value || null, ss_number: document.getElementById('ss_number').value, legal_status: document.getElementById('legal_status').value, memo: document.getElementById('memo').value }; try { let response; if (isEditing) { response = await fetch(`/api/customers/${editingCustomerId}`, { method: 'PUT', headers: getAuthHeaders(), body: JSON.stringify(customerData) }); } else { response = await fetch('/api/customers/', { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify(customerData) }); } if (!response.ok) { const error = await response.json(); throw new Error(error.detail || 'Failed to save customer'); } const savedCustomer = await response.json(); // Handle phones for (const phone of window.currentPhones) { if (phone.action === 'add') { await fetch(`/api/customers/${savedCustomer.id}/phones`, { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify({location: phone.location, phone: phone.phone}) }); } else if (phone.action === 'update') { await fetch(`/api/customers/${savedCustomer.id}/phones/${phone.id}`, { method: 'PUT', headers: getAuthHeaders(), body: JSON.stringify({location: phone.location, phone: phone.phone}) }); } else if (phone.action === 'delete') { await fetch(`/api/customers/${savedCustomer.id}/phones/${phone.id}`, { method: 'DELETE', headers: getAuthHeaders() }); } } showAlert(`Customer ${isEditing ? 'updated' : 'created'} successfully`, 'success'); closeCustomerModal(); loadCustomers(currentPage, currentSearch); } catch (error) { showAlert(`Error saving customer: ${error.message}`, 'danger'); } } // Edit customer async function editCustomer(customerId) { try { const response = await fetch(`/api/customers/${customerId}`, { headers: getAuthHeaders() }); if (!response.ok) throw new Error('Failed to load customer'); const customer = await response.json(); isEditing = true; editingCustomerId = customerId; populateCustomerForm(customer); showEditCustomerModal(); } catch (error) { showAlert(`Error loading customer: ${error.message}`, 'danger'); } } // Delete customer async function deleteCustomer() { if (!confirm('Are you sure you want to delete this customer?')) return; try { const response = await fetch(`/api/customers/${editingCustomerId}`, { method: 'DELETE', headers: getAuthHeaders() }); if (!response.ok) throw new Error('Failed to delete customer'); showAlert('Customer deleted successfully', 'success'); closeCustomerModal(); loadCustomers(currentPage, currentSearch); } catch (error) { showAlert(`Error deleting customer: ${error.message}`, 'danger'); } } // Populate datalists async function populateDatalists() { try { const groupsResp = await fetch('/api/customers/groups', {headers: getAuthHeaders()}); const groups = await groupsResp.json(); const groupList = document.getElementById('groupList'); groups.forEach(g => { const option = document.createElement('option'); option.value = g.group; groupList.appendChild(option); }); const statesResp = await fetch('/api/customers/states', {headers: getAuthHeaders()}); const states = await statesResp.json(); const stateList = document.getElementById('stateList'); states.forEach(s => { const option = document.createElement('option'); option.value = s.state; stateList.appendChild(option); }); } catch (error) { console.error('Error loading datalists:', error); } } // Update setupEventListeners function setupEventListeners() { // Existing listeners... document.getElementById('searchBtn').addEventListener('click', performSearch); document.getElementById('searchInput').addEventListener('keypress', function(e) { if (e.key === 'Enter') performSearch(); }); document.getElementById('phoneSearchBtn').addEventListener('click', performPhoneSearch); document.getElementById('phoneSearch').addEventListener('keypress', function(e) { if (e.key === 'Enter') performPhoneSearch(); }); document.getElementById('addCustomerBtn').addEventListener('click', showAddCustomerModal); document.getElementById('saveCustomerBtn').addEventListener('click', saveCustomer); document.getElementById('deleteCustomerBtn').addEventListener('click', deleteCustomer); document.getElementById('statsBtn').addEventListener('click', showStats); // Form validation on blur for customerId document.getElementById('customerId').addEventListener('blur', async function() { if (!isEditing && this.value) { try { const response = await fetch(`/api/customers/${this.value}`); if (response.ok) { showAlert('Customer ID already exists', 'warning'); this.classList.add('border-danger-500'); } else { this.classList.remove('border-danger-500'); } } catch (error) {} } }); } // Update DOMContentLoaded document.addEventListener('DOMContentLoaded', function() { const token = localStorage.getItem('auth_token'); if (!token) { window.location.href = '/login'; return; } loadCustomers(); loadGroups(); loadStates(); populateDatalists(); setupEventListeners(); clearCustomerForm(); // Initial clear });