fix(audit): corrige errores criticos y mayores, mejora UX/accesibilidad y optimiza rendimiento
Some checks failed
CI / lint-and-test (3.11) (push) Has been cancelled
CI / lint-and-test (3.13) (push) Has been cancelled

- 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:
2026-06-29 23:54:58 +00:00
parent 59a4893e84
commit 2bdeb2973a
61 changed files with 2879 additions and 706 deletions

View File

@@ -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: "📊" });
}
})();