// ============================================================ // Elegant Work โ€” Roles & Permissions Manager // Rendered inside Settings page // ============================================================ const CF_MODULES = [ { key: 'dashboard', label: '๐Ÿ  Dashboard', actions: ['view'] }, { key: 'clients', label: '๐Ÿข Clients', actions: ['view', 'create', 'edit', 'delete'] }, { key: 'employees', label: '๐Ÿ‘ฅ Employees', actions: ['view', 'create', 'edit', 'delete', 'reports'] }, { key: 'projects', label: '๐Ÿ“ Projects', actions: ['view', 'create', 'edit', 'delete'] }, { key: 'jobcards', label: '๐Ÿ”ง Job Cards', actions: ['view', 'view_own', 'create', 'edit', 'delete', 'reports', 'view_costing'] }, { key: 'stock', label: '๐Ÿ“ฆ Stock Control', actions: ['view', 'create', 'edit', 'delete', 'reports'] }, { key: 'fleet', label: '๐Ÿš— Fleet', actions: ['view', 'create', 'edit', 'delete', 'reports'] }, { key: 'slips', label: '๐Ÿงพ Slip Manager', actions: ['view', 'create', 'edit', 'delete', 'reports'] }, { key: 'cashflow', label: '๐Ÿ’ฐ Cash Flow', actions: ['view', 'create', 'edit', 'delete', 'reports'] }, { key: 'checklists', label: 'โœ… Checklists', actions: ['view', 'create', 'edit', 'delete'] }, { key: 'calendar', label: '๐Ÿ“… Calendar', actions: ['view', 'create', 'edit', 'delete'] }, { key: 'payroll', label: '๐Ÿ’ผ Payroll', actions: ['view', 'create', 'edit', 'delete', 'reports'] }, { key: 'settings', label: 'โš™๏ธ Settings', actions: ['view', 'edit'] }, { key: 'users', label: '๐Ÿ‘ค Users', actions: ['view', 'create', 'edit', 'delete'] }, ]; const ACTION_LABELS = { view: 'View', create: 'Create', edit: 'Edit', delete: 'Delete', reports: 'Reports', view_own: 'Own Only', view_costing: 'Costing' }; const ACTION_COLORS = { view: { bg: '#eff6ff', color: '#1d4ed8' }, create: { bg: '#f0fdf4', color: '#15803d' }, edit: { bg: '#fefce8', color: '#92400e' }, delete: { bg: '#fff1f2', color: '#dc2626' }, reports: { bg: '#fdf4ff', color: '#7c3aed' }, view_own: { bg: '#fff7ed', color: '#c2410c' }, view_costing: { bg: '#ecfdf5', color: '#065f46' }, }; // โ”€โ”€ Main entry โ€” render roles table into a container โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ async function renderRolesSection(containerId) { const wrap = document.getElementById(containerId); if (!wrap) return; wrap.innerHTML = '
'; const res = await API.post('roles/list', {}); if (!res.success) { wrap.innerHTML = `

Failed to load roles.

