From 76f738652b61bc351975f5895d8523c4f5312349 Mon Sep 17 00:00:00 2001 From: consultoria-as Date: Tue, 31 Mar 2026 03:36:42 +0000 Subject: [PATCH] =?UTF-8?q?feat(pos):=20add=20customers=20page=20=E2=80=94?= =?UTF-8?q?=20search,=20credit,=20vehicles,=20statements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 (1M context) --- pos/static/js/customers.js | 307 +++++++++++++++++++++++++++++++++++ pos/templates/customers.html | 221 +++++++++++++++++++++++++ 2 files changed, 528 insertions(+) create mode 100644 pos/static/js/customers.js create mode 100644 pos/templates/customers.html diff --git a/pos/static/js/customers.js b/pos/static/js/customers.js new file mode 100644 index 0000000..dda21b1 --- /dev/null +++ b/pos/static/js/customers.js @@ -0,0 +1,307 @@ +// /home/Autopartes/pos/static/js/customers.js +/** + * Customers management frontend. + * Communicates with /pos/api/customers (customers_bp). + */ +const Customers = (() => { + let token = localStorage.getItem('pos_token') || ''; + let currentPage = 1; + let currentCustomer = null; + let searchTimeout = null; + + const fmt = (n) => '$' + parseFloat(n || 0).toLocaleString('es-MX', { + minimumFractionDigits: 2, maximumFractionDigits: 2 + }); + + function headers() { + return { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }; + } + + async function api(url, options = {}) { + options.headers = headers(); + const res = await fetch(url, options); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`); + return data; + } + + // ─── List ──────────────────────────── + async function loadCustomers(page, q) { + page = page || currentPage; + q = q !== undefined ? q : (document.getElementById('searchInput').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); + } catch (e) { + console.error('Load customers failed:', e); + } + } + + function renderTable(customers) { + const tbody = document.getElementById('customersBody'); + const tiers = { 1: ['Mostrador', 'tier-1'], 2: ['Taller', 'tier-2'], 3: ['Mayoreo', 'tier-3'] }; + + 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'; + } + + tbody.innerHTML = html; + } + + function renderPagination(pag) { + const container = document.getElementById('pagination'); + if (pag.total_pages <= 1) { container.innerHTML = ''; return; } + + let html = ''; + if (pag.page > 1) { + html += ``; + } + for (let i = 1; i <= Math.min(pag.total_pages, 10); i++) { + html += ``; + } + if (pag.page < pag.total_pages) { + html += ``; + } + container.innerHTML = html; + } + + function goToPage(page) { + currentPage = page; + loadCustomers(page); + } + + function search() { + clearTimeout(searchTimeout); + searchTimeout = setTimeout(() => { + currentPage = 1; + loadCustomers(1); + }, 300); + } + + // ─── Detail ────────────────────────── + async function showDetail(id) { + try { + const c = await api(`/pos/api/customers/${id}`); + currentCustomer = c; + + document.getElementById('detailName').textContent = c.name; + + // 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); + + // 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; + + // Contact + let contactHtml = ''; + contactHtml += `
Telefono${c.phone || '-'}
`; + contactHtml += `
Email${c.email || '-'}
`; + contactHtml += `
Direccion${c.address || '-'}
`; + document.getElementById('detailContact').innerHTML = contactHtml; + + // Vehicles + const vehicles = c.vehicle_info || []; + if (vehicles.length === 0) { + document.getElementById('detailVehicles').innerHTML = '
Sin vehiculos registrados
'; + } else { + document.getElementById('detailVehicles').innerHTML = vehicles.map(v => + `
+ ${v.make || ''} ${v.model || ''} ${v.year || ''} + ${v.plates ? `Placas: ${v.plates}` : ''} + ${v.vin ? `
VIN: ${v.vin}
` : ''} +
` + ).join(''); + } + + // Recent purchases + const purchases = c.recent_purchases || []; + if (purchases.length === 0) { + document.getElementById('detailPurchases').innerHTML = '
Sin compras recientes
'; + } else { + document.getElementById('detailPurchases').innerHTML = purchases.map(p => + `
+ Venta #${p.id} - ${new Date(p.created_at).toLocaleDateString('es-MX')} + ${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; + } + + // ─── Create/Edit Modal ─────────────── + function showCreateModal() { + document.getElementById('modalTitle').textContent = 'Nuevo Cliente'; + document.getElementById('editId').value = ''; + document.getElementById('fName').value = ''; + document.getElementById('fRfc').value = ''; + document.getElementById('fRazonSocial').value = ''; + document.getElementById('fRegimenFiscal').value = ''; + document.getElementById('fUsoCfdi').value = 'G03'; + document.getElementById('fCp').value = ''; + document.getElementById('fPhone').value = ''; + document.getElementById('fEmail').value = ''; + document.getElementById('fAddress').value = ''; + document.getElementById('fPriceTier').value = '1'; + document.getElementById('fCreditLimit').value = '0'; + document.getElementById('customerModal').classList.add('active'); + document.getElementById('fName').focus(); + } + + function editCurrent() { + if (!currentCustomer) return; + const c = currentCustomer; + document.getElementById('modalTitle').textContent = 'Editar Cliente'; + document.getElementById('editId').value = c.id; + document.getElementById('fName').value = c.name || ''; + document.getElementById('fRfc').value = c.rfc || ''; + document.getElementById('fRazonSocial').value = c.razon_social || ''; + document.getElementById('fRegimenFiscal').value = c.regimen_fiscal || ''; + document.getElementById('fUsoCfdi').value = c.uso_cfdi || 'G03'; + document.getElementById('fCp').value = c.cp || ''; + document.getElementById('fPhone').value = c.phone || ''; + document.getElementById('fEmail').value = c.email || ''; + 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'); + } + + function closeModal() { + document.getElementById('customerModal').classList.remove('active'); + } + + async function save() { + const name = document.getElementById('fName').value.trim(); + if (!name) { alert('Nombre es requerido'); return; } + + 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, + }; + + const editId = document.getElementById('editId').value; + + try { + if (editId) { + await api(`/pos/api/customers/${editId}`, { + method: 'PUT', + body: JSON.stringify(body), + }); + } else { + await api('/pos/api/customers', { + method: 'POST', + body: JSON.stringify(body), + }); + } + + closeModal(); + loadCustomers(); + if (editId && currentCustomer) { + showDetail(editId); + } + } catch (e) { + alert('Error: ' + e.message); + } + } + + // ─── Statement ─────────────────────── + async function showStatement() { + if (!currentCustomer) return; + document.getElementById('statementName').textContent = currentCustomer.name; + + try { + const data = await api(`/pos/api/customers/${currentCustomer.id}/statement`); + + let html = `
+ Saldo actual: ${fmt(data.balance)} | + Limite: ${fmt(data.customer.credit_limit)} +
`; + + if (data.entries.length === 0) { + html += '
Sin movimientos
'; + } else { + html += ''; + html += ''; + + data.entries.forEach(e => { + const dateStr = new Date(e.date).toLocaleDateString('es-MX'); + html += ` + + + + + + `; + }); + html += '
FechaConceptoCargoAbonoSaldo
${dateStr}${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'); + } catch (e) { + alert('Error: ' + e.message); + } + } + + // ─── Init ──────────────────────────── + loadCustomers(); + + return { + search, goToPage, loadCustomers, + showDetail, closeDetail, + showCreateModal, editCurrent, closeModal, save, + showStatement, + }; +})(); diff --git a/pos/templates/customers.html b/pos/templates/customers.html new file mode 100644 index 0000000..323dd1f --- /dev/null +++ b/pos/templates/customers.html @@ -0,0 +1,221 @@ + + + + + + + Clientes - Nexus POS + + + + +
+

Clientes

+ +
+ +
+
+ + +
+ + + + + + + + + + + + + +
NombreRFCTelefonoListaCreditoSaldo
+ + +
+ + +
+
+

Cliente

+ +
+
+
+
+
Credito disponible
+
$0.00
+
+ Limite: $0.00 | + Saldo: $0.00 +
+
+
+ +
+

Datos Fiscales

+
+
+ +
+

Contacto

+
+
+ +
+

Vehiculos

+
+
+ +
+

Compras Recientes

+
+
+ +
+ + +
+
+
+ + + + + + + + + +