feat: cashier/counter reports, service-order & remission flows, Rached migration utils
- Add "Mis cortes de caja" report for cashiers/counters with sales detail. - Cash register history scoped to own cuts for non-admin roles; new /register/<id>/sales endpoint. - Remove dashboard from cashier menu; add Reports to cashier/counter. - Service orders: assign mechanic, budget field, invoice flag, counter/cashier can add items/remissions, convert to remission. - Remission notes module (UI, CSS, courier, counter remissions). - Customer hard-delete and vehicle/customer linkage in workshop. - POS: always show search results, compact payment grid, credit validation, tier pricing (5%/10%), ticket with customer/folio. - Inventory: CSV template with sku_secondary, alias import. - Rached migration scripts and DB migrations. - Version-bump cached JS/CSS query strings. Excludes local Rached session tokens/captures (rached_*.json / rached_*.txt).
This commit is contained in:
@@ -54,7 +54,20 @@ const Reports = (() => {
|
||||
}
|
||||
|
||||
// Track which tabs have been loaded
|
||||
var loaded = { ventas: false, inventario: false, clientes: false, financieros: false, historico: false };
|
||||
var loaded = { ventas: false, inventario: false, clientes: false, financieros: false, historico: false, cortes: false };
|
||||
|
||||
function currentUser() {
|
||||
return (typeof window.POS_USER !== 'undefined') ? window.POS_USER : {};
|
||||
}
|
||||
function hasPerm(p) {
|
||||
var u = currentUser();
|
||||
return (u.role === 'owner' || u.role === 'admin') || (u.permissions || []).indexOf(p) !== -1;
|
||||
}
|
||||
function isLimitedUser() {
|
||||
var u = currentUser();
|
||||
var r = (u.role || '').toLowerCase();
|
||||
return (r === 'cashier' || r === 'counter') && !hasPerm('pos.view');
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Theme switcher
|
||||
@@ -86,6 +99,7 @@ const Reports = (() => {
|
||||
else if (id === 'clientes') loadClientes();
|
||||
else if (id === 'financieros') loadFinancieros();
|
||||
else if (id === 'historico') loadHistorico();
|
||||
else if (id === 'cortes') loadCortes();
|
||||
}
|
||||
}
|
||||
window.switchTab = switchTab;
|
||||
@@ -737,6 +751,153 @@ const Reports = (() => {
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// TAB 6: CORTES DE CAJA
|
||||
// =========================================================================
|
||||
async function loadCortes() {
|
||||
loaded.cortes = true;
|
||||
var dateFrom = document.getElementById('cortes-date-from').value;
|
||||
var dateTo = document.getElementById('cortes-date-to').value;
|
||||
var employeeId = document.getElementById('cortes-employee').value;
|
||||
|
||||
// Cashiers/counters without pos.view can only see their own cuts
|
||||
var u = currentUser();
|
||||
if (isLimitedUser()) {
|
||||
employeeId = u.employeeId || '';
|
||||
var empSel = document.getElementById('cortes-employee');
|
||||
if (empSel) empSel.value = employeeId;
|
||||
}
|
||||
|
||||
var kpiEl = document.getElementById('cortes-kpis');
|
||||
var detalleEl = document.getElementById('cortes-detalle');
|
||||
var ventasDetalleEl = document.getElementById('corte-ventas-detalle');
|
||||
kpiEl.innerHTML = spinner();
|
||||
detalleEl.innerHTML = spinner();
|
||||
if (ventasDetalleEl) ventasDetalleEl.style.display = 'none';
|
||||
|
||||
var params = new URLSearchParams();
|
||||
if (dateFrom) params.set('date_from', dateFrom);
|
||||
if (dateTo) params.set('date_to', dateTo);
|
||||
if (employeeId) params.set('employee_id', employeeId);
|
||||
params.set('per_page', '200');
|
||||
|
||||
try {
|
||||
var data = await apiFetch('/pos/api/register/history?' + params.toString());
|
||||
var regs = data.data || [];
|
||||
|
||||
var totalEsperado = 0;
|
||||
var totalCierre = 0;
|
||||
var totalDiferencia = 0;
|
||||
regs.forEach(function(r) {
|
||||
totalEsperado += r.expected_amount || 0;
|
||||
totalCierre += r.closing_amount || 0;
|
||||
totalDiferencia += r.difference || 0;
|
||||
});
|
||||
|
||||
kpiEl.innerHTML =
|
||||
kpiCard('Cortes', fmtInt(regs.length), 'en el periodo') +
|
||||
kpiCard('Ventas esperadas', '$' + fmt(totalEsperado), 'total acumulado') +
|
||||
kpiCard('Cierre real', '$' + fmt(totalCierre), 'efectivo contado') +
|
||||
kpiCard('Diferencia', '$' + fmt(totalDiferencia), totalDiferencia >= 0 ? 'sobrante' : 'faltante');
|
||||
|
||||
var cHtml = '<div class="table-card__header"><span class="table-card__title">Cortes de Caja</span>' +
|
||||
'<span class="pill pill--muted">' + (data.pagination ? data.pagination.total : regs.length) + ' registros</span></div>';
|
||||
cHtml += '<div class="table-wrap"><table class="data-table"><thead><tr>' +
|
||||
'<th>Caja</th><th>Empleado</th><th>Apertura</th><th>Cierre</th>' +
|
||||
'<th class="align-right">Monto Apertura</th><th class="align-right">Esperado</th>' +
|
||||
'<th class="align-right">Cierre Real</th><th class="align-right">Diferencia</th>' +
|
||||
'<th>Acciones</th>' +
|
||||
'</tr></thead><tbody>';
|
||||
regs.forEach(function(r) {
|
||||
var diffColor = r.difference < 0 ? 'color:var(--color-error)' :
|
||||
r.difference > 0 ? 'color:var(--color-warning)' : 'color:var(--color-success)';
|
||||
cHtml += '<tr data-register-id="' + r.id + '" style="cursor:pointer" onclick="Reports.showCorteVentas(' + r.id + ')">' +
|
||||
'<td class="td-mono">#' + r.register_number + '</td>' +
|
||||
'<td class="td-strong">' + (r.employee_name || '--') + '</td>' +
|
||||
'<td style="color:var(--color-text-muted)">' + fmtDateTime(r.opened_at) + '</td>' +
|
||||
'<td style="color:var(--color-text-muted)">' + fmtDateTime(r.closed_at) + '</td>' +
|
||||
'<td class="align-right td-mono">$' + fmt(r.opening_amount) + '</td>' +
|
||||
'<td class="align-right td-mono">$' + fmt(r.expected_amount) + '</td>' +
|
||||
'<td class="align-right td-mono">$' + fmt(r.closing_amount) + '</td>' +
|
||||
'<td class="align-right td-mono" style="' + diffColor + '">$' + fmt(r.difference) + '</td>' +
|
||||
'<td><button class="btn btn-sm btn-ghost" onclick="event.stopPropagation(); Reports.showCorteVentas(' + r.id + ')">Ver ventas</button></td></tr>';
|
||||
});
|
||||
cHtml += '</tbody></table></div>';
|
||||
detalleEl.innerHTML = regs.length ? cHtml : emptyMsg('No hay cortes de caja en el periodo seleccionado');
|
||||
} catch (err) {
|
||||
kpiEl.innerHTML = errorMsg('Error cargando cortes de caja: ' + err.message);
|
||||
detalleEl.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Detail: sales for a selected cash cut
|
||||
// -------------------------------------------------------------------------
|
||||
async function showCorteVentas(registerId) {
|
||||
var panel = document.getElementById('corte-ventas-detalle');
|
||||
if (!panel) return;
|
||||
panel.style.display = 'block';
|
||||
panel.innerHTML = spinner();
|
||||
panel.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
|
||||
var methodLabels = {
|
||||
'cash': 'Efectivo', 'card': 'Tarjeta', 'transfer': 'Transferencia',
|
||||
'credit': 'Crédito', 'mixed': 'Mixto', 'efectivo': 'Efectivo',
|
||||
'tarjeta': 'Tarjeta', 'transferencia': 'Transferencia'
|
||||
};
|
||||
|
||||
try {
|
||||
var data = await apiFetch('/pos/api/register/' + registerId + '/sales');
|
||||
var sales = data.sales || [];
|
||||
var summary = data.summary || {};
|
||||
|
||||
var total = summary.total || 0;
|
||||
var count = summary.count || 0;
|
||||
var byMethod = summary.by_method || {};
|
||||
|
||||
var hHtml = '<div class="table-card__header"><span class="table-card__title">Ventas del corte #' + registerId + '</span>' +
|
||||
'<span class="pill pill--muted">' + count + ' ventas · $' + fmt(total) + '</span></div>';
|
||||
|
||||
// Summary by payment method
|
||||
var methods = Object.entries(byMethod).sort(function(a, b) { return b[1] - a[1]; });
|
||||
if (methods.length) {
|
||||
hHtml += '<div style="display:flex;gap:var(--space-3);flex-wrap:wrap;padding:var(--space-4) var(--space-5);border-bottom:1px solid var(--color-border);">';
|
||||
methods.forEach(function(m) {
|
||||
var label = methodLabels[m[0]] || m[0];
|
||||
hHtml += '<div class="pill pill--muted">' + label + ': <strong>$' + fmt(m[1]) + '</strong></div>';
|
||||
});
|
||||
hHtml += '</div>';
|
||||
}
|
||||
|
||||
hHtml += '<div class="table-wrap"><table class="data-table"><thead><tr>' +
|
||||
'<th># Venta</th><th>Fecha</th><th>Cliente</th><th>Método</th>' +
|
||||
'<th class="align-right">Subtotal</th><th class="align-right">Desc.</th>' +
|
||||
'<th class="align-right">Total</th><th>Estado</th>' +
|
||||
'</tr></thead><tbody>';
|
||||
sales.forEach(function(s) {
|
||||
var statusPill = s.status === 'completed' ? 'pill--success' :
|
||||
s.status === 'cancelled' ? 'pill--error' : 'pill--warning';
|
||||
var statusLabel = s.status === 'completed' ? 'Completada' :
|
||||
s.status === 'cancelled' ? 'Cancelada' : s.status;
|
||||
var method = methodLabels[s.payment_method] || s.payment_method || '--';
|
||||
hHtml += '<tr><td class="td-mono">' + s.id + '</td>' +
|
||||
'<td>' + fmtDateTime(s.created_at) + '</td>' +
|
||||
'<td>' + (s.customer_name || 'Mostrador') + '</td>' +
|
||||
'<td>' + method + '</td>' +
|
||||
'<td class="align-right td-mono">$' + fmt(s.subtotal) + '</td>' +
|
||||
'<td class="align-right td-mono">$' + fmt(s.discount_total) + '</td>' +
|
||||
'<td class="align-right td-mono-accent">$' + fmt(s.total) + '</td>' +
|
||||
'<td><span class="pill ' + statusPill + '">' + statusLabel + '</span></td></tr>';
|
||||
});
|
||||
hHtml += '</tbody></table></div>';
|
||||
|
||||
panel.innerHTML = sales.length ? hHtml : emptyMsg('No hay ventas registradas en este corte');
|
||||
} catch (err) {
|
||||
panel.innerHTML = errorMsg('Error cargando ventas del corte: ' + err.message);
|
||||
}
|
||||
}
|
||||
window.showCorteVentas = showCorteVentas;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Init
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -753,6 +914,9 @@ const Reports = (() => {
|
||||
updateClock();
|
||||
setInterval(updateClock, 1000);
|
||||
|
||||
var u = currentUser();
|
||||
var limited = isLimitedUser();
|
||||
|
||||
// Set default date range: first day of current month to today
|
||||
var now = new Date();
|
||||
var firstDay = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
@@ -761,6 +925,49 @@ const Reports = (() => {
|
||||
if (fromEl) fromEl.value = firstDay.toISOString().substring(0, 10);
|
||||
if (toEl) toEl.value = now.toISOString().substring(0, 10);
|
||||
|
||||
// Set default date range for cortes de caja
|
||||
var cortesFrom = document.getElementById('cortes-date-from');
|
||||
var cortesTo = document.getElementById('cortes-date-to');
|
||||
if (cortesFrom) cortesFrom.value = firstDay.toISOString().substring(0, 10);
|
||||
if (cortesTo) cortesTo.value = now.toISOString().substring(0, 10);
|
||||
|
||||
// Populate cajero filter for cortes de caja (admins/owners only see it)
|
||||
var empSel = document.getElementById('cortes-employee');
|
||||
if (empSel && !limited) {
|
||||
apiFetch('/pos/api/config/employees?per_page=200').then(function(data) {
|
||||
(data.data || []).forEach(function(e) {
|
||||
var opt = document.createElement('option');
|
||||
opt.value = e.id;
|
||||
opt.textContent = e.name;
|
||||
empSel.appendChild(opt);
|
||||
});
|
||||
}).catch(function() {});
|
||||
} else if (empSel && limited) {
|
||||
empSel.value = u.employeeId || '';
|
||||
}
|
||||
|
||||
// For cashiers/counters without pos.view, limit the page to "Mis cortes"
|
||||
if (limited) {
|
||||
['ventas', 'inventario', 'clientes', 'financieros', 'historico'].forEach(function(id) {
|
||||
var btn = document.querySelector('.tab-btn[onclick="switchTab(\'' + id + '\', this)"]');
|
||||
if (btn) btn.style.display = 'none';
|
||||
});
|
||||
var empFilter = document.getElementById('cortes-employee-filter');
|
||||
if (empFilter) empFilter.style.display = 'none';
|
||||
|
||||
document.querySelectorAll('.tab-panel').forEach(function(p) { p.classList.remove('is-active'); });
|
||||
document.querySelectorAll('.tab-btn').forEach(function(b) { b.classList.remove('is-active'); });
|
||||
var cortesPanel = document.getElementById('panel-cortes');
|
||||
var cortesBtn = document.querySelector('.tab-btn[onclick="switchTab(\'cortes\', this)"]');
|
||||
if (cortesPanel) cortesPanel.classList.add('is-active');
|
||||
if (cortesBtn) {
|
||||
cortesBtn.classList.add('is-active');
|
||||
var svg = '<svg viewBox="0 0 15 15" fill="none" stroke="currentColor" stroke-width="1.4"><rect x="1" y="3" width="13" height="10" rx="1"/><path d="M4 7h7M4 10h5"/><circle cx="11" cy="10" r="1.5" fill="currentColor"/></svg>';
|
||||
cortesBtn.innerHTML = svg + '<span>Mis cortes de caja</span>';
|
||||
}
|
||||
loadCortes();
|
||||
}
|
||||
|
||||
// Populate financial period selectors
|
||||
var monthSel = document.getElementById('fin-month');
|
||||
var yearSel = document.getElementById('fin-year');
|
||||
@@ -784,15 +991,18 @@ const Reports = (() => {
|
||||
}
|
||||
}
|
||||
|
||||
// Load the default active tab (ventas)
|
||||
loadVentas();
|
||||
// Load the default active tab (ventas) only for privileged users
|
||||
if (!limited) {
|
||||
loadVentas();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
|
||||
return {
|
||||
init, setTheme, switchTab,
|
||||
loadVentas, loadInventario, loadClientes, loadFinancieros, loadHistorico, fmt
|
||||
loadVentas, loadInventario, loadClientes, loadFinancieros, loadHistorico, loadCortes,
|
||||
showCorteVentas, fmt
|
||||
};
|
||||
// Register Cmd+K items
|
||||
if (typeof registerCmdKItem === "function") {
|
||||
|
||||
Reference in New Issue
Block a user