From c2acd65764274f584011469b4e4b9dee3732a6f6 Mon Sep 17 00:00:00 2001 From: Esdras Renan Date: Tue, 4 Nov 2025 19:53:54 -0300 Subject: [PATCH] refine queue metrics and devices ui --- convex/crons.ts | 9 -- convex/queues.ts | 41 +++++--- docs/requests-status.md | 35 +++++++ .../admin/devices/admin-devices-overview.tsx | 55 ++++++----- src/components/app-sidebar.tsx | 3 +- src/components/reports/company-report.tsx | 97 ++++++++++--------- src/components/tickets/new-ticket-dialog.tsx | 2 +- .../tickets/play-next-ticket-card.tsx | 15 ++- src/components/tickets/ticket-csat-card.tsx | 4 - .../tickets/ticket-queue-summary.tsx | 21 ++-- src/lib/schemas/ticket.ts | 15 +-- 11 files changed, 181 insertions(+), 116 deletions(-) create mode 100644 docs/requests-status.md diff --git a/convex/crons.ts b/convex/crons.ts index 7279f87..74c7fb2 100644 --- a/convex/crons.ts +++ b/convex/crons.ts @@ -1,14 +1,5 @@ import { cronJobs } from "convex/server" -import { api } from "./_generated/api" const crons = cronJobs() -// Check hourly and the action will gate by America/Sao_Paulo hour -crons.interval( - "hours-usage-alerts-hourly", - { hours: 1 }, - api.alerts_actions.sendHoursUsageAlerts, - {} -) - export default crons diff --git a/convex/queues.ts b/convex/queues.ts index 4705d3c..985f493 100644 --- a/convex/queues.ts +++ b/convex/queues.ts @@ -113,20 +113,39 @@ export const summary = query({ const queues = await ctx.db.query("queues").withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)).collect(); const result = await Promise.all( queues.map(async (qItem) => { - const pending = await ctx.db + const tickets = await ctx.db .query("tickets") .withIndex("by_tenant_queue", (q) => q.eq("tenantId", tenantId).eq("queueId", qItem._id)) .collect(); - const waiting = pending.filter((t) => { - const status = normalizeStatus(t.status); - return status === "PENDING" || status === "PAUSED"; - }).length; - const open = pending.filter((t) => { - const status = normalizeStatus(t.status); - return status !== "RESOLVED"; - }).length; - const breached = 0; - return { id: qItem._id, name: renameQueueString(qItem.name), pending: open, waiting, breached }; + let pending = 0; + let inProgress = 0; + let paused = 0; + let breached = 0; + const now = Date.now(); + for (const ticket of tickets) { + const status = normalizeStatus(ticket.status); + if (status === "PENDING") { + pending += 1; + } else if (status === "AWAITING_ATTENDANCE") { + inProgress += 1; + } else if (status === "PAUSED") { + paused += 1; + } + if (status !== "RESOLVED") { + const dueAt = typeof ticket.dueAt === "number" ? ticket.dueAt : null; + if (dueAt && dueAt < now) { + breached += 1; + } + } + } + return { + id: qItem._id, + name: renameQueueString(qItem.name), + pending, + inProgress, + paused, + breached, + }; }) ); return result; diff --git a/docs/requests-status.md b/docs/requests-status.md new file mode 100644 index 0000000..fbb2132 --- /dev/null +++ b/docs/requests-status.md @@ -0,0 +1,35 @@ +# Solicitações mapeadas + +## Tickets e fluxo +- [feito] Revisão dos status para Pendente / Em andamento / Pausado / Resolvido em listagens e componente de fila. +- [feito] Botões de play interno/externo com rastreio de horas. +- [feito] Pausa com motivo obrigatório registrado na timeline. +- [feito] Encerramento manual com diálogo atualizado e templates. +- [pendente] Encerramento automático após 3 tentativas de contato (não implementado). + +## Comentários e notificações +- [feito] Comentários internos como padrão para equipe; públicos visíveis ao cliente. +- [feito] Exportação do histórico completo do ticket em PDF. +- [feito] E-mails automáticos para comentários públicos e encerramento. +- [não aplicável] Resumo com IA (feature solicitada, ainda não iniciada). + +## Relatórios e contratos +- [feito] Relatório de horas internas x externas por cliente com exportação XLSX. +- [feito] Campo "Cliente avulso" e horas contratadas no cadastro de empresas, com alertas configuráveis. +- [removido] Alertas automáticos por e-mail de uso de horas (cron desativado a pedido). + +## Inventário e dispositivos +- [feito] Unificação de máquinas em “Dispositivos”, suporte a desktops e celulares. +- [feito] Campos personalizados e templates de exportação no inventário. +- [aguardando uso] Tag de serviço pode ser adicionada como campo personalizado conforme necessidade. + +## Perfis e acesso +- [feito] Perfis Admin/Agente/Gestor/Usuário com restrições de visualização por empresa. +- [feito] Motivo da troca de responsável registrado como comentário interno. +- [feito] Templates de comentário e encerramento gerenciáveis na área de configurações. +- [feito] Ajuste de horas manual com motivo e log. + +## Itens adicionais +- [pendente] Dashboard personalizado para gestores (fora do escopo atual). +- [pendente] Integração com IA/assistente para resumo automático. +- [pendente] Gatilho automático para encerramento por falta de contato (ver item em Tickets e fluxo). diff --git a/src/components/admin/devices/admin-devices-overview.tsx b/src/components/admin/devices/admin-devices-overview.tsx index d918bee..e3a6696 100644 --- a/src/components/admin/devices/admin-devices-overview.tsx +++ b/src/components/admin/devices/admin-devices-overview.tsx @@ -3520,13 +3520,13 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {

