Skip to content

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 ponto

config.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.000ms

Funções exportadas

FunçãoRetornoDescrição
todayBRT()stringHoje como "YYYY-MM-DD" em BRT
brtDateStr(d: Date)string"YYYY-MM-DD" de qualquer Date em BRT
brtYear(d: Date)numberAno em BRT
brtMonth(d: Date)numberMês 0-indexado em BRT
brtDay(d: Date)numberDia do mês em BRT
brtHour(d?: Date)numberHora 0-23 em BRT (default: now)
brtQuarter(d: Date)numberTrimestre 1-4 em BRT
formatDateBRT(d, opts?)stringData formatada pt-BR com TZ BRT ("05/05/2026")
formatTimeBRT(d, opts?)stringHora 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() = 5

formatDateBRT 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 scroll

pdf-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[]): string

Combina 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)

Documentação interna — Galdix CRM