"use client" import { useMemo } from "react" import { useQuery } from "convex/react" import { IconAlertTriangle, IconGraph, IconClockHour4 } from "@tabler/icons-react" import { api } from "@/convex/_generated/api" import type { Id } from "@/convex/_generated/dataModel" import { useAuth } from "@/lib/auth-client" import { DEFAULT_TENANT_ID } from "@/lib/constants" import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Skeleton } from "@/components/ui/skeleton" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { useState } from "react" import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group" import { usePersistentCompanyFilter } from "@/lib/use-company-filter" import { Area, AreaChart, Bar, BarChart, CartesianGrid, XAxis } from "recharts" import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart" import { cn, formatDateDM, formatDateDMY, formatHoursCompact } from "@/lib/utils" import { SearchableCombobox, type SearchableComboboxOption } from "@/components/ui/searchable-combobox" const agentProductivityChartConfig = { resolved: { label: "Chamados resolvidos", }, } const priorityLabelMap: Record = { LOW: "Baixa", MEDIUM: "Média", HIGH: "Alta", URGENT: "Urgente", } type CategoryBreakdownEntry = { categoryId: string | null categoryName: string priority: string total: number responseMet: number solutionMet: number responseRate: number | null solutionRate: number | null } function formatMinutes(value: number | null) { if (value === null) return "—" if (value < 60) return `${value.toFixed(0)} min` const hours = Math.floor(value / 60) const minutes = Math.round(value % 60) if (minutes === 0) return `${hours}h` return `${hours}h ${minutes}min` } export function SlaReport() { const [companyId, setCompanyId] = usePersistentCompanyFilter("all") const [timeRange, setTimeRange] = useState("90d") const { session, convexUserId, isStaff } = useAuth() const tenantId = session?.user.tenantId ?? DEFAULT_TENANT_ID const enabled = Boolean(isStaff && convexUserId) const data = useQuery( api.reports.slaOverview, enabled ? ({ tenantId, viewerId: convexUserId as Id<"users">, range: timeRange, companyId: companyId === "all" ? undefined : (companyId as Id<"companies">) }) : "skip" ) const agents = useQuery( api.reports.agentProductivity, enabled ? ({ tenantId, viewerId: convexUserId as Id<"users">, range: timeRange, companyId: companyId === "all" ? undefined : (companyId as Id<"companies">) }) : "skip" ) as { rangeDays: number; items: Array<{ agentId: string; name: string | null; email: string | null; open: number; resolved: number; avgFirstResponseMinutes: number | null; avgResolutionMinutes: number | null; workedHours: number }> } | undefined const openedResolved = useQuery( api.reports.openedResolvedByDay, enabled ? ({ tenantId, viewerId: convexUserId as Id<"users">, range: timeRange, companyId: companyId === "all" ? undefined : (companyId as Id<"companies">) }) : "skip" ) as { rangeDays: number; series: Array<{ date: string; opened: number; resolved: number }> } | undefined const channelsSeries = useQuery( api.reports.ticketsByChannel, enabled ? ({ tenantId, viewerId: convexUserId as Id<"users">, range: timeRange, companyId: companyId === "all" ? undefined : (companyId as Id<"companies">) }) : "skip" ) as { rangeDays: number; channels: string[]; points: Array<{ date: string; values: Record }> } | undefined const companies = useQuery( api.companies.list, enabled ? { tenantId, viewerId: convexUserId as Id<"users"> } : "skip" ) as Array<{ id: Id<"companies">; name: string }> | undefined const companyOptions = useMemo(() => { const base: SearchableComboboxOption[] = [{ value: "all", label: "Todas as empresas" }] if (!companies || companies.length === 0) { return base } const sorted = [...companies].sort((a, b) => a.name.localeCompare(b.name, "pt-BR")) return [ base[0], ...sorted.map((company) => ({ value: company.id, label: company.name, })), ] }, [companies]) const queueTotal = useMemo( () => data?.queueBreakdown.reduce((acc: number, queue: { open: number }) => acc + queue.open, 0) ?? 0, [data] ) const categoryBreakdown = (data?.categoryBreakdown ?? []) as CategoryBreakdownEntry[] if (!data) { return (
{Array.from({ length: 4 }).map((_, index) => ( ))}
) } return (
Tickets abertos Chamados ativos acompanhados pelo SLA. {data.totals.open} Vencidos Tickets que ultrapassaram o prazo previsto. {data.totals.overdue} Tempo resposta médio Com base nos tickets respondidos. {formatMinutes(data.response.averageFirstResponseMinutes ?? null)}

{data.response.responsesRegistered} registros

Tempo resolução médio Chamados finalizados no período analisado. {formatMinutes(data.resolution.averageResolutionMinutes ?? null)}

{data.resolution.resolvedCount} resolvidos

Fila x Volume aberto Distribuição dos {queueTotal} tickets abertos por fila de atendimento.
setCompanyId(next ?? "all")} options={companyOptions} placeholder="Todas as empresas" className="w-full min-w-56 sm:w-56" /> 90 dias 30 dias 7 dias
{data.queueBreakdown.length === 0 ? (

Nenhuma fila com tickets ativos no momento.

) : (
    {data.queueBreakdown.map((queue: { id: string; name: string; open: number }) => (
  • {queue.name} {((queue.open / Math.max(queueTotal, 1)) * 100).toFixed(0)}% do volume aberto
    {queue.open} tickets
  • ))}
)}
SLA por categoria & prioridade Taxa de cumprimento de resposta/solução considerando as regras configuradas em Categorias → SLA.
{categoryBreakdown.length === 0 ? (

Ainda não há tickets categorizados ou com SLA aplicado para este período.

) : (
{categoryBreakdown.slice(0, 8).map((row) => ( ))}
Categoria Prioridade Tickets SLA resposta SLA solução
{row.categoryName} {priorityLabelMap[row.priority as keyof typeof priorityLabelMap] ?? row.priority} {row.total}
{categoryBreakdown.length > 8 ? (
Mostrando 8 de {categoryBreakdown.length} combinações. Refine o período ou exporte o XLSX para visão completa.
) : null}
)}
Abertos x Resolvidos Comparativo diário no período selecionado.
{!openedResolved || openedResolved.series.length === 0 ? (

Sem dados para o período.

) : ( formatDateDM(new Date(v))} /> formatDateDMY(new Date(value as string))} /> } /> )}
Volume por canal Distribuição diária por canal (empilhado).
{!channelsSeries || channelsSeries.points.length === 0 ? (

Sem dados para o período.

) : ( ({ date: p.date, ...p.values }))}> formatDateDM(new Date(v))} /> formatDateDMY(new Date(value as string))} /> } /> {channelsSeries.channels.map((ch, idx) => ( ))} )}
Produtividade por agente Chamados resolvidos no período por agente (top 10) e horas trabalhadas.
{!agents || agents.items.length === 0 ? (

Nenhum dado para o período selecionado.

) : (

Resolvidos por agente

({ name: a.name || a.email || 'Agente', resolved: a.resolved }))} margin={{ top: 8, left: 20, right: 20, bottom: 56 }} > } />

Horas trabalhadas (estimado)

    {agents.items.slice(0, 10).map((a) => (
  • {a.name || a.email || 'Agente'} {formatHoursCompact(a.workedHours)}
  • ))}
)}
) } function RateBadge({ value, label, colorClass }: { value: number | null; label: string; colorClass: string }) { const percent = value === null ? null : Math.round(value * 100) return (
{label} {percent === null ? "—" : `${percent}%`}
) }