Add workshop module toggle in Modules & Integrations settings
Some checks failed
CI / lint-and-test (3.11) (push) Has been cancelled
CI / lint-and-test (3.13) (push) Has been cancelled

This commit is contained in:
2026-07-16 08:13:56 +00:00
parent c90012ca37
commit 6d07eab5c0
7 changed files with 46 additions and 6 deletions

View File

@@ -1066,6 +1066,7 @@ def get_modules():
'marketplace': _bool('module_marketplace'),
'meli': _bool('module_meli'),
'catalog': _bool('module_catalog'),
'workshop': _bool('module_workshop'),
})
@@ -1082,6 +1083,7 @@ def update_modules():
'module_marketplace': 'true' if data.get('marketplace') else 'false',
'module_meli': 'true' if data.get('meli') else 'false',
'module_catalog': 'true' if data.get('catalog') else 'false',
'module_workshop': 'true' if data.get('workshop') else 'false',
}
for key, value in settings.items():
@@ -1098,6 +1100,8 @@ def update_modules():
'whatsapp': data.get('whatsapp'),
'marketplace': data.get('marketplace'),
'meli': data.get('meli'),
'catalog': data.get('catalog'),
'workshop': data.get('workshop'),
}})

View File

@@ -183,13 +183,20 @@
};
// ─── Page guard based on role + permissions ───
function moduleEnabled(key) {
try {
var modules = JSON.parse(localStorage.getItem('pos_modules') || '{}');
return modules[key] !== false;
} catch(e) { return true; }
}
function isPageAllowed(pagePath, userRole, userPerms) {
if (userRole === 'owner' || userRole === 'admin') return true;
// Restricted roles (workshop/mechanic/counter/cashier) see modules based on permissions.
if (['workshop', 'mechanic', 'counter', 'cashier'].indexOf(userRole) !== -1) {
var allowed = [];
if (userRole === 'workshop' || userRole === 'mechanic') {
if ((userRole === 'workshop' || userRole === 'mechanic') && moduleEnabled('workshop')) {
allowed = ['/pos/workshop'];
}
var permMap = {
@@ -208,6 +215,8 @@
};
for (var p in permMap) {
if (userPerms.indexOf(p) !== -1 && allowed.indexOf(permMap[p]) === -1) {
// Hide workshop if its module is disabled
if (permMap[p] === '/pos/workshop' && !moduleEnabled('workshop')) continue;
allowed.push(permMap[p]);
}
}
@@ -229,7 +238,9 @@
fallback = '/pos/dashboard';
} else if (['workshop', 'mechanic', 'counter', 'cashier'].indexOf(userRole) !== -1) {
var allowed = [];
if (userRole === 'workshop' || userRole === 'mechanic') allowed = ['/pos/workshop'];
if ((userRole === 'workshop' || userRole === 'mechanic') && moduleEnabled('workshop')) {
allowed = ['/pos/workshop'];
}
var permMap = {
'pos.sell': '/pos/sale',
'pos.view': '/pos/sale',
@@ -246,6 +257,7 @@
};
for (var p in permMap) {
if (userPerms.indexOf(p) !== -1 && allowed.indexOf(permMap[p]) === -1) {
if (permMap[p] === '/pos/workshop' && !moduleEnabled('workshop')) continue;
allowed.push(permMap[p]);
}
}

View File

@@ -956,10 +956,12 @@ const Config = (() => {
var cbMp = document.getElementById('cfg-module-marketplace');
var cbMeli = document.getElementById('cfg-module-meli');
var cbCat = document.getElementById('cfg-module-catalog');
var cbWork = document.getElementById('cfg-module-workshop');
if (cbWa) cbWa.checked = data.whatsapp !== false;
if (cbMp) cbMp.checked = data.marketplace !== false;
if (cbMeli) cbMeli.checked = data.meli !== false;
if (cbCat) cbCat.checked = data.catalog !== false;
if (cbWork) cbWork.checked = data.workshop !== false;
localStorage.setItem('pos_modules', JSON.stringify(data));
} catch (e) {
console.error('Config.loadModules:', e);
@@ -980,14 +982,16 @@ const Config = (() => {
var cbMp = document.getElementById('cfg-module-marketplace');
var cbMeli = document.getElementById('cfg-module-meli');
var cbCat = document.getElementById('cfg-module-catalog');
var cbWork = document.getElementById('cfg-module-workshop');
var cbCr = document.getElementById('cfg-module-counter-remission');
if (!cbWa && !cbMp && !cbMeli && !cbCat && !cbCr) return;
if (!cbWa && !cbMp && !cbMeli && !cbCat && !cbWork && !cbCr) return;
try {
var data = {
whatsapp: cbWa ? cbWa.checked : true,
marketplace: cbMp ? cbMp.checked : true,
meli: cbMeli ? cbMeli.checked : true,
catalog: cbCat ? cbCat.checked : true,
workshop: cbWork ? cbWork.checked : true,
};
var res = await fetch(API + '/modules', {
method: 'PUT',

View File

@@ -34,6 +34,7 @@ const POS = (() => {
let counterRemissionEnabled = false;
let allowZeroPriceSales = true;
let allowNegativeStock = false;
let workshopModuleEnabled = true;
let currentPerms = [];
let receiptConfig = {};
let couriers = [];
@@ -93,7 +94,7 @@ const POS = (() => {
}
if (!canDiscount) hide('[onclick="POS.applyDiscount()"]');
if (!canEditPrice) hide('[onclick="POS.modifyPrice()"]');
if (!canCreateWorkshopOrder) {
if (!canCreateWorkshopOrder || !workshopModuleEnabled) {
hide('[onclick="POS.createServiceOrder()"]');
hide('[title="Orden de servicio"]');
}
@@ -153,6 +154,14 @@ const POS = (() => {
allowZeroPriceSales = true;
allowNegativeStock = false;
}
// Module toggles (from app-init preloaded modules)
try {
var modules = window.POS_USER && window.POS_USER.modules;
if (!modules) modules = JSON.parse(localStorage.getItem('pos_modules') || '{}');
workshopModuleEnabled = modules.workshop !== false;
} catch (e) {
workshopModuleEnabled = true;
}
// Counter remission workflow applies only to the counter role.
canCreateRemission = counterRemissionEnabled && employeeRole === 'counter' && perms.includes('pos.remission');
@@ -1390,6 +1399,7 @@ const POS = (() => {
// ─── Service Order from POS ──────────
function createServiceOrder() {
if (!workshopModuleEnabled) { showToast('El módulo de taller no está habilitado'); return; }
if (!canCreateWorkshopOrder) { showToast('No tienes permiso para ordenes de taller'); return; }
if (cart.length === 0) { showToast('Carrito vacio'); return; }
const zeroItem = findZeroPriceItem();

View File

@@ -80,7 +80,7 @@ window.renderSidebar = function(modulesOverride) {
{ 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"/>' },
moduleEnabled('workshop') ? { 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"/>' } : null,
{ 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,
moduleEnabled('meli') ? { id: 'meli', name: 'MercadoLibre', href: '/pos/marketplace-external', icon: '<rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/>' } : null,

View File

@@ -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 = 45;
const VERSION = 46;
const CACHE_NAME = 'nexus-pos-v' + VERSION;
const APP_SHELL = [

View File

@@ -421,6 +421,16 @@
<span class="toggle__slider"></span>
</label>
</div>
<div class="toggle-row">
<div class="toggle-row__info">
<span class="toggle-row__label">Taller / Servicio</span>
<span class="toggle-row__desc">Mostrar el módulo de órdenes de servicio y taller</span>
</div>
<label class="toggle">
<input type="checkbox" id="cfg-module-workshop" checked />
<span class="toggle__slider"></span>
</label>
</div>
<div class="toggle-row">
<div class="toggle-row__info">
<span class="toggle-row__label">Notas de remisión en mostrador</span>