A PME entra. O admin aprova. Cinco fases. Um relatório.
ESG Lab é um pipeline linear gated por aprovação humana. Uma empresa regista-se → fica em conta_pendente → admin aprova → percorre cinco fases (F0 autoavaliação opcional + F1→F4 obrigatórias) → cada fase submetida vai a revisão admin → completar uma fase desbloqueia a seguinte → no fim geram-se PDFs (5 relatórios principais + 3 VSME).
A1·A2·A3
D1·D2·D3
M1–M8
E1·E2·E3
R1–R5 + VSME
Atores
- PME (conta_aprovada)
- Administrador
- Gestor de projeto (read-only)
- Sistema (Next.js API)
- Externo (Resend · Supabase · Telegram)
Setas
Pipeline de alto nível
conta_pendente
limitedAcabou de se registar. Não tem acesso a nada além de /pendente e logout.
- · Middleware força redirect para /pendente se tentar outra rota
- · Recebe email registration ao registar
- · Recebe email account_approved quando admin aprova
conta_aprovada
PMEEmpresa cliente real. Percorre F0→F4 e descarrega relatórios.
- · /dashboard · vista de fases & progresso
- · /f0..f4 · preenche formulários (guard: phase_status='unlocked')
- · /anexos · gere ficheiros
- · /apoio · pede ajuda (insere support_requests + Telegram)
- · /relatorios · descarrega PDFs gerados
- · /dashboard/definicoes · edita perfil & password
gestor_projeto
read-onlyAcompanha PMEs que lhe são atribuídas via manager_assignments. NÃO aprova fases.
- · /admin/gestor · landing dedicado
- · /admin/utilizadores/[id] · vê ficha (scoped via manager_assignments)
- · Pode regenerar PDFs: POST /admin/reports/[id]/regenerate
administrador_projeto
superuserAcesso total. Aprova contas, aprova cada fase submetida, gere utilizadores, gera relatórios.
- · /admin · home
- · /admin/utilizadores · CRUD utilizadores (delete, role, password, paid_phases)
- · /admin/aprovacoes · queue de fases submetidas
- · /admin/aprovacoes/[userId]/[phase] · fiche de revisão
- · /admin/relatorios · trigger geração PDFs
- · /admin/emails · edita templates
- · /admin/gestor · atribui gestores
- · /admin/settings · liga/desliga email notifications
Signup → Aprovação → Primeira fase desbloqueada
3 lanes · 1 happy path · 1 edge (autoavaliacao_requested)F0 — Autoavaliação A1+A2+A3 (com upload de evidências)
Fase submetida → Admin revê → Aprova / Rejeita
F4 completo → Admin gera relatórios → PME descarrega
Account (profiles.role)
4 estados · transições gated por admin · sem auto-promote
Phase status (phase_status.status)
4 estados por (user_id, phase) · phase ∈ {f0,f1,f2,f3,f4}
Generated PDF
profiles · User account
core
- 1 · seed Row criada por trigger DB ao auth.signUp
- 2 · enrich Client profiles.update com telefone, NIF, designacao_social, autoavaliacao_requested
- 3 · approve Admin patch · role=conta_aprovada · paid_phases inicializadas
- 4 · admin edits PATCH /admin/users/[id] · pode mudar role/paid_phases sem reconciliar phase_status (⚠)
- 5 · delete auth.admin.deleteUser · cascade DB?
phase_status · 5×N grid
workflow
- 1 · init Admin approve action upsert 5 rows (uma por fase)
- 2 · unlock unlocked_at definido · UI deixa editar
- 3 · submit PME POST → submitted_at · email a admins
- 4 · decide Admin PATCH → completed_at OU reset (volta a unlocked)
- 5 · cascade Phase n+1 passa automaticamente a unlocked
Form data · f0_autoavaliacao · f1_diagnostico · f2_materialidade · f3_estrategia · f4_reporte · f4_vsme
content
- 1 · draft POST /api/forms/* upsert por (user_id, section/module)
- 2 · enrich F0: score + nivel calculado (Elementar/Consciente/Integrador/Transformador)
- 3 · attachments F0 evidence files (bucket evidence) · F1 logo (bucket company-logos) · F2 matrix-image (bucket reports) · F3 e3 admin photo (bucket evidence)
- 4 · derive Lidos por PDF generator para compor relatórios
- 5 · admin review Renderizados read-only em /admin/aprovacoes/[id]/[phase]
generated_pdfs · Final reports
artifact
- 1 · generate Admin POST /admin/users/[id]/reports · loop por type
- 2 · render react-pdf renderToBuffer · lê profile + tabela da fase + buckets imagem
- 3 · store Storage bucket reports · path {userId}/{file}.pdf · upsert overwrite
- 4 · index Upsert row generated_pdfs(id, phase, module, file_url=/api/reports/{id}, file_path, generated_at)
- 5 · serve GET /api/reports/[id] · stream inline (sem role check)
- 6 · regenerate POST /admin/reports/[id]/regenerate mantém mesmo id
support_requests · Help & bug reports
support
- 1 · create POST /api/support · user submete em /apoio
- 2 · notify Telegram a chat OPS (env TELEGRAM_OPS_CHAT_ID); silent fail
- 3 · ⚠ no email Admin não recebe email · não há UI de gestão (apenas visível em /admin/utilizadores/[id])
- 4 · ? Sem campo de resolução · sem reply ao utilizador
manager_assignments · gestor → PME
relation
- 1 · assign Admin POST /api/admin/assign-manager · valida role=gestor_projeto
- 2 · notify Email manager_assigned só ao utilizador · gestor não é avisado (⚠)
- 3 · scope Gestor lê apenas PMEs com assignment activo
- 4 · unassign DELETE /api/admin/assign-manager · sem email a ninguém
Rejeição de fase sem motivo
Quando o admin re-abre uma fase (submitted → unlocked) a PME recebe email phase_unlocked mas não há campo para anexar uma razão. Template phase_blocked usa mensagem="".
Fix: adicionar coluna rejection_reason a phase_status + UI de admin + variable no template.
/api/reports/[id] sem role check
Qualquer utilizador autenticado pode pedir um id de PDF. Depende inteiramente de RLS nas tabelas e bucket Storage.
Fix: verificar dono no handler (user.id === pdf.user_id ou role admin/gestor com assignment).
update_paid_phases não reconcilia phase_status
Conceder/revogar autoavaliacao via esta acção não destrava nem bloqueia F0. Estado inconsistente entre profiles.paid_phases e phase_status.
Fix: chamar ensurePhaseProgression após mutação.
Admin pode apagar outro admin
Apenas self-delete está bloqueado em /api/admin/users action=delete_user.
Fix: 2 admins mínimo · confirmação · audit log.
Gestor não notificado quando atribuído
triggerManagerAssignedEmail envia só ao utilizador. O gestor descobre sozinho.
Fix: enviar também ao gestor (template separado).
/api/auth/registration-email re-triggerable
Qualquer user autenticado pode chamar o endpoint e re-enviar o email. Vector de spam interno.
Fix: rate-limit + flag profiles.registration_email_sent_at.
phase_blocked template ambíguo
triggerPhaseStatusEmail dispara phase_blocked para qualquer status ∉ {completed, unlocked}, incluindo um set artificial para "submitted" — semântica errada.
Fix: explícito por status; default = no-op em vez de "blocked".
Reset para unlocked apaga timestamps
getTimestampState('unlocked') zera submitted_at e completed_at · histórico perdido.
Fix: tabela append-only phase_status_history.
Support requests · sem feedback loop
PME submete pedido, vai a Telegram OPS, mas não há resposta de volta no produto. Admin tem que reagir fora da plataforma.
PDFs sem versioning nem delete
Regenerar sobrescreve o ficheiro · sem histórico · stale relatórios podem ficar acessíveis após delete do utilizador (cascade não está claro).
change_role / set_password / delete sem email
Operações silenciosas. Utilizador pode ficar logged-out sem perceber.
phase_status nunca auto-relock
Se dados a montante forem invalidados, fases seguintes continuam completed/unlocked. Sem trigger de invalidação.
Middleware: /api/* tudo público
Cada handler tem que reverificar auth. Inconsistência possível (e.g. seed routes presentes na árvore).
Dashboard "definicoes" duplica /api/user/password
Coexistem: Supabase auth.updateUser (client) e API custom. Validar qual é o canónico.