fix: botón Historial en clientes abre modal con historial completo de compras
This commit is contained in:
@@ -140,6 +140,39 @@ def get_customer(customer_id):
|
||||
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'])
|
||||
@require_auth('customers.create')
|
||||
def create_customer():
|
||||
|
||||
@@ -387,10 +387,80 @@ const Customers = (() => {
|
||||
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);
|
||||
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()">×</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 ───────────────
|
||||
function showCreateModal() {
|
||||
const modal = document.getElementById('customerModal');
|
||||
@@ -819,6 +889,7 @@ const Customers = (() => {
|
||||
showCreateModal, editCurrent, editCustomer, closeModal, save,
|
||||
showStatement, closeStatement,
|
||||
showPaymentModal, closePayment, recordPayment,
|
||||
showCustomerHistory, closeCustomerHistoryModal,
|
||||
};
|
||||
|
||||
// Bulk selection
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// The fetch handler normalizes static asset URLs (strips ?v= query strings)
|
||||
// so templates can use cache-busting query params freely.
|
||||
|
||||
const CACHE_NAME = 'nexus-pos-v23';
|
||||
const CACHE_NAME = 'nexus-pos-v24';
|
||||
|
||||
const APP_SHELL = [
|
||||
'/pos/static/css/tokens.css',
|
||||
|
||||
@@ -648,7 +648,7 @@
|
||||
<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/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/sync-engine.js" defer></script>
|
||||
<script>if('serviceWorker' in navigator){navigator.serviceWorker.register('/pos/sw.js',{scope:'/pos/'});}</script>
|
||||
|
||||
Reference in New Issue
Block a user