fix(audit): corrige errores criticos y mayores, mejora UX/accesibilidad y optimiza rendimiento
- Arregla @require_auth, permisos, race conditions, locks de caja/stock - Elimina N+1 en layaway, flotilla, dashboard y global_invoice - Asegura folios atomicos para CFDI, ordenes de servicio y polizas - Protege client_secret de MercadoLibre en backend - Conecta botones/filtros de config, customers, accounting e invoicing - Mejora accesibilidad (labels/aria-label) y estados de carga/vacio - Limpia accounting.js obsoleto y consolida accounting.v9.js - Actualiza cache busting a v32 y Service Worker a v32 - Documenta todo en docs/AUDIT_Y_MEJORAS_2026-06-15.md Tests: 35 passed
This commit is contained in:
@@ -21,10 +21,15 @@ const POS = (() => {
|
||||
let paymentMethod = 'efectivo';
|
||||
let canViewCost = false;
|
||||
let employeeMaxDiscount = 100;
|
||||
let lastSaleId = null;
|
||||
let lastSaleId = sessionStorage.getItem('pos_last_sale_id') || null;
|
||||
let lastSaleData = null;
|
||||
let searchTimeout = null;
|
||||
let customerSearchTimeout = null;
|
||||
let canCancel = false;
|
||||
let canDiscount = false;
|
||||
let canEditPrice = false;
|
||||
let canCreateWorkshopOrder = false;
|
||||
let canCreateLayaway = false;
|
||||
|
||||
// Currency-aware formatter: reads pos_currency from localStorage
|
||||
const _posCurrency = localStorage.getItem('pos_currency') || 'MXN';
|
||||
@@ -54,6 +59,25 @@ const POS = (() => {
|
||||
setTimeout(() => el.remove(), 2100);
|
||||
}
|
||||
|
||||
// ─── Permission-based UI toggles ─────
|
||||
function applyPermissionsUI() {
|
||||
function hide(selector) {
|
||||
const el = document.querySelector(selector);
|
||||
if (el) el.style.display = 'none';
|
||||
}
|
||||
if (!canCancel) {
|
||||
hide('#btnCancelSale');
|
||||
hide('#fkeyEsc');
|
||||
}
|
||||
if (!canDiscount) hide('[onclick="POS.applyDiscount()"]');
|
||||
if (!canEditPrice) hide('[onclick="POS.modifyPrice()"]');
|
||||
if (!canCreateWorkshopOrder) {
|
||||
hide('[onclick="POS.createServiceOrder()"]');
|
||||
hide('[title="Orden de servicio"]');
|
||||
}
|
||||
if (!canCreateLayaway) hide('[onclick="POS.createLayaway()"]');
|
||||
}
|
||||
|
||||
// ─── Init ────────────────────────────
|
||||
async function init() {
|
||||
// Parse JWT to get employee info
|
||||
@@ -61,7 +85,13 @@ const POS = (() => {
|
||||
const payload = JSON.parse(atob(token.split('.')[1]));
|
||||
document.getElementById('employeeName').textContent = payload.name || 'Empleado';
|
||||
document.getElementById('branchName').textContent = payload.branch_name || '';
|
||||
canViewCost = (payload.permissions || []).includes('pos.view_cost');
|
||||
const perms = payload.permissions || [];
|
||||
canViewCost = perms.includes('pos.view_cost');
|
||||
canCancel = perms.includes('pos.cancel');
|
||||
canDiscount = perms.includes('pos.discount');
|
||||
canEditPrice = perms.includes('config.edit_prices');
|
||||
canCreateWorkshopOrder = perms.includes('workshop.edit');
|
||||
canCreateLayaway = perms.includes('pos.sell');
|
||||
employeeMaxDiscount = payload.max_discount_pct || 100;
|
||||
|
||||
// Show cost/margin columns and toggle button if permission
|
||||
@@ -78,6 +108,8 @@ const POS = (() => {
|
||||
const parts = payload.name.split(' ');
|
||||
avatar.textContent = parts.map(p => p[0]).join('').substring(0, 2).toUpperCase();
|
||||
}
|
||||
|
||||
applyPermissionsUI();
|
||||
} catch (e) {
|
||||
console.warn('Could not parse token:', e);
|
||||
}
|
||||
@@ -278,6 +310,7 @@ const POS = (() => {
|
||||
}
|
||||
|
||||
function openCancelModal() {
|
||||
if (!canCancel) { showToast('No tienes permiso para cancelar'); return; }
|
||||
const overlay = document.getElementById('overlay-cancelar-venta');
|
||||
const dialog = document.getElementById('modal-cancelar-venta');
|
||||
if (overlay) overlay.classList.add('active');
|
||||
@@ -307,6 +340,7 @@ const POS = (() => {
|
||||
}
|
||||
|
||||
function applyDiscount() {
|
||||
if (!canDiscount) { showToast('No tienes permiso para aplicar descuentos'); return; }
|
||||
if (selectedRow < 0 || selectedRow >= cart.length) {
|
||||
showToast('Selecciona un articulo primero', 'warn');
|
||||
return;
|
||||
@@ -322,6 +356,7 @@ const POS = (() => {
|
||||
}
|
||||
|
||||
function modifyPrice() {
|
||||
if (!canEditPrice) { showToast('No tienes permiso para modificar precios'); return; }
|
||||
if (selectedRow < 0 || selectedRow >= cart.length) {
|
||||
showToast('Selecciona un articulo primero', 'warn');
|
||||
return;
|
||||
@@ -952,6 +987,7 @@ const POS = (() => {
|
||||
|
||||
lastSaleId = sale.id;
|
||||
lastSaleData = sale;
|
||||
try { sessionStorage.setItem('pos_last_sale_id', sale.id); } catch(e) {}
|
||||
closePaymentModal();
|
||||
showTicket(sale);
|
||||
|
||||
@@ -1006,6 +1042,7 @@ const POS = (() => {
|
||||
|
||||
lastSaleId = sale.id;
|
||||
lastSaleData = sale;
|
||||
try { sessionStorage.setItem('pos_last_sale_id', sale.id); } catch(e) {}
|
||||
showTicket(sale);
|
||||
cart = [];
|
||||
selectedRow = -1;
|
||||
@@ -1057,6 +1094,7 @@ const POS = (() => {
|
||||
|
||||
// ─── Layaway ─────────────────────────
|
||||
async function createLayaway() {
|
||||
if (!canCreateLayaway) { showToast('No tienes permiso para crear apartados'); return; }
|
||||
if (cart.length === 0) { alert('Carrito vacio'); return; }
|
||||
if (!currentCustomer) { alert('Seleccione un cliente para apartado'); return; }
|
||||
|
||||
@@ -1097,6 +1135,132 @@ const POS = (() => {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Service Order from POS ──────────
|
||||
function createServiceOrder() {
|
||||
if (!canCreateWorkshopOrder) { showToast('No tienes permiso para ordenes de taller'); return; }
|
||||
if (cart.length === 0) { showToast('Carrito vacio'); return; }
|
||||
const custInput = document.getElementById('soCustomer');
|
||||
if (custInput) custInput.value = currentCustomer ? currentCustomer.name : 'Publico General';
|
||||
document.getElementById('serviceOrderModal').classList.add('open');
|
||||
}
|
||||
|
||||
function closeServiceOrderModal() {
|
||||
document.getElementById('serviceOrderModal').classList.remove('open');
|
||||
}
|
||||
|
||||
async function confirmServiceOrder() {
|
||||
const customerId = currentCustomer ? currentCustomer.id : null;
|
||||
if (!customerId) { alert('Seleccione un cliente para la orden de servicio'); return; }
|
||||
|
||||
const vehicleText = document.getElementById('soVehicle').value.trim();
|
||||
const delivery = document.getElementById('soDelivery').value;
|
||||
const notes = document.getElementById('soNotes').value.trim();
|
||||
const isDirect = document.getElementById('soDirect').checked;
|
||||
|
||||
const items = cart.map(item => ({
|
||||
inventory_id: item.inventory_id,
|
||||
part_number: item.part_number,
|
||||
name: item.name,
|
||||
quantity: item.quantity,
|
||||
unit_price: item.unit_price,
|
||||
status: 'pending'
|
||||
}));
|
||||
|
||||
try {
|
||||
const order = await api('/pos/api/service-orders/from-pos', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
customer_id: customerId,
|
||||
priority: 'normal',
|
||||
reception_notes: (vehicleText ? 'Vehiculo: ' + vehicleText + '. ' : '') + (notes || ''),
|
||||
delivery_method: delivery || null,
|
||||
is_direct: isDirect,
|
||||
items: items
|
||||
})
|
||||
});
|
||||
|
||||
showToast(`Orden de servicio ${order.order_number} creada`);
|
||||
closeServiceOrderModal();
|
||||
cart = [];
|
||||
selectedRow = -1;
|
||||
renderCart();
|
||||
showServiceOrderTicket(order);
|
||||
} catch (e) {
|
||||
alert('Error: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function showServiceOrderTicket(order) {
|
||||
const dateStr = new Date(order.created_at).toLocaleString('es-MX', {
|
||||
year: 'numeric', month: 'short', day: 'numeric',
|
||||
hour: '2-digit', minute: '2-digit'
|
||||
});
|
||||
|
||||
const customerName = order.customer_name || (currentCustomer ? currentCustomer.name : 'Publico General');
|
||||
let itemsHtml = '';
|
||||
(order.items || []).forEach(item => {
|
||||
const itemTotal = item.unit_price * item.quantity;
|
||||
itemsHtml += `
|
||||
<div class="item-line-wide ticket-line">
|
||||
<span class="qty">${item.quantity}</span>
|
||||
<span class="name">${item.name || ''}</span>
|
||||
<span class="price">${fmt(item.unit_price)}</span>
|
||||
<span class="subtotal">${fmt(itemTotal)}</span>
|
||||
</div>`;
|
||||
});
|
||||
|
||||
const ticketHtml = `
|
||||
<div class="store-name">NEXUS AUTOPARTS</div>
|
||||
<div class="store-tagline">Orden de servicio</div>
|
||||
<div class="store-info">
|
||||
Sucursal: ${currentRegister ? currentRegister.branch_name || '' : ''}<br>
|
||||
RFC: NAU210315XX1
|
||||
</div>
|
||||
<hr class="divider-double">
|
||||
<div class="folio-line">
|
||||
<span>ORDEN: ${order.order_number}</span>
|
||||
<span>${dateStr}</span>
|
||||
</div>
|
||||
<div class="ticket-row" style="font-size: 9px; color: #555; margin-bottom: 4px;">
|
||||
<span>Cliente: ${customerName}</span>
|
||||
</div>
|
||||
<hr class="divider">
|
||||
<div class="item-line-wide" style="font-weight: bold; font-size: 9px; color: #555; text-transform: uppercase;">
|
||||
<span class="qty">Cant</span>
|
||||
<span class="name">Descripcion</span>
|
||||
<span class="price">P. Unit</span>
|
||||
<span class="subtotal">Importe</span>
|
||||
</div>
|
||||
<hr class="divider" style="margin: 2px 0;">
|
||||
${itemsHtml}
|
||||
<hr class="divider-double">
|
||||
<div class="total-section">
|
||||
<div class="total-line">
|
||||
<span>Refacciones:</span><span>${fmt(order.total_parts || 0)}</span>
|
||||
</div>
|
||||
<div class="total-line">
|
||||
<span>Mano de obra:</span><span>${fmt(order.total_labor || 0)}</span>
|
||||
</div>
|
||||
<div class="total-line grand">
|
||||
<span>TOTAL:</span><span>${fmt(order.total || 0)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="divider">
|
||||
<div class="footer-section">
|
||||
<div class="thanks">Gracias por su preferencia!</div>
|
||||
<div>Conserve su ticket como comprobante.</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const printArea = document.getElementById('ticketContent');
|
||||
if (printArea) printArea.innerHTML = ticketHtml;
|
||||
const preview = document.getElementById('ticketPreviewContent');
|
||||
if (preview) preview.innerHTML = ticketHtml;
|
||||
const modalHeader = document.querySelector('#ticketModal .modal-header h3');
|
||||
if (modalHeader) modalHeader.textContent = 'Ticket de Orden de Servicio';
|
||||
document.getElementById('ticketModal').classList.add('open');
|
||||
}
|
||||
|
||||
// ─── Ticket ──────────────────────────
|
||||
function showTicket(sale) {
|
||||
const dateStr = new Date(sale.created_at).toLocaleString('es-MX', {
|
||||
@@ -1111,7 +1275,7 @@ const POS = (() => {
|
||||
(sale.items || []).forEach(item => {
|
||||
const itemTotal = (item.unit_price * item.quantity * (1 - (item.discount_pct || 0) / 100));
|
||||
itemsHtml += `
|
||||
<div class="item-line-wide">
|
||||
<div class="item-line-wide ticket-line">
|
||||
<span class="qty">${item.quantity}</span>
|
||||
<span class="name">${item.name || ''}</span>
|
||||
<span class="price">${fmt(item.unit_price)}</span>
|
||||
@@ -1182,6 +1346,8 @@ const POS = (() => {
|
||||
if (printArea) printArea.innerHTML = ticketHtml;
|
||||
const preview = document.getElementById('ticketPreviewContent');
|
||||
if (preview) preview.innerHTML = ticketHtml;
|
||||
const modalHeader = document.querySelector('#ticketModal .modal-header h3');
|
||||
if (modalHeader) modalHeader.textContent = 'Ticket de Venta';
|
||||
|
||||
document.getElementById('ticketModal').classList.add('open');
|
||||
}
|
||||
@@ -1286,6 +1452,10 @@ const POS = (() => {
|
||||
e.preventDefault();
|
||||
openDrawer();
|
||||
break;
|
||||
case 'F7':
|
||||
e.preventDefault();
|
||||
createServiceOrder();
|
||||
break;
|
||||
case 'Escape':
|
||||
e.preventDefault();
|
||||
if (document.getElementById('paymentModal').classList.contains('open')) {
|
||||
@@ -1294,6 +1464,8 @@ const POS = (() => {
|
||||
closeNewCustomerModal();
|
||||
} else if (document.getElementById('ticketModal').classList.contains('open')) {
|
||||
closeTicketModal();
|
||||
} else if (document.getElementById('serviceOrderModal').classList.contains('open')) {
|
||||
closeServiceOrderModal();
|
||||
} else if (document.querySelector('.confirm-overlay.active')) {
|
||||
// Close cancel modal
|
||||
const overlay = document.getElementById('overlay-cancelar-venta');
|
||||
@@ -1361,6 +1533,14 @@ const POS = (() => {
|
||||
});
|
||||
}
|
||||
|
||||
// Register Cmd+K items
|
||||
if (typeof registerCmdKItem === "function") {
|
||||
registerCmdKItem({ group: "Principal", label: "POS Ventas", href: "/pos/sale", icon: "🛒" });
|
||||
registerCmdKItem({ group: "Principal", label: "Catálogo", href: "/pos/catalog", icon: "📁" });
|
||||
registerCmdKItem({ group: "Principal", label: "Clientes", href: "/pos/customers", icon: "👤" });
|
||||
registerCmdKItem({ group: "Principal", label: "Dashboard", href: "/pos/dashboard", icon: "📊" });
|
||||
}
|
||||
|
||||
// ─── Public API ──────────────────────
|
||||
init();
|
||||
|
||||
@@ -1373,6 +1553,7 @@ const POS = (() => {
|
||||
checkout, confirmPayment, closePaymentModal,
|
||||
selectPaymentMethod, updateChange, updateMixedTotal,
|
||||
creditSale, saveQuotation, createLayaway,
|
||||
createServiceOrder, closeServiceOrderModal, confirmServiceOrder, showServiceOrderTicket,
|
||||
showLastSale, openDrawer,
|
||||
showTicket, closeTicketModal, printTicket,
|
||||
connectThermal, thermalPrint,
|
||||
@@ -1380,12 +1561,5 @@ const POS = (() => {
|
||||
showCutZModal, closeCutZModal, loadCutX, confirmCutZ,
|
||||
openCancelModal, closeCancelModal, changeQuantity, applyDiscount, modifyPrice,
|
||||
};
|
||||
// Register Cmd+K items
|
||||
if (typeof registerCmdKItem === "function") {
|
||||
registerCmdKItem({ group: "Principal", label: "POS Ventas", href: "/pos/sale", icon: "🛒" });
|
||||
registerCmdKItem({ group: "Principal", label: "Catálogo", href: "/pos/catalog", icon: "📁" });
|
||||
registerCmdKItem({ group: "Principal", label: "Clientes", href: "/pos/customers", icon: "👤" });
|
||||
registerCmdKItem({ group: "Principal", label: "Dashboard", href: "/pos/dashboard", icon: "📊" });
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user