Pular para o conteúdo principal

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:

TabelaDescrição
user_login_eventsLogin e logout dos usuários (event_type: 'login' | 'logout')
report_access_eventsAcesso a um relatório específico (account_id + report_slug)

Políticas RLS:

  • INSERT liberado para authenticated apenas com user_id = auth.uid() (o usuário só pode inserir eventos seus próprios)
  • Não há SELECT para authenticated — os dados só são lidos pelas funções RPC via service_role
Sem SELECT direto

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çãoParâmetrosDescrição
get_access_por_grupo_empresap_account_id, p_interval_start?, p_interval_end?Acumulado de acessos por grupo/empresa
get_access_por_relatoriop_account_id, p_interval_start?, p_interval_end?Acumulado por relatório acessado
get_access_por_usuariop_account_id, p_interval_start?, p_interval_end?Acumulado por usuário
get_access_last_updatedp_account_idTimestamp do último acesso registrado para a conta

Função de resumo:

FunçãoParâmetrosDescrição
get_access_summaryp_account_idJSON com contagens de hoje/ontem/mês para as três dimensões

Funções de detalhe (usadas no modal):

FunçãoParâmetrosDescrição
get_access_relatorio_detailsp_account_id, p_report_slugHorário de pico, totais e últimos usuários de um relatório
get_access_grupo_detailsp_account_id, p_grupo_nomeTop relatórios, usuários ativos e totais de um grupo
get_access_usuario_detailsp_account_id, p_usuario_emailTop relatórios, últimos acessos e primeiro acesso de um usuário
Grupo identificado pelo nome

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âmetroTipoDescrição
tabAccessTabAba ativa: 'por-grupo-empresa' | 'por-relatorio' | 'por-usuario'
filtersAccessFiltersFiltros 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;
AbaLinha da tabelaChave do modal
por-grupo-empresaNome do grupo/empresaNome do grupo (_slug)
por-relatorioNome legível do relatórioSlug bruto do relatório (_slug)
por-usuarioE-mail do usuárioE-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:

ParamTipoPadrão
tabAccessTab'por-grupo-empresa'
intervalStartYYYY-MM-DD
intervalEndYYYY-MM-DD
statestring
groupstring

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:

  1. Monitora usePathname() via useEffect
  2. Extrai o slug do relatório da URL com /\/reports\/([^/?]+)/
  3. Chama trackReportAccessAction(account.id, slug) como fire-and-forget
  4. Um useRef evita disparos duplicados para a mesma URL (proteção contra React StrictMode)
Slug capturado automaticamente

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:

ActionQuando chamarFire-and-forget
trackReportAccessAction(accountId, slug)Ao navegar para um relatórioSim
trackLogoutAction()Ao fazer logoutSim

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.


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:

ConstanteSegundosUso
CACHE_TTL.DIARIO600Dados que mudam ao longo do dia
CACHE_TTL.HISTORICO3600Dados 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
Disponível apenas fora de produção

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.