775 lines
36 KiB
JavaScript
775 lines
36 KiB
JavaScript
// /home/Autopartes/pos/static/js/customers.js
|
||
/**
|
||
* 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;
|
||
|
||
const fmt = (n) => '$' + parseFloat(n || 0).toLocaleString('es-MX', {
|
||
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 };
|
||
}
|
||
|
||
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;
|
||
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 || {});
|
||
} catch (e) {
|
||
console.error('Load customers failed:', e);
|
||
}
|
||
}
|
||
|
||
function renderTable(customers) {
|
||
const tbody = document.getElementById('customersBody');
|
||
if (!tbody) return;
|
||
tbody.innerHTML = '';
|
||
|
||
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;
|
||
}
|
||
|
||
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.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 = '';
|
||
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>`;
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
function search() {
|
||
clearTimeout(searchTimeout);
|
||
searchTimeout = setTimeout(() => {
|
||
currentPage = 1;
|
||
loadCustomers(1);
|
||
}, 300);
|
||
}
|
||
|
||
// 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;
|
||
|
||
// 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';
|
||
|
||
// Avatar & Header
|
||
const avatarEl = document.getElementById('detailAvatar');
|
||
if (avatarEl) avatarEl.textContent = getInitials(c.name);
|
||
|
||
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
|
||
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));
|
||
|
||
// 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) {
|
||
vehiclesEl.innerHTML = '<span style="color:var(--color-text-muted);font-size:var(--text-body-sm);">Sin vehiculos registrados</span>';
|
||
} else {
|
||
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="color:var(--color-text-muted);">Placas: ${v.plates}</span>` : ''}
|
||
</div>`
|
||
).join('');
|
||
}
|
||
}
|
||
|
||
// Purchases
|
||
const purchasesEl = document.getElementById('panelPurchases');
|
||
if (purchasesEl) {
|
||
const purchases = c.recent_purchases || [];
|
||
if (purchases.length === 0) {
|
||
purchasesEl.innerHTML = '<span style="color:var(--color-text-muted);font-size:var(--text-body-sm);">Sin compras recientes</span>';
|
||
} else {
|
||
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('');
|
||
}
|
||
}
|
||
}
|
||
|
||
function closeDetail() {
|
||
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 = '';
|
||
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';
|
||
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 || '';
|
||
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;
|
||
modal.classList.add('active');
|
||
}
|
||
|
||
function closeModal() {
|
||
const modal = document.getElementById('customerModal');
|
||
if (modal) modal.classList.remove('active');
|
||
}
|
||
|
||
async function save() {
|
||
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: 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 = val('editId');
|
||
|
||
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) {
|
||
selectCustomer(editId);
|
||
}
|
||
} catch (e) {
|
||
alert('Error: ' + e.message);
|
||
}
|
||
}
|
||
|
||
// ─── Statement Modal ─────────────────
|
||
async function showStatement() {
|
||
if (!currentCustomer) return;
|
||
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 ? data.customer.credit_limit : 0)}
|
||
</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: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 => {
|
||
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>
|
||
</tr>`;
|
||
});
|
||
html += '</table>';
|
||
}
|
||
|
||
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 ────────────────────────────
|
||
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, selectCustomer, closeDetail,
|
||
showCreateModal, editCurrent, closeModal, save,
|
||
showStatement, closeStatement,
|
||
showPaymentModal, closePayment, recordPayment,
|
||
};
|
||
})();
|