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)
|
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():
|
||||||
|
|||||||
@@ -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()">×</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
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user