From 073c97406b3077a0d7fe87c79b9179d24147b237 Mon Sep 17 00:00:00 2001 From: consultoria-as Date: Tue, 30 Jun 2026 19:58:45 +0000 Subject: [PATCH] feat(customers): show vehicles and delete customer - get_customer now returns fleet_vehicles assigned to the customer. - Added DELETE /pos/api/customers/ soft-delete (is_active=false) with customers.delete permission. - Added Vehicles section in customer detail panel. - Added Eliminar button in quick actions (owner/admin/customers.delete). --- pos/blueprints/customers_bp.py | 35 +++++++++++++++++++++++ pos/static/css/customers.css | 10 +++++++ pos/static/js/customers.js | 51 +++++++++++++++++++++++++++++++++- pos/templates/customers.html | 21 ++++++++++++-- 4 files changed, 114 insertions(+), 3 deletions(-) diff --git a/pos/blueprints/customers_bp.py b/pos/blueprints/customers_bp.py index 1a9613e..c571f03 100644 --- a/pos/blueprints/customers_bp.py +++ b/pos/blueprints/customers_bp.py @@ -165,6 +165,21 @@ def get_customer(customer_id): float(customer['credit_limit']) - float(customer['credit_balance']), 2 ) + # Fleet vehicles assigned to this customer + cur.execute(""" + SELECT id, plate, vin, make, model, year, current_mileage, color, owner_name, is_active, created_at + FROM fleet_vehicles + WHERE customer_id = %s + ORDER BY is_active DESC, created_at DESC + """, (customer_id,)) + customer['fleet_vehicles'] = [] + for r in cur.fetchall(): + customer['fleet_vehicles'].append({ + 'id': r[0], 'plate': r[1], 'vin': r[2], 'make': r[3], 'model': r[4], + 'year': r[5], 'current_mileage': r[6], 'color': r[7], 'owner_name': r[8], + 'is_active': r[9], 'created_at': str(r[10]) if r[10] else None, + }) + cur.close() conn.close() return jsonify(customer) @@ -303,6 +318,26 @@ def update_customer(customer_id): return jsonify({'message': 'Customer updated'}) +@customers_bp.route('/', methods=['DELETE']) +@require_auth('customers.delete') +def delete_customer(customer_id): + """Soft-delete (deactivate) a customer.""" + conn = get_tenant_conn(g.tenant_id) + cur = conn.cursor() + + cur.execute("SELECT id FROM customers WHERE id = %s", (customer_id,)) + if not cur.fetchone(): + cur.close(); conn.close() + return jsonify({'error': 'Customer not found'}), 404 + + cur.execute("UPDATE customers SET is_active = false WHERE id = %s", (customer_id,)) + log_action(conn, 'CUSTOMER_DEACTIVATE', 'customer', customer_id) + conn.commit() + cur.close() + conn.close() + return jsonify({'message': 'Cliente eliminado'}) + + @customers_bp.route('//statement', methods=['GET']) @require_auth('customers.view') def customer_statement(customer_id): diff --git a/pos/static/css/customers.css b/pos/static/css/customers.css index fe98a56..c156952 100644 --- a/pos/static/css/customers.css +++ b/pos/static/css/customers.css @@ -1209,6 +1209,16 @@ color: #000; } + .action-btn--danger { + color: var(--color-error); + border-color: var(--color-error); + } + + .action-btn--danger:hover { + background-color: var(--color-error); + color: #fff; + } + .action-btn__icon { width: 20px; height: 20px; diff --git a/pos/static/js/customers.js b/pos/static/js/customers.js index 2bc1d84..e2c17d0 100644 --- a/pos/static/js/customers.js +++ b/pos/static/js/customers.js @@ -11,6 +11,11 @@ const Customers = (() => { let currentCustomer = null; let searchTimeout = null; + const user = window.POS_USER || {}; + const userRole = (user.role || '').toLowerCase(); + const userPerms = user.permissions || []; + const canDeleteCustomer = userRole === 'owner' || userRole === 'admin' || userPerms.includes('customers.delete'); + const fmt = (n) => '$' + parseFloat(n || 0).toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); @@ -295,6 +300,28 @@ const Customers = (() => { const discountEl = document.getElementById('detailMaxDiscount'); if (discountEl) discountEl.textContent = (c.max_discount_pct || 0) + '%'; + // Vehicles + const vehiclesEl = document.getElementById('detailVehicles'); + if (vehiclesEl) { + const vehicles = c.fleet_vehicles || []; + if (vehicles.length === 0) { + vehiclesEl.innerHTML = 'Sin vehículos registrados'; + } else { + vehiclesEl.innerHTML = vehicles.map(v => { + const title = [v.year, v.make, v.model].filter(Boolean).join(' '); + const subtitle = [v.plate, v.vin, v.color].filter(Boolean).join(' · '); + return `
+
${title || 'Vehículo'}
+
${subtitle}
+
`; + }).join(''); + } + } + + // Delete button visibility + const btnDelete = document.getElementById('btnDeleteCustomer'); + if (btnDelete) btnDelete.style.display = canDeleteCustomer ? 'inline-flex' : 'none'; + // Re-wire action buttons after detail panel is visible wireActionButtons(); @@ -396,7 +423,7 @@ const Customers = (() => { // Wire action buttons in detail panel function wireActionButtons() { const btns = document.querySelectorAll('.quick-actions .action-btn'); - // Order: Nueva Venta, Editar, Estado de Cuenta, Historial + // Order: Nueva Venta, Editar, Estado de Cuenta, Historial, Eliminar if (btns.length >= 1) btns[0].onclick = () => { if (currentCustomer) window.location.href = '/pos/sale?customer=' + currentCustomer.id; }; @@ -405,8 +432,29 @@ const Customers = (() => { if (btns.length >= 4) btns[3].onclick = () => { if (currentCustomer) showCustomerHistory(currentCustomer.id); }; + const btnDelete = document.getElementById('btnDeleteCustomer'); + if (btnDelete) btnDelete.onclick = () => deleteCustomer(); } + async function deleteCustomer() { + if (!currentCustomer) return; + if (!canDeleteCustomer) { + alert('No tienes permiso para eliminar clientes'); + return; + } + if (!confirm(`¿Eliminar al cliente "${currentCustomer.name}"? Se marcará como inactivo.`)) return; + try { + await api(`/pos/api/customers/${currentCustomer.id}`, { method: 'DELETE' }); + alert('Cliente eliminado'); + currentCustomer = null; + closeDetail(); + loadCustomers(currentPage); + } catch (e) { + alert('Error: ' + e.message); + } + } + window.deleteCustomer = deleteCustomer; + async function showCustomerHistory(customerId) { try { const res = await api(`/pos/api/customers/${customerId}/purchases`); @@ -906,6 +954,7 @@ const Customers = (() => { showStatement, closeStatement, showPaymentModal, closePayment, recordPayment, showCustomerHistory, closeCustomerHistoryModal, + deleteCustomer, }; // Register Cmd+K items diff --git a/pos/templates/customers.html b/pos/templates/customers.html index 39e5b6c..6abf0f3 100644 --- a/pos/templates/customers.html +++ b/pos/templates/customers.html @@ -15,7 +15,7 @@ - + @@ -451,6 +451,14 @@ + +
+
Vehículos
+
+ Sin vehículos registrados +
+
+
Acciones Rápidas
@@ -489,6 +497,15 @@ Historial +
@@ -639,7 +656,7 @@ - +