${esc(b.value)}
(function () { 'use strict'; const esc = (s) => String(s ?? '').replace(/&/g, '&').replace(//g, '>'); const LEVEL_SHORT = { full: 'ADM', read: 'READ', link: 'LINK', api: 'API', system: 'SYS', none: '—', }; const state = { data: null, tab: 'overview', selectedRole: 'super_admin', softwareFilter: 'all', saving: false, saveError: null, moduleDraft: null, bindingDraft: null, }; async function crudApi(method, path, body) { const opts = { method, headers: authHeaders({ 'Content-Type': 'application/json' }), }; if (body !== undefined) opts.body = JSON.stringify(body); const r = await fetchWithTimeout(`/api/v1${path}`, opts); if (!r.ok) throw new Error(`${r.status} ${(await r.text()).slice(0, 200)}`); if (method === 'DELETE' || r.status === 204) return {}; const ct = r.headers.get('content-type') || ''; if (ct.includes('json')) return r.json(); return r.text(); } async function reloadMatrix() { state.data = await api('/rbac/matrix'); state.moduleDraft = null; state.bindingDraft = null; } function selectedRoleMeta() { return state.data?.catalog?.roles?.[state.selectedRole] || {}; } function canCrud() { return !!state.data?.crud_enabled; } function isRoleLocked() { return !!selectedRoleMeta().locked; } async function handleNewRole() { const id = window.prompt('ID da função (slug, ex.: billing_analyst):'); if (!id) return; const label = window.prompt('Nome exibido (pt-BR):', id.replace(/_/g, ' ')); if (!label) return; state.saving = true; state.saveError = null; try { await crudApi('POST', '/rbac/roles', { id: id.trim().toLowerCase(), label: label.trim(), category: 'custom', description: '', }); await reloadMatrix(); state.selectedRole = id.trim().toLowerCase(); const root = document.getElementById('access-matrix-content'); if (root) paint(root); } catch (e) { state.saveError = e.message; const root = document.getElementById('access-matrix-content'); if (root) paint(root); } finally { state.saving = false; } } async function handleCloneRole() { const newId = window.prompt(`Copiar ${state.selectedRole} — novo ID (slug):`); if (!newId) return; const newLabel = window.prompt('Nome da cópia:', `${selectedRoleMeta().label || state.selectedRole} (cópia)`); if (!newLabel) return; state.saving = true; try { await crudApi('POST', `/rbac/roles/${encodeURIComponent(state.selectedRole)}/clone`, { new_id: newId.trim().toLowerCase(), new_label: newLabel.trim(), }); await reloadMatrix(); state.selectedRole = newId.trim().toLowerCase(); const root = document.getElementById('access-matrix-content'); if (root) paint(root); } catch (e) { state.saveError = e.message; const root = document.getElementById('access-matrix-content'); if (root) paint(root); } finally { state.saving = false; } } async function handleFreezeRole() { const meta = selectedRoleMeta(); const next = meta.status === 'frozen' ? 'active' : 'frozen'; const msg = next === 'frozen' ? 'Pausar esta função? Novas atribuições serão bloqueadas.' : 'Reactivar função?'; if (!window.confirm(msg)) return; state.saving = true; try { await crudApi('PATCH', `/rbac/roles/${encodeURIComponent(state.selectedRole)}/status`, { status: next }); await reloadMatrix(); const root = document.getElementById('access-matrix-content'); if (root) paint(root); } catch (e) { state.saveError = e.message; const root = document.getElementById('access-matrix-content'); if (root) paint(root); } finally { state.saving = false; } } async function handleDeleteRole() { if (!window.confirm(`Arquivar função ${state.selectedRole}? Falha se houver utilizadores activos.`)) return; state.saving = true; try { await crudApi('DELETE', `/rbac/roles/${encodeURIComponent(state.selectedRole)}`); await reloadMatrix(); state.selectedRole = state.data.role_columns?.[0] || 'super_admin'; const root = document.getElementById('access-matrix-content'); if (root) paint(root); } catch (e) { state.saveError = e.message; const root = document.getElementById('access-matrix-content'); if (root) paint(root); } finally { state.saving = false; } } function handleReportDownload() { const token = localStorage.getItem('desk_token') || sessionStorage.getItem('desk_token'); const url = `/api/v1/rbac/roles/${encodeURIComponent(state.selectedRole)}/report.csv`; fetch(url, { headers: authHeaders() }) .then((r) => { if (!r.ok) throw new Error(String(r.status)); return r.blob(); }) .then((blob) => { const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = `matriz-${state.selectedRole}.csv`; a.click(); }) .catch((e) => { state.saveError = e.message; paint(document.getElementById('access-matrix-content')); }); } async function saveModuleDraft() { if (!state.moduleDraft) return; state.saving = true; try { await crudApi('PUT', `/rbac/roles/${encodeURIComponent(state.selectedRole)}/modules`, { modules: state.moduleDraft, }); await reloadMatrix(); const root = document.getElementById('access-matrix-content'); if (root) paint(root); } catch (e) { state.saveError = e.message; const root = document.getElementById('access-matrix-content'); if (root) paint(root); } finally { state.saving = false; } } async function saveBindingDraft() { if (!state.bindingDraft) return; state.saving = true; try { await crudApi('PUT', `/rbac/roles/${encodeURIComponent(state.selectedRole)}/bindings`, { bindings: state.bindingDraft, }); await reloadMatrix(); const root = document.getElementById('access-matrix-content'); if (root) paint(root); } catch (e) { state.saveError = e.message; const root = document.getElementById('access-matrix-content'); if (root) paint(root); } finally { state.saving = false; } } function renderToolbar() { if (!canCrud()) return ''; const meta = selectedRoleMeta(); const frozen = meta.status === 'frozen'; return `
`; } async function patchApi(path, body) { const r = await fetchWithTimeout(`/api/v1${path}`, { method: 'PATCH', headers: authHeaders({ 'Content-Type': 'application/json' }), body: JSON.stringify(body), }); if (!r.ok) throw new Error(`${r.status} ${(await r.text()).slice(0, 200)}`); return r.json(); } function bindingMeta(agentId, roleId, relation) { const b = state.data?.agent_bindings_matrix?.agents?.[agentId]?.[roleId]?.[relation]; return { enabled: !!b?.enabled, locked: !!b?.locked, }; } function capMeta(capId, roleId) { const c = state.data?.agent_bindings_matrix?.caps?.[capId]?.[roleId]; return { enabled: !!c?.enabled, locked: !!c?.locked }; } async function toggleBinding(agentId, roleId, relation) { const meta = bindingMeta(agentId, roleId, relation); if (meta.locked) return; state.saving = true; state.saveError = null; try { await patchApi('/rbac/agent-bindings', { agent_id: agentId, role_id: roleId, relation, enabled: !meta.enabled, }); state.data = await api('/rbac/matrix'); const root = document.getElementById('access-matrix-content'); if (root) paint(root); } catch (e) { state.saveError = e.message; const root = document.getElementById('access-matrix-content'); if (root) paint(root); } finally { state.saving = false; } } async function toggleCap(capId, roleId) { const meta = capMeta(capId, roleId); if (meta.locked) return; state.saving = true; state.saveError = null; try { await patchApi('/rbac/agent-governance', { cap_id: capId, role_id: roleId, enabled: !meta.enabled, }); state.data = await api('/rbac/matrix'); const root = document.getElementById('access-matrix-content'); if (root) paint(root); } catch (e) { state.saveError = e.message; const root = document.getElementById('access-matrix-content'); if (root) paint(root); } finally { state.saving = false; } } async function api(path) { const r = await fetchWithTimeout(`/api/v1${path}`, { headers: authHeaders({ 'Content-Type': 'application/json' }), }); if (!r.ok) throw new Error(`${r.status} ${(await r.text()).slice(0, 200)}`); return r.json(); } function levelMeta(level) { return state.data.legend[level] || state.data.legend.none; } function badgeHtml(level, opts = {}) { const meta = levelMeta(level); const short = LEVEL_SHORT[meta.access] || '—'; const cls = opts.compact ? ' am-badge--compact' : ''; return ``; } function serviceLabel(svc) { return state.data.service_labels?.[svc] || svc; } function rolesByCategory(catalog, categoryLabels) { const groups = {}; Object.values(catalog.roles || {}).forEach((role) => { const cat = role.category || 'platform'; if (!groups[cat]) groups[cat] = []; groups[cat].push(role); }); const order = ['ops', 'commercial', 'business', 'platform', 'custom', 'system']; return order .filter((c) => groups[c]?.length) .map((c) => ({ id: c, label: categoryLabels[c] || c, roles: groups[c] })); } function roleDeskRows() { const roleId = state.selectedRole; return (state.data.desk_modules || []).map((row) => ({ ...row, level: row.levels[roleId] || 'none', })).filter((r) => r.level !== 'none'); } function renderRoleSidebar() { const groups = rolesByCategory(state.data.catalog, state.data.category_labels || {}); return groups.map((g) => `${esc(row[idKey] || '')}`;
return `| ${esc(rowLabel)} | ${head}
|---|
${esc(row.id)}Editor de módulos Desk — função ${esc(roleLabel(state.selectedRole))}
| Módulo | Acesso |
|---|
Grelha completa (todas as funções)
${renderMatrixTable(state.data.desk_modules || [], 'Módulo Desk', 'id')}`; } return `Grelha completa — módulos internos do Desk (VM122).
${renderMatrixTable(state.data.desk_modules || [], 'Módulo Desk', 'id')}`; } function renderBindingCards(bindings, title) { if (!bindings.length) { return `${title ? esc(title) + ' — ' : ''}sem registos para esta função.
`; } const byService = {}; bindings.forEach((b) => { if (!byService[b.service]) byService[b.service] = []; byService[b.service].push(b); }); return ` ${title ? `${esc(b.value)}
${esc(title || 'Software')} — sem acesso directo.
`; } return ` ${title ? `${esc(r.group)}
${r.host && r.host !== '—' ? `${esc(r.host)}` : ''}
Sem módulos Desk activos para esta função.
'; } return `Matriz de software fora do Desk — VM112 (onboard/mail), VM123 (finance/hosting) e consolas de infra.
${renderSoftwareCards(sw, 'Resumo da função seleccionada')} ${filterBar}${sections}`; } function renderBindings() { const role = state.data.catalog.roles[state.selectedRole]; if (!role) return 'Função não encontrada
'; let editor = ''; if (canCrud() && !isRoleLocked()) { if (!state.bindingDraft) { state.bindingDraft = (role.bindings || []).map((b) => ({ ...b })); } const rows = state.bindingDraft.map((b, i) => `| Serviço | Tipo | Valor | Acesso |
|---|
${esc(g.id)} — ${esc(g.label)} Bindings estilo Odoo — grupos, roles e permissões provisionados por função.
${renderBindingCards(role.bindings || [], '')} ${groups ? `Sem alterações registadas ainda.
'; return `${esc(e.entity_id)}Esta função não acede ao módulo Agentics.
'; } return `${esc(title || 'Agentes')} — sem interacção directa.
`; } return ` ${title ? `${esc(a.role)}
${esc(a.role)}
${hit ? `| Agente | ${head}
|---|
Agentes A0–A7 usam conta agent_system.
${state.data.editable
? 'Clique UI / OP / AP para ligar ou desligar atribuições (audit activo).'
: 'Três relações: UI · Operador · Aprova.'}
${esc(state.saveError)}
` : ''} ${(state.data.editable || caps.length) ? `${esc(role?.description || '')}
${activeTabHint() ? `${esc(activeTabHint())}
` : ''}${esc(state.saveError)}
` : ''}Carregando Matriz de Acesso…
'; try { state.data = await api('/rbac/matrix'); if (!state.selectedRole && state.data.role_columns?.length) { state.selectedRole = state.data.role_columns[0]; } const tabIds = (state.data.tabs || []).map((t) => t.id); if (typeof window.getDeskState === 'function') { const ds = window.getDeskState(); if (ds?.matrixTab && tabIds.includes(ds.matrixTab)) { state.tab = ds.matrixTab; } } if (!tabIds.includes(state.tab) && tabIds.length) state.tab = tabIds[0]; paint(root); } catch (e) { root.innerHTML = `Matriz indisponível: ${esc(e.message)}
`; } } window.renderAccessMatrix = renderAccessMatrix; window.DeskAccessMatrix = { renderAccessMatrix, getState: () => state, }; })();