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:
@@ -713,18 +713,23 @@
|
||||
}
|
||||
|
||||
.pago-tabs {
|
||||
display: flex; border-bottom: 2px solid var(--color-border); padding: 0 var(--space-6);
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(90px, 1fr));
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-4) var(--space-6);
|
||||
border-bottom: none;
|
||||
}
|
||||
.pago-tab {
|
||||
padding: var(--space-3) var(--space-5); font-family: var(--font-body);
|
||||
font-size: var(--text-body-sm); font-weight: var(--font-weight-semibold);
|
||||
background: transparent; border: none; color: var(--color-text-muted);
|
||||
cursor: pointer; border-bottom: 2px solid transparent;
|
||||
margin-bottom: -2px; transition: var(--transition-fast);
|
||||
display: flex; align-items: center; gap: var(--space-2);
|
||||
padding: var(--space-3) var(--space-2); font-family: var(--font-body);
|
||||
font-size: var(--text-caption); font-weight: var(--font-weight-semibold);
|
||||
background: var(--color-surface); border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md); color: var(--color-text-muted);
|
||||
cursor: pointer; transition: var(--transition-fast);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
text-align: center; min-height: 44px;
|
||||
}
|
||||
.pago-tab:hover { color: var(--color-text-primary); }
|
||||
.pago-tab.active { color: var(--color-text-accent); border-bottom-color: var(--color-primary); }
|
||||
.pago-tab:hover { background: var(--color-surface-2); color: var(--color-text-primary); }
|
||||
.pago-tab.active { background: var(--color-primary); border-color: var(--color-primary); color: #fff; }
|
||||
|
||||
.tab-content { padding: var(--space-6); display: none; }
|
||||
.tab-content.active { display: block; }
|
||||
|
||||
195
pos/static/css/remission_notes.css
Normal file
195
pos/static/css/remission_notes.css
Normal file
@@ -0,0 +1,195 @@
|
||||
/* Remission notes page — matches the Nexus POS design system */
|
||||
|
||||
/* Scrollable page content area */
|
||||
.page-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: var(--space-5) var(--space-6);
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
|
||||
}
|
||||
.page-content::-webkit-scrollbar { width: 6px; }
|
||||
.page-content::-webkit-scrollbar-track { background: var(--scrollbar-track); }
|
||||
.page-content::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb); border-radius: var(--radius-full); }
|
||||
|
||||
/* Filters card */
|
||||
.filters-card {
|
||||
background: var(--color-bg-elevated);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-4);
|
||||
margin-bottom: var(--space-5);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
[data-theme="modern"] .filters-card {
|
||||
background: var(--color-bg-overlay);
|
||||
}
|
||||
|
||||
/* Date input reuse select-filter styling */
|
||||
.select-filter[type="date"] {
|
||||
padding: 0 var(--space-3);
|
||||
}
|
||||
|
||||
/* Status badges for remission notes */
|
||||
.badge--pending_payment { background: var(--color-primary-muted); color: var(--color-primary); }
|
||||
.badge--completed { background: rgba(34, 197, 94, 0.15); color: var(--color-success); }
|
||||
.badge--cancelled { background: rgba(115, 115, 115, 0.15); color: var(--color-text-muted); }
|
||||
|
||||
/* Action buttons in table */
|
||||
.action-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-1);
|
||||
padding: 0 var(--space-2);
|
||||
height: 28px;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: var(--text-caption);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
transition: var(--transition-fast);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.action-btn svg {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
stroke: currentColor;
|
||||
fill: none;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
.action-btn--ghost {
|
||||
background: var(--btn-ghost-bg);
|
||||
color: var(--btn-ghost-text);
|
||||
border-color: var(--btn-ghost-border);
|
||||
}
|
||||
.action-btn--ghost:hover {
|
||||
background: var(--color-surface-2);
|
||||
border-color: var(--color-border-strong);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
.action-btn--primary {
|
||||
background: var(--btn-primary-bg);
|
||||
color: var(--btn-primary-text);
|
||||
border-color: var(--btn-primary-border);
|
||||
}
|
||||
.action-btn--primary:hover { background: var(--btn-primary-bg-hover); }
|
||||
|
||||
/* Empty state */
|
||||
.empty-state {
|
||||
padding: var(--space-10) var(--space-6);
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
backdrop-filter: blur(4px);
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: var(--z-modal, 1050);
|
||||
padding: var(--space-4);
|
||||
}
|
||||
.modal-overlay.is-open { display: flex; }
|
||||
|
||||
.modal {
|
||||
background: var(--color-bg-elevated);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: var(--shadow-xl);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
[data-theme="modern"] .modal {
|
||||
background: var(--color-bg-overlay);
|
||||
}
|
||||
|
||||
.modal__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--space-4) var(--space-5);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
.modal__title {
|
||||
font-family: var(--font-heading);
|
||||
font-size: var(--text-h6);
|
||||
font-weight: var(--heading-weight-primary);
|
||||
color: var(--color-text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
.modal__close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 1.25rem;
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
padding: var(--space-1);
|
||||
}
|
||||
.modal__close:hover { color: var(--color-text-primary); }
|
||||
|
||||
.modal__body {
|
||||
padding: var(--space-5);
|
||||
}
|
||||
|
||||
.modal__footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-4) var(--space-5);
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
/* Ticket preview (monospace) */
|
||||
.ticket-preview {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.45;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
.ticket-preview__center { text-align: center; }
|
||||
.ticket-preview__bold { font-weight: 700; }
|
||||
.ticket-preview__line {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
.ticket-preview__divider {
|
||||
border-top: 1px dashed var(--color-border-strong);
|
||||
margin: var(--space-3) 0;
|
||||
}
|
||||
.ticket-preview__items { margin: var(--space-3) 0; }
|
||||
.ticket-preview__item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
.ticket-preview__footer {
|
||||
text-align: center;
|
||||
margin-top: var(--space-3);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 1024px) {
|
||||
.page-content {
|
||||
padding: var(--space-4);
|
||||
}
|
||||
.toolbar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
.search-box, .select-filter {
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
.toolbar__spacer { display: none; }
|
||||
}
|
||||
@@ -947,6 +947,30 @@ body {
|
||||
.badge--delivered { background: rgba(16, 185, 129, 0.12); color: #10b981; }
|
||||
.badge--cancelled { background: rgba(239, 68, 68, 0.12); color: #ef4444; }
|
||||
.badge--pending { background: rgba(148, 163, 184, 0.12); color: #94a3b8; }
|
||||
.badge--por_revisar { background: rgba(59, 130, 246, 0.12); color: #3b82f6; }
|
||||
.badge--en_revision { background: rgba(99, 102, 241, 0.12); color: #6366f1; }
|
||||
.badge--revisada { background: rgba(139, 92, 246, 0.12); color: #8b5cf6; }
|
||||
.badge--cotizada { background: rgba(245, 166, 35, 0.12); color: #f5a623; }
|
||||
.badge--por_autorizar { background: rgba(249, 115, 22, 0.12); color: #f97316; }
|
||||
.badge--autorizada { background: rgba(34, 197, 94, 0.12); color: #22c55e; }
|
||||
.badge--autorizacion_parcial { background: rgba(16, 185, 129, 0.12); color: #10b981; }
|
||||
.badge--en_reparacion { background: rgba(245, 166, 35, 0.18); color: #d97706; }
|
||||
.badge--reparada { background: rgba(20, 184, 166, 0.12); color: #14b8a6; }
|
||||
.badge--por_entregar { background: rgba(59, 130, 246, 0.16); color: #2563eb; }
|
||||
.badge--entregado { background: rgba(16, 185, 129, 0.12); color: #10b981; }
|
||||
.badge--por_enviar { background: rgba(99, 102, 241, 0.16); color: #4f46e5; }
|
||||
.badge--enviado { background: rgba(14, 165, 233, 0.12); color: #0ea5e9; }
|
||||
.badge--por_facturar { background: rgba(168, 85, 247, 0.12); color: #a855f7; }
|
||||
.badge--facturada { background: rgba(236, 72, 153, 0.12); color: #ec4899; }
|
||||
.badge--por_recolectar { background: rgba(100, 116, 139, 0.12); color: #64748b; }
|
||||
.badge--cancelada { background: rgba(239, 68, 68, 0.12); color: #ef4444; }
|
||||
.badge--revisando { background: rgba(99, 102, 241, 0.12); color: #6366f1; }
|
||||
.badge--revisado { background: rgba(139, 92, 246, 0.12); color: #8b5cf6; }
|
||||
.badge--cotizado { background: rgba(245, 166, 35, 0.12); color: #f5a623; }
|
||||
.badge--por_autorizar { background: rgba(249, 115, 22, 0.12); color: #f97316; }
|
||||
.badge--autorizado { background: rgba(34, 197, 94, 0.12); color: #22c55e; }
|
||||
.badge--reparado { background: rgba(20, 184, 166, 0.12); color: #14b8a6; }
|
||||
.badge--cancelado { background: rgba(239, 68, 68, 0.12); color: #ef4444; }
|
||||
|
||||
.badge--normal { background: rgba(148, 163, 184, 0.12); color: #94a3b8; }
|
||||
.badge--high { background: rgba(245, 166, 35, 0.12); color: #f5a623; }
|
||||
|
||||
@@ -182,10 +182,85 @@
|
||||
permissions: payload.permissions || []
|
||||
};
|
||||
|
||||
// ─── Restrict workshop/mechanic users to the workshop view only ───
|
||||
if ((role === 'workshop' || role === 'mechanic') && window.location.pathname !== '/pos/workshop') {
|
||||
window.location.replace('/pos/workshop');
|
||||
return;
|
||||
// ─── Page guard based on role + permissions ───
|
||||
function isPageAllowed(pagePath, userRole, userPerms) {
|
||||
if (userRole === 'owner' || userRole === 'admin') return true;
|
||||
|
||||
// Workshop/mechanic accounts always see Taller; extra modules depend on permissions.
|
||||
if (userRole === 'workshop' || userRole === 'mechanic') {
|
||||
var allowed = ['/pos/workshop'];
|
||||
var permMap = {
|
||||
'customers.view': '/pos/customers',
|
||||
'inventory.view': '/pos/inventory',
|
||||
'catalog.view': '/pos/catalog',
|
||||
'pos.sell': '/pos/sale',
|
||||
'pos.view': '/pos/sale',
|
||||
'pos.remission': '/pos/remission-notes',
|
||||
'invoicing.view': '/pos/invoicing',
|
||||
'quotations.view': '/pos/quotations',
|
||||
'accounting.view': '/pos/accounting',
|
||||
'reports.view': '/pos/reports',
|
||||
'config.view': '/pos/config',
|
||||
'config.edit': '/pos/config'
|
||||
};
|
||||
for (var p in permMap) {
|
||||
if (userPerms.indexOf(p) !== -1 && allowed.indexOf(permMap[p]) === -1) {
|
||||
allowed.push(permMap[p]);
|
||||
}
|
||||
}
|
||||
return allowed.indexOf(pagePath) !== -1;
|
||||
}
|
||||
|
||||
// Counter: fixed module set (no dashboard).
|
||||
if (userRole === 'counter') {
|
||||
return ['/pos/sale','/pos/catalog','/pos/inventory','/pos/customers','/pos/workshop','/pos/remission-notes','/pos/reports'].indexOf(pagePath) !== -1;
|
||||
}
|
||||
|
||||
// Cashier: fixed module set (no dashboard).
|
||||
if (userRole === 'cashier') {
|
||||
return ['/pos/sale','/pos/catalog','/pos/inventory','/pos/customers','/pos/workshop','/pos/remission-notes','/pos/invoicing','/pos/reports'].indexOf(pagePath) !== -1;
|
||||
}
|
||||
|
||||
// Any other role (accountant, warehouse, sales, etc.) keeps the previous permissive behavior.
|
||||
return true;
|
||||
}
|
||||
|
||||
function enforcePageGuard(userRole, userPerms) {
|
||||
if (isPageAllowed(path, userRole, userPerms)) return true;
|
||||
var fallback = '/pos/catalog';
|
||||
if (userRole === 'counter' || userRole === 'cashier') fallback = '/pos/sale';
|
||||
else if (userRole === 'workshop' || userRole === 'mechanic') fallback = '/pos/workshop';
|
||||
window.location.replace(fallback);
|
||||
return false;
|
||||
}
|
||||
|
||||
// ─── Refresh permissions/token from server before enforcing the guard ───
|
||||
// This makes permission changes effective without requiring a full re-login.
|
||||
try {
|
||||
fetch('/pos/api/auth/refresh', {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': 'Bearer ' + token }
|
||||
}).then(function(r) {
|
||||
if (r.ok) return r.json();
|
||||
return null;
|
||||
}).then(function(data) {
|
||||
if (data && data.token) {
|
||||
localStorage.setItem('pos_token', data.token);
|
||||
localStorage.setItem('pos_employee', JSON.stringify(data.employee));
|
||||
token = data.token;
|
||||
window.POS_USER.token = data.token;
|
||||
window.POS_USER.permissions = data.permissions || [];
|
||||
window.POS_USER.branchId = data.employee.branch_id;
|
||||
}
|
||||
if (!enforcePageGuard(window.POS_USER.role, window.POS_USER.permissions)) return;
|
||||
if (typeof window.renderSidebar === 'function') {
|
||||
window.renderSidebar(window.POS_USER.modules || JSON.parse(localStorage.getItem('pos_modules') || '{}'));
|
||||
}
|
||||
}).catch(function() {
|
||||
enforcePageGuard(role, payload.permissions || []);
|
||||
});
|
||||
} catch(e) {
|
||||
enforcePageGuard(role, payload.permissions || []);
|
||||
}
|
||||
|
||||
// ─── Preload enabled modules for sidebar filtering ───
|
||||
|
||||
@@ -9,6 +9,8 @@ const Config = (() => {
|
||||
|
||||
// Cache for branches (used by employee modal selector)
|
||||
let _branches = [];
|
||||
let _rolePermissions = {};
|
||||
let _availablePermissions = [];
|
||||
|
||||
function token() {
|
||||
return localStorage.getItem('pos_token') || '';
|
||||
@@ -108,6 +110,7 @@ const Config = (() => {
|
||||
owner: 'Dueno',
|
||||
admin: 'Admin',
|
||||
cashier: 'Cajero',
|
||||
counter: 'Mostrador',
|
||||
warehouse: 'Almacenista',
|
||||
accountant: 'Contador',
|
||||
workshop: 'Taller',
|
||||
@@ -118,6 +121,7 @@ const Config = (() => {
|
||||
owner: 'badge--owner',
|
||||
admin: 'badge--blue',
|
||||
cashier: 'badge--green',
|
||||
counter: 'badge--gray',
|
||||
warehouse: 'badge--yellow',
|
||||
accountant: 'badge--purple',
|
||||
workshop: 'badge--orange',
|
||||
@@ -961,6 +965,15 @@ const Config = (() => {
|
||||
} catch (e) {
|
||||
console.error('Config.loadModules:', e);
|
||||
}
|
||||
try {
|
||||
var res2 = await fetch(API + '/counter-remission', { headers: headers() });
|
||||
if (!res2.ok) return;
|
||||
var d2 = await res2.json();
|
||||
var cbCr = document.getElementById('cfg-module-counter-remission');
|
||||
if (cbCr) cbCr.checked = d2.enabled === true;
|
||||
} catch (e) {
|
||||
console.error('Config.loadCounterRemission:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveModules() {
|
||||
@@ -968,7 +981,8 @@ const Config = (() => {
|
||||
var cbMp = document.getElementById('cfg-module-marketplace');
|
||||
var cbMeli = document.getElementById('cfg-module-meli');
|
||||
var cbCat = document.getElementById('cfg-module-catalog');
|
||||
if (!cbWa && !cbMp && !cbMeli && !cbCat) return;
|
||||
var cbCr = document.getElementById('cfg-module-counter-remission');
|
||||
if (!cbWa && !cbMp && !cbMeli && !cbCat && !cbCr) return;
|
||||
try {
|
||||
var data = {
|
||||
whatsapp: cbWa ? cbWa.checked : true,
|
||||
@@ -986,12 +1000,112 @@ const Config = (() => {
|
||||
throw new Error(err.error || 'Save failed');
|
||||
}
|
||||
localStorage.setItem('pos_modules', JSON.stringify(data));
|
||||
if (cbCr) {
|
||||
var res2 = await fetch(API + '/counter-remission', {
|
||||
method: 'PUT',
|
||||
headers: headers(),
|
||||
body: JSON.stringify({ enabled: cbCr.checked })
|
||||
});
|
||||
if (!res2.ok) {
|
||||
var err2 = await res2.json().catch(function() { return { error: res2.statusText }; });
|
||||
throw new Error(err2.error || 'Save failed');
|
||||
}
|
||||
}
|
||||
toast('Módulos actualizados');
|
||||
} catch (e) {
|
||||
toast(e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Tab navigation
|
||||
// -------------------------------------------------------------------------
|
||||
function switchTab(tab) {
|
||||
document.querySelectorAll('.cfg-tab-btn').forEach(function(btn) {
|
||||
btn.classList.toggle('active', btn.dataset.tab === tab);
|
||||
});
|
||||
document.querySelectorAll('.settings-section[data-tab]').forEach(function(sec) {
|
||||
var isActive = sec.dataset.tab === tab;
|
||||
sec.classList.toggle('active', isActive);
|
||||
sec.style.display = isActive ? '' : 'none';
|
||||
});
|
||||
try { localStorage.setItem('pos_config_tab', tab); } catch(e) {}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Role permissions editor
|
||||
// -------------------------------------------------------------------------
|
||||
async function loadRolePermissions() {
|
||||
try {
|
||||
var res = await fetch(API + '/role-permissions', { headers: headers() });
|
||||
if (!res.ok) return;
|
||||
var data = await res.json();
|
||||
_rolePermissions = data.roles || {};
|
||||
_availablePermissions = data.available || [];
|
||||
renderRolePermissions();
|
||||
} catch (e) {
|
||||
console.error('Config.loadRolePermissions:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function renderRolePermissions() {
|
||||
var container = document.getElementById('role-permissions-container');
|
||||
var roleSel = document.getElementById('cfg-perm-role');
|
||||
if (!container || !roleSel) return;
|
||||
var role = roleSel.value;
|
||||
if (!role) {
|
||||
container.innerHTML = '<p style="color:var(--color-text-muted);">Selecciona un rol para ver y editar sus permisos.</p>';
|
||||
return;
|
||||
}
|
||||
var current = _rolePermissions[role] || [];
|
||||
var html = '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:var(--space-4);">';
|
||||
_availablePermissions.forEach(function(group) {
|
||||
html += '<div style="border:1px solid var(--color-border);border-radius:var(--radius-md);padding:var(--space-3);background:var(--color-surface-2);">';
|
||||
html += '<h4 style="margin:0 0 var(--space-3);font-size:var(--text-body-sm);color:var(--color-text-primary);">' + escapeHtml(group.module) + '</h4>';
|
||||
group.permissions.forEach(function(p) {
|
||||
var checked = current.indexOf(p.key) !== -1 ? 'checked' : '';
|
||||
html += '<label style="display:flex;align-items:center;gap:var(--space-2);margin-bottom:var(--space-2);cursor:pointer;font-size:var(--text-body-sm);">';
|
||||
html += '<input type="checkbox" data-perm-key="' + escapeHtml(p.key) + '" ' + checked + ' style="width:auto;" />';
|
||||
html += '<span>' + escapeHtml(p.label) + '</span>';
|
||||
html += '</label>';
|
||||
});
|
||||
html += '</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
async function saveRolePermissions() {
|
||||
var roleSel = document.getElementById('cfg-perm-role');
|
||||
var status = document.getElementById('role-permissions-status');
|
||||
if (!roleSel || !roleSel.value) {
|
||||
if (status) status.textContent = 'Selecciona un rol';
|
||||
return;
|
||||
}
|
||||
var role = roleSel.value;
|
||||
var selected = [];
|
||||
document.querySelectorAll('#role-permissions-container input[data-perm-key]').forEach(function(cb) {
|
||||
if (cb.checked) selected.push(cb.dataset.permKey);
|
||||
});
|
||||
var payload = { roles: {} };
|
||||
payload.roles[role] = selected;
|
||||
try {
|
||||
var res = await fetch(API + '/role-permissions', {
|
||||
method: 'PUT',
|
||||
headers: headers(),
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
var data = await res.json().catch(function() { return { error: res.statusText }; });
|
||||
if (!res.ok) throw new Error(data.error || 'Error al guardar');
|
||||
_rolePermissions[role] = selected;
|
||||
if (status) status.textContent = 'Permisos guardados y aplicados a empleados existentes';
|
||||
setTimeout(function() { if (status) status.textContent = ''; }, 4000);
|
||||
} catch (e) {
|
||||
if (status) status.textContent = e.message;
|
||||
toast(e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Init
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -1044,6 +1158,11 @@ const Config = (() => {
|
||||
});
|
||||
}
|
||||
|
||||
// Show permissions tab only for owner/admin
|
||||
var isAdmin = user.role === 'owner' || user.role === 'admin';
|
||||
var permTabBtn = document.querySelector('.cfg-tab-btn--permissions');
|
||||
if (permTabBtn) permTabBtn.style.display = isAdmin ? '' : 'none';
|
||||
|
||||
// Load real data in parallel
|
||||
loadBranches();
|
||||
loadEmployees();
|
||||
@@ -1053,6 +1172,15 @@ const Config = (() => {
|
||||
loadAllowedBrands();
|
||||
loadModules();
|
||||
loadReceiptConfig();
|
||||
if (isAdmin) loadRolePermissions();
|
||||
|
||||
// Activate default or stored tab
|
||||
var defaultTab = 'general';
|
||||
try {
|
||||
var storedTab = localStorage.getItem('pos_config_tab');
|
||||
if (storedTab) defaultTab = storedTab;
|
||||
} catch(e) {}
|
||||
switchTab(defaultTab);
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
@@ -1074,7 +1202,8 @@ const Config = (() => {
|
||||
loadVehicleCompatSource, saveVehicleCompatSource,
|
||||
loadModules, saveModules,
|
||||
loadReceiptConfig, saveReceiptConfig, handleReceiptLogo, removeReceiptLogo,
|
||||
openModal, closeModal, openBranchModal, editBranch
|
||||
openModal, closeModal, openBranchModal, editBranch,
|
||||
switchTab, loadRolePermissions, renderRolePermissions, saveRolePermissions
|
||||
};
|
||||
|
||||
})();
|
||||
|
||||
@@ -10,6 +10,7 @@ var I18N = {
|
||||
'catalog': 'Catalogo',
|
||||
'inventory': 'Inventario',
|
||||
'diagrams': 'Diagramas',
|
||||
'remission_notes': 'Notas de Remisión',
|
||||
'customers': 'Clientes',
|
||||
'invoicing': 'Facturacion',
|
||||
'accounting': 'Contabilidad',
|
||||
@@ -166,6 +167,7 @@ var I18N = {
|
||||
'catalog': 'Catalog',
|
||||
'inventory': 'Inventory',
|
||||
'diagrams': 'Diagrams',
|
||||
'remission_notes': 'Remission Notes',
|
||||
'customers': 'Customers',
|
||||
'invoicing': 'Invoicing',
|
||||
'accounting': 'Accounting',
|
||||
|
||||
@@ -21,6 +21,19 @@
|
||||
var userRole = (user.role || '').toLowerCase();
|
||||
var userPerms = user.permissions || [];
|
||||
var canEditPrices = userRole === 'owner' || userRole === 'admin' || userPerms.indexOf('config.edit_prices') !== -1;
|
||||
var canCreateItem = userRole === 'owner' || userRole === 'admin' || userRole === 'counter' || userRole === 'cashier' || userRole === 'warehouse' || userPerms.indexOf('inventory.create') !== -1;
|
||||
var canEditItem = userRole === 'owner' || userRole === 'admin' || userRole === 'warehouse' || userPerms.indexOf('inventory.edit') !== -1;
|
||||
var canImportItems = userRole === 'owner' || userRole === 'admin' || userRole === 'warehouse' || userPerms.indexOf('inventory.edit') !== -1;
|
||||
|
||||
// Hide toolbar actions the user is not allowed to use
|
||||
(function applyInventoryPermissions() {
|
||||
var headerNew = document.getElementById('btnHeaderNewProduct');
|
||||
var stockNew = document.getElementById('btnStockNewProduct');
|
||||
var headerImport = document.getElementById('btnHeaderImport');
|
||||
if (headerNew) headerNew.style.display = canCreateItem ? '' : 'none';
|
||||
if (stockNew) stockNew.style.display = canCreateItem ? '' : 'none';
|
||||
if (headerImport) headerImport.style.display = canImportItems ? '' : 'none';
|
||||
})();
|
||||
|
||||
// Load compatibility source setting
|
||||
(function loadCompatSource() {
|
||||
@@ -186,11 +199,11 @@
|
||||
'<td>' + esc(it.location) + '</td>' +
|
||||
'<td>' +
|
||||
'<button class="btn btn--ghost btn--sm" onclick="event.stopPropagation();viewHistory(' + it.id + ')">Historial</button> ' +
|
||||
'<button class="btn btn--ghost btn--sm" onclick="event.stopPropagation();showEditItemModal(' + it.id + ')">Editar</button> ' +
|
||||
'<button class="btn btn--ghost btn--sm" style="color:var(--color-accent);" onclick="event.stopPropagation();showPurchaseModalForItem(' + it.id + ')">Entrada</button> ' +
|
||||
'<button class="btn btn--sm btn--meli" onclick="event.stopPropagation();publishToMeli(' + it.id + ')">ML</button> ' +
|
||||
(canEditItem ? '<button class="btn btn--ghost btn--sm" onclick="event.stopPropagation();showEditItemModal(' + it.id + ')">Editar</button> ' : '') +
|
||||
(canCreateItem ? '<button class="btn btn--ghost btn--sm" style="color:var(--color-accent);" onclick="event.stopPropagation();showPurchaseModalForItem(' + it.id + ')">Entrada</button> ' : '') +
|
||||
(userPerms.indexOf('marketplace.manage') !== -1 || userRole === 'owner' || userRole === 'admin' ? '<button class="btn btn--sm btn--meli" onclick="event.stopPropagation();publishToMeli(' + it.id + ')">ML</button> ' : '') +
|
||||
'<button class="btn btn--ghost btn--sm" onclick="event.stopPropagation();printBarcode(\'' + esc(it.barcode) + '\',\'' + esc(it.part_number) + '\',\'' + esc(it.name) + '\')">Etiqueta</button> ' +
|
||||
'<button class="btn btn--ghost btn--sm" style="color:var(--color-error);" onclick="event.stopPropagation();deleteItem(' + it.id + ')">Eliminar</button>' +
|
||||
(canEditItem ? '<button class="btn btn--ghost btn--sm" style="color:var(--color-error);" onclick="event.stopPropagation();deleteItem(' + it.id + ')">Eliminar</button>' : '') +
|
||||
'</td></tr>';
|
||||
}
|
||||
|
||||
@@ -544,6 +557,28 @@
|
||||
}
|
||||
window.submitBulkImport = submitBulkImport;
|
||||
|
||||
function downloadBulkImportTemplate() {
|
||||
var headers = [
|
||||
'numero_de_parte','nombre','marca','precio','cantidad','costo',
|
||||
'sku_secundario','descripcion','categoria','fabricante','modelo','anio','motor','codigo_motor'
|
||||
];
|
||||
var example = [
|
||||
'EJ-001','Filtro de aceite','ACDelco','150.00','10','90.00',
|
||||
'EJ001-ALT','Filtro para sedan','Filtros','Nissan','Sentra','2020','1.8','MR18DE'
|
||||
];
|
||||
var csv = [headers.join(','), example.join(',')].join('\n');
|
||||
var blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||
var url = URL.createObjectURL(blob);
|
||||
var a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'plantilla_inventario.csv';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
window.downloadBulkImportTemplate = downloadBulkImportTemplate;
|
||||
|
||||
// =====================================================================
|
||||
// PURCHASE / ENTRADA (purchaseModal)
|
||||
// =====================================================================
|
||||
|
||||
@@ -83,7 +83,12 @@
|
||||
localStorage.setItem('pos_employee', JSON.stringify(result.data.employee));
|
||||
localStorage.setItem('pos_tenant_id', tenantId);
|
||||
document.cookie = 'pos_role=' + (result.data.employee.role || '') + '; path=/pos; SameSite=Lax';
|
||||
window.location.href = '/pos/catalog';
|
||||
var role = (result.data.employee.role || '').toLowerCase();
|
||||
if (role === 'workshop' || role === 'mechanic' || role === 'counter') {
|
||||
window.location.href = '/pos/workshop';
|
||||
} else {
|
||||
window.location.href = '/pos/catalog';
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
errorEl.textContent = 'Error de conexion';
|
||||
|
||||
@@ -30,7 +30,12 @@ const POS = (() => {
|
||||
let canEditPrice = false;
|
||||
let canCreateWorkshopOrder = false;
|
||||
let canCreateLayaway = false;
|
||||
let canCreateRemission = false;
|
||||
let counterRemissionEnabled = false;
|
||||
let currentPerms = [];
|
||||
let receiptConfig = {};
|
||||
let couriers = [];
|
||||
let selectedCourierId = null;
|
||||
|
||||
// Currency-aware formatter: reads pos_currency from localStorage
|
||||
const _posCurrency = localStorage.getItem('pos_currency') || 'MXN';
|
||||
@@ -38,6 +43,16 @@ const POS = (() => {
|
||||
const _currLocale = _posCurrency === 'USD' ? 'en-US' : 'es-MX';
|
||||
const fmt = (n) => (_currSymbols[_posCurrency] || '$') + parseFloat(n || 0).toLocaleString(_currLocale, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||
|
||||
// Price based on customer tier: base price (price_1) with tier discount.
|
||||
// Tier 2 = Taller (5% off), Tier 3 = Mayoreo (10% off), Tier 1 = base.
|
||||
function priceForTier(basePrice, tier) {
|
||||
const p = parseFloat(basePrice) || 0;
|
||||
const t = parseInt(tier, 10) || 1;
|
||||
if (t === 2) return Math.round(p * 0.95 * 100) / 100;
|
||||
if (t === 3) return Math.round(p * 0.90 * 100) / 100;
|
||||
return p;
|
||||
}
|
||||
|
||||
function headers() {
|
||||
return { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token };
|
||||
}
|
||||
@@ -66,6 +81,10 @@ const POS = (() => {
|
||||
const el = document.querySelector(selector);
|
||||
if (el) el.style.display = 'none';
|
||||
}
|
||||
function show(selector) {
|
||||
const el = document.querySelector(selector);
|
||||
if (el) el.style.display = '';
|
||||
}
|
||||
if (!canCancel) {
|
||||
hide('#btnCancelSale');
|
||||
hide('#fkeyEsc');
|
||||
@@ -77,6 +96,25 @@ const POS = (() => {
|
||||
hide('[title="Orden de servicio"]');
|
||||
}
|
||||
if (!canCreateLayaway) hide('[onclick="POS.createLayaway()"]');
|
||||
|
||||
// Counter remission workflow
|
||||
if (canCreateRemission) {
|
||||
hide('#btnCobrar');
|
||||
hide('.fkey[onclick="POS.checkout()"]');
|
||||
|
||||
hide('[onclick="POS.createLayaway()"]');
|
||||
hide('[onclick="POS.saveQuotation()"]');
|
||||
show('#btnRemission');
|
||||
show('#courierSelectField');
|
||||
} else {
|
||||
hide('#btnRemission');
|
||||
hide('#courierSelectField');
|
||||
}
|
||||
if (!currentPerms.includes('pos.sell')) {
|
||||
hide('#btnPayRemission');
|
||||
} else {
|
||||
show('#btnPayRemission');
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Init ────────────────────────────
|
||||
@@ -87,14 +125,26 @@ const POS = (() => {
|
||||
document.getElementById('employeeName').textContent = payload.name || 'Empleado';
|
||||
document.getElementById('branchName').textContent = payload.branch_name || '';
|
||||
const perms = payload.permissions || [];
|
||||
currentPerms = perms;
|
||||
const employeeRole = payload.role || '';
|
||||
canViewCost = perms.includes('pos.view_cost');
|
||||
canCancel = perms.includes('pos.cancel');
|
||||
canDiscount = perms.includes('pos.discount');
|
||||
canEditPrice = perms.includes('config.edit_prices');
|
||||
canEditPrice = perms.includes('config.edit_prices') || employeeRole === 'cashier' || employeeRole === 'counter';
|
||||
canCreateWorkshopOrder = perms.includes('workshop.edit');
|
||||
canCreateLayaway = perms.includes('pos.sell');
|
||||
employeeMaxDiscount = payload.max_discount_pct || 100;
|
||||
|
||||
// Counter remission feature
|
||||
try {
|
||||
const crCfg = await api('/pos/api/config/counter-remission');
|
||||
counterRemissionEnabled = crCfg.enabled === true;
|
||||
} catch (e) {
|
||||
counterRemissionEnabled = false;
|
||||
}
|
||||
// Counter remission workflow applies only to the counter role.
|
||||
canCreateRemission = counterRemissionEnabled && employeeRole === 'counter' && perms.includes('pos.remission');
|
||||
|
||||
// Show cost/margin columns and toggle button if permission
|
||||
if (canViewCost) {
|
||||
document.getElementById('thCost').style.display = '';
|
||||
@@ -138,6 +188,7 @@ const POS = (() => {
|
||||
// Load current register and receipt config
|
||||
await loadRegister();
|
||||
await loadReceiptConfig();
|
||||
await loadCouriers();
|
||||
|
||||
// Setup event listeners
|
||||
setupKeyboard();
|
||||
@@ -157,8 +208,10 @@ const POS = (() => {
|
||||
currentRegister = null;
|
||||
document.getElementById('registerInfo').innerHTML =
|
||||
'<span style="color:var(--color-error);cursor:pointer;" onclick="POS.showOpenRegisterModal()" title="Clic para abrir caja">⚠ Sin caja abierta — Clic para abrir</span>';
|
||||
// Force open register modal on first load
|
||||
showOpenRegisterModal();
|
||||
// Force open register modal on first load only for users who can sell
|
||||
if (currentPerms.includes('pos.sell')) {
|
||||
showOpenRegisterModal();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Register check failed:', e);
|
||||
@@ -174,6 +227,24 @@ const POS = (() => {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCouriers() {
|
||||
try {
|
||||
const data = await api('/pos/api/logistics/couriers');
|
||||
couriers = data.couriers || [];
|
||||
const sel = document.getElementById('remissionCourier');
|
||||
if (sel) {
|
||||
sel.innerHTML = '<option value="">-- Sin repartidor --</option>' +
|
||||
couriers.map(c => `<option value="${c.id}">${c.name}</option>`).join('');
|
||||
sel.addEventListener('change', () => {
|
||||
selectedCourierId = sel.value ? parseInt(sel.value, 10) : null;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Could not load couriers:', e);
|
||||
couriers = [];
|
||||
}
|
||||
}
|
||||
|
||||
function rc(key, fallback) {
|
||||
const v = receiptConfig[key];
|
||||
return v !== undefined && v !== null && v !== '' ? v : fallback;
|
||||
@@ -570,26 +641,11 @@ const POS = (() => {
|
||||
if (data.data.length === 0) {
|
||||
container.innerHTML = '<div style="padding:20px;text-align:center;color:var(--color-text-muted);">Sin resultados</div>';
|
||||
if (window.BarcodeFeedback) BarcodeFeedback.error();
|
||||
} else if (data.data.length === 1 && q.length >= 8) {
|
||||
// Auto-select single result on barcode scan (long codes)
|
||||
const item = data.data[0];
|
||||
let price = item.price_1;
|
||||
if (currentCustomer) {
|
||||
const tier = currentCustomer.price_tier || 1;
|
||||
price = tier === 3 ? item.price_3 : tier === 2 ? item.price_2 : item.price_1;
|
||||
}
|
||||
addFromSearch(item, price);
|
||||
input.value = '';
|
||||
hideSearchResults();
|
||||
return;
|
||||
} else {
|
||||
let html = '';
|
||||
data.data.forEach(item => {
|
||||
let price = item.price_1;
|
||||
if (currentCustomer) {
|
||||
const tier = currentCustomer.price_tier || 1;
|
||||
price = tier === 3 ? item.price_3 : tier === 2 ? item.price_2 : item.price_1;
|
||||
}
|
||||
const tier = currentCustomer ? (currentCustomer.price_tier || 1) : 1;
|
||||
const price = priceForTier(item.price_1, tier);
|
||||
html += `<div style="padding:var(--space-3) var(--space-4);border-bottom:1px solid var(--color-border);cursor:pointer;display:flex;justify-content:space-between;align-items:center;transition:var(--transition-fast);" onmouseover="this.style.background='var(--color-primary-muted)'" onmouseout="this.style.background=''" onclick='POS.addFromSearch(${JSON.stringify(item).replace(/'/g, "'")}, ${price})'>
|
||||
<div>
|
||||
<div style="font-weight:var(--font-weight-semibold);">${item.name}</div>
|
||||
@@ -684,7 +740,7 @@ const POS = (() => {
|
||||
const tier = currentCustomer ? (currentCustomer.price_tier || 1) : 1;
|
||||
cart.forEach(item => {
|
||||
if (item.price_1 > 0) {
|
||||
item.unit_price = tier === 3 ? item.price_3 : tier === 2 ? item.price_2 : item.price_1;
|
||||
item.unit_price = priceForTier(item.price_1, tier);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -880,9 +936,12 @@ const POS = (() => {
|
||||
transferencia: 'refPayment',
|
||||
tarjeta: 'refPayment',
|
||||
mixto: 'mixedPayment',
|
||||
credito: 'creditPayment',
|
||||
cheque: 'chequePayment',
|
||||
pendiente: 'pendingPayment',
|
||||
};
|
||||
|
||||
['cashPayment', 'refPayment', 'mixedPayment'].forEach(id => {
|
||||
['cashPayment', 'refPayment', 'mixedPayment', 'creditPayment', 'chequePayment', 'pendingPayment'].forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
const isActive = el.id === tabs[method];
|
||||
@@ -896,6 +955,12 @@ const POS = (() => {
|
||||
const ref = document.getElementById('paymentRef');
|
||||
if (ref) ref.focus();
|
||||
}
|
||||
if (method === 'cheque') {
|
||||
const chequeAmount = document.getElementById('chequeAmount');
|
||||
if (chequeAmount) chequeAmount.value = fmt(getTotal());
|
||||
const chequeRef = document.getElementById('chequeRef');
|
||||
if (chequeRef) chequeRef.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function updateChange() {
|
||||
@@ -933,6 +998,7 @@ const POS = (() => {
|
||||
let amountPaid = 0;
|
||||
let paymentDetails = [];
|
||||
let reference = '';
|
||||
let saleType = 'cash';
|
||||
|
||||
if (paymentMethod === 'efectivo') {
|
||||
amountPaid = parseFloat(document.getElementById('cashReceived').value) || 0;
|
||||
@@ -952,6 +1018,20 @@ const POS = (() => {
|
||||
}
|
||||
});
|
||||
if (amountPaid < total) { alert(`Monto total insuficiente. Falta: ${fmt(total - amountPaid)}`); return; }
|
||||
} else if (paymentMethod === 'credito') {
|
||||
if (!currentCustomer) { alert('Seleccione un cliente para venta a crédito'); return; }
|
||||
const available = (currentCustomer.credit_limit || 0) - (currentCustomer.credit_balance || 0);
|
||||
if (total > available) {
|
||||
alert(`Crédito insuficiente. Disponible: ${fmt(available)}, Total: ${fmt(total)}`);
|
||||
return;
|
||||
}
|
||||
saleType = 'credit';
|
||||
amountPaid = 0;
|
||||
} else if (paymentMethod === 'cheque') {
|
||||
amountPaid = total;
|
||||
reference = document.getElementById('chequeRef').value.trim();
|
||||
} else if (paymentMethod === 'pendiente') {
|
||||
amountPaid = 0;
|
||||
}
|
||||
|
||||
const saleData = {
|
||||
@@ -964,7 +1044,7 @@ const POS = (() => {
|
||||
})),
|
||||
customer_id: currentCustomer ? currentCustomer.id : null,
|
||||
payment_method: paymentMethod,
|
||||
sale_type: 'cash',
|
||||
sale_type: saleType,
|
||||
register_id: currentRegister ? currentRegister.id : null,
|
||||
amount_paid: amountPaid,
|
||||
payment_details: paymentDetails,
|
||||
@@ -983,9 +1063,10 @@ const POS = (() => {
|
||||
const convertData = {
|
||||
register_id: currentRegister ? currentRegister.id : null,
|
||||
payment_method: paymentMethod,
|
||||
sale_type: 'cash',
|
||||
sale_type: saleType,
|
||||
amount_paid: amountPaid,
|
||||
payment_details: paymentDetails,
|
||||
reference: reference,
|
||||
};
|
||||
sale = await api('/pos/api/quotations/' + convertQuoteId + '/convert', {
|
||||
method: 'POST',
|
||||
@@ -1020,22 +1101,13 @@ const POS = (() => {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Credit Sale ─────────────────────
|
||||
async function creditSale() {
|
||||
if (cart.length === 0) { alert('Carrito vacio'); return; }
|
||||
if (!currentCustomer) { alert('Seleccione un cliente para venta a credito'); return; }
|
||||
if (!currentRegister) { alert('No hay caja abierta.'); return; }
|
||||
// ─── Counter Remission Note ────────────
|
||||
async function createRemissionNote() {
|
||||
if (cart.length === 0) { showToast('Carrito vacio'); return; }
|
||||
if (!canCreateRemission) { showToast('No tienes permiso para generar notas de remision'); return; }
|
||||
|
||||
const total = getTotal();
|
||||
const available = (currentCustomer.credit_limit || 0) - (currentCustomer.credit_balance || 0);
|
||||
|
||||
if (currentCustomer.credit_limit > 0 && total > available) {
|
||||
if (!confirm(`Credito insuficiente. Disponible: ${fmt(available)}, Total: ${fmt(total)}. Continuar?`)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const saleData = {
|
||||
const noteData = {
|
||||
items: cart.map(item => ({
|
||||
inventory_id: item.inventory_id,
|
||||
quantity: item.quantity,
|
||||
@@ -1043,19 +1115,21 @@ const POS = (() => {
|
||||
discount_pct: item.discount_pct,
|
||||
tax_rate: item.tax_rate,
|
||||
})),
|
||||
customer_id: currentCustomer.id,
|
||||
payment_method: 'credito',
|
||||
sale_type: 'credit',
|
||||
customer_id: currentCustomer ? currentCustomer.id : null,
|
||||
notes: 'Nota de remision generada desde mostrador',
|
||||
register_id: currentRegister ? currentRegister.id : null,
|
||||
amount_paid: 0,
|
||||
courier_id: selectedCourierId,
|
||||
};
|
||||
|
||||
try {
|
||||
const sale = await api('/pos/api/sales', {
|
||||
const sale = await api('/pos/api/sales/remission', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(saleData),
|
||||
body: JSON.stringify(noteData),
|
||||
});
|
||||
|
||||
if (selectedCourierId) {
|
||||
const courier = couriers.find(c => c.id === selectedCourierId);
|
||||
sale.courier_name = courier ? courier.name : '';
|
||||
}
|
||||
lastSaleId = sale.id;
|
||||
lastSaleData = sale;
|
||||
try { sessionStorage.setItem('pos_last_sale_id', sale.id); } catch(e) {}
|
||||
@@ -1063,12 +1137,128 @@ const POS = (() => {
|
||||
cart = [];
|
||||
selectedRow = -1;
|
||||
clearCustomer();
|
||||
selectedCourierId = null;
|
||||
const sel = document.getElementById('remissionCourier');
|
||||
if (sel) sel.value = '';
|
||||
renderCart();
|
||||
showToast(`Nota de remision NR-${sale.id} generada`);
|
||||
} catch (e) {
|
||||
alert('Error: ' + e.message);
|
||||
alert('Error al generar nota de remision: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function openPayRemissionModal() {
|
||||
if (!currentPerms.includes('pos.sell')) { showToast('No tienes permiso para cobrar notas'); return; }
|
||||
document.getElementById('payRemissionFolio').value = '';
|
||||
document.getElementById('payRemissionDetail').innerHTML = '';
|
||||
document.getElementById('payRemissionResult').innerHTML = '';
|
||||
document.getElementById('payRemissionActions').style.display = 'none';
|
||||
document.getElementById('payRemissionModal').classList.add('open');
|
||||
setTimeout(() => document.getElementById('payRemissionFolio').focus(), 100);
|
||||
}
|
||||
|
||||
function closePayRemissionModal() {
|
||||
document.getElementById('payRemissionModal').classList.remove('open');
|
||||
}
|
||||
|
||||
let pendingRemissionToPay = null;
|
||||
|
||||
async function searchRemissionToPay() {
|
||||
const folio = parseInt(document.getElementById('payRemissionFolio').value, 10);
|
||||
if (!folio) { showToast('Ingresa un folio valido'); return; }
|
||||
pendingRemissionToPay = null;
|
||||
try {
|
||||
const sale = await api('/pos/api/sales/' + folio);
|
||||
if (sale.status !== 'pending_payment') {
|
||||
document.getElementById('payRemissionDetail').innerHTML = `<div class="error-msg">La venta ${folio} no esta pendiente de pago</div>`;
|
||||
document.getElementById('payRemissionActions').style.display = 'none';
|
||||
return;
|
||||
}
|
||||
pendingRemissionToPay = sale;
|
||||
let itemsHtml = (sale.items || []).map(it => `
|
||||
<div class="ticket-line">
|
||||
<span class="qty">${it.quantity}</span>
|
||||
<span class="name">${it.name || ''}</span>
|
||||
<span class="subtotal">${fmt(it.subtotal || 0)}</span>
|
||||
</div>
|
||||
`).join('');
|
||||
document.getElementById('payRemissionDetail').innerHTML = `
|
||||
<div class="info-row"><span>Cliente:</span><span>${sale.customer_name || 'Publico General'}</span></div>
|
||||
<div class="info-row"><span>Vendedor:</span><span>${sale.employee_name || ''}</span></div>
|
||||
<div class="info-row"><span>Total:</span><span class="grand">${fmt(sale.total)}</span></div>
|
||||
<hr class="divider">
|
||||
${itemsHtml}
|
||||
`;
|
||||
document.getElementById('payRemissionActions').style.display = '';
|
||||
} catch (e) {
|
||||
document.getElementById('payRemissionDetail').innerHTML = `<div class="error-msg">${e.message}</div>`;
|
||||
document.getElementById('payRemissionActions').style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmPayRemission() {
|
||||
if (!pendingRemissionToPay) return;
|
||||
const sale = pendingRemissionToPay;
|
||||
const paymentMethod = document.getElementById('payRemissionMethod').value;
|
||||
let amountPaid = parseFloat(sale.total);
|
||||
let paymentDetails = [];
|
||||
let reference = '';
|
||||
|
||||
if (paymentMethod === 'efectivo') {
|
||||
const received = parseFloat(document.getElementById('payRemissionReceived').value) || 0;
|
||||
if (received < sale.total) { alert('Monto insuficiente'); return; }
|
||||
amountPaid = received;
|
||||
} else if (paymentMethod === 'mixto') {
|
||||
const rows = document.querySelectorAll('#payRemissionMixed .mixed-row');
|
||||
let sum = 0;
|
||||
rows.forEach(row => {
|
||||
const method = row.querySelector('select').value;
|
||||
const amount = parseFloat(row.querySelector('.mixed-amount').value) || 0;
|
||||
const ref = row.querySelectorAll('input')[1]?.value || '';
|
||||
if (amount > 0) {
|
||||
paymentDetails.push({ method, amount, reference: ref });
|
||||
sum += amount;
|
||||
}
|
||||
});
|
||||
if (sum < sale.total) { alert(`Monto total insuficiente. Falta: ${fmt(sale.total - sum)}`); return; }
|
||||
amountPaid = sum;
|
||||
} else {
|
||||
reference = document.getElementById('payRemissionReference').value.trim();
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await api('/pos/api/sales/' + sale.id + '/pay', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
payment_method: paymentMethod,
|
||||
amount_paid: amountPaid,
|
||||
payment_details: paymentDetails,
|
||||
register_id: currentRegister ? currentRegister.id : null,
|
||||
reference: reference,
|
||||
}),
|
||||
});
|
||||
closePayRemissionModal();
|
||||
showToast(`Nota NR-${sale.id} pagada`);
|
||||
// Refresh sale object to print paid ticket
|
||||
const updated = await api('/pos/api/sales/' + sale.id);
|
||||
lastSaleId = updated.id;
|
||||
lastSaleData = updated;
|
||||
showTicket(updated);
|
||||
} catch (e) {
|
||||
alert('Error al cobrar nota: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function updatePayRemissionMethod() {
|
||||
const method = document.getElementById('payRemissionMethod').value;
|
||||
const cashEl = document.getElementById('payRemissionCash');
|
||||
const refEl = document.getElementById('payRemissionRef');
|
||||
const mixedEl = document.getElementById('payRemissionMixed');
|
||||
if (cashEl) cashEl.style.display = method === 'efectivo' ? '' : 'none';
|
||||
if (refEl) refEl.style.display = (method === 'transferencia' || method === 'tarjeta') ? '' : 'none';
|
||||
if (mixedEl) mixedEl.style.display = method === 'mixto' ? '' : 'none';
|
||||
}
|
||||
|
||||
// ─── Quotation ───────────────────────
|
||||
async function saveQuotation() {
|
||||
if (cart.length === 0) { showToast('Carrito vacio'); return; }
|
||||
@@ -1284,8 +1474,9 @@ const POS = (() => {
|
||||
hour: '2-digit', minute: '2-digit'
|
||||
});
|
||||
|
||||
const customerName = currentCustomer ? currentCustomer.name : 'Publico General';
|
||||
const customerRfc = currentCustomer && currentCustomer.rfc ? currentCustomer.rfc : '';
|
||||
const isRemission = sale.status === 'pending_payment';
|
||||
const customerName = sale.customer_name || (currentCustomer ? currentCustomer.name : 'Publico General');
|
||||
const customerRfc = sale.customer_rfc || (currentCustomer && currentCustomer.rfc ? currentCustomer.rfc : '');
|
||||
|
||||
let itemsHtml = '';
|
||||
(sale.items || []).forEach(item => {
|
||||
@@ -1333,13 +1524,14 @@ const POS = (() => {
|
||||
</div>
|
||||
<hr class="divider-double">
|
||||
<div class="folio-line">
|
||||
<span>VENTA: V-${sale.id}</span>
|
||||
<span>${isRemission ? 'NOTA DE REMISION' : 'VENTA'}: ${isRemission ? 'NR' : 'V'}-${sale.id}</span>
|
||||
<span>${dateStr}</span>
|
||||
</div>
|
||||
<div class="ticket-row" style="font-size: 9px; color: #555; margin-bottom: 4px;">
|
||||
<span>Cliente: ${customerName}</span>
|
||||
${customerRfc ? `<span>RFC: ${customerRfc}</span>` : ''}
|
||||
</div>
|
||||
${isRemission && sale.courier_name ? `<div class="ticket-row" style="font-size: 9px; color: #555; margin-bottom: 4px;"><span>Repartidor: ${sale.courier_name}</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>
|
||||
@@ -1363,6 +1555,12 @@ const POS = (() => {
|
||||
${showPayment ? `
|
||||
<hr class="divider">
|
||||
<div class="payment-section">
|
||||
${isRemission ? `
|
||||
<div class="ticket-row" style="font-weight: bold; color: #b91c1c;">
|
||||
<span>Estado:</span><span>PENDIENTE DE PAGO</span>
|
||||
</div>
|
||||
<div style="font-size: 9px; text-align: center; margin-top: 4px;">Presente esta nota en caja para pagar</div>
|
||||
` : `
|
||||
<div class="ticket-row">
|
||||
<span>Forma de pago:</span><span>${sale.payment_method || paymentMethod}</span>
|
||||
</div>
|
||||
@@ -1373,11 +1571,13 @@ const POS = (() => {
|
||||
<div class="ticket-row" style="font-weight: bold;">
|
||||
<span>Cambio:</span><span>${fmt(sale.change_given || 0)}</span>
|
||||
</div>` : ''}
|
||||
`}
|
||||
</div>` : ''}
|
||||
<hr class="divider">
|
||||
<div class="footer-section">
|
||||
<div class="thanks">${thanksMsg}</div>
|
||||
${footerMsg ? `<div>${footerMsg}</div>` : ''}
|
||||
<div class="thanks">${isRemission ? 'Gracias por su preferencia' : thanksMsg}</div>
|
||||
${footerMsg && !isRemission ? `<div>${footerMsg}</div>` : ''}
|
||||
${isRemission ? '<div style="font-size: 9px;">Conserve esta nota para el pago</div>' : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -1387,7 +1587,7 @@ const POS = (() => {
|
||||
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';
|
||||
if (modalHeader) modalHeader.textContent = isRemission ? 'Nota de Remision' : 'Ticket de Venta';
|
||||
|
||||
document.getElementById('ticketModal').classList.add('open');
|
||||
}
|
||||
@@ -1592,7 +1792,9 @@ const POS = (() => {
|
||||
showNewCustomerModal, closeNewCustomerModal, saveNewCustomer,
|
||||
checkout, confirmPayment, closePaymentModal,
|
||||
selectPaymentMethod, updateChange, updateMixedTotal,
|
||||
creditSale, saveQuotation, createLayaway,
|
||||
createRemissionNote, openPayRemissionModal, closePayRemissionModal,
|
||||
searchRemissionToPay, confirmPayRemission, updatePayRemissionMethod,
|
||||
saveQuotation, createLayaway,
|
||||
createServiceOrder, closeServiceOrderModal, confirmServiceOrder, showServiceOrderTicket,
|
||||
showLastSale, openDrawer,
|
||||
showTicket, closeTicketModal, printTicket,
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -41,10 +41,32 @@ window.renderSidebar = function(modulesOverride) {
|
||||
function itemAllowed(id) {
|
||||
if (role === 'owner' || role === 'admin') return true;
|
||||
if (role === 'workshop' || role === 'mechanic') {
|
||||
return id === 'workshop';
|
||||
// Taller siempre visible; el resto depende de los permisos asignados.
|
||||
var allowed = ['workshop'];
|
||||
var permMap = {
|
||||
'customers.view': 'customers',
|
||||
'inventory.view': 'inventory',
|
||||
'catalog.view': 'catalog',
|
||||
'pos.sell': 'pos',
|
||||
'pos.view': 'pos',
|
||||
'pos.remission': 'remission_notes',
|
||||
'invoicing.view': 'invoicing',
|
||||
'quotations.view': 'quotations',
|
||||
'accounting.view': 'accounting',
|
||||
'reports.view': 'reports'
|
||||
};
|
||||
for (var p in permMap) {
|
||||
if (perms.indexOf(p) !== -1 && allowed.indexOf(permMap[p]) === -1) {
|
||||
allowed.push(permMap[p]);
|
||||
}
|
||||
}
|
||||
return allowed.indexOf(id) !== -1;
|
||||
}
|
||||
if (role === 'counter') {
|
||||
return ['pos','catalog','inventory','customers','workshop','remission_notes','reports'].indexOf(id) !== -1;
|
||||
}
|
||||
if (role === 'cashier') {
|
||||
return ['dashboard','pos','catalog','inventory','customers'].indexOf(id) !== -1;
|
||||
return ['pos','catalog','inventory','customers','workshop','remission_notes','invoicing','reports'].indexOf(id) !== -1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -58,6 +80,7 @@ window.renderSidebar = function(modulesOverride) {
|
||||
].filter(Boolean).filter(function(i){ return itemAllowed(i.id); })},
|
||||
{ label: _t('nav_management'), items: [
|
||||
{ id: 'customers', name: _t('customers'), href: '/pos/customers', icon: '<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"/>' },
|
||||
{ id: 'remission_notes', name: _t('remission_notes'), href: '/pos/remission-notes', icon: '<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"/>' },
|
||||
{ id: 'workshop', name: 'Taller', href: '/pos/workshop', icon: '<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>' },
|
||||
{ id: 'quotations', name: 'Cotizaciones', href: '/pos/quotations', icon: '<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="9" y1="15" x2="15" y2="15"/><line x1="12" y1="12" x2="12" y2="18"/>' },
|
||||
moduleEnabled('marketplace') ? { id: 'marketplace', name: 'Marketplace', href: '/pos/marketplace', icon: '<circle cx="9" cy="21" r="1"/><circle cx="20" cy="21" r="1"/><path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"/>' } : null,
|
||||
@@ -65,7 +88,6 @@ window.renderSidebar = function(modulesOverride) {
|
||||
{ id: 'invoicing', name: _t('invoicing'), href: '/pos/invoicing', icon: '<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"/>' },
|
||||
{ id: 'accounting', name: _t('accounting'), href: '/pos/accounting', icon: '<line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>' },
|
||||
{ id: 'reports', name: _t('reports'), href: '/pos/reports', icon: '<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"/>' },
|
||||
hasPerm('fleet.view') ? { id: 'fleet', name: _t('fleet'), href: '/pos/fleet', icon: '<path d="M1 13h22M1 13l2-6h6l2 6M9 7h6l2 6M15 13l2-6M5 17a2 2 0 1 0 0-4 2 2 0 0 0 0 4zM19 17a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/>' } : null,
|
||||
moduleEnabled('whatsapp') ? { id: 'whatsapp', name: _t('whatsapp'), href: '/pos/whatsapp', icon: '<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/>' } : null,
|
||||
].filter(Boolean).filter(function(i){ return itemAllowed(i.id); })},
|
||||
{ label: _t('nav_system'), items: [
|
||||
@@ -132,14 +154,14 @@ window.renderSidebar = function(modulesOverride) {
|
||||
+ '</div>';
|
||||
|
||||
// Replace existing sidebar
|
||||
var existing = document.querySelector('aside.sidebar, .sidebar, #sidebar');
|
||||
var existing = document.querySelector('.pos-sidebar, aside.sidebar, .sidebar, #sidebar');
|
||||
if (existing) {
|
||||
existing.className = 'pos-sidebar';
|
||||
existing.className = 'pos-sidebar sidebar';
|
||||
existing.innerHTML = sidebarHtml;
|
||||
existing.removeAttribute('style');
|
||||
} else {
|
||||
var el = document.createElement('aside');
|
||||
el.className = 'pos-sidebar';
|
||||
el.className = 'pos-sidebar sidebar';
|
||||
el.innerHTML = sidebarHtml;
|
||||
document.body.insertBefore(el, document.body.firstChild);
|
||||
}
|
||||
|
||||
@@ -30,47 +30,99 @@ var Workshop = (function() {
|
||||
|
||||
var user = window.POS_USER || {};
|
||||
var role = (user.role || '').toLowerCase();
|
||||
var hidePrices = role === 'workshop' || role === 'mechanic';
|
||||
var isRestricted = role === 'workshop' || role === 'mechanic';
|
||||
var isMechanic = role === 'mechanic';
|
||||
var hidePrices = isRestricted;
|
||||
var perms = user.permissions || [];
|
||||
var canEdit = role === 'owner' || role === 'admin' || perms.indexOf('workshop.edit') !== -1;
|
||||
// Owner/admin/counter/cashier can create/edit/delete service orders.
|
||||
// Mechanics have limited access: view allowed statuses, change status, edit diagnosis/repair notes.
|
||||
var canEdit = role === 'owner' || role === 'admin' || role === 'counter' || role === 'cashier';
|
||||
var canCreate = canEdit;
|
||||
var canDelete = role === 'owner' || role === 'admin';
|
||||
var canSell = role === 'owner' || role === 'admin' || perms.indexOf('pos.sell') !== -1;
|
||||
var canSell = role === 'owner' || role === 'admin' || role === 'counter' || role === 'cashier' || perms.indexOf('pos.sell') !== -1;
|
||||
var canChangeStatus = canEdit || isMechanic;
|
||||
|
||||
var COLUMNS = [
|
||||
{key: 'received', label: 'Recibido'},
|
||||
{key: 'diagnosis', label: 'Diagnóstico'},
|
||||
{key: 'waiting_parts', label: 'Espera refacciones'},
|
||||
{key: 'repair', label: 'En reparación'},
|
||||
{key: 'ready', label: 'Listo'},
|
||||
{key: 'delivered', label: 'Entregado'},
|
||||
{key: 'por_revisar', label: 'Por revisar'},
|
||||
{key: 'en_revision', label: 'En revisión'},
|
||||
{key: 'revisada', label: 'Revisada'},
|
||||
{key: 'cotizada', label: 'Cotizada'},
|
||||
{key: 'por_autorizar', label: 'Por autorizar'},
|
||||
{key: 'autorizada', label: 'Autorizada'},
|
||||
{key: 'autorizacion_parcial', label: 'Autorización parcial'},
|
||||
{key: 'en_reparacion', label: 'En reparación'},
|
||||
{key: 'reparada', label: 'Reparada'},
|
||||
{key: 'por_entregar', label: 'Por entregar'},
|
||||
{key: 'entregado', label: 'Entregado'},
|
||||
{key: 'por_enviar', label: 'Por enviar'},
|
||||
{key: 'enviado', label: 'Enviado'},
|
||||
{key: 'por_facturar', label: 'Por facturar'},
|
||||
{key: 'facturada', label: 'Facturada'},
|
||||
{key: 'por_recolectar', label: 'Por recolectar'},
|
||||
];
|
||||
|
||||
var STATUS_LABELS = {
|
||||
received: 'Recibido',
|
||||
diagnosis: 'Diagnóstico',
|
||||
waiting_parts: 'Espera refacciones',
|
||||
repair: 'En reparación',
|
||||
ready: 'Listo',
|
||||
delivered: 'Entregado',
|
||||
cancelled: 'Cancelado'
|
||||
por_revisar: 'Por revisar',
|
||||
en_revision: 'En revisión',
|
||||
revisada: 'Revisada',
|
||||
cotizada: 'Cotizada',
|
||||
por_autorizar: 'Por autorizar',
|
||||
autorizada: 'Autorizada',
|
||||
autorizacion_parcial: 'Autorización parcial',
|
||||
en_reparacion: 'En reparación',
|
||||
reparada: 'Reparada',
|
||||
por_entregar: 'Por entregar',
|
||||
entregado: 'Entregado',
|
||||
por_enviar: 'Por enviar',
|
||||
enviado: 'Enviado',
|
||||
por_facturar: 'Por facturar',
|
||||
facturada: 'Facturada',
|
||||
por_recolectar: 'Por recolectar',
|
||||
cancelada: 'Cancelada'
|
||||
};
|
||||
|
||||
var VALID_NEXT = {
|
||||
received: ['diagnosis', 'cancelled'],
|
||||
diagnosis: ['waiting_parts', 'repair', 'cancelled'],
|
||||
waiting_parts: ['repair', 'cancelled'],
|
||||
repair: ['ready', 'cancelled'],
|
||||
ready: ['delivered', 'cancelled'],
|
||||
delivered: [],
|
||||
cancelled: []
|
||||
por_revisar: ['en_revision', 'cancelada'],
|
||||
en_revision: ['revisada', 'por_revisar', 'cancelada'],
|
||||
revisada: ['cotizada', 'en_revision', 'cancelada'],
|
||||
cotizada: ['por_autorizar', 'revisada', 'cancelada'],
|
||||
por_autorizar: ['autorizada', 'autorizacion_parcial', 'cotizada', 'cancelada'],
|
||||
autorizada: ['en_reparacion', 'por_autorizar', 'cancelada'],
|
||||
autorizacion_parcial: ['en_reparacion', 'por_autorizar', 'cancelada'],
|
||||
en_reparacion: ['reparada', 'por_autorizar', 'cancelada'],
|
||||
reparada: ['por_entregar', 'en_reparacion', 'cancelada'],
|
||||
por_entregar: ['entregado', 'por_enviar', 'reparada', 'cancelada'],
|
||||
por_enviar: ['enviado', 'por_entregar', 'cancelada'],
|
||||
enviado: ['entregado', 'por_enviar', 'cancelada'],
|
||||
por_recolectar: ['por_revisar', 'cancelada'],
|
||||
por_facturar: ['facturada', 'cancelada'],
|
||||
entregado: ['por_facturar'],
|
||||
facturada: [],
|
||||
cancelada: []
|
||||
};
|
||||
|
||||
var DELIVERY_LABELS = {
|
||||
pickup: 'Pasa cliente',
|
||||
pickup: 'Mostrador',
|
||||
delivery: 'Envío a domicilio',
|
||||
courier: 'Motociclista'
|
||||
};
|
||||
|
||||
var ITEM_STATUS_LABELS = {
|
||||
por_revisar: 'Por revisar',
|
||||
revisando: 'Revisando',
|
||||
revisado: 'Revisado',
|
||||
cotizado: 'Cotizado',
|
||||
por_autorizar: 'Por autorizar',
|
||||
autorizado: 'Autorizado',
|
||||
en_reparacion: 'En reparación',
|
||||
reparado: 'Reparado',
|
||||
por_entregar: 'Por entregar',
|
||||
entregado: 'Entregado',
|
||||
por_enviar: 'Por enviar',
|
||||
enviado: 'Enviado',
|
||||
cancelado: 'Cancelado'
|
||||
};
|
||||
|
||||
function headers() {
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -88,6 +140,16 @@ var Workshop = (function() {
|
||||
return '$' + parseFloat(n).toLocaleString('es-MX', {minimumFractionDigits: 2, maximumFractionDigits: 2});
|
||||
}
|
||||
|
||||
// Price based on customer tier: base price (price_1) with tier discount.
|
||||
// Tier 2 = Taller (5% off), Tier 3 = Mayoreo (10% off), Tier 1 = base.
|
||||
function priceForTier(basePrice, tier) {
|
||||
var p = parseFloat(basePrice) || 0;
|
||||
var t = parseInt(tier, 10) || 1;
|
||||
if (t === 2) return Math.round(p * 0.95 * 100) / 100;
|
||||
if (t === 3) return Math.round(p * 0.90 * 100) / 100;
|
||||
return p;
|
||||
}
|
||||
|
||||
function fmtDate(d) {
|
||||
if (!d) return '—';
|
||||
var dt = new Date(d);
|
||||
@@ -126,6 +188,15 @@ var Workshop = (function() {
|
||||
if (savedView) currentView = savedView;
|
||||
bindFilters();
|
||||
bindDeliverySelect();
|
||||
if (!canCreate) {
|
||||
var btnNewOrder = document.getElementById('btnNewOrder');
|
||||
if (btnNewOrder) btnNewOrder.style.display = 'none';
|
||||
}
|
||||
if (isRestricted) {
|
||||
document.querySelectorAll('.restricted-hide').forEach(function(el) { el.style.display = 'none'; });
|
||||
var searchInput = document.getElementById('filterSearch');
|
||||
if (searchInput) searchInput.placeholder = 'Buscar orden';
|
||||
}
|
||||
if (hidePrices) {
|
||||
var btnCatalog = document.getElementById('btnCatalog');
|
||||
if (btnCatalog) btnCatalog.style.display = 'none';
|
||||
@@ -151,11 +222,19 @@ var Workshop = (function() {
|
||||
|
||||
function bindDeliverySelect() {
|
||||
var sel = document.getElementById('noDelivery');
|
||||
if (!sel) return;
|
||||
sel.addEventListener('change', function() {
|
||||
var cf = document.getElementById('courierField');
|
||||
if (cf) cf.style.display = sel.value === 'courier' ? 'block' : 'none';
|
||||
});
|
||||
if (sel) {
|
||||
sel.addEventListener('change', function() {
|
||||
var cf = document.getElementById('courierField');
|
||||
if (cf) cf.style.display = (sel.value === 'delivery' || sel.value === 'courier') ? 'block' : 'none';
|
||||
});
|
||||
}
|
||||
var eoSel = document.getElementById('eoDelivery');
|
||||
if (eoSel) {
|
||||
eoSel.addEventListener('change', function() {
|
||||
var cf = document.getElementById('eoCourierField');
|
||||
if (cf) cf.style.display = (eoSel.value === 'delivery' || eoSel.value === 'courier') ? 'block' : 'none';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function debounce(fn, ms) {
|
||||
@@ -172,9 +251,9 @@ var Workshop = (function() {
|
||||
fetch(API + '/kanban/summary', {headers: headers()})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
document.getElementById('statReceived').textContent = fmt(d.received || 0);
|
||||
document.getElementById('statRepair').textContent = fmt((d.repair || 0) + (d.diagnosis || 0) + (d.waiting_parts || 0) + (d.quality_check || 0));
|
||||
document.getElementById('statReady').textContent = fmt(d.ready || 0);
|
||||
document.getElementById('statReceived').textContent = fmt(d.por_revisar || 0);
|
||||
document.getElementById('statRepair').textContent = fmt((d.en_reparacion || 0) + (d.en_revision || 0) + (d.revisada || 0) + (d.cotizada || 0) + (d.por_autorizar || 0));
|
||||
document.getElementById('statReady').textContent = fmt(d.por_entregar || 0);
|
||||
document.getElementById('statOverdue').textContent = fmt(d.overdue || 0);
|
||||
})
|
||||
.catch(function() {});
|
||||
@@ -243,20 +322,33 @@ var Workshop = (function() {
|
||||
|
||||
function renderList() {
|
||||
var body = document.getElementById('listBody');
|
||||
var fullCols = 8;
|
||||
var restrictedCols = 4;
|
||||
var cols = isRestricted ? restrictedCols : fullCols;
|
||||
if (!orders.length) {
|
||||
body.innerHTML = '<tr><td colspan="7" style="text-align:center;padding:var(--space-4);">No se encontraron órdenes</td></tr>';
|
||||
body.innerHTML = '<tr><td colspan="' + cols + '" style="text-align:center;padding:var(--space-4);">No se encontraron órdenes</td></tr>';
|
||||
return;
|
||||
}
|
||||
body.innerHTML = orders.map(function(o) {
|
||||
var vehicle = esc((o.vehicle_plate || '—') + ' ' + (o.vehicle_make || '') + ' ' + (o.vehicle_model || ''));
|
||||
var statusCell = '<td><span class="badge badge--' + esc(o.status) + '">' + esc(STATUS_LABELS[o.status] || o.status) + '</span></td>';
|
||||
var actionCell = '<td><button class="btn btn--sm btn--secondary" onclick="Workshop.openDetail(' + o.id + ')">Ver</button></td>';
|
||||
var base = '<td>' + esc(o.branch_name || '—') + '</td>' +
|
||||
'<td><strong>' + esc(o.order_number) + '</strong></td>' +
|
||||
statusCell +
|
||||
actionCell;
|
||||
if (isRestricted) {
|
||||
return '<tr>' + base + '</tr>';
|
||||
}
|
||||
var vehicle = esc(o.vehicle_description || (o.vehicle_plate ? (o.vehicle_plate + ' ' + (o.vehicle_make || '') + ' ' + (o.vehicle_model || '')).trim() : '—'));
|
||||
return '<tr>' +
|
||||
'<td>' + esc(o.branch_name || '—') + '</td>' +
|
||||
'<td><strong>' + esc(o.order_number) + '</strong></td>' +
|
||||
'<td>' + esc(o.customer_name || 'Cliente general') + '</td>' +
|
||||
'<td>' + esc(o.workshop_name || '—') + '</td>' +
|
||||
'<td>' + vehicle + '</td>' +
|
||||
'<td><span class="badge badge--' + esc(o.status) + '">' + esc(STATUS_LABELS[o.status] || o.status) + '</span></td>' +
|
||||
statusCell +
|
||||
'<td class="price-col" style="text-align:right;">' + fmtMoney(o.total) + '</td>' +
|
||||
'<td><button class="btn btn--sm btn--secondary" onclick="Workshop.openDetail(' + o.id + ')">Ver</button></td>' +
|
||||
actionCell +
|
||||
'</tr>';
|
||||
}).join('');
|
||||
if (hidePrices) {
|
||||
@@ -287,7 +379,8 @@ var Workshop = (function() {
|
||||
function renderKanban() {
|
||||
var board = document.getElementById('kanbanBoard');
|
||||
board.innerHTML = '';
|
||||
COLUMNS.forEach(function(col) {
|
||||
var hiddenCols = isMechanic ? ['cotizada','por_autorizar','autorizada','autorizacion_parcial','por_facturar','facturada'] : [];
|
||||
COLUMNS.filter(function(col) { return hiddenCols.indexOf(col.key) === -1; }).forEach(function(col) {
|
||||
var colOrders = orders.filter(function(o) { return o.status === col.key; });
|
||||
var colEl = document.createElement('div');
|
||||
colEl.className = 'kanban-column';
|
||||
@@ -322,13 +415,22 @@ var Workshop = (function() {
|
||||
card.className = 'kanban-card';
|
||||
card.onclick = function() { openDetail(o.id); };
|
||||
var priceHtml = hidePrices ? '' : '<span>' + fmtMoney(o.estimated_cost || o.total) + '</span>';
|
||||
if (isRestricted) {
|
||||
card.innerHTML =
|
||||
'<div class="kanban-card__header">' +
|
||||
' <span class="kanban-card__id">' + esc(o.order_number) + '</span>' +
|
||||
' <span class="kanban-card__priority badge badge--' + esc(o.status) + '">' + esc(STATUS_LABELS[o.status] || o.status) + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="kanban-card__branch">' + esc(o.branch_name || '—') + '</div>';
|
||||
return card;
|
||||
}
|
||||
card.innerHTML =
|
||||
'<div class="kanban-card__header">' +
|
||||
' <span class="kanban-card__id">' + esc(o.order_number) + '</span>' +
|
||||
' <span class="kanban-card__priority badge badge--' + esc(o.priority) + '">' + esc(priorityLabel(o.priority)) + '</span>' +
|
||||
' <span class="kanban-card__priority badge badge--' + esc(o.status) + '">' + esc(STATUS_LABELS[o.status] || o.status) + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="kanban-card__customer">' + esc(o.customer_name || 'Cliente general') + '</div>' +
|
||||
'<div class="kanban-card__vehicle">' + esc(o.vehicle_plate || 'Sin vehículo') + '</div>' +
|
||||
'<div class="kanban-card__vehicle">' + esc(o.vehicle_description || o.vehicle_plate || 'Sin vehículo') + '</div>' +
|
||||
'<div class="kanban-card__meta">' +
|
||||
' <span class="kanban-card__mechanic">🔧 ' + esc(o.employee_name || 'Sin asignar') + '</span>' +
|
||||
priceHtml +
|
||||
@@ -355,47 +457,56 @@ var Workshop = (function() {
|
||||
var activeTab = document.querySelector('.so-tabs__btn.is-active');
|
||||
var selectedTab = activeTab ? activeTab.dataset.tab : 'service';
|
||||
|
||||
var mechanicAssignHtml = isMechanic ?
|
||||
' <div class="so-detail-header__row"><span class="so-detail__label">Mecánico asignado:</span> ' +
|
||||
'<input id="mechanicNameInput" class="form-input" style="width:auto;min-width:180px;" value="' + esc(o.mechanic_name || '') + '" placeholder="Nombre del mecánico" />' +
|
||||
' <button class="btn btn--sm btn--secondary" onclick="Workshop.saveMechanicName()">Guardar</button></div>' :
|
||||
' <div class="so-detail-header__row"><span class="so-detail__label">Mecánico asignado:</span> ' + esc(o.mechanic_name || o.employee_name || 'Sin asignar') + '</div>';
|
||||
|
||||
var html =
|
||||
'<div class="so-detail-header">' +
|
||||
' <div class="so-detail-header__row"><span class="so-detail__label">Estatus:</span> <span class="badge badge--' + esc(o.status) + '">' + esc(STATUS_LABELS[o.status] || o.status) + '</span></div>' +
|
||||
' <div class="so-detail-header__row"><span class="so-detail__label">Fecha:</span> ' + fmtDate(o.created_at) + '</div>' +
|
||||
' <div class="so-detail-header__row"><span class="so-detail__label">Registrado por:</span> ' + esc(o.created_by_name || '—') + '</div>' +
|
||||
' <div class="so-detail-header__row"><span class="so-detail__label">Sucursal:</span> ' + esc(o.branch_name || '—') + '</div>' +
|
||||
mechanicAssignHtml +
|
||||
'</div>' +
|
||||
|
||||
(isRestricted ? '' :
|
||||
'<div class="so-detail-info">' +
|
||||
' <div class="so-detail__field"><span class="so-detail__label">Cliente</span><span class="so-detail__value">' + esc(o.customer_name || '—') + '</span></div>' +
|
||||
' <div class="so-detail__field"><span class="so-detail__label">Taller</span><span class="so-detail__value">' + esc(o.workshop_name || '—') + '</span></div>' +
|
||||
' <div class="so-detail__field"><span class="so-detail__label">Dirección</span><span class="so-detail__value">' + esc(o.customer_address || '—') + '</span></div>' +
|
||||
' <div class="so-detail__field"><span class="so-detail__label">Teléfono</span><span class="so-detail__value">' + esc(o.customer_phone || '—') + '</span></div>' +
|
||||
' <div class="so-detail__field"><span class="so-detail__label">Vehículo</span><span class="so-detail__value">' + esc((o.vehicle_plate || '—') + ' ' + (o.vehicle_make || '') + ' ' + (o.vehicle_model || '')) + '</span></div>' +
|
||||
' <div class="so-detail__field"><span class="so-detail__label">Vehículo</span><span class="so-detail__value">' + esc(o.vehicle_description || (o.vehicle_plate ? (o.vehicle_plate + ' ' + (o.vehicle_make || '') + ' ' + (o.vehicle_model || '')).trim() : '—')) + '</span></div>' +
|
||||
' <div class="so-detail__field"><span class="so-detail__label">Vía de entrega</span><span class="so-detail__value">' + esc(DELIVERY_LABELS[o.delivery_method] || o.delivery_method || '—') + '</span></div>' +
|
||||
' <div class="so-detail__field"><span class="so-detail__label">Motociclista</span><span class="so-detail__value">' + esc(o.courier_name || '—') + '</span></div>' +
|
||||
' <div class="so-detail__field"><span class="so-detail__label">Factura</span><span class="so-detail__value">' + (o.requires_invoice ? 'Sí requiere' : 'No requiere') + '</span></div>' +
|
||||
' <div class="so-detail__field"><span class="so-detail__label">Mecánico</span><span class="so-detail__value">' + esc(o.employee_name || 'Sin asignar') + '</span></div>' +
|
||||
' <div class="so-detail__field"><span class="so-detail__label">Entrega estimada</span><span class="so-detail__value">' + fmtDate(o.estimated_completion) + '</span></div>' +
|
||||
' <div class="so-detail__field"><span class="so-detail__label">Kilometraje entrada</span><span class="so-detail__value">' + fmt(o.mileage_in) + '</span></div>' +
|
||||
(hidePrices ? '' : '<div class="so-detail__field"><span class="so-detail__label">Presupuesto</span><span class="so-detail__value">' + fmtMoney(o.estimated_cost) + '</span></div>') +
|
||||
(hidePrices ? '' : '<div class="so-detail__field"><span class="so-detail__label">Total</span><span class="so-detail__value">' + fmtMoney(o.total) + '</span></div>') +
|
||||
'</div>' +
|
||||
'</div>') +
|
||||
|
||||
(isRestricted ? '' :
|
||||
'<div class="so-tabs">' +
|
||||
' <button class="so-tabs__btn ' + (selectedTab === 'service' ? 'is-active' : '') + '" data-tab="service" onclick="Workshop.switchTab(\'service\')">Orden de servicio</button>' +
|
||||
' <button class="so-tabs__btn ' + (selectedTab === 'articles' ? 'is-active' : '') + '" data-tab="articles" onclick="Workshop.switchTab(\'articles\')">Artículos</button>' +
|
||||
'</div>' +
|
||||
'</div>') +
|
||||
|
||||
'<div class="so-tab-panel" id="tab-service" ' + (selectedTab === 'service' ? '' : 'style="display:none;"') + '>' +
|
||||
'<div class="so-tab-panel" id="tab-service" ' + (selectedTab === 'service' || isRestricted ? '' : 'style="display:none;"') + '>' +
|
||||
' <div class="so-detail__section">' +
|
||||
' <h3>Notas</h3>' +
|
||||
' <div class="so-notes-grid">' +
|
||||
' <label class="form-label">Recepción</label>' +
|
||||
' <textarea id="noteReception" class="form-input" rows="2" ' + (canEdit ? '' : 'readonly') + '>' + esc(o.reception_notes || '') + '</textarea>' +
|
||||
' <label class="form-label">Diagnóstico</label>' +
|
||||
' <textarea id="noteDiagnosis" class="form-input" rows="2" ' + (canEdit ? '' : 'readonly') + '>' + esc(o.diagnosis_notes || '') + '</textarea>' +
|
||||
' <textarea id="noteDiagnosis" class="form-input" rows="2" ' + (canEdit || isMechanic ? '' : 'readonly') + '>' + esc(o.diagnosis_notes || '') + '</textarea>' +
|
||||
' <label class="form-label">Reparación</label>' +
|
||||
' <textarea id="noteRepair" class="form-input" rows="2" ' + (canEdit ? '' : 'readonly') + '>' + esc(o.repair_notes || '') + '</textarea>' +
|
||||
' <textarea id="noteRepair" class="form-input" rows="2" ' + (canEdit || isMechanic ? '' : 'readonly') + '>' + esc(o.repair_notes || '') + '</textarea>' +
|
||||
' <label class="form-label">Entrega</label>' +
|
||||
' <textarea id="noteDelivery" class="form-input" rows="2" ' + (canEdit ? '' : 'readonly') + '>' + esc(o.delivery_notes || '') + '</textarea>' +
|
||||
' </div>' +
|
||||
(canEdit ? ' <button class="btn btn--secondary" style="margin-top:var(--space-2);" onclick="Workshop.saveNotes()">Guardar notas</button>' : '') +
|
||||
(canEdit || isMechanic ? ' <button class="btn btn--secondary" style="margin-top:var(--space-2);" onclick="Workshop.saveNotes()">Guardar notas</button>' : '') +
|
||||
' </div>' +
|
||||
' <div class="so-detail__section">' +
|
||||
' <h3>Bitácora</h3>' +
|
||||
@@ -403,16 +514,18 @@ var Workshop = (function() {
|
||||
' </div>' +
|
||||
'</div>' +
|
||||
|
||||
'<div class="so-tab-panel" id="tab-articles" ' + (selectedTab === 'articles' ? '' : 'style="display:none;"') + '>' +
|
||||
(isRestricted ? '' : '<div class="so-tab-panel" id="tab-articles" ' + (selectedTab === 'articles' ? '' : 'style="display:none;"') + '>' +
|
||||
renderArticles(o) +
|
||||
'</div>';
|
||||
'</div>');
|
||||
|
||||
document.getElementById('detailBody').innerHTML = html;
|
||||
|
||||
// Footer actions
|
||||
var footer = document.getElementById('detailFooter');
|
||||
var allowedNext = VALID_NEXT[o.status] || [];
|
||||
var statusHtml = canEdit && allowedNext.length ?
|
||||
var allowedNext = (VALID_NEXT[o.status] || []).filter(function(s) {
|
||||
return !isMechanic || ['cotizada','por_autorizar','autorizada','autorizacion_parcial','por_facturar','facturada'].indexOf(s) === -1;
|
||||
});
|
||||
var statusHtml = canChangeStatus && allowedNext.length ?
|
||||
'<div class="so-detail__actions" style="margin-right:auto;">' +
|
||||
' <select class="form-input" id="statusSelect" style="width:auto;">' +
|
||||
'<option value="' + esc(o.status) + '" selected>' + esc(STATUS_LABELS[o.status] || o.status) + '</option>' +
|
||||
@@ -424,10 +537,11 @@ var Workshop = (function() {
|
||||
'<button class="btn btn--ghost" onclick="Workshop.closeDetailModal()">Cerrar</button>' +
|
||||
(canEdit ? '<button class="btn btn--secondary" onclick="Workshop.openEditOrderModal()">Editar orden</button>' : '') +
|
||||
(canDelete ? '<button class="btn btn--danger" onclick="Workshop.deleteOrder()">Eliminar orden</button>' : '') +
|
||||
'<button class="btn btn--secondary" onclick="Workshop.printOrder()">' +
|
||||
(isRestricted ? '' : '<button class="btn btn--secondary" onclick="Workshop.printOrder()">' +
|
||||
'<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 orden</button>' +
|
||||
(canEdit && canSell && o.status === 'ready' && !o.sale_id ? '<button class="btn btn--primary" onclick="Workshop.convertToSale()">Convertir a venta</button>' : '') +
|
||||
'Imprimir orden</button>') +
|
||||
(canEdit && canSell && (o.status === 'por_entregar' || o.status === 'entregado') && !o.sale_id ? '<button class="btn btn--primary" onclick="Workshop.convertToSale()">Convertir a venta</button>' : '') +
|
||||
(canEdit && canSell && !o.sale_id && o.status !== 'cancelled' ? '<button class="btn btn--secondary" onclick="Workshop.convertToRemission()">Generar nota de remisión</button>' : '') +
|
||||
(o.sale_id ? '<a class="btn btn--secondary" href="/pos/invoicing?sale_id=' + o.sale_id + '">Ver venta #' + o.sale_id + '</a>' : '');
|
||||
}
|
||||
|
||||
@@ -439,93 +553,105 @@ var Workshop = (function() {
|
||||
|
||||
function renderBitacora(history) {
|
||||
if (!history.length) return '<p style="color:var(--color-text-muted);">Sin movimientos</p>';
|
||||
var vehicle = currentOrder ? (currentOrder.vehicle_description || (currentOrder.vehicle_plate ? currentOrder.vehicle_plate + ' ' + (currentOrder.vehicle_make || '') + ' ' + (currentOrder.vehicle_model || '') : null)) : null;
|
||||
var rows = history.map(function(h) {
|
||||
return '<tr>' +
|
||||
'<td><span class="badge badge--' + esc(h.new_status) + '">' + esc(STATUS_LABELS[h.new_status] || h.new_status) + '</span></td>' +
|
||||
'<td>' + fmtDate(h.created_at) + '</td>' +
|
||||
'<td>' + esc(h.changed_by_name || '—') + '</td>' +
|
||||
(isMechanic && vehicle ? '<td>' + esc(vehicle) + '</td>' : '') +
|
||||
'<td>' + esc(h.notes || '—') + '</td>' +
|
||||
'</tr>';
|
||||
}).join('');
|
||||
return '<table class="data-table bitacora-table"><thead><tr><th>Estatus</th><th>Fecha</th><th>Usuario</th><th>Observaciones</th></tr></thead><tbody>' + rows + '</tbody></table>';
|
||||
var vehicleTh = isMechanic && vehicle ? '<th>Vehículo</th>' : '';
|
||||
return '<table class="data-table bitacora-table"><thead><tr><th>Estatus</th><th>Fecha</th><th>Usuario</th>' + vehicleTh + '<th>Observaciones</th></tr></thead><tbody>' + rows + '</tbody></table>';
|
||||
}
|
||||
|
||||
function mechanicName(id) {
|
||||
var e = employees.find(function(x) { return x.id === id; });
|
||||
return e ? e.name : '—';
|
||||
}
|
||||
|
||||
function ensureEmployees() {
|
||||
if (employees && employees.length) return Promise.resolve(employees);
|
||||
return fetch('/pos/api/config/employees?per_page=500', {headers: headers()})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
employees = (d.data || d.employees || []);
|
||||
return employees;
|
||||
})
|
||||
.catch(function() { employees = []; return employees; });
|
||||
}
|
||||
|
||||
function populateMechanicSelect(selectId, selectedId) {
|
||||
var sel = document.getElementById(selectId);
|
||||
if (!sel) return;
|
||||
ensureEmployees().then(function() {
|
||||
sel.innerHTML = '<option value="">— Sin asignar —</option>' +
|
||||
employees
|
||||
.filter(function(e) { return e.is_active && (e.role === 'mechanic' || e.role === 'workshop'); })
|
||||
.map(function(e) {
|
||||
return '<option value="' + e.id + '"' + (e.id === selectedId ? ' selected' : '') + '>' + esc(e.name) + '</option>';
|
||||
}).join('');
|
||||
});
|
||||
}
|
||||
|
||||
function renderArticles(o) {
|
||||
var colCount = (hidePrices ? 5 : 6) - (isRestricted ? 1 : 0);
|
||||
var partsRows = (o.items || []).map(function(it) {
|
||||
var priceCells = hidePrices ? '' :
|
||||
'<td>' + fmtMoney(it.unit_price) + '</td>';
|
||||
var actionCell = canEdit && it.status !== 'cancelled' ?
|
||||
'<td><button class="btn btn--sm btn--secondary" onclick="event.stopPropagation();Workshop.reserveItem(' + it.id + ')">Reservar</button></td>' : '<td></td>';
|
||||
var priceCells = hidePrices ? '' : '<td>' + fmtMoney(it.unit_price) + '</td>';
|
||||
var mechanicCells = isRestricted ? '' : '<td>' + esc(mechanicName(it.mechanic_id)) + '</td>';
|
||||
var actionCell = canEdit && it.status !== 'cancelado' ?
|
||||
'<td><button class="btn btn--sm btn--secondary" onclick="event.stopPropagation();Workshop.editItemInline(' + it.id + ')">Editar</button></td>' : '<td></td>';
|
||||
return '<tr>' +
|
||||
'<td>' + esc(it.name) + '<br><small>' + esc(it.part_number || '') + '</small></td>' +
|
||||
'<td>' + fmt(it.quantity) + '</td>' +
|
||||
priceCells +
|
||||
'<td><span class="badge ' + statusBadgeClass(it.status) + '">' + esc(STATUS_LABELS[it.status] || it.status) + '</span></td>' +
|
||||
mechanicCells +
|
||||
'<td><span class="badge ' + statusBadgeClass(it.status) + '">' + esc(ITEM_STATUS_LABELS[it.status] || it.status) + '</span></td>' +
|
||||
'<td>' + esc(it.observations || '') + '</td>' +
|
||||
actionCell +
|
||||
'</tr>';
|
||||
}).join('');
|
||||
var partsHeader = '<tr><th>Concepto</th><th>Cant.</th>' + (hidePrices ? '' : '<th>Precio</th>') + '<th>Estado</th><th></th></tr>';
|
||||
|
||||
var laborRows = (o.labor || []).map(function(l) {
|
||||
var priceCells = hidePrices ? '' : '<td>' + fmtMoney(l.hourly_rate) + '</td><td>' + fmtMoney(l.total_cost) + '</td>';
|
||||
return '<tr>' +
|
||||
'<td>' + esc(l.description) + '</td>' +
|
||||
'<td>' + fmt(l.hours) + '</td>' +
|
||||
priceCells +
|
||||
'<td><span class="badge ' + statusBadgeClass(l.status) + '">' + esc(STATUS_LABELS[l.status] || l.status) + '</span></td>' +
|
||||
'</tr>';
|
||||
}).join('');
|
||||
var laborHeader = '<tr><th>Concepto</th><th>Horas</th>' + (hidePrices ? '' : '<th>Precio/hr</th><th>Total</th>') + '<th>Estado</th></tr>';
|
||||
var partsHeader = '<tr><th>Concepto</th><th>Cant.</th>' + (hidePrices ? '' : '<th>Precio</th>') + (isRestricted ? '' : '<th>Mecánico</th>') + '<th>Estado</th><th>Observaciones</th><th></th></tr>';
|
||||
|
||||
var addParts = canEdit ?
|
||||
'<div style="margin-top:var(--space-3);display:flex;gap:var(--space-2);align-items:flex-start;flex-wrap:wrap;">' +
|
||||
' <div style="position:relative;flex:1;min-width:200px;">' +
|
||||
' <div style="position:relative;flex:1;min-width:160px;">' +
|
||||
' <input class="form-input" id="newItemSearch" placeholder="Buscar refacción por nombre/número" autocomplete="off" oninput="Workshop.searchItemsForSO()" />' +
|
||||
' <div id="itemSearchResults" style="display:none;position:absolute;z-index:10;top:100%;left:0;right:0;max-height:220px;overflow-y:auto;background:#fff;border:1px solid var(--color-border);border-radius:var(--radius-md);box-shadow:0 4px 12px rgba(0,0,0,.15);"></div>' +
|
||||
' </div>' +
|
||||
' <input class="form-input" id="newItemQty" type="number" value="1" min="1" style="width:80px;" />' +
|
||||
' <input class="form-input" id="newItemQty" type="number" value="1" min="1" style="width:70px;" />' +
|
||||
(hidePrices ? '' : '<input class="form-input" id="newItemPrice" type="number" step="0.01" placeholder="Precio" style="width:90px;" />') +
|
||||
' <select class="form-input" id="newItemMechanic" style="width:auto;"><option value="">— Mecánico —</option></select>' +
|
||||
' <select class="form-input" id="newItemStatus" style="width:auto;">' +
|
||||
Object.keys(ITEM_STATUS_LABELS).map(function(s) { return '<option value="' + s + '">' + esc(ITEM_STATUS_LABELS[s]) + '</option>'; }).join('') +
|
||||
' </select>' +
|
||||
' <input class="form-input" id="newItemObs" placeholder="Observaciones" style="min-width:140px;flex:1;" />' +
|
||||
' <button class="btn btn--secondary" onclick="Workshop.addSelectedItem()">Agregar</button>' +
|
||||
'</div>' : '';
|
||||
|
||||
var addLabor = canEdit ?
|
||||
'<div style="margin-top:var(--space-3);display:flex;gap:var(--space-2);flex-wrap:wrap;">' +
|
||||
' <select class="form-input" id="laborCatalogSelect"><option value="">Concepto manual</option></select>' +
|
||||
' <input class="form-input" id="laborDesc" placeholder="Descripción" style="flex:1;min-width:160px;" />' +
|
||||
' <input class="form-input" id="laborHours" type="number" step="0.1" placeholder="Hrs" style="width:80px;" />' +
|
||||
(hidePrices ? '' : '<input class="form-input" id="laborRate" type="number" step="0.01" placeholder="$/hr" style="width:100px;" />') +
|
||||
' <button class="btn btn--secondary" onclick="Workshop.addLabor()">Agregar</button>' +
|
||||
'</div>' : '';
|
||||
|
||||
var html =
|
||||
'<div class="so-detail__section">' +
|
||||
' <h3>Refacciones</h3>' +
|
||||
' <table class="data-table"><thead>' + partsHeader + '</thead><tbody>' + (partsRows || '<tr><td colspan="' + (hidePrices ? 4 : 5) + '" style="text-align:center;">Sin refacciones</td></tr>') + '</tbody></table>' +
|
||||
' <h3>Artículos</h3>' +
|
||||
' <table class="data-table"><thead>' + partsHeader + '</thead><tbody>' + (partsRows || '<tr><td colspan="' + colCount + '" style="text-align:center;">Sin artículos</td></tr>') + '</tbody></table>' +
|
||||
addParts +
|
||||
'</div>' +
|
||||
'<div class="so-detail__section">' +
|
||||
' <h3>Mano de obra</h3>' +
|
||||
' <table class="data-table"><thead>' + laborHeader + '</thead><tbody>' + (laborRows || '<tr><td colspan="' + (hidePrices ? 3 : 5) + '" style="text-align:center;">Sin mano de obra</td></tr>') + '</tbody></table>' +
|
||||
addLabor +
|
||||
'</div>';
|
||||
|
||||
// schedule labor catalog select population after DOM insertion
|
||||
// populate mechanic select after DOM insertion
|
||||
setTimeout(function() {
|
||||
var sel = document.getElementById('laborCatalogSelect');
|
||||
if (!sel || sel.dataset.populated) return;
|
||||
catalog.forEach(function(c) {
|
||||
var opt = document.createElement('option');
|
||||
opt.value = JSON.stringify(c);
|
||||
opt.textContent = c.name + (hidePrices ? '' : ' ($' + fmtMoney(c.suggested_hours * c.suggested_rate).replace('$', '') + ')');
|
||||
sel.appendChild(opt);
|
||||
ensureEmployees().then(function() {
|
||||
var sel = document.getElementById('newItemMechanic');
|
||||
if (!sel || sel.dataset.populated) return;
|
||||
employees.forEach(function(e) {
|
||||
if (!e.is_active) return;
|
||||
var opt = document.createElement('option');
|
||||
opt.value = e.id;
|
||||
opt.textContent = e.name;
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
sel.dataset.populated = '1';
|
||||
});
|
||||
sel.dataset.populated = '1';
|
||||
sel.onchange = function() {
|
||||
if (!sel.value) return;
|
||||
var c = JSON.parse(sel.value);
|
||||
document.getElementById('laborDesc').value = c.name;
|
||||
document.getElementById('laborHours').value = c.suggested_hours;
|
||||
if (!hidePrices) document.getElementById('laborRate').value = c.suggested_rate;
|
||||
};
|
||||
}, 0);
|
||||
|
||||
return html;
|
||||
@@ -559,12 +685,9 @@ var Workshop = (function() {
|
||||
async function populateEditOrderModal() {
|
||||
var o = currentOrder;
|
||||
selectedEditCustomer = o.customer_id ? {id: o.customer_id, name: o.customer_name || ''} : null;
|
||||
selectedEditVehicle = o.vehicle_id ? {id: o.vehicle_id, label: (o.vehicle_plate || '') + ' ' + (o.vehicle_make || '') + ' ' + (o.vehicle_model || '')} : null;
|
||||
|
||||
document.getElementById('eoCustomerSearch').value = selectedEditCustomer ? selectedEditCustomer.name : '';
|
||||
document.getElementById('eoCustomerId').value = selectedEditCustomer ? selectedEditCustomer.id : '';
|
||||
document.getElementById('eoVehicleSearch').value = selectedEditVehicle ? selectedEditVehicle.label.trim() : '';
|
||||
document.getElementById('eoVehicleId').value = selectedEditVehicle ? selectedEditVehicle.id : '';
|
||||
|
||||
// Branches
|
||||
var branchSel = document.getElementById('eoBranch');
|
||||
@@ -572,25 +695,26 @@ var Workshop = (function() {
|
||||
return '<option value="' + b.id + '"' + (b.id === o.branch_id ? ' selected' : '') + '>' + esc(b.name) + '</option>';
|
||||
}).join('');
|
||||
|
||||
// Mechanics/employees
|
||||
var mechSel = document.getElementById('eoMechanic');
|
||||
mechSel.innerHTML = '<option value="">— Ninguno —</option>';
|
||||
try {
|
||||
var res = await fetch('/pos/api/config/employees', {headers: headers()});
|
||||
var json = await res.json();
|
||||
(json.data || []).forEach(function(e) {
|
||||
if (!e.is_active) return;
|
||||
mechSel.innerHTML += '<option value="' + e.id + '"' + (e.id === o.employee_id ? ' selected' : '') + '>' + esc(e.name) + '</option>';
|
||||
});
|
||||
} catch (e) {}
|
||||
document.getElementById('eoWorkshopName').value = o.workshop_name || '';
|
||||
document.getElementById('eoCustomerAddress').value = o.customer_address || '';
|
||||
document.getElementById('eoCustomerPhone').value = o.customer_phone || '';
|
||||
document.getElementById('eoVehicleDescription').value = o.vehicle_description || '';
|
||||
|
||||
// Delivery / courier
|
||||
var deliverySel = document.getElementById('eoDelivery');
|
||||
deliverySel.value = o.delivery_method || '';
|
||||
var courierField = document.getElementById('eoCourierField');
|
||||
var courierSel = document.getElementById('eoCourier');
|
||||
courierSel.innerHTML = couriers.map(function(c) {
|
||||
return '<option value="' + c.id + '"' + (c.id === o.courier_id ? ' selected' : '') + '>' + esc(c.name) + '</option>';
|
||||
}).join('');
|
||||
courierField.style.display = (o.delivery_method === 'delivery' || o.delivery_method === 'courier') ? 'block' : 'none';
|
||||
|
||||
document.getElementById('eoPriority').value = o.priority || 'normal';
|
||||
document.getElementById('eoFuelLevel').value = o.fuel_level || '';
|
||||
document.getElementById('eoMileageIn').value = o.mileage_in != null ? o.mileage_in : '';
|
||||
document.getElementById('eoMileageOut').value = o.mileage_out != null ? o.mileage_out : '';
|
||||
document.getElementById('eoEstimatedCompletion').value = toDatetimeLocal(o.estimated_completion);
|
||||
document.getElementById('eoEstimatedCost').value = o.estimated_cost != null ? o.estimated_cost : '';
|
||||
populateMechanicSelect('eoMechanic', o.employee_id || null);
|
||||
document.getElementById('eoMechanicName').value = o.mechanic_name || '';
|
||||
document.getElementById('eoNotes').value = o.reception_notes || '';
|
||||
document.getElementById('eoRequiresInvoice').checked = !!o.requires_invoice;
|
||||
}
|
||||
|
||||
function hideEditSearchResults(type) {
|
||||
@@ -638,36 +762,9 @@ var Workshop = (function() {
|
||||
}
|
||||
|
||||
function searchVehiclesForSO() {
|
||||
var input = document.getElementById('eoVehicleSearch');
|
||||
// Fleet module removed; vehicle is free-text in the taller flow.
|
||||
var box = document.getElementById('eoVehicleResults');
|
||||
var q = input.value.trim();
|
||||
selectedEditVehicle = null;
|
||||
document.getElementById('eoVehicleId').value = '';
|
||||
if (!q || q.length < 2) {
|
||||
box.style.display = 'none'; box.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
clearTimeout(vehicleSearchTimer);
|
||||
vehicleSearchTimer = setTimeout(function() {
|
||||
fetch('/pos/api/fleet/vehicles?q=' + encodeURIComponent(q) + '&per_page=20&active_only=true', {headers: headers()})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
var items = d.data || [];
|
||||
if (!items.length) {
|
||||
box.innerHTML = '<div style="padding:var(--space-2);color:var(--color-text-muted);">Sin resultados</div>';
|
||||
box.style.display = 'block'; return;
|
||||
}
|
||||
box.innerHTML = items.map(function(v) {
|
||||
var label = (v.plate || '') + ' · ' + (v.make || '') + ' ' + (v.model || '');
|
||||
return '<div class="so-search-result" style="padding:var(--space-2);cursor:pointer;border-bottom:1px solid var(--color-border);" onclick="Workshop.selectEditVehicle(' + v.id + ', \'' + escJs(label) + '\')">' +
|
||||
'<strong>' + esc(v.plate || '') + '</strong>' +
|
||||
'<small>' + esc(v.make || '') + ' ' + esc(v.model || '') + ' · ' + esc(v.owner_name || '') + '</small>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
box.style.display = 'block';
|
||||
})
|
||||
.catch(function() { box.style.display = 'none'; });
|
||||
}, 250);
|
||||
if (box) { box.style.display = 'none'; box.innerHTML = ''; }
|
||||
}
|
||||
|
||||
function selectEditVehicle(id, label) {
|
||||
@@ -680,19 +777,22 @@ var Workshop = (function() {
|
||||
async function saveOrderChanges() {
|
||||
if (!currentOrderId) return;
|
||||
var customerId = selectedEditCustomer ? selectedEditCustomer.id : parseInt(document.getElementById('eoCustomerId').value, 10) || null;
|
||||
var vehicleId = selectedEditVehicle ? selectedEditVehicle.id : parseInt(document.getElementById('eoVehicleId').value, 10) || null;
|
||||
var delivery = document.getElementById('eoDelivery').value;
|
||||
var mechanicVal = document.getElementById('eoMechanic').value;
|
||||
var payload = {
|
||||
customer_id: customerId,
|
||||
vehicle_id: vehicleId,
|
||||
branch_id: parseInt(document.getElementById('eoBranch').value, 10) || null,
|
||||
employee_id: parseInt(document.getElementById('eoMechanic').value, 10) || null,
|
||||
priority: document.getElementById('eoPriority').value,
|
||||
fuel_level: document.getElementById('eoFuelLevel').value || null,
|
||||
mileage_in: document.getElementById('eoMileageIn').value ? parseInt(document.getElementById('eoMileageIn').value, 10) : null,
|
||||
mileage_out: document.getElementById('eoMileageOut').value ? parseInt(document.getElementById('eoMileageOut').value, 10) : null,
|
||||
estimated_completion: document.getElementById('eoEstimatedCompletion').value || null,
|
||||
workshop_name: document.getElementById('eoWorkshopName').value.trim() || null,
|
||||
customer_address: document.getElementById('eoCustomerAddress').value.trim() || null,
|
||||
customer_phone: document.getElementById('eoCustomerPhone').value.trim() || null,
|
||||
vehicle_description: document.getElementById('eoVehicleDescription').value.trim() || null,
|
||||
delivery_method: delivery || null,
|
||||
courier_id: (delivery === 'delivery' || delivery === 'courier') ? (parseInt(document.getElementById('eoCourier').value, 10) || null) : null,
|
||||
estimated_cost: document.getElementById('eoEstimatedCost').value ? parseFloat(document.getElementById('eoEstimatedCost').value) : null,
|
||||
reception_notes: document.getElementById('eoNotes').value
|
||||
employee_id: mechanicVal ? parseInt(mechanicVal, 10) : null,
|
||||
mechanic_name: document.getElementById('eoMechanicName').value.trim() || null,
|
||||
reception_notes: document.getElementById('eoNotes').value,
|
||||
requires_invoice: document.getElementById('eoRequiresInvoice').checked
|
||||
};
|
||||
try {
|
||||
await api('PUT', '/' + currentOrderId, payload);
|
||||
@@ -832,8 +932,11 @@ var Workshop = (function() {
|
||||
opt.textContent = label.trim();
|
||||
sel.appendChild(opt);
|
||||
sel.value = res.id;
|
||||
document.getElementById('noWorkshopName').value = name;
|
||||
document.getElementById('noCustomerPhone').value = payload.phone || '';
|
||||
document.getElementById('noCustomerAddress').value = payload.address || '';
|
||||
if (typeof customers !== 'undefined') {
|
||||
customers.push({id: res.id, name: name, phone: payload.phone, rfc: payload.rfc});
|
||||
customers.push({id: res.id, name: name, phone: payload.phone, rfc: payload.rfc, address: payload.address});
|
||||
}
|
||||
} else {
|
||||
selectedEditCustomer = {id: res.id, name: name};
|
||||
@@ -867,25 +970,46 @@ var Workshop = (function() {
|
||||
|
||||
function saveNotes() {
|
||||
if (!currentOrderId) return;
|
||||
var payload = {
|
||||
reception_notes: document.getElementById('noteReception').value,
|
||||
diagnosis_notes: document.getElementById('noteDiagnosis').value,
|
||||
repair_notes: document.getElementById('noteRepair').value,
|
||||
delivery_notes: document.getElementById('noteDelivery').value
|
||||
};
|
||||
var payload;
|
||||
if (isMechanic) {
|
||||
payload = {
|
||||
diagnosis_notes: document.getElementById('noteDiagnosis').value,
|
||||
repair_notes: document.getElementById('noteRepair').value
|
||||
};
|
||||
} else {
|
||||
payload = {
|
||||
reception_notes: document.getElementById('noteReception').value,
|
||||
diagnosis_notes: document.getElementById('noteDiagnosis').value,
|
||||
repair_notes: document.getElementById('noteRepair').value,
|
||||
delivery_notes: document.getElementById('noteDelivery').value
|
||||
};
|
||||
}
|
||||
api('PUT', '/' + currentOrderId, payload)
|
||||
.then(function() {
|
||||
alert('Notas guardadas');
|
||||
if (currentOrder) {
|
||||
currentOrder.reception_notes = payload.reception_notes;
|
||||
currentOrder.diagnosis_notes = payload.diagnosis_notes;
|
||||
currentOrder.repair_notes = payload.repair_notes;
|
||||
currentOrder.delivery_notes = payload.delivery_notes;
|
||||
if ('reception_notes' in payload) currentOrder.reception_notes = payload.reception_notes;
|
||||
if ('diagnosis_notes' in payload) currentOrder.diagnosis_notes = payload.diagnosis_notes;
|
||||
if ('repair_notes' in payload) currentOrder.repair_notes = payload.repair_notes;
|
||||
if ('delivery_notes' in payload) currentOrder.delivery_notes = payload.delivery_notes;
|
||||
}
|
||||
})
|
||||
.catch(function(e) { alert('Error: ' + e.message); });
|
||||
}
|
||||
|
||||
function saveMechanicName() {
|
||||
if (!currentOrderId) return;
|
||||
var input = document.getElementById('mechanicNameInput');
|
||||
if (!input) return;
|
||||
var payload = { mechanic_name: input.value.trim() || null };
|
||||
api('PUT', '/' + currentOrderId, payload)
|
||||
.then(function() {
|
||||
alert('Mecánico asignado guardado');
|
||||
if (currentOrder) currentOrder.mechanic_name = payload.mechanic_name;
|
||||
})
|
||||
.catch(function(e) { alert('Error: ' + e.message); });
|
||||
}
|
||||
|
||||
function reserveItem(itemId) {
|
||||
api('POST', '/' + currentOrderId + '/items/' + itemId + '/reserve', {})
|
||||
.then(function() {
|
||||
@@ -923,10 +1047,12 @@ var Workshop = (function() {
|
||||
box.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
var orderTier = currentOrder ? (currentOrder.customer_price_tier || 1) : 1;
|
||||
box.innerHTML = items.map(function(it) {
|
||||
return '<div class="so-search-result" data-id="' + it.id + '" data-name="' + esc(it.name) + '" data-part="' + esc(it.part_number || '') + '" data-price="' + (it.price_1 || 0) + '" data-cost="' + (it.cost || 0) + '" style="padding:var(--space-2);cursor:pointer;border-bottom:1px solid var(--color-border);" onclick="Workshop.selectInventoryItem(' + it.id + ')">' +
|
||||
var price = priceForTier(it.price_1, orderTier);
|
||||
return '<div class="so-search-result" data-id="' + it.id + '" data-name="' + esc(it.name) + '" data-part="' + esc(it.part_number || '') + '" data-price="' + price + '" data-cost="' + (it.cost || 0) + '" style="padding:var(--space-2);cursor:pointer;border-bottom:1px solid var(--color-border);" onclick="Workshop.selectInventoryItem(' + it.id + ')">' +
|
||||
'<div><strong>' + esc(it.name) + '</strong></div>' +
|
||||
'<small>' + esc(it.part_number || '') + ' · ' + esc(it.brand || '') + ' · Stock: ' + fmt(it.stock) + (hidePrices ? '' : ' · ' + fmtMoney(it.price_1)) + '</small>' +
|
||||
'<small>' + esc(it.part_number || '') + ' · ' + esc(it.brand || '') + ' · Stock: ' + fmt(it.stock) + (hidePrices ? '' : ' · ' + fmtMoney(price)) + '</small>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
box.style.display = 'block';
|
||||
@@ -954,20 +1080,28 @@ var Workshop = (function() {
|
||||
|
||||
function addSelectedItem() {
|
||||
if (!currentOrderId) return;
|
||||
var qty = parseInt(document.getElementById('newItemQty').value, 10) || 1;
|
||||
var qty = parseFloat(document.getElementById('newItemQty').value) || 1;
|
||||
var status = document.getElementById('newItemStatus').value || 'por_revisar';
|
||||
var mechanicId = parseInt(document.getElementById('newItemMechanic').value, 10) || null;
|
||||
var observations = document.getElementById('newItemObs').value.trim();
|
||||
if (selectedInventoryItem) {
|
||||
var price = hidePrices ? selectedInventoryItem.unit_price : (parseFloat(document.getElementById('newItemPrice').value) || selectedInventoryItem.unit_price || 0);
|
||||
api('POST', '/' + currentOrderId + '/items', {
|
||||
inventory_id: selectedInventoryItem.id,
|
||||
part_number: selectedInventoryItem.part_number,
|
||||
name: selectedInventoryItem.name,
|
||||
quantity: qty,
|
||||
unit_cost: selectedInventoryItem.unit_cost,
|
||||
unit_price: selectedInventoryItem.unit_price,
|
||||
status: 'pending'
|
||||
unit_price: price,
|
||||
status: status,
|
||||
mechanic_id: mechanicId,
|
||||
observations: observations
|
||||
}).then(function() {
|
||||
selectedInventoryItem = null;
|
||||
document.getElementById('newItemSearch').value = '';
|
||||
document.getElementById('newItemQty').value = '1';
|
||||
if (!hidePrices) document.getElementById('newItemPrice').value = '';
|
||||
document.getElementById('newItemObs').value = '';
|
||||
openDetail(currentOrderId);
|
||||
}).catch(function(e) { alert('Error: ' + e.message); });
|
||||
return;
|
||||
@@ -975,19 +1109,36 @@ var Workshop = (function() {
|
||||
// Fallback: manual placeholder (no inventory link)
|
||||
var name = document.getElementById('newItemSearch').value.trim();
|
||||
if (!name) return;
|
||||
if (!confirm('No seleccionaste una refacción del inventario. ¿Agregar como concepto manual? No podrá reservarse.')) return;
|
||||
var manualPrice = hidePrices ? 0 : (parseFloat(document.getElementById('newItemPrice').value) || 0);
|
||||
api('POST', '/' + currentOrderId + '/items', {
|
||||
name: name,
|
||||
quantity: qty,
|
||||
unit_price: 0,
|
||||
status: 'pending'
|
||||
unit_price: manualPrice,
|
||||
status: status,
|
||||
mechanic_id: mechanicId,
|
||||
observations: observations
|
||||
}).then(function() {
|
||||
document.getElementById('newItemSearch').value = '';
|
||||
document.getElementById('newItemQty').value = '1';
|
||||
if (!hidePrices) document.getElementById('newItemPrice').value = '';
|
||||
document.getElementById('newItemObs').value = '';
|
||||
openDetail(currentOrderId);
|
||||
}).catch(function(e) { alert('Error: ' + e.message); });
|
||||
}
|
||||
|
||||
function editItemInline(itemId) {
|
||||
if (!currentOrder || !currentOrder.items) return;
|
||||
var it = currentOrder.items.find(function(x) { return x.id === itemId; });
|
||||
if (!it) return;
|
||||
var newStatus = prompt('Nuevo estado (' + Object.keys(ITEM_STATUS_LABELS).join(', ') + '):', it.status);
|
||||
if (!newStatus || !ITEM_STATUS_LABELS[newStatus]) return;
|
||||
var newObs = prompt('Observaciones:', it.observations || '');
|
||||
var payload = {status: newStatus, observations: newObs != null ? newObs : it.observations};
|
||||
api('PUT', '/items/' + itemId, payload)
|
||||
.then(function() { openDetail(currentOrderId); })
|
||||
.catch(function(e) { alert('Error: ' + e.message); });
|
||||
}
|
||||
|
||||
function addLabor() {
|
||||
var desc = document.getElementById('laborDesc').value.trim();
|
||||
var hours = parseFloat(document.getElementById('laborHours').value) || 0;
|
||||
@@ -1020,6 +1171,18 @@ var Workshop = (function() {
|
||||
}).catch(function(e) { alert('Error: ' + e.message); });
|
||||
}
|
||||
|
||||
function convertToRemission() {
|
||||
if (!currentOrderId) return;
|
||||
if (!confirm('¿Generar nota de remisión desde esta orden? Se reservarán las refacciones y quedará pendiente de cobro.')) return;
|
||||
api('POST', '/' + currentOrderId + '/convert-to-remission', {})
|
||||
.then(function(r) {
|
||||
alert('Nota de remisión creada: #' + r.sale_id + ' Total: ' + fmtMoney(r.total));
|
||||
closeDetailModal();
|
||||
loadSummary();
|
||||
loadOrders();
|
||||
}).catch(function(e) { alert('Error: ' + e.message); });
|
||||
}
|
||||
|
||||
function deleteOrder() {
|
||||
if (!currentOrderId) return;
|
||||
if (!confirm('¿Eliminar esta orden de servicio? Se ocultará del taller pero las reservas de inventario y la venta asociada (si existe) no se verán afectadas.')) return;
|
||||
@@ -1058,10 +1221,22 @@ var Workshop = (function() {
|
||||
// ─── New order ───
|
||||
|
||||
function openNewOrderModal() {
|
||||
populateSelect('noBranch', branches, function(b) { return {value: b.id, text: b.name}; });
|
||||
populateSelect('noCustomer', customers, function(c) { return {value: c.id, text: c.name + ' (' + (c.phone || '') + ')'}; });
|
||||
populateSelect('noVehicle', vehicles, function(v) { return {value: v.id, text: v.plate + ' ' + v.make + ' ' + v.model}; });
|
||||
populateSelect('noMechanic', employees, function(e) { return {value: e.id, text: e.name}; });
|
||||
populateSelect('noCourier', couriers, function(c) { return {value: c.id, text: c.name}; });
|
||||
populateMechanicSelect('noMechanic', null);
|
||||
document.getElementById('noMechanicName').value = '';
|
||||
document.getElementById('noEstimatedCost').value = '';
|
||||
var noCustomer = document.getElementById('noCustomer');
|
||||
noCustomer.onchange = function() {
|
||||
var cid = parseInt(noCustomer.value, 10);
|
||||
var c = customers.find(function(x) { return x.id === cid; });
|
||||
if (c) {
|
||||
document.getElementById('noWorkshopName').value = c.name || '';
|
||||
document.getElementById('noCustomerPhone').value = c.phone || '';
|
||||
document.getElementById('noCustomerAddress').value = c.address || '';
|
||||
}
|
||||
};
|
||||
document.getElementById('newOrderModal').classList.add('is-open');
|
||||
}
|
||||
|
||||
@@ -1074,19 +1249,27 @@ var Workshop = (function() {
|
||||
|
||||
function submitNewOrder() {
|
||||
var customerId = document.getElementById('noCustomer').value;
|
||||
var branchId = document.getElementById('noBranch').value;
|
||||
if (!customerId) return alert('Selecciona un cliente');
|
||||
if (!branchId) return alert('Selecciona una sucursal');
|
||||
var delivery = document.getElementById('noDelivery').value;
|
||||
var mechanicVal = document.getElementById('noMechanic').value;
|
||||
var estimatedCostVal = document.getElementById('noEstimatedCost').value;
|
||||
var payload = {
|
||||
branch_id: parseInt(branchId, 10),
|
||||
customer_id: parseInt(customerId, 10),
|
||||
vehicle_id: parseInt(document.getElementById('noVehicle').value, 10) || null,
|
||||
employee_id: parseInt(document.getElementById('noMechanic').value, 10) || null,
|
||||
priority: document.getElementById('noPriority').value,
|
||||
estimated_completion: document.getElementById('noEstimatedCompletion').value || null,
|
||||
mileage_in: parseInt(document.getElementById('noMileage').value, 10) || null,
|
||||
workshop_name: document.getElementById('noWorkshopName').value.trim() || null,
|
||||
customer_address: document.getElementById('noCustomerAddress').value.trim() || null,
|
||||
customer_phone: document.getElementById('noCustomerPhone').value.trim() || null,
|
||||
vehicle_description: document.getElementById('noVehicleDescription').value.trim() || null,
|
||||
reception_notes: document.getElementById('noNotes').value,
|
||||
delivery_method: delivery || null,
|
||||
courier_id: delivery === 'courier' ? (parseInt(document.getElementById('noCourier').value, 10) || null) : null,
|
||||
is_direct: document.getElementById('noDirect').checked
|
||||
courier_id: (delivery === 'delivery' || delivery === 'courier') ? (parseInt(document.getElementById('noCourier').value, 10) || null) : null,
|
||||
employee_id: mechanicVal ? parseInt(mechanicVal, 10) : null,
|
||||
mechanic_name: document.getElementById('noMechanicName').value.trim() || null,
|
||||
estimated_cost: estimatedCostVal ? parseFloat(estimatedCostVal) : null,
|
||||
is_direct: document.getElementById('noDirect').checked,
|
||||
requires_invoice: document.getElementById('noRequiresInvoice').checked
|
||||
};
|
||||
api('POST', '', payload).then(function() {
|
||||
closeNewOrderModal();
|
||||
@@ -1168,18 +1351,13 @@ var Workshop = (function() {
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) { customers = (d.data || d.customers || []); })
|
||||
.catch(function() {});
|
||||
// Vehicles
|
||||
fetch('/pos/api/fleet/vehicles?per_page=500', {headers: headers()})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) { vehicles = (d.data || []); })
|
||||
.catch(function() { vehicles = []; });
|
||||
// Employees
|
||||
fetch('/pos/api/config/employees?per_page=500', {headers: headers()})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) { employees = (d.data || d.employees || []); })
|
||||
.catch(function() { employees = []; });
|
||||
// Couriers
|
||||
fetch('/pos/api/couriers?per_page=500', {headers: headers()})
|
||||
fetch('/pos/api/logistics/couriers?per_page=500', {headers: headers()})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
couriers = (d.data || d.couriers || []);
|
||||
@@ -1192,6 +1370,11 @@ var Workshop = (function() {
|
||||
.then(function(d) {
|
||||
branches = (d.data || []);
|
||||
populateBranchFilter();
|
||||
var noBranch = document.getElementById('noBranch');
|
||||
var userBranch = (window.POS_USER || {}).branch_id;
|
||||
if (noBranch && userBranch && branches.some(function(b) { return b.id == userBranch; })) {
|
||||
noBranch.value = userBranch;
|
||||
}
|
||||
})
|
||||
.catch(function() { branches = []; });
|
||||
}
|
||||
@@ -1235,11 +1418,14 @@ var Workshop = (function() {
|
||||
changeStatus: changeStatus,
|
||||
reserveItem: reserveItem,
|
||||
saveNotes: saveNotes,
|
||||
saveMechanicName: saveMechanicName,
|
||||
searchItemsForSO: searchItemsForSO,
|
||||
selectInventoryItem: selectInventoryItem,
|
||||
addSelectedItem: addSelectedItem,
|
||||
editItemInline: editItemInline,
|
||||
addLabor: addLabor,
|
||||
convertToSale: convertToSale,
|
||||
convertToRemission: convertToRemission,
|
||||
deleteOrder: deleteOrder,
|
||||
printOrder: printOrder,
|
||||
openNewOrderModal: openNewOrderModal,
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// The fetch handler normalizes static asset URLs (strips ?v= query strings)
|
||||
// so templates can use cache-busting query params freely.
|
||||
|
||||
const VERSION = 33;
|
||||
const VERSION = 40;
|
||||
const CACHE_NAME = 'nexus-pos-v' + VERSION;
|
||||
|
||||
const APP_SHELL = [
|
||||
|
||||
Reference in New Issue
Block a user