{device.authEmail ?? "E-mail não definido"} {device.authEmail ? ( - ) : null} @@ -3541,12 +3541,17 @@ export function DeviceDetails({ device }: DeviceDetailsProps) { {/* Campos personalizados (posicionado logo após métricas) */}

-
-
-

Campos personalizados

- - {(device.customFields ?? []).length} - +
+
+
+

Campos personalizados

+ + {(device.customFields ?? []).length} + +
+ {(!device.customFields || device.customFields.length === 0) ? ( +

Nenhum campo personalizado definido para este dispositivo.

+ ) : null}
))}
- ) : ( -

Nenhum campo personalizado definido para este dispositivo.

- )} + ) : null}
@@ -3587,18 +3590,10 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {

)}
-
+
{totalOpenTickets}
- {deviceTicketsHref ? ( - - Ver todos - - ) : null}
{totalOpenTickets > 0 ? ( @@ -3630,6 +3625,16 @@ export function DeviceDetails({ device }: DeviceDetailsProps) { })}
) : null} + {deviceTicketsHref ? ( +
+ + Ver todos + +
+ ) : null}
diff --git a/src/components/app-sidebar.tsx b/src/components/app-sidebar.tsx index fff69d5..d462728 100644 --- a/src/components/app-sidebar.tsx +++ b/src/components/app-sidebar.tsx @@ -11,6 +11,7 @@ import { PanelsTopLeft, UserCog, Building2, + Skyscraper, Waypoints, Clock4, Timer, @@ -105,7 +106,7 @@ const navigation: NavigationGroup[] = [ { title: "Empresas", url: "/admin/companies", - icon: Building2, + icon: Skyscraper, requiredRole: "admin", }, { title: "Usuários", url: "/admin/users", icon: Users, requiredRole: "admin" }, diff --git a/src/components/reports/company-report.tsx b/src/components/reports/company-report.tsx index b19d345..6480e51 100644 --- a/src/components/reports/company-report.tsx +++ b/src/components/reports/company-report.tsx @@ -1,6 +1,7 @@ "use client" -import { useEffect, useState } from "react" +import { useEffect, useMemo, useState } from "react" +import Link from "next/link" import { useQuery } from "convex/react" import { api } from "@/convex/_generated/api" import type { Id } from "@/convex/_generated/dataModel" @@ -8,14 +9,7 @@ import { DEFAULT_TENANT_ID } from "@/lib/constants" import { useAuth } from "@/lib/auth-client" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Skeleton } from "@/components/ui/skeleton" -import { Badge } from "@/components/ui/badge" -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { ChartConfig, ChartContainer, @@ -25,6 +19,12 @@ import { ChartTooltipContent, } from "@/components/ui/chart" import { Area, AreaChart, CartesianGrid, XAxis, Bar, BarChart, Pie, PieChart } from "recharts" +import { SearchableCombobox, type SearchableComboboxOption } from "@/components/ui/searchable-combobox" +import { TicketPriorityPill } from "@/components/tickets/priority-pill" +import { TicketStatusBadge } from "@/components/tickets/status-badge" +import { formatDistanceToNow } from "date-fns" +import { ptBR } from "date-fns/locale" +import type { TicketPriority, TicketStatus } from "@/lib/schemas/ticket" type CompanyRecord = { id: Id<"companies">; name: string } @@ -67,11 +67,20 @@ export function CompanyReport() { isStaff && convexUserId ? { tenantId, viewerId: convexUserId as Id<"users"> } : "skip" ) as CompanyRecord[] | undefined + const companyOptions = useMemo( + () => + (companies ?? []).map((company) => ({ + value: company.id as string, + label: company.name, + })), + [companies] + ) + useEffect(() => { - if (!selectedCompany && companies && companies.length > 0) { - setSelectedCompany(companies[0].id) + if (!selectedCompany && companyOptions.length > 0) { + setSelectedCompany(companyOptions[0]?.value ?? "") } - }, [companies, selectedCompany]) + }, [companyOptions, selectedCompany]) const report = useQuery( api.reports.companyOverview, @@ -142,18 +151,19 @@ export function CompanyReport() {

Acompanhe tickets, inventário e colaboradores de um cliente específico.

- +
+ { + if (value) setSelectedCompany(value) + }} + options={companyOptions} + placeholder="Selecionar empresa" + searchPlaceholder="Buscar empresa..." + emptyText="Nenhuma empresa encontrada." + disabled={!companyOptions.length} + /> +