437 lines
20 KiB
HTML
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 — 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()">×</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' : ''}>←</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' : ''}>→</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>
|