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
79 lines
2.9 KiB
TypeScript
79 lines
2.9 KiB
TypeScript
export function formatInt(n: number): string {
|
|
return new Intl.NumberFormat("pt-BR").format(Math.round(n));
|
|
}
|
|
|
|
/** mm:ss a partir de segundos — usado por TME/TMA (agente.md secao 162). */
|
|
export function formatDuration(seconds: number): string {
|
|
const total = Math.round(seconds);
|
|
const mm = Math.floor(total / 60);
|
|
const ss = total % 60;
|
|
return `${mm}:${String(ss).padStart(2, "0")}`;
|
|
}
|
|
|
|
export function formatPercent(ratio: number): string {
|
|
return new Intl.NumberFormat("pt-BR", { style: "percent", maximumFractionDigits: 1 }).format(ratio);
|
|
}
|
|
|
|
export function formatBytes(bytes: number): { value: string; unit: string } {
|
|
if (bytes === 0) return { value: "0", unit: "B" };
|
|
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
const exp = Math.min(Math.floor(Math.log(bytes) / Math.log(1000)), units.length - 1);
|
|
const value = bytes / 1000 ** exp;
|
|
return { value: value.toFixed(exp === 0 ? 0 : 1), unit: units[exp] };
|
|
}
|
|
|
|
export const AI_USAGE_LABELS: Record<string, string> = {
|
|
AI_TRANSCRIPTION_SECONDS: "Transcrição (s)",
|
|
AI_ANALYSIS_REQUEST: "Análises",
|
|
AI_INPUT_TOKENS: "Tokens de entrada",
|
|
AI_OUTPUT_TOKENS: "Tokens de saída",
|
|
};
|
|
|
|
export function formatCurrency(value: number, currency = "BRL"): string {
|
|
return new Intl.NumberFormat("pt-BR", { style: "currency", currency, maximumFractionDigits: 6 }).format(value);
|
|
}
|
|
|
|
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> = {
|
|
BASE_SUBSCRIPTION: "Assinatura base",
|
|
EXTENSION_MONTH: "Ramal / mês",
|
|
AGENT_MONTH: "Agente / mês",
|
|
TRUNK_MONTH: "Tronco / mês",
|
|
CALL: "Chamada (fixo)",
|
|
CALL_MINUTE: "Minuto de chamada (padrão)",
|
|
FIXED_MINUTE: "Minuto — fixo",
|
|
MOBILE_MINUTE: "Minuto — móvel",
|
|
INTERNATIONAL_MINUTE: "Minuto — internacional",
|
|
AI_TRANSCRIPTION_MINUTE: "IA — transcrição / minuto",
|
|
AI_ANALYSIS_CALL: "IA — análise / chamada",
|
|
AI_INPUT_TOKEN: "IA — token de entrada",
|
|
AI_OUTPUT_TOKEN: "IA — token de saída",
|
|
RECORDING_GB_MONTH: "Armazenamento / GB / mês",
|
|
};
|
|
|
|
export const PRICE_ITEM_TYPES = Object.keys(PRICE_ITEM_TYPE_LABELS) as Array<keyof typeof PRICE_ITEM_TYPE_LABELS>;
|
|
|
|
/** agente.md secao 129 — tipos de destino de um RateDeckEntry. */
|
|
export const DESTINATION_TYPE_LABELS: Record<string, string> = {
|
|
FIXED: "Fixo",
|
|
MOBILE: "Móvel",
|
|
INTERNATIONAL: "Internacional",
|
|
};
|
|
|
|
export const DESTINATION_TYPES = Object.keys(DESTINATION_TYPE_LABELS) as Array<keyof typeof DESTINATION_TYPE_LABELS>;
|