/** * 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 ` `; } function toggleRow(name, title, desc, checked) { return ` `; } function summaryHtml() { const rm = roleMeta(form.role); const stats = permStats(); const name = form.display_name.trim() || '—'; const email = form.email.trim() || '—'; return ` `; } function stepBasic() { const statusChips = STATUS_OPTIONS.map((s) => `` ).join(''); return `

Dados pessoais

${SVG.whatsapp}
${SVG.info}
${SVG.calendar}

Segurança e acesso

Estado da conta
${statusChips}

Acesso adicional

${toggleRow('mfa_required', 'Activar autenticação de dois factores (2FA)', 'Recomendado para perfis administrativos', form.mfa_required)} ${toggleRow('notifications_enabled', 'Receber notificações do sistema', 'Alertas operacionais e avisos de segurança', form.notifications_enabled)} ${toggleRow('api_access', 'Permitir acesso via API', 'Gera token de API para integrações externas', form.api_access)}
${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) => ``).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 `

Perfil e grupo

Perfil sugerido com base nas permissões seleccionadas.

${groupHintOpen ? `

${esc(groupHint)}

` : ''}
${tags}
${SVG.info} Perfil define o que o utilizador pode fazer. Grupo define onde tem acesso.
${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) => ``).join(''); return ``; } function stepPermissions() { const tabs = PERM_TABS.map((t) => `` ).join(''); let tabBody = ''; if (permTab === 'modules') { const modules = filteredModules(); tabBody = `
${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 `
${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 `

Permissões

Ajuste fino das permissões antes de criar a conta.

${tabs}
${tabBody}
${profileSummaryHtml()}`; } function stepReview() { const secGroups = form.secondary_groups.length ? form.secondary_groups.join(', ') : '—'; const stats = permStats(); return `

Revisão

Confirme todos os dados antes de criar o utilizador.

Dados do utilizador

Nome
${esc(form.display_name)}
E-mail
${esc(form.email)}
Telefone
${esc(form.phone || '—')}
Estado da conta
${esc(statusLabel(form.account_status))}
2FA
${form.mfa_required ? 'Activado' : 'Opcional'}

Perfil e grupo

Perfil principal
${esc(roleMeta(form.role).label)}
Grupo principal
${esc(form.main_group)}
Grupos adicionais
${esc(secGroups)}

Permissões

Módulos
${stats.modules} módulos
Funções
${stats.functions}

Acessos adicionais

2FA
${form.mfa_required ? 'Activado' : 'Não'}
Acesso API
${form.api_access ? 'Permitido' : 'Não permitido'}
Notificações
${form.notifications_enabled ? 'Activas' : 'Desactivadas'}
${form.notes.length}/200 caracteres
`; } 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 || '')}

${invite ? `` : ''}
${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»:

`; } 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 = ` `; 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 }; })();