From 323912748f79b21b4408eba23e90299ef931147d Mon Sep 17 00:00:00 2001 From: Tiago dos Santos Date: Sun, 16 Aug 2026 09:41:13 -0300 Subject: [PATCH] =?UTF-8?q?Frontend=20completo=20(Etapas=2011-18)=20e=20ex?= =?UTF-8?q?tens=C3=B5es=20de=20backend=20(Etapas=2013=20e=2018)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .husky/pre-commit | 1 + Etapas/09-controle-de-progresso.md | 20 + Etapas/V2/00-README-ordem-das-etapas (1).md | 81 + Etapas/V2/10-frontend-schema-arquitetura.md | 261 + Etapas/V2/11-frontend-setup-projeto.md | 92 + Etapas/V2/12-frontend-design-system.md | 105 + Etapas/V2/13-frontend-autenticacao-tema.md | 102 + Etapas/V2/14-frontend-layout-navegacao.md | 94 + Etapas/V2/15-frontend-area-master.md | 99 + .../V2/16-frontend-area-tenant-cadastros.md | 86 + Etapas/V2/17-frontend-area-tenant-vendas.md | 97 + ...frontend-area-tenant-integracoes-perfil.md | 97 + Etapas/V2/19-frontend-polimento-testes.md | 98 + .../V2/20-controle-de-progresso-frontend.md | 1030 ++ eslint.config.mjs | 2 +- frontend/.env.local.example | 3 + frontend/.gitignore | 42 + frontend/.prettierrc | 5 + frontend/README.md | 50 + frontend/app/(public)/esqueci-senha/page.tsx | 125 + frontend/app/(public)/login/login-form.tsx | 153 + frontend/app/(public)/login/page.tsx | 12 + .../(public)/redefinir-senha/[token]/page.tsx | 101 + frontend/app/app/alertas/logs/page.tsx | 209 + frontend/app/app/alertas/templates/page.tsx | 412 + frontend/app/app/clientes/[id]/page.tsx | 473 + frontend/app/app/clientes/novo/page.tsx | 140 + frontend/app/app/clientes/page.tsx | 145 + frontend/app/app/dashboard/page.tsx | 181 + frontend/app/app/email-config/page.tsx | 215 + frontend/app/app/equipe/page.tsx | 359 + frontend/app/app/layout.tsx | 11 + frontend/app/app/perfil/page.tsx | 332 + frontend/app/app/produtos/[id]/page.tsx | 114 + frontend/app/app/produtos/novo/page.tsx | 59 + frontend/app/app/produtos/page.tsx | 167 + frontend/app/app/recebimentos/page.tsx | 299 + frontend/app/app/vendas/[id]/page.tsx | 301 + frontend/app/app/vendas/nova/page.tsx | 70 + frontend/app/app/vendas/page.tsx | 153 + frontend/app/app/whatsapp/page.tsx | 338 + frontend/app/dev/componentes/page.tsx | 11 + .../app/dev/componentes/showcase-client.tsx | 408 + frontend/app/diagnostico/page.tsx | 65 + frontend/app/favicon.ico | Bin 0 -> 25931 bytes frontend/app/globals.css | 129 + frontend/app/layout.tsx | 33 + frontend/app/master/alertas/logs/page.tsx | 209 + .../app/master/alertas/templates/page.tsx | 349 + frontend/app/master/configuracoes/page.tsx | 106 + frontend/app/master/dashboard/page.tsx | 86 + frontend/app/master/evolution-config/page.tsx | 223 + frontend/app/master/faturas/page.tsx | 218 + frontend/app/master/layout.tsx | 11 + frontend/app/master/planos/page.tsx | 248 + frontend/app/master/tenants/[id]/page.tsx | 271 + frontend/app/master/tenants/novo/page.tsx | 238 + frontend/app/master/tenants/page.tsx | 152 + frontend/app/page.tsx | 5 + frontend/components.json | 25 + frontend/components/domain/confirm-dialog.tsx | 66 + .../domain/customer-quick-create-dialog.tsx | 92 + .../components/domain/marcar-pago-dialog.tsx | 81 + frontend/components/domain/product-form.tsx | 116 + frontend/components/domain/stat-card.tsx | 40 + .../domain/tenant-status-dialog.tsx | 90 + frontend/components/domain/venda-form.tsx | 497 + frontend/components/layout/app-shell.tsx | 115 + frontend/components/layout/bottom-tab-bar.tsx | 82 + frontend/components/layout/breadcrumb.tsx | 46 + frontend/components/layout/drawer.tsx | 40 + frontend/components/layout/master-shell.tsx | 63 + frontend/components/layout/nav-list.tsx | 105 + frontend/components/layout/sidebar.tsx | 66 + frontend/components/layout/tenant-shell.tsx | 94 + frontend/components/layout/topbar.tsx | 52 + frontend/components/layout/user-footer.tsx | 48 + frontend/components/ui/badge.tsx | 45 + frontend/components/ui/button.tsx | 95 + frontend/components/ui/calendar.tsx | 186 + frontend/components/ui/card.tsx | 88 + frontend/components/ui/combobox.tsx | 109 + frontend/components/ui/command.tsx | 180 + frontend/components/ui/data-pagination.tsx | 86 + frontend/components/ui/data-table.tsx | 306 + frontend/components/ui/date-picker.tsx | 128 + frontend/components/ui/dialog.tsx | 145 + frontend/components/ui/empty-state.tsx | 41 + frontend/components/ui/form-field.tsx | 66 + frontend/components/ui/input-group.tsx | 144 + frontend/components/ui/input.tsx | 19 + frontend/components/ui/label.tsx | 21 + frontend/components/ui/popover.tsx | 74 + frontend/components/ui/responsive-dialog.tsx | 85 + frontend/components/ui/select.tsx | 185 + frontend/components/ui/separator.tsx | 28 + frontend/components/ui/sheet.tsx | 130 + frontend/components/ui/skeleton.tsx | 34 + frontend/components/ui/sonner.tsx | 45 + frontend/components/ui/status-pill.tsx | 58 + frontend/components/ui/table.tsx | 92 + frontend/components/ui/tabs.tsx | 80 + frontend/components/ui/textarea.tsx | 18 + frontend/eslint.config.mjs | 23 + frontend/hooks/use-media-query.ts | 32 + frontend/lib/api/alert-logs.ts | 35 + frontend/lib/api/alert-templates.ts | 113 + frontend/lib/api/asset-url.ts | 16 + frontend/lib/api/client.ts | 115 + frontend/lib/api/customers.ts | 107 + frontend/lib/api/dashboard.ts | 20 + frontend/lib/api/email-config.ts | 46 + frontend/lib/api/equipe.ts | 53 + frontend/lib/api/evolution-config.ts | 55 + frontend/lib/api/faturas.ts | 57 + frontend/lib/api/master-admins.ts | 15 + frontend/lib/api/planos.ts | 50 + frontend/lib/api/products.ts | 61 + frontend/lib/api/sale-payments.ts | 35 + frontend/lib/api/sales.ts | 127 + frontend/lib/api/tenant-alert-logs.ts | 21 + frontend/lib/api/tenant-alert-templates.ts | 69 + frontend/lib/api/tenant-dashboard.ts | 36 + frontend/lib/api/tenant-profile.ts | 37 + frontend/lib/api/tenants.ts | 97 + frontend/lib/api/types.ts | 15 + frontend/lib/api/whatsapp.ts | 74 + frontend/lib/auth/auth-context.tsx | 130 + frontend/lib/auth/jwt.ts | 25 + frontend/lib/auth/session-cookie.ts | 36 + frontend/lib/br-states.ts | 5 + frontend/lib/format.ts | 31 + frontend/lib/layout/nav-items.ts | 81 + frontend/lib/layout/sidebar-cookie.ts | 17 + frontend/lib/query/query-provider.tsx | 30 + frontend/lib/theme/contrast.ts | 67 + frontend/lib/theme/resolve-theme.ts | 32 + frontend/lib/theme/theme-provider.tsx | 109 + frontend/lib/toast.ts | 1 + frontend/lib/utils.ts | 6 + frontend/lib/validators/alert-templates.ts | 22 + frontend/lib/validators/auth.ts | 31 + frontend/lib/validators/customers.ts | 33 + frontend/lib/validators/email-config.ts | 43 + frontend/lib/validators/equipe.ts | 22 + frontend/lib/validators/evolution-config.ts | 21 + frontend/lib/validators/planos.ts | 23 + frontend/lib/validators/products.ts | 15 + frontend/lib/validators/sales.ts | 35 + .../lib/validators/tenant-alert-templates.ts | 18 + frontend/lib/validators/tenant-profile.ts | 16 + frontend/lib/validators/tenants.ts | 40 + frontend/lib/validators/whatsapp.ts | 11 + frontend/middleware.ts | 39 + frontend/next.config.ts | 29 + frontend/package-lock.json | 12193 ++++++++++++++++ frontend/package.json | 59 + frontend/postcss.config.mjs | 5 + frontend/styles/tokens.css | 65 + frontend/tsconfig.json | 27 + package-lock.json | 31 + package.json | 2 + src/app.module.ts | 2 + src/modules/auth/auth.controller.ts | 74 +- src/modules/auth/dto/logout.dto.ts | 6 +- src/modules/auth/dto/refresh.dto.ts | 6 +- .../equipe/dto/create-equipe-user.dto.ts | 22 + .../equipe/dto/update-equipe-user.dto.ts | 14 + src/modules/equipe/equipe.controller.ts | 51 + src/modules/equipe/equipe.module.ts | 9 + src/modules/equipe/equipe.service.ts | 159 + .../tenant-profile/dto/update-perfil.dto.ts | 11 + .../tenant-profile.controller.ts | 10 + .../tenant-profile/tenant-profile.service.ts | 38 + src/security.util.ts | 39 +- test/auth.e2e-spec.ts | 75 +- test/equipe-perfil.e2e-spec.ts | 452 + tsconfig.build.json | 2 +- tsconfig.json | 3 +- 179 files changed, 29312 insertions(+), 22 deletions(-) create mode 100755 .husky/pre-commit create mode 100644 Etapas/V2/00-README-ordem-das-etapas (1).md create mode 100644 Etapas/V2/10-frontend-schema-arquitetura.md create mode 100644 Etapas/V2/11-frontend-setup-projeto.md create mode 100644 Etapas/V2/12-frontend-design-system.md create mode 100644 Etapas/V2/13-frontend-autenticacao-tema.md create mode 100644 Etapas/V2/14-frontend-layout-navegacao.md create mode 100644 Etapas/V2/15-frontend-area-master.md create mode 100644 Etapas/V2/16-frontend-area-tenant-cadastros.md create mode 100644 Etapas/V2/17-frontend-area-tenant-vendas.md create mode 100644 Etapas/V2/18-frontend-area-tenant-integracoes-perfil.md create mode 100644 Etapas/V2/19-frontend-polimento-testes.md create mode 100644 Etapas/V2/20-controle-de-progresso-frontend.md create mode 100644 frontend/.env.local.example create mode 100644 frontend/.gitignore create mode 100644 frontend/.prettierrc create mode 100644 frontend/README.md create mode 100644 frontend/app/(public)/esqueci-senha/page.tsx create mode 100644 frontend/app/(public)/login/login-form.tsx create mode 100644 frontend/app/(public)/login/page.tsx create mode 100644 frontend/app/(public)/redefinir-senha/[token]/page.tsx create mode 100644 frontend/app/app/alertas/logs/page.tsx create mode 100644 frontend/app/app/alertas/templates/page.tsx create mode 100644 frontend/app/app/clientes/[id]/page.tsx create mode 100644 frontend/app/app/clientes/novo/page.tsx create mode 100644 frontend/app/app/clientes/page.tsx create mode 100644 frontend/app/app/dashboard/page.tsx create mode 100644 frontend/app/app/email-config/page.tsx create mode 100644 frontend/app/app/equipe/page.tsx create mode 100644 frontend/app/app/layout.tsx create mode 100644 frontend/app/app/perfil/page.tsx create mode 100644 frontend/app/app/produtos/[id]/page.tsx create mode 100644 frontend/app/app/produtos/novo/page.tsx create mode 100644 frontend/app/app/produtos/page.tsx create mode 100644 frontend/app/app/recebimentos/page.tsx create mode 100644 frontend/app/app/vendas/[id]/page.tsx create mode 100644 frontend/app/app/vendas/nova/page.tsx create mode 100644 frontend/app/app/vendas/page.tsx create mode 100644 frontend/app/app/whatsapp/page.tsx create mode 100644 frontend/app/dev/componentes/page.tsx create mode 100644 frontend/app/dev/componentes/showcase-client.tsx create mode 100644 frontend/app/diagnostico/page.tsx create mode 100644 frontend/app/favicon.ico create mode 100644 frontend/app/globals.css create mode 100644 frontend/app/layout.tsx create mode 100644 frontend/app/master/alertas/logs/page.tsx create mode 100644 frontend/app/master/alertas/templates/page.tsx create mode 100644 frontend/app/master/configuracoes/page.tsx create mode 100644 frontend/app/master/dashboard/page.tsx create mode 100644 frontend/app/master/evolution-config/page.tsx create mode 100644 frontend/app/master/faturas/page.tsx create mode 100644 frontend/app/master/layout.tsx create mode 100644 frontend/app/master/planos/page.tsx create mode 100644 frontend/app/master/tenants/[id]/page.tsx create mode 100644 frontend/app/master/tenants/novo/page.tsx create mode 100644 frontend/app/master/tenants/page.tsx create mode 100644 frontend/app/page.tsx create mode 100644 frontend/components.json create mode 100644 frontend/components/domain/confirm-dialog.tsx create mode 100644 frontend/components/domain/customer-quick-create-dialog.tsx create mode 100644 frontend/components/domain/marcar-pago-dialog.tsx create mode 100644 frontend/components/domain/product-form.tsx create mode 100644 frontend/components/domain/stat-card.tsx create mode 100644 frontend/components/domain/tenant-status-dialog.tsx create mode 100644 frontend/components/domain/venda-form.tsx create mode 100644 frontend/components/layout/app-shell.tsx create mode 100644 frontend/components/layout/bottom-tab-bar.tsx create mode 100644 frontend/components/layout/breadcrumb.tsx create mode 100644 frontend/components/layout/drawer.tsx create mode 100644 frontend/components/layout/master-shell.tsx create mode 100644 frontend/components/layout/nav-list.tsx create mode 100644 frontend/components/layout/sidebar.tsx create mode 100644 frontend/components/layout/tenant-shell.tsx create mode 100644 frontend/components/layout/topbar.tsx create mode 100644 frontend/components/layout/user-footer.tsx create mode 100644 frontend/components/ui/badge.tsx create mode 100644 frontend/components/ui/button.tsx create mode 100644 frontend/components/ui/calendar.tsx create mode 100644 frontend/components/ui/card.tsx create mode 100644 frontend/components/ui/combobox.tsx create mode 100644 frontend/components/ui/command.tsx create mode 100644 frontend/components/ui/data-pagination.tsx create mode 100644 frontend/components/ui/data-table.tsx create mode 100644 frontend/components/ui/date-picker.tsx create mode 100644 frontend/components/ui/dialog.tsx create mode 100644 frontend/components/ui/empty-state.tsx create mode 100644 frontend/components/ui/form-field.tsx create mode 100644 frontend/components/ui/input-group.tsx create mode 100644 frontend/components/ui/input.tsx create mode 100644 frontend/components/ui/label.tsx create mode 100644 frontend/components/ui/popover.tsx create mode 100644 frontend/components/ui/responsive-dialog.tsx create mode 100644 frontend/components/ui/select.tsx create mode 100644 frontend/components/ui/separator.tsx create mode 100644 frontend/components/ui/sheet.tsx create mode 100644 frontend/components/ui/skeleton.tsx create mode 100644 frontend/components/ui/sonner.tsx create mode 100644 frontend/components/ui/status-pill.tsx create mode 100644 frontend/components/ui/table.tsx create mode 100644 frontend/components/ui/tabs.tsx create mode 100644 frontend/components/ui/textarea.tsx create mode 100644 frontend/eslint.config.mjs create mode 100644 frontend/hooks/use-media-query.ts create mode 100644 frontend/lib/api/alert-logs.ts create mode 100644 frontend/lib/api/alert-templates.ts create mode 100644 frontend/lib/api/asset-url.ts create mode 100644 frontend/lib/api/client.ts create mode 100644 frontend/lib/api/customers.ts create mode 100644 frontend/lib/api/dashboard.ts create mode 100644 frontend/lib/api/email-config.ts create mode 100644 frontend/lib/api/equipe.ts create mode 100644 frontend/lib/api/evolution-config.ts create mode 100644 frontend/lib/api/faturas.ts create mode 100644 frontend/lib/api/master-admins.ts create mode 100644 frontend/lib/api/planos.ts create mode 100644 frontend/lib/api/products.ts create mode 100644 frontend/lib/api/sale-payments.ts create mode 100644 frontend/lib/api/sales.ts create mode 100644 frontend/lib/api/tenant-alert-logs.ts create mode 100644 frontend/lib/api/tenant-alert-templates.ts create mode 100644 frontend/lib/api/tenant-dashboard.ts create mode 100644 frontend/lib/api/tenant-profile.ts create mode 100644 frontend/lib/api/tenants.ts create mode 100644 frontend/lib/api/types.ts create mode 100644 frontend/lib/api/whatsapp.ts create mode 100644 frontend/lib/auth/auth-context.tsx create mode 100644 frontend/lib/auth/jwt.ts create mode 100644 frontend/lib/auth/session-cookie.ts create mode 100644 frontend/lib/br-states.ts create mode 100644 frontend/lib/format.ts create mode 100644 frontend/lib/layout/nav-items.ts create mode 100644 frontend/lib/layout/sidebar-cookie.ts create mode 100644 frontend/lib/query/query-provider.tsx create mode 100644 frontend/lib/theme/contrast.ts create mode 100644 frontend/lib/theme/resolve-theme.ts create mode 100644 frontend/lib/theme/theme-provider.tsx create mode 100644 frontend/lib/toast.ts create mode 100644 frontend/lib/utils.ts create mode 100644 frontend/lib/validators/alert-templates.ts create mode 100644 frontend/lib/validators/auth.ts create mode 100644 frontend/lib/validators/customers.ts create mode 100644 frontend/lib/validators/email-config.ts create mode 100644 frontend/lib/validators/equipe.ts create mode 100644 frontend/lib/validators/evolution-config.ts create mode 100644 frontend/lib/validators/planos.ts create mode 100644 frontend/lib/validators/products.ts create mode 100644 frontend/lib/validators/sales.ts create mode 100644 frontend/lib/validators/tenant-alert-templates.ts create mode 100644 frontend/lib/validators/tenant-profile.ts create mode 100644 frontend/lib/validators/tenants.ts create mode 100644 frontend/lib/validators/whatsapp.ts create mode 100644 frontend/middleware.ts create mode 100644 frontend/next.config.ts create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/postcss.config.mjs create mode 100644 frontend/styles/tokens.css create mode 100644 frontend/tsconfig.json create mode 100644 src/modules/equipe/dto/create-equipe-user.dto.ts create mode 100644 src/modules/equipe/dto/update-equipe-user.dto.ts create mode 100644 src/modules/equipe/equipe.controller.ts create mode 100644 src/modules/equipe/equipe.module.ts create mode 100644 src/modules/equipe/equipe.service.ts create mode 100644 src/modules/tenant-profile/dto/update-perfil.dto.ts create mode 100644 test/equipe-perfil.e2e-spec.ts diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..f450749 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +cd frontend && npx lint-staged diff --git a/Etapas/09-controle-de-progresso.md b/Etapas/09-controle-de-progresso.md index 35d0b2b..79ec071 100644 --- a/Etapas/09-controle-de-progresso.md +++ b/Etapas/09-controle-de-progresso.md @@ -181,6 +181,26 @@ propositalmente para depois — detalhar em Observações). `npm run lint`, `tsc --noEmit` e `npm run build` (produção) também passam sem erros; smoke test manual via `curl` contra a imagem `production` real confirmou login + guards funcionando fim a fim. +- **[2026-08-15, revisitado pela Etapa 13 do frontend]** Adicionado + suporte a cookie httpOnly para o refresh token (`security.util.ts`, + `auth.controller.ts`, `RefreshDto`/`LogoutDto`) — mudança aditiva, os + 21 testes originais continuam passando sem alteração, mais 4 novos + cobrindo o cookie (total 24/24 em `auth.e2e-spec.ts`). Detalhe completo + em `Etapas/V2/20-controle-de-progresso-frontend.md`, Etapa 13. +- **[2026-08-16, revisitado pela Etapa 18 do frontend]** `tenant_users` + (criada nesta etapa) ganhou CRUD real do lado do tenant: novo módulo + `src/modules/equipe/` (`GET/POST /tenant/equipe`, `PATCH + /tenant/equipe/:id`, só `tenant_admin`, RLS via `TenantContextService` + como `customers`/`products` da Etapa 5) — o `GET /auth/tenant/users` + deste módulo continua exatamente como estava, só leitura/demonstração, + não foi tocado. Também aditivo: `PUT /tenant/perfil` + (`nome_fantasia`/`razao_social`) em `src/modules/tenant-profile/`, ao + lado do `PUT /tenant/perfil/cor` já existente da Etapa 8. Mudança + autorizada explicitamente pelo usuário antes de implementar (a Etapa 18 + do frontend precisava de CRUD real de equipe e edição de perfil, que + não existiam). Detalhe completo — DTOs, regras de limite de plano, + auto-bloqueio, testes e2e — em + `Etapas/V2/20-controle-de-progresso-frontend.md`, Etapa 18. --- diff --git a/Etapas/V2/00-README-ordem-das-etapas (1).md b/Etapas/V2/00-README-ordem-das-etapas (1).md new file mode 100644 index 0000000..1a2c293 --- /dev/null +++ b/Etapas/V2/00-README-ordem-das-etapas (1).md @@ -0,0 +1,81 @@ +# SeConectai Vendas — Prompts de Build Faseado + +Este pacote contém 8 prompts sequenciais e autocontidos para entregar a uma +IA de geração de código (Claude Code, Cursor, Windsurf etc). Cada arquivo +pode ser colado como um prompt único — eles já trazem o contexto necessário +para não depender de memória de conversa anterior. + +## Ordem obrigatória — Backend + +1. `01-setup-projeto.md` — estrutura base, Docker Compose, Postgres, Prisma +2. `02-autenticacao-rls.md` — login master/tenant, JWT, RLS +3. `03-painel-master-tenants.md` — CRUD de tenants, planos, faturas +4. `04-integracoes-evolution-email.md` — Evolution API, instâncias WhatsApp, SMTP +5. `05-painel-tenant-customers-produtos.md` — CRUD de clientes e produtos/serviços +6. `06-vendas-dashboards.md` — vendas, parcelas, resumos +7. `07-motor-de-alertas.md` — BullMQ, templates, disparo e logs +8. `08-personalizacao-whitelabel.md` — logo/cor por tenant, ajustes finais + +Controle de progresso: `09-controle-de-progresso.md` + +## Ordem obrigatória — Frontend + +Pré-requisito: backend concluído (ou ao menos as etapas cujos endpoints +cada tela consome). Arquitetura de referência: +`10-frontend-schema-arquitetura.md`. + +11. `11-frontend-setup-projeto.md` — Next.js, Tailwind, tokens, estrutura +12. `12-frontend-design-system.md` — biblioteca de componentes base +13. `13-frontend-autenticacao-tema.md` — login, sessão, tema dinâmico whitelabel +14. `14-frontend-layout-navegacao.md` — sidebar/topbar/drawer/bottom-nav +15. `15-frontend-area-master.md` — telas de negócio do painel master +16. `16-frontend-area-tenant-cadastros.md` — dashboard, clientes, produtos +17. `17-frontend-area-tenant-vendas.md` — vendas, parcelas, financeiro +18. `18-frontend-area-tenant-integracoes-perfil.md` — WhatsApp, e-mail, alertas, equipe, whitelabel +19. `19-frontend-polimento-testes.md` — responsividade, acessibilidade, E2E + +Controle de progresso: `20-controle-de-progresso-frontend.md` + +## Arquivos de controle de progresso (obrigatórios) + +`09-controle-de-progresso.md` (backend) e +`20-controle-de-progresso-frontend.md` (frontend) são o estado oficial +de cada parte do projeto. Regras: + +- **Sempre envie o arquivo correspondente junto com o prompt de cada + etapa** (o de backend para as etapas 1–8, o de frontend para as + etapas 11–19). +- A IA deve lê-lo antes de começar e recusar iniciar uma etapa se a + anterior não estiver 100% concluída na checklist. +- Ao final de cada etapa, a IA deve devolver o arquivo atualizado (itens + marcados, status geral e observações preenchidas) — é esse arquivo + atualizado que você guarda e reenvia na próxima etapa/sessão. +- Sem esse arquivo, uma IA nova (em outra sessão) não tem como saber o + que já foi feito — por isso ele substitui a necessidade de repetir todo + o histórico da conversa. + +## Como usar cada prompt + +- Rode nessa ordem — cada etapa assume que a anterior está pronta e + funcionando (migrations aplicadas, testes passando). +- Ao final de cada etapa, confira o checklist de "Critérios de aceite" + antes de avançar para a próxima. Se algo não for atendido, peça correção + antes de seguir. +- O arquivo `seconectai-vendas-schema.sql` é a fonte de verdade do modelo + de dados — qualquer etapa que crie migration deve refletir exatamente + essas tabelas (a IA pode adaptar para sintaxe Prisma, mas a estrutura, + nomes de campo e tipos devem ser preservados). +- Cada prompt já repete o essencial do contexto (stack, convenções), então + eles funcionam mesmo em uma conversa nova/sessão nova com a IA. + +## Stack fixada em todas as etapas + +- Node.js 20+ / TypeScript +- NestJS (modular por domínio) +- PostgreSQL 16 + Prisma ORM +- Redis + BullMQ (a partir da etapa 7) +- Docker + Docker Compose +- Autenticação: JWT (access curto) + refresh token com rotação, senha com + argon2id +- Multi-tenancy: banco único, isolamento por `tenant_id` + Row Level + Security do Postgres diff --git a/Etapas/V2/10-frontend-schema-arquitetura.md b/Etapas/V2/10-frontend-schema-arquitetura.md new file mode 100644 index 0000000..75beb3d --- /dev/null +++ b/Etapas/V2/10-frontend-schema-arquitetura.md @@ -0,0 +1,261 @@ +# SeConectai Vendas — Schema de Frontend (Web Responsivo) + +Este documento define a arquitetura do frontend que vai consumir a API +já especificada nas etapas 1–8. Cobre estrutura de telas, navegação, +sistema de design e a estratégia de responsividade para funcionar bem +tanto em tela de PC quanto em smartphone. + +## 1. Visão geral + +Uma única aplicação, com duas áreas autenticadas separadas: + +- **Área master** (`/master/...`) — seu painel, gestão dos tenants. +- **Área tenant** (`/app/...`) — painel que seus clientes usam no dia a + dia (vendas, clientes, produtos, alertas). + +Ambas consomem a mesma API REST do backend. O layout de cada área é +diferente (menu, cores, permissões), mas compartilham o mesmo sistema de +design (componentes, tokens, grid responsivo). + +## 2. Stack sugerida + +- **Next.js 14+ (App Router)** + React 18 + TypeScript +- **Tailwind CSS** para estilos utilitários, com tokens de design via + CSS variables (necessário para o whitelabel: cor primária muda por + tenant em tempo de execução, não pode ser fixa no build) +- **shadcn/ui** como base de componentes acessíveis, customizados por + cima (não usar como está "fora da caixa" — ajustar aos tokens do + projeto) +- **TanStack Query (React Query)** para cache/estado de dados vindos da + API (listagens, dashboards) +- **React Hook Form + Zod** para formulários e validação — os schemas + Zod devem espelhar os DTOs do backend +- **Recharts** para os gráficos de dashboard (resumo de vendas etc) +- Estado de UI leve (sidebar aberta/fechada, tema) via Context/Zustand — + não precisa de Redux para este escopo + +## 3. Estratégia de responsividade + +Mobile-first. Breakpoints (padrão Tailwind, manter): + +| Breakpoint | Largura | Uso principal | +|------------|-----------|----------------| +| base | < 640px | smartphone | +| `sm` | ≥ 640px | smartphone grande / phablet | +| `md` | ≥ 768px | tablet | +| `lg` | ≥ 1024px | desktop | +| `xl` | ≥ 1280px | desktop grande | + +Regras de layout por faixa: + +- **Desktop (`lg` e acima):** sidebar fixa lateral (~240px) + topbar. + Conteúdo em grid de 2 colunas quando fizer sentido (ex: formulário + + resumo lateral). +- **Tablet (`md`):** sidebar colapsada só com ícones (expande em hover + ou toque), conteúdo em coluna única ou 2 colunas conforme espaço. +- **Mobile (abaixo de `md`):** + - Sidebar vira **drawer** (menu lateral que desliza, acionado por + ícone de hambúrguer na topbar). + - **Bottom tab bar fixa** com os 4–5 itens mais usados de cada área + (ex. no tenant: Dashboard, Clientes, Vendas, Recebimentos, Mais). + - Tabelas **não** fazem scroll horizontal como solução padrão — cada + linha vira um **card empilhado** (rótulo: valor), com as + informações mais importantes em destaque e as secundárias + colapsadas/expansíveis. + - Formulários em coluna única; campos grandes o suficiente para touch + (mínimo 44x44px de área de toque). + - Modais viram **tela cheia** ou **bottom sheet** (desliza de baixo), + nunca uma caixa pequena centralizada como no desktop. + - Filtros de listagem (que no desktop ficam visíveis lateralmente) + viram um botão "Filtros" que abre um bottom sheet no mobile. + +Checklist de teste obrigatório em **toda** tela construída: 375px +(mobile pequeno), 768px (tablet) e 1280px (desktop), sem scroll +horizontal indevido em nenhuma delas. + +## 4. Mapa de rotas + +### Público (sem autenticação) +``` +/login → detecta master ou tenant pelo e-mail/domínio +/esqueci-senha +/redefinir-senha/:token +``` + +### Área master — `/master` +``` +/master/dashboard +/master/tenants (lista) +/master/tenants/novo +/master/tenants/:id (detalhe, edição, faturas do tenant) +/master/planos +/master/faturas (visão consolidada de todas as mensalidades) +/master/evolution-config +/master/alertas/templates +/master/alertas/logs +/master/configuracoes (perfil do admin master, senha, MFA) +``` + +### Área tenant — `/app` +``` +/app/dashboard +/app/clientes (lista) +/app/clientes/novo +/app/clientes/:id (detalhe, contatos, histórico de vendas) +/app/produtos (produtos e serviços) +/app/produtos/novo +/app/produtos/:id +/app/vendas (lista) +/app/vendas/nova +/app/vendas/:id (detalhe, itens, parcelas) +/app/recebimentos (contas a receber, marcar pago) +/app/whatsapp (instâncias Evolution, QR code, status) +/app/email-config +/app/alertas/templates +/app/alertas/logs +/app/equipe (usuários do tenant, só para tenant_admin) +/app/perfil (logo, cor, dados da empresa) +``` + +## 5. Navegação e layout por área + +**Desktop:** +- Sidebar: logo do tenant (ou logo master) no topo, itens de menu com + ícone + label, indicador do item ativo, avatar do usuário + logout no + rodapé. +- Topbar: breadcrumb da página atual, sino de notificações (alertas + recentes), busca rápida (clientes/vendas). + +**Mobile:** +- Topbar fixa: logo compacta (só o símbolo, não o nome completo) + + ícone de menu (drawer) + sino de notificações. +- Drawer: réplica do menu completo da sidebar desktop, ocupa a tela + inteira ou 80% da largura. +- Bottom tab bar: 4 itens fixos mais "Mais" (que abre o restante do + menu). Ícone + label curto embaixo. + +## 6. Sistema de design (tokens) + +Paleta **neutra da plataforma** (usada em textos, fundos, bordas — +igual em master e em todos os tenants): + +- `--neutral-950: #12141A` — texto principal +- `--neutral-500: #6B7280` — texto secundário +- `--neutral-200: #E5E7EB` — bordas +- `--neutral-50: #F8F9FB` — fundo de página +- `--surface: #FFFFFF` — fundo de cards/painéis +- `--danger: #DC2626` — erros, valores atrasados +- `--success: #16A34A` — pagos, confirmados +- `--warning: #D97706` — pendências, avisos + +Cor de marca **do painel master** (fixa, identidade do SeConectai +Vendas): `--brand-master: #1E3A5F` (azul petróleo escuro) com acento +`--brand-master-accent: #2DD4BF` (verde-água) — usada apenas na área +`/master`. + +Cor de marca **de cada tenant** (dinâmica, whitelabel): variável +`--color-primary`, resolvida em runtime a partir de +`tenants.cor_primaria` (endpoint público de tema, já definido na Etapa +8 do backend) e aplicada via CSS variable no layout de `/app`. Se o +tenant não definiu cor própria, cair para um azul neutro padrão +(`#2563EB`). Validar contraste mínimo (WCAG AA) contra branco antes de +aplicar — se a cor escolhida pelo tenant não passar, usar uma versão +escurecida automaticamente para texto sobre fundo colorido. + +Tipografia: uma família para UI (ex: Inter ou similar, boa legibilidade +em telas pequenas e números tabulares para valores monetários) e uso de +`font-variant-numeric: tabular-nums` em qualquer coluna de valores para +alinhamento visual em tabelas/cards. + +## 7. Componentes de design system (biblioteca a construir) + +Base (`components/ui/`): Button, Input, Select, DatePicker, Textarea, +Table (com variante que vira lista de Cards abaixo do breakpoint `md`), +Modal/Sheet (adaptável tela-cheia no mobile), Toast, Badge/StatusPill +(para status de fatura/parcela/venda), Card, Pagination, EmptyState, +Skeleton (loading), Tabs, FormField (wrapper de label+erro+input). + +Layout (`components/layout/`): Sidebar, Topbar, Drawer, BottomTabBar, +Breadcrumb. + +Domínio (`components/domain/`): ClienteCard, VendaForm, ParcelaRow, +ProdutoForm, StatusFaturaBadge, ResumoVendasChart, InstanciaWhatsappCard. + +## 8. Dados e autenticação no client + +- **TanStack Query** para toda chamada de leitura (listagens, + dashboards, detalhes), com invalidação automática após mutations + (criar/editar/excluir). +- **Zod schemas** espelhando os DTOs do backend — usados tanto na + validação de formulário quanto na tipagem das respostas da API (fonte + única de verdade dos tipos no frontend). +- **Access token em memória** (não em `localStorage`, para reduzir + superfície de XSS); **refresh token em cookie `httpOnly`**, setado + pelo backend. Interceptor de requisição renova o access token + automaticamente em caso de 401 e repete a chamada original. +- **Middleware do Next.js** protegendo `/master/*` e `/app/*`, + verificando sessão válida antes de renderizar a página (evita "flash" + de conteúdo protegido). +- Tema (cor primária + logo) resolvido no login: chama o endpoint + público de tema com o identificador do tenant, aplica as CSS + variables antes mesmo do usuário autenticar (tela de login já com a + cara do tenant certo). + +## 9. Estrutura de pastas sugerida + +``` +frontend/ +├── app/ +│ ├── (public)/ +│ │ ├── login/ +│ │ ├── esqueci-senha/ +│ │ └── redefinir-senha/[token]/ +│ ├── master/ +│ │ ├── layout.tsx +│ │ ├── dashboard/ +│ │ ├── tenants/ +│ │ ├── planos/ +│ │ ├── faturas/ +│ │ ├── evolution-config/ +│ │ └── alertas/ +│ └── app/ +│ ├── layout.tsx +│ ├── dashboard/ +│ ├── clientes/ +│ ├── produtos/ +│ ├── vendas/ +│ ├── recebimentos/ +│ ├── whatsapp/ +│ ├── email-config/ +│ ├── alertas/ +│ ├── equipe/ +│ └── perfil/ +├── components/ +│ ├── ui/ +│ ├── layout/ +│ └── domain/ +├── lib/ +│ ├── api/ (client HTTP + funções por domínio) +│ ├── auth/ +│ ├── theme/ +│ └── validators/ (schemas Zod) +├── hooks/ +└── styles/ + └── tokens.css (CSS variables da paleta e tipografia) +``` + +## 10. Checklist de responsividade (aplicar em toda tela nova) + +- [ ] Testado em 375px, 768px e 1280px sem overflow horizontal indevido +- [ ] Tabelas de listagem viram cards empilhados abaixo de `md` +- [ ] Navegação principal acessível via bottom tab bar ou drawer no mobile +- [ ] Touch targets ≥ 44x44px em elementos interativos no mobile +- [ ] Formulário utilizável com teclado virtual aberto (campo em foco + não fica escondido atrás do teclado) +- [ ] Modais/diálogos viram tela cheia ou bottom sheet no mobile +- [ ] Cor primária dinâmica do tenant aplicada corretamente em + botões/links/destaques, com contraste validado +- [ ] Foco de teclado visível em todos os elementos interativos + (acessibilidade, não só mobile) +- [ ] Estados de carregamento (skeleton) e vazio (empty state) tratados + em toda listagem diff --git a/Etapas/V2/11-frontend-setup-projeto.md b/Etapas/V2/11-frontend-setup-projeto.md new file mode 100644 index 0000000..58b7678 --- /dev/null +++ b/Etapas/V2/11-frontend-setup-projeto.md @@ -0,0 +1,92 @@ +# Etapa 11 — Setup do Projeto Frontend + +## Contexto do projeto + +Estou construindo o frontend do **SeConectai Vendas** — CRM/vendas +multi-tenant. O backend (API REST em NestJS) já está pronto, seguindo o +schema e as etapas descritas em documentos separados. O frontend consome +essa API e tem duas áreas: painel **master** (`/master`) e painel +**tenant** (`/app`). + +A arquitetura completa (rotas, breakpoints, tokens de design, estrutura +de pastas) está definida no arquivo `10-frontend-schema-arquitetura.md` +— **use esse arquivo como fonte de verdade** para tudo que for construído +nesta e nas próximas etapas. + +Esta é a **Etapa 1 de 9 do frontend** (numerada como Etapa 11 no conjunto +geral do projeto, para não colidir com as 8 etapas do backend). Aqui é +só a fundação — nenhuma tela de negócio ainda. + +## Objetivo desta etapa + +Inicializar o projeto Next.js com toda a configuração base: Tailwind com +os tokens de design do schema, estrutura de pastas, variáveis de +ambiente, lint/format, e uma página inicial validando a conexão com a +API do backend. + +## Tarefas + +1. **Inicialização do projeto** + - Next.js 14+ com App Router, TypeScript, ESLint já integrado. + - Configurar Tailwind CSS. + - Configurar Prettier + regra de integração com ESLint (sem conflito + de formatação). + +2. **Tokens de design** + - Criar `styles/tokens.css` com todas as CSS variables definidas na + seção 6 do schema (`--neutral-*`, `--surface`, `--danger`, + `--success`, `--warning`, `--brand-master`, `--brand-master-accent`, + `--color-primary` com fallback `#2563EB`). + - Configurar `tailwind.config.ts` para expor essas variáveis como + cores utilitárias (ex: `bg-primary`, `text-danger`), em vez de usar + `var(--...)` espalhado pelo código. + - Configurar fonte (Inter ou equivalente definida no schema), incluindo + `font-variant-numeric: tabular-nums` disponível como classe utilitária + para colunas de valores monetários. + +3. **Estrutura de pastas** + - Criar exatamente a estrutura de pastas da seção 9 do schema: + `app/(public)/`, `app/master/`, `app/app/`, `components/ui/`, + `components/layout/`, `components/domain/`, `lib/api/`, + `lib/auth/`, `lib/theme/`, `lib/validators/`, `hooks/`, `styles/`. + - Cada pasta de rota deve existir com um `page.tsx` placeholder (ex: + "Em construção — Etapa X") para validar que o roteamento do mapa de + rotas inteiro já resolve sem erro 404, mesmo antes das telas reais + existirem. + +4. **Variáveis de ambiente e cliente HTTP base** + - `.env.local.example` com `NEXT_PUBLIC_API_URL` e demais chaves que + forem necessárias. + - Criar `lib/api/client.ts`: wrapper mínimo sobre `fetch` (ou axios, + justificar a escolha) com base URL configurável, tratamento de erro + padronizado (parse de erro da API em formato consistente), e um + ponto único de onde o interceptor de refresh token será plugado na + Etapa 13 (deixar o gancho pronto, sem implementar auth ainda). + +5. **Página de diagnóstico** + - Uma rota simples (ex: `/diagnostico`, pode ser removida depois) que + chama `GET /health` do backend e mostra o resultado — só para + confirmar que a integração frontend↔backend está funcionando de + ponta a ponta antes de seguir. + +6. **Qualidade** + - Scripts no `package.json`: `dev`, `build`, `start`, `lint`, + `format`. + - Configurar Husky + lint-staged (ou equivalente) para rodar lint no + pre-commit, se fizer sentido para o fluxo do projeto. + +## Critérios de aceite + +- [ ] `npm run dev` sobe o projeto sem erro +- [ ] Todas as rotas do mapa de rotas (seção 4 do schema) resolvem, com + página placeholder, sem 404 +- [ ] `tokens.css` contém todas as variáveis do schema, aplicadas + corretamente (inspecionar no DevTools que os valores computados + batem) +- [ ] Classes utilitárias do Tailwind (`bg-primary`, `text-danger` etc) + funcionam e refletem os tokens +- [ ] `/diagnostico` mostra o retorno real de `GET /health` do backend + (com o backend rodando local) +- [ ] `npm run lint` roda sem erro +- [ ] Estrutura de pastas idêntica à definida na seção 9 do schema de + frontend diff --git a/Etapas/V2/12-frontend-design-system.md b/Etapas/V2/12-frontend-design-system.md new file mode 100644 index 0000000..c590789 --- /dev/null +++ b/Etapas/V2/12-frontend-design-system.md @@ -0,0 +1,105 @@ +# Etapa 12 — Sistema de Design e Componentes Base + +## Contexto do projeto + +Continuação do frontend do **SeConectai Vendas**. A Etapa 11 entregou o +projeto Next.js configurado com Tailwind, tokens de design e estrutura +de pastas. Nenhum componente de UI reutilizável existe ainda. + +Referência obrigatória: seção 6 (tokens) e seção 7 (lista de +componentes) do `10-frontend-schema-arquitetura.md`. + +## Objetivo desta etapa + +Construir a biblioteca de componentes base (`components/ui/`) que será +usada por **todas** as telas de negócio das próximas etapas. Tudo aqui +deve já nascer responsivo e acessível — as telas de negócio só vão +compor esses componentes, não reimplementar comportamento responsivo +caso a caso. + +## Tarefas + +1. **Base a partir do shadcn/ui** + - Instalar e configurar shadcn/ui, mas customizar cada componente + usado para consumir os tokens do projeto (não deixar cores/spacing + hardcoded vindos do template padrão). + - Justificar no código (comentário curto) qualquer componente que + fugir do padrão shadcn por necessidade específica do projeto. + +2. **Componentes a construir** (todos em `components/ui/`, cada um com + suas variantes documentadas via comentário ou arquivo `.stories` + opcional): + + - **Button**: variantes `primary` (usa `--color-primary`), + `secondary`, `destructive`, `ghost`; tamanhos `sm`/`md`/`lg`; estado + `loading` (spinner substituindo o label, sem mudar a largura do + botão); estado `disabled`. + - **Input / Textarea**: com label, texto de ajuda, estado de erro + (borda + mensagem), prefixo/sufixo opcional (ex: "R$" para campos + de valor). + - **Select**: nativo estilizado ou baseado em Radix (via shadcn), + com busca para listas longas (ex: seleção de cliente/produto). + - **DatePicker**: seleção de data única e, quando necessário, período + (usado nos filtros de dashboard/listagens). + - **FormField**: wrapper que padroniza label + input + mensagem de + erro + texto de ajuda — todo formulário do sistema deve usar esse + wrapper, nunca montar label/erro manualmente. + - **Table**: **componente central desta etapa**. Recebe colunas e + dados de forma genérica (tipado com generics em TypeScript). + Abaixo do breakpoint `md`, renderiza automaticamente como lista de + **Cards empilhados** (cada linha vira um card com pares + rótulo:valor, permitindo destacar 1–2 campos principais e colapsar + o resto). Suporta: ordenação por coluna, estado de loading + (skeleton), estado vazio (usa o `EmptyState`), paginação integrada. + - **Modal / Sheet**: um único componente que se comporta como modal + centralizado no desktop e como tela cheia (ou bottom sheet, definir + e justificar qual) no mobile, controlado pelo mesmo breakpoint `md`. + - **Toast**: sistema de notificação temporária (sucesso, erro, aviso, + info), empilhável, com auto-dismiss configurável. + - **Badge / StatusPill**: variantes semânticas mapeadas para os + status de negócio já existentes no backend (`pendente`, `pago`, + `atrasado`, `cancelado`, `ativo`, `inadimplente`, `suspenso`, + `trial`, `confirmada`, `concluida`, `orcamento`) — cada status com + cor e label em português já resolvidos aqui, para não repetir esse + mapeamento em cada tela. + - **Card**: contêiner base usado em dashboards e nas listas + responsivas da Table. + - **Pagination**: controles de página, compatível com os parâmetros + de paginação da API do backend. + - **EmptyState**: ilustração/texto + ação sugerida, usado em toda + listagem vazia (ex: "Nenhum cliente cadastrado ainda" + botão + "Cadastrar cliente"). + - **Skeleton**: blocos de carregamento reutilizáveis (linha de texto, + card, linha de tabela). + - **Tabs**: navegação em abas (usado, por exemplo, no detalhe de + venda: Itens / Parcelas / Histórico). + +3. **Acessibilidade (obrigatório, não opcional)** + - Todos os componentes interativos com foco de teclado visível + (`outline` customizado com a cor de foco do design system, não o + padrão do navegador nem `outline: none` sem substituto). + - Componentes baseados em Radix/shadcn já trazem boa parte da + semântica ARIA — não remover isso ao customizar. + - Respeitar `prefers-reduced-motion` em qualquer transição/animação. + - Touch targets mínimos de 44x44px nos componentes interativos. + +4. **Página de showcase** + - Criar uma rota de desenvolvimento (ex: `/dev/componentes`, não + exposta em produção) que renderiza todos os componentes acima com + suas variantes, para revisão visual rápida em qualquer breakpoint + sem precisar montar uma tela de negócio primeiro. + +## Critérios de aceite + +- [ ] Todos os componentes listados existem, tipados em TypeScript, com + as variantes descritas +- [ ] `Table` alterna corretamente entre tabela (≥768px) e cards + empilhados (<768px) na página de showcase +- [ ] `Modal/Sheet` alterna corretamente entre modal centralizado + (desktop) e tela cheia/bottom sheet (mobile) +- [ ] `StatusPill` cobre todos os status de negócio listados, com cor e + label em português +- [ ] Navegação por teclado (Tab) percorre todos os componentes + interativos da página de showcase com foco visível +- [ ] Nenhum componente quebra ou tem overflow horizontal em 375px +- [ ] Página de showcase acessível apenas em ambiente de desenvolvimento diff --git a/Etapas/V2/13-frontend-autenticacao-tema.md b/Etapas/V2/13-frontend-autenticacao-tema.md new file mode 100644 index 0000000..ed06a5a --- /dev/null +++ b/Etapas/V2/13-frontend-autenticacao-tema.md @@ -0,0 +1,102 @@ +# Etapa 13 — Autenticação e Tema Dinâmico (Whitelabel) + +## Contexto do projeto + +Continuação do frontend do **SeConectai Vendas**. Já existem: projeto +configurado (Etapa 11) e a biblioteca de componentes base (Etapa 12). O +backend já expõe toda a API de autenticação (login master/tenant, +refresh, esqueci senha) e o endpoint público de tema por tenant +(definidos nas Etapas 2 e 8 do backend). + +## Objetivo desta etapa + +Implementar as telas públicas de autenticação, o mecanismo de sessão no +client (armazenamento de token, renovação automática, proteção de +rotas), e a resolução do tema (logo + cor) por tenant antes mesmo do +login. + +## Tarefas + +1. **Telas públicas (`app/(public)/`)** + - **`/login`**: formulário de e-mail + senha. Deve detectar se o + login é de master ou de tenant (definir estratégia: campo + oculto/toggle, ou dois formulários, ou detecção automática pelo + backend a partir do e-mail — escolher a mais simples de implementar + e documentar a decisão). Erros de credencial inválida devem ser + claros mas não revelar se o e-mail existe ou não (mensagem genérica + "e-mail ou senha inválidos"). + - **`/esqueci-senha`**: formulário de e-mail, chama o endpoint de + recuperação, sempre mostra mensagem de sucesso genérica (não + revelar se o e-mail existe no sistema). + - **`/redefinir-senha/[token]`**: formulário de nova senha (com + confirmação), valida força mínima no client espelhando a regra do + backend, trata token expirado/inválido com mensagem clara. + - Todas essas telas devem ser **totalmente responsivas**: em mobile, + formulário ocupa a largura da tela com padding adequado; em + desktop, formulário centralizado com largura máxima (ex: 420px) e + fundo com identidade visual do tema resolvido (ver item 3). + +2. **Gerenciamento de sessão (`lib/auth/`)** + - Access token guardado **em memória** (ex: em um Context/store, não + em `localStorage`/`sessionStorage`). + - Refresh token: o backend deve setar como cookie `httpOnly`; o + client não manipula esse cookie diretamente, apenas confia que ele + é enviado automaticamente nas chamadas relevantes. + - Interceptor no `lib/api/client.ts` (gancho deixado pronto na Etapa + 11): ao receber 401, tenta chamar o endpoint de refresh + automaticamente; se conseguir, repete a requisição original com o + novo token; se falhar, desloga e redireciona para `/login`. + - Função `logout()` que limpa o estado em memória e chama o endpoint + de logout do backend (revoga o refresh token no servidor). + +3. **Middleware de proteção de rotas** + - Middleware do Next.js protegendo `/master/*` e `/app/*`: verifica + se existe sessão válida (ex: checando um cookie de sinalização não + sensível, já que o access token real fica só em memória no client — + definir estratégia de verificação no servidor sem expor o token) e + redireciona para `/login` se não houver. + - Usuário master tentando acessar `/app/*` (ou vice-versa) deve ser + redirecionado para a área correta, não para login. + - Após login bem-sucedido, redirecionar automaticamente para + `/master/dashboard` ou `/app/dashboard` conforme o tipo de usuário + retornado pela API. + +4. **Tema dinâmico por tenant (`lib/theme/`)** + - Na tela `/login`, antes de qualquer autenticação, resolver o tema: + se a URL/subdomínio/parâmetro identificar um tenant específico, + chamar o endpoint público `/public/tenants/:slugOuId/tema` e + aplicar `--color-primary` e o logo retornados. Se não houver tenant + identificável (ex: acesso genérico), mostrar o tema padrão da + plataforma. + - Após o login, na área `/app`, resolver o tema a partir do perfil do + tenant autenticado (não precisa mais do endpoint público). + - Implementar a validação de contraste descrita na seção 6 do schema: + se `cor_primaria` do tenant não atingir contraste mínimo (WCAG AA) + contra branco, escurecer automaticamente antes de aplicar como cor + de texto/ícone sobre fundo colorido (manter a cor original para + fundos, ajustar só onde teria problema de legibilidade). + +5. **Feedback de erro e loading** + - Todos os formulários desta etapa usam os componentes `FormField`, + `Button` (estado `loading`) e `Toast` já construídos na Etapa 12 — + nenhuma solução de UI paralela aqui. + +## Critérios de aceite + +- [ ] Login master e login tenant funcionam ponta a ponta com o backend + real +- [ ] Sessão expira corretamente e é renovada automaticamente via + refresh (testar deixando o access token expirar e fazendo uma + ação) +- [ ] Logout limpa a sessão e o backend revoga o refresh token +- [ ] Middleware bloqueia acesso não autenticado a `/master/*` e + `/app/*`, redirecionando para `/login` +- [ ] Usuário master não consegue acessar `/app/*` diretamente pela URL + (e vice-versa) sem ser redirecionado +- [ ] Fluxo completo de esqueci/redefinir senha funciona, incluindo + tratamento de token expirado +- [ ] Tela de login aplica a cor/logo do tenant certo quando o tenant é + identificável, e cai no tema padrão quando não é +- [ ] Cor de tenant com baixo contraste é ajustada automaticamente onde + necessário, sem quebrar a legibilidade +- [ ] Todas as telas desta etapa testadas em 375px, 768px e 1280px diff --git a/Etapas/V2/14-frontend-layout-navegacao.md b/Etapas/V2/14-frontend-layout-navegacao.md new file mode 100644 index 0000000..ca851f2 --- /dev/null +++ b/Etapas/V2/14-frontend-layout-navegacao.md @@ -0,0 +1,94 @@ +# Etapa 14 — Layout e Navegação (Master e Tenant) + +## Contexto do projeto + +Continuação do frontend do **SeConectai Vendas**. Já existem: setup, +componentes base, e autenticação/tema funcionando. Ainda não existe +nenhum layout real das áreas autenticadas — as rotas de `/master` e +`/app` só têm placeholders. + +Referência obrigatória: seção 5 (navegação por área) do +`10-frontend-schema-arquitetura.md`. + +## Objetivo desta etapa + +Construir o "casco" (shell) de navegação das duas áreas autenticadas — +sidebar, topbar, drawer mobile, bottom tab bar — totalmente responsivo, +que vai envolver todas as telas de negócio das próximas etapas. + +## Tarefas + +1. **Layout master (`app/master/layout.tsx`)** + - Desktop (`lg+`): sidebar fixa à esquerda (~240px) com: logo/marca + do SeConectai Vendas no topo (identidade fixa do master, não + whitelabel), lista de itens de menu com ícone + label, item ativo + destacado, avatar + nome do admin master + botão de logout no + rodapé. + - Itens do menu master: Dashboard, Tenants, Planos, Faturas, + Configuração Evolution API, Alertas (Templates/Logs), + Configurações. + - Topbar: breadcrumb da página atual + ícone de notificações (pode + ficar como placeholder visual nesta etapa, funcionalidade real na + Etapa 18/19) + busca rápida (input, funcionalidade real pode ficar + para depois, mas o componente deve existir). + - Mobile (abaixo de `md`): topbar fixa compacta (logo símbolo + + hambúrguer + notificações) + drawer com o menu completo + bottom + tab bar com os itens mais usados (definir 4 principais: Dashboard, + Tenants, Faturas, Alertas — o resto entra em "Mais"). + +2. **Layout tenant (`app/app/layout.tsx`)** + - Mesma estrutura de sidebar/topbar/drawer/bottom-nav do master, mas: + - Logo e cor vêm do tema do tenant autenticado (usar o que foi + resolvido na Etapa 13). + - Itens do menu: Dashboard, Clientes, Produtos, Vendas, + Recebimentos, WhatsApp, E-mail, Alertas, Equipe, Perfil. + - Bottom tab bar mobile com 4 principais: Dashboard, Clientes, + Vendas, Recebimentos — resto em "Mais". + - Itens do menu visíveis conforme o papel do usuário logado (ex: + "Equipe" só aparece para `tenant_admin`) — implementar o + controle de visibilidade por `role` aqui, para ser reaproveitado + em todas as telas. + +3. **Componentes de layout (`components/layout/`)** + - `Sidebar`: recebe a lista de itens de menu e o slot de rodapé + (usuário) como props/children — não deve ter os itens hardcoded + dentro dela, para poder ser reaproveitada entre master e tenant com + conteúdo diferente. + - `Topbar`: recebe breadcrumb e slots de ações (notificações, busca). + - `Drawer`: menu deslizante mobile, fecha ao navegar ou ao tocar fora. + - `BottomTabBar`: recebe até 4 itens fixos + item "Mais" que abre um + sheet com o restante do menu. + - `Breadcrumb`: gerado a partir da estrutura de rotas (pode ser + manual por página nesta primeira versão, com nota no código sobre + automatizar depois). + +4. **Estado de navegação** + - Sidebar colapsável (ícones apenas) no breakpoint `md`, com um botão + para expandir/recolher manualmente também no desktop, se fizer + sentido — decisão livre, mas documentar o comportamento escolhido. + - Persistir a preferência de sidebar colapsada/expandida (ex: em + cookie ou localStorage, não é dado sensível). + +5. **Empty shells das páginas de negócio** + - Cada rota de `/master/*` e `/app/*` já criada como placeholder na + Etapa 11 deve agora estar **dentro** do layout correto (sidebar + + topbar aplicados), ainda com conteúdo placeholder — só a moldura + precisa estar pronta e correta aqui. + +## Critérios de aceite + +- [ ] Layout master renderiza sidebar/topbar no desktop e + drawer+bottom-nav no mobile, com os itens de menu corretos +- [ ] Layout tenant renderiza com a cor/logo do tenant autenticado + aplicada corretamente na sidebar/topbar +- [ ] Item "Equipe" só aparece no menu do tenant para usuários + `tenant_admin` (testar com usuário de outro papel) +- [ ] Navegação entre itens do menu funciona e destaca corretamente o + item ativo, tanto no desktop quanto no drawer/bottom-nav mobile +- [ ] Drawer mobile fecha ao selecionar um item ou tocar fora dele +- [ ] Bottom tab bar mostra os 4 itens definidos + "Mais" com o restante +- [ ] Sidebar colapsável funciona no breakpoint `md` e a preferência do + usuário é lembrada entre sessões +- [ ] Todas as rotas do mapa de rotas abrem dentro do layout correto + (nenhuma "solta" sem sidebar/topbar) +- [ ] Testado em 375px, 768px e 1280px sem quebra de layout diff --git a/Etapas/V2/15-frontend-area-master.md b/Etapas/V2/15-frontend-area-master.md new file mode 100644 index 0000000..03570d6 --- /dev/null +++ b/Etapas/V2/15-frontend-area-master.md @@ -0,0 +1,99 @@ +# Etapa 15 — Área Master: Telas de Negócio + +## Contexto do projeto + +Continuação do frontend do **SeConectai Vendas**. Já existem: setup, +design system, autenticação/tema, e o layout/navegação da área master +funcionando. Esta etapa implementa as telas reais dentro desse layout, +consumindo os endpoints do painel master já prontos no backend (Etapas +3 e 4 do backend). + +## Objetivo desta etapa + +Todas as telas funcionais da área `/master`: dashboard, gestão de +tenants, planos, faturas, configuração da Evolution API e templates/logs +de alerta. + +## Tarefas + +1. **`/master/dashboard`** + - Cards de resumo (usando o componente `Card`/`StatCard`): total de + tenants por status, MRR, total recebido no mês, total em aberto. + - Gráfico simples (Recharts) de evolução de faturamento ou novos + tenants nos últimos meses, se o backend expuser dado suficiente + para isso — caso não exponha, usar só os cards de resumo e anotar a + limitação. + - Layout em grid (2–4 colunas no desktop, coluna única no mobile). + +2. **`/master/tenants`** + - Listagem usando o componente `Table` (vira cards no mobile + automaticamente), colunas: nome, status (`StatusPill`), plano, + data de criação. Busca por nome/documento, filtro por status, + paginação. + - Ação rápida por linha: ver detalhe, mudar status. + - Botão "Novo tenant" levando para `/master/tenants/novo`. + +3. **`/master/tenants/novo`** + - Formulário de criação: dados cadastrais, seleção de plano + (`Select`), dia de vencimento da mensalidade, dados do primeiro + usuário `tenant_admin` (nome, e-mail, senha provisória — mostrar a + senha gerada/informada em um `Toast`/modal de confirmação após + criar, com aviso claro para copiar/repassar ao cliente). + +4. **`/master/tenants/:id`** + - Detalhe completo com `Tabs`: Dados cadastrais (editável), Faturas + (lista de `tenant_faturas` desse tenant, com ação de marcar como + paga), Uso (quantidade de usuários/customers/vendas — dados + agregados do backend). + - Ação de mudar status (`ativo`/`suspenso`/`cancelado`/`inadimplente`) + com confirmação explícita (modal) antes de aplicar, já que afeta o + login dos usuários do tenant. + +5. **`/master/planos`** + - CRUD simples em lista + modal de criação/edição (não precisa de + página própria, dado o volume baixo de planos). + - Impedir exclusão de plano em uso (o backend já bloqueia — o + frontend deve mostrar a mensagem de erro retornada de forma clara, + não apenas um erro genérico). + +6. **`/master/faturas`** + - Listagem consolidada de todas as `tenant_faturas`, filtros por + status/tenant/período, ação de marcar como paga. + - Botão de ação para disparar a geração de faturas do mês (endpoint + manual do backend nesta fase) com confirmação antes de executar. + +7. **`/master/evolution-config`** + - Formulário de cadastro/edição da conexão (nome, URL base, API key — + campo de senha/mascarado, nunca mostrar a key completa depois de + salva). + - Botão "Testar conexão" com feedback claro de sucesso/erro (`Toast`). + +8. **`/master/alertas/templates`** e **`/master/alertas/logs`** + - Templates: lista + editor de template (tipo, canal, assunto, + corpo com suporte a inserir os placeholders disponíveis via um + menu de ajuda ao lado do editor). + - Logs: listagem com filtro por canal/status/período, mostrando o + conteúdo enviado e o erro (se houver) ao expandir uma linha. + +9. **`/master/configuracoes`** + - Dados do próprio admin master (nome, e-mail), troca de senha. + +## Critérios de aceite + +- [ ] Todas as telas listadas funcionam ponta a ponta com o backend real +- [ ] Dashboard mostra números corretos comparados com dados de teste + conhecidos +- [ ] Criar tenant exibe claramente a senha provisória gerada, com aviso + de que não será mostrada novamente +- [ ] Mudar status de tenant exige confirmação explícita antes de + aplicar +- [ ] Erros de negócio retornados pela API (ex: exclusão de plano em + uso, limite de instância) aparecem como mensagem clara para o + usuário, não como erro genérico +- [ ] API key da Evolution nunca aparece em texto completo na tela após + salva +- [ ] Editor de template mostra os placeholders disponíveis e insere + corretamente no corpo da mensagem +- [ ] Todas as listagens usam paginação, busca/filtro e estados de + loading/vazio corretamente +- [ ] Todas as telas testadas em 375px, 768px e 1280px diff --git a/Etapas/V2/16-frontend-area-tenant-cadastros.md b/Etapas/V2/16-frontend-area-tenant-cadastros.md new file mode 100644 index 0000000..6714619 --- /dev/null +++ b/Etapas/V2/16-frontend-area-tenant-cadastros.md @@ -0,0 +1,86 @@ +# Etapa 16 — Área Tenant: Dashboard, Clientes e Produtos + +## Contexto do projeto + +Continuação do frontend do **SeConectai Vendas**. Já existem: setup, +design system, autenticação/tema, layout/navegação e a área master +completa. Esta etapa inicia as telas de negócio da área `/app` (o +painel que os seus clientes usam), consumindo os endpoints das Etapas 5 +e 6 (parte de dashboard) do backend. + +## Objetivo desta etapa + +Dashboard inicial do tenant, e os cadastros de clientes (customers) e +produtos/serviços — a base de dados que toda venda vai referenciar. + +## Tarefas + +1. **`/app/dashboard`** + - Cards de resumo: total recebido no mês, total a receber + (pendente+atrasado, com o atrasado destacado visualmente, ex: cor + de alerta), quantidade de vendas no mês. + - Gráfico de resumo semanal/mensal de vendas (Recharts), consumindo + as views de dashboard do backend. + - Lista das próximas parcelas a vencer (top 5–10), com link direto + para `/app/recebimentos`. + - Grid responsivo: 2–3 colunas no desktop, coluna única no mobile, + gráfico sempre com largura total. + +2. **`/app/clientes`** + - Listagem (`Table`, vira cards no mobile) com busca por + nome/documento, filtro por ativo/inativo, paginação. + - Colunas/cards mostrando: razão social, documento, telefone/e-mail, + status. + - Botão "Novo cliente" → `/app/clientes/novo`. + +3. **`/app/clientes/novo` e `/app/clientes/:id`** + - Formulário completo: dados cadastrais (razão social, nome + fantasia, documento, e-mail, telefone/WhatsApp, endereço, cidade, + UF, observações). + - No detalhe (`:id`), usar `Tabs`: Dados cadastrais (editável), + Contatos (CRUD aninhado — lista de `customer_contacts` com + indicação clara de qual é o principal e ação de marcar + principal), Histórico de vendas (lista somente leitura das vendas + desse cliente, com link para o detalhe de cada venda — mesmo que a + tela de vendas só seja construída na próxima etapa, deixar o link + já apontando para a rota correta). + - Ação de desativar (soft delete) com confirmação, mantendo o + registro visível no histórico. + +4. **`/app/produtos`** + - Listagem com filtro por tipo (`produto`/`servico`) e por ativo, + busca por nome/SKU. + - Colunas/cards: nome, tipo (`StatusPill` ou badge simples), valor de + venda, SKU, status. + +5. **`/app/produtos/novo` e `/app/produtos/:id`** + - Formulário: tipo (produto/serviço), nome, descrição, SKU (opcional, + validar unicidade — mostrar erro claro se a API retornar conflito), + valor de venda, valor de custo (opcional), unidade, flag + "recorrente". + - Ação de desativar (soft delete) com confirmação. + +6. **Validação e limites de plano** + - Ao tentar criar cliente ou produto além do limite do plano do + tenant, a API retorna erro — o frontend deve mostrar essa mensagem + de forma clara (ex: banner ou toast explicando o limite atingido), + não travar silenciosamente o formulário. + +## Critérios de aceite + +- [ ] Dashboard do tenant mostra números batendo com dados de teste + conhecidos +- [ ] CRUD completo de clientes funciona, incluindo contatos aninhados e + regra de contato principal único +- [ ] Histórico de vendas no detalhe do cliente lista corretamente + (mesmo que a tela de destino ainda não exista até a próxima etapa) +- [ ] CRUD completo de produtos/serviços funciona, incluindo validação + de SKU duplicado +- [ ] Tentar exceder limite do plano (clientes ou produtos) mostra + mensagem clara ao usuário +- [ ] Desativar cliente/produto pede confirmação e reflete corretamente + na listagem (sai da lista de ativos, mas seria visível com filtro + "inativos") +- [ ] Todas as listagens com busca, filtro, paginação e estados de + loading/vazio +- [ ] Todas as telas testadas em 375px, 768px e 1280px diff --git a/Etapas/V2/17-frontend-area-tenant-vendas.md b/Etapas/V2/17-frontend-area-tenant-vendas.md new file mode 100644 index 0000000..29d8b39 --- /dev/null +++ b/Etapas/V2/17-frontend-area-tenant-vendas.md @@ -0,0 +1,97 @@ +# Etapa 17 — Área Tenant: Vendas e Financeiro + +## Contexto do projeto + +Continuação do frontend do **SeConectai Vendas**. Já existem: setup, +design system, autenticação/tema, layout/navegação, área master +completa, e clientes/produtos no painel do tenant. Esta é a etapa mais +crítica do painel do tenant — o fluxo de vendas e o controle financeiro +— consumindo os endpoints da Etapa 6 do backend. + +## Objetivo desta etapa + +Registro completo de vendas (com itens e parcelas), listagem e ação +sobre recebimentos, e os dashboards financeiros/semana-mês. + +## Tarefas + +1. **`/app/vendas`** + - Listagem (`Table`) com filtro por status (`orcamento`, + `confirmada`, `concluida`, `cancelada`), por cliente, por período + de `data_venda`, paginação. + - Colunas/cards: número da venda, cliente, valor total, status + (`StatusPill`), data. + - Botão "Nova venda" → `/app/vendas/nova`. + +2. **`/app/vendas/nova`** — a tela mais complexa do projeto + - Seleção de cliente (`Select` com busca, puxando de `/app/clientes`) + — se o cliente não existir ainda, oferecer atalho para cadastrar + rápido sem sair do fluxo (modal com o formulário mínimo de + cliente). + - Lista de itens da venda: adicionar produto/serviço (busca por + nome/SKU), quantidade, valor unitário (pré-preenchido do catálogo, + editável para casos de desconto pontual — deixar claro visualmente + quando o valor foi alterado do catálogo). Cálculo automático do + subtotal por item e do total geral, considerando o campo de + desconto geral da venda. + - Definição das parcelas: opção "gerar parcelas automaticamente" + (informar número de parcelas e primeira data de vencimento, o + sistema calcula o resto) ou "definir manualmente" (lista editável + de valor+vencimento por parcela). Validação em tempo real: soma das + parcelas precisa bater com o valor total da venda antes de permitir + salvar (mostrar a diferença, se houver, de forma clara). + - Layout responsivo: no desktop, itens da venda e resumo/parcelas + podem ficar lado a lado (2 colunas); no mobile, tudo empilhado com + o resumo total sempre visível (ex: barra fixa no rodapé mostrando o + total atualizado enquanto o usuário edita os itens). + - Salvar como `orcamento` ou já como `confirmada` (duas ações + distintas, não um único botão "Salvar"). + +3. **`/app/vendas/:id`** + - Detalhe com `Tabs`: Itens (somente leitura se a venda estiver + `confirmada`/`concluida`, editável se `orcamento`), Parcelas (lista + com status e ação de marcar como paga por parcela), Histórico + (mudanças de status, se o backend expuser isso via `audit_logs`). + - Ações de topo: mudar status (confirmar orçamento, cancelar venda — + com confirmação explícita explicando que isso cancela as parcelas + pendentes associadas). + +4. **`/app/recebimentos`** + - Listagem de parcelas (`sale_payments`) via a view/endpoint do + backend, com filtro por status, por cliente, por período de + vencimento. Parcelas atrasadas destacadas visualmente (cor de + alerta) e, se possível, ordenadas por padrão com as mais urgentes + no topo. + - Ação "Marcar como pago" por linha, com seleção de forma de + pagamento — se o backend suportar ação em lote, oferecer seleção + múltipla de linhas com ação "Marcar selecionadas como pagas". + +5. **Dashboards financeiros (complementando `/app/dashboard` da Etapa + 16, se ainda não coberto)** + - Gráfico de resumo semanal e mensal lado a lado ou alternável (toggle + de período), usando as views `vw_resumo_vendas_semanal` / + `vw_resumo_vendas_mensal` do backend. + - Bloco "Financeiro": total recebido, total a receber, total + atrasado, com cores semânticas consistentes com o `StatusPill`. + +## Critérios de aceite + +- [ ] Criar venda completa (cliente + itens + parcelas) funciona ponta a + ponta e reflete corretamente no backend +- [ ] Validação de soma das parcelas vs. valor total impede salvar + quando não bate, com mensagem clara da diferença +- [ ] Cadastro rápido de cliente dentro do fluxo de nova venda funciona + sem perder o que já foi preenchido na venda +- [ ] Venda confirmada não permite editar itens/valores na tela de + detalhe (reflete a regra do backend) +- [ ] Cancelar venda pede confirmação e reflete o cancelamento das + parcelas pendentes na tela de recebimentos +- [ ] Marcar parcela como paga atualiza corretamente o dashboard + financeiro sem precisar recarregar a página manualmente + (invalidação de cache do React Query) +- [ ] Parcelas atrasadas visualmente destacadas na listagem de + recebimentos +- [ ] Barra de total fixa no mobile durante a montagem de uma venda + funciona corretamente e não sobrepõe conteúdo importante +- [ ] Todas as telas testadas em 375px, 768px e 1280px, com atenção + especial à tela de nova venda (a mais densa de informação) diff --git a/Etapas/V2/18-frontend-area-tenant-integracoes-perfil.md b/Etapas/V2/18-frontend-area-tenant-integracoes-perfil.md new file mode 100644 index 0000000..2726e01 --- /dev/null +++ b/Etapas/V2/18-frontend-area-tenant-integracoes-perfil.md @@ -0,0 +1,97 @@ +# Etapa 18 — Área Tenant: Integrações, Alertas, Equipe e Perfil + +## Contexto do projeto + +Continuação do frontend do **SeConectai Vendas**. Já existem: setup, +design system, autenticação/tema, layout/navegação, área master +completa, e clientes/produtos/vendas/recebimentos no painel do tenant. +Esta etapa fecha o painel do tenant com as telas de configuração: +integrações de mensageria, templates/logs de alerta próprios do tenant, +gestão da equipe e personalização whitelabel — consumindo os endpoints +das Etapas 4, 7 e 8 do backend. + +## Objetivo desta etapa + +Telas de `/app/whatsapp`, `/app/email-config`, `/app/alertas/*`, +`/app/equipe` e `/app/perfil`. + +## Tarefas + +1. **`/app/whatsapp`** + - Lista de instâncias WhatsApp do tenant (pode ser mais de uma, + conforme limite do plano), com status (`desconectado`, + `conectando`, `conectado`, `erro`) via `StatusPill`. + - Ação "Nova instância" (respeitando limite do plano — mostrar + mensagem clara se atingido). + - Fluxo de conexão: ao criar/selecionar uma instância, mostrar o QR + code retornado pela API (imagem), com polling automático do status + a cada poucos segundos até detectar `conectado` (parar o polling + nesse momento e mostrar sucesso). + - Botão de teste de envio (número + mensagem) com feedback claro. + - Ação de desconectar/remover instância, com confirmação. + +2. **`/app/email-config`** + - Toggle "Usar configuração padrão da plataforma" vs "Usar meu + próprio SMTP" — ao desativar o padrão, exibir e exigir os campos de + SMTP próprio. + - Botão de teste de envio (e-mail de destino) com feedback claro. + - Senha do SMTP: campo tipo password, nunca pré-preenchido com o + valor real ao editar (mesma lógica de segurança da API key da + Evolution na área master). + +3. **`/app/alertas/templates`** + - Mesma lógica de editor da Etapa 15 (master), mas aqui os templates + são do tenant, com fallback visual indicando quando um tipo/canal + ainda não tem template próprio e está usando o padrão da + plataforma (ex: badge "usando padrão da plataforma" com opção de + "criar meu próprio"). + +4. **`/app/alertas/logs`** + - Listagem de `alert_logs` filtrada para os alertas enviados pelo + tenant (para os próprios `customers` dele), com os mesmos filtros + da tela master (canal, status, período) e expansão de linha para + ver conteúdo enviado/erro. + +5. **`/app/equipe`** (visível só para `tenant_admin`, conforme já + controlado no layout da Etapa 14) + - Listagem dos `tenant_users` do tenant, com papel (`role`) e status + ativo/inativo. + - Convite/criação de novo usuário: nome, e-mail, papel, senha + provisória (mesma UX de exibir e avisar sobre a senha gerada, como + na criação de tenant pelo master). + - Ação de desativar usuário e de alterar papel. + +6. **`/app/perfil`** + - Dados da empresa (nome fantasia, razão social — parte editável + dentro do que o backend permitir ao tenant alterar). + - **Upload de logo**: componente de upload com preview imediato, + validação de tipo/tamanho no client antes de enviar (espelhando as + regras do backend: png/jpg/svg, até 2MB), e ação de remover logo + (volta ao padrão da plataforma). + - **Seletor de cor primária**: color picker, com preview ao vivo de + como a cor fica aplicada nos componentes principais (ex: mostrar um + botão de exemplo com a cor escolhida antes de salvar), incluindo o + aviso de contraste automático descrito na Etapa 13. + - Ao salvar, o tema da aplicação inteira (sidebar, botões) deve + atualizar imediatamente, sem precisar recarregar a página. + +## Critérios de aceite + +- [ ] Fluxo completo de conexão de instância WhatsApp funciona (criar → + escanear QR → status muda para conectado automaticamente via + polling) +- [ ] Teste de envio de WhatsApp e de e-mail funcionam com feedback + claro de sucesso/erro +- [ ] Alternância entre SMTP próprio e padrão da plataforma funciona + corretamente, exigindo campos quando aplicável +- [ ] Templates do tenant mostram claramente quando estão usando + fallback do padrão da plataforma +- [ ] Logs de alerta do tenant mostram apenas os alertas dele + (isolamento correto) +- [ ] CRUD de equipe funciona e respeita a visibilidade só para + `tenant_admin` +- [ ] Upload de logo funciona com validação client-side e preview + imediato +- [ ] Alteração de cor primária reflete instantaneamente em toda a + aplicação após salvar, sem reload manual +- [ ] Todas as telas testadas em 375px, 768px e 1280px diff --git a/Etapas/V2/19-frontend-polimento-testes.md b/Etapas/V2/19-frontend-polimento-testes.md new file mode 100644 index 0000000..36e1570 --- /dev/null +++ b/Etapas/V2/19-frontend-polimento-testes.md @@ -0,0 +1,98 @@ +# Etapa 19 — Polimento, Acessibilidade e Testes E2E + +## Contexto do projeto + +Etapa final do frontend do **SeConectai Vendas**. Todas as telas de +negócio das áreas master e tenant já estão implementadas (Etapas 11–18). +Esta etapa é uma auditoria completa e o fechamento com testes +automatizados, antes de considerar o frontend pronto para produção. + +## Objetivo desta etapa + +Revisão sistemática de responsividade e acessibilidade em todas as +telas, ajustes de performance, e uma suíte de testes E2E cobrindo os +fluxos críticos do sistema. + +## Tarefas + +1. **Auditoria de responsividade** + - Percorrer **todas** as rotas do mapa de rotas (master e tenant) nos + três breakpoints de referência (375px, 768px, 1280px) e registrar/ + corrigir qualquer: + - overflow horizontal + - texto cortado ou sobreposto + - touch target menor que 44x44px em mobile + - tabela que não virou card corretamente + - modal que não se adaptou (tela cheia/bottom sheet) no mobile + - Prestar atenção especial às telas mais densas: `/app/vendas/nova`, + `/master/tenants/:id`, dashboards com gráficos. + +2. **Auditoria de acessibilidade** + - Rodar um checador automatizado (ex: axe-core) em todas as rotas + principais e corrigir violações críticas e sérias. + - Verificar manualmente: navegação completa por teclado (Tab/Shift+Tab) + em pelo menos um fluxo completo por área (ex: criar um cliente + inteiro só com teclado), foco visível em 100% dos elementos + interativos, contraste de texto mínimo AA em todos os textos + (incluindo sobre a cor primária dinâmica do tenant). + - Confirmar que `prefers-reduced-motion` é respeitado em todas as + transições/animações implementadas ao longo do projeto. + - Textos alternativos em imagens relevantes (logo do tenant, ícones + que carregam significado além do decorativo). + +3. **Estados de loading, erro e vazio** + - Conferir que **toda** listagem e todo formulário do sistema trata + corretamente: carregando (skeleton), erro de rede/API (mensagem + clara com ação de tentar novamente quando fizer sentido), e vazio + (EmptyState com ação sugerida). Nenhuma tela pode ficar em branco + silenciosamente em caso de erro. + +4. **Performance** + - Rodar Lighthouse (ou equivalente) nas rotas principais, mobile e + desktop, e corrigir problemas de maior impacto (ex: imagens sem + otimização, JS não utilizado sendo carregado antecipadamente, + ausência de `next/image` onde aplicável). + - Garantir `loading="lazy"` em imagens fora da dobra inicial (ex: + logos em listagens longas, se aplicável). + +5. **Testes E2E (Playwright ou Cypress — escolher um e justificar)** + - Fluxo 1: login master → criar tenant → confirmar que o + `tenant_admin` criado consegue logar. + - Fluxo 2: login tenant → cadastrar cliente → cadastrar produto → + criar venda com parcelas → marcar uma parcela como paga → conferir + que o dashboard financeiro reflete o pagamento. + - Fluxo 3: alteração de logo/cor no perfil do tenant e confirmação + visual de que o tema mudou na sidebar. + - Fluxo 4: tentativa de acesso não autenticado a rota protegida + (`/app/dashboard` sem login) redireciona corretamente para + `/login`. + - Rodar a suíte pelo menos no viewport mobile e no desktop (Playwright + permite configurar múltiplos projetos/viewports). + +6. **Revisão final de conteúdo/copy** + - Passar por todos os textos de botão, mensagem de erro, empty state + e confirmação verificando: voz ativa, consistência de nome de ação + (o botão "Salvar" deve gerar uma confirmação que também fala em + "salvo", não "criado" ou outra variação), e nenhuma mensagem de + erro genérica onde a API já fornece detalhe suficiente para ser + específica. + +## Critérios de aceite + +- [ ] Nenhuma rota do mapa de rotas tem overflow horizontal em 375px, + 768px ou 1280px +- [ ] Checador de acessibilidade automatizado sem violações críticas ou + sérias nas rotas principais +- [ ] Um fluxo completo (ex: cadastro de cliente) realizado do início ao + fim apenas com teclado, com foco sempre visível +- [ ] Contraste de texto validado sobre a cor primária dinâmica em pelo + menos 3 tenants de teste com cores diferentes (incluindo uma cor + clara, para forçar o ajuste automático de contraste) +- [ ] Todas as listagens e formulários tratam loading/erro/vazio + corretamente — testado forçando erro de rede manualmente +- [ ] Lighthouse mobile e desktop nas rotas principais sem alertas de + performance de alto impacto +- [ ] Suíte de testes E2E dos 4 fluxos descritos passa em mobile e + desktop +- [ ] Revisão de copy concluída, sem mensagens de erro genéricas onde a + API fornece detalhe específico diff --git a/Etapas/V2/20-controle-de-progresso-frontend.md b/Etapas/V2/20-controle-de-progresso-frontend.md new file mode 100644 index 0000000..6cb6cad --- /dev/null +++ b/Etapas/V2/20-controle-de-progresso-frontend.md @@ -0,0 +1,1030 @@ +# 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 ``) 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 ``). + +--- + +## 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=` 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 `
` envolve o ``, 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 `` 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 + `` 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 + ` + +
+ + + + + + {error && ( +

+ {error} +

+ )} + + + + + Voltar para o login + + + )} + + + ); +} diff --git a/frontend/app/(public)/login/login-form.tsx b/frontend/app/(public)/login/login-form.tsx new file mode 100644 index 0000000..ecb52b3 --- /dev/null +++ b/frontend/app/(public)/login/login-form.tsx @@ -0,0 +1,153 @@ +'use client'; + +import * as React from 'react'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import Image from 'next/image'; +import { useAuth } from '@/lib/auth/auth-context'; +import { useTenantTheme, TenantThemeProvider } from '@/lib/theme/theme-provider'; +import { ApiError } from '@/lib/api/client'; +import { loginSchema, type LoginFormValues } from '@/lib/validators/auth'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { FormField } from '@/components/ui/form-field'; +import Link from 'next/link'; + +type LoginArea = 'tenant' | 'master'; + +function GenericInvalidCredentialsMessage(): string { + return 'E-mail ou senha inválidos.'; +} + +function LoginFormInner() { + const router = useRouter(); + const searchParams = useSearchParams(); + const { loginMaster, loginTenant } = useAuth(); + const { theme } = useTenantTheme(); + + // Estratégia de detecção de tipo de login (Etapa 13): o backend expõe + // dois endpoints separados (`/auth/master/login` e `/auth/tenant/login`, + // sem detecção automática por e-mail — ver observações da Etapa 13 no + // controle de progresso). Escolhido um toggle explícito em vez de dois + // formulários separados por ser a opção mais simples de implementar e + // mais clara para o usuário; "tenant" é o padrão porque é o público + // majoritário (clientes), master é uso interno raro. + const [area, setArea] = React.useState('tenant'); + const [submitError, setSubmitError] = React.useState(null); + + const { + register, + handleSubmit, + formState: { errors, isSubmitting }, + } = useForm({ resolver: zodResolver(loginSchema) }); + + const onSubmit = async (values: LoginFormValues) => { + setSubmitError(null); + try { + if (area === 'master') { + await loginMaster(values.email, values.senha); + } else { + await loginTenant(values.email, values.senha); + } + + const redirectParam = searchParams.get('redirect'); + const fallback = area === 'master' ? '/master/dashboard' : '/app/dashboard'; + router.push(redirectParam && redirectParam.startsWith('/') ? redirectParam : fallback); + } catch (err) { + if (err instanceof ApiError && err.statusCode === 401) { + setSubmitError(GenericInvalidCredentialsMessage()); + } else if (err instanceof ApiError) { + setSubmitError(err.message); + } else { + setSubmitError('Não foi possível conectar. Tente novamente.'); + } + } + }; + + return ( +
+
+ {theme?.logoUrl ? ( + {theme.nome} + ) : null} +

+ {theme?.nome ?? 'SeConectai Vendas'} +

+

Entre com seu e-mail e senha

+
+ +
+ + +
+ +
+ + + + + + + + {submitError && ( +

+ {submitError} +

+ )} + + + + + Esqueci minha senha + +
+
+ ); +} + +export function LoginForm() { + const searchParams = useSearchParams(); + const tenantId = searchParams.get('tenant'); + + return ( + + + + ); +} diff --git a/frontend/app/(public)/login/page.tsx b/frontend/app/(public)/login/page.tsx new file mode 100644 index 0000000..c8a660a --- /dev/null +++ b/frontend/app/(public)/login/page.tsx @@ -0,0 +1,12 @@ +import { Suspense } from 'react'; +import { LoginForm } from './login-form'; + +export default function LoginPage() { + return ( +
+ + + +
+ ); +} diff --git a/frontend/app/(public)/redefinir-senha/[token]/page.tsx b/frontend/app/(public)/redefinir-senha/[token]/page.tsx new file mode 100644 index 0000000..a4f5d0e --- /dev/null +++ b/frontend/app/(public)/redefinir-senha/[token]/page.tsx @@ -0,0 +1,101 @@ +'use client'; + +import * as React from 'react'; +import Link from 'next/link'; +import { useParams } from 'next/navigation'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { apiClient, ApiError } from '@/lib/api/client'; +import { resetPasswordSchema, type ResetPasswordFormValues } from '@/lib/validators/auth'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { FormField } from '@/components/ui/form-field'; + +export default function RedefinirSenhaPage() { + const { token } = useParams<{ token: string }>(); + const [done, setDone] = React.useState(false); + const [tokenInvalido, setTokenInvalido] = React.useState(false); + const [error, setError] = React.useState(null); + + const { + register, + handleSubmit, + formState: { errors, isSubmitting }, + } = useForm({ resolver: zodResolver(resetPasswordSchema) }); + + const onSubmit = async (values: ResetPasswordFormValues) => { + setError(null); + setTokenInvalido(false); + try { + await apiClient.post('/auth/password/reset', { token, nova_senha: values.novaSenha }); + setDone(true); + } catch (err) { + if (err instanceof ApiError && err.statusCode === 401) { + setTokenInvalido(true); + } else if (err instanceof ApiError) { + setError(err.message); + } else { + setError('Não foi possível conectar. Tente novamente.'); + } + } + }; + + return ( +
+
+
+

Redefinir senha

+
+ + {done ? ( +
+

+ Senha redefinida com sucesso. +

+ + Ir para o login + +
+ ) : tokenInvalido ? ( +
+

+ Este link de redefinição é inválido ou já expirou. +

+ + Solicitar um novo link + +
+ ) : ( +
+ + + + + + + + {error && ( +

+ {error} +

+ )} + + +
+ )} +
+
+ ); +} diff --git a/frontend/app/app/alertas/logs/page.tsx b/frontend/app/app/alertas/logs/page.tsx new file mode 100644 index 0000000..596aae6 --- /dev/null +++ b/frontend/app/app/alertas/logs/page.tsx @@ -0,0 +1,209 @@ +'use client'; + +import * as React from 'react'; +import { useQuery } from '@tanstack/react-query'; +import type { DateRange } from 'react-day-picker'; + +import { + ALERT_LOG_STATUSES, + listTenantAlertLogs, + type AlertLog, + type AlertLogStatus, +} from '@/lib/api/tenant-alert-logs'; +import { CANAIS_TEMPLATE, CANAL_TEMPLATE_LABEL, TIPO_TEMPLATE_LABEL } from '@/lib/api/alert-templates'; +import type { CanalTemplate } from '@/lib/api/alert-templates'; +import { formatDateTime } from '@/lib/format'; + +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { DataTable, type DataTableColumn } from '@/components/ui/data-table'; +import { DateRangePicker } from '@/components/ui/date-picker'; +import { ResponsiveDialog } from '@/components/ui/responsive-dialog'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Separator } from '@/components/ui/separator'; + +const STATUS_LABEL: Record = { + enviado: 'Enviado', + entregue: 'Entregue', + lido: 'Lido', + falhou: 'Falhou', +}; + +const STATUS_TONE: Record = { + enviado: 'outline', + entregue: 'default', + lido: 'default', + falhou: 'destructive', +}; + +const STATUS_OPTIONS: { value: AlertLogStatus | 'todos'; label: string }[] = [ + { value: 'todos', label: 'Todos os status' }, + ...ALERT_LOG_STATUSES.map((s) => ({ value: s, label: STATUS_LABEL[s] })), +]; + +const CANAL_OPTIONS: { value: CanalTemplate | 'todos'; label: string }[] = [ + { value: 'todos', label: 'Todos os canais' }, + ...CANAIS_TEMPLATE.map((c) => ({ value: c, label: CANAL_TEMPLATE_LABEL[c] })), +]; + +const LIMIT = 20; + +export default function TenantAlertLogsPage() { + const [canal, setCanal] = React.useState('todos'); + const [status, setStatus] = React.useState('todos'); + const [periodo, setPeriodo] = React.useState(); + const [page, setPage] = React.useState(1); + const [detail, setDetail] = React.useState(null); + + const { data, isLoading } = useQuery({ + queryKey: [ + 'tenant', + 'alert-logs', + { canal, status, de: periodo?.from?.toISOString(), ate: periodo?.to?.toISOString(), page }, + ], + queryFn: () => + listTenantAlertLogs({ + canal: canal === 'todos' ? undefined : canal, + status: status === 'todos' ? undefined : status, + data_inicio: periodo?.from?.toISOString(), + data_fim: periodo?.to?.toISOString(), + page, + limit: LIMIT, + }), + placeholderData: (prev) => prev, + }); + + const columns: DataTableColumn[] = [ + { + id: 'tipo', + header: 'Tipo', + primary: true, + cell: (row) => (row.template ? TIPO_TEMPLATE_LABEL[row.template.tipo] : '—'), + }, + { id: 'canal', header: 'Canal', primary: true, cell: (row) => CANAL_TEMPLATE_LABEL[row.canal] }, + { + id: 'status', + header: 'Status', + cell: (row) => {STATUS_LABEL[row.status]}, + }, + { id: 'enviado_em', header: 'Enviado em', cell: (row) => formatDateTime(row.enviado_em) }, + ]; + + return ( +
+
+

Logs de alerta

+

Histórico de envios de WhatsApp/e-mail aos seus clientes

+
+ +
+ + + { + setPeriodo(range); + setPage(1); + }} + placeholder="Período" + className="w-full sm:w-64" + /> +
+ + row.id} + loading={isLoading} + emptyTitle="Nenhum log encontrado" + emptyDescription="Ajuste os filtros de período/canal/status." + onRowClick={(row) => setDetail(row)} + rowActions={() => ( + + )} + pagination={ + data ? { page: data.meta.page, limit: data.meta.limit, total: data.meta.total, onPageChange: setPage } : undefined + } + /> + + !open && setDetail(null)} + title="Detalhe do envio" + description={detail ? formatDateTime(detail.enviado_em) : undefined} + > + {detail && ( +
+
+
+

Tipo

+

+ {detail.template ? TIPO_TEMPLATE_LABEL[detail.template.tipo] : '—'} +

+
+
+

Canal

+

{CANAL_TEMPLATE_LABEL[detail.canal]}

+
+
+

Status

+ {STATUS_LABEL[detail.status]} +
+
+ +
+

Conteúdo enviado

+

+ {detail.conteudo_enviado || '—'} +

+
+ {detail.erro && ( +
+

Erro

+

+ {detail.erro} +

+
+ )} +
+ )} +
+
+ ); +} diff --git a/frontend/app/app/alertas/templates/page.tsx b/frontend/app/app/alertas/templates/page.tsx new file mode 100644 index 0000000..7c27192 --- /dev/null +++ b/frontend/app/app/alertas/templates/page.tsx @@ -0,0 +1,412 @@ +'use client'; + +import * as React from 'react'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { Plus } from 'lucide-react'; + +import { + CANAIS_TEMPLATE, + CANAL_TEMPLATE_LABEL, + PLACEHOLDERS_POR_TIPO, + TIPOS_TEMPLATE_TENANT, + TIPO_TEMPLATE_TENANT_LABEL, + createTenantAlertTemplate, + deleteTenantAlertTemplate, + listTenantAlertTemplates, + updateTenantAlertTemplate, + type AlertTemplate, + type CanalTemplate, + type TipoTemplateTenant, +} from '@/lib/api/tenant-alert-templates'; +import { + tenantAlertTemplateSchema, + type TenantAlertTemplateFormValues, +} from '@/lib/validators/tenant-alert-templates'; +import { ApiError } from '@/lib/api/client'; +import { toast } from '@/lib/toast'; + +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Card } from '@/components/ui/card'; +import { DataTable, type DataTableColumn } from '@/components/ui/data-table'; +import { FormField } from '@/components/ui/form-field'; +import { Input } from '@/components/ui/input'; +import { ResponsiveDialog } from '@/components/ui/responsive-dialog'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Textarea } from '@/components/ui/textarea'; +import { ConfirmDialog } from '@/components/domain/confirm-dialog'; + +const TIPO_OPTIONS: { value: TipoTemplateTenant | 'todos'; label: string }[] = [ + { value: 'todos', label: 'Todos os tipos' }, + ...TIPOS_TEMPLATE_TENANT.map((tipo) => ({ value: tipo, label: TIPO_TEMPLATE_TENANT_LABEL[tipo] })), +]; + +const CANAL_OPTIONS: { value: CanalTemplate | 'todos'; label: string }[] = [ + { value: 'todos', label: 'Todos os canais' }, + ...CANAIS_TEMPLATE.map((canal) => ({ value: canal, label: CANAL_TEMPLATE_LABEL[canal] })), +]; + +const LIMIT = 20; +// Limite alto o bastante para trazer todas as combinações tipo×canal numa +// única chamada — usado só para calcular a matriz de fallback abaixo, não +// para a listagem paginada principal. +const MATRIX_LIMIT = 100; + +export default function TenantAlertTemplatesPage() { + const queryClient = useQueryClient(); + const [tipo, setTipo] = React.useState('todos'); + const [canal, setCanal] = React.useState('todos'); + const [page, setPage] = React.useState(1); + + const [editing, setEditing] = React.useState(null); + const [formOpen, setFormOpen] = React.useState(false); + const [deleting, setDeleting] = React.useState(null); + + const { data, isLoading } = useQuery({ + queryKey: ['tenant', 'alert-templates', { tipo, canal, page }], + queryFn: () => + listTenantAlertTemplates({ + tipo: tipo === 'todos' ? undefined : tipo, + canal: canal === 'todos' ? undefined : canal, + page, + limit: LIMIT, + }), + placeholderData: (prev) => prev, + }); + + // Matriz de fallback: não existe endpoint que informe se um tipo/canal + // está usando o padrão da plataforma — inferido client-side comparando + // contra a lista completa de templates ativos do tenant. + const { data: allTemplates } = useQuery({ + queryKey: ['tenant', 'alert-templates', 'matrix'], + queryFn: () => listTenantAlertTemplates({ ativo: true, limit: MATRIX_LIMIT }), + }); + + const form = useForm({ resolver: zodResolver(tenantAlertTemplateSchema) }); + const corpoFieldRef = React.useRef(null); + const { ref: corpoRegisterRef, ...corpoRegister } = form.register('corpo'); + const watchedTipo = form.watch('tipo'); + const watchedCanal = form.watch('canal'); + + function openCreate(preset?: { tipo: TipoTemplateTenant; canal: CanalTemplate }) { + setEditing(null); + form.reset({ + tipo: preset?.tipo ?? 'cobranca_cliente', + canal: preset?.canal ?? 'whatsapp', + assunto: '', + corpo: '', + ativo: true, + }); + setFormOpen(true); + } + + function openEdit(template: AlertTemplate) { + setEditing(template); + form.reset({ + tipo: template.tipo as TipoTemplateTenant, + canal: template.canal, + assunto: template.assunto ?? '', + corpo: template.corpo, + ativo: template.ativo, + }); + setFormOpen(true); + } + + function insertPlaceholder(chave: string) { + const token = `{{${chave}}}`; + const el = corpoFieldRef.current; + const current = form.getValues('corpo') ?? ''; + + if (!el) { + form.setValue('corpo', current + token, { shouldValidate: true }); + return; + } + + const start = el.selectionStart ?? current.length; + const end = el.selectionEnd ?? current.length; + const next = current.slice(0, start) + token + current.slice(end); + form.setValue('corpo', next, { shouldValidate: true }); + + requestAnimationFrame(() => { + el.focus(); + const pos = start + token.length; + el.setSelectionRange(pos, pos); + }); + } + + function invalidateAll() { + queryClient.invalidateQueries({ queryKey: ['tenant', 'alert-templates'] }); + } + + const saveMutation = useMutation({ + mutationFn: (values: TenantAlertTemplateFormValues) => { + const payload = { ...values, assunto: values.canal === 'email' ? values.assunto : undefined }; + return editing ? updateTenantAlertTemplate(editing.id, payload) : createTenantAlertTemplate(payload); + }, + onSuccess: () => { + invalidateAll(); + toast.success(editing ? 'Template atualizado.' : 'Template criado.'); + setFormOpen(false); + }, + onError: (err) => { + toast.error(err instanceof ApiError ? err.message : 'Não foi possível salvar o template.'); + }, + }); + + const deleteMutation = useMutation({ + mutationFn: (id: string) => deleteTenantAlertTemplate(id), + onSuccess: () => { + invalidateAll(); + toast.success('Template desativado.'); + setDeleting(null); + }, + onError: (err) => { + toast.error(err instanceof ApiError ? err.message : 'Não foi possível desativar o template.'); + setDeleting(null); + }, + }); + + const columns: DataTableColumn[] = [ + { id: 'tipo', header: 'Tipo', primary: true, cell: (row) => TIPO_TEMPLATE_TENANT_LABEL[row.tipo as TipoTemplateTenant] }, + { id: 'canal', header: 'Canal', primary: true, cell: (row) => CANAL_TEMPLATE_LABEL[row.canal] }, + { + id: 'corpo', + header: 'Prévia', + cell: (row) => {row.corpo}, + }, + { + id: 'status', + header: 'Status', + cell: (row) => {row.ativo ? 'Ativo' : 'Inativo'}, + }, + ]; + + const placeholders = watchedTipo ? PLACEHOLDERS_POR_TIPO[watchedTipo] : []; + + return ( +
+
+
+

Templates de alerta

+

Mensagens enviadas aos seus clientes

+
+ +
+ +
+ {TIPOS_TEMPLATE_TENANT.map((t) => + CANAIS_TEMPLATE.map((c) => { + const usaProprio = allTemplates?.data.some((tpl) => tpl.tipo === t && tpl.canal === c) ?? false; + return ( + +

+ {TIPO_TEMPLATE_TENANT_LABEL[t]} · {CANAL_TEMPLATE_LABEL[c]} +

+ {usaProprio ? ( + + Template próprio configurado + + ) : ( + <> + + Usando padrão da plataforma + + + + )} +
+ ); + }), + )} +
+ +
+ + +
+ + row.id} + loading={isLoading} + emptyTitle="Nenhum template próprio cadastrado" + emptyDescription="Sem um template próprio, o padrão da plataforma é usado automaticamente." + emptyAction={ + + } + rowActions={(row) => ( + <> + + + + )} + pagination={ + data ? { page: data.meta.page, limit: data.meta.limit, total: data.meta.total, onPageChange: setPage } : undefined + } + /> + + + + + + } + > +
saveMutation.mutate(values))} + > +
+ + + + + + +
+ + {watchedCanal === 'email' && ( + + + + )} + +
+ +