Secretariado

ou
Voltar à página do evento
Dashboard
Secretário

Dashboard

Resumo geral das inscrições do evento.

0
Total inscritos
0
Confirmadas ↗
0
Pendentes ↗
0
Rejeitadas ↗
Total cobrado · AOA

Inscrições recentes

CódigoNomeFunçãoIgreja / DistritoEstadoDataAcção

Todas as Inscrições

Pesquise, filtre e gira todas as inscrições.

CódigoNomeFunçãoIgreja TelefoneFacturaValor AOA (Kz) Comprova.EstadoDataAcções

Aprovação Rápida

Inscrições que aguardam verificação do pagamento.

Relatórios e Exportação

Exporte listas de pagamento para Excel e gere relatórios PDF organizados por filtros.

Excel · Lista de Inscritos

Exporta todos os campos — província, município, telefone, email, estado, comprovativo.

Excel · Pagamentos

Dados reais de pagamento: método, referência, recibo, datas e estado de conciliação.

PDF · Lista filtrada

Relatório com os filtros activos, agrupado e com totais por secção.

PDF · Credenciamento

Lista de confirmados agrupada por igreja, com coluna de assinatura para o evento.

PDF · Pagamentos

Relatório financeiro completo com todos os pagamentos registados, agrupado por estado.

Resumo Executivo

Estatísticas por Função

Tesouraria

Controlo financeiro das inscrições — valores esperados, recebidos e por validar.

Total esperado · AOA
Recebido (confirmado)
Por validar
Pendente
Estornado
Taxa de cobrança

Por Igreja / Distrito

Igreja / DistritoInscritosRecebido (AOA)Total esperado (AOA)

Por Tipo de Inscrição

Registar pagamento

Pagamentos recentes

RecebidoParticipanteValorMétodoReferência / reciboEstadoAcções

Fecho diário de tesouraria

Histórico de fechos

DataConciliadoDeclaradoDivergênciaPendentesFechado por

Divergências por inscrição

CódigoParticipanteIgrejaEsperadoPagoSaldoTipo

Gestão de Utilizadores

Adicione e gira os membros com acesso ao painel.

Adicionar utilizador

Pode deixar vazio para gerar automaticamente.
Mínimo 6 caracteres.

Utilizadores com acesso 0

Tabela de Permissões

Acção Super AdminAdminSecretárioTesoureiroVisualizador
Ver inscriçõesSimSimSimSimSim
Aprovar / RejeitarSimSimSimSimNão
Exportar ExcelSimSimNãoNãoNão
Imprimir listasSimSimNãoNãoNão
Ver auditoriaSimSimNãoNãoNão
Gerir utilizadoresSimSim*NãoNãoNão
Configurar sistemaSimNãoNãoNãoNão

* O Admin pode criar e gerir utilizadores, mas não pode criar nem alterar contas de Admin ou Super Admin (evita escalada de privilégios). Só o Super Admin gere todos os papéis.

Publicações das Comissões

Aprove ou rejeite os comunicados submetidos pelas comissões.

Auditoria

Registo de acessos, alterações, exportações e acções sensíveis do painel.

DataActorPapelCategoriaAcçãoAlvoRiscoIPDetalhe

Configurações de Segurança

Parâmetros centrais de auditoria, limites de acesso e política de relatórios.

Encomenda de Camisas
Permite aos participantes encomendarem camisas no portal
Encomenda de Chapéus
Permite aos participantes encomendarem chapéus no portal
Link do Livro dos 50 Anos

Operação recomendada

Use senhas geradas pelo painel, mantenha apenas Super Admins indispensáveis e reveja a auditoria diariamente durante o período de inscrições.

