feat(frontend): app do tenant, dashboard ao vivo, fluxo de login multi-tenant

Corrige um gap real: o login sempre mandava pra /platform mesmo pra
usuarios sem role de plataforma, sem nunca chamar /auth/tenants ou
select-tenant. Agora POST /api/post-login decide o destino server-side
(plataforma / tenant unico / seletor com >1 tenant) antes de redirecionar,
trocando o access token quando necessario sem nunca expor token ao client.

Adiciona GET /reports/dashboard (secao 162) e a tela /app correspondente
— chamadas/agentes/TME/TMA/rates ao vivo, "consumo do plano" e "valor
estimado" seguindo a mesma disciplina de honestidade (null > numero
inventado) do dashboard de plataforma.

Shell (sidebar/topbar/drawer mobile) extraido pra components/shell,
compartilhado entre os menus Platform e Tenant (secao 168-169) via
wrappers client-only por area — corrige de quebra um erro real de
serializacao RSC (passar NavSection[] com icones como prop de Server
Component pra Client Component quebra: "Functions cannot be passed
directly to Client Components").

Testado ponta a ponta com um tenant semeado (Acme Call Center): login →
/app direto, platform admin barrado de /app e vice-versa, dashboard com
numeros reais (zeros honestos), drawer mobile, dark mode.

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 09:30:16 -03:00
parent aad16d136e
commit 565110eac3
28 changed files with 662 additions and 67 deletions

65
TODO.md
View File

@@ -711,7 +711,70 @@ docs/BILLING.md
`useState`/`useMemo` puro, sem cache de query; suficiente pro
tamanho de catálogo atual
## PHASE 24+ver `agente.md` seções 140 em diante (resto do Frontend,
## PHASE 24 — Frontend: shell compartilhado, fluxo de login multi-tenant,
app do Tenant + Dashboard (agente.md secao 162, 169)
- [x] **Bug real, achado nesta fase**: o fluxo de login sempre mandava
pra `/platform`, mesmo pra um usuário que não é platform admin —
nunca chamava `/auth/tenants`/`select-tenant`. Corrigido com
`POST /api/post-login` (Route Handler server-side): decide
platform vs tenant, e no caso de 1 tenant só já troca o access
token (`select-tenant`) antes de redirecionar — o client nunca vê
token nenhum. Mais de 1 tenant vai pra `/select-tenant` (nova
página, `TenantPicker` + Server Action `chooseTenant`).
- [x] `GET /auth/me` agora inclui `tenant: {id, code, name} | null`
(nome do tenant ativo do JWT) — precisa pra topbar/sidebar do app
do tenant mostrarem de quem é a conta.
- [x] `apps/platform/layout.tsx` e `apps/app/layout.tsx` (novo) se
protegem mutuamente: platform admin sem tenant ativo cai em
`/platform`; qualquer um tentando `/platform` sem
`isPlatformUser` cai em `/app`; sem tenant ativo nenhum e sem ser
platform admin cai em `/select-tenant`.
- [x] Shell refatorado pra ser compartilhado (`components/shell/`:
`Sidebar`/`Topbar`/`MobileNavDrawer`/`NavList`/`getPageMeta`,
parametrizados por `items: NavSection[]`) — o menu Platform
(secao 168) e o menu Tenant (secao 169, `tenant-shell/nav-data.ts`)
usam a mesma implementação, só os dados mudam.
- [x] **Bug real, achado nesta fase**: passar `PLATFORM_NAV`/`TENANT_NAV`
(array com `icon: LucideIcon`, ou seja funções) como prop de um
Server Component (`layout.tsx`) pra um Client Component
(`Sidebar`/`Topbar`) quebra a serialização RSC ("Functions cannot
be passed directly to Client Components"). Corrigido com
wrappers client-only por área (`platform-sidebar.tsx`/
`platform-topbar.tsx`/`tenant-sidebar.tsx`/`tenant-topbar.tsx`)
que importam o nav-data direto (nunca recebem como prop vindo do
server) — só dados serializáveis (strings) cruzam a fronteira.
- [x] `GET /reports/dashboard` (novo, agente.md secao 162): leitura ao
vivo (não aceita `from`/`to` como o resto de `ReportsController`)
— chamadas hoje/atendidas/em andamento/esperando agente, agentes
por estado (disponível/ocupado/pausado via `AgentState`), TME/TMA/
answer rate/abandon rate do dia, "Consumo do plano" (chamadas hoje
vs `Plan.maxDailyCalls` — decisão desta implementação, a
especificação não define o que "consumo" significa aqui) e "Valor
estimado no mês" (null até existir `BillingStatement` do mês
corrente — mesma disciplina de honestidade do
`PlatformOverviewController`, nunca um valor calculado ad hoc fora
do RatingEngine).
- [x] Dashboard do tenant (`/app`) consumindo esse endpoint, mesmo
`InstrumentTile`/ghost-dash da PHASE 23.
- [x] Testado ponta a ponta com um tenant real semeado
(`admin@acme.b2bcall.local`, tenant "Acme Call Center", plano
trial): login → `/app` direto (1 tenant só); platform admin
continua indo pra `/platform` (regressão confirmada); tenant admin
tentando `/platform` é mandado de volta pra `/app`; dashboard
mostra zeros reais (nenhuma chamada/agente ainda) com os textos
`pending` corretos, "Consumo do plano" mostra `0 de 200 hoje`
batendo com `maxDailyCalls` do plano trial; drawer mobile abre e
fecha corretamente; dark mode conferido. Screenshots em
`apps/frontend/.impeccable/review/`.
- [ ] Menu Tenant inteiro (Discador/Call Center/Telefonia/Monitoramento/
Gravações/IA/Relatórios/Administração) ainda "em breve" — próxima
fase natural é Telefonia > Ramais (CRUD mais simples já testado
ponta a ponta desde a PHASE 08, mesmo padrão de tabela da Tarifas)
- [ ] "Itens sem permissão não aparecem" (secao 169) não é aplicado
ainda — a IA do tenant aparece inteira pra qualquer usuário
autenticado do tenant, independente das permissions reais dele
## PHASE 25+ — ver `agente.md` seções 140 em diante (resto do Frontend,
Security, Tests)
---