fix: botón Historial en clientes abre modal con historial completo de compras
Some checks failed
CI / lint-and-test (3.11) (push) Has been cancelled
CI / lint-and-test (3.13) (push) Has been cancelled

This commit is contained in:
2026-06-26 00:28:18 +00:00
parent 2ed672c3c3
commit 9a55e15142
4 changed files with 107 additions and 3 deletions

View File

@@ -140,6 +140,39 @@ def get_customer(customer_id):
return jsonify(customer) return jsonify(customer)
@customers_bp.route('/<int:customer_id>/purchases', methods=['GET'])
@require_auth('customers.view')
def get_customer_purchases(customer_id):
"""Return full purchase history for a customer."""
conn = get_tenant_conn(g.tenant_id)
cur = conn.cursor()
cur.execute("""
SELECT s.id, s.total, s.payment_method, s.sale_type, s.status, s.created_at,
e.name as employee_name
FROM sales s
LEFT JOIN employees e ON s.employee_id = e.id
WHERE s.customer_id = %s
ORDER BY s.created_at DESC
""", (customer_id,))
purchases = []
for r in cur.fetchall():
purchases.append({
'id': r[0],
'total': float(r[1]) if r[1] else 0,
'payment_method': r[2],
'sale_type': r[3],
'status': r[4],
'created_at': str(r[5]),
'employee_name': r[6],
})
cur.close()
conn.close()
return jsonify({'data': purchases})
@customers_bp.route('', methods=['POST']) @customers_bp.route('', methods=['POST'])
@require_auth('customers.create') @require_auth('customers.create')
def create_customer(): def create_customer():

View File

@@ -387,10 +387,80 @@ const Customers = (() => {
if (btns.length >= 2) btns[1].onclick = () => editCurrent(); if (btns.length >= 2) btns[1].onclick = () => editCurrent();
if (btns.length >= 3) btns[2].onclick = () => showStatement(); if (btns.length >= 3) btns[2].onclick = () => showStatement();
if (btns.length >= 4) btns[3].onclick = () => { if (btns.length >= 4) btns[3].onclick = () => {
if (currentCustomer) selectCustomer(currentCustomer.id); if (currentCustomer) showCustomerHistory(currentCustomer.id);
}; };
} }
async function showCustomerHistory(customerId) {
try {
const res = await api(`/pos/api/customers/${customerId}/purchases`);
const purchases = res.data || [];
let modal = document.getElementById('customerHistoryModal');
if (!modal) {
modal = document.createElement('div');
modal.id = 'customerHistoryModal';
modal.className = 'modal-overlay';
modal.style.display = 'none';
modal.innerHTML = `
<div class="modal-content" style="max-width:700px;">
<div class="modal-header">
<h3>Historial de Compras — <span id="customerHistoryName"></span></h3>
<button class="modal-close" onclick="Customers.closeCustomerHistoryModal()">&times;</button>
</div>
<div class="modal-body">
<table class="history-table" style="width:100%;">
<thead>
<tr>
<th>Fecha</th>
<th>Folio</th>
<th>Total</th>
<th>Pago</th>
<th>Estado</th>
</tr>
</thead>
<tbody id="customerHistoryBody"></tbody>
</table>
</div>
</div>
`;
document.body.appendChild(modal);
}
const tbody = document.getElementById('customerHistoryBody');
const title = document.getElementById('customerHistoryName');
if (title && currentCustomer) title.textContent = currentCustomer.name;
if (purchases.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" style="text-align:center;color:var(--color-text-muted);padding:var(--space-4);">Sin compras registradas</td></tr>';
} else {
tbody.innerHTML = purchases.map(p => {
const statusClass = p.status === 'paid' ? 'mbadge--paid' : p.status === 'cancelled' ? 'mbadge--error' : p.status === 'overdue' ? 'mbadge--overdue' : 'mbadge--pending';
const statusLabel = p.status === 'paid' ? 'Pagado' : p.status === 'cancelled' ? 'Cancelado' : p.status === 'overdue' ? 'Vencido' : 'Pendiente';
return `<tr>
<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>${p.payment_method || '-'}</td>
<td><span class="mbadge ${statusClass}">${statusLabel}</span></td>
</tr>`;
}).join('');
}
modal.style.display = 'flex';
modal.classList.add('active');
} catch (e) {
console.error('Error loading customer history:', e);
alert('Error al cargar historial: ' + e.message);
}
}
function closeCustomerHistoryModal() {
const modal = document.getElementById('customerHistoryModal');
if (modal) {
modal.style.display = 'none';
modal.classList.remove('active');
}
}
// ─── Create/Edit Modal ─────────────── // ─── Create/Edit Modal ───────────────
function showCreateModal() { function showCreateModal() {
const modal = document.getElementById('customerModal'); const modal = document.getElementById('customerModal');
@@ -819,6 +889,7 @@ const Customers = (() => {
showCreateModal, editCurrent, editCustomer, closeModal, save, showCreateModal, editCurrent, editCustomer, closeModal, save,
showStatement, closeStatement, showStatement, closeStatement,
showPaymentModal, closePayment, recordPayment, showPaymentModal, closePayment, recordPayment,
showCustomerHistory, closeCustomerHistoryModal,
}; };
// Bulk selection // Bulk selection

View File

@@ -6,7 +6,7 @@
// The fetch handler normalizes static asset URLs (strips ?v= query strings) // The fetch handler normalizes static asset URLs (strips ?v= query strings)
// so templates can use cache-busting query params freely. // so templates can use cache-busting query params freely.
const CACHE_NAME = 'nexus-pos-v23'; const CACHE_NAME = 'nexus-pos-v24';
const APP_SHELL = [ const APP_SHELL = [
'/pos/static/css/tokens.css', '/pos/static/css/tokens.css',

View File

@@ -648,7 +648,7 @@
<script src="/pos/static/js/pos-utils.js?v=2" defer></script> <script src="/pos/static/js/pos-utils.js?v=2" defer></script>
<script src="/pos/static/js/sidebar.js" defer></script> <script src="/pos/static/js/sidebar.js" defer></script>
<script src="/pos/static/js/virtual-scroll.js" defer></script> <script src="/pos/static/js/virtual-scroll.js" defer></script>
<script src="/pos/static/js/customers.js?v=2" defer></script> <script src="/pos/static/js/customers.js?v=3" defer></script>
<script src="/pos/static/js/offline-banner.js" defer></script> <script src="/pos/static/js/offline-banner.js" defer></script>
<script src="/pos/static/js/sync-engine.js" defer></script> <script src="/pos/static/js/sync-engine.js" defer></script>
<script>if('serviceWorker' in navigator){navigator.serviceWorker.register('/pos/sw.js',{scope:'/pos/'});}</script> <script>if('serviceWorker' in navigator){navigator.serviceWorker.register('/pos/sw.js',{scope:'/pos/'});}</script>