coming together
This commit is contained in:
@@ -3,6 +3,18 @@ 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) {
|
||||
@@ -18,51 +30,74 @@ function displayCustomers(customers) {
|
||||
|
||||
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 <strong> to styled <mark>
|
||||
const strongHtml = window.highlightUtils.highlight(String(text), tokens);
|
||||
return strongHtml
|
||||
.replace(/<strong>/g, '<mark class="bg-yellow-200 text-neutral-900 rounded px-0.5">')
|
||||
.replace(/<\/strong>/g, '</mark>');
|
||||
}
|
||||
|
||||
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 ? ` <span class="text-xs text-neutral-500">(+${phoneCount - 1} more)</span>` : ''}`;
|
||||
const row = document.createElement('tr');
|
||||
row.className = 'group border-b border-neutral-100 dark:border-neutral-700/50 hover:bg-gradient-to-r hover:from-blue-50/30 hover:to-indigo-50/30 dark:hover:from-blue-900/10 dark:hover:to-indigo-900/10 transition-all duration-200';
|
||||
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 = `
|
||||
<td class="px-4 py-4 cursor-pointer customer-cell">
|
||||
<div class="text-sm font-mono font-semibold text-neutral-900 dark:text-neutral-100 bg-gradient-to-br from-neutral-50 to-neutral-100 dark:from-neutral-800 dark:to-neutral-700 px-3 py-2 rounded-lg shadow-sm border border-neutral-200/50 dark:border-neutral-600/50 group-hover:shadow-md transition-shadow">
|
||||
${escapeHtml(customer.id || '')}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-4 cursor-pointer customer-cell">
|
||||
<td class="${pad} cursor-pointer customer-cell">
|
||||
<div class="text-sm font-mono font-semibold text-neutral-900 dark:text-neutral-100 bg-gradient-to-br from-neutral-50 to-neutral-100 dark:from-neutral-800 dark:to-neutral-700 px-3 py-2 rounded-lg shadow-sm border border-neutral-200/50 dark:border-neutral-600/50 group-hover:shadow-md transition-shadow">
|
||||
${highlightText(customer.id || '')}
|
||||
</div>
|
||||
</td>
|
||||
<td class="${pad} cursor-pointer customer-cell">
|
||||
<div class="text-sm font-semibold text-neutral-900 dark:text-neutral-100 group-hover:text-blue-700 dark:group-hover:text-blue-300 transition-colors">
|
||||
${escapeHtml(formatFullName(customer))}
|
||||
${highlightText(formatFullName(customer))}
|
||||
</div>
|
||||
${customer.title ? `<div class="text-xs text-neutral-500 dark:text-neutral-400 mt-1 font-medium">${escapeHtml(customer.title)}</div>` : ''}
|
||||
${customerCompactMode ? '' : (customer.title ? `<div class="text-xs text-neutral-500 dark:text-neutral-400 mt-1 font-medium">${highlightText(customer.title)}</div>` : '')}
|
||||
</td>
|
||||
<td class="px-4 py-4 cursor-pointer customer-cell">
|
||||
${customer.group ? `<span class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-bold bg-gradient-to-r from-blue-50 to-blue-100 dark:from-blue-900/40 dark:to-blue-800/40 text-blue-800 dark:text-blue-200 border border-blue-200/70 dark:border-blue-700/70 shadow-sm">${escapeHtml(customer.group)}</span>` : '<span class="text-neutral-400 text-sm font-medium">-</span>'}
|
||||
</td>
|
||||
<td class="px-4 py-4 cursor-pointer customer-cell">
|
||||
<td class="${pad} cursor-pointer customer-cell">
|
||||
${customer.group ? `<span class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-bold bg-gradient-to-r from-blue-50 to-blue-100 dark:from-blue-900/40 dark:to-blue-800/40 text-blue-800 dark:text-blue-200 border border-blue-200/70 dark:border-blue-700/70 shadow-sm">${highlightText(customer.group)}</span>` : '<span class="text-neutral-400 text-sm font-medium">-</span>'}
|
||||
</td>
|
||||
<td class="${pad} cursor-pointer customer-cell">
|
||||
<div class="text-sm font-medium text-neutral-900 dark:text-neutral-100">
|
||||
${escapeHtml(formatCityState(customer))}
|
||||
${highlightText(formatCityState(customer))}
|
||||
</div>
|
||||
${customer.a1 ? `<div class="text-xs text-neutral-500 dark:text-neutral-400 mt-1 truncate max-w-xs font-medium">${escapeHtml(customer.a1)}</div>` : ''}
|
||||
${customerCompactMode ? '' : (customer.a1 ? `<div class="text-xs text-neutral-500 dark:text-neutral-400 mt-1 truncate max-w-xs font-medium">${highlightText(customer.a1)}</div>` : '')}
|
||||
</td>
|
||||
<td class="px-4 py-4 cursor-pointer customer-cell">
|
||||
<td class="${pad} cursor-pointer customer-cell">
|
||||
<div class="text-sm font-mono font-medium text-neutral-900 dark:text-neutral-100">
|
||||
${formatPrimaryPhone(customer.phone_numbers || [])}
|
||||
${phoneHtml}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-4 cursor-pointer customer-cell">
|
||||
${customer.email ? `<a href="mailto:${encodeURIComponent(customer.email)}" class="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-200 text-sm font-medium transition-colors underline decoration-blue-300/50 hover:decoration-blue-500" onclick="event.stopPropagation()">${escapeHtml(customer.email)}</a>` : '<span class="text-neutral-400 text-sm font-medium">-</span>'}
|
||||
</td>
|
||||
<td class="px-4 py-4 text-right">
|
||||
<td class="${pad} cursor-pointer customer-cell">
|
||||
${customer.email ? `<a href="mailto:${encodeURIComponent(customer.email)}" class="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-200 text-sm font-medium transition-colors underline decoration-blue-300/50 hover:decoration-blue-500" onclick="event.stopPropagation()">${highlightText(customer.email)}</a>` : '<span class="text-neutral-400 text-sm font-medium">-</span>'}
|
||||
</td>
|
||||
<td class="${pad} text-right">
|
||||
<div class="flex items-center justify-end space-x-2 opacity-70 group-hover:opacity-100 transition-opacity">
|
||||
<button class="view-customer-btn inline-flex items-center px-3 py-2 bg-gradient-to-r from-slate-100 to-slate-200 dark:from-slate-700 dark:to-slate-600 text-slate-700 dark:text-slate-200 hover:from-slate-200 hover:to-slate-300 dark:hover:from-slate-600 dark:hover:to-slate-500 rounded-lg text-sm font-semibold transition-all duration-200 shadow-sm hover:shadow-md border border-slate-300/50 dark:border-slate-500/50">
|
||||
<i class="fa-solid fa-eye mr-2"></i>
|
||||
View
|
||||
</button>
|
||||
<button class="edit-customer-btn" style="display: inline-flex; align-items: center; background-color: #dc2626; color: white; padding: 8px 12px; border-radius: 6px; border: none; font-weight: 600; font-size: 14px;">
|
||||
<i class="fa-solid fa-pencil" style="margin-right: 8px;"></i>
|
||||
<button class="edit-customer-btn inline-flex items-center px-3 py-2 bg-primary-600 text-white hover:bg-primary-700 rounded-lg text-sm font-semibold transition-all duration-200 shadow-sm hover:shadow-md">
|
||||
<i class="fa-solid fa-pencil mr-2"></i>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
@@ -90,6 +125,8 @@ function displayCustomers(customers) {
|
||||
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
|
||||
// No select-all
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
@@ -713,4 +750,132 @@ window.populateEditForm = populateEditForm;
|
||||
window.populatePhoneNumbers = populatePhoneNumbers;
|
||||
window.clearCustomerForm = clearCustomerForm;
|
||||
window.addPhoneNumber = addPhoneNumber;
|
||||
window.removePhoneNumber = removePhoneNumber;
|
||||
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 = `<input type="checkbox" class="customer-row-select h-4 w-4" data-id="${escapeHtml(id)}">`;
|
||||
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;
|
||||
Reference in New Issue
Block a user