308 lines
14 KiB
JavaScript
308 lines
14 KiB
JavaScript
// /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 += `<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>';
|
|
}
|
|
|
|
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 += `<button class="btn btn-secondary" onclick="Customers.goToPage(${pag.page - 1})">Anterior</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>`;
|
|
}
|
|
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 += `<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;
|
|
|
|
// 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;
|
|
|
|
// Vehicles
|
|
const vehicles = c.vehicle_info || [];
|
|
if (vehicles.length === 0) {
|
|
document.getElementById('detailVehicles').innerHTML = '<div style="color:#999;font-size:13px;">Sin vehiculos registrados</div>';
|
|
} else {
|
|
document.getElementById('detailVehicles').innerHTML = vehicles.map(v =>
|
|
`<div class="vehicle-card">
|
|
<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>` : ''}
|
|
</div>`
|
|
).join('');
|
|
}
|
|
|
|
// Recent purchases
|
|
const purchases = c.recent_purchases || [];
|
|
if (purchases.length === 0) {
|
|
document.getElementById('detailPurchases').innerHTML = '<div style="color:#999;font-size:13px;">Sin compras recientes</div>';
|
|
} 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>
|
|
<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;
|
|
}
|
|
|
|
// ─── 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 = `<div style="margin-bottom:12px;font-size:14px;">
|
|
<strong>Saldo actual: ${fmt(data.balance)}</strong> |
|
|
Limite: ${fmt(data.customer.credit_limit)}
|
|
</div>`;
|
|
|
|
if (data.entries.length === 0) {
|
|
html += '<div style="color:#999;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>';
|
|
|
|
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>
|
|
<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>
|
|
</tr>`;
|
|
});
|
|
html += '</table>';
|
|
}
|
|
|
|
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,
|
|
};
|
|
})();
|