Merge pull request #21 from esdrasrenan/feat/turbopack-category-save

feat: harden ticket creation ux and seeding
This commit is contained in:
esdrasrenan 2025-10-06 14:45:02 -03:00 committed by GitHub
commit 14b25e2cb3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 338 additions and 537 deletions

505
agents.md
View file

@ -3,461 +3,76 @@
## Contato principal
- **Esdras Renan** — monkeyesdras@gmail.com
## Ambiente local
- Admin: `admin@sistema.dev` / `admin123`
- Agentes seed (senha inicial `agent123` — alterar no primeiro acesso):
- Gabriel Oliveira · george.araujo@rever.com.br
- George Araujo · george.araujo@rever.com.br
- Hugo Soares · hugo.soares@rever.com.br
- Julio Cesar · julio@rever.com.br
- Lorena Magalhães · lorena@rever.com.br
- Rever · renan.pac@paulicon.com.br
- Telão · suporte@rever.com.br
- Thiago Medeiros · thiago.medeiros@rever.com.br
- Weslei Magalhães · weslei@rever.com.br
## Credenciais padrão (Better Auth)
- Administrador: `admin@sistema.dev` / `admin123`
- Agente Demo: `agente.demo@sistema.dev` / `agent123`
- Cliente Demo: `cliente.demo@sistema.dev` / `cliente123`
> Execute `pnpm --dir web auth:seed` após configurar `.env.local`. O script atualiza as contas acima ou cria novas conforme variáveis `SEED_USER_*`.
> Todos os usuários estão sincronizados com o Convex via `scripts/seed-agents.mjs`.
## Sincronização com Convex
- Usuários e tickets demo são garantidos via `web/convex/seed.ts`.
- Com `pnpm convex:dev` rodando, acesse `/dev/seed` uma vez para popular dados quando necessário.
## Visão geral atual
- **Meta imediata:** consolidar o núcleo de tickets web/desktop com canais, SLAs e automações futuras.
- **Stack:** Next.js (App Router) + Convex + Better Auth + Prisma (referência de domínio).
- **Estado:** núcleo web funcional (tickets, play mode, painéis administrativos, portal do cliente) com Turbopack habilitado no `pnpm dev`.
## Setup local rápido
1. `cd web && pnpm install`
2. `cp .env.example .env.local` e ajuste `NEXT_PUBLIC_CONVEX_URL` apontando para o servidor Convex local.
3. `pnpm --dir web auth:seed`
4. `pnpm --dir web convex:dev`
5. Em outro terminal: `pnpm --dir web dev`
## Entregas concluídas
- Scaffold Next.js + Tailwind + shadcn/ui, shell com sidebar/header, login real com Better Auth.
- Integração Convex completa: listas/detalhe de tickets, mutations (status, categorias, filas, comentários, play next).
- Painel administrativo: gestão de filas, times, campos personalizados e convites Better Auth.
- Portal do cliente isolado por `viewerId`; dashboard principal consumindo métricas reais do Convex.
- Fluxo de convites Better Auth ponta a ponta + seed automatizado de agentes/admin.
## Estado atual
- Autenticação Better Auth com guardas client-side (`AuthGuard`) bloqueando rotas protegidas.
- Menu de usuário no rodapé da sidebar com link para `/settings` e logout confiável.
- Formulários de novo ticket (dialog, página e portal) com seleção de responsável, placeholders claros e validação obrigatória de assunto/descrição/categorias.
- Portal do cliente restringe visualização e criação ao próprio requester; clientes não atribuem responsáveis.
- Relatórios e dashboards utilizam `AppShell`, garantindo header/sidebar consistentes.
## Desenvolvimento em curso
- Refinar sincronização Better Auth ↔ Convex (resets de senha, revogação automática de convites).
- Melhorar UX do ticket header (categorias, status, prioridades) e comandos rápidos na listagem.
- Manter hidratação consistente na sidebar e componentes Radix após migração para React 19.
## Entregas recentes relevantes
- Correção do redirecionamento após logout evitando retorno imediato ao dashboard.
- Validações manuais dos formulários de rich text para eliminar `ZodError` durante edição.
- Dropdown de responsáveis na criação de tickets com preenchimento automático pelo autor e evento inicial de comentário.
- Indicadores visuais de campos obrigatórios e botão "Novo ticket" funcional no cabeçalho do detalhe.
- Seeds (Better Auth e Convex) ampliados para incluir agente e cliente de teste.
## Próximas prioridades
1. Expandir suíte de testes (UI + Convex) e habilitar pipeline CI obrigatória (lint + vitest).
2. Implementar resets de senha automatizados e auditoria de convites para onboarding/offboarding.
3. Expor categorias/subcategorias dinâmicas na criação/edição de tickets (web e desktop).
4. Adicionar ações avançadas para agentes (edição de categorias, reassignment rápido) sob RBAC.
## Fluxos suportados
## Boas práticas e rotinas
- **Seeds:** `node --env-file=.env.local scripts/seed-agents.mjs` (mantém admin e agentes) + `/dev/seed` para dados demo.
- **Serviços locais:** `pnpm convex:dev` (gera tipos e roda backend) e `pnpm dev` (Next.js com Turbopack).
- **Testes e lint:** execute `pnpm lint` e `pnpm vitest run` antes de cada PR.
- **Convex:** retorne apenas tipos suportados (`number` para datas) e valide no front via mappers Zod.
- **UI:** textos PTBR, toasts com feedback, atualizações otimistas com rollback em caso de erro.
- **Git/PR:** branches descritivas, checklist padrão (tipos Convex, labels PTBR, loaders, mappers atualizados) e coautor `factory-droid[bot]` quando aplicável.
### Equipe interna (admin/agent/collaborator)
- Criar tickets com categorias, responsável inicial e anexos.
- Abrir novos tickets diretamente a partir do detalhe via dialog reutilizável.
- Acessar `/settings` para ajustes pessoais e efetuar logout pelo menu.
## Histórico de marcos
- Fase A (scaffold/UX base) e Fase B (núcleo de tickets) concluídas.
- Iniciativa “Autenticação real e personas” entregue com RBAC completo e portal do cliente.
- Roadmap imediato focado em credenciais unificadas, automações de convites e cobertura de testes.
### Clientes
- Autenticam com `cliente.demo@sistema.dev`.
- Abrem tickets para si mesmos a partir do portal com assunto/descrição obrigatórios.
- Não visualizam campo de responsável nem tickets de outros usuários.
# Plano de Desenvolvimento - Sistema de Chamados
## Próximos passos sugeridos
1. Finalizar redefinição de senha/auditoria de convites Better Auth.
2. Expandir cobertura de testes (`vitest`) para guardas de autenticação e criação de tickets.
3. Implementar ações rápidas (status/fila) diretamente na listagem de tickets.
4. Definir limites e monitoramento para anexos por tenant.
## Meta imediata
Construir o nucleo de tickets compartilhado entre web e desktop (Tauri), garantindo base solida para canais, SLAs e automacoes futuras.
## Rotina antes de abrir PR
- `pnpm --dir "web" lint`
- `pnpm --dir "web" exec vitest run`
- Revisar toasts/labels em PT-BR e ausência de segredos no diff.
- Adicionar coautor `factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>` quando aplicável.
### Contato principal
- **Esdras Renan** — monkeyesdras@gmail.com
### Credenciais seed (ambiente local)
- Administrador padrão: `admin@sistema.dev` / `admin123`
- Agentes carregados via seed (senha inicial `agent123`, altere após o primeiro acesso):
- Gabriel Oliveira — gabriel.oliveira@rever.com.br
- George Araujo — george.araujo@rever.com.br
- Hugo Soares — hugo.soares@rever.com.br
- Julio Cesar — julio@rever.com.br
- Lorena Magalhães — lorena@rever.com.br
- Rever — renan.pac@paulicon.com.br
- Telão — suporte@rever.com.br
- Thiago Medeiros — thiago.medeiros@rever.com.br
- Weslei Magalhães — weslei@rever.com.br
> Observação: todos os usuários acima foram sincronizados com o Convex. Atualize as senhas imediatamente após o primeiro login.
## Fase A - Fundamentos da plataforma
1. **Scaffold e DX**
- Criar projeto Next.js (App Router) com Typescript, ESLint, Tailwind, shadcn/ui.
- Configurar alias de paths, lint/prettier opinativo.
- Ajustar `globals.css` para tokens de cor/tipografia conforme layout base.
2. **Design system inicial**
- Importar componentes `dashboard-01` e `sidebar-01` via shadcn.
- Ajustar paleta (tons de cinza + destaque primario) e tipografia (Inter/Manrope).
- Implementar layout shell (sidebar + header) reutilizavel.
3. **Autenticacao placeholder**
- Configurar stub de sessao (cookie + middleware) para navegacao protegida.
### Status da fase
- OK Scaffold Next.js + Tailwind + shadcn/ui criado em `web/`.
- OK Layout base atualizado (sidebar, header, cards, grafico) com identidade da aplicacao.
- OK Auth placeholder via cookie + middleware e bootstrap de usuario no Convex.
## Fase B - Nucleo de tickets
1. **Modelagem compartilhada**
- Definir esquema Prisma para Ticket, TicketEvent, User (minimo), Queue/View.
- Publicar Zod schemas/Types para uso no frontend.
2. **Fluxo principal**
- Pagina `tickets` com tabela (TanStack) suportando filtros basicos.
- Pagina de ticket com timeline de eventos/comentarios (dados mockados).
- Implementar modo play preliminar (simula proxima tarefa da fila).
3. **Mutations**
- Formulario de criacao/edicao com validacao.
- Comentarios publico/privado (UX + componentes).
### Status parcial
- OK `prisma/schema.prisma` criado com entidades centrais (User, Team, Ticket, Comment, Event, SLA).
- OK Schemas Zod e mocks compartilhados em `src/lib/schemas` e `src/lib/mocks`.
- OK Paginas `/tickets`, `/tickets/[id]` e `/play` prontas com componentes dedicados (filtros, tabela, timeline, modo play).
- OK Integração com backend Convex (consultas/mutações + file storage). Prisma mantido apenas como referência.
## Fase C - Servicos complementares (posterior)
- SLAs (BullMQ + Redis), notificacoes, ingest de e-mail, portal cliente, etc.
## Backlog imediato
- [x] Expor portal do cliente com listagem de tickets filtrada por `viewerId` (Convex + UI)
- [x] Completar painel administrativo (times, filas, campos e SLAs) com RBAC server/client
- [ ] Finalizar sincronização Better Auth ↔ Convex para resets de senha e revogações automáticas de convites
- [ ] Expandir suite de testes (UI + Convex) cobrindo guardas, relatórios e mapeadores críticos
- [x] Implementar fluxo completo de convites (criação, envio, revogação e aceite) para administradores
- [ ] Habilitar ações avançadas para agentes (edição de categorias, reassigação rápida) com as devidas permissões
- [ ] Integrar campos personalizados e categorias dinâmicas nos formulários de criação/edição de tickets
### Iniciativa atual — Autenticação real e personas
- [x] Migrar placeholder para Better Auth + Prisma (handlers Next, cliente React e sync Convex).
- [x] Expor roles (`admin`, `agent`, `customer`) e aplicar guardas (`requireUser/Staff/Admin/Customer`) no Convex.
- [x] Ajustar middleware e componentes para usar `viewerId`/`actorId`, evitando vazamento de dados entre tenants.
- [x] Criar portal do cliente para abertura/consulta de chamados e comentários públicos.
- [x] Consolidar painel administrativo (times, filas, campos e SLAs) com UI protegida por RBAC completo.
- [x] Entregar fluxo de convites Better Auth (criação, envio, revogação) e gerenciamento de agentes.
- [ ] Unificar ciclo de vida de credenciais (reset de senha, expiração automática e reenvio de convites).
## Proximas entregas sugeridas
1. Consolidar onboarding/offboarding de agentes com resets de senha, reenvio automático e auditoria de convites Better Auth.
2. Expor categorias, subcategorias e campos personalizados dinamicamente nas telas de criação/edição de tickets (web e desktop).
3. Definir permissões intermediárias para agentes (edição limitada de categorias/campos) e refletir no Convex.
4. Expandir relatórios operacionais (workSummary, métricas por canal/categoria) usando os novos campos personalizados.
5. Automatizar pipeline CI (lint + vitest) integrando checagens obrigatórias antes de merge.
## Acompanhamento
Atualizar este arquivo a cada marco relevante (setup concluido, nucleo funcional, etc.).
---
# Guia do Projeto (para agentes e contribuidores)
Este repositório foi atualizado para usar Convex como backend em tempo real para o núcleo de tickets. Abaixo, um guia prático conforme o padrão de AGENTS.md para orientar contribuições futuras.
## Decisões técnicas atuais
- Backend: Convex (funções + banco + storage) em `web/convex/`.
- Esquema: `web/convex/schema.ts`.
- Tickets API: `web/convex/tickets.ts` (list/getById/create/addComment/updateStatus/playNext).
- Upload de arquivos: `web/convex/files.ts` (Convex Storage).
- Filas: `web/convex/queues.ts` (resumo por fila).
- Seed/bootstrap: `web/convex/seed.ts`, `web/convex/bootstrap.ts`.
- Autenticação: Better Auth + Prisma (SQLite) com roles (`admin`, `agent`, `customer`) sincronizadas com Convex
- Login: `web/src/app/login/page.tsx` + `web/src/components/login/login-form.tsx`
- Middleware e guards: `web/middleware.ts`, helpers em `web/src/lib/auth{,z, -server}.ts`
- Cliente React: `web/src/lib/auth-client.tsx` (sincroniza sessão Better Auth ↔ Convex, expõe helpers de role)
- Frontend (Next.js + shadcn/ui)
- Páginas principais: `/tickets`, `/tickets/[id]`, `/tickets/new`, `/play`.
- UI ligada ao Convex com `convex/react`.
- Toasts: `sonner` via `Toaster` em `web/src/app/layout.tsx`.
- Mapeamento/validação de dados
- Convex retorna datas como `number` (epoch). A UI usa `Date`.
- Sempre converter/validar via Zod em `web/src/lib/mappers/ticket.ts`.
- Não retornar `Date` a partir de funções do Convex.
- Prisma: mantido apenas como referência de domínio (não é fonte de dados ativa).
## Como rodar
- Prérequisitos: Node LTS + pnpm.
- Passos:
- `cd web && pnpm i`
- `pnpm convex:dev` (mantém gerando tipos e rodando backend dev)
- Criar `.env.local` com `NEXT_PUBLIC_CONVEX_URL=<url exibida pelo convex dev>`
- Em outro terminal: `pnpm dev`
- Login em `/login`; seed opcional em `/dev/seed`.
## Convenções de código
- Não use `Date` em payloads do Convex; use `number` (epoch ms).
- Normalize dados no front via mappers Zod antes de renderizar.
- UI com shadcn/ui; priorize componentes existentes e consistência visual.
- Labels e mensagens em PTBR (status, timeline, toasts, etc.).
- Atualizações otimistas com rollback em erro + toasts de feedback.
- Comentários de supressão: prefira `@ts-expect-error` com justificativa curta para módulos gerados do Convex; evite `@ts-ignore`.
## Convenções
- Convex deve retornar apenas tipos primitivos; converta datas via mappers em `src/lib/mappers`.
- Manter textos em PT-BR e evitar comentários supérfluos no código.
- Reutilizar componentes shadcn existentes e seguir o estilo do arquivo editado.
- Validações client-side críticas devem sinalizar erros inline e exibir toast.
## Estrutura útil
- `web/convex/*` — API backend Convex.
- `web/src/lib/mappers/*` — Conversores server→UI com Zod.
- `web/src/components/tickets/*` — Tabela, filtros, detalhe, timeline, comentários, play.
- `web/convex/` — queries e mutations (ex.: `tickets.ts`, `users.ts`).
- `web/src/components/tickets/` — UI interna (dialog, listas, header, timeline).
- `web/src/components/portal/` — formulários e fluxos do portal do cliente.
- `web/scripts/` — seeds Better Auth e utilidades.
- `web/src/components/auth/auth-guard.tsx` — proteção de rotas client-side.
## Scripts (pnpm)
- `pnpm convex:dev` — Convex (dev + geração de tipos)
- `pnpm dev` — Next.js (App Router)
- `pnpm build` / `pnpm start` — build/produção
## Backlog imediato (próximos passos)
- Form “Novo ticket” em Dialog shadcn + React Hook Form + Zod + toasts.
- Atribuição/transferência de fila no detalhe (selects com update otimista).
- Melhorias de layout adicionais no painel “Detalhes” (quebras, largura responsiva) e unificação de textos PTBR.
- Testes unitários dos mapeadores com Vitest.
## Checklist de PRs
- [ ] Funções Convex retornam apenas tipos suportados (sem `Date`).
- [ ] Dados validados/convertidos via Zod mappers antes da UI.
- [ ] Textos/labels em PTBR.
- [ ] Eventos de UI com feedback (toast) e rollback em erro.
- [ ] Documentação atualizada se houver mudanças em fluxo/env.
---
## Próximas Entregas (Roadmap detalhado)
1) UX/Visual (shadcn/ui)
- Padronizar cartões em todas as telas (Play, Visualizações) com o mesmo padrão aplicado em Conversa/Detalhes/Timeline (bordas, sombra, paddings).
- Aplicar microtipografia consistente: headings H1/H2, tracking, tamanhos, cores em PTBR.
- Skeletons de carregamento nos principais painéis (lista de tickets, recentes, play next).
- Melhorar tabela: estados hover/focus, ícones de canal, largura de colunas previsível e truncamento.
2) Comentários e anexos
- Dropzone também no “Novo ticket” (já implementado) com registro de comentário inicial e anexos.
- Grid de anexos com miniaturas e legenda; manter atributo `download` com o nome original.
- Preview em modal para imagens (feito) e suporte a múltiplas linhas no grid.
- Botão para copiar link de arquivo (futuro, usar URL do storage).
3) Timeline e eventos
- Mensagens amigáveis em PTBR (feito para CREATED/STATUS/ASSIGNEE/QUEUE).
- Incluir sempre `actorName`/`actorAvatar` no payload; evitar JSON cru na UI.
- Exibir avatar e nome do ator nas entradas (parcialmente feito).
4) Dados e camada Convex
- Sempre retornar datas como `number` (epoch) e converter no front via mappers Zod.
- Padronizar import do Convex com `@/convex/_generated/api` (alias criado).
- Evitar `useQuery` com args vazios — proteger chamadas (gates) e, quando necessário, fallback de mock para IDs `ticket-*`.
5) Autenticação / Sessão (placeholder)
- Cookie `demoUser` e bootstrap de usuário no Convex (feito). Trocar por Auth.js/Clerk quando for o momento.
6) Testes
- Vitest configurado; adicionar casos para mapeadores (já iniciado) e smoke tests básicos de páginas.
- Não usar Date em assertions de payload — sempre comparar epoch ou `instanceof Date` após mapeamento.
7) Acessibilidade e internacionalização
- Labels e mensagens 100% em PTBR; evitar termos como `QUEUE_CHANGED` na UI.
- Navegação por teclado em Dialogs/Selects; aria-labels em botões de ação.
8) Observabilidade (posterior)
- Logs de evento estruturados no Convex; traces simples no client para ações críticas.
---
## Endpoints Convex (resumo)
- `tickets.list({ tenantId, status?, priority?, channel?, queueId?, search?, limit? })`
- `tickets.getById({ tenantId, id })`
- `tickets.create({ tenantId, subject, summary?, priority, channel, queueId?, requesterId })`
- `tickets.addComment({ ticketId, authorId, visibility, body, attachments?[] })`
- `tickets.updateStatus({ ticketId, status, actorId })` — gera evento com `toLabel` e `actorName`.
- `tickets.changeAssignee({ ticketId, assigneeId, actorId })` — gera evento com `assigneeName`.
- `tickets.changeQueue({ ticketId, queueId, actorId })` — gera evento com `queueName`.
- `tickets.playNext({ tenantId, queueId?, agentId })` — atribui ticket e registra evento.
- `tickets.updatePriority({ ticketId, priority, actorId })` — altera prioridade e registra `PRIORITY_CHANGED`.
- `tickets.remove({ ticketId, actorId })` — remove ticket, eventos e comentários (tenta excluir anexos do storage).
- `queues.summary({ tenantId })`
- `files.generateUploadUrl()` — usar via `useAction`.
- `users.ensureUser({ tenantId, email, name, avatarUrl?, role?, teams? })`
Observações:
- Não retornar `Date` nas funções Convex; usar `number` e converter na UI com os mappers em `src/lib/mappers`.
- Evitar passar `{}` para `useQuery` — args devem estar definidos ou a query não deve ser invocada.
---
## Padrões de Código
- UI: shadcn/ui (Field, Dialog, Select, Badge, Table, Spinner) + Tailwind.
- Dados: Zod para validação; mappers para converter server→UI (epoch→Date, null→undefined).
- Texto: PTBR em labels, toasts e timeline.
- UX: updates otimistas + toasts (status, assignee, fila, comentários).
- Imports do Convex: sempre `@/convex/_generated/api`.
---
## Como abrir PR
- Crie uma branch descritiva (ex.: `feat/tickets-attachments-grid`).
- Preencha a descrição com: contexto, mudanças, como testar (pnpm scripts), screenshots quando útil.
- Checklist:
- [ ] Sem `Date` no retorno Convex.
- [ ] Labels PTBR.
- [ ] Skeleton/Loading onde couber.
- [ ] Mappers atualizados se tocar em payloads.
- [ ] AGENTS.md atualizado se houver mudança de padrões.
---
## Atualizações recentes (dez/2025)
- RBAC do Convex reforçado: `tickets.list`, `tickets.getById`, `workSummary` e mutações sensíveis (`changeQueue`, `updateCategories`, `startWork/pauseWork`, `updatePriority`) agora exigem `viewerId/actorId` e validam `requireStaff` com `tenantId`.
- Componentes de tickets (tabela, painel de recentes, play next, cabeçalho/detalhe) passam a usar o contexto Better Auth para prover `viewerId`, com `useQuery` protegido por `"skip"` enquanto não há sessão.
- Testes (`pnpm vitest run`) executados após as alterações para garantir regressão zero.
## Progresso recente (mar/2025)
Resumo do que foi implementado desde o último marco:
- Rich text (Tiptap) com SSR seguro para comentários e descrição inicial do ticket
- Componente: `web/src/components/ui/rich-text-editor.tsx`
- Comentários: `web/src/components/tickets/ticket-comments.rich.tsx` (visibilidade Público/Interno, anexos tipados)
- Novo ticket (Dialog + Página): campos de descrição usam rich text; primeiro comentário é registrado quando houver conteúdo.
- Tipagem estrita (remoção de `any`) no front e no Convex
- Uso consistente de `Id<>` e `Doc<>` (Convex) e schemas Zod (record tipado em v4).
- Queries `useQuery` com "skip" quando necessário; mapeadores atualizados.
- Filtros server-side
- `tickets.list` agora escolhe o melhor índice (por `status`, `queueId` ou `tenant`) e só então aplica filtros complementares.
- UI do detalhe do ticket (Header)
- Prioridade como dropdown-badge translúcida: `web/src/components/tickets/priority-select.tsx` (nova Convex `tickets.updatePriority`).
- Seleção de responsável com avatar no menu.
- Ação de exclusão com modal (ícones, confirmação): `web/src/components/tickets/delete-ticket-dialog.tsx` (Convex `tickets.remove`).
- Correções e DX
- Tiptap: `immediatelyRender: false` + `setContent({ emitUpdate: false })` para evitar mismatch de hidratação.
- Validação de assunto no Dialog “Novo ticket” (trim + `setError`) para prevenir `ZodError` em runtime.
Arquivos principais tocados:
- Convex: `web/convex/schema.ts`, `web/convex/tickets.ts` (novas mutations + tipagem `Doc/Id`).
- UI: `ticket-summary-header.tsx`, `ticket-detail-view.tsx`, `ticket-comments.rich.tsx`, `new-ticket-dialog.tsx`, `play-next-ticket-card.tsx`.
- Tipos e mapeadores: `web/src/lib/schemas/ticket.ts`, `web/src/lib/mappers/ticket.ts`.
## Guia de layout/UX aplicado
- Header do ticket
- Ordem: `#ref` • PrioritySelect (badge) • Status (badge/select) • Ações (Excluir)
- Tipografia: título forte, resumo como texto auxiliar, metadados em texto pequeno.
- Combos de Categoria/ Subcategoria exibidos como selects dependentes com salvamento automático (sem botões dedicados).
- Comentários
- Composer com rich text + Dropzone; seletor de visibilidade.
- Lista com avatar, nome, carimbo relativo e conteúdo rich text.
- Prioridades (labels)
- LOW (cinza), MEDIUM (azul), HIGH (âmbar), URGENT (vermelho) — badge translúcida no trigger do select.
## Próximos passos sugeridos (UI/Funcionais)
Curto prazo (incremental):
- [ ] Transformar Status em dropdown-badge (mesmo padrão de Prioridade).
- [ ] Estados vazios com `Empty` (ícone, título, descrição, CTA) na lista de comentários e tabela.
- [ ] Edição inline no header (Assunto/Resumo) com botões Reset/Salvar (mutations dedicadas).
- [ ] Polir cards (bordas/padding/sombra) nas telas Play/Tickets para padronizar com Header/Conversa.
Médio prazo:
- [ ] Combobox (command) para responsável com busca.
- [ ] Paginação/ordenção server-side em `tickets.list`.
- [ ] Unificar mensagens de timeline e payloads (sempre `actorName`/`actorAvatar`).
- [ ] Testes Vitest para mapeadores e smoke tests básicos das páginas.
## Como validar manualmente
- Rich text: comentar em `/tickets/[id]` com formatação, anexos e alternando visibilidade.
- Prioridade: alterar no cabeçalho; observar evento de timeline e toasts.
- Exclusão: acionar modal no cabeçalho e confirmar; conferir redirecionamento para `/tickets`.
- Novo ticket: usar Dialog; assunto com menos de 3 chars deve bloquear submit com erro no campo.
---
## Atualizações recentes (abr/2025)
Resumo do que foi integrado nesta rodada para o núcleo de tickets e UX:
- Header do ticket
- Status como dropdownbadge (padrão visual alinhado às badges existentes).
- Edição inline de Assunto/Resumo com Cancelar/Salvar e toasts.
- Ação de Play/Pause (toggle de atendimento) com eventos WORK_STARTED/WORK_PAUSED na timeline.
- Layout dos campos reorganizado: labels acima e controles abaixo (evita redundância do valor + dropdown lado a lado).
- Tabela e comentários
- Empty states padronizados com Empty + CTA de novo ticket.
- Notificações
- Toaster centralizado no rodapé (bottomcenter) com estilo consistente.
- Título do app
- Atualizado para “Sistema de chamados”.
Backend Convex
- ickets.updateSubject e ickets.updateSummary adicionadas para edição do cabeçalho.
- ickets.toggleWork adicionada; campo opcional working no schema de ickets.
Próximos passos sugeridos
- Status dropdownbadge também na tabela (edição rápida opcional com confirmação).
- Combobox (command) para busca de responsável no select.
- Tokens de cor: manter badges padrão do design atual; quando migração completa para paleta Rever estiver definida, aplicar via globals.css para herdar em todos os componentes.
- Testes (Vitest): adicionar casos de mappers e smoke tests de páginas.
Observações de codificação
- Evitar `any`; usar TicketStatus/TicketPriority e Id<>/Doc<> do Convex.
- Não retornar Date do Convex; sempre epoch (number) e converter via mappers Zod.
## Atualizações recentes (out/2025)
- Cabeçalho de ticket agora persiste automaticamente mudanças de categoria/subcategoria, mostrando toasts e bloqueando os selects enquanto a mutação é processada.
- Normalização de nomes de fila/time aplicada também ao retorno de `tickets.playNext`, garantindo rótulos "Chamados"/"Laboratório" em todos os fluxos.
- ESLint ignora `convex/_generated/**` e supressões migradas para `@ts-expect-error` com justificativa explícita.
- Mutação `tickets.remove` não requer mais `actorId`; o diálogo de exclusão apenas envia `ticketId`.
## Atualizações recentes (nov/2025)
- Dialog de novo ticket redesenhado: duas colunas com botão “Criar” no cabeçalho, dropzone mais compacta, categorias primária/secundária empilhadas e rótulos explícitos.
- Validação do assunto relaxada para evitar `ZodError` prematuro; verificação manual permanece na submissão.
- Placeholder cinza claro "Escreva um comentário..." aplicado ao editor Tiptap e seção renomeada para “Comentários”.
- Linhas da tabela de tickets agora são totalmente clicáveis (mouse e teclado), reforçando acessibilidade e atalho de navegação.
- Toasts e layouts refinados para manter consistência entre criação, listagem e detalhe dos tickets.
## Atualizações recentes (out/2025)
- Tabela de tickets refinada com ícones de canal, prioridade ajustável inline e indicadores suavizados (fila/status/categoria) para reduzir ruído visual.
- Definido plano de migração para Better Auth com RBAC (admin/agent/customer), portal do cliente e painel administrativo para filas/categorias/agentes.
- Próximo passo: iniciar fase de implementação da autenticação real, substituindo middleware placeholder e alinhando Convex aos novos papéis.
- Better Auth agora usa banco SQLite local (`db.sqlite`) e o schema Prisma foi migrado com sucesso via `pnpm exec prisma migrate dev --name init`.
- Configuração do `postcss.config.mjs` corrigida para usar `@tailwindcss/postcss` como plugin executável, liberando a suíte do Vitest (`pnpm exec vitest run`).
- Script `pnpm auth:seed` cria/atualiza o usuário inicial (`admin@sistema.dev` / `admin123`) usando `better-auth/crypto` para hash de senha.
- Página de login refeita com layout em duas colunas (header + imagem lateral) e formulário integrado ao Better Auth (`LoginForm`).
- Middleware atualizado aplica RBAC inicial (clientes direcionados ao portal, rotas `/admin` reservadas a administradores) e helpers de role expostos em `src/lib/authz.ts`; página `/portal` criada como placeholder do futuro autosserviço.
## Próximos passos estratégicos
### Produto / Experiência
- [ ] Unificar revisão visual do modal de novo ticket com microinterações (estado de salvamento, validações inline).
- [ ] Implementar filtros salváveis e quick actions na listagem (ex.: alterar status diretamente).
- [ ] Exibir indicadores de anexos na tabela e nos cartões de “tickets recentes”.
### Técnica
- [ ] Corrigir configuração do `postcss.config.mjs` (plugin inválido impede execução do Vitest) e restaurar cobertura de testes automatizados.
- [ ] Formalizar camada de autenticação (Auth.js ou Clerk) com refresh de sessão e proteção de rotas no Convex (`auth.getUserIdentity`).
- [ ] Mapear RBAC inicial (admin/agente/visualização) e refletir nas mutations do Convex.
- [ ] Configurar ambientes `staging`/`production` do Convex com variáveis (.env) versionadas via doppler/1Password.
- [ ] Automatizar lint/test/build no CI (GitHub Actions) e bloquear merge sem execução.
### Administrativa / Operacional
- [ ] Inventariar acessos: quem possui permissão no Convex, GitHub e futuros serviços (Redis, email, armazenamento S3?).
- [ ] Criar checklists de onboarding/offboarding de agentes (criação de usuário, associação a filas, provisionamento de avatar).
- [ ] Definir plano de capacidade para armazenamento de anexos (quotas por tenant, política de retenção) e alertas.
- [ ] Preparar mock de integrações externas (e-mail entrante, WhatsApp) para futuras etapas.
- [ ] Documentar fluxo de suporte interno (quem revisa PRs, janelas de deploy, rollback).
Manter este arquivo atualizado ao concluir cada item estratégico ou quando surgirem novas dependências administrativas.
## Atualizações recentes (mai/2026)
- Login corporativo refinado com instruções revisadas para primeiro acesso e mensagens de erro totalmente em PT-BR.
- Script `pnpm auth:seed` executado para garantir o usuário administrador padrão (`admin@sistema.dev` / `admin123`).
- Toast de autenticação inválida agora informa "E-mail ou senha inválidos", alinhando o feedback com o restante da interface.
### Próximos passos imediatos
- [ ] Implementar fluxo completo de convites (criação, expiração, revogação) integrado ao Better Auth e Convex.
- [ ] Adicionar testes Vitest/E2E cobrindo dashboards, relatórios e guardas de RBAC no front.
- [ ] Mapear permissões de edição avançada para agentes (categorias, campos rápidos) antes de liberar novas mutações.
## Atualizações recentes (jun/2026)
- RBAC do Convex reforçado em times, filas, campos, SLAs e relatórios; todas as chamadas exigem `viewerId`/`actorId` conforme o papel (admin ou staff).
- Painel administrativo atualizado para consumir as novas assinaturas protegidas, com validações de sessão Better Auth e feedback de toasts.
- Dashboard principal passou a exibir métricas reais via `reports.dashboardOverview` e séries históricas por canal com `reports.ticketsByChannel`.
- Portal do cliente publicado com isolamento por `viewerId`, garantindo que clientes visualizem apenas seus chamados.
## Atualizações recentes (ago/2026)
- Convites Better Auth finalizados ponta a ponta: novos modelos Prisma, utilitários de servidor, rotas Next e tabela `userInvites` no Convex com sincronização e RBAC.
- Painel administrativo reorganizado com `CategoriesManager`, permitindo CRUD completo de categorias e subcategorias, inclusive cadastro em lote na criação.
- Campos personalizados de tickets agora são validados e persistidos no Convex (`tickets.customFields`) com normalização por tipo, `displayValue` e mapeamento seguro no frontend.
- Consultas e componentes que consomem `queues.summary` passaram a enviar `viewerId`, eliminando erros de autorização na UI de tickets.
- Suite de testes estendida com `invite-utils.test.ts` e configuração `vitest.setup.ts`, garantindo ambiente consistente com variáveis Better Auth.
## Histórico resumido
- Scaffold Next.js + Turbopack configurado com Better Auth e Convex.
- Portal do cliente entregue com isolamento por `viewerId`.
- Fluxo de convites e painel administrativo operacionais.
- Iteração atual focada em UX de criação de tickets, consistência de layout e guardas de sessão.

