feat: harden ticket creation ux and seeding
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
parent
be27dcfd15
commit
a51783ce29
11 changed files with 338 additions and 537 deletions
505
agents.md
505
agents.md
|
|
@ -3,461 +3,76 @@
|
||||||
## Contato principal
|
## Contato principal
|
||||||
- **Esdras Renan** — monkeyesdras@gmail.com
|
- **Esdras Renan** — monkeyesdras@gmail.com
|
||||||
|
|
||||||
## Ambiente local
|
## Credenciais padrão (Better Auth)
|
||||||
- Admin: `admin@sistema.dev` / `admin123`
|
- Administrador: `admin@sistema.dev` / `admin123`
|
||||||
- Agentes seed (senha inicial `agent123` — alterar no primeiro acesso):
|
- Agente Demo: `agente.demo@sistema.dev` / `agent123`
|
||||||
- Gabriel Oliveira · george.araujo@rever.com.br
|
- Cliente Demo: `cliente.demo@sistema.dev` / `cliente123`
|
||||||
- George Araujo · george.araujo@rever.com.br
|
> 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_*`.
|
||||||
- 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
|
|
||||||
|
|
||||||
> 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
|
## Setup local rápido
|
||||||
- **Meta imediata:** consolidar o núcleo de tickets web/desktop com canais, SLAs e automações futuras.
|
1. `cd web && pnpm install`
|
||||||
- **Stack:** Next.js (App Router) + Convex + Better Auth + Prisma (referência de domínio).
|
2. `cp .env.example .env.local` e ajuste `NEXT_PUBLIC_CONVEX_URL` apontando para o servidor Convex local.
|
||||||
- **Estado:** núcleo web funcional (tickets, play mode, painéis administrativos, portal do cliente) com Turbopack habilitado no `pnpm dev`.
|
3. `pnpm --dir web auth:seed`
|
||||||
|
4. `pnpm --dir web convex:dev`
|
||||||
|
5. Em outro terminal: `pnpm --dir web dev`
|
||||||
|
|
||||||
## Entregas concluídas
|
## Estado atual
|
||||||
- Scaffold Next.js + Tailwind + shadcn/ui, shell com sidebar/header, login real com Better Auth.
|
- Autenticação Better Auth com guardas client-side (`AuthGuard`) bloqueando rotas protegidas.
|
||||||
- Integração Convex completa: listas/detalhe de tickets, mutations (status, categorias, filas, comentários, play next).
|
- Menu de usuário no rodapé da sidebar com link para `/settings` e logout confiável.
|
||||||
- Painel administrativo: gestão de filas, times, campos personalizados e convites Better Auth.
|
- 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 isolado por `viewerId`; dashboard principal consumindo métricas reais do Convex.
|
- Portal do cliente restringe visualização e criação ao próprio requester; clientes não atribuem responsáveis.
|
||||||
- Fluxo de convites Better Auth ponta a ponta + seed automatizado de agentes/admin.
|
- Relatórios e dashboards utilizam `AppShell`, garantindo header/sidebar consistentes.
|
||||||
|
|
||||||
## Desenvolvimento em curso
|
## Entregas recentes relevantes
|
||||||
- Refinar sincronização Better Auth ↔ Convex (resets de senha, revogação automática de convites).
|
- Correção do redirecionamento após logout evitando retorno imediato ao dashboard.
|
||||||
- Melhorar UX do ticket header (categorias, status, prioridades) e comandos rápidos na listagem.
|
- Validações manuais dos formulários de rich text para eliminar `ZodError` durante edição.
|
||||||
- Manter hidratação consistente na sidebar e componentes Radix após migração para React 19.
|
- 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
|
## Fluxos suportados
|
||||||
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.
|
|
||||||
|
|
||||||
## Boas práticas e rotinas
|
### Equipe interna (admin/agent/collaborator)
|
||||||
- **Seeds:** `node --env-file=.env.local scripts/seed-agents.mjs` (mantém admin e agentes) + `/dev/seed` para dados demo.
|
- Criar tickets com categorias, responsável inicial e anexos.
|
||||||
- **Serviços locais:** `pnpm convex:dev` (gera tipos e roda backend) e `pnpm dev` (Next.js com Turbopack).
|
- Abrir novos tickets diretamente a partir do detalhe via dialog reutilizável.
|
||||||
- **Testes e lint:** execute `pnpm lint` e `pnpm vitest run` antes de cada PR.
|
- Acessar `/settings` para ajustes pessoais e efetuar logout pelo menu.
|
||||||
- **Convex:** retorne apenas tipos suportados (`number` para datas) e valide no front via mappers Zod.
|
|
||||||
- **UI:** textos PT‑BR, toasts com feedback, atualizações otimistas com rollback em caso de erro.
|
|
||||||
- **Git/PR:** branches descritivas, checklist padrão (tipos Convex, labels PT‑BR, loaders, mappers atualizados) e coautor `factory-droid[bot]` quando aplicável.
|
|
||||||
|
|
||||||
## Histórico de marcos
|
### Clientes
|
||||||
- Fase A (scaffold/UX base) e Fase B (núcleo de tickets) concluídas.
|
- Autenticam com `cliente.demo@sistema.dev`.
|
||||||
- Iniciativa “Autenticação real e personas” entregue com RBAC completo e portal do cliente.
|
- Abrem tickets para si mesmos a partir do portal com assunto/descrição obrigatórios.
|
||||||
- Roadmap imediato focado em credenciais unificadas, automações de convites e cobertura de testes.
|
- 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
|
## Rotina antes de abrir PR
|
||||||
Construir o nucleo de tickets compartilhado entre web e desktop (Tauri), garantindo base solida para canais, SLAs e automacoes futuras.
|
- `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
|
## Convenções
|
||||||
- **Esdras Renan** — monkeyesdras@gmail.com
|
- 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.
|
||||||
### Credenciais seed (ambiente local)
|
- Reutilizar componentes shadcn existentes e seguir o estilo do arquivo editado.
|
||||||
- Administrador padrão: `admin@sistema.dev` / `admin123`
|
- Validações client-side críticas devem sinalizar erros inline e exibir toast.
|
||||||
- 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 PT‑BR (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`.
|
|
||||||
|
|
||||||
## Estrutura útil
|
## Estrutura útil
|
||||||
- `web/convex/*` — API backend Convex.
|
- `web/convex/` — queries e mutations (ex.: `tickets.ts`, `users.ts`).
|
||||||
- `web/src/lib/mappers/*` — Conversores server→UI com Zod.
|
- `web/src/components/tickets/` — UI interna (dialog, listas, header, timeline).
|
||||||
- `web/src/components/tickets/*` — Tabela, filtros, detalhe, timeline, comentários, play.
|
- `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)
|
## Histórico resumido
|
||||||
- `pnpm convex:dev` — Convex (dev + geração de tipos)
|
- Scaffold Next.js + Turbopack configurado com Better Auth e Convex.
|
||||||
- `pnpm dev` — Next.js (App Router)
|
- Portal do cliente entregue com isolamento por `viewerId`.
|
||||||
- `pnpm build` / `pnpm start` — build/produção
|
- 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.
|
||||||
## 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 PT‑BR.
|
|
||||||
- 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 PT‑BR.
|
|
||||||
- [ ] 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 PT‑BR.
|
|
||||||
- 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 PT‑BR (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 PT‑BR; 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: PT‑BR 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 PT‑BR.
|
|
||||||
- [ ] 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 dropdown‑badge (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é (bottom‑center) 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 dropdown‑badge 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.
|
|
||||||
|
|
|
||||||
|
|
@ -43,9 +43,10 @@ export const seedDemo = mutation({
|
||||||
if (found) return found._id;
|
if (found) return found._id;
|
||||||
return await ctx.db.insert("users", { tenantId, name, email, role, avatarUrl: `https://avatar.vercel.sh/${name.split(" ")[0]}` });
|
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 reverId = await ensureUser("Rever", "renan.pac@paulicon.com.br");
|
||||||
const brunoId = await ensureUser("Bruno Lima", "bruno.lima@example.com");
|
const agenteDemoId = await ensureUser("Agente Demo", "agente.demo@sistema.dev");
|
||||||
const eduardaId = await ensureUser("Eduarda Rocha", "eduarda.rocha@example.com", "CUSTOMER");
|
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
|
// Seed a couple of tickets
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
@ -68,7 +69,7 @@ export const seedDemo = mutation({
|
||||||
channel: "EMAIL",
|
channel: "EMAIL",
|
||||||
queueId: queue1,
|
queueId: queue1,
|
||||||
requesterId: eduardaId,
|
requesterId: eduardaId,
|
||||||
assigneeId: anaId,
|
assigneeId: reverId,
|
||||||
createdAt: now - 1000 * 60 * 60 * 5,
|
createdAt: now - 1000 * 60 * 60 * 5,
|
||||||
updatedAt: now - 1000 * 60 * 10,
|
updatedAt: now - 1000 * 60 * 10,
|
||||||
tags: ["portal", "cliente"],
|
tags: ["portal", "cliente"],
|
||||||
|
|
@ -84,8 +85,8 @@ export const seedDemo = mutation({
|
||||||
priority: "HIGH",
|
priority: "HIGH",
|
||||||
channel: "WHATSAPP",
|
channel: "WHATSAPP",
|
||||||
queueId: queue2,
|
queueId: queue2,
|
||||||
requesterId: eduardaId,
|
requesterId: clienteDemoId,
|
||||||
assigneeId: brunoId,
|
assigneeId: agenteDemoId,
|
||||||
createdAt: now - 1000 * 60 * 60 * 8,
|
createdAt: now - 1000 * 60 * 60 * 8,
|
||||||
updatedAt: now - 1000 * 60 * 30,
|
updatedAt: now - 1000 * 60 * 30,
|
||||||
tags: ["Integração", "erp"],
|
tags: ["Integração", "erp"],
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@
|
||||||
"lucide-react": "^0.544.0",
|
"lucide-react": "^0.544.0",
|
||||||
"next": "15.5.4",
|
"next": "15.5.4",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
|
"postcss": "^8.5.6",
|
||||||
"react": "19.2.0",
|
"react": "19.2.0",
|
||||||
"react-dom": "19.2.0",
|
"react-dom": "19.2.0",
|
||||||
"react-hook-form": "^7.64.0",
|
"react-hook-form": "^7.64.0",
|
||||||
|
|
|
||||||
3
web/pnpm-lock.yaml
generated
3
web/pnpm-lock.yaml
generated
|
|
@ -113,6 +113,9 @@ importers:
|
||||||
next-themes:
|
next-themes:
|
||||||
specifier: ^0.4.6
|
specifier: ^0.4.6
|
||||||
version: 0.4.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
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:
|
react:
|
||||||
specifier: 19.2.0
|
specifier: 19.2.0
|
||||||
version: 19.2.0
|
version: 19.2.0
|
||||||
|
|
|
||||||
87
web/scripts/reassign-legacy-assignees.mjs
Normal file
87
web/scripts/reassign-legacy-assignees.mjs
Normal 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
|
||||||
|
})
|
||||||
|
|
@ -4,13 +4,43 @@ import { hashPassword } from "better-auth/crypto"
|
||||||
const { PrismaClient } = pkg
|
const { PrismaClient } = pkg
|
||||||
const prisma = new PrismaClient()
|
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"
|
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 hashedPassword = await hashPassword(password)
|
||||||
|
|
||||||
const user = await prisma.authUser.upsert({
|
const user = await prisma.authUser.upsert({
|
||||||
|
|
@ -18,13 +48,13 @@ async function main() {
|
||||||
update: {
|
update: {
|
||||||
name,
|
name,
|
||||||
role,
|
role,
|
||||||
tenantId,
|
tenantId: userTenant,
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
email,
|
email,
|
||||||
name,
|
name,
|
||||||
role,
|
role,
|
||||||
tenantId,
|
tenantId: userTenant,
|
||||||
accounts: {
|
accounts: {
|
||||||
create: {
|
create: {
|
||||||
providerId: "credential",
|
providerId: "credential",
|
||||||
|
|
@ -82,6 +112,12 @@ async function main() {
|
||||||
console.log(` Senha provisoria: ${password}`)
|
console.log(` Senha provisoria: ${password}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
for (const user of defaultUsers) {
|
||||||
|
await upsertAuthUser(user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
main()
|
main()
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error("Erro ao criar usuario seed", error)
|
console.error("Erro ao criar usuario seed", error)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { AppShell } from "@/components/app-shell"
|
||||||
import { SiteHeader } from "@/components/site-header"
|
import { SiteHeader } from "@/components/site-header"
|
||||||
import { TicketDetailView } from "@/components/tickets/ticket-detail-view"
|
import { TicketDetailView } from "@/components/tickets/ticket-detail-view"
|
||||||
import { TicketDetailStatic } from "@/components/tickets/ticket-detail-static"
|
import { TicketDetailStatic } from "@/components/tickets/ticket-detail-static"
|
||||||
|
import { NewTicketDialog } from "@/components/tickets/new-ticket-dialog"
|
||||||
import { getTicketById } from "@/lib/mocks/tickets"
|
import { getTicketById } from "@/lib/mocks/tickets"
|
||||||
import type { TicketWithDetails } from "@/lib/schemas/ticket"
|
import type { TicketWithDetails } from "@/lib/schemas/ticket"
|
||||||
|
|
||||||
|
|
@ -21,7 +22,7 @@ export default async function TicketDetailPage({ params }: TicketDetailPageProps
|
||||||
title={`Ticket #${id}`}
|
title={`Ticket #${id}`}
|
||||||
lead={"Detalhes do ticket"}
|
lead={"Detalhes do ticket"}
|
||||||
secondaryAction={<SiteHeader.SecondaryButton>Compartilhar</SiteHeader.SecondaryButton>}
|
secondaryAction={<SiteHeader.SecondaryButton>Compartilhar</SiteHeader.SecondaryButton>}
|
||||||
primaryAction={<SiteHeader.PrimaryButton>Adicionar comentário</SiteHeader.PrimaryButton>}
|
primaryAction={<NewTicketDialog />}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
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 { Spinner } from "@/components/ui/spinner"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
@ -59,6 +59,7 @@ export default function NewTicketPage() {
|
||||||
const [subcategoryId, setSubcategoryId] = useState<string | null>(null)
|
const [subcategoryId, setSubcategoryId] = useState<string | null>(null)
|
||||||
const [categoryError, setCategoryError] = useState<string | null>(null)
|
const [categoryError, setCategoryError] = useState<string | null>(null)
|
||||||
const [subcategoryError, setSubcategoryError] = useState<string | null>(null)
|
const [subcategoryError, setSubcategoryError] = useState<string | null>(null)
|
||||||
|
const [descriptionError, setDescriptionError] = useState<string | null>(null)
|
||||||
const [assigneeInitialized, setAssigneeInitialized] = useState(false)
|
const [assigneeInitialized, setAssigneeInitialized] = useState(false)
|
||||||
|
|
||||||
const queueOptions = useMemo(() => queues.map((q) => q.name), [queues])
|
const queueOptions = useMemo(() => queues.map((q) => q.name), [queues])
|
||||||
|
|
@ -91,6 +92,14 @@ export default function NewTicketPage() {
|
||||||
return
|
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)
|
setLoading(true)
|
||||||
toast.loading("Criando ticket...", { id: "create-ticket" })
|
toast.loading("Criando ticket...", { id: "create-ticket" })
|
||||||
try {
|
try {
|
||||||
|
|
@ -110,13 +119,12 @@ export default function NewTicketPage() {
|
||||||
categoryId: categoryId as Id<"ticketCategories">,
|
categoryId: categoryId as Id<"ticketCategories">,
|
||||||
subcategoryId: subcategoryId as Id<"ticketSubcategories">,
|
subcategoryId: subcategoryId as Id<"ticketSubcategories">,
|
||||||
})
|
})
|
||||||
const plainDescription = description.replace(/<[^>]*>/g, "").trim()
|
|
||||||
if (plainDescription.length > 0) {
|
if (plainDescription.length > 0) {
|
||||||
await addComment({
|
await addComment({
|
||||||
ticketId: id as Id<"tickets">,
|
ticketId: id as Id<"tickets">,
|
||||||
authorId: convexUserId as Id<"users">,
|
authorId: convexUserId as Id<"users">,
|
||||||
visibility: "PUBLIC",
|
visibility: "PUBLIC",
|
||||||
body: description,
|
body: sanitizedDescription,
|
||||||
attachments: [],
|
attachments: [],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -143,8 +151,8 @@ export default function NewTicketPage() {
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<form onSubmit={submit} className="space-y-6">
|
<form onSubmit={submit} className="space-y-6">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium text-neutral-700" htmlFor="subject">
|
<label className="flex items-center gap-1 text-sm font-medium text-neutral-700" htmlFor="subject">
|
||||||
Assunto
|
Assunto <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
id="subject"
|
id="subject"
|
||||||
|
|
@ -171,8 +179,23 @@ export default function NewTicketPage() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium text-neutral-700">Descrição</label>
|
<label className="flex items-center gap-1 text-sm font-medium text-neutral-700">
|
||||||
<RichTextEditor value={description} onChange={setDescription} placeholder="Detalhe o problema, passos para reproduzir, links, etc." />
|
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>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<CategorySelectFields
|
<CategorySelectFields
|
||||||
|
|
@ -187,6 +210,8 @@ export default function NewTicketPage() {
|
||||||
setSubcategoryId(value)
|
setSubcategoryId(value)
|
||||||
setSubcategoryError(null)
|
setSubcategoryError(null)
|
||||||
}}
|
}}
|
||||||
|
categoryLabel="Categoria primária *"
|
||||||
|
subcategoryLabel="Categoria secundária *"
|
||||||
/>
|
/>
|
||||||
{categoryError || subcategoryError ? (
|
{categoryError || subcategoryError ? (
|
||||||
<div className="text-xs font-medium text-red-500">
|
<div className="text-xs font-medium text-red-500">
|
||||||
|
|
|
||||||
|
|
@ -52,8 +52,8 @@ export function PortalTicketForm() {
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
|
||||||
const isFormValid = useMemo(() => {
|
const isFormValid = useMemo(() => {
|
||||||
return Boolean(subject.trim() && categoryId && subcategoryId)
|
return Boolean(subject.trim() && description.trim() && categoryId && subcategoryId)
|
||||||
}, [subject, categoryId, subcategoryId])
|
}, [subject, description, categoryId, subcategoryId])
|
||||||
|
|
||||||
async function handleSubmit(event: React.FormEvent) {
|
async function handleSubmit(event: React.FormEvent) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
@ -61,6 +61,7 @@ export function PortalTicketForm() {
|
||||||
|
|
||||||
const trimmedSubject = subject.trim()
|
const trimmedSubject = subject.trim()
|
||||||
const trimmedSummary = summary.trim()
|
const trimmedSummary = summary.trim()
|
||||||
|
const trimmedDescription = description.trim()
|
||||||
|
|
||||||
setIsSubmitting(true)
|
setIsSubmitting(true)
|
||||||
toast.loading("Abrindo chamado...", { id: "portal-new-ticket" })
|
toast.loading("Abrindo chamado...", { id: "portal-new-ticket" })
|
||||||
|
|
@ -78,8 +79,8 @@ export function PortalTicketForm() {
|
||||||
subcategoryId: subcategoryId as Id<"ticketSubcategories">,
|
subcategoryId: subcategoryId as Id<"ticketSubcategories">,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (description.trim().length > 0) {
|
if (trimmedDescription.length > 0) {
|
||||||
const htmlBody = sanitizeEditorHtml(toHtml(description.trim()))
|
const htmlBody = sanitizeEditorHtml(toHtml(trimmedDescription))
|
||||||
await addComment({
|
await addComment({
|
||||||
ticketId: id as Id<"tickets">,
|
ticketId: id as Id<"tickets">,
|
||||||
authorId: convexUserId as Id<"users">,
|
authorId: convexUserId as Id<"users">,
|
||||||
|
|
@ -108,8 +109,8 @@ export function PortalTicketForm() {
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<label htmlFor="subject" className="text-sm font-medium text-neutral-800">
|
<label htmlFor="subject" className="flex items-center gap-1 text-sm font-medium text-neutral-800">
|
||||||
Assunto
|
Assunto <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
id="subject"
|
id="subject"
|
||||||
|
|
@ -131,14 +132,15 @@ export function PortalTicketForm() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<label htmlFor="description" className="text-sm font-medium text-neutral-800">
|
<label htmlFor="description" className="flex items-center gap-1 text-sm font-medium text-neutral-800">
|
||||||
Detalhes
|
Detalhes <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<Textarea
|
<Textarea
|
||||||
id="description"
|
id="description"
|
||||||
value={description}
|
value={description}
|
||||||
onChange={(event) => setDescription(event.target.value)}
|
onChange={(event) => setDescription(event.target.value)}
|
||||||
placeholder="Compartilhe passos para reproduzir, mensagens de erro ou informações adicionais."
|
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"
|
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>
|
</div>
|
||||||
|
|
@ -168,8 +170,8 @@ export function PortalTicketForm() {
|
||||||
onCategoryChange={setCategoryId}
|
onCategoryChange={setCategoryId}
|
||||||
onSubcategoryChange={setSubcategoryId}
|
onSubcategoryChange={setSubcategoryId}
|
||||||
layout="stacked"
|
layout="stacked"
|
||||||
categoryLabel="Categoria"
|
categoryLabel="Categoria *"
|
||||||
subcategoryLabel="Subcategoria"
|
subcategoryLabel="Subcategoria *"
|
||||||
secondaryEmptyLabel="Selecione uma categoria"
|
secondaryEmptyLabel="Selecione uma categoria"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { Spinner } from "@/components/ui/spinner"
|
import { Spinner } from "@/components/ui/spinner"
|
||||||
import { Dropzone } from "@/components/ui/dropzone"
|
import { Dropzone } from "@/components/ui/dropzone"
|
||||||
import { RichTextEditor } from "@/components/ui/rich-text-editor"
|
import { RichTextEditor, sanitizeEditorHtml } from "@/components/ui/rich-text-editor"
|
||||||
import {
|
import {
|
||||||
PriorityIcon,
|
PriorityIcon,
|
||||||
priorityStyles,
|
priorityStyles,
|
||||||
|
|
@ -27,9 +27,9 @@ import {
|
||||||
import { CategorySelectFields } from "@/components/tickets/category-select"
|
import { CategorySelectFields } from "@/components/tickets/category-select"
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
subject: z.string().optional(),
|
subject: z.string().default(""),
|
||||||
summary: z.string().optional(),
|
summary: z.string().optional(),
|
||||||
description: z.string().optional(),
|
description: z.string().default(""),
|
||||||
priority: z.enum(["LOW", "MEDIUM", "HIGH", "URGENT"]).default("MEDIUM"),
|
priority: z.enum(["LOW", "MEDIUM", "HIGH", "URGENT"]).default("MEDIUM"),
|
||||||
channel: z.enum(["EMAIL", "WHATSAPP", "CHAT", "PHONE", "API", "MANUAL"]).default("MANUAL"),
|
channel: z.enum(["EMAIL", "WHATSAPP", "CHAT", "PHONE", "API", "MANUAL"]).default("MANUAL"),
|
||||||
queueName: z.string().nullable().optional(),
|
queueName: z.string().nullable().optional(),
|
||||||
|
|
@ -124,11 +124,20 @@ export function NewTicketDialog() {
|
||||||
|
|
||||||
async function submit(values: z.infer<typeof schema>) {
|
async function submit(values: z.infer<typeof schema>) {
|
||||||
if (!convexUserId) return
|
if (!convexUserId) return
|
||||||
|
|
||||||
const subjectTrimmed = (values.subject ?? "").trim()
|
const subjectTrimmed = (values.subject ?? "").trim()
|
||||||
if (subjectTrimmed.length < 3) {
|
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
|
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)
|
setLoading(true)
|
||||||
toast.loading("Criando ticket…", { id: "new-ticket" })
|
toast.loading("Criando ticket…", { id: "new-ticket" })
|
||||||
try {
|
try {
|
||||||
|
|
@ -138,7 +147,7 @@ export function NewTicketDialog() {
|
||||||
actorId: convexUserId as Id<"users">,
|
actorId: convexUserId as Id<"users">,
|
||||||
tenantId: DEFAULT_TENANT_ID,
|
tenantId: DEFAULT_TENANT_ID,
|
||||||
subject: subjectTrimmed,
|
subject: subjectTrimmed,
|
||||||
summary: values.summary,
|
summary: values.summary?.trim() || undefined,
|
||||||
priority: values.priority,
|
priority: values.priority,
|
||||||
channel: values.channel,
|
channel: values.channel,
|
||||||
queueId: sel?.id as Id<"queues"> | undefined,
|
queueId: sel?.id as Id<"queues"> | undefined,
|
||||||
|
|
@ -147,8 +156,8 @@ export function NewTicketDialog() {
|
||||||
categoryId: values.categoryId as Id<"ticketCategories">,
|
categoryId: values.categoryId as Id<"ticketCategories">,
|
||||||
subcategoryId: values.subcategoryId as Id<"ticketSubcategories">,
|
subcategoryId: values.subcategoryId as Id<"ticketSubcategories">,
|
||||||
})
|
})
|
||||||
const hasDescription = (values.description ?? "").replace(/<[^>]*>/g, "").trim().length > 0
|
const summaryFallback = values.summary?.trim() ?? ""
|
||||||
const bodyHtml = hasDescription ? (values.description as string) : (values.summary || "")
|
const bodyHtml = plainDescription.length > 0 ? sanitizedDescription : summaryFallback
|
||||||
if (attachments.length > 0 || bodyHtml.trim().length > 0) {
|
if (attachments.length > 0 || bodyHtml.trim().length > 0) {
|
||||||
const typedAttachments = attachments.map((a) => ({
|
const typedAttachments = attachments.map((a) => ({
|
||||||
storageId: a.storageId as unknown as Id<"_storage">,
|
storageId: a.storageId as unknown as Id<"_storage">,
|
||||||
|
|
@ -160,7 +169,18 @@ export function NewTicketDialog() {
|
||||||
}
|
}
|
||||||
toast.success("Ticket criado!", { id: "new-ticket" })
|
toast.success("Ticket criado!", { id: "new-ticket" })
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
form.reset()
|
form.reset({
|
||||||
|
subject: "",
|
||||||
|
summary: "",
|
||||||
|
description: "",
|
||||||
|
priority: "MEDIUM",
|
||||||
|
channel: "MANUAL",
|
||||||
|
queueName: null,
|
||||||
|
assigneeId: convexUserId ?? null,
|
||||||
|
categoryId: "",
|
||||||
|
subcategoryId: "",
|
||||||
|
})
|
||||||
|
form.clearErrors()
|
||||||
setAssigneeInitialized(false)
|
setAssigneeInitialized(false)
|
||||||
setAttachments([])
|
setAttachments([])
|
||||||
// Navegar para o ticket recém-criado
|
// 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)]">
|
<FieldGroup className="lg:grid-cols-[minmax(0,1.2fr)_minmax(0,0.8fr)]">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Field>
|
<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" />
|
<Input id="subject" {...form.register("subject")} placeholder="Ex.: Erro 500 no portal" />
|
||||||
<FieldError errors={form.formState.errors.subject ? [{ message: form.formState.errors.subject.message }] : []} />
|
<FieldError errors={form.formState.errors.subject ? [{ message: form.formState.errors.subject.message }] : []} />
|
||||||
</Field>
|
</Field>
|
||||||
|
|
@ -227,12 +249,27 @@ export function NewTicketDialog() {
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field>
|
<Field>
|
||||||
<FieldLabel>Descrição</FieldLabel>
|
<FieldLabel className="flex items-center gap-1">
|
||||||
|
Descrição <span className="text-destructive">*</span>
|
||||||
|
</FieldLabel>
|
||||||
<RichTextEditor
|
<RichTextEditor
|
||||||
value={form.watch("description") || ""}
|
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."
|
placeholder="Detalhe o problema, passos para reproduzir, links, etc."
|
||||||
/>
|
/>
|
||||||
|
<FieldError
|
||||||
|
errors={
|
||||||
|
form.formState.errors.description
|
||||||
|
? [{ message: form.formState.errors.description.message }]
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field>
|
<Field>
|
||||||
<FieldLabel>Anexos</FieldLabel>
|
<FieldLabel>Anexos</FieldLabel>
|
||||||
|
|
@ -251,8 +288,8 @@ export function NewTicketDialog() {
|
||||||
subcategoryId={subcategoryIdValue || null}
|
subcategoryId={subcategoryIdValue || null}
|
||||||
onCategoryChange={handleCategoryChange}
|
onCategoryChange={handleCategoryChange}
|
||||||
onSubcategoryChange={handleSubcategoryChange}
|
onSubcategoryChange={handleSubcategoryChange}
|
||||||
categoryLabel="Categoria primária"
|
categoryLabel="Categoria primária *"
|
||||||
subcategoryLabel="Categoria secundária"
|
subcategoryLabel="Categoria secundária *"
|
||||||
layout="stacked"
|
layout="stacked"
|
||||||
/>
|
/>
|
||||||
{form.formState.errors.categoryId?.message || form.formState.errors.subcategoryId?.message ? (
|
{form.formState.errors.categoryId?.message || form.formState.errors.subcategoryId?.message ? (
|
||||||
|
|
|
||||||
|
|
@ -17,18 +17,11 @@ const tenantId = "tenant-atlas"
|
||||||
type UserRecord = z.infer<typeof ticketSchema>["requester"]
|
type UserRecord = z.infer<typeof ticketSchema>["requester"]
|
||||||
|
|
||||||
const users: Record<string, UserRecord> = {
|
const users: Record<string, UserRecord> = {
|
||||||
ana: {
|
rever: {
|
||||||
id: "user-ana",
|
id: "user-rever",
|
||||||
name: "Ana Souza",
|
name: "Rever",
|
||||||
email: "ana.souza@example.com",
|
email: "renan.pac@paulicon.com.br",
|
||||||
avatarUrl: "https://avatar.vercel.sh/ana",
|
avatarUrl: "https://avatar.vercel.sh/rever",
|
||||||
teams: ["Chamados"],
|
|
||||||
},
|
|
||||||
bruno: {
|
|
||||||
id: "user-bruno",
|
|
||||||
name: "Bruno Lima",
|
|
||||||
email: "bruno.lima@example.com",
|
|
||||||
avatarUrl: "https://avatar.vercel.sh/bruno",
|
|
||||||
teams: ["Chamados"],
|
teams: ["Chamados"],
|
||||||
},
|
},
|
||||||
carla: {
|
carla: {
|
||||||
|
|
@ -72,7 +65,7 @@ const baseTickets = [
|
||||||
channel: ticketChannelSchema.enum.EMAIL,
|
channel: ticketChannelSchema.enum.EMAIL,
|
||||||
queue: "Chamados",
|
queue: "Chamados",
|
||||||
requester: users.eduarda,
|
requester: users.eduarda,
|
||||||
assignee: users.ana,
|
assignee: users.rever,
|
||||||
slaPolicy: {
|
slaPolicy: {
|
||||||
id: "sla-critical",
|
id: "sla-critical",
|
||||||
name: "SLA Crítico",
|
name: "SLA Crítico",
|
||||||
|
|
@ -85,7 +78,7 @@ const baseTickets = [
|
||||||
updatedAt: subMinutes(new Date(), 10),
|
updatedAt: subMinutes(new Date(), 10),
|
||||||
createdAt: subHours(new Date(), 5),
|
createdAt: subHours(new Date(), 5),
|
||||||
tags: ["portal", "cliente"],
|
tags: ["portal", "cliente"],
|
||||||
lastTimelineEntry: "Prioridade atualizada para URGENT por Bruno",
|
lastTimelineEntry: "Prioridade atualizada para URGENT por Rever",
|
||||||
metrics: {
|
metrics: {
|
||||||
timeWaitingMinutes: 12,
|
timeWaitingMinutes: 12,
|
||||||
timeOpenedMinutes: 300,
|
timeOpenedMinutes: 300,
|
||||||
|
|
@ -183,7 +176,7 @@ const baseTickets = [
|
||||||
channel: ticketChannelSchema.enum.EMAIL,
|
channel: ticketChannelSchema.enum.EMAIL,
|
||||||
queue: "Chamados",
|
queue: "Chamados",
|
||||||
requester: users.eduarda,
|
requester: users.eduarda,
|
||||||
assignee: users.bruno,
|
assignee: users.rever,
|
||||||
slaPolicy: {
|
slaPolicy: {
|
||||||
id: "sla-standard",
|
id: "sla-standard",
|
||||||
name: "SLA Padrão",
|
name: "SLA Padrão",
|
||||||
|
|
@ -210,7 +203,7 @@ const commentsByTicket: Record<string, z.infer<typeof ticketWithDetailsSchema>["
|
||||||
"ticket-1001": [
|
"ticket-1001": [
|
||||||
{
|
{
|
||||||
id: "comment-1",
|
id: "comment-1",
|
||||||
author: users.ana,
|
author: users.rever,
|
||||||
visibility: commentVisibilitySchema.enum.INTERNAL,
|
visibility: commentVisibilitySchema.enum.INTERNAL,
|
||||||
body: "Logs coletados e enviados para o time de infraestrutura.",
|
body: "Logs coletados e enviados para o time de infraestrutura.",
|
||||||
attachments: [],
|
attachments: [],
|
||||||
|
|
@ -219,7 +212,7 @@ const commentsByTicket: Record<string, z.infer<typeof ticketWithDetailsSchema>["
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "comment-2",
|
id: "comment-2",
|
||||||
author: users.bruno,
|
author: users.rever,
|
||||||
visibility: commentVisibilitySchema.enum.PUBLIC,
|
visibility: commentVisibilitySchema.enum.PUBLIC,
|
||||||
body: "Estamos investigando o incidente, retorno em 30 minutos.",
|
body: "Estamos investigando o incidente, retorno em 30 minutos.",
|
||||||
attachments: [],
|
attachments: [],
|
||||||
|
|
@ -251,13 +244,13 @@ const timelineByTicket: Record<string, z.infer<typeof ticketWithDetailsSchema>["
|
||||||
{
|
{
|
||||||
id: "timeline-2",
|
id: "timeline-2",
|
||||||
type: "ASSIGNEE_CHANGED",
|
type: "ASSIGNEE_CHANGED",
|
||||||
payload: { assignee: users.ana.name },
|
payload: { assignee: users.rever.name },
|
||||||
createdAt: subHours(new Date(), 4),
|
createdAt: subHours(new Date(), 4),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "timeline-3",
|
id: "timeline-3",
|
||||||
type: "COMMENT_ADDED",
|
type: "COMMENT_ADDED",
|
||||||
payload: { author: users.ana.name },
|
payload: { author: users.rever.name },
|
||||||
createdAt: subHours(new Date(), 1),
|
createdAt: subHours(new Date(), 1),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue