/**
* UserWizard — criar utilizador
* Spec 040 · DS-FE-003 · mockup Roger · POST /api/v1/governance/users/wizard
*/
(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 MODULES = [
{ id: 'desk', label: 'Desk' },
{ id: 'openpanel', label: 'OpenPanel' },
{ id: 'billing', label: 'Billing' },
{ id: 'api', label: 'API' },
{ id: 'security', label: 'Security' },
{ id: 'ai_agents', label: 'AI Agents' },
];
const LEVELS = [
{ value: 'full', label: 'Total' },
{ value: 'partial', label: 'Parcial' },
{ value: 'read', label: 'Leitura' },
{ value: 'none', label: 'Sem acesso' },
];
const MODULE_TREE = [
{ id: 'desk', label: 'Desk (Help Desk)', actions: ['Criar / editar / remover utilizadores Desk', 'Gerir tickets e filas', 'Ver relatórios operacionais'] },
{ id: 'openpanel', label: 'OpenPanel (Plataforma)', actions: ['Provisionar sites', 'Gerir DNS e SSL'] },
{ id: 'billing', label: 'Faturação & Billing', actions: ['Ver facturas', 'Validar estado billing'] },
{ id: 'ai_agents', label: 'Agentes IA', actions: ['Executar runbooks', 'Aprovar remediação A7'] },
{ id: 'security', label: 'Segurança / SOC', actions: ['Ver incidentes', 'Reset 2FA utilizadores'] },
{ id: 'api', label: 'API & Integrações', actions: ['Gerir tokens API', 'Webhooks inbound'] },
];
const SECONDARY_GROUP_OPTIONS = ['Financeiro', 'Suporte Nível 2', 'Ops', 'Comercial', 'Negócio', 'Plataforma', 'Externo'];
const GROUP_HINTS = {
Ops: 'Operações internas — NOC, suporte e administradores.',
Comercial: 'Equipa comercial, vendas e pós-venda.',
Negócio: 'Financeiro, marketing e conteúdo.',
Plataforma: 'Dev, DevOps, segurança e agentes IA.',
Externo: 'Parceiros e acessos limitados.',
};
const PERM_TABS = [
{ id: 'modules', label: 'Módulos e funções' },
{ id: 'special', label: 'Recursos especiais' },
{ id: 'restrictions', label: 'Restrições' },
{ id: 'data', label: 'Acesso a dados' },
];
const STEPS = ['Dados do utilizador', 'Perfil e grupo', 'Permissões', 'Revisão'];
const STATUS_OPTIONS = [
{ value: 'active', label: 'Ativo' },
{ value: 'pending', label: 'Inativo' },
{ value: 'frozen', label: 'Congelado' },
{ value: 'invited', label: 'Desactivado' },
];
const SVG = {
userPlus: ' ',
close: ' ',
eye: ' ',
whatsapp: ' ',
info: ' ',
calendar: ' ',
shield: ' ',
bell: ' ',
key: ' ',
arrow: ' ',
chevron: ' ',
};
let open = false;
let step = 0;
let saving = false;
let result = null;
let onDone = null;
let defaultRole = 'technician';
let permTab = 'modules';
let permSearch = '';
let expandedModules = new Set(['desk']);
let confirmOpen = false;
let groupHintOpen = false;
const form = {
display_name: '',
email: '',
phone: '',
document: '',
birthdate: '',
password: '',
password_confirm: '',
account_status: 'active',
force_password_change: true,
mfa_required: true,
notifications_enabled: true,
api_access: false,
role: 'technician',
main_group: 'Ops',
secondary_groups: [],
module_permissions: {},
notes: '',
send_invite_email: true,
activate_account: true,
};
function esc(s) {
return String(s ?? '').replace(/&/g, '&').replace(//g, '>');
}
function roleMeta(id) {
return ROLE_META.find((r) => r.value === id) || { label: id, group: '—', code: '?' };
}
function initials(name) {
const parts = String(name || '').trim().split(/\s+/).filter(Boolean);
if (!parts.length) return '—';
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
}
function statusLabel(value) {
return STATUS_OPTIONS.find((s) => s.value === value)?.label || value;
}
function statusBadgeClass(value) {
if (value === 'active') return 'lb-badge lb-badge--active';
if (value === 'frozen') return 'lb-badge lb-badge--frozen';
return 'lb-badge lb-badge--warn';
}
function defaultPerms(role) {
const p = {};
MODULES.forEach((m) => { p[m.id] = 'none'; });
if (role === 'super_admin') MODULES.forEach((m) => { p[m.id] = 'full'; });
else if (role === 'ops_lead' || role === 'devops') {
Object.assign(p, { desk: 'full', openpanel: 'partial', security: 'partial', api: 'read' });
} else if (role === 'technician') Object.assign(p, { desk: 'partial', openpanel: 'read' });
else if (role === 'finance') Object.assign(p, { billing: 'full', desk: 'read' });
else if (role === 'agentic_operator') Object.assign(p, { ai_agents: 'full', desk: 'partial' });
return p;
}
function permStats() {
const perms = form.module_permissions || {};
const allowed = MODULES.filter((m) => perms[m.id] && perms[m.id] !== 'none');
const fullCount = allowed.filter((m) => perms[m.id] === 'full').length;
const partialCount = allowed.filter((m) => perms[m.id] === 'partial').length;
const readCount = allowed.filter((m) => perms[m.id] === 'read').length;
const functionsTotal = 32;
const functionsReleased = Math.min(functionsTotal, allowed.length * 4 + fullCount * 2 + partialCount + readCount);
return {
modules: allowed.length,
functions: `${functionsReleased} de ${functionsTotal}`,
special: String(fullCount + partialCount).padStart(2, '0'),
};
}
function resetForm(role) {
form.display_name = '';
form.email = '';
form.phone = '';
form.document = '';
form.birthdate = '';
form.password = '';
form.password_confirm = '';
form.account_status = 'active';
form.force_password_change = true;
form.mfa_required = true;
form.notifications_enabled = true;
form.api_access = false;
form.role = role || defaultRole;
form.main_group = roleMeta(form.role).group;
form.secondary_groups = [];
form.module_permissions = defaultPerms(form.role);
form.notes = '';
form.send_invite_email = true;
form.activate_account = true;
step = 0;
result = null;
permTab = 'modules';
permSearch = '';
expandedModules = new Set(['desk']);
confirmOpen = false;
groupHintOpen = false;
}
function levelLabel(value) {
return LEVELS.find((l) => l.value === value)?.label || value;
}
function profileSummaryHtml() {
const stats = permStats();
return `
Permissões por perfil (resumo)
Módulos permitidos ${stats.modules} módulos
Funções libertadas ${stats.functions}
Recursos especiais ${stats.special} habilitados
Restrições Nenhuma
${SVG.info}
Perfil define o que o utilizador pode fazer. Grupo define onde tem acesso.
`;
}
function toggleRow(name, title, desc, checked) {
return `
${SVG[name === 'mfa_required' ? 'shield' : name === 'notifications_enabled' ? 'bell' : 'key']}
${esc(title)}
${esc(desc)}
`;
}
function summaryHtml() {
const rm = roleMeta(form.role);
const stats = permStats();
const name = form.display_name.trim() || '—';
const email = form.email.trim() || '—';
return `
Resumo do utilizador
${esc(initials(form.display_name))}
${esc(name)}
${esc(email)}
${esc(statusLabel(form.account_status))}
Perfil ${esc(rm.label)}
Grupo ${esc(form.main_group)}
Estado ${esc(statusLabel(form.account_status))}
2FA ${form.mfa_required ? 'Activado' : 'Opcional'}
Acesso API ${form.api_access ? 'Permitido' : 'Não permitido'}
Notificações ${form.notifications_enabled ? 'Activas' : 'Desactivadas'}
Permissões iniciais (resumo)
Módulos permitidos ${stats.modules}
Funções libertadas ${stats.functions}
Recursos especiais ${stats.special} habilitados
${SVG.info}
Após a criação, o perfil e as permissões podem ser ajustados na gestão de utilizadores.
`;
}
function stepBasic() {
const statusChips = STATUS_OPTIONS.map((s) =>
`${esc(s.label)} `
).join('');
return `
${summaryHtml()}`;
}
function stepProfile() {
const groups = [...new Set(ROLE_META.map((r) => r.group))];
const opts = ROLE_META.filter((r) => r.value !== 'super_admin' || form.role === 'super_admin')
.map((r) => `${esc(r.label)} (${r.code}) `).join('');
const addable = SECONDARY_GROUP_OPTIONS.filter((g) => !form.secondary_groups.includes(g));
const tags = form.secondary_groups.map((g) =>
`${esc(g)}× `
).join('') || 'Nenhum grupo adicional ';
const groupHint = GROUP_HINTS[form.main_group] || 'Grupo organizacional do utilizador.';
return `
${profileSummaryHtml()}`;
}
function filteredModules() {
const q = permSearch.trim().toLowerCase();
if (!q) return MODULE_TREE;
return MODULE_TREE.filter((m) =>
m.label.toLowerCase().includes(q) || m.actions.some((a) => a.toLowerCase().includes(q))
);
}
function moduleLevelSelect(moduleId) {
const lv = form.module_permissions[moduleId] || 'none';
const opts = LEVELS.map((l) =>
`${l.label} `).join('');
return `${opts} `;
}
function stepPermissions() {
const tabs = PERM_TABS.map((t) =>
`${esc(t.label)} `
).join('');
let tabBody = '';
if (permTab === 'modules') {
const modules = filteredModules();
tabBody = `
Expandir todos
${modules.map((m) => {
const isOpen = expandedModules.has(m.id);
const actions = m.actions.map((label, i) => `
${esc(label)}
${i === 0 ? moduleLevelSelect(m.id) : `${esc(levelLabel(form.module_permissions[m.id] || 'none'))} `}
`).join('');
return `
${SVG.chevron}
${esc(m.label)}
${m.actions.length} funções
${actions}
`;
}).join('')}
`;
} else if (permTab === 'special') {
tabBody = `Recursos especiais — ${permStats().special} habilitados pelo perfil ${esc(roleMeta(form.role).label)} .
`;
} else if (permTab === 'restrictions') {
tabBody = `Nenhuma restrição adicional configurada para este utilizador.
`;
} else {
tabBody = `Acesso a dados sensíveis herda do perfil e grupo principal.
`;
}
return `
${profileSummaryHtml()}`;
}
function stepReview() {
const secGroups = form.secondary_groups.length ? form.secondary_groups.join(', ') : '—';
const stats = permStats();
return `
`;
}
function stepSuccess() {
const email = result?.user?.email || form.email;
const invite = result?.invite_link || '';
return `
✓
Utilizador criado com sucesso!
${form.send_invite_email ? `E-mail de convite enviado para ${esc(email)}` : `Conta criada: ${esc(email)}`}
ID interno · ${esc(result?.internal_id || '')}
Ver detalhes do utilizador
Criar outro utilizador
Voltar para a lista
${invite ? `Copiar link convite ` : ''}
${form.send_invite_email ? `
ligbox
Olá ${esc(form.display_name)} ,
Foi criada a sua conta no Ligbox Ops Desk. Clique abaixo para definir a sua senha no primeiro acesso.
Definir minha senha
Link válido por 24 horas · ${esc(email)}
` : ''}
`;
}
function confirmModalHtml() {
return `
Confirmar criação do utilizador
O que acontece ao clicar «Criar utilizador»:
Conta criada na base Desk
${form.send_invite_email ? 'E-mail de convite enviado' : 'Convite por e-mail não enviado'}
Utilizador define senha no 1.º acesso${form.force_password_change ? ' (obrigatório)' : ''}
Permissões RBAC aplicadas conforme revisão
Registo em audit log
Cancelar
${saving ? 'A criar…' : 'Criar utilizador'}
`;
}
function renderSteps() {
if (step >= 4) return '';
return STEPS.map((label, i) => {
const cls = i === step ? 'active' : i < step ? 'done' : '';
return `${i + 1} ${esc(label)}
`;
}).join('');
}
function renderBody() {
if (step === 0) return stepBasic();
if (step === 1) return stepProfile();
if (step === 2) return stepPermissions();
if (step === 3) return stepReview();
return stepSuccess();
}
function syncDom() {
const root = document.getElementById('lb-user-wizard-root');
if (!open) {
if (root) root.innerHTML = '';
document.body.classList.remove('um-scroll-lock');
return;
}
document.body.classList.add('um-scroll-lock');
const el = root || (() => {
const d = document.createElement('div');
d.id = 'lb-user-wizard-root';
document.body.appendChild(d);
return d;
})();
const isSuccess = step >= 4;
const isWide = step === 3 || isSuccess;
const nextLabel = step === 3 ? 'Criar utilizador' : 'Continuar';
el.innerHTML = `
${renderBody()}
${!isSuccess ? `
` : ''}
${confirmOpen ? confirmModalHtml() : ''}
`;
bindWizard(el);
}
function readForm(container) {
const g = (n) => container.querySelector(`[name="${n}"]`);
if (step === 0) {
form.display_name = g('display_name')?.value?.trim() || '';
form.email = g('email')?.value?.trim().toLowerCase() || '';
form.phone = g('phone')?.value?.trim() || '';
form.document = g('document')?.value?.trim() || '';
form.birthdate = g('birthdate')?.value?.trim() || '';
form.password = g('password')?.value || '';
form.password_confirm = g('password_confirm')?.value || '';
form.account_status = g('account_status')?.value || 'active';
form.force_password_change = !!container.querySelector('[name="force_password_change"]')?.checked;
form.mfa_required = !!container.querySelector('[name="mfa_required"]')?.checked;
form.notifications_enabled = !!container.querySelector('[name="notifications_enabled"]')?.checked;
form.api_access = !!container.querySelector('[name="api_access"]')?.checked;
if (!form.display_name) throw new Error('Nome completo é obrigatório');
if (!form.email) throw new Error('E-mail é obrigatório');
if (form.password !== form.password_confirm) throw new Error('Senhas não coincidem');
} else if (step === 1) {
form.role = g('role')?.value || form.role;
form.main_group = g('main_group')?.value || roleMeta(form.role).group;
form.module_permissions = defaultPerms(form.role);
} else if (step === 2) {
MODULES.forEach((m) => {
form.module_permissions[m.id] = g(`perm_${m.id}`)?.value || 'none';
});
} else if (step === 3) {
form.notes = g('notes')?.value?.trim() || '';
form.send_invite_email = !!container.querySelector('[name="send_invite_email"]')?.checked;
}
}
function parseApiError(text) {
try {
const data = JSON.parse(text);
if (Array.isArray(data.detail)) {
return data.detail.map((d) => d.msg || d.message || JSON.stringify(d)).join('\n');
}
if (typeof data.detail === 'string') return data.detail;
return data.message || text;
} catch (_) {
return text;
}
}
async function submitWizard() {
saving = true;
syncDom();
try {
const perms = {};
MODULES.forEach((m) => { perms[m.id] = form.module_permissions[m.id] || 'none'; });
const extraNotes = [form.document && `Doc: ${form.document}`, form.birthdate && `Nasc.: ${form.birthdate}`].filter(Boolean).join(' · ');
const notesRaw = [form.notes, extraNotes].filter(Boolean).join('\n') || null;
const notes = notesRaw ? notesRaw.slice(0, 200) : null;
if (form.password.length < 6) throw new Error('Senha inicial: mínimo 6 caracteres');
const r = await fetchWithTimeout('/api/v1/governance/users/wizard', {
method: 'POST',
headers: authHeaders({ 'Content-Type': 'application/json' }),
body: JSON.stringify({
display_name: form.display_name,
email: form.email,
phone: form.phone || null,
password: form.password,
account_status: form.account_status,
force_password_change: form.force_password_change,
mfa_required: form.mfa_required,
notifications_enabled: form.notifications_enabled,
api_access: form.api_access,
role: form.role,
main_group: form.main_group,
secondary_groups: form.secondary_groups,
module_permissions: perms,
notes,
send_invite_email: form.send_invite_email,
activate_account: form.account_status === 'active',
}),
});
if (!r.ok) throw new Error(parseApiError(await r.text()));
result = await r.json();
step = 4;
if (typeof onDone === 'function') onDone(result);
} catch (e) {
window.alert(e.message);
} finally {
saving = false;
syncDom();
}
}
function bindWizard(root) {
root.querySelector('[data-wz-close]')?.addEventListener('click', close);
root.querySelector('[data-wz-cancel]')?.addEventListener('click', close);
root.querySelector('.lb-wizard-root')?.addEventListener('click', (e) => {
if (e.target.classList.contains('lb-wizard-root')) close();
});
root.querySelectorAll('[data-toggle-pw]').forEach((btn) => {
btn.addEventListener('click', () => {
const field = root.querySelector(`[name="${btn.dataset.togglePw}"]`);
if (!field) return;
field.type = field.type === 'password' ? 'text' : 'password';
});
});
root.querySelectorAll('[data-status]').forEach((chip) => {
chip.addEventListener('click', () => {
form.account_status = chip.dataset.status;
syncDom();
});
});
root.querySelector('[data-wz-back]')?.addEventListener('click', () => {
if (step > 0) { step -= 1; syncDom(); }
});
root.querySelector('[data-wz-next]')?.addEventListener('click', async () => {
const body = root.querySelector('.lb-wizard-body');
try {
readForm(body);
} catch (e) {
window.alert(e.message);
return;
}
if (step === 3) {
confirmOpen = true;
syncDom();
return;
}
step += 1;
syncDom();
});
root.querySelector('[data-wz-confirm-cancel]')?.addEventListener('click', () => {
confirmOpen = false;
syncDom();
});
root.querySelector('[data-wz-confirm-overlay]')?.addEventListener('click', (e) => {
if (e.target.classList.contains('lw-confirm-overlay')) {
confirmOpen = false;
syncDom();
}
});
root.querySelector('[data-wz-confirm-ok]')?.addEventListener('click', async () => {
confirmOpen = false;
await submitWizard();
});
root.querySelector('[data-toggle-group-hint]')?.addEventListener('click', () => {
groupHintOpen = !groupHintOpen;
syncDom();
});
root.querySelector('[data-add-group]')?.addEventListener('change', (e) => {
const val = e.target.value;
if (val && !form.secondary_groups.includes(val)) {
form.secondary_groups.push(val);
syncDom();
}
});
root.querySelectorAll('[data-remove-group]').forEach((btn) => {
btn.addEventListener('click', () => {
form.secondary_groups = form.secondary_groups.filter((g) => g !== btn.dataset.removeGroup);
syncDom();
});
});
root.querySelectorAll('[data-perm-tab]').forEach((btn) => {
btn.addEventListener('click', () => {
permTab = btn.dataset.permTab;
syncDom();
});
});
root.querySelector('[data-perm-search]')?.addEventListener('input', (e) => {
permSearch = e.target.value;
clearTimeout(root._permTimer);
root._permTimer = setTimeout(() => syncDom(), 180);
});
root.querySelector('[data-expand-all]')?.addEventListener('click', () => {
MODULE_TREE.forEach((m) => expandedModules.add(m.id));
syncDom();
});
root.querySelectorAll('[data-toggle-module]').forEach((btn) => {
btn.addEventListener('click', () => {
const id = btn.dataset.toggleModule;
if (expandedModules.has(id)) expandedModules.delete(id);
else expandedModules.add(id);
syncDom();
});
});
root.querySelectorAll('.lw-module-level').forEach((sel) => {
sel.addEventListener('change', (e) => {
form.module_permissions[e.target.dataset.module] = e.target.value;
syncDom();
});
});
root.querySelector('[name="notes"]')?.addEventListener('input', (e) => {
form.notes = e.target.value.slice(0, 200);
const hint = root.querySelector('.lw-field-hint');
if (hint) hint.textContent = `${form.notes.length}/200 caracteres`;
});
root.querySelector('[name="role"]')?.addEventListener('change', (e) => {
form.role = e.target.value;
form.main_group = roleMeta(form.role).group;
form.module_permissions = defaultPerms(form.role);
syncDom();
});
['display_name', 'email', 'phone'].forEach((field) => {
root.querySelector(`[name="${field}"]`)?.addEventListener('input', (e) => {
form[field] = e.target.value;
const summary = root.querySelector('.lw-summary');
if (summary && step === 0) {
const nameEl = summary.querySelector('.lw-summary-name');
const emailEl = summary.querySelector('.lw-summary-email');
const avatarEl = summary.querySelector('.lw-summary-avatar');
if (nameEl) nameEl.textContent = form.display_name.trim() || '—';
if (emailEl) emailEl.textContent = form.email.trim() || '—';
if (avatarEl) avatarEl.textContent = initials(form.display_name);
}
});
});
root.querySelectorAll('.lw-toggle input[type="checkbox"]').forEach((el) => {
el.addEventListener('change', () => {
if (step !== 0) return;
const body = root.querySelector('.lb-wizard-body');
form.force_password_change = !!body.querySelector('[name="force_password_change"]')?.checked;
form.mfa_required = !!body.querySelector('[name="mfa_required"]')?.checked;
form.notifications_enabled = !!body.querySelector('[name="notifications_enabled"]')?.checked;
form.api_access = !!body.querySelector('[name="api_access"]')?.checked;
const old = root.querySelector('.lw-summary');
if (old) old.outerHTML = summaryHtml();
});
});
root.querySelector('[data-wz-another]')?.addEventListener('click', () => {
resetForm(defaultRole);
syncDom();
});
root.querySelector('[data-wz-goto-mgmt]')?.addEventListener('click', () => {
close();
if (typeof setView === 'function') {
const ds = window.getDeskState?.();
if (ds) ds.matrixTab = 'access-control';
setView('access-matrix');
}
});
root.querySelector('[data-wz-view-user]')?.addEventListener('click', () => {
const email = result?.user?.username || result?.user?.email;
close();
if (email && window.DeskAccessControlHub?.selectUser) {
window.DeskAccessControlHub.selectUser(email);
if (typeof onDone === 'function') onDone(result);
}
});
root.querySelector('[data-wz-copy-invite]')?.addEventListener('click', async () => {
try {
await navigator.clipboard.writeText(result?.invite_link || '');
window.alert('Link copiado');
} catch (_) {
window.prompt('Copiar link:', result?.invite_link);
}
});
}
function openWizard(opts = {}) {
defaultRole = opts.defaultRole || 'technician';
onDone = opts.onDone || null;
resetForm(defaultRole);
open = true;
syncDom();
}
function close() {
open = false;
syncDom();
}
window.DeskUserWizard = { open: openWizard, close };
})();