From 2cf399dcb1e46f2d4019210a2a8b813e7b61a15e Mon Sep 17 00:00:00 2001 From: Esdras Renan Date: Tue, 7 Oct 2025 14:18:59 -0300 Subject: [PATCH] feat(filters): ticket company filter + column; reports: company filter in CSVs; dashboard: queue summary; docs: agents.md and roadmap updates --- PROXIMOS_PASSOS.md | 13 +++--- agents.md | 44 ++++++++++++------- convex/reports.ts | 13 +++--- src/app/api/reports/backlog.csv/route.ts | 3 ++ .../reports/tickets-by-channel.csv/route.ts | 4 +- src/app/dashboard/page.tsx | 11 ++++- src/components/tickets/tickets-filters.tsx | 22 +++++++++- src/components/tickets/tickets-table.tsx | 8 ++++ src/components/tickets/tickets-view.tsx | 13 +++++- 9 files changed, 100 insertions(+), 31 deletions(-) diff --git a/PROXIMOS_PASSOS.md b/PROXIMOS_PASSOS.md index 392097a..d89fd94 100644 --- a/PROXIMOS_PASSOS.md +++ b/PROXIMOS_PASSOS.md @@ -28,10 +28,10 @@ Lista priorizada de evoluções propostas para o Sistema de Chamados. Confira `a # 📊 Dashboards e relatórios -- [ ] Criar **dashboard inicial com fila de atendimento** - - [ ] Exibir chamados em: atendimento, laboratório, visitas - - [ ] Indicadores: abertos, resolvidos, tempo médio, SLA -- [ ] Criar **relatório de horas por cliente (CSV/Dashboard)** +- [x] Criar **dashboard inicial com fila de atendimento** + - [x] Exibir chamados em: atendimento, laboratório, visitas + - [x] Indicadores: abertos, resolvidos, tempo médio, SLA +- [x] Criar **relatório de horas por cliente (CSV/Dashboard)** - [x] Separar por atendimento interno e externo - [x] Filtrar por período (dia, semana, mês) - [x] Permitir exportar relatórios completos (CSV ou PDF) @@ -43,10 +43,9 @@ Lista priorizada de evoluções propostas para o Sistema de Chamados. Confira `a - [x] Adicionar botão **Play interno** (atendimento remoto) - [x] Adicionar botão **Play externo** (atendimento presencial) - [x] Separar contagem de horas por tipo (interno/externo) -- [ ] Exibir e somar **horas gastas por cliente** (com base no tipo) - - [x] Relatório com totais (interno/externo/total) +- [x] Exibir e somar **horas gastas por cliente** (com base no tipo) - [ ] Incluir no cadastro: - - [ ] Horas contratadas por mês + - [ ] Horas contratadas por mês (Convex pronto; falta migração Prisma) - [x] Tipo de cliente: mensalista ou avulso - [ ] Enviar alerta automático por e-mail quando atingir limite de horas diff --git a/agents.md b/agents.md index 5dce4b8..f79e5c8 100644 --- a/agents.md +++ b/agents.md @@ -35,12 +35,20 @@ 9. Inicie o backend Convex em um terminal (`pnpm convex:dev`) e, em outro, suba a aplicação Next.js (`pnpm dev`). 10. Acesse `http://localhost:3000` e teste login com os usuários padrão listados acima antes de continuar o desenvolvimento. -## 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. +## 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. +- Relatórios e dashboards utilizam `AppShell`, garantindo header/sidebar consistentes. + +## Entregas recentes +- Exportações CSV (Backlog, Canais, CSAT, SLA e Horas por cliente) com parâmetros de período. +- PDF do ticket (via pdfkit standalone), com espaçamento e traduções PT-BR. +- Play interno/externo com somatório por tipo por ticket e relatório por cliente. +- Admin > Empresas & clientes: cadastro/edição, `Cliente avulso?` e `Horas contratadas/mês`. +- Admin > Usuários: vincular colaborador à empresa. +- Dashboard: cards de filas (Chamados/Laboratório/Visitas) e indicadores principais. +- Lista de tickets: filtro por Empresa, coluna Empresa, alinhamento vertical e melhor espaçamento entre colunas. ## Entregas recentes relevantes - Correção do redirecionamento após logout evitando retorno imediato ao dashboard. @@ -56,16 +64,22 @@ - Abrir novos tickets diretamente a partir do detalhe via dialog reutilizável. - Acessar `/settings` para ajustes pessoais e efetuar logout pelo menu. -### 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. +### Papéis +- Papéis válidos: `admin`, `manager`, `agent`, `collaborator` (papel `customer` removido). +- Gestores veem os tickets da própria empresa e só podem registrar comentários públicos. -## 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. +## Próximos passos sugeridos +1. Disparo de e-mails automáticos quando uso de horas ≥ 90% do contratado. +2. Ações rápidas (status/fila) diretamente na listagem de tickets. +3. Limites e monitoramento para anexos por tenant. +4. PDF do ticket com layout idêntico ao app (logo/cores/fontes). + +## Referências de endpoints úteis +- Backlog CSV: `/api/reports/backlog.csv?range=7d|30d|90d[&companyId=...]` +- Canais CSV: `/api/reports/tickets-by-channel.csv?range=7d|30d|90d[&companyId=...]` +- CSAT CSV: `/api/reports/csat.csv?range=7d|30d|90d` +- SLA CSV: `/api/reports/sla.csv` +- Horas por cliente CSV: `/api/reports/hours-by-client.csv?range=7d|30d|90d` ## Rotina antes de abrir PR - `pnpm lint` diff --git a/convex/reports.ts b/convex/reports.ts index a221615..e111fbe 100644 --- a/convex/reports.ts +++ b/convex/reports.ts @@ -217,10 +217,11 @@ export const csatOverview = query({ }); export const backlogOverview = query({ - args: { tenantId: v.string(), viewerId: v.id("users"), range: v.optional(v.string()) }, - handler: async (ctx, { tenantId, viewerId, range }) => { + args: { tenantId: v.string(), viewerId: v.id("users"), range: v.optional(v.string()), companyId: v.optional(v.id("companies")) }, + handler: async (ctx, { tenantId, viewerId, range, companyId }) => { const viewer = await requireStaff(ctx, viewerId, tenantId); - const tickets = await fetchScopedTickets(ctx, tenantId, viewer); + let tickets = await fetchScopedTickets(ctx, tenantId, viewer); + if (companyId) tickets = tickets.filter((t) => t.companyId === companyId) // Optional range filter (createdAt) for reporting purposes const days = range === "7d" ? 7 : range === "30d" ? 30 : 90; @@ -350,10 +351,12 @@ export const ticketsByChannel = query({ tenantId: v.string(), viewerId: v.id("users"), range: v.optional(v.string()), + companyId: v.optional(v.id("companies")), }, - handler: async (ctx, { tenantId, viewerId, range }) => { + handler: async (ctx, { tenantId, viewerId, range, companyId }) => { const viewer = await requireStaff(ctx, viewerId, tenantId); - const tickets = await fetchScopedTickets(ctx, tenantId, viewer); + let tickets = await fetchScopedTickets(ctx, tenantId, viewer); + if (companyId) tickets = tickets.filter((t) => t.companyId === companyId) const days = range === "7d" ? 7 : range === "30d" ? 30 : 90; const end = new Date(); diff --git a/src/app/api/reports/backlog.csv/route.ts b/src/app/api/reports/backlog.csv/route.ts index addc2db..e8cb99c 100644 --- a/src/app/api/reports/backlog.csv/route.ts +++ b/src/app/api/reports/backlog.csv/route.ts @@ -57,15 +57,18 @@ export async function GET(request: Request) { try { const { searchParams } = new URL(request.url) const range = searchParams.get("range") ?? undefined + const companyId = searchParams.get("companyId") ?? undefined const report = await client.query(api.reports.backlogOverview, { tenantId, viewerId: viewerId as unknown as Id<"users">, range, + companyId: companyId as any, }) const rows: Array> = [] rows.push(["Relatório", "Backlog"]) rows.push(["Período", report.rangeDays ? `Últimos ${report.rangeDays} dias` : "—"]) + if (companyId) rows.push(["EmpresaId", companyId]) rows.push([]) rows.push(["Seção", "Chave", "Valor"]) // header diff --git a/src/app/api/reports/tickets-by-channel.csv/route.ts b/src/app/api/reports/tickets-by-channel.csv/route.ts index de7d958..aef39d3 100644 --- a/src/app/api/reports/tickets-by-channel.csv/route.ts +++ b/src/app/api/reports/tickets-by-channel.csv/route.ts @@ -34,6 +34,7 @@ export async function GET(request: Request) { const { searchParams } = new URL(request.url) const range = searchParams.get("range") ?? undefined // "7d" | "30d" | undefined(=90d) + const companyId = searchParams.get("companyId") ?? undefined const client = new ConvexHttpClient(convexUrl) const tenantId = session.user.tenantId ?? DEFAULT_TENANT_ID @@ -62,6 +63,7 @@ export async function GET(request: Request) { tenantId, viewerId: viewerId as unknown as Id<"users">, range, + companyId: companyId as any, }) const channels = report.channels @@ -91,7 +93,7 @@ export async function GET(request: Request) { return new NextResponse(csv, { headers: { "Content-Type": "text/csv; charset=UTF-8", - "Content-Disposition": `attachment; filename="tickets-by-channel-${tenantId}-${range ?? '90d'}.csv"`, + "Content-Disposition": `attachment; filename="tickets-by-channel-${tenantId}-${range ?? '90d'}${companyId ? `-${companyId}` : ''}.csv"`, "Cache-Control": "no-store", }, }) diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 346d821..49084cf 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -2,6 +2,12 @@ import { AppShell } from "@/components/app-shell" import { SectionCards } from "@/components/section-cards" import { SiteHeader } from "@/components/site-header" import { RecentTicketsPanel } from "@/components/tickets/recent-tickets-panel" +import dynamic from "next/dynamic" + +const TicketQueueSummaryCards = dynamic( + () => import("@/components/tickets/ticket-queue-summary").then((m) => ({ default: m.TicketQueueSummaryCards })), + { ssr: false } +) import { ChartAreaInteractive } from "@/components/chart-area-interactive" export default function Dashboard() { @@ -18,9 +24,12 @@ export default function Dashboard() { >
- +
+
+ +
) } diff --git a/src/components/tickets/tickets-filters.tsx b/src/components/tickets/tickets-filters.tsx index fbe68f6..280c53f 100644 --- a/src/components/tickets/tickets-filters.tsx +++ b/src/components/tickets/tickets-filters.tsx @@ -66,6 +66,7 @@ export type TicketFiltersState = { priority: string | null queue: string | null channel: string | null + company: string | null view: "active" | "completed" } @@ -75,17 +76,19 @@ export const defaultTicketFilters: TicketFiltersState = { priority: null, queue: null, channel: null, + company: null, view: "active", } interface TicketsFiltersProps { onChange?: (filters: TicketFiltersState) => void queues?: QueueOption[] + companies?: string[] } const ALL_VALUE = "ALL" -export function TicketsFilters({ onChange, queues = [] }: TicketsFiltersProps) { +export function TicketsFilters({ onChange, queues = [], companies = [] }: TicketsFiltersProps) { const [filters, setFilters] = useState(defaultTicketFilters) function setPartial(partial: Partial) { @@ -103,6 +106,7 @@ export function TicketsFilters({ onChange, queues = [] }: TicketsFiltersProps) { if (filters.priority) chips.push(`Prioridade: ${filters.priority}`) if (filters.queue) chips.push(`Fila: ${filters.queue}`) if (filters.channel) chips.push(`Canal: ${filters.channel}`) + if (filters.company) chips.push(`Empresa: ${filters.company}`) if (!filters.status && filters.view === "completed") chips.push("Exibindo concluídos") return chips }, [filters]) @@ -133,6 +137,22 @@ export function TicketsFilters({ onChange, queues = [] }: TicketsFiltersProps) { ))} +