/**
* Gestão de utilizadores — painel direito do Controle de acesso
* Criar · Editar · Congelar · Copiar · Eliminar
*/
(function () {
'use strict';
const ROLE_META = [
{ value: 'super_admin', label: 'Super Admin', group: 'Ops', code: 'SU' },
{ value: 'ops_lead', label: 'Chefe Ops', group: 'Ops', code: 'CO' },
{ value: 'technician', label: 'Suporte', group: 'Ops', code: 'TEC' },
{ value: 'noc', label: 'NOC', group: 'Ops', code: 'NOC' },
{ value: 'sales_admin', label: 'Sales Admin', group: 'Comercial', code: 'SAD' },
{ value: 'sales_support', label: 'Sales Support', group: 'Comercial', code: 'SSU' },
{ value: 'finance', label: 'Financeiro', group: 'Negócio', code: 'FIN' },
{ value: 'marketing', label: 'Marketing', group: 'Negócio', code: 'MKT' },
{ value: 'seo', label: 'SEO', group: 'Negócio', code: 'SEO' },
{ value: 'developer', label: 'Developer', group: 'Plataforma', code: 'DEV' },
{ value: 'devops', label: 'DevOps', group: 'Plataforma', code: 'DVO' },
{ value: 'security_analyst', label: 'Segurança / SOC', group: 'Plataforma', code: 'SOC' },
{ value: 'content_editor', label: 'Conteúdo / CMS', group: 'Plataforma', code: 'CMS' },
{ value: 'agentic_operator', label: 'Operador Agentes IA', group: 'Plataforma', code: 'AIO' },
{ value: 'partner', label: 'Partner', group: 'Externo', code: 'PTR' },
];
const ASSIGNABLE = ROLE_META.filter((r) => r.value !== 'super_admin');
const PORTAL_ID = 'um-modal-portal';
let users = [];
let filterQ = '';
let filterStatus = 'all';
let modalMode = null;
let modalUser = null;
let createDefaultRole = 'technician';
let saving = false;
let msg = '';
let currentHost = null;
let currentOpts = null;
function esc(s) {
return String(s ?? '').replace(/&/g, '&').replace(//g, '>');
}
function roleMeta(roleId) {
return ROLE_META.find((r) => r.value === roleId) || { label: roleId, group: '—', code: '?' };
}
function groupForRole(roleId) {
return roleMeta(roleId).group;
}
function roleSelectGrouped(selected, includeSuper = false) {
const list = includeSuper ? ROLE_META : ASSIGNABLE;
const groups = [...new Set(list.map((r) => r.group))];
return groups.map((g) => {
const opts = list.filter((r) => r.group === g).map((r) =>
``
).join('');
return ``;
}).join('');
}
function filteredUsers(roleLock) {
const q = filterQ.trim().toLowerCase();
return users.filter((u) => {
if (roleLock && u.role !== roleLock && !(roleLock === 'super_admin' && u.username === 'root')) return false;
if (filterStatus === 'active' && !u.active) return false;
if (filterStatus === 'inactive' && u.active) return false;
if (!q) return true;
const hay = [u.username, u.email, u.display_name, roleMeta(u.role).label, roleMeta(u.role).group].join(' ').toLowerCase();
return hay.includes(q);
});
}
async function loadUsers() {
const r = await fetchWithTimeout('/api/v1/auth/users', { headers: authHeaders() });
if (!r.ok) throw new Error(`${r.status}`);
const data = await r.json();
users = data.users || [];
return users;
}
async function apiUser(path, opts = {}) {
const r = await fetchWithTimeout(`/api/v1/auth${path}`, {
...opts,
headers: authHeaders({ 'Content-Type': 'application/json', ...(opts.headers || {}) }),
});
if (!r.ok) {
const t = await r.text();
throw new Error(t.slice(0, 200) || String(r.status));
}
return r.json();
}
function renderModalHtml() {
if (!modalMode) return '';
const isCreate = modalMode === 'create';
const isClone = modalMode === 'clone';
const u = modalUser || {};
const title = isCreate ? 'Criar utilizador' : isClone ? 'Copiar utilizador' : 'Editar utilizador';
const defaultRole = isCreate ? createDefaultRole : (u.role || createDefaultRole);
return `
`;
}
function syncModalPortal() {
let portal = document.getElementById(PORTAL_ID);
if (!modalMode) {
if (portal) portal.innerHTML = '';
document.body.classList.remove('um-scroll-lock');
return;
}
if (!portal) {
portal = document.createElement('div');
portal.id = PORTAL_ID;
document.body.appendChild(portal);
}
portal.innerHTML = renderModalHtml();
document.body.classList.toggle('um-scroll-lock', true);
bindModalEvents(portal);
}
function renderTable(list) {
if (!list.length) {
return 'Nenhum utilizador nesta função. Use «Criar utilizador» para adicionar.
';
}
const rows = list.map((u) => {
const rm = roleMeta(u.role);
const isRoot = u.username === 'root';
return `
|
${esc(u.display_name || u.username)}
${esc(u.email || u.username)}
|
${esc(rm.code)} |
${u.active ? 'Activo' : 'Off'} |
${!isRoot ? `` : ''}
${!isRoot ? `` : ''}
|
`;
}).join('');
return `
| Utilizador | Perfil | Estado | Acções |
${rows}
`;
}
function renderContent(roleLock, roleMeta, embedded) {
const list = filteredUsers(roleLock);
const code = window.DeskAccessControlPanel?.ROLE_CODES?.[roleLock] || '';
const active = list.filter((u) => u.active).length;
return `
${list.length} filtrados
${active} activos
${users.length} total
${currentOpts?.onRegistration ? '' : ''}
${renderTable(list)}
`;
}
function refreshList() {
if (!currentHost) return;
const roleLock = currentOpts?.roleFilter || null;
const listEl = currentHost.querySelector('[data-um-list]');
if (listEl) listEl.innerHTML = renderTable(filteredUsers(roleLock));
bindRowEvents(currentHost);
}
let editCallback = null;
async function handleSubmit(form) {
const roleLock = currentOpts?.roleFilter || null;
const fd = new FormData(form);
saving = true;
msg = '';
syncModalPortal();
try {
if (modalMode === 'create') {
await apiUser('/users', {
method: 'POST',
body: JSON.stringify({
email: fd.get('email'),
password: fd.get('password'),
role: fd.get('role'),
display_name: fd.get('display_name') || null,
active: !!form.querySelector('[name="active"]')?.checked,
}),
});
} else if (modalMode === 'clone') {
const res = await apiUser(`/users/${encodeURIComponent(modalUser.username)}/clone`, {
method: 'POST',
body: JSON.stringify({
email: fd.get('email'),
password: fd.get('password') || null,
display_name: fd.get('display_name') || null,
active: !!form.querySelector('[name="active"]')?.checked,
}),
});
if (res.generated_password) {
window.alert(`Copiado.\nSenha gerada: ${res.generated_password}`);
}
} else {
const payload = {
display_name: fd.get('display_name') || null,
role: fd.get('role'),
active: !!form.querySelector('[name="active"]')?.checked,
};
const pwd = fd.get('password');
if (pwd && String(pwd).length >= 6) payload.password = pwd;
await apiUser(`/users/${encodeURIComponent(modalUser.username)}`, {
method: 'PATCH',
body: JSON.stringify(payload),
});
}
modalMode = null;
modalUser = null;
msg = '';
syncModalPortal();
await loadUsers();
refreshList();
if (editCallback) { const cb = editCallback; editCallback = null; cb(); }
} catch (e) {
msg = e.message;
syncModalPortal();
} finally {
saving = false;
}
}
function closeModal() {
modalMode = null;
modalUser = null;
msg = '';
saving = false;
syncModalPortal();
}
function bindModalEvents(portal) {
portal.querySelector('.um-backdrop')?.addEventListener('click', (e) => {
if (e.target !== e.currentTarget) return;
closeModal();
});
portal.querySelectorAll('[data-um-close]').forEach((el) => {
el.addEventListener('click', (e) => { e.stopPropagation(); closeModal(); });
});
portal.querySelector('[data-um-card]')?.addEventListener('mousedown', (e) => e.stopPropagation());
portal.querySelector('[data-um-form]')?.addEventListener('submit', async (e) => {
e.preventDefault();
await handleSubmit(e.target);
});
portal.querySelector('[data-um-role-select]')?.addEventListener('change', (e) => {
const preview = portal.querySelector('[data-um-group-preview]');
if (preview) preview.textContent = groupForRole(e.target.value);
});
}
function bindRowEvents(host) {
host.querySelectorAll('[data-um-edit]').forEach((btn) => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
modalUser = users.find((u) => u.username === btn.dataset.umEdit);
modalMode = 'edit';
msg = '';
syncModalPortal();
});
});
host.querySelectorAll('[data-um-clone]').forEach((btn) => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
modalUser = users.find((u) => u.username === btn.dataset.umClone);
modalMode = 'clone';
msg = '';
syncModalPortal();
});
});
host.querySelectorAll('[data-um-freeze]').forEach((btn) => {
btn.addEventListener('click', async (e) => {
e.stopPropagation();
const u = users.find((x) => x.username === btn.dataset.umFreeze);
if (!u) return;
const label = u.active ? 'Congelar' : 'Activar';
if (!window.confirm(`${label} ${u.username}?`)) return;
try {
await apiUser(`/users/${encodeURIComponent(u.username)}`, {
method: 'PATCH',
body: JSON.stringify({ active: !u.active }),
});
await loadUsers();
refreshList();
} catch (err) {
window.alert(err.message);
}
});
});
host.querySelectorAll('[data-um-delete]').forEach((btn) => {
btn.addEventListener('click', async (e) => {
e.stopPropagation();
const username = btn.dataset.umDelete;
if (!window.confirm(`Eliminar ${username}? Irreversível.`)) return;
try {
await apiUser(`/users/${encodeURIComponent(username)}`, { method: 'DELETE' });
await loadUsers();
refreshList();
} catch (err) {
window.alert(err.message);
}
});
});
}
function bindEvents(host) {
host.querySelector('[data-um-create]')?.addEventListener('click', () => {
modalMode = 'create';
modalUser = null;
msg = '';
syncModalPortal();
});
host.querySelector('[data-um-reg]')?.addEventListener('click', () => {
if (typeof currentOpts?.onRegistration === 'function') currentOpts.onRegistration();
});
host.querySelector('[data-um-search]')?.addEventListener('input', (e) => {
filterQ = e.target.value;
clearTimeout(host._umTimer);
host._umTimer = setTimeout(refreshList, 250);
});
host.querySelector('[data-um-filter-status]')?.addEventListener('change', (e) => {
filterStatus = e.target.value;
refreshList();
});
bindRowEvents(host);
}
async function paint(host, opts = {}) {
if (!host) return;
currentHost = host;
currentOpts = opts;
createDefaultRole = opts.roleFilter || 'technician';
if (typeof canManageUsers === 'function' && !canManageUsers()) {
host.innerHTML = 'Sem permissão.
';
return;
}
const firstLoad = !users.length;
if (firstLoad) {
host.innerHTML = 'Carregando utilizadores…
';
try {
await loadUsers();
} catch (e) {
host.innerHTML = `Erro: ${esc(e.message)}
`;
return;
}
}
host.innerHTML = renderContent(opts.roleFilter, opts.roleMeta, opts.embedded);
bindEvents(host);
syncModalPortal();
}
function reset() {
users = [];
filterQ = '';
filterStatus = 'all';
modalMode = null;
modalUser = null;
msg = '';
currentHost = null;
currentOpts = null;
closeModal();
}
function openEdit(username, onSaved) {
loadUsers().then(() => {
modalUser = users.find((u) => u.username === username);
if (!modalUser) return;
modalMode = 'edit';
msg = '';
editCallback = onSaved || null;
syncModalPortal();
}).catch((e) => window.alert(e.message));
}
window.DeskUserManagement = { paint, reset, loadUsers, openEdit, ROLE_META };
})();