fix(pos): rewrite customers.js to match design system HTML structure
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,10 +2,12 @@
|
||||
/**
|
||||
* Customers management frontend.
|
||||
* Communicates with /pos/api/customers (customers_bp).
|
||||
* Wired to the design-system HTML (customers.html).
|
||||
*/
|
||||
const Customers = (() => {
|
||||
let token = localStorage.getItem('pos_token') || '';
|
||||
let currentPage = 1;
|
||||
let totalPages = 1;
|
||||
let currentCustomer = null;
|
||||
let searchTimeout = null;
|
||||
|
||||
@@ -13,6 +15,13 @@ const Customers = (() => {
|
||||
minimumFractionDigits: 2, maximumFractionDigits: 2
|
||||
});
|
||||
|
||||
const fmtShort = (n) => {
|
||||
n = parseFloat(n || 0);
|
||||
if (n >= 1000000) return '$' + (n / 1000000).toFixed(1) + 'M';
|
||||
if (n >= 1000) return '$' + (n / 1000).toFixed(0) + 'K';
|
||||
return '$' + n.toFixed(0);
|
||||
};
|
||||
|
||||
function headers() {
|
||||
return { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token };
|
||||
}
|
||||
@@ -20,23 +29,66 @@ const Customers = (() => {
|
||||
async function api(url, options = {}) {
|
||||
options.headers = headers();
|
||||
const res = await fetch(url, options);
|
||||
if (res.status === 401) {
|
||||
window.location.href = '/pos/login';
|
||||
return;
|
||||
}
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
|
||||
return data;
|
||||
}
|
||||
|
||||
// ─── Tier / Status helpers ───────────
|
||||
const tierMap = { 1: 'Mostrador', 2: 'Taller', 3: 'Mayoreo' };
|
||||
const tierClass = { 1: 'mostrador', 2: 'taller', 3: 'mayoreo' };
|
||||
|
||||
function statusBadge(c) {
|
||||
// Derive status: if credit_balance > credit_limit => Mora, else Activo
|
||||
if (c.credit_balance > 0 && c.credit_limit > 0 && c.credit_balance > c.credit_limit) {
|
||||
return '<span class="badge badge--warning"><span class="badge-dot"></span>Mora</span>';
|
||||
}
|
||||
return '<span class="badge badge--active"><span class="badge-dot"></span>Activo</span>';
|
||||
}
|
||||
|
||||
function getInitials(name) {
|
||||
if (!name) return '--';
|
||||
const parts = name.trim().split(/\s+/);
|
||||
if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase();
|
||||
return name.substring(0, 2).toUpperCase();
|
||||
}
|
||||
|
||||
function formatDate(d) {
|
||||
if (!d) return '-';
|
||||
try {
|
||||
return new Date(d).toLocaleDateString('es-MX', { day: '2-digit', month: 'short', year: 'numeric' });
|
||||
} catch (_) { return d; }
|
||||
}
|
||||
|
||||
// ─── Summary Cards ──────────────────
|
||||
async function loadSummary() {
|
||||
try {
|
||||
// Load a small page just to get pagination totals
|
||||
const data = await api('/pos/api/customers?page=1&per_page=1');
|
||||
const totalClientes = data.pagination ? data.pagination.total : 0;
|
||||
|
||||
const cards = document.querySelectorAll('.summary-card__value');
|
||||
if (cards.length >= 1) cards[0].textContent = totalClientes.toLocaleString('es-MX');
|
||||
} catch (_) { /* non-critical */ }
|
||||
}
|
||||
|
||||
// ─── List ────────────────────────────
|
||||
async function loadCustomers(page, q) {
|
||||
page = page || currentPage;
|
||||
q = q !== undefined ? q : (document.getElementById('searchInput').value || '');
|
||||
const searchEl = document.getElementById('searchInput');
|
||||
q = q !== undefined ? q : (searchEl ? searchEl.value || '' : '');
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams({ page, per_page: 50 });
|
||||
if (q) params.append('q', q);
|
||||
|
||||
const data = await api(`/pos/api/customers?${params}`);
|
||||
renderTable(data.data);
|
||||
renderPagination(data.pagination);
|
||||
renderTable(data.data || []);
|
||||
renderPagination(data.pagination || {});
|
||||
} catch (e) {
|
||||
console.error('Load customers failed:', e);
|
||||
}
|
||||
@@ -44,53 +96,90 @@ const Customers = (() => {
|
||||
|
||||
function renderTable(customers) {
|
||||
const tbody = document.getElementById('customersBody');
|
||||
const tiers = { 1: ['Mostrador', 'tier-1'], 2: ['Taller', 'tier-2'], 3: ['Mayoreo', 'tier-3'] };
|
||||
if (!tbody) return;
|
||||
tbody.innerHTML = '';
|
||||
|
||||
let html = '';
|
||||
customers.forEach(c => {
|
||||
const [tierName, tierClass] = tiers[c.price_tier] || ['P1', 'tier-1'];
|
||||
const limit = c.credit_limit || 0;
|
||||
const balance = c.credit_balance || 0;
|
||||
const usagePct = limit > 0 ? Math.min(100, (balance / limit) * 100) : 0;
|
||||
const fillClass = usagePct > 90 ? 'danger' : usagePct > 70 ? 'warning' : '';
|
||||
|
||||
html += `<tr onclick="Customers.showDetail(${c.id})">
|
||||
<td><strong>${c.name}</strong></td>
|
||||
<td>${c.rfc || '-'}</td>
|
||||
<td>${c.phone || '-'}</td>
|
||||
<td><span class="tier-badge ${tierClass}">${tierName}</span></td>
|
||||
<td>${fmt(limit)}
|
||||
${limit > 0 ? `<div class="credit-bar"><div class="credit-fill ${fillClass}" style="width:${usagePct}%"></div></div>` : ''}
|
||||
</td>
|
||||
<td>${balance > 0 ? fmt(balance) : '-'}</td>
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
if (customers.length === 0) {
|
||||
html = '<tr><td colspan="6" style="text-align:center;color:#999;padding:20px;">Sin resultados</td></tr>';
|
||||
if (!customers || customers.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="9" style="text-align:center;padding:var(--space-8);color:var(--color-text-muted);">Sin resultados.</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = html;
|
||||
customers.forEach((c, idx) => {
|
||||
const tier = tierMap[c.price_tier] || 'Mostrador';
|
||||
const tClass = tierClass[c.price_tier] || 'mostrador';
|
||||
const limit = parseFloat(c.credit_limit || 0);
|
||||
const balance = parseFloat(c.credit_balance || 0);
|
||||
const available = Math.max(0, limit - balance);
|
||||
const usedPct = limit > 0 ? Math.round((balance / limit) * 100) : 0;
|
||||
const creditClass = usedPct >= 80 ? 'none' : usedPct >= 60 ? 'low' : '';
|
||||
const num = String(c.id).padStart(5, '0');
|
||||
|
||||
const tr = document.createElement('tr');
|
||||
if (currentCustomer && currentCustomer.id === c.id) tr.className = 'selected';
|
||||
tr.onclick = () => selectCustomer(c.id);
|
||||
tr.innerHTML = `
|
||||
<td class="cell-num">${num}</td>
|
||||
<td>
|
||||
<div class="cell-name">${c.name || ''}</div>
|
||||
<div class="cell-name-sub hide-mobile">${c.email || ''}</div>
|
||||
</td>
|
||||
<td class="cell-rfc hide-mobile">${c.rfc || '-'}</td>
|
||||
<td class="hide-mobile">${c.phone || '-'}</td>
|
||||
<td class="hide-mobile" style="font-size:var(--text-caption);color:var(--color-text-secondary);">${c.email || '-'}</td>
|
||||
<td><span class="tipo-chip tipo-chip--${tClass}">${tier}</span></td>
|
||||
<td class="cell-credit ${creditClass}">${fmt(available)}</td>
|
||||
<td class="cell-date hide-mobile">${formatDate(c.last_purchase || c.created_at)}</td>
|
||||
<td>${statusBadge(c)}</td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
function renderPagination(pag) {
|
||||
const container = document.getElementById('pagination');
|
||||
if (pag.total_pages <= 1) { container.innerHTML = ''; return; }
|
||||
const container = document.querySelector('.pagination');
|
||||
const info = document.getElementById('tableInfo');
|
||||
|
||||
if (!pag || !pag.total_pages) {
|
||||
if (container) container.innerHTML = '';
|
||||
if (info) info.textContent = '';
|
||||
return;
|
||||
}
|
||||
|
||||
totalPages = pag.total_pages;
|
||||
currentPage = pag.page;
|
||||
const total = pag.total || 0;
|
||||
const perPage = pag.per_page || 50;
|
||||
const start = (pag.page - 1) * perPage + 1;
|
||||
const end = Math.min(pag.page * perPage, total);
|
||||
|
||||
if (info) info.textContent = `Mostrando ${start}–${end} de ${total.toLocaleString('es-MX')} clientes`;
|
||||
|
||||
if (!container) return;
|
||||
if (totalPages <= 1) { container.innerHTML = ''; return; }
|
||||
|
||||
let html = '';
|
||||
if (pag.page > 1) {
|
||||
html += `<button class="btn btn-secondary" onclick="Customers.goToPage(${pag.page - 1})">Anterior</button>`;
|
||||
html += `<button class="page-btn" ${pag.page <= 1 ? 'disabled' : ''} onclick="Customers.goToPage(${pag.page - 1})">‹</button>`;
|
||||
|
||||
const maxButtons = 7;
|
||||
let startP = Math.max(1, pag.page - 3);
|
||||
let endP = Math.min(totalPages, startP + maxButtons - 1);
|
||||
if (endP - startP < maxButtons - 1) startP = Math.max(1, endP - maxButtons + 1);
|
||||
|
||||
for (let i = startP; i <= endP; i++) {
|
||||
html += `<button class="page-btn ${i === pag.page ? 'active' : ''}" onclick="Customers.goToPage(${i})">${i}</button>`;
|
||||
}
|
||||
for (let i = 1; i <= Math.min(pag.total_pages, 10); i++) {
|
||||
html += `<button class="btn ${i === pag.page ? 'active' : 'btn-secondary'}" onclick="Customers.goToPage(${i})">${i}</button>`;
|
||||
}
|
||||
if (pag.page < pag.total_pages) {
|
||||
html += `<button class="btn btn-secondary" onclick="Customers.goToPage(${pag.page + 1})">Siguiente</button>`;
|
||||
|
||||
if (endP < totalPages) {
|
||||
html += '<span style="color:var(--color-text-muted);font-size:var(--text-caption);padding:0 4px;">...</span>';
|
||||
html += `<button class="page-btn" onclick="Customers.goToPage(${totalPages})">${totalPages}</button>`;
|
||||
}
|
||||
|
||||
html += `<button class="page-btn" ${pag.page >= totalPages ? 'disabled' : ''} onclick="Customers.goToPage(${pag.page + 1})">›</button>`;
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
function goToPage(page) {
|
||||
if (page < 1 || page > totalPages) return;
|
||||
currentPage = page;
|
||||
loadCustomers(page);
|
||||
}
|
||||
@@ -103,76 +192,182 @@ const Customers = (() => {
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// ─── Detail ──────────────────────────
|
||||
async function showDetail(id) {
|
||||
// Alias for inline HTML oninput="filterCustomers()"
|
||||
window.filterCustomers = function() { search(); };
|
||||
|
||||
// ─── Detail Panel (right side) ──────
|
||||
async function selectCustomer(id) {
|
||||
try {
|
||||
const c = await api(`/pos/api/customers/${id}`);
|
||||
currentCustomer = c;
|
||||
|
||||
document.getElementById('detailName').textContent = c.name;
|
||||
// Toggle empty/content
|
||||
const emptyEl = document.getElementById('detailEmpty');
|
||||
const contentEl = document.getElementById('detailContent');
|
||||
if (emptyEl) emptyEl.style.display = 'none';
|
||||
if (contentEl) contentEl.style.display = 'flex';
|
||||
|
||||
// Credit
|
||||
const available = (c.credit_limit || 0) - (c.credit_balance || 0);
|
||||
document.getElementById('detailCreditAvailable').textContent = fmt(available);
|
||||
document.getElementById('detailCreditLimit').textContent = fmt(c.credit_limit);
|
||||
document.getElementById('detailCreditBalance').textContent = fmt(c.credit_balance);
|
||||
// Avatar & Header
|
||||
const avatarEl = document.getElementById('detailAvatar');
|
||||
if (avatarEl) avatarEl.textContent = getInitials(c.name);
|
||||
|
||||
// Fiscal
|
||||
let fiscalHtml = '';
|
||||
fiscalHtml += `<div class="detail-field"><span class="label">RFC</span><span>${c.rfc || '-'}</span></div>`;
|
||||
fiscalHtml += `<div class="detail-field"><span class="label">Razon Social</span><span>${c.razon_social || '-'}</span></div>`;
|
||||
fiscalHtml += `<div class="detail-field"><span class="label">Regimen</span><span>${c.regimen_fiscal || '-'}</span></div>`;
|
||||
fiscalHtml += `<div class="detail-field"><span class="label">Uso CFDI</span><span>${c.uso_cfdi || '-'}</span></div>`;
|
||||
fiscalHtml += `<div class="detail-field"><span class="label">CP</span><span>${c.cp || '-'}</span></div>`;
|
||||
document.getElementById('detailFiscal').innerHTML = fiscalHtml;
|
||||
const nameEl = document.getElementById('detailName');
|
||||
if (nameEl) nameEl.textContent = (c.name || '').toUpperCase();
|
||||
|
||||
const rfcEl = document.getElementById('detailRFC');
|
||||
if (rfcEl) rfcEl.textContent = c.rfc || '-';
|
||||
|
||||
// Tipo chip
|
||||
const tipoEl = document.getElementById('detailTipo');
|
||||
if (tipoEl) {
|
||||
const tier = tierMap[c.price_tier] || 'Mostrador';
|
||||
const tClass = tierClass[c.price_tier] || 'mostrador';
|
||||
tipoEl.textContent = tier;
|
||||
tipoEl.className = `tipo-chip tipo-chip--${tClass}`;
|
||||
}
|
||||
|
||||
// Status badge
|
||||
const statEl = document.getElementById('detailStatus');
|
||||
if (statEl) statEl.innerHTML = statusBadge(c);
|
||||
|
||||
// Contact
|
||||
let contactHtml = '';
|
||||
contactHtml += `<div class="detail-field"><span class="label">Telefono</span><span>${c.phone || '-'}</span></div>`;
|
||||
contactHtml += `<div class="detail-field"><span class="label">Email</span><span>${c.email || '-'}</span></div>`;
|
||||
contactHtml += `<div class="detail-field"><span class="label">Direccion</span><span>${c.address || '-'}</span></div>`;
|
||||
document.getElementById('detailContact').innerHTML = contactHtml;
|
||||
const set = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val || '-'; };
|
||||
set('detailAddress', c.address);
|
||||
set('detailPhone', c.phone);
|
||||
set('detailEmail', c.email);
|
||||
set('detailSince', formatDate(c.created_at));
|
||||
set('detailLastPurchase', formatDate(c.last_purchase));
|
||||
|
||||
// Vehicles
|
||||
// Credit
|
||||
const limit = parseFloat(c.credit_limit || 0);
|
||||
const balance = parseFloat(c.credit_balance || 0);
|
||||
const available = Math.max(0, limit - balance);
|
||||
const pct = limit > 0 ? Math.round((balance / limit) * 100) : 0;
|
||||
|
||||
set('detailCreditLimit', fmt(limit));
|
||||
set('detailCreditAvail', fmt(available));
|
||||
set('detailCreditUsed', fmt(balance));
|
||||
set('detailCreditPct', pct + '%');
|
||||
|
||||
const bar = document.getElementById('detailCreditBar');
|
||||
if (bar) {
|
||||
bar.style.width = pct + '%';
|
||||
bar.className = `progress-bar__fill ${pct < 40 ? 'low' : pct > 75 ? 'high' : ''}`;
|
||||
}
|
||||
|
||||
// Purchase History
|
||||
const hbody = document.getElementById('historyBody');
|
||||
if (hbody) {
|
||||
const purchases = c.recent_purchases || [];
|
||||
if (purchases.length === 0) {
|
||||
hbody.innerHTML = '<tr><td colspan="4" style="text-align:center;color:var(--color-text-muted);padding:var(--space-4);">Sin compras recientes</td></tr>';
|
||||
} else {
|
||||
hbody.innerHTML = '';
|
||||
purchases.forEach(p => {
|
||||
const statusClass = p.status === 'paid' ? 'mbadge--paid' : p.status === 'overdue' ? 'mbadge--overdue' : 'mbadge--pending';
|
||||
const statusLabel = p.status === 'paid' ? 'Pagado' : p.status === 'overdue' ? 'Vencido' : 'Pendiente';
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td class="date">${formatDate(p.created_at)}</td>
|
||||
<td class="folio">NX-${String(p.id).padStart(5, '0')}</td>
|
||||
<td class="total">${fmt(p.total)}</td>
|
||||
<td><span class="mbadge ${statusClass}">${statusLabel}</span></td>
|
||||
`;
|
||||
hbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Also populate slide panel fields
|
||||
populateSlidePanel(c);
|
||||
|
||||
// Re-render table to highlight selected row
|
||||
loadCustomers(currentPage);
|
||||
} catch (e) {
|
||||
console.error('Error loading customer:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Populate the slide-panel (mobile/alt detail view)
|
||||
function populateSlidePanel(c) {
|
||||
const set = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val || '--'; };
|
||||
|
||||
set('panelAvatar', getInitials(c.name));
|
||||
set('panelName', c.name);
|
||||
set('panelRfc', 'RFC: ' + (c.rfc || '-'));
|
||||
|
||||
const limit = parseFloat(c.credit_limit || 0);
|
||||
const balance = parseFloat(c.credit_balance || 0);
|
||||
const available = Math.max(0, limit - balance);
|
||||
const pct = limit > 0 ? Math.round((balance / limit) * 100) : 0;
|
||||
|
||||
set('panelCreditLimit', fmt(limit));
|
||||
set('panelCreditUsed', fmt(balance));
|
||||
set('panelCreditAvail', fmt(available));
|
||||
|
||||
const panelBar = document.getElementById('panelCreditBar');
|
||||
if (panelBar) panelBar.style.width = pct + '%';
|
||||
|
||||
// Vehicles
|
||||
const vehiclesEl = document.getElementById('panelVehicles');
|
||||
if (vehiclesEl) {
|
||||
const vehicles = c.vehicle_info || [];
|
||||
if (vehicles.length === 0) {
|
||||
document.getElementById('detailVehicles').innerHTML = '<div style="color:#999;font-size:13px;">Sin vehiculos registrados</div>';
|
||||
vehiclesEl.innerHTML = '<span style="color:var(--color-text-muted);font-size:var(--text-body-sm);">Sin vehiculos registrados</span>';
|
||||
} else {
|
||||
document.getElementById('detailVehicles').innerHTML = vehicles.map(v =>
|
||||
`<div class="vehicle-card">
|
||||
vehiclesEl.innerHTML = vehicles.map(v =>
|
||||
`<div style="padding:4px 0;border-bottom:1px solid var(--color-border);">
|
||||
<strong>${v.make || ''} ${v.model || ''} ${v.year || ''}</strong>
|
||||
${v.plates ? `<span style="margin-left:8px;color:#666;">Placas: ${v.plates}</span>` : ''}
|
||||
${v.vin ? `<div style="font-size:11px;color:#999;">VIN: ${v.vin}</div>` : ''}
|
||||
${v.plates ? ` <span style="color:var(--color-text-muted);">Placas: ${v.plates}</span>` : ''}
|
||||
</div>`
|
||||
).join('');
|
||||
}
|
||||
}
|
||||
|
||||
// Recent purchases
|
||||
// Purchases
|
||||
const purchasesEl = document.getElementById('panelPurchases');
|
||||
if (purchasesEl) {
|
||||
const purchases = c.recent_purchases || [];
|
||||
if (purchases.length === 0) {
|
||||
document.getElementById('detailPurchases').innerHTML = '<div style="color:#999;font-size:13px;">Sin compras recientes</div>';
|
||||
purchasesEl.innerHTML = '<span style="color:var(--color-text-muted);font-size:var(--text-body-sm);">Sin compras recientes</span>';
|
||||
} else {
|
||||
document.getElementById('detailPurchases').innerHTML = purchases.map(p =>
|
||||
`<div class="purchase-item">
|
||||
<span>Venta #${p.id} - ${new Date(p.created_at).toLocaleDateString('es-MX')}</span>
|
||||
purchasesEl.innerHTML = purchases.slice(0, 5).map(p =>
|
||||
`<div style="display:flex;justify-content:space-between;padding:4px 0;border-bottom:1px solid var(--color-border);font-size:var(--text-body-sm);">
|
||||
<span>NX-${String(p.id).padStart(5, '0')} — ${formatDate(p.created_at)}</span>
|
||||
<span>${fmt(p.total)}</span>
|
||||
</div>`
|
||||
).join('');
|
||||
}
|
||||
|
||||
document.getElementById('detailPanel').classList.add('active');
|
||||
} catch (e) {
|
||||
alert('Error: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function closeDetail() {
|
||||
document.getElementById('detailPanel').classList.remove('active');
|
||||
currentCustomer = null;
|
||||
const emptyEl = document.getElementById('detailEmpty');
|
||||
const contentEl = document.getElementById('detailContent');
|
||||
if (emptyEl) emptyEl.style.display = 'flex';
|
||||
if (contentEl) contentEl.style.display = 'none';
|
||||
loadCustomers(currentPage);
|
||||
}
|
||||
|
||||
// Wire action buttons in detail panel
|
||||
function wireActionButtons() {
|
||||
const btns = document.querySelectorAll('.quick-actions .action-btn');
|
||||
// Order: Nueva Venta, Editar, Estado de Cuenta, Historial
|
||||
if (btns.length >= 1) btns[0].onclick = () => {
|
||||
if (currentCustomer) window.location.href = '/pos/?customer=' + currentCustomer.id;
|
||||
};
|
||||
if (btns.length >= 2) btns[1].onclick = () => editCurrent();
|
||||
if (btns.length >= 3) btns[2].onclick = () => showStatement();
|
||||
if (btns.length >= 4) btns[3].onclick = () => {
|
||||
if (currentCustomer) selectCustomer(currentCustomer.id);
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Create/Edit Modal ───────────────
|
||||
function showCreateModal() {
|
||||
const modal = document.getElementById('customerModal');
|
||||
if (!modal) return;
|
||||
document.getElementById('modalTitle').textContent = 'Nuevo Cliente';
|
||||
document.getElementById('editId').value = '';
|
||||
document.getElementById('fName').value = '';
|
||||
@@ -186,13 +381,15 @@ const Customers = (() => {
|
||||
document.getElementById('fAddress').value = '';
|
||||
document.getElementById('fPriceTier').value = '1';
|
||||
document.getElementById('fCreditLimit').value = '0';
|
||||
document.getElementById('customerModal').classList.add('active');
|
||||
modal.classList.add('active');
|
||||
document.getElementById('fName').focus();
|
||||
}
|
||||
|
||||
function editCurrent() {
|
||||
if (!currentCustomer) return;
|
||||
const c = currentCustomer;
|
||||
const modal = document.getElementById('customerModal');
|
||||
if (!modal) return;
|
||||
document.getElementById('modalTitle').textContent = 'Editar Cliente';
|
||||
document.getElementById('editId').value = c.id;
|
||||
document.getElementById('fName').value = c.name || '';
|
||||
@@ -206,32 +403,36 @@ const Customers = (() => {
|
||||
document.getElementById('fAddress').value = c.address || '';
|
||||
document.getElementById('fPriceTier').value = c.price_tier || '1';
|
||||
document.getElementById('fCreditLimit').value = c.credit_limit || 0;
|
||||
document.getElementById('customerModal').classList.add('active');
|
||||
modal.classList.add('active');
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('customerModal').classList.remove('active');
|
||||
const modal = document.getElementById('customerModal');
|
||||
if (modal) modal.classList.remove('active');
|
||||
}
|
||||
|
||||
async function save() {
|
||||
const name = document.getElementById('fName').value.trim();
|
||||
const nameEl = document.getElementById('fName');
|
||||
const name = nameEl ? nameEl.value.trim() : '';
|
||||
if (!name) { alert('Nombre es requerido'); return; }
|
||||
|
||||
const val = (id) => { const el = document.getElementById(id); return el ? el.value.trim() : ''; };
|
||||
|
||||
const body = {
|
||||
name: name,
|
||||
rfc: document.getElementById('fRfc').value.trim() || null,
|
||||
razon_social: document.getElementById('fRazonSocial').value.trim() || null,
|
||||
regimen_fiscal: document.getElementById('fRegimenFiscal').value || null,
|
||||
uso_cfdi: document.getElementById('fUsoCfdi').value || 'G03',
|
||||
cp: document.getElementById('fCp').value.trim() || null,
|
||||
phone: document.getElementById('fPhone').value.trim() || null,
|
||||
email: document.getElementById('fEmail').value.trim() || null,
|
||||
address: document.getElementById('fAddress').value.trim() || null,
|
||||
price_tier: parseInt(document.getElementById('fPriceTier').value) || 1,
|
||||
credit_limit: parseFloat(document.getElementById('fCreditLimit').value) || 0,
|
||||
rfc: val('fRfc') || null,
|
||||
razon_social: val('fRazonSocial') || null,
|
||||
regimen_fiscal: val('fRegimenFiscal') || null,
|
||||
uso_cfdi: val('fUsoCfdi') || 'G03',
|
||||
cp: val('fCp') || null,
|
||||
phone: val('fPhone') || null,
|
||||
email: val('fEmail') || null,
|
||||
address: val('fAddress') || null,
|
||||
price_tier: parseInt(val('fPriceTier')) || 1,
|
||||
credit_limit: parseFloat(val('fCreditLimit')) || 0,
|
||||
};
|
||||
|
||||
const editId = document.getElementById('editId').value;
|
||||
const editId = val('editId');
|
||||
|
||||
try {
|
||||
if (editId) {
|
||||
@@ -249,37 +450,43 @@ const Customers = (() => {
|
||||
closeModal();
|
||||
loadCustomers();
|
||||
if (editId && currentCustomer) {
|
||||
showDetail(editId);
|
||||
selectCustomer(editId);
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Error: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Statement ───────────────────────
|
||||
// ─── Statement Modal ─────────────────
|
||||
async function showStatement() {
|
||||
if (!currentCustomer) return;
|
||||
document.getElementById('statementName').textContent = currentCustomer.name;
|
||||
const modal = document.getElementById('statementModal');
|
||||
if (!modal) return;
|
||||
const nameEl = document.getElementById('statementName');
|
||||
if (nameEl) nameEl.textContent = currentCustomer.name;
|
||||
|
||||
const content = document.getElementById('statementContent');
|
||||
if (content) content.innerHTML = '<div style="text-align:center;padding:20px;color:var(--color-text-muted);">Cargando...</div>';
|
||||
modal.classList.add('active');
|
||||
|
||||
try {
|
||||
const data = await api(`/pos/api/customers/${currentCustomer.id}/statement`);
|
||||
|
||||
let html = `<div style="margin-bottom:12px;font-size:14px;">
|
||||
<strong>Saldo actual: ${fmt(data.balance)}</strong> |
|
||||
Limite: ${fmt(data.customer.credit_limit)}
|
||||
Limite: ${fmt(data.customer ? data.customer.credit_limit : 0)}
|
||||
</div>`;
|
||||
|
||||
if (data.entries.length === 0) {
|
||||
html += '<div style="color:#999;padding:20px;text-align:center;">Sin movimientos</div>';
|
||||
if (!data.entries || data.entries.length === 0) {
|
||||
html += '<div style="color:var(--color-text-muted);padding:20px;text-align:center;">Sin movimientos</div>';
|
||||
} else {
|
||||
html += '<table style="width:100%;border-collapse:collapse;font-size:13px;">';
|
||||
html += '<tr style="background:#f5f5f5;"><th style="padding:8px;text-align:left;">Fecha</th><th style="text-align:left;">Concepto</th><th style="text-align:right;padding:8px;">Cargo</th><th style="text-align:right;padding:8px;">Abono</th><th style="text-align:right;padding:8px;">Saldo</th></tr>';
|
||||
html += '<tr style="background:var(--color-surface-2);"><th style="padding:8px;text-align:left;">Fecha</th><th style="text-align:left;">Concepto</th><th style="text-align:right;padding:8px;">Cargo</th><th style="text-align:right;padding:8px;">Abono</th><th style="text-align:right;padding:8px;">Saldo</th></tr>';
|
||||
|
||||
data.entries.forEach(e => {
|
||||
const dateStr = new Date(e.date).toLocaleDateString('es-MX');
|
||||
html += `<tr style="border-bottom:1px solid #eee;">
|
||||
<td style="padding:6px 8px;">${dateStr}</td>
|
||||
<td>${e.description}</td>
|
||||
html += `<tr style="border-bottom:1px solid var(--color-border);">
|
||||
<td style="padding:6px 8px;">${formatDate(e.date)}</td>
|
||||
<td>${e.description || ''}</td>
|
||||
<td style="text-align:right;padding:6px 8px;">${e.type === 'charge' ? fmt(e.amount) : ''}</td>
|
||||
<td style="text-align:right;padding:6px 8px;">${e.type === 'payment' ? fmt(e.amount) : ''}</td>
|
||||
<td style="text-align:right;padding:6px 8px;">${fmt(e.running_balance)}</td>
|
||||
@@ -288,20 +495,280 @@ const Customers = (() => {
|
||||
html += '</table>';
|
||||
}
|
||||
|
||||
document.getElementById('statementContent').innerHTML = html;
|
||||
document.getElementById('statementModal').classList.add('active');
|
||||
if (content) content.innerHTML = html;
|
||||
} catch (e) {
|
||||
if (content) content.innerHTML = `<div style="color:var(--color-error);padding:20px;text-align:center;">Error: ${e.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function closeStatement() {
|
||||
const modal = document.getElementById('statementModal');
|
||||
if (modal) modal.classList.remove('active');
|
||||
}
|
||||
|
||||
// ─── Payment Modal ───────────────────
|
||||
function showPaymentModal() {
|
||||
if (!currentCustomer) return;
|
||||
const modal = document.getElementById('paymentModal');
|
||||
if (!modal) return;
|
||||
document.getElementById('paymentCustomerName').textContent = currentCustomer.name;
|
||||
document.getElementById('paymentAmount').value = '';
|
||||
document.getElementById('paymentMethod').value = 'cash';
|
||||
document.getElementById('paymentRef').value = '';
|
||||
modal.classList.add('active');
|
||||
document.getElementById('paymentAmount').focus();
|
||||
}
|
||||
|
||||
function closePayment() {
|
||||
const modal = document.getElementById('paymentModal');
|
||||
if (modal) modal.classList.remove('active');
|
||||
}
|
||||
|
||||
async function recordPayment() {
|
||||
if (!currentCustomer) return;
|
||||
const amount = parseFloat(document.getElementById('paymentAmount').value);
|
||||
if (!amount || amount <= 0) { alert('Ingresa un monto valido'); return; }
|
||||
|
||||
const method = document.getElementById('paymentMethod').value;
|
||||
const reference = document.getElementById('paymentRef').value.trim();
|
||||
|
||||
try {
|
||||
await api(`/pos/api/customers/${currentCustomer.id}/payment`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ amount, method, reference }),
|
||||
});
|
||||
closePayment();
|
||||
selectCustomer(currentCustomer.id);
|
||||
} catch (e) {
|
||||
alert('Error: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Slide Panel (from HTML) ─────────
|
||||
// The HTML already has openSlidePanel/closeSlidePanel. We hook showDetail into it.
|
||||
function showDetail(id) {
|
||||
selectCustomer(id);
|
||||
if (typeof openSlidePanel === 'function') openSlidePanel();
|
||||
}
|
||||
|
||||
// ─── Init ────────────────────────────
|
||||
loadCustomers();
|
||||
function init() {
|
||||
// Auth check
|
||||
if (!token) {
|
||||
window.location.href = '/pos/login';
|
||||
return;
|
||||
}
|
||||
|
||||
// Wire the "Nuevo Cliente" button from the page header
|
||||
window.openNewCustomerModal = showCreateModal;
|
||||
|
||||
// Wire action buttons in detail panel
|
||||
wireActionButtons();
|
||||
|
||||
// Wire the inline HTML viewCustomer to our showDetail
|
||||
window.viewCustomer = showDetail;
|
||||
|
||||
// Inject modals if not present
|
||||
injectModals();
|
||||
|
||||
// Load data
|
||||
loadCustomers();
|
||||
loadSummary();
|
||||
}
|
||||
|
||||
function injectModals() {
|
||||
// Customer Create/Edit Modal
|
||||
if (!document.getElementById('customerModal')) {
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = `
|
||||
<div id="customerModal" class="modal-overlay" style="display:none;">
|
||||
<div class="modal-box">
|
||||
<div class="modal-header">
|
||||
<h3 id="modalTitle">Nuevo Cliente</h3>
|
||||
<button class="btn-close" onclick="Customers.closeModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="editId" />
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>Nombre *</label><input type="text" id="fName" class="form-input" placeholder="Nombre del cliente" /></div>
|
||||
<div class="form-group"><label>RFC</label><input type="text" id="fRfc" class="form-input" placeholder="RFC" maxlength="13" /></div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>Razon Social</label><input type="text" id="fRazonSocial" class="form-input" placeholder="Razon Social" /></div>
|
||||
<div class="form-group"><label>Regimen Fiscal</label>
|
||||
<select id="fRegimenFiscal" class="form-input">
|
||||
<option value="">-- Seleccionar --</option>
|
||||
<option value="601">601 - General de Ley PM</option>
|
||||
<option value="603">603 - Personas Morales Fines No Lucrativos</option>
|
||||
<option value="605">605 - Sueldos y Salarios</option>
|
||||
<option value="606">606 - Arrendamiento</option>
|
||||
<option value="608">608 - Demas Ingresos</option>
|
||||
<option value="612">612 - Personas Fisicas Empresariales</option>
|
||||
<option value="616">616 - Sin Obligaciones Fiscales</option>
|
||||
<option value="621">621 - Incorporacion Fiscal</option>
|
||||
<option value="625">625 - RESICO</option>
|
||||
<option value="626">626 - RESICO PM</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>Uso CFDI</label>
|
||||
<select id="fUsoCfdi" class="form-input">
|
||||
<option value="G01">G01 - Adquisicion de mercancias</option>
|
||||
<option value="G03" selected>G03 - Gastos en general</option>
|
||||
<option value="I01">I01 - Construcciones</option>
|
||||
<option value="I08">I08 - Otra maquinaria</option>
|
||||
<option value="P01">P01 - Por definir</option>
|
||||
<option value="S01">S01 - Sin efectos fiscales</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group"><label>Codigo Postal</label><input type="text" id="fCp" class="form-input" placeholder="C.P." maxlength="5" /></div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>Telefono</label><input type="text" id="fPhone" class="form-input" placeholder="Telefono" /></div>
|
||||
<div class="form-group"><label>Email</label><input type="email" id="fEmail" class="form-input" placeholder="correo@ejemplo.com" /></div>
|
||||
</div>
|
||||
<div class="form-group"><label>Direccion</label><input type="text" id="fAddress" class="form-input" placeholder="Direccion" /></div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>Tipo Precio</label>
|
||||
<select id="fPriceTier" class="form-input">
|
||||
<option value="1">Mostrador</option>
|
||||
<option value="2">Taller</option>
|
||||
<option value="3">Mayoreo</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group"><label>Limite de Credito</label><input type="number" id="fCreditLimit" class="form-input" value="0" min="0" step="1000" /></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="Customers.closeModal()">Cancelar</button>
|
||||
<button class="btn btn-primary" onclick="Customers.save()">Guardar</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
document.body.appendChild(div);
|
||||
}
|
||||
|
||||
// Statement Modal
|
||||
if (!document.getElementById('statementModal')) {
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = `
|
||||
<div id="statementModal" class="modal-overlay" style="display:none;">
|
||||
<div class="modal-box modal-box--wide">
|
||||
<div class="modal-header">
|
||||
<h3>Estado de Cuenta — <span id="statementName"></span></h3>
|
||||
<button class="btn-close" onclick="Customers.closeStatement()">×</button>
|
||||
</div>
|
||||
<div class="modal-body" id="statementContent" style="max-height:60vh;overflow-y:auto;">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-primary" onclick="Customers.showPaymentModal()">Registrar Abono</button>
|
||||
<button class="btn btn-secondary" onclick="Customers.closeStatement()">Cerrar</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
document.body.appendChild(div);
|
||||
}
|
||||
|
||||
// Payment Modal
|
||||
if (!document.getElementById('paymentModal')) {
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = `
|
||||
<div id="paymentModal" class="modal-overlay" style="display:none;">
|
||||
<div class="modal-box">
|
||||
<div class="modal-header">
|
||||
<h3>Registrar Abono</h3>
|
||||
<button class="btn-close" onclick="Customers.closePayment()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p style="margin-bottom:var(--space-4);color:var(--color-text-secondary);">Cliente: <strong id="paymentCustomerName"></strong></p>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>Monto</label><input type="number" id="paymentAmount" class="form-input" placeholder="0.00" min="0" step="0.01" /></div>
|
||||
<div class="form-group"><label>Metodo</label>
|
||||
<select id="paymentMethod" class="form-input">
|
||||
<option value="cash">Efectivo</option>
|
||||
<option value="transfer">Transferencia</option>
|
||||
<option value="card">Tarjeta</option>
|
||||
<option value="check">Cheque</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group"><label>Referencia</label><input type="text" id="paymentRef" class="form-input" placeholder="Num. referencia (opcional)" /></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="Customers.closePayment()">Cancelar</button>
|
||||
<button class="btn btn-primary" onclick="Customers.recordPayment()">Registrar</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
document.body.appendChild(div);
|
||||
}
|
||||
|
||||
// Inject modal styles
|
||||
if (!document.getElementById('modalStyles')) {
|
||||
const style = document.createElement('style');
|
||||
style.id = 'modalStyles';
|
||||
style.textContent = `
|
||||
.modal-overlay {
|
||||
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0,0,0,0.5); z-index: 9000;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
.modal-overlay.active { display: flex !important; }
|
||||
.modal-box {
|
||||
background: var(--color-bg-elevated); border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg); width: 90%; max-width: 600px;
|
||||
max-height: 85vh; overflow-y: auto;
|
||||
box-shadow: var(--shadow-xl);
|
||||
}
|
||||
.modal-box--wide { max-width: 800px; }
|
||||
.modal-header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: var(--space-4) var(--space-5);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
.modal-header h3 {
|
||||
font-family: var(--font-heading); font-size: var(--text-h6);
|
||||
font-weight: var(--heading-weight-primary); color: var(--color-text-primary);
|
||||
letter-spacing: var(--tracking-wide); text-transform: uppercase;
|
||||
}
|
||||
.modal-body { padding: var(--space-5); }
|
||||
.modal-footer {
|
||||
display: flex; justify-content: flex-end; gap: var(--space-3);
|
||||
padding: var(--space-4) var(--space-5);
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
.form-row { display: flex; gap: var(--space-4); margin-bottom: var(--space-4); }
|
||||
.form-row > .form-group { flex: 1; }
|
||||
.form-group { margin-bottom: var(--space-4); }
|
||||
.form-group label {
|
||||
display: block; font-size: var(--text-caption); font-weight: var(--font-weight-semibold);
|
||||
color: var(--color-text-muted); text-transform: uppercase; letter-spacing: var(--tracking-wider);
|
||||
margin-bottom: var(--space-1);
|
||||
}
|
||||
.form-input {
|
||||
width: 100%; height: 38px; padding: 0 var(--space-3);
|
||||
background: var(--color-bg-overlay); border: 1.5px solid var(--color-border);
|
||||
border-radius: var(--radius-md); font-family: var(--font-body);
|
||||
font-size: var(--text-body-sm); color: var(--color-text-primary); outline: none;
|
||||
transition: var(--transition-fast);
|
||||
}
|
||||
.form-input:focus { border-color: var(--color-border-focus); box-shadow: var(--shadow-focus); }
|
||||
select.form-input { cursor: pointer; }
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
}
|
||||
|
||||
// Run init
|
||||
init();
|
||||
|
||||
return {
|
||||
search, goToPage, loadCustomers,
|
||||
showDetail, closeDetail,
|
||||
showDetail, selectCustomer, closeDetail,
|
||||
showCreateModal, editCurrent, closeModal, save,
|
||||
showStatement,
|
||||
showStatement, closeStatement,
|
||||
showPaymentModal, closePayment, recordPayment,
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -2044,428 +2044,7 @@
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
SAMPLE DATA
|
||||
------------------------------------------------------------------ */
|
||||
const customers = [
|
||||
{
|
||||
id: 1, num: '00001',
|
||||
name: 'Taller Star Automotriz', initials: 'TS',
|
||||
rfc: 'TSA820115HDF', phone: '55 1234-5678', email: 'ventas@tallerstar.mx',
|
||||
tipo: 'Taller', credit: 48500, creditLimit: 80000,
|
||||
lastPurchase: '28 Mar 2026', estado: 'Activo',
|
||||
address: 'Av. Insurgentes Sur 1602, Col. Crédito Constructor, CDMX',
|
||||
since: '14 Mar 2019',
|
||||
history: [
|
||||
{ date: '28 Mar 2026', folio: 'NX-20498', total: '$4,820.00', status: 'Pagado' },
|
||||
{ date: '22 Mar 2026', folio: 'NX-20341', total: '$1,250.50', status: 'Pagado' },
|
||||
{ date: '15 Mar 2026', folio: 'NX-20188', total: '$9,670.00', status: 'Pendiente' },
|
||||
{ date: '07 Mar 2026', folio: 'NX-19982', total: '$3,100.00', status: 'Pagado' },
|
||||
{ date: '28 Feb 2026', folio: 'NX-19740', total: '$6,450.00', status: 'Pagado' },
|
||||
{ date: '14 Feb 2026', folio: 'NX-19510', total: '$2,890.00', status: 'Vencido' },
|
||||
{ date: '05 Feb 2026', folio: 'NX-19301', total: '$5,220.00', status: 'Pagado' },
|
||||
{ date: '25 Ene 2026', folio: 'NX-19050', total: '$780.00', status: 'Pagado' },
|
||||
{ date: '18 Ene 2026', folio: 'NX-18890', total: '$11,340.00', status: 'Pagado' },
|
||||
{ date: '10 Ene 2026', folio: 'NX-18720', total: '$4,100.00', status: 'Pagado' },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 2, num: '00002',
|
||||
name: 'Refacciones El Trueno', initials: 'RT',
|
||||
rfc: 'RTE930720HDF', phone: '55 9876-5432', email: 'compras@eltrueno.com',
|
||||
tipo: 'Mayoreo', credit: 120000, creditLimit: 200000,
|
||||
lastPurchase: '27 Mar 2026', estado: 'Activo',
|
||||
address: 'Blvd. Adolfo López Mateos 2855, Álvaro Obregón, CDMX',
|
||||
since: '02 Jun 2017',
|
||||
history: [
|
||||
{ date: '27 Mar 2026', folio: 'NX-20481', total: '$28,500.00', status: 'Pendiente' },
|
||||
{ date: '20 Mar 2026', folio: 'NX-20290', total: '$15,670.00', status: 'Pagado' },
|
||||
{ date: '12 Mar 2026', folio: 'NX-20110', total: '$42,300.00', status: 'Pagado' },
|
||||
{ date: '04 Mar 2026', folio: 'NX-19930', total: '$8,900.00', status: 'Pagado' },
|
||||
{ date: '25 Feb 2026', folio: 'NX-19720', total: '$33,100.00', status: 'Pagado' },
|
||||
{ date: '16 Feb 2026', folio: 'NX-19480', total: '$21,450.00', status: 'Pagado' },
|
||||
{ date: '07 Feb 2026', folio: 'NX-19240', total: '$9,800.00', status: 'Pagado' },
|
||||
{ date: '28 Ene 2026', folio: 'NX-19010', total: '$47,200.00', status: 'Pagado' },
|
||||
{ date: '19 Ene 2026', folio: 'NX-18840', total: '$18,660.00', status: 'Pagado' },
|
||||
{ date: '10 Ene 2026', folio: 'NX-18650', total: '$12,400.00', status: 'Pagado' },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 3, num: '00003',
|
||||
name: 'Karla Mendoza Jiménez', initials: 'KM',
|
||||
rfc: 'MEJK891004MDF', phone: '55 5551-2233', email: 'karla.m@gmail.com',
|
||||
tipo: 'Mostrador', credit: 0, creditLimit: 5000,
|
||||
lastPurchase: '25 Mar 2026', estado: 'Activo',
|
||||
address: 'Calle Fresno 44, Col. San Marcos, Xochimilco, CDMX',
|
||||
since: '10 Ene 2023',
|
||||
history: [
|
||||
{ date: '25 Mar 2026', folio: 'NX-20445', total: '$340.00', status: 'Pagado' },
|
||||
{ date: '10 Mar 2026', folio: 'NX-20080', total: '$780.00', status: 'Pagado' },
|
||||
{ date: '18 Feb 2026', folio: 'NX-19560', total: '$210.00', status: 'Pagado' },
|
||||
{ date: '05 Feb 2026', folio: 'NX-19290', total: '$1,200.00',status: 'Pagado' },
|
||||
{ date: '20 Ene 2026', folio: 'NX-18920', total: '$450.00', status: 'Pagado' },
|
||||
{ date: '03 Ene 2026', folio: 'NX-18600', total: '$890.00', status: 'Pagado' },
|
||||
{ date: '15 Dic 2025', folio: 'NX-18100', total: '$620.00', status: 'Pagado' },
|
||||
{ date: '01 Dic 2025', folio: 'NX-17890', total: '$330.00', status: 'Pagado' },
|
||||
{ date: '18 Nov 2025', folio: 'NX-17620', total: '$1,100.00',status: 'Pagado' },
|
||||
{ date: '05 Nov 2025', folio: 'NX-17390', total: '$780.00', status: 'Pagado' },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 4, num: '00004',
|
||||
name: 'Distribuidora Central Norte', initials: 'DC',
|
||||
rfc: 'DCN760315B24', phone: '55 8888-0011', email: 'pedidos@cnorte.mx',
|
||||
tipo: 'Mayoreo', credit: 65000, creditLimit: 300000,
|
||||
lastPurchase: '26 Mar 2026', estado: 'Mora',
|
||||
address: 'Calz. de los Misterios 1420, Gustavo A. Madero, CDMX',
|
||||
since: '30 Ago 2014',
|
||||
history: [
|
||||
{ date: '26 Mar 2026', folio: 'NX-20470', total: '$55,200.00', status: 'Pendiente' },
|
||||
{ date: '15 Mar 2026', folio: 'NX-20140', total: '$38,900.00', status: 'Vencido' },
|
||||
{ date: '05 Mar 2026', folio: 'NX-19960', total: '$22,100.00', status: 'Vencido' },
|
||||
{ date: '18 Feb 2026', folio: 'NX-19500', total: '$41,700.00', status: 'Pagado' },
|
||||
{ date: '06 Feb 2026', folio: 'NX-19270', total: '$29,800.00', status: 'Pagado' },
|
||||
{ date: '25 Ene 2026', folio: 'NX-19040', total: '$63,400.00', status: 'Pagado' },
|
||||
{ date: '14 Ene 2026', folio: 'NX-18810', total: '$17,600.00', status: 'Pagado' },
|
||||
{ date: '03 Ene 2026', folio: 'NX-18580', total: '$34,900.00', status: 'Pagado' },
|
||||
{ date: '22 Dic 2025', folio: 'NX-18200', total: '$48,100.00', status: 'Pagado' },
|
||||
{ date: '10 Dic 2025', folio: 'NX-17980', total: '$26,300.00', status: 'Pagado' },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 5, num: '00005',
|
||||
name: 'Taller Rápido Orozco', initials: 'TR',
|
||||
rfc: 'ORCH751122HMC', phone: '55 7744-3322', email: 'rapidorozco@outlook.com',
|
||||
tipo: 'Taller', credit: 18000, creditLimit: 25000,
|
||||
lastPurchase: '24 Mar 2026', estado: 'Activo',
|
||||
address: 'Calle Morelos 88, Col. Centro, Naucalpan, Edo. Méx.',
|
||||
since: '05 May 2021',
|
||||
history: [
|
||||
{ date: '24 Mar 2026', folio: 'NX-20430', total: '$3,450.00', status: 'Pagado' },
|
||||
{ date: '17 Mar 2026', folio: 'NX-20210', total: '$1,880.00', status: 'Pagado' },
|
||||
{ date: '09 Mar 2026', folio: 'NX-20040', total: '$6,100.00', status: 'Pendiente' },
|
||||
{ date: '01 Mar 2026', folio: 'NX-19870', total: '$2,200.00', status: 'Pagado' },
|
||||
{ date: '22 Feb 2026', folio: 'NX-19660', total: '$4,760.00', status: 'Pagado' },
|
||||
{ date: '12 Feb 2026', folio: 'NX-19420', total: '$920.00', status: 'Pagado' },
|
||||
{ date: '02 Feb 2026', folio: 'NX-19200', total: '$3,340.00', status: 'Pagado' },
|
||||
{ date: '24 Ene 2026', folio: 'NX-18990', total: '$1,560.00', status: 'Pagado' },
|
||||
{ date: '15 Ene 2026', folio: 'NX-18800', total: '$5,280.00', status: 'Pagado' },
|
||||
{ date: '06 Ene 2026', folio: 'NX-18610', total: '$2,990.00', status: 'Pagado' },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 6, num: '00006',
|
||||
name: 'Servicios Automotrices Luna', initials: 'SL',
|
||||
rfc: 'SAL880903HDF', phone: '55 3311-7766', email: 'luna.serv@yahoo.com',
|
||||
tipo: 'Taller', credit: 4500, creditLimit: 15000,
|
||||
lastPurchase: '23 Mar 2026', estado: 'Activo',
|
||||
address: 'Av. Taxqueña 1201, Coyoacán, CDMX',
|
||||
since: '12 Sep 2020',
|
||||
history: [
|
||||
{ date: '23 Mar 2026', folio: 'NX-20410', total: '$2,100.00', status: 'Pagado' },
|
||||
{ date: '14 Mar 2026', folio: 'NX-20170', total: '$880.00', status: 'Pagado' },
|
||||
{ date: '06 Mar 2026', folio: 'NX-19990', total: '$3,650.00', status: 'Pagado' },
|
||||
{ date: '25 Feb 2026', folio: 'NX-19730', total: '$1,220.00', status: 'Pagado' },
|
||||
{ date: '15 Feb 2026', folio: 'NX-19490', total: '$4,400.00', status: 'Pagado' },
|
||||
{ date: '05 Feb 2026', folio: 'NX-19260', total: '$760.00', status: 'Pagado' },
|
||||
{ date: '26 Ene 2026', folio: 'NX-19030', total: '$2,980.00', status: 'Pagado' },
|
||||
{ date: '16 Ene 2026', folio: 'NX-18820', total: '$1,450.00', status: 'Pagado' },
|
||||
{ date: '07 Ene 2026', folio: 'NX-18630', total: '$3,100.00', status: 'Pagado' },
|
||||
{ date: '28 Dic 2025', folio: 'NX-18240', total: '$590.00', status: 'Pagado' },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 7, num: '00007',
|
||||
name: 'Autoservicio El Piston', initials: 'AP',
|
||||
rfc: 'AEP920601HDF', phone: '55 2299-1144', email: 'elpiston@hotmail.com',
|
||||
tipo: 'Mostrador', credit: 3200, creditLimit: 10000,
|
||||
lastPurchase: '21 Mar 2026', estado: 'Inactivo',
|
||||
address: 'Calle Hidalgo 557, Tlalnepantla, Edo. Méx.',
|
||||
since: '20 Feb 2022',
|
||||
history: [
|
||||
{ date: '21 Mar 2026', folio: 'NX-20380', total: '$1,800.00', status: 'Pagado' },
|
||||
{ date: '08 Mar 2026', folio: 'NX-20020', total: '$940.00', status: 'Pagado' },
|
||||
{ date: '18 Feb 2026', folio: 'NX-19520', total: '$2,600.00', status: 'Pagado' },
|
||||
{ date: '05 Feb 2026', folio: 'NX-19290', total: '$780.00', status: 'Pagado' },
|
||||
{ date: '23 Ene 2026', folio: 'NX-19010', total: '$3,400.00', status: 'Pagado' },
|
||||
{ date: '12 Ene 2026', folio: 'NX-18790', total: '$560.00', status: 'Pagado' },
|
||||
{ date: '02 Ene 2026', folio: 'NX-18560', total: '$1,230.00', status: 'Pagado' },
|
||||
{ date: '20 Dic 2025', folio: 'NX-18160', total: '$2,100.00', status: 'Pagado' },
|
||||
{ date: '09 Dic 2025', folio: 'NX-17960', total: '$870.00', status: 'Pagado' },
|
||||
{ date: '28 Nov 2025', folio: 'NX-17740', total: '$1,540.00', status: 'Pagado' },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 8, num: '00008',
|
||||
name: 'Mega Partes Industriales', initials: 'MP',
|
||||
rfc: 'MPI840718B56', phone: '55 6600-9988', email: 'compras@megapartes.mx',
|
||||
tipo: 'Mayoreo', credit: 200000, creditLimit: 500000,
|
||||
lastPurchase: '29 Mar 2026', estado: 'Activo',
|
||||
address: 'Av. 5 de Mayo 3800, Zona Industrial, Tlalnepantla, Edo. Méx.',
|
||||
since: '08 Nov 2012',
|
||||
history: [
|
||||
{ date: '29 Mar 2026', folio: 'NX-20495', total: '$88,400.00', status: 'Pendiente' },
|
||||
{ date: '23 Mar 2026', folio: 'NX-20415', total: '$62,100.00', status: 'Pagado' },
|
||||
{ date: '16 Mar 2026', folio: 'NX-20230', total: '$105,800.00', status: 'Pagado' },
|
||||
{ date: '08 Mar 2026', folio: 'NX-20050', total: '$47,300.00', status: 'Pagado' },
|
||||
{ date: '28 Feb 2026', folio: 'NX-19750', total: '$93,600.00', status: 'Pagado' },
|
||||
{ date: '19 Feb 2026', folio: 'NX-19540', total: '$38,700.00', status: 'Pagado' },
|
||||
{ date: '09 Feb 2026', folio: 'NX-19310', total: '$71,200.00', status: 'Pagado' },
|
||||
{ date: '30 Ene 2026', folio: 'NX-19080', total: '$56,900.00', status: 'Pagado' },
|
||||
{ date: '21 Ene 2026', folio: 'NX-18870', total: '$43,500.00', status: 'Pagado' },
|
||||
{ date: '12 Ene 2026', folio: 'NX-18680', total: '$82,100.00', status: 'Pagado' },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 9, num: '00009',
|
||||
name: 'Taller Mecánico Pérez Hnos.', initials: 'TP',
|
||||
rfc: 'PHM950228HDF', phone: '55 4433-6677', email: 'perezhermanos@gmail.com',
|
||||
tipo: 'Taller', credit: 12000, creditLimit: 20000,
|
||||
lastPurchase: '20 Mar 2026', estado: 'Activo',
|
||||
address: 'Calle 5 No. 234, Col. Agrícola Oriental, Iztacalco, CDMX',
|
||||
since: '14 Jul 2020',
|
||||
history: [
|
||||
{ date: '20 Mar 2026', folio: 'NX-20360', total: '$5,600.00', status: 'Pagado' },
|
||||
{ date: '11 Mar 2026', folio: 'NX-20100', total: '$2,340.00', status: 'Pendiente' },
|
||||
{ date: '02 Mar 2026', folio: 'NX-19900', total: '$7,800.00', status: 'Pagado' },
|
||||
{ date: '21 Feb 2026', folio: 'NX-19640', total: '$1,450.00', status: 'Pagado' },
|
||||
{ date: '11 Feb 2026', folio: 'NX-19400', total: '$3,900.00', status: 'Pagado' },
|
||||
{ date: '01 Feb 2026', folio: 'NX-19180', total: '$6,200.00', status: 'Pagado' },
|
||||
{ date: '23 Ene 2026', folio: 'NX-18970', total: '$2,890.00', status: 'Pagado' },
|
||||
{ date: '14 Ene 2026', folio: 'NX-18780', total: '$4,100.00', status: 'Pagado' },
|
||||
{ date: '05 Ene 2026', folio: 'NX-18590', total: '$1,780.00', status: 'Pagado' },
|
||||
{ date: '27 Dic 2025', folio: 'NX-18210', total: '$8,400.00', status: 'Pagado' },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 10, num: '00010',
|
||||
name: 'Import Autoparts Villanueva', initials: 'IV',
|
||||
rfc: 'IAV010315B34', phone: '55 9900-1122', email: 'ventas@importav.mx',
|
||||
tipo: 'Mayoreo', credit: 95000, creditLimit: 150000,
|
||||
lastPurchase: '28 Mar 2026', estado: 'Activo',
|
||||
address: 'Blvd. Manuel Ávila Camacho 600, Naucalpan, Edo. Méx.',
|
||||
since: '22 Mar 2015',
|
||||
history: [
|
||||
{ date: '28 Mar 2026', folio: 'NX-20490', total: '$34,500.00', status: 'Pendiente' },
|
||||
{ date: '21 Mar 2026', folio: 'NX-20370', total: '$22,800.00', status: 'Pagado' },
|
||||
{ date: '13 Mar 2026', folio: 'NX-20160', total: '$48,900.00', status: 'Pagado' },
|
||||
{ date: '05 Mar 2026', folio: 'NX-19950', total: '$16,700.00', status: 'Pagado' },
|
||||
{ date: '24 Feb 2026', folio: 'NX-19700', total: '$39,200.00', status: 'Pagado' },
|
||||
{ date: '14 Feb 2026', folio: 'NX-19460', total: '$27,600.00', status: 'Pagado' },
|
||||
{ date: '04 Feb 2026', folio: 'NX-19220', total: '$11,400.00', status: 'Pagado' },
|
||||
{ date: '26 Ene 2026', folio: 'NX-19000', total: '$53,800.00', status: 'Pagado' },
|
||||
{ date: '17 Ene 2026', folio: 'NX-18810', total: '$24,100.00', status: 'Pagado' },
|
||||
{ date: '08 Ene 2026', folio: 'NX-18620', total: '$31,600.00', status: 'Pagado' },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 11, num: '00011',
|
||||
name: 'Centro Automotriz Garza', initials: 'CG',
|
||||
rfc: 'GAR780904HNL', phone: '81 2244-5566', email: 'garza.auto@prodigy.net',
|
||||
tipo: 'Taller', credit: 9000, creditLimit: 30000,
|
||||
lastPurchase: '18 Mar 2026', estado: 'Activo',
|
||||
address: 'Av. Constitución 1888, Monterrey, N.L.',
|
||||
since: '07 Feb 2018',
|
||||
history: [
|
||||
{ date: '18 Mar 2026', folio: 'NX-20280', total: '$7,200.00', status: 'Pagado' },
|
||||
{ date: '08 Mar 2026', folio: 'NX-20030', total: '$3,400.00', status: 'Pagado' },
|
||||
{ date: '26 Feb 2026', folio: 'NX-19760', total: '$9,100.00', status: 'Pendiente' },
|
||||
{ date: '16 Feb 2026', folio: 'NX-19510', total: '$4,800.00', status: 'Pagado' },
|
||||
{ date: '06 Feb 2026', folio: 'NX-19270', total: '$6,550.00', status: 'Pagado' },
|
||||
{ date: '27 Ene 2026', folio: 'NX-19040', total: '$2,100.00', status: 'Pagado' },
|
||||
{ date: '17 Ene 2026', folio: 'NX-18820', total: '$8,300.00', status: 'Pagado' },
|
||||
{ date: '08 Ene 2026', folio: 'NX-18630', total: '$3,670.00', status: 'Pagado' },
|
||||
{ date: '29 Dic 2025', folio: 'NX-18250', total: '$11,400.00', status: 'Pagado' },
|
||||
{ date: '18 Dic 2025', folio: 'NX-18020', total: '$5,890.00', status: 'Pagado' },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 12, num: '00012',
|
||||
name: 'Refacciones El Farol', initials: 'RF',
|
||||
rfc: 'REF011120B45', phone: '33 5577-8899', email: 'elfarol.ref@gmail.com',
|
||||
tipo: 'Mostrador', credit: 7500, creditLimit: 10000,
|
||||
lastPurchase: '17 Mar 2026', estado: 'Inactivo',
|
||||
address: 'Calz. Independencia Sur 2340, Guadalajara, Jal.',
|
||||
since: '15 Oct 2021',
|
||||
history: [
|
||||
{ date: '17 Mar 2026', folio: 'NX-20260', total: '$2,500.00', status: 'Pendiente' },
|
||||
{ date: '06 Mar 2026', folio: 'NX-20010', total: '$1,800.00', status: 'Pagado' },
|
||||
{ date: '23 Feb 2026', folio: 'NX-19680', total: '$3,200.00', status: 'Pagado' },
|
||||
{ date: '12 Feb 2026', folio: 'NX-19440', total: '$900.00', status: 'Pagado' },
|
||||
{ date: '01 Feb 2026', folio: 'NX-19200', total: '$4,100.00', status: 'Pagado' },
|
||||
{ date: '21 Ene 2026', folio: 'NX-18960', total: '$1,550.00', status: 'Pagado' },
|
||||
{ date: '11 Ene 2026', folio: 'NX-18770', total: '$2,800.00', status: 'Pagado' },
|
||||
{ date: '02 Ene 2026', folio: 'NX-18540', total: '$680.00', status: 'Pagado' },
|
||||
{ date: '21 Dic 2025', folio: 'NX-18170', total: '$3,600.00', status: 'Pagado' },
|
||||
{ date: '10 Dic 2025', folio: 'NX-17970', total: '$1,200.00', status: 'Pagado' },
|
||||
]
|
||||
},
|
||||
];
|
||||
|
||||
let selectedCustomerId = null;
|
||||
let filteredCustomers = [...customers];
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
RENDER TABLE
|
||||
------------------------------------------------------------------ */
|
||||
function renderTable(data) {
|
||||
const tbody = document.getElementById('customersBody');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if (data.length === 0) {
|
||||
tbody.innerHTML = `<tr><td colspan="9" style="text-align:center;padding:var(--space-8);color:var(--color-text-muted);">Sin resultados para la búsqueda.</td></tr>`;
|
||||
return;
|
||||
}
|
||||
|
||||
data.forEach(c => {
|
||||
const pct = Math.round(((c.creditLimit - c.credit) / c.creditLimit) * 100);
|
||||
const creditClass = pct >= 80 ? 'none' : pct >= 60 ? 'low' : '';
|
||||
const creditFormatted = new Intl.NumberFormat('es-MX', { style:'currency', currency:'MXN', minimumFractionDigits:0 }).format(c.credit);
|
||||
|
||||
let estadoBadge = '';
|
||||
if (c.estado === 'Activo') estadoBadge = `<span class="badge badge--active"><span class="badge-dot"></span>Activo</span>`;
|
||||
else if (c.estado === 'Inactivo') estadoBadge = `<span class="badge badge--inactive"><span class="badge-dot"></span>Inactivo</span>`;
|
||||
else estadoBadge = `<span class="badge badge--warning"><span class="badge-dot"></span>Mora</span>`;
|
||||
|
||||
const tipoClass = c.tipo === 'Taller' ? 'tipo-chip--taller' : c.tipo === 'Mayoreo' ? 'tipo-chip--mayoreo' : 'tipo-chip--mostrador';
|
||||
const isSelected = c.id === selectedCustomerId;
|
||||
|
||||
const tr = document.createElement('tr');
|
||||
tr.className = isSelected ? 'selected' : '';
|
||||
tr.onclick = () => selectCustomer(c.id);
|
||||
tr.innerHTML = `
|
||||
<td class="cell-num">${c.num}</td>
|
||||
<td>
|
||||
<div class="cell-name">${c.name}</div>
|
||||
<div class="cell-name-sub hide-mobile">${c.email}</div>
|
||||
</td>
|
||||
<td class="cell-rfc hide-mobile">${c.rfc}</td>
|
||||
<td class="hide-mobile">${c.phone}</td>
|
||||
<td class="hide-mobile" style="font-size:var(--text-caption);color:var(--color-text-secondary);">${c.email}</td>
|
||||
<td><span class="tipo-chip ${tipoClass}">${c.tipo}</span></td>
|
||||
<td class="cell-credit ${creditClass}">${creditFormatted}</td>
|
||||
<td class="cell-date hide-mobile">${c.lastPurchase}</td>
|
||||
<td>${estadoBadge}</td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
FILTER
|
||||
------------------------------------------------------------------ */
|
||||
function filterCustomers() {
|
||||
const q = document.getElementById('searchInput').value.toLowerCase();
|
||||
const tipo = document.getElementById('tipoFilter').value;
|
||||
const estado = document.getElementById('estadoFilter').value;
|
||||
|
||||
filteredCustomers = customers.filter(c => {
|
||||
const matchQ = !q || c.name.toLowerCase().includes(q) || c.rfc.toLowerCase().includes(q) || c.phone.includes(q) || c.email.toLowerCase().includes(q);
|
||||
const matchT = !tipo || c.tipo === tipo;
|
||||
const matchE = !estado || c.estado === estado;
|
||||
return matchQ && matchT && matchE;
|
||||
});
|
||||
|
||||
renderTable(filteredCustomers);
|
||||
document.getElementById('tableInfo').textContent = `Mostrando ${filteredCustomers.length} de ${customers.length} clientes`;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
SELECT CUSTOMER — populate detail panel
|
||||
------------------------------------------------------------------ */
|
||||
function selectCustomer(id) {
|
||||
const c = customers.find(x => x.id === id);
|
||||
if (!c) return;
|
||||
selectedCustomerId = id;
|
||||
|
||||
// Toggle empty/content
|
||||
document.getElementById('detailEmpty').style.display = 'none';
|
||||
const detail = document.getElementById('detailContent');
|
||||
detail.style.display = 'flex';
|
||||
|
||||
// Avatar
|
||||
document.getElementById('detailAvatar').textContent = c.initials;
|
||||
|
||||
// Header
|
||||
document.getElementById('detailName').textContent = c.name.toUpperCase();
|
||||
document.getElementById('detailRFC').textContent = c.rfc;
|
||||
|
||||
// Tipo chip
|
||||
const tipoEl = document.getElementById('detailTipo');
|
||||
tipoEl.textContent = c.tipo;
|
||||
tipoEl.className = `tipo-chip tipo-chip--${c.tipo.toLowerCase()}`;
|
||||
|
||||
// Status badge
|
||||
const statEl = document.getElementById('detailStatus');
|
||||
if (c.estado === 'Activo') {
|
||||
statEl.className = 'badge badge--active';
|
||||
statEl.innerHTML = '<span class="badge-dot"></span>Activo';
|
||||
} else if (c.estado === 'Inactivo') {
|
||||
statEl.className = 'badge badge--inactive';
|
||||
statEl.innerHTML = '<span class="badge-dot"></span>Inactivo';
|
||||
} else {
|
||||
statEl.className = 'badge badge--warning';
|
||||
statEl.innerHTML = '<span class="badge-dot"></span>En Mora';
|
||||
}
|
||||
|
||||
// Contact
|
||||
document.getElementById('detailAddress').textContent = c.address;
|
||||
document.getElementById('detailPhone').textContent = c.phone;
|
||||
document.getElementById('detailEmail').textContent = c.email;
|
||||
document.getElementById('detailSince').textContent = c.since;
|
||||
document.getElementById('detailLastPurchase').textContent = c.lastPurchase;
|
||||
|
||||
// Credit
|
||||
const fmt = n => new Intl.NumberFormat('es-MX', { style:'currency', currency:'MXN', minimumFractionDigits:0 }).format(n);
|
||||
const used = c.creditLimit - c.credit;
|
||||
const pct = Math.round((used / c.creditLimit) * 100);
|
||||
document.getElementById('detailCreditLimit').textContent = fmt(c.creditLimit);
|
||||
document.getElementById('detailCreditAvail').textContent = fmt(c.credit);
|
||||
document.getElementById('detailCreditUsed').textContent = fmt(used);
|
||||
document.getElementById('detailCreditPct').textContent = `${pct}%`;
|
||||
|
||||
const bar = document.getElementById('detailCreditBar');
|
||||
bar.style.width = `${pct}%`;
|
||||
bar.className = `progress-bar__fill ${pct < 40 ? 'low' : pct > 75 ? 'high' : ''}`;
|
||||
|
||||
// Purchase History
|
||||
const hbody = document.getElementById('historyBody');
|
||||
hbody.innerHTML = '';
|
||||
c.history.forEach(h => {
|
||||
const statusClass = h.status === 'Pagado' ? 'mbadge--paid' : h.status === 'Vencido' ? 'mbadge--overdue' : 'mbadge--pending';
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td class="date">${h.date}</td>
|
||||
<td class="folio">${h.folio}</td>
|
||||
<td class="total">${h.total}</td>
|
||||
<td><span class="mbadge ${statusClass}">${h.status}</span></td>
|
||||
`;
|
||||
hbody.appendChild(tr);
|
||||
});
|
||||
|
||||
// Re-render table to update selected row highlight
|
||||
renderTable(filteredCustomers);
|
||||
}
|
||||
|
||||
function closeDetail() {
|
||||
selectedCustomerId = null;
|
||||
document.getElementById('detailEmpty').style.display = 'flex';
|
||||
document.getElementById('detailContent').style.display = 'none';
|
||||
renderTable(filteredCustomers);
|
||||
}
|
||||
|
||||
function openNewCustomerModal() {
|
||||
alert('Modal "Nuevo Cliente" — pendiente de implementación en sprint 2.');
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
INIT
|
||||
------------------------------------------------------------------ */
|
||||
renderTable(customers);
|
||||
// Pre-select first customer on load for demo
|
||||
selectCustomer(1);
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
SLIDE PANEL — wired to Customers.showDetail()
|
||||
SLIDE PANEL
|
||||
------------------------------------------------------------------ */
|
||||
const panelOverlay = document.getElementById('panelOverlay');
|
||||
const slidePanel = document.getElementById('slidePanel');
|
||||
@@ -2487,18 +2066,25 @@
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') closeSlidePanel();
|
||||
if (e.key === 'Escape') {
|
||||
closeSlidePanel();
|
||||
if (typeof Customers !== 'undefined') Customers.closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Override viewCustomer / showDetail to open the slide panel
|
||||
window.viewCustomer = function(id) {
|
||||
if (typeof Customers !== 'undefined' && Customers.showDetail) {
|
||||
Customers.showDetail(id);
|
||||
}
|
||||
/* Placeholder — overridden by customers.js init */
|
||||
function openNewCustomerModal() {}
|
||||
function filterCustomers() {}
|
||||
function closeDetail() {
|
||||
if (typeof Customers !== 'undefined') Customers.closeDetail();
|
||||
}
|
||||
function viewCustomer(id) {
|
||||
if (typeof Customers !== 'undefined') Customers.showDetail(id);
|
||||
openSlidePanel();
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<!-- Offline Banner -->
|
||||
<div id="offlineBanner" class="banner banner--warning" style="display:none;position:fixed;top:0;left:0;right:0;z-index:9999;border-radius:0;animation:none;">
|
||||
<span class="banner__icon"></span>
|
||||
|
||||
Reference in New Issue
Block a user