View file

@ -43,9 +43,10 @@ export const seedDemo = mutation({
if (found) return found._id;
return await ctx.db.insert("users", { tenantId, name, email, role, avatarUrl: `https://avatar.vercel.sh/${name.split(" ")[0]}` });
}
const anaId = await ensureUser("Ana Souza", "ana.souza@example.com");
const brunoId = await ensureUser("Bruno Lima", "bruno.lima@example.com");
const reverId = await ensureUser("Rever", "renan.pac@paulicon.com.br");
const agenteDemoId = await ensureUser("Agente Demo", "agente.demo@sistema.dev");
const eduardaId = await ensureUser("Eduarda Rocha", "eduarda.rocha@example.com", "CUSTOMER");
const clienteDemoId = await ensureUser("Cliente Demo", "cliente.demo@sistema.dev", "CUSTOMER");
// Seed a couple of tickets
const now = Date.now();
@ -68,7 +69,7 @@ export const seedDemo = mutation({
channel: "EMAIL",
queueId: queue1,
requesterId: eduardaId,
assigneeId: anaId,
assigneeId: reverId,
createdAt: now - 1000 * 60 * 60 * 5,
updatedAt: now - 1000 * 60 * 10,
tags: ["portal", "cliente"],
@ -84,8 +85,8 @@ export const seedDemo = mutation({
priority: "HIGH",
channel: "WHATSAPP",
queueId: queue2,
requesterId: eduardaId,
assigneeId: brunoId,
requesterId: clienteDemoId,
assigneeId: agenteDemoId,
createdAt: now - 1000 * 60 * 60 * 8,
updatedAt: now - 1000 * 60 * 30,
tags: ["Integração", "erp"],

View file

@ -48,6 +48,7 @@
"lucide-react": "^0.544.0",
"next": "15.5.4",
"next-themes": "^0.4.6",
"postcss": "^8.5.6",
"react": "19.2.0",
"react-dom": "19.2.0",
"react-hook-form": "^7.64.0",

3
web/pnpm-lock.yaml generated
View file

@ -113,6 +113,9 @@ importers:
next-themes:
specifier: ^0.4.6
version: 0.4.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
postcss:
specifier: ^8.5.6
version: 8.5.6
react:
specifier: 19.2.0
version: 19.2.0

View file

@ -0,0 +1,87 @@
import { ConvexHttpClient } from "convex/browser"
const CONVEX_URL = process.env.NEXT_PUBLIC_CONVEX_URL
const TENANT_ID = process.env.SEED_TENANT_ID ?? "tenant-atlas"
if (!CONVEX_URL) {
console.error("NEXT_PUBLIC_CONVEX_URL não definido. Configure o endpoint do Convex e execute novamente.")
process.exit(1)
}
const TARGET_NAMES = new Set(["Ana Souza", "Bruno Lima"])
const REPLACEMENT = {
name: "Rever",
email: "renan.pac@paulicon.com.br",
}
async function main() {
const client = new ConvexHttpClient(CONVEX_URL)
const admin = await client.mutation("users:ensureUser", {
tenantId: TENANT_ID,
email: "admin@sistema.dev",
name: "Administrador",
role: "ADMIN",
})
if (!admin?._id) {
throw new Error("Não foi possível garantir o usuário administrador")
}
const replacementUser = await client.mutation("users:ensureUser", {
tenantId: TENANT_ID,
email: REPLACEMENT.email,
name: REPLACEMENT.name,
role: "AGENT",
})
if (!replacementUser?._id) {
throw new Error("Não foi possível garantir o usuário Rever")
}
const agents = await client.query("users:listAgents", { tenantId: TENANT_ID })
const targets = agents.filter((agent) => TARGET_NAMES.has(agent.name))
if (targets.length === 0) {
console.log("Nenhum responsável legado encontrado. Nada a atualizar.")
}
const targetIds = new Set(targets.map((agent) => agent._id))
const tickets = await client.query("tickets:list", {
tenantId: TENANT_ID,
viewerId: admin._id,
})
let reassignedCount = 0
for (const ticket of tickets) {
if (ticket.assignee && targetIds.has(ticket.assignee.id)) {
await client.mutation("tickets:changeAssignee", {
ticketId: ticket.id,
assigneeId: replacementUser._id,
actorId: admin._id,
})
reassignedCount += 1
console.log(`Ticket ${ticket.reference} reatribuído para ${replacementUser.name}`)
}
}
for (const agent of targets) {
try {
await client.mutation("users:deleteUser", {
userId: agent._id,
actorId: admin._id,
})
console.log(`Usuário removido: ${agent.name}`)
} catch (error) {
console.error(`Falha ao remover ${agent.name}:`, error)
}
}
console.log(`Total de tickets reatribuídos: ${reassignedCount}`)
}
main().catch((error) => {
console.error("Erro ao reatribuir responsáveis legacy:", error)
process.exitCode = 1
})

View file

@ -4,13 +4,43 @@ import { hashPassword } from "better-auth/crypto"
const { PrismaClient } = pkg
const prisma = new PrismaClient()
const email = process.env.SEED_USER_EMAIL ?? "admin@sistema.dev"
const password = process.env.SEED_USER_PASSWORD ?? "admin123"
const name = process.env.SEED_USER_NAME ?? "Administrador"
const role = process.env.SEED_USER_ROLE ?? "admin"
const tenantId = process.env.SEED_USER_TENANT ?? "tenant-atlas"
async function main() {
const singleUserFromEnv = process.env.SEED_USER_EMAIL
? [{
email: process.env.SEED_USER_EMAIL,
password: process.env.SEED_USER_PASSWORD ?? "admin123",
name: process.env.SEED_USER_NAME ?? "Administrador",
role: process.env.SEED_USER_ROLE ?? "admin",
tenantId,
}]
: null
const defaultUsers = singleUserFromEnv ?? [
{
email: "admin@sistema.dev",
password: "admin123",
name: "Administrador",
role: "admin",
tenantId,
},
{
email: "agente.demo@sistema.dev",
password: "agent123",
name: "Agente Demo",
role: "agent",
tenantId,
},
{
email: "cliente.demo@sistema.dev",
password: "cliente123",
name: "Cliente Demo",
role: "customer",
tenantId,
},
]
async function upsertAuthUser({ email, password, name, role, tenantId: userTenant }: (typeof defaultUsers)[number]) {
const hashedPassword = await hashPassword(password)
const user = await prisma.authUser.upsert({
@ -18,13 +48,13 @@ async function main() {
update: {
name,
role,
tenantId,
tenantId: userTenant,
},
create: {
email,
name,
role,
tenantId,
tenantId: userTenant,
accounts: {
create: {
providerId: "credential",
@ -79,7 +109,13 @@ async function main() {
console.log(` Role: ${user.role}`)
console.log(` Tenant: ${user.tenantId ?? "(nenhum)"}`)
console.log(` Provider: ${account?.providerId ?? "-"}`)
console.log(`Senha provisoria: ${password}`)
console.log(` Senha provisoria: ${password}`)
}
async function main() {
for (const user of defaultUsers) {
await upsertAuthUser(user)
}
}
main()

View file

@ -2,6 +2,7 @@ import { AppShell } from "@/components/app-shell"
import { SiteHeader } from "@/components/site-header"
import { TicketDetailView } from "@/components/tickets/ticket-detail-view"
import { TicketDetailStatic } from "@/components/tickets/ticket-detail-static"
import { NewTicketDialog } from "@/components/tickets/new-ticket-dialog"
import { getTicketById } from "@/lib/mocks/tickets"
import type { TicketWithDetails } from "@/lib/schemas/ticket"
@ -21,7 +22,7 @@ export default async function TicketDetailPage({ params }: TicketDetailPageProps
title={`Ticket #${id}`}
lead={"Detalhes do ticket"}
secondaryAction={<SiteHeader.SecondaryButton>Compartilhar</SiteHeader.SecondaryButton>}
primaryAction={<SiteHeader.PrimaryButton>Adicionar comentário</SiteHeader.PrimaryButton>}
primaryAction={<NewTicketDialog />}
/>
}
>

View file

@ -14,7 +14,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { RichTextEditor } from "@/components/ui/rich-text-editor"
import { RichTextEditor, sanitizeEditorHtml } from "@/components/ui/rich-text-editor"
import { Spinner } from "@/components/ui/spinner"
import { Badge } from "@/components/ui/badge"
import { cn } from "@/lib/utils"
@ -59,6 +59,7 @@ export default function NewTicketPage() {
const [subcategoryId, setSubcategoryId] = useState<string | null>(null)
const [categoryError, setCategoryError] = useState<string | null>(null)
const [subcategoryError, setSubcategoryError] = useState<string | null>(null)
const [descriptionError, setDescriptionError] = useState<string | null>(null)
const [assigneeInitialized, setAssigneeInitialized] = useState(false)
const queueOptions = useMemo(() => queues.map((q) => q.name), [queues])
@ -91,6 +92,14 @@ export default function NewTicketPage() {
return
}
const sanitizedDescription = sanitizeEditorHtml(description)
const plainDescription = sanitizedDescription.replace(/<[^>]*>/g, "").trim()
if (plainDescription.length === 0) {
setDescriptionError("Descreva o contexto do chamado.")
return
}
setDescriptionError(null)
setLoading(true)
toast.loading("Criando ticket...", { id: "create-ticket" })
try {
@ -110,13 +119,12 @@ export default function NewTicketPage() {
categoryId: categoryId as Id<"ticketCategories">,
subcategoryId: subcategoryId as Id<"ticketSubcategories">,
})
const plainDescription = description.replace(/<[^>]*>/g, "").trim()
if (plainDescription.length > 0) {
await addComment({
ticketId: id as Id<"tickets">,
authorId: convexUserId as Id<"users">,
visibility: "PUBLIC",
body: description,
body: sanitizedDescription,
attachments: [],
})
}
@ -143,8 +151,8 @@ export default function NewTicketPage() {
<CardContent>
<form onSubmit={submit} className="space-y-6">
<div className="space-y-2">
<label className="text-sm font-medium text-neutral-700" htmlFor="subject">
Assunto
<label className="flex items-center gap-1 text-sm font-medium text-neutral-700" htmlFor="subject">
Assunto <span className="text-red-500">*</span>
</label>
<Input
id="subject"
@ -171,8 +179,23 @@ export default function NewTicketPage() {
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-neutral-700">Descrição</label>
<RichTextEditor value={description} onChange={setDescription} placeholder="Detalhe o problema, passos para reproduzir, links, etc." />
<label className="flex items-center gap-1 text-sm font-medium text-neutral-700">
Descrição <span className="text-red-500">*</span>
</label>
<RichTextEditor
value={description}
onChange={(html) => {
setDescription(html)
if (descriptionError) {
const plain = html.replace(/<[^>]*>/g, "").trim()
if (plain.length > 0) {
setDescriptionError(null)
}
}
}}
placeholder="Detalhe o problema, passos para reproduzir, links, etc."
/>
{descriptionError ? <p className="text-xs font-medium text-red-500">{descriptionError}</p> : null}
</div>
<div className="space-y-2">
<CategorySelectFields
@ -187,6 +210,8 @@ export default function NewTicketPage() {
setSubcategoryId(value)
setSubcategoryError(null)
}}
categoryLabel="Categoria primária *"
subcategoryLabel="Categoria secundária *"
/>
{categoryError || subcategoryError ? (
<div className="text-xs font-medium text-red-500">

View file

@ -52,8 +52,8 @@ export function PortalTicketForm() {
const [isSubmitting, setIsSubmitting] = useState(false)
const isFormValid = useMemo(() => {
return Boolean(subject.trim() && categoryId && subcategoryId)
}, [subject, categoryId, subcategoryId])
return Boolean(subject.trim() && description.trim() && categoryId && subcategoryId)
}, [subject, description, categoryId, subcategoryId])
async function handleSubmit(event: React.FormEvent) {
event.preventDefault()
@ -61,6 +61,7 @@ export function PortalTicketForm() {
const trimmedSubject = subject.trim()
const trimmedSummary = summary.trim()
const trimmedDescription = description.trim()
setIsSubmitting(true)
toast.loading("Abrindo chamado...", { id: "portal-new-ticket" })
@ -78,8 +79,8 @@ export function PortalTicketForm() {
subcategoryId: subcategoryId as Id<"ticketSubcategories">,
})
if (description.trim().length > 0) {
const htmlBody = sanitizeEditorHtml(toHtml(description.trim()))
if (trimmedDescription.length > 0) {
const htmlBody = sanitizeEditorHtml(toHtml(trimmedDescription))
await addComment({
ticketId: id as Id<"tickets">,
authorId: convexUserId as Id<"users">,
@ -108,8 +109,8 @@ export function PortalTicketForm() {
<form onSubmit={handleSubmit} className="space-y-6">
<div className="space-y-3">
<div className="space-y-1">
<label htmlFor="subject" className="text-sm font-medium text-neutral-800">
Assunto
<label htmlFor="subject" className="flex items-center gap-1 text-sm font-medium text-neutral-800">
Assunto <span className="text-red-500">*</span>
</label>
<Input
id="subject"
@ -131,14 +132,15 @@ export function PortalTicketForm() {
/>
</div>
<div className="space-y-1">
<label htmlFor="description" className="text-sm font-medium text-neutral-800">
Detalhes
<label htmlFor="description" className="flex items-center gap-1 text-sm font-medium text-neutral-800">
Detalhes <span className="text-red-500">*</span>
</label>
<Textarea
id="description"
value={description}
onChange={(event) => setDescription(event.target.value)}
placeholder="Compartilhe passos para reproduzir, mensagens de erro ou informações adicionais."
required
className="min-h-[140px] resize-y rounded-xl border border-slate-200 px-4 py-3 text-sm text-neutral-800 shadow-sm focus-visible:border-neutral-900 focus-visible:ring-neutral-900/20"
/>
</div>
@ -168,8 +170,8 @@ export function PortalTicketForm() {
onCategoryChange={setCategoryId}
onSubcategoryChange={setSubcategoryId}
layout="stacked"
categoryLabel="Categoria"
subcategoryLabel="Subcategoria"
categoryLabel="Categoria *"
subcategoryLabel="Subcategoria *"
secondaryEmptyLabel="Selecione uma categoria"
/>
</div>

View file

@ -19,7 +19,7 @@ import { zodResolver } from "@hookform/resolvers/zod"
import { toast } from "sonner"
import { Spinner } from "@/components/ui/spinner"
import { Dropzone } from "@/components/ui/dropzone"
import { RichTextEditor } from "@/components/ui/rich-text-editor"
import { RichTextEditor, sanitizeEditorHtml } from "@/components/ui/rich-text-editor"
import {
PriorityIcon,
priorityStyles,
@ -27,9 +27,9 @@ import {
import { CategorySelectFields } from "@/components/tickets/category-select"
const schema = z.object({
subject: z.string().optional(),
subject: z.string().default(""),
summary: z.string().optional(),
description: z.string().optional(),
description: z.string().default(""),
priority: z.enum(["LOW", "MEDIUM", "HIGH", "URGENT"]).default("MEDIUM"),
channel: z.enum(["EMAIL", "WHATSAPP", "CHAT", "PHONE", "API", "MANUAL"]).default("MANUAL"),
queueName: z.string().nullable().optional(),
@ -124,11 +124,20 @@ export function NewTicketDialog() {
async function submit(values: z.infer<typeof schema>) {
if (!convexUserId) return
const subjectTrimmed = (values.subject ?? "").trim()
if (subjectTrimmed.length < 3) {
form.setError("subject", { type: "min", message: "Informe um assunto" })
form.setError("subject", { type: "min", message: "Informe um assunto com pelo menos 3 caracteres." })
return
}
const sanitizedDescription = sanitizeEditorHtml(values.description ?? "")
const plainDescription = sanitizedDescription.replace(/<[^>]*>/g, "").trim()
if (plainDescription.length === 0) {
form.setError("description", { type: "custom", message: "Descreva o contexto do chamado." })
return
}
setLoading(true)
toast.loading("Criando ticket…", { id: "new-ticket" })
try {
@ -138,7 +147,7 @@ export function NewTicketDialog() {
actorId: convexUserId as Id<"users">,
tenantId: DEFAULT_TENANT_ID,
subject: subjectTrimmed,
summary: values.summary,
summary: values.summary?.trim() || undefined,
priority: values.priority,
channel: values.channel,
queueId: sel?.id as Id<"queues"> | undefined,
@ -147,8 +156,8 @@ export function NewTicketDialog() {
categoryId: values.categoryId as Id<"ticketCategories">,
subcategoryId: values.subcategoryId as Id<"ticketSubcategories">,
})
const hasDescription = (values.description ?? "").replace(/<[^>]*>/g, "").trim().length > 0
const bodyHtml = hasDescription ? (values.description as string) : (values.summary || "")
const summaryFallback = values.summary?.trim() ?? ""
const bodyHtml = plainDescription.length > 0 ? sanitizedDescription : summaryFallback
if (attachments.length > 0 || bodyHtml.trim().length > 0) {
const typedAttachments = attachments.map((a) => ({
storageId: a.storageId as unknown as Id<"_storage">,
@ -160,7 +169,18 @@ export function NewTicketDialog() {
}
toast.success("Ticket criado!", { id: "new-ticket" })
setOpen(false)
form.reset()
form.reset({
subject: "",
summary: "",
description: "",
priority: "MEDIUM",
channel: "MANUAL",
queueName: null,
assigneeId: convexUserId ?? null,
categoryId: "",
subcategoryId: "",
})
form.clearErrors()
setAssigneeInitialized(false)
setAttachments([])
// Navegar para o ticket recém-criado
@ -213,7 +233,9 @@ export function NewTicketDialog() {
<FieldGroup className="lg:grid-cols-[minmax(0,1.2fr)_minmax(0,0.8fr)]">
<div className="space-y-4">
<Field>
<FieldLabel htmlFor="subject">Assunto</FieldLabel>
<FieldLabel htmlFor="subject" className="flex items-center gap-1">
Assunto <span className="text-destructive">*</span>
</FieldLabel>
<Input id="subject" {...form.register("subject")} placeholder="Ex.: Erro 500 no portal" />
<FieldError errors={form.formState.errors.subject ? [{ message: form.formState.errors.subject.message }] : []} />
</Field>
@ -227,12 +249,27 @@ export function NewTicketDialog() {
/>
</Field>
<Field>
<FieldLabel>Descrição</FieldLabel>
<FieldLabel className="flex items-center gap-1">
Descrição <span className="text-destructive">*</span>
</FieldLabel>
<RichTextEditor
value={form.watch("description") || ""}
onChange={(html) => form.setValue("description", html)}
onChange={(html) =>
form.setValue("description", html, {
shouldDirty: true,
shouldTouch: true,
shouldValidate: form.formState.isSubmitted,
})
}
placeholder="Detalhe o problema, passos para reproduzir, links, etc."
/>
<FieldError
errors={
form.formState.errors.description
? [{ message: form.formState.errors.description.message }]
: []
}
/>
</Field>
<Field>
<FieldLabel>Anexos</FieldLabel>
@ -251,8 +288,8 @@ export function NewTicketDialog() {
subcategoryId={subcategoryIdValue || null}
onCategoryChange={handleCategoryChange}
onSubcategoryChange={handleSubcategoryChange}
categoryLabel="Categoria primária"
subcategoryLabel="Categoria secundária"
categoryLabel="Categoria primária *"
subcategoryLabel="Categoria secundária *"
layout="stacked"
/>
{form.formState.errors.categoryId?.message || form.formState.errors.subcategoryId?.message ? (

View file

@ -17,18 +17,11 @@ const tenantId = "tenant-atlas"
type UserRecord = z.infer<typeof ticketSchema>["requester"]
const users: Record<string, UserRecord> = {
ana: {
id: "user-ana",
name: "Ana Souza",
email: "ana.souza@example.com",
avatarUrl: "https://avatar.vercel.sh/ana",
teams: ["Chamados"],
},
bruno: {
id: "user-bruno",
name: "Bruno Lima",
email: "bruno.lima@example.com",
avatarUrl: "https://avatar.vercel.sh/bruno",
rever: {
id: "user-rever",
name: "Rever",
email: "renan.pac@paulicon.com.br",
avatarUrl: "https://avatar.vercel.sh/rever",
teams: ["Chamados"],
},
carla: {
@ -72,7 +65,7 @@ const baseTickets = [
channel: ticketChannelSchema.enum.EMAIL,
queue: "Chamados",
requester: users.eduarda,
assignee: users.ana,
assignee: users.rever,
slaPolicy: {
id: "sla-critical",
name: "SLA Crítico",
@ -85,7 +78,7 @@ const baseTickets = [
updatedAt: subMinutes(new Date(), 10),
createdAt: subHours(new Date(), 5),
tags: ["portal", "cliente"],
lastTimelineEntry: "Prioridade atualizada para URGENT por Bruno",
lastTimelineEntry: "Prioridade atualizada para URGENT por Rever",
metrics: {
timeWaitingMinutes: 12,
timeOpenedMinutes: 300,
@ -183,7 +176,7 @@ const baseTickets = [
channel: ticketChannelSchema.enum.EMAIL,
queue: "Chamados",
requester: users.eduarda,
assignee: users.bruno,
assignee: users.rever,
slaPolicy: {
id: "sla-standard",
name: "SLA Padrão",
@ -210,7 +203,7 @@ const commentsByTicket: Record<string, z.infer<typeof ticketWithDetailsSchema>["
"ticket-1001": [
{
id: "comment-1",
author: users.ana,
author: users.rever,
visibility: commentVisibilitySchema.enum.INTERNAL,
body: "Logs coletados e enviados para o time de infraestrutura.",
attachments: [],
@ -219,7 +212,7 @@ const commentsByTicket: Record<string, z.infer<typeof ticketWithDetailsSchema>["
},
{
id: "comment-2",
author: users.bruno,
author: users.rever,
visibility: commentVisibilitySchema.enum.PUBLIC,
body: "Estamos investigando o incidente, retorno em 30 minutos.",
attachments: [],
@ -251,13 +244,13 @@ const timelineByTicket: Record<string, z.infer<typeof ticketWithDetailsSchema>["
{
id: "timeline-2",
type: "ASSIGNEE_CHANGED",
payload: { assignee: users.ana.name },
payload: { assignee: users.rever.name },
createdAt: subHours(new Date(), 4),
},
{
id: "timeline-3",
type: "COMMENT_ADDED",
payload: { author: users.ana.name },
payload: { author: users.rever.name },
createdAt: subHours(new Date(), 1),
},
],