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 += '| Fecha | Concepto | Cargo | Abono | Saldo |
';
+
+ data.entries.forEach(e => {
+ const dateStr = new Date(e.date).toLocaleDateString('es-MX');
+ html += `
+ | ${dateStr} |
+ ${e.description} |
+ ${e.type === 'charge' ? fmt(e.amount) : ''} |
+ ${e.type === 'payment' ? fmt(e.amount) : ''} |
+ ${fmt(e.running_balance)} |
+
`;
+ });
+ html += '
';
+ }
+
+ 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Nombre |
+ RFC |
+ Telefono |
+ Lista |
+ Credito |
+ Saldo |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Credito disponible
+
$0.00
+
+ Limite: $0.00 |
+ Saldo: $0.00
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Nuevo Cliente
+
+
+
+
+
+
+
+
+
+
+
+
+
Estado de Cuenta:
+
+
+
+
+
+
+
+
+
+