Adiciona o frontend Next.js em frontend/ (setup, design system, autenticação/tema, layout/navegação, área master, área tenant completa: clientes, produtos, vendas, recebimentos, whatsapp, e-mail, alertas, equipe e perfil). Estende o backend de forma aditiva e autorizada: cookie httpOnly para refresh token (Etapa 13) e CRUD de equipe + edição de perfil do tenant (Etapa 18, módulo src/modules/equipe/ novo e PUT /tenant/perfil). Detalhes completos em Etapas/V2/*. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1031 lines
63 KiB
Markdown
1031 lines
63 KiB
Markdown
# SeConectai Vendas — Controle de Progresso do Frontend
|
||
|
||
Este arquivo é o **estado oficial do frontend**. Segue exatamente a
|
||
mesma lógica do `09-controle-de-progresso.md` (backend), mas aplicado
|
||
às etapas 11–19.
|
||
|
||
## Instruções obrigatórias para a IA
|
||
|
||
1. **Antes de iniciar qualquer etapa**, peça este arquivo e leia a
|
||
seção "Status Geral" para confirmar que a etapa anterior está 100%
|
||
concluída.
|
||
2. **Não inicie uma etapa** com critério de aceite pendente na etapa
|
||
anterior. Avise o usuário e pergunte se deve terminar o que falta
|
||
antes de avançar.
|
||
3. **Ao concluir uma etapa**, marque com `[x]` cada item realmente
|
||
implementado e testado, atualize "Status Geral" e preencha
|
||
"Observações" com decisões relevantes para as próximas etapas.
|
||
4. **Devolva este arquivo atualizado** ao usuário ao final de cada
|
||
etapa.
|
||
5. Esta etapa numerada 11–19 pressupõe que o **backend já está
|
||
funcionando** conforme `09-controle-de-progresso.md`. Se o backend
|
||
ainda não estiver pronto, o frontend pode ser construído contra uma
|
||
API mockada, mas isso deve ficar registrado em "Observações" da
|
||
Etapa 11.
|
||
|
||
## Status Geral
|
||
|
||
| # | Etapa | Status | Concluída em |
|
||
|---|-------|--------|---------------|
|
||
| 11 | Setup do Projeto Frontend | Concluída | 2026-08-15 |
|
||
| 12 | Sistema de Design e Componentes Base | Concluída | 2026-08-15 |
|
||
| 13 | Autenticação e Tema Dinâmico | Concluída | 2026-08-15 |
|
||
| 14 | Layout e Navegação | Concluída | 2026-08-15 |
|
||
| 15 | Área Master — Telas de Negócio | Concluída | 2026-08-15 |
|
||
| 16 | Área Tenant — Dashboard, Clientes e Produtos | Concluída | 2026-08-15 |
|
||
| 17 | Área Tenant — Vendas e Financeiro | Concluída | 2026-08-15 |
|
||
| 18 | Área Tenant — Integrações e Perfil | Concluída | 2026-08-16 |
|
||
| 19 | Polimento, Acessibilidade e Testes E2E | [ ] Não iniciada | |
|
||
|
||
Valores possíveis para "Status": `Não iniciada`, `Em andamento`,
|
||
`Concluída`, `Concluída com pendências`.
|
||
|
||
---
|
||
|
||
## Etapa 11 — Setup do Projeto Frontend
|
||
|
||
- [x] `npm run dev` sobe o projeto sem erro
|
||
- [x] Todas as rotas do mapa de rotas resolvem, com placeholder, sem 404
|
||
- [x] `tokens.css` contém todas as variáveis do schema, aplicadas corretamente
|
||
- [x] Classes utilitárias do Tailwind funcionam e refletem os tokens
|
||
- [x] `/diagnostico` mostra o retorno real de `GET /health` do backend
|
||
- [x] `npm run lint` roda sem erro
|
||
- [x] Estrutura de pastas idêntica à definida no schema de frontend
|
||
|
||
**Observações:**
|
||
|
||
- Projeto criado em `frontend/` (subpasta deste mesmo repositório, não um
|
||
repo separado). Next.js 15.5.23 + React 19.1 + TypeScript, pinado
|
||
deliberadamente em vez do Next.js 16 (que o `create-next-app@latest`
|
||
instala por padrão hoje): o próprio scaffold do Next 16 gera um
|
||
`AGENTS.md` avisando que a versão tem mudanças de API/estrutura ainda
|
||
não documentadas de forma estável — risco desnecessário para uma build
|
||
de 9 etapas. Next 15 já satisfaz o requisito "Next.js 14+" da etapa.
|
||
- Tailwind vem em v4 (`@tailwindcss/postcss`), que já era o padrão do
|
||
`create-next-app` mesmo pinando `next@15`. Diferença relevante para as
|
||
próximas etapas: **não existe `tailwind.config.ts`** — os tokens em
|
||
`styles/tokens.css` são expostos como utilities (`bg-primary`,
|
||
`text-danger` etc) via bloco `@theme inline` (CSS-first, forma
|
||
idiomática do Tailwind v4), não via `theme.extend.colors` em JS. Isso
|
||
cumpre o critério de aceite (utilities funcionando e refletindo os
|
||
tokens) sem precisar de um arquivo de config duplicando os mesmos
|
||
valores. A Etapa 12 (shadcn/ui) deve considerar isso — versões
|
||
recentes do shadcn CLI já suportam Tailwind v4 CSS-first.
|
||
- Fonte: Inter via `next/font/google`, exposta como `--font-inter` e
|
||
mapeada para `font-sans` em `tokens.css`.
|
||
- `lib/api/client.ts`: wrapper sobre `fetch` nativo (não axios — não há
|
||
necessidade de interceptors globais além do gancho de refresh, que o
|
||
próprio client já expõe via `authInterceptor`, preenchido na Etapa 13).
|
||
Já usa `credentials: 'include'` para o cookie `httpOnly` de refresh
|
||
token que a Etapa 13 vai configurar no backend.
|
||
- Husky + lint-staged: como o `.git` fica na raiz do repo (acima de
|
||
`frontend/`) e o backend não usa Husky, o hook fica em `.husky/`
|
||
**na raiz do repo** (`git config core.hooksPath .husky`, já aplicado),
|
||
com `pre-commit` fazendo `cd frontend && npx lint-staged` — isso evita
|
||
interferir no fluxo de commit do backend. Não usei `npx husky init`
|
||
(assume que o hook mora ao lado do `package.json`, o que não é o caso
|
||
aqui) — o hook foi criado manualmente.
|
||
- Conflito descoberto e corrigido: o `tsconfig.json`/`tsconfig.build.json`
|
||
do **backend** (raiz do repo) não tinham `exclude`, então o `nest start
|
||
--watch` tentava compilar os `.tsx` de `frontend/` e quebrava com "Cannot
|
||
use JSX unless the '--jsx' flag is provided". Adicionado `"frontend"` ao
|
||
`exclude` de ambos os tsconfig do backend, e ao `ignores` do
|
||
`eslint.config.mjs` do backend. Necessário para qualquer etapa futura
|
||
que rode `docker compose up` com as duas pastas coexistindo.
|
||
- `.gitignore` gerado pelo `create-next-app` ignorava `.env*` (incluindo
|
||
`.env.local.example`, que precisa ser versionado) — ajustado para
|
||
`.env*` + `!.env*.example`.
|
||
- Verificação ponta a ponta feita com o backend real via
|
||
`docker compose up` (app+worker+postgres+redis) e o frontend em
|
||
`npm run dev` (porta 3001 — 3000 já ocupada pelo backend;
|
||
`CORS_ORIGINS` do `.env` do backend já incluía `localhost:3001`).
|
||
`/diagnostico` confirmado funcional via `curl` com header `Origin`
|
||
(retornou `Access-Control-Allow-Origin: http://localhost:3001` e o JSON
|
||
real do `/health`) — não há navegador gráfico disponível neste
|
||
ambiente para captura visual, então a verificação final "no navegador"
|
||
fica pendente para quem tiver acesso a um browser local.
|
||
- `npm audit` acusa 3 vulnerabilidades "high" em `postcss`/`sharp`
|
||
(dependências transitivas do `next@15.5.23`), só corrigidas subindo
|
||
para `next@16` (breaking, descartado acima). Reavaliar quando o Next 16
|
||
estabilizar.
|
||
|
||
---
|
||
|
||
## Etapa 12 — Sistema de Design e Componentes Base
|
||
|
||
- [x] Todos os componentes listados existem, tipados, com as variantes descritas
|
||
- [x] `Table` alterna corretamente entre tabela e cards empilhados
|
||
- [x] `Modal/Sheet` alterna corretamente entre modal e tela cheia/bottom sheet
|
||
- [x] `StatusPill` cobre todos os status de negócio, com cor e label em português
|
||
- [x] Navegação por teclado percorre todos os componentes com foco visível
|
||
- [x] Nenhum componente quebra em 375px
|
||
- [x] Página de showcase acessível apenas em desenvolvimento
|
||
|
||
**Observações:**
|
||
|
||
- shadcn/ui inicializado com **Radix UI** como base de primitivas (a CLI
|
||
atual oferece Base UI/React Aria/Radix — escolhido Radix por ser
|
||
exatamente o que a Etapa 12 cita nominalmente e o que tenho mais
|
||
domínio para customizar com segurança). Preset visual "Nova" usado só
|
||
como ponto de partida — todas as cores foram remapeadas para os tokens
|
||
do projeto (ver abaixo), fonte continua Inter (não Geist, que o preset
|
||
tentou injetar; removido).
|
||
- **Bug real encontrado e corrigido**, não cosmético: `styles/tokens.css`
|
||
era importado no `layout.tsx` **antes** de `globals.css`. Como
|
||
`tokens.css` tem seu próprio bloco `@layer utilities { .tabular-nums }`,
|
||
isso registrava a layer `utilities` no CSS *antes* de `@import
|
||
'tailwindcss'` (dentro de `globals.css`) declarar a ordem canônica
|
||
`theme, base, components, utilities`. Resultado: a ordem das cascade
|
||
layers ficava invertida — `utilities` virava a de **menor** prioridade
|
||
do documento inteiro, então qualquer classe utilitária (`px-3`, `py-2`
|
||
etc) perdia silenciosamente para regras da layer `base` em conflito.
|
||
Sintoma visível: padding de `TableCell`/`TableHead` renderizando como
|
||
`0px` mesmo com a classe certa aplicada no DOM. Corrigido movendo o
|
||
`@import '../styles/tokens.css'` para dentro do próprio `globals.css`
|
||
(logo após `@import 'tailwindcss'`), removendo o import solto do
|
||
`layout.tsx`. Isso também corrigiu de quebra um problema correlato:
|
||
`--color-neutral-*` dos nossos tokens estava perdendo para a paleta
|
||
cinza *default* do Tailwind (mesmo nome de variável, tema padrão
|
||
vencendo por causa da mesma inversão de ordem). **Vale a pena qualquer
|
||
IA futura desconfiar de "build passou mas o layout não bate" — nem
|
||
sempre é a classe errada, pode ser ordem de import de CSS.**
|
||
- Detectado só porque o build de produção e o `tsc`/`eslint` passam limpo
|
||
mesmo com esse bug (é um erro de cascata, não de sintaxe/tipo) — só
|
||
apareceu medindo `getComputedStyle` real no navegador headless. Reforça
|
||
por que essa etapa foi validada com Playwright real, não só build.
|
||
- Verificação: Playwright headless (imagem oficial
|
||
`mcr.microsoft.com/playwright`, já que Alpine/musl não roda o Chromium
|
||
do Playwright) nos três breakpoints — 0 overflow horizontal, screenshots
|
||
conferidas visualmente. Tab por teclado percorrido 15 vezes: todo
|
||
elemento focável tem `box-shadow` de foco visível (ring), nenhum
|
||
`outline: none` sem substituto.
|
||
- Touch target 44px: `Button` (`md`/`lg`) e `Input`/`Select`/`Textarea`
|
||
agora têm `h-11` (44px) por padrão — o `sm` do `Button` continua abaixo
|
||
de 44px **de propósito**, documentado no código como reservado para
|
||
contexto denso (ex: ação "Ver" na linha da `Table`), nunca a única forma
|
||
de disparar uma ação. Corrigidos durante a auditoria: botão "Ver mais"
|
||
dos cards mobile da `Table` (estava com 20px de altura) e os
|
||
`TabsTrigger` (estavam fixos em 32px, sem variante de tamanho — agora
|
||
44px). Grade de dias do `Calendar` ficou pequena de propósito (~28px):
|
||
é a exceção padrão do setor para grids de data-picker densos, aumentar
|
||
quebraria o calendário inteiro em mobile.
|
||
- `Pagination` gerada pelo shadcn (baseada em `<a>`) foi **removida** —
|
||
construí `DataPagination` própria (botões controlados, compatível com
|
||
`page`/`limit`/`meta.total` que o backend já retorna).
|
||
- `Modal/Sheet`: decisão tomada por bottom sheet no mobile (não tela
|
||
cheia) — a maioria dos formulários do projeto é curta o bastante para
|
||
caber sem ocupar a tela inteira; `max-h-[85vh]` com scroll interno
|
||
cobre os casos mais longos (documentado em `responsive-dialog.tsx`).
|
||
- `Select` com busca implementado como `Combobox` separado (Command +
|
||
Popover), já que o `Select` nativo do Radix não suporta busca — o
|
||
`Select` padrão continua disponível para listas curtas (ex: papel de
|
||
usuário).
|
||
- Toast usa `sonner` (padrão atual do shadcn, substituiu o componente
|
||
`Toast` clássico) — `success`/`error`/`warning`/`info` nativos,
|
||
empilhável, auto-dismiss configurável. Reexportado em `lib/toast.ts`.
|
||
- Dark mode **não** foi implementado (fora de escopo — não citado em
|
||
nenhum lugar do schema de frontend). O bloco `.dark { ... }` que o
|
||
shadcn gera foi removido do `globals.css`; classes `dark:` que
|
||
aparecerem em componentes shadcn adicionados em etapas futuras são
|
||
inofensivas (ficam inertes sem a classe `.dark` no `<html>`).
|
||
|
||
---
|
||
|
||
## Etapa 13 — Autenticação e Tema Dinâmico
|
||
|
||
- [x] Login master e tenant funcionam ponta a ponta
|
||
- [x] Sessão expira e renova automaticamente via refresh
|
||
- [x] Logout limpa sessão e revoga refresh token no backend
|
||
- [x] Middleware bloqueia acesso não autenticado a `/master/*` e `/app/*`
|
||
- [x] Master não acessa `/app/*` e vice-versa sem redirecionamento
|
||
- [x] Fluxo completo de esqueci/redefinir senha funciona
|
||
- [x] Login aplica cor/logo do tenant certo; fallback para tema padrão
|
||
- [x] Cor de baixo contraste ajustada automaticamente
|
||
- [x] Telas testadas em 375px, 768px, 1280px
|
||
|
||
**Observações:**
|
||
|
||
- **Mudança no backend (Etapa 2, já "concluída")**, com autorização
|
||
explícita do usuário antes de fazer: o backend devolvia o refresh token
|
||
só no corpo JSON, nunca em cookie — contrariava o requisito do schema
|
||
("refresh token em cookie httpOnly"). Adicionado suporte a cookie
|
||
httpOnly de forma aditiva (não quebra os clientes existentes):
|
||
- `cookie-parser` instalado; `security.util.ts` ganhou
|
||
`setRefreshTokenCookie`/`clearRefreshTokenCookie`
|
||
(`httpOnly, secure: true, sameSite: 'none', path: '/auth'` — `Secure`
|
||
funciona em `localhost` por exceção do browser; qualquer deploy real
|
||
precisa estar atrás de HTTPS) e `app.enableCors({ credentials: true })`.
|
||
- `POST /auth/master/login`, `/auth/tenant/login`, `/auth/refresh`
|
||
setam o cookie; `/auth/refresh` e `/auth/logout` agora aceitam
|
||
`refresh_token` no corpo **ou** no cookie (corpo tem prioridade,
|
||
então os testes e2e antigos continuam passando sem alteração) —
|
||
`RefreshDto`/`LogoutDto.refresh_token` viraram opcionais.
|
||
- 4 testes novos em `test/auth.e2e-spec.ts` cobrindo o cookie
|
||
(Set-Cookie no login, refresh só com cookie, logout limpa o cookie).
|
||
Suíte completa rodada: 24/24 em `auth.e2e-spec.ts`, 133/136 no total
|
||
dos e2e (ver nota de teste pré-existente abaixo), lint/tsc limpos.
|
||
- **Gotcha de infra**: `docker-compose.override.yml` monta
|
||
`/app/node_modules` como volume anônimo — depois de adicionar uma
|
||
dependência nova (`cookie-parser`), `docker compose up --build`
|
||
sozinho **não** basta, o volume anônimo antigo (sem a dependência)
|
||
sobrevive à recriação do container e esconde o `node_modules` da
|
||
imagem nova. Precisa de `--renew-anon-volumes` (`-V`). Vale para
|
||
qualquer etapa futura que adicione dependência ao backend.
|
||
- **Nota — achado incidental, não corrigido (fora do escopo desta
|
||
etapa)**: rodando a suíte e2e completa, `test/alertas.e2e-spec.ts`
|
||
tem 3 testes falhando por timeout do Jest (5000ms padrão), não
|
||
relacionados a nada desta etapa. Causa raiz aparente: o helper
|
||
`aguardarFilasOciosas` do próprio arquivo já assume até 15000ms de
|
||
espera (retry/backoff de job BullMQ), mais que o timeout padrão do
|
||
Jest — só os testes que exercitam o caminho de falha/retry (que
|
||
demoram mais) estouram. Fica registrado para quem for revisar a
|
||
Etapa 7.
|
||
- **Estratégia de login master/tenant**: toggle explícito ("Sou cliente"
|
||
/ "Sou administrador") na tela de login e em "esqueci minha senha",
|
||
não detecção automática — o backend expõe dois endpoints separados
|
||
(`/auth/master/login` e `/auth/tenant/login`) sem nenhum jeito de
|
||
descobrir o tipo a partir só do e-mail sem tentar os dois (o que
|
||
duplicaria contagem de tentativas de força bruta). "Cliente" é o
|
||
padrão selecionado.
|
||
- **Sinalização de sessão para o middleware**: o cookie httpOnly de
|
||
refresh token pertence ao **backend** (`Path=/auth`, origem/porta
|
||
diferente do frontend) — o middleware do Next.js não tem acesso a ele,
|
||
nem deveria. Criado um segundo cookie, `sc_session` (não sensível, só
|
||
`master`/`tenant`), setado pelo próprio frontend via `document.cookie`
|
||
depois de um login bem-sucedido (`lib/auth/session-cookie.ts`). É uma
|
||
checagem otimista (evita "flash" de conteúdo protegido), não uma
|
||
fronteira de segurança — a autorização real acontece no backend, em
|
||
toda chamada de API, via o access token de verdade.
|
||
- **Identificação do tenant antes do login**: o schema não tem
|
||
`slug`/subdomínio (confirmado no backend), e não existe endpoint de
|
||
"descobrir tenant pelo e-mail". Então a resolução de tema pré-login só
|
||
funciona via `?tenant=<uuid>` explícito na URL de `/login` — sem esse
|
||
parâmetro, cai no tema padrão da plataforma (comportamento já previsto
|
||
pelo próprio schema como fallback). Pós-login, o tema é resolvido a
|
||
partir do `tenant_id` decodificado do JWT.
|
||
- **Ajuste de contraste**: `--color-primary` nunca é alterada (fundos
|
||
continuam com a cor exata do tenant); criado um token novo,
|
||
`--color-primary-on-light` (`lib/theme/contrast.ts` +
|
||
`text-primary-on-light`), só para texto/ícone sobre fundo claro.
|
||
Testado de ponta a ponta com uma cor de tenant propositalmente clara
|
||
(`#FCD34D`) → resolveu para `#8b742a` automaticamente.
|
||
- Access token confirmado em memória (`useRef` dentro do `AuthProvider`)
|
||
via Playwright: `localStorage`/`sessionStorage` vazios depois do login.
|
||
- `lib/api/client.ts` ganhou `fetchRefreshToken()`, deliberadamente **fora**
|
||
do `request()`/`authInterceptor` — a chamada de refresh não pode passar
|
||
pelo mesmo interceptor que ela alimenta, ou um 401 nela mesma causaria
|
||
recursão infinita.
|
||
- Sem botão de logout na UI ainda (a função `logout()` existe e funciona,
|
||
testada isoladamente) — o gatilho visual entra na Etapa 14 junto com
|
||
sidebar/topbar.
|
||
|
||
---
|
||
|
||
## Etapa 14 — Layout e Navegação
|
||
|
||
- [x] Layout master com sidebar/topbar (desktop) e drawer+bottom-nav (mobile)
|
||
- [x] Layout tenant com cor/logo do tenant aplicada corretamente
|
||
- [x] "Equipe" só visível para `tenant_admin`
|
||
- [x] Navegação destaca item ativo em todos os formatos (desktop/drawer/bottom-nav)
|
||
- [x] Drawer fecha ao selecionar item ou tocar fora
|
||
- [x] Bottom tab bar com 4 itens + "Mais"
|
||
- [x] Sidebar colapsável no `md`, preferência persistida
|
||
- [x] Todas as rotas dentro do layout correto
|
||
- [x] Testado em 375px, 768px, 1280px
|
||
|
||
**Observações:**
|
||
|
||
- Componentes novos em `components/layout/`: `Sidebar`, `Topbar`, `Drawer`,
|
||
`BottomTabBar`, `Breadcrumb`, `NavList` (lista de navegação com estado de
|
||
item/grupo ativo, compartilhada entre `Sidebar` e `Drawer` para não
|
||
duplicar essa lógica), `AppShell` (orquestra os cinco acima — composto uma
|
||
vez e reaproveitado pelos dois shells, em vez de duplicar toda a
|
||
montagem), `UserFooter` (avatar+identidade+logout do rodapé) e os shells
|
||
finais `MasterShell`/`TenantShell` (client components que buscam
|
||
`useAuth()`/tema e montam os itens de menu certos). Mapa de itens em
|
||
`lib/layout/nav-items.ts` (`MASTER_NAV_ITEMS`/`TENANT_NAV_ITEMS` +
|
||
`*_BOTTOM_HREFS` + `filterByRole`). `app/master/layout.tsx`/`app/app/layout.tsx`
|
||
viraram server components (`async`, leem o cookie de sidebar via
|
||
`next/headers` e passam pro shell) — antes eram só placeholders.
|
||
- **Achado de segurança/consistência visual corrigido durante o teste manual
|
||
desta etapa:** o destaque de item ativo em `NavList`/`BottomTabBar` usa os
|
||
tokens `--color-primary`/`--color-primary-on-light` — dinâmicos por tenant
|
||
(Etapa 13, aplicados via `document.documentElement.style.setProperty` no
|
||
`:root`, global). Como o painel master tem identidade **fixa** (seção 6 da
|
||
arquitetura, `--brand-master`), usar os mesmos tokens no master faria a
|
||
cor de destaque do master mudar dependendo do que ficou setado no `:root`
|
||
por uma sessão de tenant anterior na mesma aba (o `TenantThemeProvider` só
|
||
reseta esses tokens quando `tenantId` é `null`, e o master nunca passa por
|
||
ele). Corrigido em `MasterShell`: um `<div style={{'--color-primary':
|
||
'var(--brand-master)', ...}}>` envolve o `<AppShell>`, sobrescrevendo os
|
||
tokens só para essa subárvore (não toca no `:root`) — `Sidebar`,
|
||
`Drawer`, `BottomTabBar` e o conteúdo herdam a cor certa sem precisar
|
||
saber se estão no master ou no tenant. Confirmado visualmente (ver
|
||
screenshots do teste manual abaixo) e via
|
||
`getComputedStyle(document.documentElement).getPropertyValue('--color-primary')`
|
||
continuando no valor padrão/tenant no `:root` (prova de que a sobrescrita
|
||
é local, não vazou).
|
||
- **Grupo expansível para "Alertas":** o prompt lista "Alertas
|
||
(Templates/Logs)" como um item de menu só, mas as rotas já existentes
|
||
desde a Etapa 11 são duas (`/master|app/alertas/templates` e `.../logs`),
|
||
sem página índice em `/alertas`. Modelado como `NavItem.children` (só
|
||
`href`+`label`, sem ícone próprio) — clique no item pai expande/recolhe a
|
||
lista com Templates/Logs; com a sidebar recolhida (ícones-only) ou no
|
||
bottom-tab, vira link direto pro primeiro filho (Templates). `href` do
|
||
item pai é `.../alertas/templates`, então a home de alertas nunca 404.
|
||
- **Decisão de comportamento do colapso da sidebar, documentada conforme
|
||
pedido na tarefa 4:** o colapso é **só manual** (botão na base da
|
||
sidebar), sem auto-colapso automático ao entrar no breakpoint `md`. A
|
||
sidebar aparece a partir de `md` (`hidden md:flex`) já no estado
|
||
persistido (cookie `sc_sidebar_collapsed`, lido no server component do
|
||
layout via `next/headers` antes do primeiro paint — sem "flash" de
|
||
sidebar expandida depois recolhendo). Escolhido em vez de um auto-colapso
|
||
heurístico por breakpoint por ser mais previsível e mais simples de
|
||
testar/explicar; o critério de aceite ("colapsável no breakpoint md, com
|
||
botão pra expandir/recolher manualmente também no desktop") permite as
|
||
duas leituras — essa ficou registrada aqui como a interpretação adotada.
|
||
- **Rodapé do usuário mostra o papel, não o nome:** o payload do access
|
||
token (`lib/auth/jwt.ts`, espelhando `src/modules/auth/types/jwt-payload.type.ts`
|
||
no backend) só carrega `sub`/`tipo`/`tenant_id`/`role` — nunca nome ou
|
||
e-mail. `UserFooter` (usado tanto no master quanto no tenant) mostra
|
||
"Administrador master" (fixo) ou o papel do `tenant_user` traduzido
|
||
(`Administrador`/`Vendedor`/`Financeiro`/`Visualizador`), com a inicial
|
||
dessa palavra como avatar — não um nome de pessoa. Trazer o nome/e-mail
|
||
reais exigiria um endpoint de perfil mais rico (a Etapa 18, que constrói
|
||
a tela `/app/perfil` de verdade, é o lugar natural pra decidir se isso
|
||
vale a pena).
|
||
- `TenantBrand` usa `next/image` pro logo do tenant (mesmo padrão já usado
|
||
em `app/(public)/login/login-form.tsx` na Etapa 13, `width`/`height`
|
||
fixos, sem `unoptimized`) — sem cair pra `<img>` puro, por consistência
|
||
com o resto do código já existente.
|
||
- Breadcrumb gerado automaticamente em `AppShell` (`findBreadcrumb`) a
|
||
partir do item de menu ativo (casa `pathname` contra `items`/`children`)
|
||
— não é por página manual como o prompt desta etapa cogitava como
|
||
alternativa aceitável; ficou simples o suficiente por já ter a árvore de
|
||
itens disponível. Fica limitado a no máximo 2 níveis (grupo → filho);
|
||
páginas de detalhe (`/app/clientes/[id]`, `/app/vendas/[id]` etc., que só
|
||
chegam nas Etapas 16/17) vão cair no mesmo breadcrumb da listagem-mãe até
|
||
que uma etapa futura estenda `findBreadcrumb` com um 3º nível — registrado
|
||
aqui como a limitação esperada da "primeira versão" que a tarefa 3 já
|
||
autorizava.
|
||
- **Teste manual desta etapa, já que não há Playwright configurado no
|
||
projeto ainda:** sem Node/npm no host (só Docker), rodei
|
||
`tsc --noEmit`/`eslint .`/`next build` via `docker run node:20-alpine`
|
||
(mesmo padrão das etapas de backend) — todos limpos. Para o teste visual
|
||
real, subi o `next dev` num container (`--network host`, porta 3001,
|
||
`NEXT_PUBLIC_API_URL` apontando pro backend já rodando via
|
||
`docker compose` na 3000) e dirigi um Chromium headless via Playwright
|
||
(instalado ad-hoc num container separado, sem ficar como dependência do
|
||
projeto) cobrindo: login master e tenant reais contra a API, screenshot em
|
||
375/768/1280px, abrir/fechar drawer mobile, expandir o grupo "Alertas",
|
||
colapsar a sidebar e confirmar que o item ativo e a preferência
|
||
sobrevivem a um reload de página. Também criei dados de teste no banco de
|
||
dev via API/SQL direto: um plano, um tenant novo ("Etapa14 Demo", cor
|
||
`#7C3AED`) com seu `tenant_admin` e um `tenant_user` role `vendedor`
|
||
adicional — confirmando visual e programaticamente que "Equipe" aparece
|
||
só pro admin. **Ação que precisa de atenção do usuário:** pra testar o
|
||
login master, sobrescrevi a senha do `master_admin` já existente
|
||
(`admin@seconectai.com.br`) sem guardar o hash anterior — a senha antiga
|
||
desse usuário foi perdida; se ela importava, é preciso redefini-la
|
||
manualmente (ex: direto no banco ou, se existir, via o fluxo de "esqueci
|
||
minha senha"). Os outros dados criados (`Teste Etapa14` em `planos`,
|
||
`Etapa14 Demo` em `tenants` + seus `tenant_users`) são só aditivos, não
|
||
destrutivos — posso removê-los se não forem úteis como dado de
|
||
demonstração pras próximas etapas.
|
||
|
||
## Etapa 15 — Área Master: Telas de Negócio
|
||
|
||
- [x] Todas as telas master funcionam ponta a ponta com o backend real
|
||
- [x] Dashboard com números corretos
|
||
- [x] Criação de tenant exibe senha provisória com aviso claro
|
||
- [x] Mudança de status de tenant exige confirmação
|
||
- [x] Erros de negócio da API aparecem como mensagem clara
|
||
- [x] API key da Evolution nunca aparece em texto completo após salva
|
||
- [x] Editor de template mostra e insere placeholders corretamente
|
||
- [x] Listagens com paginação, busca/filtro, loading/vazio
|
||
- [x] Testado em 375px, 768px, 1280px
|
||
|
||
**Observações:**
|
||
|
||
- Infra nova para esta etapa: `@tanstack/react-query` instalado e plugado em
|
||
`app/layout.tsx` via `lib/query/query-provider.tsx` (`QueryClient` com
|
||
`retry` desligado pra `ApiError` — erro de negócio não se resolve
|
||
tentando de novo, só falha de rede/infra vale 1 retry). `recharts`
|
||
**não** foi instalado — ver limitação do dashboard abaixo. Camada de API
|
||
por domínio nova em `lib/api/{planos,tenants,faturas,dashboard,
|
||
evolution-config,alert-templates,alert-logs,master-admins}.ts` (tipos +
|
||
funções `apiClient.*`, espelhando os DTOs/respostas do backend campo a
|
||
campo) e schemas zod em `lib/validators/{planos,tenants,
|
||
evolution-config,alert-templates}.ts`, mesmo padrão de
|
||
`lib/validators/auth.ts` da Etapa 13. `lib/format.ts` novo
|
||
(`formatCurrency`/`formatDate`/`formatDateTime`, pt-BR).
|
||
- **Limitação de backend, registrada conforme a própria tarefa 1 já
|
||
previa:** `GET /master/dashboard` (`dashboard.service.ts#resumo`) só
|
||
devolve o snapshot atual (contagem por status, MRR, recebido no mês, em
|
||
aberto) — nenhuma série histórica de faturamento/novos tenants. Sem dado
|
||
pra alimentar, o gráfico Recharts da tarefa 1 não foi construído (nem a
|
||
dependência instalada); o dashboard ficou só com os cards de resumo (8
|
||
cards: 5 de status + MRR/recebido/em aberto), como a tarefa já
|
||
autorizava nesse cenário.
|
||
- **Limitação de backend, relevante pra `/master/configuracoes`:** não
|
||
existe endpoint de perfil/senha do master admin (só `GET
|
||
/auth/master/admins`, somente leitura, listando todos os admins — sem
|
||
filtro por usuário nem rota de update). A tela resolve "meus dados"
|
||
filtrando essa lista pelo `sub` do JWT decodificado
|
||
(`useAuth().user.sub`) e mostra só leitura; a troca de senha reaproveita
|
||
o fluxo genérico `POST /auth/password/forgot` (`usuario_tipo: 'master'`)
|
||
com o e-mail já preenchido — não uma troca "senha atual → nova senha"
|
||
direta na tela, porque essa rota não existe no backend. Registrado como
|
||
gap de backend caso o produto queira uma troca de senha in-app mais
|
||
direta numa etapa futura.
|
||
- **Placeholders de template**: `src/modules/alerts/placeholders.ts`
|
||
(`PLACEHOLDERS_POR_TIPO`) não é exposto por nenhum endpoint — é uma
|
||
constante interna do backend. Replicada manualmente em
|
||
`lib/api/alert-templates.ts` para alimentar o painel de ajuda do editor
|
||
(clicar num placeholder insere `{{chave}}` na posição do cursor do
|
||
textarea, via manipulação direta de `selectionStart`/`selectionEnd` +
|
||
`form.setValue`). Se a lista mudar no backend, esta cópia precisa ser
|
||
atualizada junto — fica documentado no comentário do arquivo.
|
||
- Campos numéricos de formulário (`valor_mensal`, `limite_customers`,
|
||
`dia_vencimento_mensalidade` etc) usam `z.number()` puro nos schemas
|
||
(sem `z.coerce`/`z.preprocess`) — a conversão string→number do
|
||
`<input type="number">` acontece só no `register(...)` do
|
||
react-hook-form (`valueAsNumber`/`setValueAs`). Misturar coerção no
|
||
schema com `zodResolver` faz o tipo de entrada do formulário divergir do
|
||
tipo de saída e quebra a inferência de tipos entre `useForm`/
|
||
`zodResolver` (erro real de TS encontrado e corrigido nesta etapa).
|
||
- Modal de criação de tenant (`/master/tenants/novo`) mostra a senha
|
||
provisória num `ResponsiveDialog` que só fecha levando para o detalhe do
|
||
tenant (não é dismissível "por engano") — com botão de copiar
|
||
(`navigator.clipboard`) e aviso de que não será exibida de novo, como
|
||
pedido no critério de aceite.
|
||
- Mudança de status de tenant reaproveitada entre a listagem e o detalhe
|
||
via `components/domain/tenant-status-dialog.tsx` — confirmação explícita
|
||
com aviso ("bloqueia login mesmo com sessões abertas"), Select só com as
|
||
transições válidas (`ativo`/`inadimplente`/`suspenso`/`cancelado` —
|
||
nunca `trial`, que só existe na criação automática).
|
||
- **Três bugs pré-existentes (Etapas 12/13, não desta etapa) encontrados
|
||
durante o teste manual ponta a ponta e corrigidos aqui, por serem
|
||
bloqueadores reais para "todas as telas funcionam com o backend real":**
|
||
1. **Race condition em `AuthProvider.refreshSession`**
|
||
(`lib/auth/auth-context.tsx`): após qualquer reload de página
|
||
protegida, duas fontes disparavam `POST /auth/refresh`
|
||
quase ao mesmo tempo (o efeito de montagem do Provider e o
|
||
`authInterceptor.onUnauthorized` de uma request que saiu antes do
|
||
token estar pronto) — a 2ª chamada reenviava o cookie já rotacionado
|
||
pela 1ª, e o backend tratava isso como reuso de token revogado,
|
||
revogando a família inteira e derrubando a sessão que acabara de ser
|
||
validada. Corrigido deduplicando chamadas concorrentes via uma
|
||
`Promise` compartilhada (`refreshPromiseRef`). Sem esse fix, um F5
|
||
em qualquer tela de `/master/*` ou `/app/*` podia deslogar o usuário
|
||
de forma intermitente.
|
||
2. **`Button asChild` quebrado** (`components/ui/button.tsx`): o
|
||
`<button>` sempre envolvia `children` num `<span>` (mais um
|
||
`{loading && <Loader2Icon/>}` condicional do lado), então quando
|
||
`asChild` trocava o elemento por `Slot.Root` (Radix), o Slot recebia
|
||
2 filhos em vez de 1 — Radix exige exatamente 1 filho React em
|
||
`asChild`, e lançava "Slot failed to slot onto its children" em
|
||
runtime. Ninguém tinha usado `asChild` em `Button` antes desta etapa
|
||
(`/master/tenants`, botão "Novo tenant"/"Ver" como `Link`). Corrigido
|
||
dando um caminho de render separado pro `asChild` (passa `children`
|
||
direto pro `Slot.Root`, sem o wrapper de loading — não faz sentido
|
||
ter spinner quando quem renderiza de verdade é o filho).
|
||
3. **`DataTable` cortava a última coluna em vez de rolar**
|
||
(`components/ui/data-table.tsx`): o container da tabela desktop usava
|
||
`overflow-hidden`; com 4+ colunas perto do breakpoint `md` (768px,
|
||
ex: `/master/tenants`), a tabela ficava mais larga que o espaço
|
||
disponível e a(s) coluna(s) da direita (inclusive as ações de linha)
|
||
simplesmente desapareciam, sem forma de acessá-las. Trocado para
|
||
`overflow-x-auto overflow-y-hidden` — scroll horizontal **contido**
|
||
dentro do card da tabela (confirmado que não vaza pra um scroll
|
||
horizontal da página inteira).
|
||
- Também corrigido durante o teste: `FormField` clona `id`/`aria-invalid`/
|
||
`aria-describedby` só no filho direto — quando o filho era `<Select>`
|
||
(não `<SelectTrigger>`), o `id` nunca chegava no elemento real e o
|
||
`<Label htmlFor>` ficava órfão (falha de acessibilidade real, pega pelo
|
||
teste ao tentar interagir via `getByLabel`). Corrigido nos 3 lugares que
|
||
usam `Select` dentro de `FormField` (`tenants/novo`, `tenants/[id]`,
|
||
`alertas/templates`) passando o mesmo `id` explicitamente pro
|
||
`FormField` e pro `SelectTrigger`. `Select`s usados como filtro (fora de
|
||
`FormField`) ganharam `aria-label` explícito pelo mesmo motivo.
|
||
- **Teste manual ponta a ponta** (sem Playwright como dependência do
|
||
projeto — instalado ad-hoc num container à parte, mesmo padrão da Etapa
|
||
14): rodei `tsc --noEmit`/`eslint .`/`next build` via
|
||
`docker run node:20-alpine` — limpos. Subi o `next dev` (porta 3001,
|
||
`NEXT_PUBLIC_API_URL` pro backend real via `docker compose` na 3000) e
|
||
dirigi um Chromium headless cobrindo, contra a API real: dashboard,
|
||
criar plano, criar tenant (com plano recém-criado) até o modal de senha
|
||
provisória, as 3 abas do detalhe do tenant, mudar status com
|
||
confirmação, listagem de tenants com busca/filtro em 375/768/1280px,
|
||
gerar faturas do mês, cadastrar config Evolution (key mascarada
|
||
confirmada na listagem) + testar conexão (contra host inexistente,
|
||
proposital — confirma que erro de rede vira toast claro, não tela
|
||
quebrada), criar template com inserção de 2 placeholders no corpo
|
||
(`{{nome}}{{valor}}`, confirmado via `inputValue()`), logs de alerta
|
||
(estado vazio) e configurações. Dados de teste ficaram no banco de dev
|
||
(tenants/planos/config Evolution com nomes prefixados
|
||
"Playwright"/"E2E"/"PW", inofensivos, mesma natureza dos dados de teste
|
||
já deixados na Etapa 14).
|
||
|
||
---
|
||
|
||
## Etapa 16 — Área Tenant: Dashboard, Clientes e Produtos
|
||
|
||
- [x] Dashboard do tenant com números corretos
|
||
- [x] CRUD completo de clientes, com contatos aninhados e regra de principal único
|
||
- [x] Histórico de vendas no detalhe do cliente lista corretamente
|
||
- [x] CRUD completo de produtos/serviços, com validação de SKU duplicado
|
||
- [x] Limite de plano (clientes/produtos) mostra mensagem clara ao ser excedido
|
||
- [x] Desativação com confirmação reflete corretamente na listagem
|
||
- [x] Listagens com busca, filtro, paginação, loading/vazio
|
||
- [x] Testado em 375px, 768px, 1280px
|
||
|
||
**Observações:**
|
||
|
||
- `recharts` (v3) instalado — primeiro uso real no projeto. Camada de API
|
||
nova em `lib/api/{tenant-dashboard,customers,products,sales}.ts`
|
||
(`sales.ts` só com o mínimo pra listagem/histórico; CRUD completo de
|
||
vendas fica pra Etapa 17) e validators em
|
||
`lib/validators/{customers,products}.ts`. `lib/br-states.ts` novo (27
|
||
UFs, usado no `Select` de estado do formulário de cliente).
|
||
- **Dashboard**: `GET /tenant/dashboard/financeiro` já devolve
|
||
`proximas_parcelas` pronto (top N por `vencimento asc`, com
|
||
`sale.numero_venda`/`sale.customer.razao_social` incluídos) — não
|
||
precisou montar isso a partir de `/tenant/recebimentos` como a tarefa 1
|
||
cogitava como alternativa. Único porém: esse endpoint **não inclui
|
||
`sale.id`**, então a lista de "próximas parcelas" no dashboard não linka
|
||
pra venda individual — só um link geral "Ver todos" pra
|
||
`/app/recebimentos` (a própria tarefa 1 já pedia só esse link direto,
|
||
não um link por parcela). Gráfico usa `--color-primary` lido em runtime
|
||
via `getComputedStyle` (não uma classe Tailwind, já que Recharts precisa
|
||
de string de cor em `stroke`/`fill`) — reflete a cor do tenant
|
||
automaticamente. Abas Semanal/Mensal chamam
|
||
`resumo-semanal`/`resumo-mensal` (views do backend) sob demanda.
|
||
- **Histórico de vendas** (`/app/clientes/:id`, aba Histórico): não existe
|
||
endpoint dedicado — usa `GET /tenant/vendas?customer_id=<id>`
|
||
(`SalesController` já aceita esse filtro). Cada linha linka para
|
||
`/app/vendas/:id` (rota ainda placeholder até a Etapa 17, como o próprio
|
||
prompt desta etapa já autorizava deixar apontando).
|
||
- **Regra de contato principal**: em vez de um checkbox "principal" dentro
|
||
do formulário de criar/editar contato (não há componente `Checkbox` no
|
||
design system ainda), a UI usa uma ação de linha dedicada "Tornar
|
||
principal" (mutation direta `PUT .../contatos/:id { principal: true }`,
|
||
sem abrir modal) — o primeiro contato de um cliente já nasce principal
|
||
por padrão (`contatos.length === 0` no `openCreate`). Testado
|
||
criando 2 contatos e promovendo o 2º: o 1º perde o badge "Principal"
|
||
automaticamente (comportamento do backend, Etapa 5).
|
||
- Mesmo raciocínio pro campo `recorrente` de produto: sem `Checkbox`, virou
|
||
um `Select` "Não"/"Sim — cobrança recorrente" dentro do formulário
|
||
(diferente do `ativo` de plano/produto, que tem uma ação dedicada de
|
||
desativar — `recorrente` não tem esse paralelo, por isso precisou mesmo
|
||
de um controle no formulário).
|
||
- `ProductForm` (`components/domain/product-form.tsx`) compartilhado entre
|
||
`/app/produtos/novo` e `/app/produtos/:id` — campos 100% idênticos nas
|
||
duas telas (diferente de tenants na Etapa 15, onde `:id` omite os campos
|
||
de admin), então fatorar em vez de duplicar fez sentido aqui.
|
||
- **Erros de negócio testados de ponta a ponta contra a API real**: SKU
|
||
duplicado (`POST /tenant/produtos` → 409, mensagem
|
||
`SKU "..." já cadastrado para este tenant` mostrada direto via toast,
|
||
formulário preservado) e limite de plano de clientes (testado
|
||
temporariamente com `limite_customers=1` via SQL — 400 com mensagem
|
||
`Limite de 1 cliente(s) do plano atingido — desative um cliente
|
||
existente ou faça upgrade de plano`, restaurado ao valor original depois
|
||
do teste). Produtos **não têm** limite de plano no backend atual (só
|
||
`limite_customers`/`limite_usuarios`/`limite_instancias_whatsapp`
|
||
existem) — confirmado por busca no código, não é uma lacuna do
|
||
frontend.
|
||
- `STATUS_PILL_MAP` (`components/ui/status-pill.tsx`, Etapa 12) ganhou uma
|
||
entrada nova, `cancelada` (status de venda, `venda_status`) — faltava
|
||
porque só `cancelado` (fatura/parcela) existia; usado na aba Histórico
|
||
de vendas. Adição aditiva, não muda nenhum uso existente.
|
||
- **Teste manual ponta a ponta** (mesmo padrão das Etapas 14/15): `tsc
|
||
--noEmit`/`eslint .`/`next build` via `docker run node:20-alpine` —
|
||
limpos. Chromium headless (Playwright ad-hoc) dirigido contra o backend
|
||
real, logado como o `tenant_admin` de teste já existente
|
||
(`Etapa14 Demo`, cor `#7C3AED`): dashboard, criar cliente, criar 2
|
||
contatos + trocar o principal, histórico de vendas (vazio, sem quebrar),
|
||
criar produto, SKU duplicado, limite de plano de clientes, desativar
|
||
produto com confirmação + reaparecer no filtro "Somente inativos", tudo
|
||
em 375/768/1280px. Nenhum bug novo de plataforma encontrado desta vez
|
||
(as três correções de componentes compartilhados ficaram todas na Etapa
|
||
15) — só um ajuste de entendimento do próprio script de teste
|
||
(`Button asChild` com `Link` renderiza `<a>`, então o role de
|
||
acessibilidade correto pra Playwright é `link`, não `button` — nada a
|
||
mudar no app).
|
||
|
||
---
|
||
|
||
## Etapa 17 — Área Tenant: Vendas e Financeiro
|
||
|
||
- [x] Criar venda completa (cliente + itens + parcelas) funciona ponta a ponta
|
||
- [x] Validação de soma das parcelas vs valor total impede salvar quando não bate
|
||
- [x] Cadastro rápido de cliente dentro do fluxo de venda funciona
|
||
- [x] Venda confirmada não permite editar itens/valores
|
||
- [x] Cancelar venda cancela parcelas pendentes, refletido em recebimentos
|
||
- [x] Marcar parcela como paga atualiza dashboard sem reload manual
|
||
- [x] Parcelas atrasadas destacadas visualmente
|
||
- [x] Barra de total fixa no mobile funciona sem sobrepor conteúdo
|
||
- [x] Testado em 375px, 768px, 1280px (atenção à tela de nova venda)
|
||
|
||
**Observações:**
|
||
|
||
- A tela mais densa do projeto, como o próprio prompt já antecipava.
|
||
`components/domain/venda-form.tsx` é o núcleo: formulário compartilhado
|
||
entre `/app/vendas/nova` (modo `create`, dois botões — "Salvar como
|
||
orçamento" e "Confirmar venda") e a aba "Itens" de `/app/vendas/:id`
|
||
quando a venda está em `orcamento` (modo `edit`, um botão "Salvar
|
||
alterações") — o backend redefine itens/parcelas por inteiro no `PUT`,
|
||
então editar "só os itens" precisa reenviar o formulário inteiro
|
||
(itens+parcelas+cliente+data) de qualquer forma; reaproveitar o mesmo
|
||
componente de criação evitou duplicar toda essa lógica de cálculo/
|
||
validação num editor separado.
|
||
- **Duas lacunas de backend confirmadas antes de implementar** (evitou
|
||
desenhar UI para algo que não tem onde ir):
|
||
1. **Parcelas automáticas não aceitam "primeira data de vencimento"** —
|
||
`CreateSaleDto`/`UpdateSaleDto` só tem `numero_parcelas`; o backend
|
||
sempre calcula os vencimentos como `data_venda + 30×n` dias
|
||
(`sale-calculo.util.ts`). A tarefa 2 cogitava um campo separado pra
|
||
isso — não implementado por não existir onde mandar esse valor; o
|
||
formulário mostra uma prévia dos vencimentos calculados (só
|
||
client-side, ilustrativa) em vez do campo.
|
||
2. **Sem endpoint de histórico/auditoria tenant-facing** — `audit_logs`
|
||
só é gravado (várias services já escrevem lá), nunca lido por nenhum
|
||
controller. A aba "Histórico" existe (a tarefa 3 já condicionava
|
||
isso a "se o backend expuser") mas mostra um `EmptyState` explicando
|
||
a limitação, em vez de fingir que tem dado.
|
||
3. **Sem endpoint de marcar-pago em lote** — `PATCH
|
||
.../marcar-pago` só aceita um id por vez. A seleção múltipla em
|
||
`/app/recebimentos` (checkbox por linha nas parcelas
|
||
pendente/atrasado + barra "N selecionadas") dispara `N` chamadas em
|
||
paralelo (`Promise.allSettled`) e resume sucessos/falhas num único
|
||
toast — não é atômico no backend, mas entrega a funcionalidade
|
||
pedida ("se possível, oferecer seleção múltipla").
|
||
- `ConfirmDialog` (Etapa 15) ganhou dois props novos, opcionais e
|
||
retrocompatíveis: `children` (conteúdo extra entre a descrição e os
|
||
botões — usado aqui pra pedir a forma de pagamento antes de confirmar o
|
||
lote) e `confirmDisabled` (trava o botão de confirmar até uma escolha
|
||
obrigatória em `children` ser feita). Nenhum uso existente (Etapa 15)
|
||
precisou mudar.
|
||
- Combobox de cliente/produto (`Cliente`, `Adicionar item...`) busca só
|
||
entre os **100 primeiros ativos** (`listCustomers`/`listProducts` com
|
||
`limit: 100`, sem busca remota conforme o usuário digita) — o
|
||
`Combobox` do design system (Etapa 12) já filtra client-side via `cmdk`
|
||
a partir da lista carregada, não foi desenhado pra busca remota
|
||
incremental; para catálogos maiores que 100 itens ativos, isso é uma
|
||
limitação conhecida a revisitar (paginação/busca server-side no
|
||
Combobox) numa etapa futura.
|
||
- **Sem componente `Checkbox`** (mesma lacuna já registrada nas Etapas
|
||
15/16): a seleção múltipla de recebimentos usa um `<input
|
||
type="checkbox">` nativo estilizado com `accent-primary` direto na
|
||
célula da tabela, em vez de um componente do design system.
|
||
- Cálculo de totais no formulário é só para exibição/validação client-side
|
||
(subtotal, desconto, total, soma de parcelas manuais em centavos via
|
||
`Math.round(valor*100)` — mesma técnica de comparação em centavos do
|
||
backend, `somaCentavos`) — o valor autoritativo sempre vem recalculado
|
||
pelo backend na resposta; o form nunca envia `valor_total`.
|
||
- **Achado de UX, não bug**: `Button asChild` (corrigido na Etapa 15)
|
||
renderiza um `<a>` quando o filho é `Link` — o role de acessibilidade
|
||
correto é `link`, não `button`. Só ajustei o script de teste manual
|
||
(mesmo aprendizado já registrado na Etapa 16); nada mudou no app.
|
||
- **Teste manual ponta a ponta** (mesmo padrão das Etapas 14-16):
|
||
`tsc --noEmit`/`eslint .`/`next build` limpos via `docker run
|
||
node:20-alpine`. Chromium headless dirigido contra o backend real,
|
||
logado como o `tenant_admin` de teste (reativei o produto que a Etapa
|
||
16 tinha desativado e criei um segundo produto/serviço, só pra ter
|
||
catálogo suficiente pro teste): venda com 2 itens + desconto + parcelas
|
||
automáticas salva como orçamento, aba Parcelas conferida (3 parcelas de
|
||
30/60/90 dias, resto do centavo na última), aba Itens editável
|
||
reaproveitando o formulário, confirmar venda (itens viram somente
|
||
leitura, ações de topo mudam pra "Concluir"/"Cancelar"), cancelar venda
|
||
com as 3 parcelas virando `cancelado` automaticamente (conferido em
|
||
`/app/recebimentos`), parcelas manuais com soma errada bloqueando o
|
||
submit com a diferença exata em R$, cadastro rápido de cliente no meio
|
||
do fluxo preservando itens/observações já preenchidos, marcar parcela
|
||
como paga em `/app/recebimentos` e navegar pro dashboard **via link do
|
||
menu (sem F5)** confirmando "Recebido no mês" e "Próximas parcelas"
|
||
atualizados na hora (prova real da invalidação do React Query), e a
|
||
barra fixa de total no mobile (375px) acima da bottom tab bar sem
|
||
sobrepor o formulário. Também usei `getByRole('combobox', {name})` do
|
||
Playwright em alguns pontos e ele não bateu com o texto visível do
|
||
`Combobox` do design system (accessible name computada parece divergir
|
||
do `textContent` por algum motivo não investigado a fundo) — contornado
|
||
no script de teste com um seletor por texto/atributo; não indica
|
||
problema de acessibilidade real percebido visualmente, mas fica
|
||
registrado como algo a olhar com mais calma numa auditoria de
|
||
acessibilidade futura, já que o mecanismo exato não foi confirmado.
|
||
|
||
---
|
||
|
||
## Etapa 18 — Área Tenant: Integrações, Alertas, Equipe e Perfil
|
||
|
||
- [ ] Fluxo de conexão WhatsApp funciona (QR code + polling de status)
|
||
- [ ] Teste de envio de WhatsApp e e-mail com feedback claro
|
||
- [x] Alternância SMTP próprio/padrão funciona corretamente
|
||
- [x] Templates do tenant indicam claramente uso de fallback do padrão
|
||
- [x] Logs de alerta mostram apenas os do próprio tenant
|
||
- [x] CRUD de equipe funciona e respeita visibilidade por papel
|
||
- [x] Upload de logo com validação client-side e preview imediato
|
||
- [x] Alteração de cor primária reflete instantaneamente sem reload
|
||
- [x] Testado em 375px, 768px, 1280px
|
||
|
||
**Observações:**
|
||
|
||
- **Duas lacunas de backend confirmadas antes de implementar, ambas
|
||
autorizadas explicitamente pelo usuário** (mesmo espírito da mudança de
|
||
backend já feita na Etapa 13):
|
||
1. **CRUD de equipe** — só existia `GET /auth/tenant/users` (Etapa 2,
|
||
somente leitura/demonstração, **não tocado**). Criado
|
||
`src/modules/equipe/` (`EquipeController`/`EquipeService`,
|
||
`GET/POST /tenant/equipe`, `PATCH /tenant/equipe/:id`, guardas
|
||
idênticas ao `WhatsappController` — `JwtAuthGuard, TenantGuard,
|
||
RolesGuard` + `@Roles('tenant_admin')`), espelhando o padrão de
|
||
`CustomersService.create` (`TenantContextService`, `tenant_id`
|
||
explícito no `create`, já que `tenant_users` tem RLS) e de
|
||
`TenantsService.create`/`provisional-password.util.ts` (senha
|
||
provisória, `assertPasswordStrength`, `hashPassword`). Primeiro uso
|
||
real de `planos.limite_usuarios` no backend inteiro (existia na
|
||
schema/DTOs desde a Etapa 3, nunca enforced). Duplicidade de e-mail
|
||
traduzida de `P2002` pra 409, mesmo padrão de SKU
|
||
(`products.service.ts`). **Guarda de segurança nova, não pedida por
|
||
nenhum prompt anterior mas necessária**: `PATCH .../:id` bloqueia
|
||
`tenant_admin` de autodesativar ou autorrebaixar (`id === actor.sub`
|
||
+ `ativo:false`/`role !== 'tenant_admin'` → 400) — sem essa trava,
|
||
um admin sozinho no tenant conseguiria se trancar fora do próprio
|
||
painel, sem nenhum caminho de recuperação (não existe outra tela de
|
||
gestão de admins).
|
||
2. **Edição de `nome_fantasia`/`razao_social`** — `TenantProfileService`
|
||
só tinha `obter`/`atualizarLogo`/`removerLogo`/`atualizarCor`.
|
||
Adicionado `PUT /tenant/perfil` (`UpdatePerfilDto`, mesmo
|
||
`Controller`/`Service` existentes, sem módulo novo), atualização
|
||
parcial (só grava os campos realmente enviados), mesmo padrão de
|
||
transação + `writeAuditLog` de `atualizarCor`.
|
||
Detalhe completo (DTOs exatos, mensagens de erro, testes) também
|
||
referenciado em `Etapas/09-controle-de-progresso.md`, nota datada dentro
|
||
da Etapa 2.
|
||
- **Backend — testes**: `test/equipe-perfil.e2e-spec.ts` novo (16 testes)
|
||
cobrindo os 4 papéis na criação, senha provisória exibida uma única vez,
|
||
senha fraca explícita rejeitada, e-mail duplicado 409, limite de
|
||
`limite_usuarios` do plano (temporariamente `1` via SQL, mesma técnica
|
||
já documentada na Etapa 16 para `limite_customers`), listagem, troca de
|
||
papel, desativação (confirmando refresh token revogado — login novo e
|
||
`POST /auth/refresh` com o token antigo os dois rejeitados — e
|
||
reativação restaurando o acesso), autodesativação/autorrebaixamento
|
||
bloqueados, isolamento por RLS entre dois tenants (404 pelo UUID
|
||
exato), bloqueio de todos os endpoints para papéis não-`tenant_admin` e
|
||
para token master "puro", e `PUT /tenant/perfil` (atualização completa e
|
||
parcial, validação de `razao_social` curta demais, 403 pra master).
|
||
Rodado com `docker compose up -d --build app worker` (build real via
|
||
`Dockerfile`, não `docker run node:20-alpine` ad-hoc — necessário porque
|
||
o motor de query do Prisma gerado num Alpine 3.23 genérico não tem mais
|
||
`libssl.so.1.1` disponível via `apk` (só o `openssl` 3.x) — só a
|
||
imagem `production`, que roda `prisma generate` no próprio build,
|
||
gera o engine certo para o ambiente onde vai rodar; achado de infra
|
||
novo, não existia nas etapas anteriores porque a imagem base do Docker
|
||
Hub mudou de versão entre sessões) + `prisma migrate deploy` (nenhuma
|
||
migration nova) + `docker compose exec app npx jest --config
|
||
./test/jest-e2e.json --runInBand` — **142/152 passando** (todos os 10
|
||
suites). Os 10 testes que falham são **pré-existentes, não desta
|
||
etapa**: 5 em `alertas.e2e-spec.ts` (timeout de 5000ms em jobs
|
||
BullMQ/retry, já documentado como achado incidental na Etapa 13) e 5 em
|
||
`integracoes.e2e-spec.ts` (timeout de 5000ms nos testes de instância de
|
||
WhatsApp, mesmo padrão). **Confirmado que não é regressão desta
|
||
etapa**: rodei a mesma suíte `integracoes.e2e-spec.ts` com `git stash`
|
||
aplicado (revertendo todas as mudanças desta etapa) e os mesmos 5 testes
|
||
falham do mesmo jeito — fica registrado como achado incidental de
|
||
ambiente (provavelmente o host ficou mais lento entre sessões), não
|
||
investigado a fundo por estar fora do escopo desta etapa. Antes desta
|
||
etapa: 136 testes totais (126 passando + os mesmos 10 falhando).
|
||
`npm run lint`, `npx tsc --noEmit`, `npm run test` (unit, 5/5 — nenhum
|
||
teste unitário novo: `EquipeService` é CRUD simples sobre Prisma, mesmo
|
||
raciocínio já usado para não criar `.spec.ts` para `customers.service.ts`)
|
||
e `npm run build` todos limpos.
|
||
- **Infra nova no frontend**: `lib/api/{whatsapp,email-config,
|
||
tenant-alert-templates,tenant-alert-logs,equipe,tenant-profile,
|
||
asset-url}.ts` e validators em `lib/validators/{whatsapp,
|
||
tenant-profile,email-config,equipe,tenant-alert-templates}.ts`.
|
||
`tenant-alert-templates.ts`/`tenant-alert-logs.ts` reexportam tipos e
|
||
constantes das versões master (`alert-templates.ts`/`alert-logs.ts`,
|
||
Etapa 15) em vez de duplicá-los — só `TIPOS_TEMPLATE_TENANT`
|
||
(`TIPOS_TEMPLATE` menos `mensalidade_tenant`, espelhando
|
||
`alert-template-tipos.ts` do backend) e os paths `/tenant/...` são
|
||
específicos. `PLACEHOLDERS_POR_TIPO` (replicado manualmente pela Etapa
|
||
15, mesma ressalva de manutenção já documentada) é reaproveitado como
|
||
está — cobre os 3 tipos do tenant sem precisar de cópia própria.
|
||
- **`lib/api/client.ts` ganhou suporte a `FormData`** (upload de logo,
|
||
primeiro upload de arquivo do frontend inteiro): `request()` detecta
|
||
`body instanceof FormData` e pula tanto o `JSON.stringify` quanto o
|
||
header `Content-Type` manual (o browser define o boundary do multipart
|
||
sozinho) — aditivo, não muda nenhum uso existente (todos passavam
|
||
objetos simples até aqui).
|
||
- **`TenantThemeProvider` ganhou `applyThemeUpdate(patch)`** (critério de
|
||
aceite "reflete instantaneamente sem reload"): atualiza tanto as CSS
|
||
variables de `:root` (`--color-primary`/`--color-primary-on-light`,
|
||
reaproveitando `ensureAccessibleTextColor` da Etapa 13) quanto o
|
||
`theme` em memória que `TenantBrand` (Etapa 14) lê pra nome/logo — sem
|
||
isso, só a cor mudaria em runtime (via CSS var direta) mas o nome
|
||
fantasia/logo exigiriam um refetch. Chamado depois de cada mutation bem
|
||
sucedida em `/app/perfil` (dados/logo/cor), sem invalidar nenhuma
|
||
query "morta" (nada mais lê `['tenant','perfil']` fora da própria
|
||
página). Confirmado via Playwright lendo
|
||
`getComputedStyle(document.documentElement).getPropertyValue('--color-primary')`
|
||
depois de salvar, sem F5 — refletiu o novo valor na hora, e a sidebar
|
||
mudou de cor/nome visivelmente no mesmo screenshot.
|
||
- **Bug real encontrado e corrigido, não cosmético (severidade alta —
|
||
quebra completamente o critério de aceite de teste de
|
||
WhatsApp/e-mail)**: `AlertDispatchService.enviarWhatsapp`/`enviarEmail`
|
||
(Etapa 4) **sempre respondem HTTP 200**, mesmo em falha — o resultado
|
||
vem como `{ sucesso: false, erro: '...' }` no corpo, nunca como erro
|
||
HTTP (decisão de design do backend para não vazar stack trace como
|
||
status code, documentada no próprio `DispatchResult`). A primeira
|
||
versão das telas `/app/whatsapp` e `/app/email-config` tratava
|
||
qualquer resposta 200 como sucesso — testando "Enviar teste" de e-mail
|
||
contra este ambiente (sem `master_email_config` cadastrada), a tela
|
||
mostrava **"E-mail de teste enviado com sucesso"** quando na verdade o
|
||
backend tinha respondido `{ sucesso: false, erro: "Nenhuma
|
||
configuração de e-mail padrão da plataforma está ativa" }` — feedback
|
||
de sucesso/erro literalmente invertido, achado só ao testar contra o
|
||
backend real (nenhum erro de tipo/lint acusa isso, já que o retorno
|
||
tipado antes era `unknown`). Corrigido em `lib/api/{whatsapp,
|
||
email-config}.ts` (tipo `TestarEnvioResult { sucesso, erro? }`) e nas
|
||
duas telas (`onSuccess` agora checa `result.sucesso` antes de decidir
|
||
toast de sucesso ou erro). Reconfirmado depois do fix: mesmo cenário
|
||
agora mostra corretamente "Nenhuma configuração de e-mail padrão da
|
||
plataforma está ativa" como toast de erro.
|
||
- **Bug real encontrado e corrigido, pré-existente (Etapas 13/14, não
|
||
desta etapa)**: `logo_url` do tenant volta do backend como caminho
|
||
relativo (`/uploads/...`, servido estaticamente pelo backend) — `<Image
|
||
src={theme.logoUrl}>` em `TenantBrand` (Etapa 14) e no
|
||
`login-form.tsx` (Etapa 13) passava esse caminho relativo direto pro
|
||
`next/image`, que resolve contra a origem do **frontend**
|
||
(`:3001`), não do backend (`:3000`) — logo nunca teria aparecido em
|
||
produção real (com origens diferentes), só passou despercebido porque
|
||
nenhum tenant de teste das Etapas 13/14/15/16/17 tinha logo cadastrada
|
||
(o `theme?.logoUrl` falsy sempre caía no fallback de avatar com
|
||
inicial). Esta é a primeira etapa que testa upload+exibição de logo de
|
||
ponta a ponta, e o bug apareceu na hora. Corrigido com
|
||
`lib/api/asset-url.ts` (`resolveAssetUrl`, prefixa `NEXT_PUBLIC_API_URL`
|
||
em caminhos relativos) usado em `lib/theme/resolve-theme.ts` e em
|
||
`/app/perfil`; como isso torna a URL absoluta **cross-origin**, também
|
||
precisou adicionar `images.remotePatterns` em `next.config.ts`
|
||
(`next/image` recusa otimizar origem não configurada) apontando pro
|
||
host/porta de `NEXT_PUBLIC_API_URL`. Confirmado via Playwright: upload
|
||
de um PNG de teste aparece corretamente tanto no preview de
|
||
`/app/perfil` quanto na sidebar (`TenantBrand`), com o `src` real
|
||
resolvendo pra
|
||
`/_next/image?url=http%3A%2F%2Flocalhost%3A3000%2Fuploads%2F...`.
|
||
- **Fallback de template (critério de aceite "badge indicando padrão em
|
||
uso")**: confirmado que não há endpoint que informe isso — inferido
|
||
client-side em `/app/alertas/templates` com uma query separada
|
||
(`limit: 100`, todos os templates ativos do tenant) comparada contra as
|
||
6 combinações `TIPOS_TEMPLATE_TENANT × CANAIS_TEMPLATE`, renderizadas
|
||
como uma grade de cards acima da tabela principal — cada combinação sem
|
||
template próprio **ativo** mostra "Usando padrão da plataforma" +
|
||
botão "Criar meu próprio" que abre o formulário de criação pré-preenchido
|
||
com aquele tipo/canal (`openCreate({ tipo, canal })`). Checar `ativo`
|
||
(não só existência) é importante — confirmado lendo
|
||
`AlertTemplateResolverService.buscar` (backend) que o fallback também
|
||
ignora templates desativados.
|
||
- **Sem `Checkbox`** (lacuna já registrada nas Etapas 15–17): não foi
|
||
necessário nesta etapa — `usar_config_padrao` virou `Select` (mesmo
|
||
padrão de `recorrente` em produtos, Etapa 16) e `ativo` de equipe é uma
|
||
ação dedicada com `ConfirmDialog` (mesmo padrão de tenant/status,
|
||
Etapa 15), nunca um checkbox cru.
|
||
- **Proteção de autolinha em `/app/equipe`**: `Alterar papel`/`Desativar`
|
||
ficam desabilitados (com `title` explicando o motivo) na própria linha
|
||
do usuário logado, lendo `useAuth().user.sub` — mesmo padrão de
|
||
`UserFooter` (Etapa 14). O backend já bloqueia isso a 400 (`EquipeService.update`),
|
||
mas desabilitar no frontend evita o usuário nem tentar; confirmado
|
||
visualmente via Playwright (2 botões desabilitados na própria linha,
|
||
0 nas outras).
|
||
- **QR code/polling de WhatsApp**: implementado em
|
||
`ConnectInstanceDialog` (`app/app/whatsapp/page.tsx`) — `useQuery`
|
||
busca o QR code só quando o diálogo está aberto e o status ainda não é
|
||
`conectado`; um `useEffect` separado, dependente de `status`, faz o
|
||
polling via `setInterval` a cada 4s e se autolimita (a guarda `status
|
||
=== 'conectado'` impede reagendar um novo intervalo, e o cleanup do
|
||
`useEffect` cobre fechamento do diálogo/desmontagem — sem vazamento).
|
||
`qrImageSrc()` prefixa `data:image/png;base64,` só se o valor ainda não
|
||
começar com `data:`, conforme o comentário do próprio backend em
|
||
`evolution-api.client.ts` sobre o formato não ser garantido.
|
||
- **Limitação honesta, mesma raiz já documentada na Etapa 4**: não há
|
||
servidor Evolution API real acessível neste ambiente. A configuração
|
||
ativa do banco de dev aponta pra `evolution-fake.local:8080`
|
||
(inexistente) — criar uma instância falha com um toast claro (`Falha ao
|
||
comunicar com a Evolution API: getaddrinfo EAI_AGAIN
|
||
evolution-fake.local`, confirmado via Playwright) **antes** de a
|
||
instância ser persistida (`WhatsappService.criarInstancia` chama a
|
||
Evolution API antes do `create` no banco), então o fluxo completo de
|
||
QR/polling/"conectado" **não pôde ser exercitado contra um backend
|
||
real** nesta sessão — só o código foi revisado e a UI confirmada
|
||
renderizando/tratando o estado de erro corretamente, sem tela quebrada.
|
||
Pelo mesmo motivo, "testar envio" de WhatsApp não pôde ser exercitado
|
||
(nenhuma instância chega a existir) — mas o mesmo bug de
|
||
`sucesso`/`erro` corrigido no e-mail (acima) usa o **mesmo contrato**
|
||
(`DispatchResult`) e a mesma correção de código, então a confiança de
|
||
que funciona é alta, só não foi observado literalmente com um envio de
|
||
WhatsApp real. Os dois itens de aceite que dependem disso
|
||
("Fluxo de conexão WhatsApp funciona" e "Teste de envio de WhatsApp e
|
||
e-mail com feedback claro") ficam **deliberadamente desmarcados** acima
|
||
— o teste de e-mail (parte do segundo item) foi sim confirmado de
|
||
ponta a ponta com sucesso e com erro.
|
||
- **Teste manual ponta a ponta** (mesmo padrão das Etapas 14–17):
|
||
`tsc --noEmit`/`eslint .`/`next build` limpos via `docker run
|
||
node:20-alpine`. Backend real via `docker compose up -d --build app
|
||
worker` (postgres/redis já rodando). Frontend em `next dev --turbopack`
|
||
(porta 3001, `--network host` num container `node:20-alpine` — sem
|
||
precisar montar `/app/node_modules` como volume anônimo, já que não
|
||
há dependência nativa nesta etapa). Chromium headless (Playwright
|
||
1.56, instalado ad-hoc num container à parte, mesmo padrão das Etapas
|
||
14–17) logado como `admin@etapa14demo.com.br` do tenant "Etapa14 Demo"
|
||
(senha redefinida via hash gerado dentro do próprio container `app`,
|
||
mesma técnica/aviso já registrado na Etapa 14 — a senha anterior desse
|
||
usuário específico foi perdida). Cobri: criação de instância WhatsApp
|
||
(falha limpa e documentada acima), configuração de e-mail (alternância
|
||
padrão↔próprio com campos obrigatórios, salvar SMTP próprio + reload
|
||
confirmando persistência com senha nunca pré-preenchida, teste de envio
|
||
com erro claro, revertido pro padrão ao final), templates com badge de
|
||
fallback + criação pré-preenchida a partir do card, logs de alerta
|
||
(estado vazio, sem quebrar), CRUD de equipe completo (criação nos 4
|
||
papéis, senha provisória exibida uma vez, senha fraca rejeitada, papel
|
||
alterado, desativação com refresh token revogado confirmado via login
|
||
novo E `POST /auth/refresh` do token antigo, reativação, autoproteção
|
||
visual e funcional), upload/remoção de logo (tipo/tamanho inválido
|
||
rejeitados client-side, preview e reflexo instantâneo na sidebar),
|
||
alteração de cor com preview + reflexo instantâneo sem reload, edição
|
||
de nome fantasia/razão social com reflexo instantâneo na sidebar — tudo
|
||
em 1280px, com passe adicional dedicado de 375px e 768px nas 6 telas
|
||
(sem overflow horizontal em nenhuma, screenshots conferidos
|
||
visualmente, inclusive os cards mobile de equipe/templates).
|
||
**Dois bugs reais encontrados e corrigidos durante este teste**,
|
||
detalhados acima: a inversão de sucesso/erro do `DispatchResult` e o
|
||
`logo_url` relativo/cross-origin quebrando `next/image`.
|
||
- **Dados deixados no banco de dev**: papel de `vendedor@etapa14demo.com.br`
|
||
restaurado para `vendedor` ao final (tinha sido alterado pra
|
||
`financeiro` durante o teste); `nome_fantasia`/`cor_primaria` do tenant
|
||
"Etapa14 Demo" ficaram como os últimos valores testados
|
||
("Etapa18 Fantasia PW"/`#16A34A`) e a logo de teste foi removida ao
|
||
final (fluxo de remoção também exercitado) — inofensivo, mesma natureza
|
||
dos dados de demonstração já deixados pelas Etapas 14–17. Um usuário
|
||
`financeiro-pw-...@teste.com` ("Financeiro Playwright") ficou inativo
|
||
na equipe do tenant, como evidência do fluxo de desativação.
|
||
|
||
---
|
||
|
||
## Etapa 19 — Polimento, Acessibilidade e Testes E2E
|
||
|
||
- [ ] Nenhuma rota com overflow horizontal em 375px, 768px, 1280px
|
||
- [ ] Checador de acessibilidade sem violações críticas/sérias
|
||
- [ ] Fluxo completo realizado só com teclado, com foco sempre visível
|
||
- [ ] Contraste validado sobre cor primária dinâmica em tenants de teste variados
|
||
- [ ] Loading/erro/vazio tratados em todas as listagens e formulários
|
||
- [ ] Lighthouse mobile/desktop sem alertas de alto impacto
|
||
- [ ] Suíte E2E dos 4 fluxos críticos passa em mobile e desktop
|
||
- [ ] Revisão de copy concluída
|
||
|
||
**Observações:**
|
||
|
||
---
|
||
|
||
## Histórico de decisões e desvios do plano original
|
||
|
||
```
|
||
[DATA] Etapa X — descrição da decisão/desvio e motivo
|
||
```
|
||
|
||
[2026-08-15] Etapa 11 — Next.js pinado em 15.5.23 em vez do 16 que o
|
||
`create-next-app@latest` instala por padrão hoje; Next 16 se
|
||
auto-descreve (via AGENTS.md gerado) como tendo mudanças de API ainda
|
||
não documentadas de forma estável. Tailwind v4 (não v3): tokens expostos
|
||
via `@theme inline` em `styles/tokens.css`, sem `tailwind.config.ts`.
|
||
|
||
[2026-08-15] Etapa 12 — shadcn/ui com base Radix UI (não Base UI, a nova
|
||
recomendação padrão da CLI). `Select` com busca virou componente
|
||
`Combobox` separado. `Pagination` do shadcn removida e substituída por
|
||
`DataPagination` própria. Modal mobile = bottom sheet (não tela cheia).
|
||
Dark mode fora de escopo, não implementado.
|
||
|
||
[2026-08-15] Etapa 13 — Backend (Etapa 2) alterado para setar o refresh
|
||
token também como cookie httpOnly (`Path=/auth`), aditivo, aprovado
|
||
explicitamente pelo usuário antes de mexer em etapa já concluída. Login
|
||
master/tenant por toggle explícito, não detecção automática (backend não
|
||
suporta). Tema pré-login só resolve com `?tenant=<uuid>` na URL — sem
|
||
slug/subdomínio no schema, não há outro jeito de identificar o tenant
|
||
antes do login.
|
||
|
||
[2026-08-16] Etapa 18 — Backend estendido de forma aditiva, autorizado
|
||
explicitamente pelo usuário: módulo novo `src/modules/equipe/` (CRUD real
|
||
de `tenant_users` para `tenant_admin`, `GET /auth/tenant/users` da Etapa 2
|
||
não tocado) e `PUT /tenant/perfil` (`nome_fantasia`/`razao_social`) no
|
||
`TenantProfileService` já existente. Dois bugs reais de frontend
|
||
encontrados e corrigidos: `AlertDispatchService` sempre responde HTTP 200
|
||
mesmo em falha de envio (`{ sucesso: false, erro }` no corpo) — as telas
|
||
de teste de envio de WhatsApp/e-mail mostravam sucesso mesmo em erro até
|
||
a correção; `logo_url` relativo do tenant resolvia contra a origem
|
||
errada em `next/image` (frontend em vez de backend), corrigido com
|
||
`lib/api/asset-url.ts` + `images.remotePatterns` em `next.config.ts`. Sem
|
||
servidor Evolution API real neste ambiente (mesma limitação já
|
||
documentada na Etapa 4 do backend): fluxo de QR/conexão de WhatsApp
|
||
revisado em código e visualmente, mas não exercitado ponta a ponta contra
|
||
um backend real — critérios de aceite correspondentes deixados
|
||
desmarcados de propósito.
|