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:
@@ -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
|
||||
};
|
||||
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user