Tema
API Client & Utilitários
Utilitários de fetch, configuração, datas e exportação localizados em frontend/lib/.
fetchWithAuth
Arquivo: lib/fetchWithAuth.ts
Fetch autenticado com retry automático para falhas de autenticação transitórias.
typescript
export async function fetchWithAuth(
url: string,
getToken: () => Promise<string | null>,
options?: RequestInit,
maxRetries = 3,
): Promise<Response>Por que retry em 401 e 403?
401: O Clerk rotaciona o JWT periodicamente. Durante a rotação getToken() pode retornar null brevemente, fazendo a requisição chegar sem token ao backend.
403: O Clerk pode levar alguns instantes para sincronizar o papel do usuário após rotação ou refresh rápido de página. Nesse intervalo o backend retorna 403 mesmo para usuários válidos.
Estratégia de retry
Tentativa 1: imediata
Tentativa 2: aguarda 500ms (se sinal não abortado)
Tentativa 3: aguarda 1000ms (se sinal não abortado)
Para cada tentativa:
1. Verifica AbortSignal → lança AbortError se abortado
2. Obtém token via getToken()
3. Se token null → warn + próxima tentativa
4. Faz fetch com Authorization: Bearer <token>
5. Se ok → retorna Response
6. Se 401 ou 403 → warn + próxima tentativa
7. Qualquer outro erro (4xx ≠ 401/403, 5xx) → lança imediatamente
Após maxRetries: lança Error("fetchWithAuth: N tentativas esgotadas")Cancelamento via AbortController
O AbortSignal é respeitado antes de cada tentativa e durante o delay de retry. Tanto a espera quanto o fetch são canceláveis.
typescript
const controller = new AbortController()
const res = await fetchWithAuth(url, getToken, { signal: controller.signal })
controller.abort() // cancela em qualquer pontoconfig.ts
Arquivo: lib/config.ts
Configuração centralizada de URLs da API.
typescript
export const API_BASE_URL: string
// Resolução em ordem:
// 1. process.env.NEXT_PUBLIC_API_BASE_URL (injetado no build)
// 2. 'https://apimerxios.galdtech.com' (browser sem variável)
// 3. 'http://localhost:3001' (SSR/Node sem variável)
export function getApiUrl(path: string): string
// Garante que path começa com "/" e concatena com API_BASE_URL
// Exemplo: getApiUrl('campaigns') → 'https://apicrm.galdix.com.br/campaigns'NEXT_PUBLIC_API_BASE_URL é injetado no build da imagem Docker (não em runtime). Alterar em produção requer rebuild via bash deploy.sh frontend.
date-brt.ts
Arquivo: lib/date-brt.ts
Utilitários de data no fuso horário de Brasília (BRT = UTC-3, sem horário de verão desde 2019).
Motivação: Transações são armazenadas como timestamps UTC no banco e retornadas pelo backend como strings ISO (ex: "2026-05-05T18:58:57.000Z"). Extrair partes de data sem ajuste de fuso resulta em agrupamento errado para usuários em outros fusos ou para o próprio servidor.
typescript
const BRT_OFFSET_MS = 3 * 60 * 60 * 1000 // 10.800.000msFunções exportadas
| Função | Retorno | Descrição |
|---|---|---|
todayBRT() | string | Hoje como "YYYY-MM-DD" em BRT |
brtDateStr(d: Date) | string | "YYYY-MM-DD" de qualquer Date em BRT |
brtYear(d: Date) | number | Ano em BRT |
brtMonth(d: Date) | number | Mês 0-indexado em BRT |
brtDay(d: Date) | number | Dia do mês em BRT |
brtHour(d?: Date) | number | Hora 0-23 em BRT (default: now) |
brtQuarter(d: Date) | number | Trimestre 1-4 em BRT |
formatDateBRT(d, opts?) | string | Data formatada pt-BR com TZ BRT ("05/05/2026") |
formatTimeBRT(d, opts?) | string | Hora formatada pt-BR com TZ BRT ("15:58") |
shortDateBRT(d: Date) | string | "DD/MM/YY" para labels de gráficos |
brtMonthKey(d: Date) | string | "YYYY-MM" para agrupamento mensal |
brtMonthLabel(d: Date) | string | "mai. 2026" para labels de gráficos |
Implementação interna
Todas as funções usam toBRTWall(d) internamente:
typescript
function toBRTWall(d: Date): Date {
return new Date(d.getTime() - BRT_OFFSET_MS)
}
// Retorna um Date cujos campos UTC correspondem ao horário de parede em BRT
// Ex: 2026-05-05T21:58:57Z → toBRTWall → 2026-05-05T18:58:57 (UTC fields)
// getUTCFullYear() = 2026, getUTCMonth() = 4, getUTCDate() = 5formatDateBRT e formatTimeBRT usam Intl.DateTimeFormat com timeZone: 'America/Sao_Paulo' para garantir output correto no browser de qualquer usuário.
exportPdf.ts
Arquivo: lib/exportPdf.ts
Exporta um elemento DOM como PDF single-page sem cortes.
typescript
export async function exportPageAsPdf(
element: HTMLElement,
filename: string,
): Promise<void>Dependências: html2canvas e jspdf são carregadas dinamicamente (import()) para não aumentar o bundle inicial.
Fluxo de captura
1. Scroll element to viewport top (previne captura em branco)
2. Stamp dimensões dos Recharts ResponsiveContainers
(ResizeObserver não dispara no clone → containers colapsam a 0×0)
3. requestAnimationFrame + setTimeout 150ms (settle)
4. html2canvas(element, {
scale: 2, → alta resolução
backgroundColor: #fff, → sem transparência
width: scrollWidth, → área total, não só viewport
height: scrollHeight,
windowWidth: max(1280, scrollWidth), → força breakpoints lg: do Tailwind
})
5. onclone callback:
- Remove 'dark' do <html> → PDF sempre em modo claro
- Remove backdrop-blur (não suportado pelo html2canvas)
- Expande overflow-hidden / overflow-x-auto (cards não são cortados)
- Expande truncate e line-clamp (texto completo visível)
- Mantém overflow do recharts-wrapper intacto (clip-paths dos SVGs)
6. Cria jsPDF com altura customizada = canvas.height / pxPerMm
(single-page: nunca corta conteúdo entre páginas)
7. addImage como JPEG 92% de qualidade
8. pdf.save(`${filename}-YYYY-MM-DD.pdf`)
9. Restaura estilos do Recharts + posição de scrollpdf-hidden: elementos com essa classe são ignorados pelo html2canvas (ignoreElements callback).
utils.ts
Arquivo: lib/utils.ts
Utilitário de classes CSS.
typescript
import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]): stringCombina clsx (suporte a condicionais, arrays, objetos) com tailwind-merge (resolve conflitos de classes Tailwind). Padrão shadcn/ui.
typescript
// Exemplo:
cn('px-4 py-2', isActive && 'bg-purple-600', 'px-2')
// → 'py-2 bg-purple-600 px-2' (tailwind-merge remove o px-4 conflitante)