Files
Autoparts-DB/pos/templates/remission_notes.html
consultoria-as 0bd5ee8ee8
Some checks failed
CI / lint-and-test (3.11) (push) Has been cancelled
CI / lint-and-test (3.13) (push) Has been cancelled
fix(permissions): honor module permissions for cashier/counter in sidebar and page guard
2026-07-03 08:10:22 +00:00

437 lines
20 KiB
HTML

<!DOCTYPE html>
<html lang="es">
<head>
<script>/*pos_theme_early*/(function(){var t=localStorage.getItem("pos_theme")||"industrial";document.documentElement.setAttribute("data-theme",t);})()</script>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Notas de Remisión — Nexus Autoparts POS</title>
<link rel="stylesheet" href="/pos/static/css/chat.css" />
<link rel="stylesheet" href="/pos/static/css/tokens.css" />
<link rel="stylesheet" href="/pos/static/css/common.css" />
<link rel="stylesheet" href="/pos/static/css/pos-ui.css?v=33" />
<link rel="stylesheet" href="/pos/static/css/sidebar.css" />
<link rel="stylesheet" href="/pos/static/css/pos-glass.css" />
<link rel="stylesheet" href="/pos/static/css/inventory.css?v=34" />
<link rel="stylesheet" href="/pos/static/css/remission_notes.css?v=1" />
<link rel="manifest" href="/pos/static/pwa/manifest.json" />
<meta name="theme-color" content="#F5A623" />
<link rel="shortcut icon" type="image/png" href="/pos/static/pwa/icon-192.png" />
</head>
<body>
<!-- THEME BAR -->
<header class="theme-bar" role="banner">
<div class="theme-bar__left">
<div class="theme-bar__store">
<span class="theme-bar__dot"></span>
Nexus Autoparts
</div>
<div class="theme-bar__sep"></div>
<span class="theme-bar__label">Sucursal Centro &mdash; Usuario: H. García</span>
</div>
<div class="theme-bar__right">
<span class="theme-bar__label">Tema:</span>
<button class="theme-btn theme-btn--industrial is-active" data-theme-target="industrial" onclick="setTheme('industrial')">
<span class="theme-btn__swatch"></span>
Industrial
</button>
<button class="theme-btn theme-btn--modern" data-theme-target="modern" onclick="setTheme('modern')">
<span class="theme-btn__swatch"></span>
Moderno
</button>
</div>
</header>
<!-- APP SHELL -->
<div class="app-shell">
<!-- SIDEBAR -->
<aside class="sidebar" role="navigation" aria-label="Navegación principal">
<div class="sidebar__brand">
<div class="brand-logo">NA</div>
<div class="brand-name">
<span class="brand-name__primary">Nexus</span>
<span class="brand-name__sub">Autoparts POS</span>
</div>
</div>
<nav class="sidebar__nav">
<div class="nav-section-label">Principal</div>
<a class="nav-item" href="/pos/dashboard">
<svg class="nav-item__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/>
<rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/>
</svg>
<span>Dashboard</span>
</a>
<a class="nav-item" href="/pos/sale">
<svg class="nav-item__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/>
</svg>
<span>POS</span>
</a>
<a class="nav-item" href="/pos/catalog">
<svg class="nav-item__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 6h16M4 10h16M4 14h16M4 18h16"/>
</svg>
<span>Catálogo</span>
</a>
<a class="nav-item" href="/pos/inventory">
<svg class="nav-item__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
<polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/>
</svg>
<span>Inventario</span>
</a>
<div class="nav-section-label">Gestión</div>
<a class="nav-item" href="/pos/customers">
<svg class="nav-item__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M23 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>
<span>Clientes</span>
</a>
<a class="nav-item is-active" href="/pos/remission-notes" aria-current="page">
<svg class="nav-item__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
<line x1="16" y1="13" x2="8" y2="13"/>
<line x1="16" y1="17" x2="8" y2="17"/>
<polyline points="10 9 9 9 8 9"/>
</svg>
<span>Notas de Remisión</span>
</a>
<a class="nav-item" href="/pos/invoicing">
<svg class="nav-item__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
<line x1="16" y1="13" x2="8" y2="13"/>
<line x1="16" y1="17" x2="8" y2="17"/>
</svg>
<span>Facturación</span>
</a>
<a class="nav-item" href="/pos/reports">
<svg class="nav-item__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/>
<line x1="6" y1="20" x2="6" y2="14"/>
</svg>
<span>Reportes</span>
</a>
<div class="nav-section-label">Sistema</div>
<a class="nav-item" href="/pos/config">
<svg class="nav-item__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="3"/>
<path d="M19.07 4.93a10 10 0 0 1 0 14.14M4.93 4.93a10 10 0 0 0 0 14.14"/>
</svg>
<span>Configuración</span>
</a>
</nav>
<div class="sidebar__footer">
<div class="sidebar__user-avatar">HG</div>
<div class="sidebar__user-info">
<div class="sidebar__user-name">Hugo García</div>
<div class="sidebar__user-role">Administrador</div>
</div>
</div>
</aside>
<!-- MAIN CONTENT -->
<main class="main" role="main">
<div class="page-header">
<div class="page-header__title-group">
<span class="page-header__eyebrow">Ventas</span>
<h1 class="page-header__title">Notas de Remisión</h1>
</div>
</div>
<div class="page-content">
<!-- Filters -->
<div class="filters-card">
<div class="toolbar">
<div class="search-box">
<svg viewBox="0 0 24 24" stroke-linecap="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
<input type="text" id="filterFolio" placeholder="Buscar folio NR-XXXX..." />
</div>
<div class="search-box">
<svg viewBox="0 0 24 24" stroke-linecap="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
<input type="text" id="filterCustomer" placeholder="Cliente..." />
</div>
<select class="select-filter" id="filterStatus">
<option value="">Todos los estados</option>
<option value="pending_payment" selected>Pendientes</option>
<option value="completed">Pagadas</option>
<option value="cancelled">Canceladas</option>
</select>
<select class="select-filter" id="filterCourier">
<option value="">Todos los repartidores</option>
</select>
<input type="date" class="select-filter" id="dateFrom" />
<input type="date" class="select-filter" id="dateTo" />
<div class="toolbar__spacer"></div>
<button class="btn btn--primary" onclick="loadData(1)">
<svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
Buscar
</button>
</div>
</div>
<!-- Table -->
<div class="table-wrapper">
<table class="data-table" id="remissionTable">
<thead>
<tr>
<th>Folio</th>
<th>Fecha</th>
<th>Cliente</th>
<th>Vendedor</th>
<th>Repartidor</th>
<th style="text-align:right">Total</th>
<th>Estado</th>
<th style="text-align:right">Acciones</th>
</tr>
</thead>
<tbody id="remissionTableBody">
<tr><td colspan="8" style="text-align:center;padding:var(--space-8);">Cargando...</td></tr>
</tbody>
</table>
<div class="table-footer">
<div class="pagination" id="pagination"></div>
</div>
</div>
</div>
</main>
</div>
<!-- Ticket Modal -->
<div class="modal-overlay" id="ticketModal" onclick="if(event.target===this) closeTicketModal()">
<div class="modal">
<div class="modal__header">
<h3 class="modal__title">Vista previa</h3>
<button class="modal__close" onclick="closeTicketModal()">&times;</button>
</div>
<div class="modal__body">
<div id="ticketContent" class="ticket-preview"></div>
</div>
<div class="modal__footer">
<button class="btn btn--ghost" onclick="closeTicketModal()">Cerrar</button>
<button class="btn btn--primary" onclick="printTicket()">
<svg viewBox="0 0 24 24"><polyline points="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8"/></svg>
Imprimir
</button>
</div>
</div>
</div>
<script src="/pos/static/js/i18n.js?v=39" defer></script>
<script src="/pos/static/js/app-init.js?v=8" defer></script>
<script src="/pos/static/js/sidebar.js?v=46" defer></script>
<script>
const token = localStorage.getItem('pos_token') || '';
let currentPage = 1;
let couriers = [];
function headers() {
return { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token };
}
async function api(url, options = {}) {
options.headers = headers();
const res = await fetch(url, options);
const data = await res.json();
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
return data;
}
const fmt = (n) => '$' + parseFloat(n || 0).toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
function statusLabel(status) {
return { pending_payment: 'Pendiente', completed: 'Pagada', cancelled: 'Cancelada' }[status] || status;
}
function statusClass(status) {
return { pending_payment: 'badge--pending_payment', completed: 'badge--completed', cancelled: 'badge--cancelled' }[status] || '';
}
async function loadCouriers() {
try {
const data = await api('/pos/api/logistics/couriers');
couriers = data.couriers || [];
const sel = document.getElementById('filterCourier');
const current = sel.value;
sel.innerHTML = '<option value="">Todos los repartidores</option>' + couriers.map(c => `<option value="${c.id}">${c.name}</option>`).join('');
sel.value = current;
} catch (e) {
console.warn('No se pudieron cargar repartidores', e);
}
}
async function loadData(page) {
currentPage = page;
const tbody = document.getElementById('remissionTableBody');
tbody.innerHTML = '<tr><td colspan="8" style="text-align:center;padding:var(--space-8);color:var(--color-text-muted);">Cargando...</td></tr>';
const params = new URLSearchParams();
params.set('sale_type', 'counter_remission');
params.set('per_page', '50');
params.set('page', String(page));
const status = document.getElementById('filterStatus').value;
if (status) params.set('status', status);
const folio = document.getElementById('filterFolio').value.trim();
if (folio) params.set('q', folio.replace(/^NR-/i, ''));
const customer = document.getElementById('filterCustomer').value.trim();
if (customer) params.set('customer', customer);
const courier = document.getElementById('filterCourier').value;
if (courier) params.set('courier_id', courier);
const from = document.getElementById('dateFrom').value;
const to = document.getElementById('dateTo').value;
if (from) params.set('date_from', from);
if (to) params.set('date_to', to);
try {
const res = await api('/pos/api/sales?' + params.toString());
render(res.data || [], res.pagination || {});
} catch (e) {
tbody.innerHTML = `<tr><td colspan="8" style="text-align:center;padding:var(--space-8);color:var(--color-error);">Error: ${e.message}</td></tr>`;
}
}
function render(rows, pagination) {
const tbody = document.getElementById('remissionTableBody');
const pag = document.getElementById('pagination');
if (!rows.length) {
tbody.innerHTML = `<tr><td colspan="8">
<div class="empty-state">
<div class="empty-state__title">No hay notas de remisión</div>
<div class="empty-state__subtitle">Ajusta los filtros o genera una nueva nota desde el POS.</div>
</div>
</td></tr>`;
pag.innerHTML = '';
return;
}
tbody.innerHTML = rows.map(r => {
const canPay = r.status === 'pending_payment';
return `
<tr>
<td class="td--mono">NR-${r.id}</td>
<td>${new Date(r.created_at).toLocaleString('es-MX')}</td>
<td class="td--primary">${r.customer_name || 'Público General'}</td>
<td>${r.employee_name || '-'}</td>
<td>${r.courier_name || '-'}</td>
<td class="td--amount" style="text-align:right">${fmt(r.total)}</td>
<td><span class="badge ${statusClass(r.status)}">${statusLabel(r.status)}</span></td>
<td style="text-align:right">
<div style="display:flex;justify-content:flex-end;gap:var(--space-2);">
<button class="action-btn action-btn--ghost" onclick="viewTicket(${r.id})" title="Imprimir">
<svg viewBox="0 0 24 24"><polyline points="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8"/></svg>
</button>
${canPay ? `<button class="action-btn action-btn--primary" onclick="payNote(${r.id}, ${r.total})" title="Cobrar">
<svg viewBox="0 0 24 24"><rect x="2" y="6" width="20" height="12" rx="2"/><path d="M6 10h.01M6 14h.01"/></svg>
</button>` : ''}
</div>
</td>
</tr>
`;
}).join('');
if (pagination.total_pages > 1) {
pag.innerHTML = `
<button class="page-btn" onclick="loadData(${pagination.page - 1})" ${pagination.page <= 1 ? 'disabled' : ''}>&larr;</button>
<span style="color:var(--color-text-muted);font-size:var(--text-caption);">Página ${pagination.page} de ${pagination.total_pages}</span>
<button class="page-btn" onclick="loadData(${pagination.page + 1})" ${pagination.page >= pagination.total_pages ? 'disabled' : ''}>&rarr;</button>
`;
} else {
pag.innerHTML = '';
}
}
async function viewTicket(saleId) {
try {
const data = await api('/pos/api/sales/' + saleId + '/print-remission', { method: 'POST', body: '{}' });
const dateStr = new Date(data.date).toLocaleString('es-MX');
const itemsHtml = (data.items || []).map(it => `
<div class="ticket-preview__item"><span>${it.quantity} x ${it.name}</span><span>${fmt(it.subtotal)}</span></div>
`).join('');
document.getElementById('ticketContent').innerHTML = `
<div class="ticket-preview__center ticket-preview__bold">${data.business_name || 'NEXUS AUTOPARTS'}</div>
<div class="ticket-preview__center">${data.business_rfc || ''}</div>
<div class="ticket-preview__center">${data.business_address || ''}</div>
<div class="ticket-preview__divider"></div>
<div class="ticket-preview__center ticket-preview__bold">NOTA DE REMISIÓN</div>
<div class="ticket-preview__center">${data.folio}</div>
<div class="ticket-preview__center">${dateStr}</div>
<div class="ticket-preview__divider"></div>
<div class="ticket-preview__line"><span>Cliente:</span><span>${data.customer || 'Público General'}</span></div>
<div class="ticket-preview__line"><span>Vendedor:</span><span>${data.employee || '-'}</span></div>
${data.courier ? `<div class="ticket-preview__line"><span>Repartidor:</span><span>${data.courier}</span></div>` : ''}
<div class="ticket-preview__items">${itemsHtml}</div>
<div class="ticket-preview__divider"></div>
<div class="ticket-preview__line"><span>Subtotal:</span><span>${fmt(data.subtotal)}</span></div>
${data.discount_total ? `<div class="ticket-preview__line"><span>Descuento:</span><span>-${fmt(data.discount_total)}</span></div>` : ''}
<div class="ticket-preview__line"><span>IVA:</span><span>${fmt(data.tax_total)}</span></div>
<div class="ticket-preview__line ticket-preview__bold"><span>TOTAL:</span><span>${fmt(data.total)}</span></div>
<div class="ticket-preview__divider"></div>
<div class="ticket-preview__footer ticket-preview__bold">PENDIENTE DE PAGO</div>
<div class="ticket-preview__footer" style="font-size:0.75rem;opacity:0.8;">Presente esta nota en caja para pagar</div>
`;
document.getElementById('ticketModal').classList.add('is-open');
} catch (e) {
alert('Error: ' + e.message);
}
}
function closeTicketModal() {
document.getElementById('ticketModal').classList.remove('is-open');
}
function printTicket() {
const w = window.open('', '_blank');
w.document.write('<html><head><title>Nota de Remisión</title></head><body>' + document.getElementById('ticketContent').innerHTML + '</body></html>');
w.document.close();
w.print();
}
async function payNote(saleId, total) {
const method = prompt(`Cobrar nota NR-${saleId}\nTotal: ${fmt(total)}\n\nForma de pago: efectivo / transferencia / tarjeta`, 'efectivo');
if (!method) return;
const reference = ['transferencia', 'tarjeta'].includes(method) ? prompt('Referencia:') : '';
try {
await api('/pos/api/sales/' + saleId + '/pay', {
method: 'POST',
body: JSON.stringify({
payment_method: method,
amount_paid: total,
reference: reference || ''
})
});
alert('Nota cobrada correctamente');
loadData(currentPage);
} catch (e) {
alert('Error al cobrar: ' + e.message);
}
}
(async function init() {
if (!token) {
document.getElementById('remissionTableBody').innerHTML = `<tr><td colspan="8">
<div class="empty-state">
<div class="empty-state__title">Inicia sesión</div>
<div class="empty-state__subtitle">Se requiere autenticación para ver las notas de remisión.</div>
</div>
</td></tr>`;
return;
}
await loadCouriers();
loadData(1);
})();
</script>
</body>
</html>