Relatório De Acessos — Vona
O relatório De Acessos exibe a atividade dos usuários dentro da plataforma Vona: quais relatórios foram acessados, por quem, e com que frequência. Os dados são coletados automaticamente durante a navegação e agregados em três dimensões (grupo/empresa, relatório, usuário) com filtros de período.
Banco de dados
Tabelas de eventos
Dois eventos são capturados e armazenados no Supabase:
| Tabela | Descrição |
|---|---|
user_login_events | Login e logout dos usuários (event_type: 'login' | 'logout') |
report_access_events | Acesso a um relatório específico (account_id + report_slug) |
Políticas RLS:
INSERTliberado paraauthenticatedapenas comuser_id = auth.uid()(o usuário só pode inserir eventos seus próprios)- Não há
SELECTparaauthenticated— os dados só são lidos pelas funções RPC viaservice_role
Não consulte report_access_events diretamente com o cliente autenticado. Use sempre as funções RPC abaixo, que são chamadas pelo AccessService com o cliente admin.
Funções RPC
Todas as funções usam SECURITY DEFINER e são executadas sob service_role.
Funções de listagem (usadas nas abas):
| Função | Parâmetros | Descrição |
|---|---|---|
get_access_por_grupo_empresa | p_account_id, p_interval_start?, p_interval_end? | Acumulado de acessos por grupo/empresa |
get_access_por_relatorio | p_account_id, p_interval_start?, p_interval_end? | Acumulado por relatório acessado |
get_access_por_usuario | p_account_id, p_interval_start?, p_interval_end? | Acumulado por usuário |
get_access_last_updated | p_account_id | Timestamp do último acesso registrado para a conta |
Função de resumo:
| Função | Parâmetros | Descrição |
|---|---|---|
get_access_summary | p_account_id | JSON com contagens de hoje/ontem/mês para as três dimensões |
Funções de detalhe (usadas no modal):
| Função | Parâmetros | Descrição |
|---|---|---|
get_access_relatorio_details | p_account_id, p_report_slug | Horário de pico, totais e últimos usuários de um relatório |
get_access_grupo_details | p_account_id, p_grupo_nome | Top relatórios, usuários ativos e totais de um grupo |
get_access_usuario_details | p_account_id, p_usuario_email | Top relatórios, últimos acessos e primeiro acesso de um usuário |
get_access_grupo_details recebe o nome do grupo como string (não um ID), usando a mesma string exibida na tabela. Isso funciona enquanto os nomes de grupo forem únicos dentro de uma conta.
AccessService
O AccessService é criado via factory, segue o mesmo padrão dos outros services de relatório, e deve ser usado apenas em Server Components ou Route Handlers.
import { createAccessService } from './_lib/server/access-service';
const service = createAccessService();
getReportData
Retorna os dados da aba ativa junto com o timestamp de última atualização.
const { card, lastUpdated } = await service.getReportData(tab, filters);
// card: AccessCard | null
// lastUpdated: string | null
Parâmetros:
| Parâmetro | Tipo | Descrição |
|---|---|---|
tab | AccessTab | Aba ativa: 'por-grupo-empresa' | 'por-relatorio' | 'por-usuario' |
filters | AccessFilters | Filtros aplicados |
Tipo AccessFilters:
interface AccessFilters {
accountId: string; // UUID da conta (team account) — obrigatório
intervalStart?: string; // YYYY-MM-DD — início do período
intervalEnd?: string; // YYYY-MM-DD — fim do período
state?: string; // Sigla do estado (reservado — não usado nas queries ainda)
group?: string; // Grupo ABRACAF (reservado — não usado nas queries ainda)
}
getAccessSummary
Retorna os três totais exibidos nos cards de resumo acima da tabela.
const summary = await service.getAccessSummary(tab, accountId);
// summary: AccessSummary
Tipo AccessSummary:
interface AccessSummary {
totalHoje: number;
totalOntem: number;
totalMes: number;
}
Os valores variam conforme a aba ativa: totalHoje de relatórios, grupos ou usuários distintos, respectivamente.
getFilterOptions
Retorna opções para os selects do modal de filtros (atualmente retorna arrays vazios — reservado para expansão futura).
const { stateOptions, groupOptions } = await service.getFilterOptions();
Métodos de detalhe
Chamados internamente pelo fetchAccessDetailsAction ao abrir o modal de um item.
// Por tab:
await service.getRelatorioDetails(accountId, reportSlug); // RelatorioDetails | null
await service.getGrupoDetails(accountId, grupoNome); // GrupoDetails | null
await service.getUsuarioDetails(accountId, usuarioEmail); // UsuarioDetails | null
Abas disponíveis
export const ACCESS_TABS = [
{ value: 'por-grupo-empresa', label: 'Por grupo/empresa' },
{ value: 'por-relatorio', label: 'Por relatório' },
{ value: 'por-usuario', label: 'Por usuário' },
] as const;
| Aba | Linha da tabela | Chave do modal |
|---|---|---|
por-grupo-empresa | Nome do grupo/empresa | Nome do grupo (_slug) |
por-relatorio | Nome legível do relatório | Slug bruto do relatório (_slug) |
por-usuario | E-mail do usuário | E-mail do usuário (_slug) |
Todas as abas exibem as colunas Total de Acessos e Último Acesso, além de um botão de detalhes por linha.
Página De Acessos
A página é um Server Component em de-acessos/page.tsx. Ela resolve os parâmetros da URL, chama os três métodos do service em paralelo e passa os dados para os componentes de UI.
URL: /[locale]/home/[account]/reports/de-acessos?tab=por-relatorio&intervalStart=2025-01-01
Search params reconhecidos:
| Param | Tipo | Padrão |
|---|---|---|
tab | AccessTab | 'por-grupo-empresa' |
intervalStart | YYYY-MM-DD | — |
intervalEnd | YYYY-MM-DD | — |
state | string | — |
group | string | — |
Rastreamento automático — ReportAccessTracker
O componente ReportAccessTracker fica no layout dos relatórios e dispara um evento de acesso sempre que o usuário navega para uma URL dentro de /reports/.
// layout.tsx — já está incluído no layout de relatórios
import { ReportAccessTracker } from './_components/report-access-tracker';
// O componente não renderiza nada visualmente
<ReportAccessTracker />
Como funciona:
- Monitora
usePathname()viauseEffect - Extrai o slug do relatório da URL com
/\/reports\/([^/?]+)/ - Chama
trackReportAccessAction(account.id, slug)como fire-and-forget - Um
useRefevita disparos duplicados para a mesma URL (proteção contra React StrictMode)
O slug enviado é o segmento de URL imediatamente após /reports/. Para /reports/de-acessos, o slug registrado é 'de-acessos'. Para adicionar um novo relatório ao tracking, basta que a rota siga esse padrão — nenhuma configuração adicional é necessária.
Server actions de rastreamento
Localizados em _lib/server/access-tracking.actions.ts:
| Action | Quando chamar | Fire-and-forget |
|---|---|---|
trackReportAccessAction(accountId, slug) | Ao navegar para um relatório | Sim |
trackLogoutAction() | Ao fazer logout | Sim |
Ambas verificam a autenticidade do usuário e, no caso de trackReportAccessAction, confirmam que o usuário é membro da conta antes de inserir o evento.
AccessEventService
Service de baixo nível que faz os inserts nas tabelas. Recebe um SupabaseClient injetado (cliente RLS-enforced do servidor).
import { createAccessEventService } from './_lib/server/access-event.service';
const svc = createAccessEventService(client);
await svc.trackLogin(userId);
await svc.trackLogout(userId);
await svc.trackReportAccess(userId, accountId, reportSlug);
Cards de resumo — AccessSummaryCards
Exibe três cards no topo da página com os totais da dimensão ativa (hoje / ontem / mês).
<AccessSummaryCards summary={summary} />
Os labels dos cards incluem a data formatada em dd/MM no fuso America/Sao_Paulo. O cálculo de "ontem" usa aritmética de dias no calendário do fuso para evitar erros em noites de horário de verão.
Modal de detalhes — useAccessDetails
Ao clicar no ícone de olho em qualquer linha da tabela, o AccessDetailsModal é aberto. Os dados são buscados via fetchAccessDetailsAction (que valida a membrana da conta antes de chamar o service admin).
O hook useAccessDetails encapsula o ciclo de vida da busca:
import { useAccessDetails } from '../_lib/hooks';
const { details, loading } = useAccessDetails(tab, accountId, itemKey);
// details: AccessDetails | null
// loading: boolean
Tipos de detalhe por aba:
// tab === 'por-relatorio'
interface RelatorioDetails {
picoHora: number | null; // Hora do dia com mais acessos (0–23)
totalHoje: number;
totalOntem: number;
totalMes: number;
ultimosUsuarios: { email: string; accessedAt: string }[];
}
// tab === 'por-grupo-empresa'
interface GrupoDetails {
topRelatorios: { relatorio: string; total: number }[];
usuariosAtivos: number;
totalHoje: number;
totalOntem: number;
totalMes: number;
ultimoAcesso: string | null;
}
// tab === 'por-usuario'
interface UsuarioDetails {
topRelatorios: { relatorio: string; total: number }[];
ultimosAcessos: { relatorio: string; accessedAt: string }[];
totalHoje: number;
totalOntem: number;
totalMes: number;
primeiroAcesso: string | null;
}
Utilitários de formatação
formatReportSlug
Converte o slug bruto do banco em label legível. Usado em todos os três modais de detalhe.
import { formatReportSlug } from './_lib/format-utils';
formatReportSlug('de-acessos') // → 'De Acessos'
formatReportSlug('por-ranking') // → 'Por Ranking'
formatReportSlug('unknown-slug') // → 'unknown-slug' (fallback: retorna o próprio slug)
formatAccessDateTime
Formata um timestamp ISO para exibição em dd/mm/aaaa hh:mm no fuso America/Sao_Paulo.
formatAccessDateTime('2025-07-01T18:30:00Z') // → '01/07/2025 15:30'
formatAccessDateTime(null) // → '—'
Cache
O relatório De Acessos usa a tag 'relatorio-de-acessos' definida em lib/cache/report-cache-keys.ts.
import { REPORT_TAGS } from '~/lib/cache';
REPORT_TAGS.ACESSOS // → 'relatorio-de-acessos'
TTLs disponíveis:
| Constante | Segundos | Uso |
|---|---|---|
CACHE_TTL.DIARIO | 600 | Dados que mudam ao longo do dia |
CACHE_TTL.HISTORICO | 3600 | Dados históricos consolidados |
Endpoint de revalidação (desenvolvimento):
POST /api/cache/revalidate → invalida todas as tags
POST /api/cache/revalidate?tag=relatorio-de-acessos → invalida só esta tag
O endpoint retorna 403 em NODE_ENV=production. Use pnpm supabase:web:reset ou navegação normal em dev para forçar atualização dos dados.