`; return; } const roles = res.data.roles; const isAdmin = Auth.isAdmin(); wrap.innerHTML = `
${roles.map(r => ` `).join('')}
Role Description Active Users Type
${r.name} ${r.description || 'โ€”'} ${r.user_count} user${r.user_count != 1 ? 's' : ''} ${r.is_system ? '๐Ÿ”’ System' : 'Custom'} ${!r.is_system && isAdmin ? ` ` : ''}
`; } // โ”€โ”€ Add Role โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ function openAddRoleModal() { Modal.open({ id: 'role-modal', title: '+ New Role', body: `
`, footer: ` ` }); } function openEditRoleModal(id, name, description) { Modal.open({ id: 'role-modal', title: `โœ๏ธ Edit Role`, body: `
`, footer: ` ` }); } async function submitRole(id) { const btn = document.querySelector('#modal-role-modal .btn-primary'); if (btn) btn.classList.add('loading'); const data = getFormData('role-form'); if (!data.name) { Toast.show('Role name required.', 'error'); if (btn) btn.classList.remove('loading'); return; } const res = await API.post('roles/save', { action: id ? 'update' : 'create', id: id || undefined, ...data }); if (btn) btn.classList.remove('loading'); if (res.success) { Toast.show(id ? 'Role updated.' : 'Role created.', 'success'); Modal.close(); renderRolesSection('roles-table-wrap'); } else Toast.show(res.message, 'error'); } async function deleteRole(id, name, userCount) { if (userCount > 0) { Toast.show(`Cannot delete "${name}" โ€” ${userCount} user(s) assigned. Reassign them first.`, 'error'); return; } if (!confirm(`Delete role "${name}"? This cannot be undone.`)) return; const res = await API.post('roles/delete', { id }); if (res.success) { Toast.show('Role deleted.', 'success'); renderRolesSection('roles-table-wrap'); } else Toast.show(res.message, 'error'); } // โ”€โ”€ Permissions Modal โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ async function openPermissionsModal(roleId, roleName, isSystem) { Modal.open({ id: 'perms-modal', title: `๐Ÿ›ก๏ธ Permissions โ€” ${roleName}`, size: 'lg', body: `
`, footer: isSystem ? `
๐Ÿ”’ Admin permissions are fixed and cannot be changed.
` : ` ` }); const res = await API.post('roles/permissions', { action: 'get', role_id: roleId }); const body = document.getElementById('perms-body'); if (!body) return; if (!res.success) { body.innerHTML = `

Failed to load permissions.

`; return; } // Build a lookup: module.action => allowed const current = {}; (res.data.permissions || []).forEach(p => { current[`${p.module}.${p.action}`] = !!parseInt(p.allowed); }); body.innerHTML = `
Quick set:
${['view', 'view_own', 'create', 'edit', 'delete', 'reports', 'view_costing'].map(a => ` `).join('')} ${CF_MODULES.map((mod, i) => ` ${['view', 'view_own', 'create', 'edit', 'delete', 'reports', 'view_costing'].map(act => { const key = `${mod.key}.${act}`; const hasAction = mod.actions.includes(act); const checked = hasAction && (current[key] !== false) && (current[key] !== undefined ? current[key] : false); return ``; }).join('')} `).join('')}
Module ${ACTION_LABELS[a] || a}
${mod.label} ${hasAction ? `` : `โ€”`}

๐Ÿ’ก Disabling View hides the module from the sidebar entirely. Actions only apply if View is also enabled.

`; } function permsSelectAll(on) { document.querySelectorAll('#perms-body input[data-perm]').forEach(cb => { if (!cb.disabled) cb.checked = on; }); } function permsSelectViewOnly() { document.querySelectorAll('#perms-body input[data-perm]').forEach(cb => { if (cb.disabled) return; const isView = cb.dataset.perm.endsWith('.view'); cb.checked = isView; }); } function permsUpdateRow(moduleKey) { // If view is unchecked, uncheck all other actions for that module const viewCb = document.querySelector(`input[data-perm="${moduleKey}.view"]`); if (viewCb && !viewCb.checked) { ['create', 'edit', 'delete'].forEach(a => { const cb = document.querySelector(`input[data-perm="${moduleKey}.${a}"]`); if (cb) cb.checked = false; }); } } async function savePermissions(roleId) { const btn = document.querySelector('#modal-perms-modal .btn-primary'); if (btn) btn.classList.add('loading'); const matrix = {}; document.querySelectorAll('#perms-body input[data-perm]').forEach(cb => { matrix[cb.dataset.perm] = cb.checked; }); const res = await API.post('roles/permissions', { action: 'set', role_id: roleId, matrix: JSON.stringify(matrix) }); if (btn) btn.classList.remove('loading'); if (res.success) { Toast.show('Permissions saved. โœ…', 'success'); Modal.close(); // Refresh own permissions if editing current user's role const me = Auth.getCurrentUser(); if (me && parseInt(me.role_id) === roleId) { await Auth.loadPermissions(); Toast.show('Your permissions were updated โ€” refreshing navigation.', 'info'); Router.navigate('settings'); } } else Toast.show(res.message, 'error'); }