`; } function buildPaymentsHtml(payments, {excel=false}={}){ const generated = new Date().toLocaleString('pt-PT'); const total = payments.reduce((s,p)=>s+parseAOA(p.valor),0); const totalEsp = payments.reduce((s,p)=>s+parseAOA((p.inscricoes||{}).valor_esperado||0),0); const byEstado = {}; payments.forEach(p=>{ const k=p.estado||'Sem estado'; (byEstado[k]||(byEstado[k]=[])).push(p); }); const meta = [ `${payments.length} pagamento${payments.length===1?'':'s'}`, `Recebido: ${formatAOA(total)}`, `Esperado: ${formatAOA(totalEsp)}`, `Gerado em ${generated}` ]; const theadCols = PAYMENT_HEADERS.map(h=>`${esc(h)}`).join(''); const tables = Object.entries(byEstado).map(([estado, rows])=>{ const sub = rows.reduce((s,p)=>s+parseAOA(p.valor),0); return `
${esc(estado)}
${rows.length} pagamento${rows.length===1?'':'s'} · ${formatAOA(sub)}
${theadCols} ${paymentRowsHtml(rows,{excel})}
Subtotal${excel?parseAOA((rows[0]?.inscricoes||{}).valor_esperado||0).toFixed(2):''}${excel?sub.toFixed(2):formatAOA(sub)}
`; }).join(''); return ` Relatório de Pagamentos — ${EVENTO_NOME} ${reportStyles({excel})}
Relatório de Pagamentos
${esc(EVENTO_NOME)} · ${esc(EVENTO_LOCAL)}
${meta.map(m=>`${esc(m)}`).join('')}
${tables||'

Sem pagamentos registados.

'} ${!excel?'
Tesouraria
Coordenação
':''} ${!excel?``:''} `; } function downloadExcelReport(overrides={}){ const opts=getReportOptions(overrides); const list=reportRows(opts); if(!list.length){showToast('Sem dados para os filtros seleccionados.','warning');return null} const html=''+buildReportHtml(list,opts,{excel:true}); const blob=new Blob([html],{type:'application/vnd.ms-excel;charset=utf-8'}); const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download=`${cleanFileName(reportTitle(opts))}-${new Date().toISOString().slice(0,10)}.xls`; a.click();URL.revokeObjectURL(a.href); return {opts,total:list.length}; } async function downloadPaymentsExcel(){ try{ showToast('A carregar dados de pagamentos…','info',3000); const result=await AdminApi.payments({page:1,pageSize:500}); const payments=result.data||[]; if(!payments.length){showToast('Sem pagamentos registados.','warning');return} const html=''+buildPaymentsHtml(payments,{excel:true}); const blob=new Blob([html],{type:'application/vnd.ms-excel;charset=utf-8'}); const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download=`pagamentos-${new Date().toISOString().slice(0,10)}.xls`; a.click();URL.revokeObjectURL(a.href); await logAudit('relatorio_exportado','pagamentos',{formato:'xls',total:payments.length},'alto'); showToast(`Excel de pagamentos exportado (${payments.length} registos).`,'success'); }catch(err){showToast('Erro ao exportar pagamentos: '+(err.message||err),'error')} } function openPdfReport(overrides={}){ const opts=getReportOptions(overrides); const list=reportRows(opts); if(!list.length){showToast('Sem dados para os filtros seleccionados.','warning');return null} const w=window.open(); if(!w){showToast('Navegador bloqueou pop-up. Autorize pop-ups para este site.','warning');return null} w.document.write(buildReportHtml(list,opts)); w.document.close();w.focus(); setTimeout(()=>w.print(),600); return {opts,total:list.length}; } async function openPaymentsPdf(){ try{ showToast('A carregar dados de pagamentos…','info',3000); const result=await AdminApi.payments({page:1,pageSize:500}); const payments=result.data||[]; const w=window.open(); if(!w){showToast('Navegador bloqueou pop-up. Autorize pop-ups para este site.','warning');return} w.document.write(buildPaymentsHtml(payments)); w.document.close();w.focus(); setTimeout(()=>w.print(),600); await logAudit('relatorio_pdf','pagamentos',{total:payments.length},'alto'); }catch(err){showToast('Erro ao gerar PDF de pagamentos: '+(err.message||err),'error')} } document.getElementById('exportCsv').addEventListener('click',async()=>{ if(!canAdmin()){showToast('Sem permissão para exportar.','error');return} const res=downloadExcelReport(); if(res){showToast(`Excel exportado — ${res.total} registos.`,'success');await logAudit('relatorio_exportado','inscricoes',{formato:'xls',modelo:res.opts.modelo,total:res.total},'alto')} }); document.getElementById('exportPayments').addEventListener('click',async()=>{ if(!canAdmin()){showToast('Sem permissão para exportar.','error');return} await downloadPaymentsExcel(); }); document.getElementById('printList').addEventListener('click',async()=>{ if(!canAdmin()){showToast('Sem permissão para imprimir.','error');return} const res=openPdfReport(); if(res){await logAudit('relatorio_pdf','inscricoes',{modelo:res.opts.modelo,total:res.total},'alto')} }); document.getElementById('printConf').addEventListener('click',async()=>{ if(!canAdmin()){showToast('Sem permissão para imprimir.','error');return} const res=openPdfReport({modelo:'confirmados',estado:'Confirmada',grupo:'igreja',ordem:'igreja_nome'}); if(res){await logAudit('relatorio_pdf','inscricoes',{modelo:'confirmados',total:res.total},'alto')} }); document.getElementById('printPayments').addEventListener('click',async()=>{ if(!canAdmin()){showToast('Sem permissão para imprimir.','error');return} await openPaymentsPdf(); }); /* ==================== GESTÃO DE UTILIZADORES ==================== */ let adminUsers=[]; async function loadAdminUsers(){ if(!canManageUsers())return; try{ const result=await AdminApi.users(); adminUsers=Array.isArray(result.data)?result.data:[]; }catch(error){console.error('Erro ao carregar utilizadores:',error);return} renderUserList(); loadPedidosRecuperacao(); } function filtrarUtilizadores(termo){ const t=(termo||'').toLowerCase().trim(); const roleF=(document.getElementById('userRoleFilter')?.value||''); const filtrados=adminUsers.filter(u=>{ const matchRole=!roleF||(roleF==='com_'?u.role.startsWith('com_'):u.role===roleF); const haystack=[(u.nome||''),(u.email||''),(u.acesso_id||'')].join(' ').toLowerCase(); const matchTermo=!t||haystack.includes(t); return matchRole&&matchTermo; }); renderUserListData(filtrados); } function renderUserList(){ const counter=document.getElementById('userCount'); if(counter)counter.textContent=adminUsers.length; filtrarUtilizadores(document.getElementById('userSearch')?.value||''); } function renderUserListData(lista){ const list=document.getElementById('userList'); document.getElementById('userEmpty').style.display=lista.length?'none':'block'; list.innerHTML=lista.map((u,i)=>{ const idx=adminUsers.indexOf(u); // índice real no array global const gerivel=canManageRole(u.role); // posso gerir este utilizador? const opts=manageableRoles(); // garante que o papel actual aparece no select mesmo que não seja gerível const selOpts=(gerivel?opts:allRoles.filter(r=>r.v===u.role)) .map(r=>``).join(''); return `
${(u.nome||u.email).charAt(0).toUpperCase()}
${esc(u.email)}
${esc(u.nome||'—')} · ID: ${esc(u.acesso_id||'por gerar')}
${userPermSummary(u.role)}${u.ultimo_login_em?` · último acesso ${new Date(u.ultimo_login_em).toLocaleDateString('pt-PT')}`:''}
${allRolesMap[u.role]||u.role} ● ${u.ativo?'Activo':'Inactivo'} ${gerivel?` `:`${App.icon('lock',12)} Protegido`}
`; }).join(''); } function userPermSummary(role){ const p=rolePermissions(role); const items=[]; if(p.alterar_inscricoes)items.push('aprovar pagamentos'); if(p.exportar_relatorios)items.push('exportar relatórios'); if(p.gerir_utilizadores)items.push('gerir utilizadores'); if(!items.length)items.push('consulta limitada'); return 'Permissões: '+items.join(' · '); } function gerarAdminID(){ const y=new Date().getFullYear(); return 'ADM-'+y+'-'+String(Math.floor(1000+Math.random()*9000)); } function gerarSenhaAcesso(){ const chars='ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; return Array.from({length:10},()=>chars[Math.floor(Math.random()*chars.length)]).join(''); } function normalizarAcessoId(v){ return String(v||'').trim().toUpperCase().replace(/\s+/g,'-').replace(/[^A-Z0-9._-]/g,''); } function renderRolePreview(){ const box=document.getElementById('rolePreview');if(!box)return; const role=document.getElementById('newRole')?.value||'secretario'; const perms=rolePermissions(role); box.innerHTML=rolePermDefs.map(([key,title,desc])=>{ const on=!!perms[key]; return `
${on?'✓':'×'} ${esc(title)}${esc(desc)}
`; }).join(''); } let lastCreatedCredsText=''; function renderCreatedCreds({nome,email,acessoId,senha,role}){ const box=document.getElementById('createdCreds');if(!box)return; const roleLabel=allRolesMap[role]||role; const texto=`Acesso ao painel admin\\n\\nNome: ${nome||'—'}\\nEmail: ${email}\\nID: ${acessoId}\\nSenha: ${senha}\\nPermissão: ${roleLabel}\\n\\nPortal: https://adventista-50anos.online/admin`; lastCreatedCredsText=texto; box.style.display='block'; box.innerHTML=`
Acesso criado com sucesso
Entregue estes dados apenas ao utilizador autorizado.
${esc(roleLabel)}
ID de cadastro
${esc(acessoId)}
Email
${esc(email)}
Senha
${esc(senha)}
Permissão
${esc(roleLabel)}
`; } async function copyLastCreatedCreds(){ const texto=lastCreatedCredsText||''; try{ await navigator.clipboard.writeText(texto); await showAdminModal({icon:App.icon('check',42),title:'Copiado',body:'Dados de acesso copiados para a área de transferência.',actions:[{label:'OK',primary:true}]}); }catch(_){ showToast(texto,'warning'); } } async function addAdminUser(){ if(!canManageUsers()){showToast('Sem permissão para gerir utilizadores.','error');return} const nome=(document.getElementById('newNome').value||'').trim(); const email=(document.getElementById('newEmail').value||'').trim().toLowerCase(); const role=document.getElementById('newRole').value; const customId=normalizarAcessoId(document.getElementById('newAcessoId').value); const customSenha=(document.getElementById('newSenha').value||'').trim(); const msg=document.getElementById('addUserMsg'); if(!canManageRole(role)){msg.style.color='#ff9d9a';msg.textContent='Não tem permissão para criar utilizadores com este papel.';return} if(!email||!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)){msg.style.color='#ff9d9a';msg.textContent='Email inválido.';return} if(customId&&customId.length<4){msg.style.color='#ff9d9a';msg.textContent='O ID de cadastro deve ter pelo menos 4 caracteres.';return} if(customSenha&&customSenha.length<6){msg.style.color='#ff9d9a';msg.textContent='A senha deve ter pelo menos 6 caracteres.';return} msg.style.color='var(--muted)';msg.textContent='A adicionar…'; const acessoId=customId||gerarAdminID(); let data; try{ data=await AdminApi.createUser({acessoId,email,nome,role,password:customSenha||undefined}); }catch(error){ msg.style.color='#ff9d9a';msg.textContent='Erro: '+(error.message||error);return; } const senha=data.credential; msg.style.color='var(--ok)';msg.textContent='Utilizador adicionado com sucesso!'; renderCreatedCreds({nome,email,acessoId:data.acesso_id||acessoId,senha,role}); document.getElementById('newNome').value=''; document.getElementById('newEmail').value=''; document.getElementById('newAcessoId').value=''; document.getElementById('newSenha').value=''; await loadAdminUsers(); setTimeout(()=>msg.textContent='',5000); } /* Preenche o select de papéis (criar utilizador) só com os papéis que o actor pode atribuir — Admin não vê admin/super_admin. */ function populateNewRoleSelect(){ const sel=document.getElementById('newRole'); if(!sel)return; const cur=sel.value; const adminRoles=[['secretario','Secretário'],['tesoureiro','Tesoureiro'],['admin','Admin'],['super_admin','Super Admin'],['visualizador','Visualizador']] .filter(([v])=>canManageRole(v)); const comRoles=allRoles.filter(r=>r.v.startsWith('com_')&&canManageRole(r.v)); let html=''; if(adminRoles.length)html+=`${adminRoles.map(([v,l])=>``).join('')}`; if(comRoles.length)html+=`${comRoles.map(r=>``).join('')}`; sel.innerHTML=html; if([...sel.options].some(o=>o.value===cur))sel.value=cur; renderRolePreview(); } renderRolePreview(); async function updateRole(i,newRole){ if(!canManageUsers()){showToast('Sem permissão para gerir utilizadores.','error');return} const u=adminUsers[i]; const oldRole=u.role; if(!canManageRole(oldRole)||!canManageRole(newRole)){ showToast('Sem permissão para atribuir este papel.','error');renderUserList();return; } try{await AdminApi.updateUserRole(u.id,newRole)} catch(error){showToast('Erro ao actualizar: '+(error.message||error),'error');renderUserList();return} adminUsers[i].role=newRole; renderUserList(); } async function toggleUserAtivo(i){ if(!canManageUsers()){showToast('Sem permissão para gerir utilizadores.','error');return} const u=adminUsers[i]; if(!canManageRole(u.role)){showToast('Sem permissão para gerir este utilizador.','error');return} try{ await AdminApi.updateUserStatus(u.id,!u.ativo); }catch(error){ if((error.message||'').includes('ULTIMO_SUPER_ADMIN')){ await showAdminModal({icon:App.icon('alert',42),title:'Acção bloqueada',body:'Não é possível desactivar o último Super Admin activo do sistema.',actions:[{label:'OK',primary:true}]});return; } showToast('Erro: '+(error.message||error),'error');return; } adminUsers[i].ativo=!u.ativo; renderUserList(); } async function resetAdminSenha(i){ if(!canManageUsers()){showToast('Sem permissão para gerir utilizadores.','error');return} const u=adminUsers[i]; if(!canManageRole(u.role)){showToast('Sem permissão para gerir este utilizador.','error');return} let result; try{result=await AdminApi.resetUserCredential(u.id)} catch(error){showToast('Erro ao gerar nova senha: '+(error.message||error),'error');return} const senha=result.credential; adminUsers[i].senha=senha; await showCredModal('Nova senha gerada',{ 'Nome':u.nome||'—','ID':u.acesso_id||'—','Email':u.email,'Nova senha':senha },`Acesso ao painel admin\\n\\nNome: ${u.nome||'—'}\\nID: ${u.acesso_id||'—'}\\nEmail: ${u.email}\\nSenha: ${senha}\\n\\nPortal: https://adventista-50anos.online/admin`); } /* Modal reutilizável para mostrar credenciais com botão copiar */ async function showCredModal(titulo,campos,copyText){ const grid=Object.entries(campos).map(([k,v])=>`
${esc(k)}
${esc(String(v))}
`).join(''); window._lastCredCopy=copyText||''; const r=await showAdminModal({ icon:App.icon('check',42),title:titulo, body:`
${grid}

Entregue estes dados apenas ao utilizador autorizado.

`, actions:[{label:'Copiar dados',value:'copy'},{label:'Fechar',value:'close',primary:true}] }); if(r==='copy'){ try{await navigator.clipboard.writeText((window._lastCredCopy||'').replace(/\\n/g,'\n'));}catch(_){} } } /* ===== PEDIDOS DE RECUPERAÇÃO DE SENHA ("esqueci a senha") ===== */ let recoverReqs=[]; async function loadPedidosRecuperacao(){ if(!canManageUsers())return; const wrap=document.getElementById('recoverWrap'); try{ const result=await AdminApi.recoveryRequests(); recoverReqs=Array.isArray(result.data)?result.data:[]; }catch(err){ console.warn('Pedidos de recuperação indisponíveis:',err?.message||err); if(wrap)wrap.style.display='none'; return; } // Admin só vê pedidos que pode resolver (não admins/super) const visiveis=recoverReqs.filter(p=>canManageRole(p.role)); const badge=document.getElementById('recoverBadge'); if(badge)badge.textContent=visiveis.length; if(!wrap)return; if(!visiveis.length){wrap.style.display='none';return} wrap.style.display='block'; document.getElementById('recoverList').innerHTML=visiveis.map(p=>`
${esc(p.email)}
${esc(p.nome||'—')} · ID: ${esc(p.acesso_id||'—')} · ${esc(allRolesMap[p.role]||p.role)}
Pedido em ${new Date(p.criado_em).toLocaleString('pt-PT')}${p.expirado?' · expirado':''}
`).join(''); } async function resolverRecuperacao(resetId){ if(!canManageUsers())return; let data; try{data=await AdminApi.resolveRecovery(resetId)} catch(error){await showAdminModal({icon:App.icon('x',42),title:'Erro',body:'Não foi possível resolver: '+esc(error.message||error),actions:[{label:'OK',primary:true}]});return} const senha=data.credential; await showCredModal('Senha recuperada',{ 'Email':data.email,'ID':data.acesso_id||'—','Nova senha':senha },`Recuperação de acesso — painel admin\\n\\nEmail: ${data.email}\\nID: ${data.acesso_id||'—'}\\nNova senha: ${senha}\\n\\nPortal: https://adventista-50anos.online/admin`); await loadAdminUsers(); } async function descartarRecuperacao(resetId){ if(!canManageUsers())return; const r=await showAdminModal({icon:App.icon('alert',42),title:'Descartar pedido',body:'Marcar este pedido de recuperação como tratado, sem alterar a senha?',actions:[{label:'Cancelar',value:'cancel'},{label:'Descartar',value:'ok'}],danger:true}); if(r!=='ok')return; try{await AdminApi.discardRecovery(resetId)} catch(error){showToast('Erro: '+(error.message||error),'error');return} await loadPedidosRecuperacao(); } /* allRoles e allRolesMap declarados no topo do script */ /* ===== PUBLICAÇÕES DAS COMISSÕES ===== */ let comPubs=[]; async function loadPublicacoes(){ try{ const result=await AdminApi.publications(); comPubs=Array.isArray(result.data)?result.data:[]; }catch(error){console.error(error);return} const pending=comPubs.filter(p=>!p.aprovado).length; const badge=document.getElementById('pubBadge'); if(badge){badge.textContent=pending;badge.style.display=pending?'':'none'} renderPublicacoes(); } function renderPublicacoes(){ const list=document.getElementById('pubAdminList'); const empty=document.getElementById('pubAdminEmpty'); if(!list)return; if(!comPubs.length){empty.style.display='block';return} empty.style.display='none'; list.innerHTML=comPubs.map(p=>`
${esc(p.comissao_id.replace('com_','').toUpperCase())}
${esc(p.titulo)}
${p.conteudo?`
${esc(p.conteudo)}
`:''}
Por: ${esc(p.criado_por||'—')} · ${new Date(p.criado_em).toLocaleDateString('pt-PT')}
${p.aprovado?'Aprovado':'Pendente'}
${!p.aprovado?`
`:''}
`).join(''); } async function aprovarPub(id){ if(!canAdmin()){showToast('Sem permissão para aprovar publicações.','error');return} try{await AdminApi.approvePublication(id);showToast('Publicação aprovada.','success')} catch(error){showToast('Erro: '+(error.message||error),'error');return} await loadPublicacoes(); } async function rejeitarPub(id){ if(!canAdmin()){showToast('Sem permissão para eliminar publicações.','error');return} if(!confirm('Eliminar esta publicação?'))return; try{await AdminApi.deletePublication(id);showToast('Publicação eliminada.','success')} catch(error){showToast('Erro: '+(error.message||error),'error');return} await loadPublicacoes(); } /* ===== AUDITORIA E CONFIGURAÇÕES ===== */ let auditPaginaActual=1; const AUDIT_LIMITE=50; document.getElementById('auditFilter').addEventListener('change',()=>aplicarFiltrosAudit()); document.getElementById('auditCatFilter').addEventListener('change',()=>aplicarFiltrosAudit()); function auditFiltrosActuais(){ return{ risco:document.getElementById('auditFilter').value||undefined, categoria:document.getElementById('auditCatFilter').value||undefined, actor:document.getElementById('auditActorFilter').value.trim()||undefined, acao:document.getElementById('auditAcaoFilter').value.trim()||undefined, dataInicio:document.getElementById('auditDataInicio').value||undefined, dataFim:document.getElementById('auditDataFim').value||undefined, }; } function aplicarFiltrosAudit(){auditPaginaActual=1;loadAuditoria();} function limparFiltrosAudit(){ document.getElementById('auditFilter').value=''; document.getElementById('auditCatFilter').value=''; document.getElementById('auditActorFilter').value=''; document.getElementById('auditAcaoFilter').value=''; document.getElementById('auditDataInicio').value=''; document.getElementById('auditDataFim').value=''; auditPaginaActual=1;loadAuditoria(); } function auditMudaPagina(delta){ const novo=auditPaginaActual+delta; if(novo<1)return; auditPaginaActual=novo;loadAuditoria(); } async function loadAuditStats(){ try{ const s=await AdminApi.auditStats(); const box=document.getElementById('auditStats'); const critico=s.critico_24h||0; box.innerHTML=[ ['Hoje',s.hoje??'…','clock','var(--gold)'], ['Esta semana',s.semana??'…','calendar','var(--pend)'], ['Críticos (24h)',critico,'alertTriangle',critico>0?'#f0a6a3':'var(--ok)'], ['Altos (24h)',s.alto_24h??0,'alertCircle','var(--warn)'], ['Total geral',s.total??'…','database','var(--muted)'], ].map(([lbl,val,ic,cor])=>`
${lbl}
${val}
`).join(''); App.hydrateIcons(box); /* badge de alerta no título */ const badge=document.getElementById('auditAlertaBadge'); if(critico>0){badge.textContent=critico+' crítico'+(critico>1?'s':'');badge.style.display='inline';} else badge.style.display='none'; }catch(e){/* silencioso */} } async function loadAuditoria(){ if(!canAdmin())return; const body=document.getElementById('auditBody'); body.innerHTML=`A carregar…`; try{ const params={...auditFiltrosActuais(),pagina:auditPaginaActual,limite:AUDIT_LIMITE}; const result=await AdminApi.audit(params); const list=Array.isArray(result.data)?result.data:[]; document.getElementById('auditEmpty').style.display=list.length?'none':'block'; const total=result.total||0; const totalPag=result.total_paginas||1; document.getElementById('auditPagInfo').textContent= `Página ${auditPaginaActual} de ${totalPag} · ${total} evento${total!==1?'s':''} total`; document.getElementById('auditBtnAnterior').disabled=auditPaginaActual<=1; document.getElementById('auditBtnProximo').disabled=auditPaginaActual>=totalPag; const catMap={acesso:'🔑',inscricao:'📋',pagamento:'💳',utilizador:'👤',documento:'📄',sistema:'⚙️'}; body.innerHTML=list.map(r=>{ let detalhe='—'; try{detalhe=r.detalhe?JSON.stringify(r.detalhe,null,0):'—';}catch(e){} const riscoClass=r.risco==='critico'?'rej':r.risco==='alto'?'val':'conf'; return ` ${r.criado_em?new Date(r.criado_em).toLocaleString('pt-PT'):'—'} ${esc(r.actor_email||'—')} ${esc(allRolesMap[r.actor_role]||r.actor_role||'—')} ${catMap[r.categoria]||'⚙️'} ${esc(r.categoria||'sistema')} ${esc(r.acao||'—')} ${esc(r.alvo||'—')} ${esc(r.risco||'normal')} ${esc(r.ip||'—')} ${esc(detalhe)} `; }).join(''); loadAuditStats(); }catch(err){ body.innerHTML=`Erro: ${esc(err.message||err)}`; } } async function exportarAuditCSV(){ if(!canAdmin())return; try{ /* Carrega todos os eventos com filtros actuais (até 500) */ const params={...auditFiltrosActuais(),pagina:1,limite:500}; const result=await AdminApi.audit(params); const list=Array.isArray(result.data)?result.data:[]; if(!list.length){showToast('Sem eventos para exportar.','warn');return;} const cabecalho=['Data','Actor','Papel','Categoria','Acção','Alvo','Risco','IP','Detalhe']; const linhas=list.map(r=>[ r.criado_em?new Date(r.criado_em).toLocaleString('pt-PT'):'', r.actor_email||'',r.actor_role||'',r.categoria||'sistema', r.acao||'',r.alvo||'',r.risco||'normal',r.ip||'', r.detalhe?JSON.stringify(r.detalhe):'' ].map(v=>'"'+String(v).replace(/"/g,'""')+'"').join(',')); const csv=''+[cabecalho.join(','),...linhas].join('\r\n'); const blob=new Blob([csv],{type:'text/csv;charset=utf-8'}); const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='auditoria-'+new Date().toISOString().slice(0,10)+'.csv'; a.click(); showToast(`${list.length} eventos exportados.`,'ok'); try{await registarAuditoria('exportar_auditoria','audit_log',{total:list.length},'normal');}catch(e){} }catch(err){showToast('Erro ao exportar: '+(err.message||err),'error');} } async function loadConfiguracoes(){ if(!canSuper())return; await loadConfigToggles(); const list=document.getElementById('configList'); list.innerHTML='

A carregar…

Configurações do sistema

'; try{ const result=await AdminApi.settings(); const rows=Array.isArray(result.data)?result.data:[]; list.innerHTML=rows.map(c=>`

${esc(c.chave)}

${esc(c.descricao||'—')}

${esc(JSON.stringify(c.valor||{},null,2))}
`).join('')||'
Sem configurações registadas.
'; App.hydrateIcons(list); }catch(err){ list.innerHTML=`
Erro ao carregar configurações: ${esc(err.message||err)}
`; } } /* Carregar ao entrar nas views */ const _origShowView=showView; function showView(name,btn){ _origShowView(name,btn); if(name==='utilizadores'&&canManageUsers()){populateNewRoleSelect();loadAdminUsers();} if(name==='tesouraria')loadTesouraria(); if(name==='publicacoes')loadPublicacoes(); if(name==='relatorios')loadRelatorioBackend(); if(name==='auditoria')loadAuditoria(); if(name==='configuracoes')loadConfiguracoes(); }