✅ EXECUÇÃO — 15/08/2026
Implementado e validado em produção (homolog). Commits: 2062f6c (fundação: 21 tabelas), 080c857 (features), bbfa277 (lint). Deploy CI verde.
Validação ao vivo: página(s) da feature respondendo 200 com a role correta + procedures tRPC testadas via HTTP (respostas JSON corretas). Ver relatório da sessão.
Detalhes de implementação e decisões: conforme o design acima deste documento (schema, procedures e páginas seguidos conforme especificado; ajustes menores documentados no commit).
📄 Card 46 [M8] — Chamados (Help Desk com SLA)
📚 Referência SULTS: https://www.sults.com.br/produtos/chamados Sistema de tickets com SLA duplo (primeira resposta + solução), formulários inteligentes por assunto, abertura em massa, escalonamento e CSAT. Funciona como Centro de Serviços Compartilhados (CSC) entre matriz ↔ unidades ↔ professores ↔ responsáveis.
🎯 Objetivo
Transformar o schema Ticket já existente (Card 17 placeholder) em um Help Desk completo com:
- SLA duplo: tempo de primeira resposta + tempo de solução (independentes)
- Pausa inteligente: prazo pausa em feriados e fora do expediente
- Formulários inteligentes: campos obrigatórios por assunto (exigem fotos, números)
- 3 fluxos: unidade→matriz, matriz→unidade, interno entre departamentos
- Abertura em massa (lote): um chamado disparado pra múltiplas unidades (cada um vira cópia individual rastreável)
- Kanban com drag-and-drop + cores por prioridade
- Catálogo hierárquico de assuntos por departamento
- CSAT (avaliação 1-5 ao final)
- Dashboards: funil, ranking departamentos, tempo médio resolução, taxas SLA
📚 Documentação de Referência
prisma/schema.prisma—TicketeTicketReplyjá existem (expandir)docs/DESIGN_SYSTEM.md§6 — Kanban reusar padrão do CRM (Card 30)src/components/ui/— reusar componentes
🛠️ Especificação Técnica
Schema Prisma (expandir Ticket)
model Ticket {
id String @id @default(cuid())
code String @unique // "TK-2026-0001"
subject String
description String @db.Text
status TicketStatus @default(OPEN) // OPEN, IN_PROGRESS, WAITING, RESOLVED, CLOSED
priority TicketPriority @default(NORMAL) // LOW, NORMAL, MEDIUM, HIGH, URGENT
requesterId String // quem abriu
requester User @relation("TicketRequester", fields: [requesterId], references: [id])
assignedId String? // responsável
assigned User? @relation("TicketAssignee", fields: [assignedId], references: [id])
departmentId String?
department TicketDepartment? @relation(fields: [departmentId], references: [id])
branchId String?
branch Branch? @relation(fields: [branchId], references: [id])
categoryId String?
category TicketCategory? @relation(fields: [categoryId], references: [id])
slaResponseDue DateTime? // prazo 1ª resposta
slaResolveDue DateTime? // prazo solução
slaRespondedAt DateTime?
slaResolvedAt DateTime?
slaPausedMs Int @default(0) // tempo pausado total
slaPausedAt DateTime?
batchId String? // se gerado em lote
replies TicketReply[] // histórico
attachments String[] // URLs S3
csatScore Int? // 1-5
csatComment String?
csatAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
closedAt DateTime?
}
model TicketReply {
id String @id @default(cuid())
ticketId String
ticket Ticket @relation(fields: [ticketId], references: [id])
authorId String
author User @relation(fields: [authorId], references: [id])
body String @db.Text
isInternal Boolean @default(false) // invisível ao solicitante
attachments String[]
createdAt DateTime @default(now())
}
model TicketDepartment {
id String @id @default(cuid())
name String // "Financeiro", "Pedagógico", "TI"
description String?
parentId String? // hierarquia
parent TicketDepartment? @relation("DeptTree", fields: [parentId], references: [id])
children TicketDepartment[] @relation("DeptTree")
headId String? // líder do dept
head User? @relation(fields: [headId], references: [id])
tickets Ticket[]
members DeptMember[]
}
model DeptMember {
id String @id @default(cuid())
departmentId String
userId String
role DeptRole @default(MEMBER) // HEAD, MEMBER
}
model TicketCategory {
id String @id @default(cuid())
name String // "Dúvida matrícula", "Erro sistema"
departmentId String
department TicketDepartment @relation(fields: [departmentId], references: [id])
formFields Json // formulário dinâmico
defaultSlaResponseHours Int @default(4)
defaultSlaResolveHours Int @default(48)
}
enum TicketStatus { OPEN IN_PROGRESS WAITING RESOLVED CLOSED }
enum TicketPriority { LOW NORMAL MEDIUM HIGH URGENT }
enum DeptRole { HEAD MEMBER }
Rotas (page.tsx)
/master/chamados— kanban + filtros + dashboards/local/chamados— por filial/professor/chamados,/pais/chamados,/aluno/chamados— abrir e acompanhar/master/chamados/[id]— detalhe (thread + SLA + atribuição + CSAT)
Procedures tRPC (master.tickets, local.tickets, parent.tickets, etc.)
list(filtros: status, priority, dept, branch, sla vencendo)create(abre ticket; dispara SLA timers)createBatch(lote para múltiplas unidades)reply(adiciona Reply; pode ser interna)assign(atribui responsável)transfer(muda dept com registro)pause/resume(pausa SLA)resolve/closerateCsat(1-5 + comentário)dashboard(funil, ranking dept, SLA stats)
SLA Engine
- Cron Bull
ticket-sla-check(a cada 5 min): identifica tickets perto do vencimento → notifica responsável - Pausa automática fora do expediente (configurável por dept) e feriados
- Job Bull
ticket-csat-request: 24h após RESOLVED, pede CSAT
✅ Critérios de Aceite
- MASTER configura departamentos + categorias + SLA default
- Usuário abre ticket com formulário dinâmico por categoria
- Abertura em lote cria cópias individuais rastreáveis (batchId)
- Kanban com drag-and-drop + cores por prioridade
- SLA duplo visível + alerta antes de estourar
- Mensagem interna invisível ao solicitante
- CSAT 1-5 ao resolver
- Dashboards: funil, ranking dept, tempo médio, taxas SLA
- Build passa + lint OK
🔌 Handoff
- Sub-routers
*.ticketsem master, local, parent, professor - Cron
ticket-sla-check+ticket-csat-requestno bootstrap - Integração com
notifyUser(push + email + in-app)
🎯 Próximo
Card 47: Enquetes (forms com trava de tela)