From 56fed52253e7e8dd9208763e3516e6551761e12e Mon Sep 17 00:00:00 2001 From: consultoria-as Date: Wed, 1 Apr 2026 08:35:19 +0000 Subject: [PATCH] fix(pos): rewrite customers.js to match design system HTML structure Co-Authored-By: Claude Opus 4.6 (1M context) --- pos/static/js/customers.js | 675 +++++++++++++++++++++++++++++------ pos/templates/customers.html | 444 +---------------------- 2 files changed, 586 insertions(+), 533 deletions(-) diff --git a/pos/static/js/customers.js b/pos/static/js/customers.js index dda21b1..9b1f025 100644 --- a/pos/static/js/customers.js +++ b/pos/static/js/customers.js @@ -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 'Mora'; + } + return 'Activo'; + } + + 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 += ` - ${c.name} - ${c.rfc || '-'} - ${c.phone || '-'} - ${tierName} - ${fmt(limit)} - ${limit > 0 ? `
` : ''} - - ${balance > 0 ? fmt(balance) : '-'} - `; - }); - - if (customers.length === 0) { - html = 'Sin resultados'; + if (!customers || customers.length === 0) { + tbody.innerHTML = 'Sin resultados.'; + 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 = ` + ${num} + +
${c.name || ''}
+
${c.email || ''}
+ + ${c.rfc || '-'} + ${c.phone || '-'} + ${c.email || '-'} + ${tier} + ${fmt(available)} + ${formatDate(c.last_purchase || c.created_at)} + ${statusBadge(c)} + `; + 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 += ``; + html += ``; + + 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 += ``; } - for (let i = 1; i <= Math.min(pag.total_pages, 10); i++) { - html += ``; - } - if (pag.page < pag.total_pages) { - html += ``; + + if (endP < totalPages) { + html += '...'; + html += ``; } + + html += ``; 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 += `
RFC${c.rfc || '-'}
`; - fiscalHtml += `
Razon Social${c.razon_social || '-'}
`; - fiscalHtml += `
Regimen${c.regimen_fiscal || '-'}
`; - fiscalHtml += `
Uso CFDI${c.uso_cfdi || '-'}
`; - fiscalHtml += `
CP${c.cp || '-'}
`; - 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 += `
Telefono${c.phone || '-'}
`; - contactHtml += `
Email${c.email || '-'}
`; - contactHtml += `
Direccion${c.address || '-'}
`; - 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 = 'Sin compras recientes'; + } 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 = ` + ${formatDate(p.created_at)} + NX-${String(p.id).padStart(5, '0')} + ${fmt(p.total)} + ${statusLabel} + `; + 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 = '
Sin vehiculos registrados
'; + vehiclesEl.innerHTML = 'Sin vehiculos registrados'; } else { - document.getElementById('detailVehicles').innerHTML = vehicles.map(v => - `
+ vehiclesEl.innerHTML = vehicles.map(v => + `
${v.make || ''} ${v.model || ''} ${v.year || ''} - ${v.plates ? `Placas: ${v.plates}` : ''} - ${v.vin ? `
VIN: ${v.vin}
` : ''} + ${v.plates ? ` Placas: ${v.plates}` : ''}
` ).join(''); } + } - // Recent purchases + // Purchases + const purchasesEl = document.getElementById('panelPurchases'); + if (purchasesEl) { const purchases = c.recent_purchases || []; if (purchases.length === 0) { - document.getElementById('detailPurchases').innerHTML = '
Sin compras recientes
'; + purchasesEl.innerHTML = 'Sin compras recientes'; } else { - document.getElementById('detailPurchases').innerHTML = purchases.map(p => - `
- Venta #${p.id} - ${new Date(p.created_at).toLocaleDateString('es-MX')} + purchasesEl.innerHTML = purchases.slice(0, 5).map(p => + `
+ NX-${String(p.id).padStart(5, '0')} — ${formatDate(p.created_at)} ${fmt(p.total)}
` ).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 = '
Cargando...
'; + modal.classList.add('active'); try { const data = await api(`/pos/api/customers/${currentCustomer.id}/statement`); let html = `
Saldo actual: ${fmt(data.balance)} | - Limite: ${fmt(data.customer.credit_limit)} + Limite: ${fmt(data.customer ? data.customer.credit_limit : 0)}
`; - if (data.entries.length === 0) { - html += '
Sin movimientos
'; + if (!data.entries || data.entries.length === 0) { + html += '
Sin movimientos
'; } else { html += ''; - html += ''; + html += ''; data.entries.forEach(e => { - const dateStr = new Date(e.date).toLocaleDateString('es-MX'); - html += ` - - + html += ` + + @@ -288,20 +495,280 @@ const Customers = (() => { html += '
FechaConceptoCargoAbonoSaldo
FechaConceptoCargoAbonoSaldo
${dateStr}${e.description}
${formatDate(e.date)}${e.description || ''} ${e.type === 'charge' ? fmt(e.amount) : ''} ${e.type === 'payment' ? fmt(e.amount) : ''} ${fmt(e.running_balance)}
'; } - document.getElementById('statementContent').innerHTML = html; - document.getElementById('statementModal').classList.add('active'); + if (content) content.innerHTML = html; + } catch (e) { + if (content) content.innerHTML = `
Error: ${e.message}
`; + } + } + + 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 = ` + `; + document.body.appendChild(div); + } + + // Statement Modal + if (!document.getElementById('statementModal')) { + const div = document.createElement('div'); + div.innerHTML = ` + `; + document.body.appendChild(div); + } + + // Payment Modal + if (!document.getElementById('paymentModal')) { + const div = document.createElement('div'); + div.innerHTML = ` + `; + 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, }; })(); diff --git a/pos/templates/customers.html b/pos/templates/customers.html index adb79f2..701b0c0 100644 --- a/pos/templates/customers.html +++ b/pos/templates/customers.html @@ -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 = `Sin resultados para la búsqueda.`; - 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 = `Activo`; - else if (c.estado === 'Inactivo') estadoBadge = `Inactivo`; - else estadoBadge = `Mora`; - - 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 = ` - ${c.num} - -
${c.name}
-
${c.email}
- - ${c.rfc} - ${c.phone} - ${c.email} - ${c.tipo} - ${creditFormatted} - ${c.lastPurchase} - ${estadoBadge} - `; - 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 = 'Activo'; - } else if (c.estado === 'Inactivo') { - statEl.className = 'badge badge--inactive'; - statEl.innerHTML = 'Inactivo'; - } else { - statEl.className = 'badge badge--warning'; - statEl.innerHTML = '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 = ` - ${h.date} - ${h.folio} - ${h.total} - ${h.status} - `; - 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(); - }; + } +