feat(customers): show vehicles and delete customer
- get_customer now returns fleet_vehicles assigned to the customer. - Added DELETE /pos/api/customers/<id> 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).
This commit is contained in:
@@ -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('/<int:customer_id>', 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('/<int:customer_id>/statement', methods=['GET'])
|
||||
@require_auth('customers.view')
|
||||
def customer_statement(customer_id):
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = '<span style="color:var(--color-text-muted);">Sin vehículos registrados</span>';
|
||||
} 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 `<div class="vehicle-row" style="padding:var(--space-2);border:1px solid var(--color-border);border-radius:var(--radius-md);">
|
||||
<div style="font-weight:600;">${title || 'Vehículo'}</div>
|
||||
<div style="font-size:var(--text-caption);color:var(--color-text-muted);">${subtitle}</div>
|
||||
</div>`;
|
||||
}).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
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<meta name="theme-color" content="#F5A623" />
|
||||
<link rel="shortcut icon" type="image/png" href="/pos/static/pwa/icon-192.png" />
|
||||
|
||||
<link rel="stylesheet" href="/pos/static/css/customers.css"></head>
|
||||
<link rel="stylesheet" href="/pos/static/css/customers.css?v=34"></head>
|
||||
|
||||
<body>
|
||||
|
||||
@@ -451,6 +451,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Vehicles -->
|
||||
<div class="detail-section">
|
||||
<div class="detail-section__title">Vehículos</div>
|
||||
<div id="detailVehicles" style="display:flex;flex-direction:column;gap:var(--space-2);">
|
||||
<span style="color:var(--color-text-muted);">Sin vehículos registrados</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="detail-section">
|
||||
<div class="detail-section__title">Acciones Rápidas</div>
|
||||
@@ -489,6 +497,15 @@
|
||||
</span>
|
||||
Historial
|
||||
</button>
|
||||
<button class="action-btn action-btn--danger" id="btnDeleteCustomer" onclick="deleteCustomer()">
|
||||
<span class="action-btn__icon">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round">
|
||||
<polyline points="3 6 5 6 13 6"/><path d="M5 6v7a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2V6"/>
|
||||
<line x1="7" y1="3" x2="9" y2="3"/><line x1="4" y1="3" x2="14" y2="3"/>
|
||||
</svg>
|
||||
</span>
|
||||
Eliminar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -639,7 +656,7 @@
|
||||
<script src="/pos/static/js/pos-utils.js?v=33" defer></script>
|
||||
<script src="/pos/static/js/sidebar.js?v=36" defer></script>
|
||||
<script src="/pos/static/js/virtual-scroll.js" defer></script>
|
||||
<script src="/pos/static/js/customers.js?v=33" defer></script>
|
||||
<script src="/pos/static/js/customers.js?v=34" 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