feat(frontend): app shell + login + platform dashboard + billing tarifas

Primeiro commit do frontend Next.js (agente.md secao 161-176): login
split-brand, dashboard "Visão Geral da Plataforma" com instrumentos ao
vivo, e a tela Billing > Tarifas (price books + rate decks) completa —
listagem com busca/ordenacao, criacao com itens/entradas dinamicos via
Server Actions, detalhe — ponta a ponta contra a API real de billing
(fase 22).

Corrige de quebra 2 bugs reais achados construindo Tarifas: a topbar
tinha o titulo fixo "Visao Geral" em toda pagina, e a sidebar fixa de
256px nao tinha nenhuma versao mobile (conteudo espremido em ~130px) —
agora vira drawer via Radix Dialog abaixo de lg, com titulo/descricao da
topbar resolvidos dinamicamente por rota.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWHKmcVJtstQFErbZ1AanY
This commit is contained in:
2026-08-29 00:54:29 -03:00
parent b27cfaab02
commit 673553975e
55 changed files with 2504 additions and 0 deletions

View File

@@ -0,0 +1,56 @@
export function formatInt(n: number): string {
return new Intl.NumberFormat("pt-BR").format(Math.round(n));
}
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));
}
/** 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>;