feat(frontend): Relatórios > Chamadas + Gravações (proxy de áudio autenticado)

Relatórios > Chamadas: lista das últimas 500 chamadas dos últimos 30 dias,
nomes de fila/agente/campanha/disposição resolvidos client-side, busca por
telefone. Só o filtro de telefone nesta primeira versão.

Gravações: lista + player + download. Achado de arquitetura resolvido
antes de codar: <audio src>/<a download> não mandam Authorization Bearer
(só cookie), e a API nunca expõe o storage por URL direta — criado um
proxy autenticado (Route Handler /api/recordings/[id]/audio) que lê o
cookie de sessão, chama a API real com o access token do lado do servidor,
e reencaminha o stream com Content-Disposition: inline (a API manda
attachment). Mesmo princípio de apiFetch: token nunca chega em JS legível.

Testado ponta a ponta contra a API real (estados vazios honestos, proxy
confirmado 401 sem sessão). Smoke test de regressão nas 15 telas
anteriores do tenant + platform, todas 200.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
This commit is contained in:
2026-08-29 16:20:01 -03:00
parent f1cadddc91
commit 4d112fcea0
12 changed files with 366 additions and 2 deletions

View File

@@ -37,6 +37,16 @@ export function formatDate(iso: string): string {
return new Intl.DateTimeFormat("pt-BR", { day: "2-digit", month: "short", year: "numeric" }).format(new Date(iso));
}
export function formatDateTime(iso: string): string {
return new Intl.DateTimeFormat("pt-BR", {
day: "2-digit",
month: "short",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
}).format(new Date(iso));
}
/** agente.md secao 128 — catálogo de preço, mesma nomenclatura do
* `PriceItemType` do backend (packages/database/prisma/schema.prisma). */
export const PRICE_ITEM_TYPE_LABELS: Record<string, string> = {

View File

@@ -0,0 +1,11 @@
export interface Recording {
id: string;
callId: string;
format: string;
durationSeconds: number | null;
channels: number;
sizeBytes: number | null;
recordedAt: string;
retentionUntil: string | null;
status: string;
}

View File

@@ -43,6 +43,29 @@ export interface CampaignReport {
iaValor: number | null;
}
export interface Call {
id: string;
direction: "INBOUND" | "OUTBOUND" | "INTERNAL";
callerNumber: string | null;
calledNumber: string | null;
queueId: string | null;
agentId: string | null;
campaignId: string | null;
hangupCause: string | null;
dispositionId: string | null;
waitTime: number | null;
talkTime: number | null;
durationSeconds: number | null;
createdAt: string;
endAt: string | null;
}
export const CALL_DIRECTION_LABELS: Record<string, string> = {
INBOUND: "Entrada",
OUTBOUND: "Saída",
INTERNAL: "Interna",
};
export interface AIDashboardReport {
callsAnalyzed: number;
avgQualityScore: number | null;