"use client" import { type ReactNode } from "react" import Link from "next/link" import { useQuery } from "convex/react" import { IconClockHour4, IconTrendingDown, IconTrendingUp } from "@tabler/icons-react" import { Area, AreaChart, CartesianGrid, XAxis } from "recharts" import { formatDistanceToNow } from "date-fns" import { ptBR } from "date-fns/locale" 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 { cn, formatDateDM, formatDateDMY, formatMinutesHuman } from "@/lib/utils" import { Badge } from "@/components/ui/badge" import { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from "@/components/ui/card" import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart" import { Skeleton } from "@/components/ui/skeleton" type DashboardOverview = { newTickets?: { last24h: number; previous24h: number; trendPercentage: number | null } inProgress?: { current: number; previousSnapshot: number; trendPercentage: number | null } firstResponse?: { averageMinutes: number | null; deltaMinutes: number | null; responsesCount: number } awaitingAction?: { total: number; atRisk: number } resolution?: { resolvedLast7d: number; previousResolved: number; rate: number | null; deltaPercentage: number | null } } type QueueTrendResponse = { rangeDays: number queues: Array<{ id: string name: string openedTotal: number resolvedTotal: number series: Array<{ date: string; opened: number; resolved: number }> }> } const queueSparkConfig = { opened: { label: "Novos", color: "var(--chart-1)" }, resolved: { label: "Resolvidos", color: "var(--chart-2)" }, } const metricBadgeClass = "gap-1 rounded-full border-border/60 px-2.5 py-0.5 text-[11px] sm:px-3 sm:py-1 sm:text-xs" export function DashboardHero() { const { session, convexUserId, isStaff } = useAuth() const tenantId = session?.user.tenantId ?? DEFAULT_TENANT_ID const dashboardEnabled = Boolean(isStaff && convexUserId) const overview = useQuery( api.reports.dashboardOverview, dashboardEnabled ? { tenantId, viewerId: convexUserId as Id<"users"> } : "skip" ) as DashboardOverview | undefined const newTicketsTrend = overview?.newTickets ? { delta: overview.newTickets.trendPercentage, label: overview.newTickets.trendPercentage === null ? "Sem comparativo" : `${overview.newTickets.trendPercentage >= 0 ? "+" : ""}${overview.newTickets.trendPercentage.toFixed(1)}% vs. 24h anteriores`, } : null const inProgressTrend = overview?.inProgress ? { delta: overview.inProgress.trendPercentage, label: overview.inProgress.trendPercentage === null ? "Sem histórico" : `${overview.inProgress.trendPercentage >= 0 ? "+" : ""}${overview.inProgress.trendPercentage.toFixed(1)}% vs. última medição`, } : null const responseDelta = overview?.firstResponse ? (() => { const delta = overview.firstResponse.deltaMinutes if (delta === null) return { delta, label: "Sem comparação" } return { delta, label: `${delta > 0 ? "+" : ""}${delta.toFixed(1)} min` } })() : null const resolutionInfo = overview?.resolution ? { delta: overview.resolution.deltaPercentage ?? null, label: overview.resolution.deltaPercentage === null ? "Sem histórico" : `${overview.resolution.deltaPercentage >= 0 ? "+" : ""}${overview.resolution.deltaPercentage.toFixed(1)}%`, rateLabel: overview.resolution.rate !== null ? `${overview.resolution.rate.toFixed(1)}% dos tickets resolvidos` : "Taxa indisponível", } : { delta: null, label: "Sem histórico", rateLabel: "Taxa indisponível" } const newTicketsFooter = (() => { if (!newTicketsTrend) return "Aguardando dados" if (newTicketsTrend.delta === null) return "Sem comparativo recente" return newTicketsTrend.delta >= 0 ? "Acima das 24h anteriores" : "Abaixo das 24h anteriores" })() const inProgressFooter = (() => { if (!inProgressTrend) return "Monitoramento aguardando histórico" if (inProgressTrend.delta === null) return "Sem histórico recente" return inProgressTrend.delta >= 0 ? "Acima da última medição" : "Abaixo da última medição" })() const responseFooter = (() => { if (!responseDelta) return "Sem dados suficientes para comparação" if (responseDelta.delta === null) return "Sem comparação recente" return responseDelta.delta <= 0 ? "Melhor que a última medição" : "Pior que a última medição" })() const resolutionFooter = resolutionInfo?.rateLabel ?? "Taxa indisponível no momento." return (
Novos tickets (24h) {overview?.newTickets ? overview.newTickets.last24h : } {newTicketsTrend ? ( {newTicketsTrend.delta !== null && newTicketsTrend.delta < 0 ? ( ) : ( )} {newTicketsTrend.label} ) : null} {newTicketsFooter} Base: entradas registradas nas últimas 24h. Em atendimento {overview?.inProgress ? overview.inProgress.current : } {inProgressTrend ? ( 0 ? "text-amber-600" : "text-emerald-600" )} > {inProgressTrend.delta !== null && inProgressTrend.delta > 0 ? ( ) : ( )} {inProgressTrend.label} ) : null} {inProgressFooter} Base: tickets ativos em SLA. Tempo médio (1ª resposta) {overview?.firstResponse ? ( overview.firstResponse.averageMinutes !== null ? ( formatMinutesHuman(overview.firstResponse.averageMinutes) ) : ( "—" ) ) : ( )} {responseDelta ? ( 0 ? "text-amber-600" : "text-emerald-600" )} > {responseDelta.delta !== null && responseDelta.delta > 0 ? ( ) : ( )} {responseDelta.label} ) : null} {responseFooter} Média móvel dos últimos 7 dias. Resolvidos (7 dias) {overview?.resolution ? overview.resolution.resolvedLast7d : } {resolutionInfo?.delta !== null && resolutionInfo.delta < 0 ? ( ) : ( )} {resolutionInfo?.label} {resolutionFooter} Comparação com os 7 dias anteriores.
) } export function DashboardQueueInsights() { const { session, convexUserId, isStaff } = useAuth() const tenantId = session?.user.tenantId ?? DEFAULT_TENANT_ID const dashboardEnabled = Boolean(isStaff && convexUserId) const overview = useQuery( api.reports.dashboardOverview, dashboardEnabled ? { tenantId, viewerId: convexUserId as Id<"users"> } : "skip" ) as DashboardOverview | undefined const queueTrend = useQuery( api.reports.queueLoadTrend, dashboardEnabled ? { tenantId, viewerId: convexUserId as Id<"users">, range: "30d", limit: 3 } : "skip" ) as QueueTrendResponse | undefined return (
) } function QueueSparklineRow({ data, isLoading, }: { data?: QueueTrendResponse["queues"] isLoading: boolean }) { return (
Filas com maior volume Comparativo diário de entradas x resolvidos
Abrir painel de filas
{isLoading ? (
{Array.from({ length: 3 }).map((_, index) => ( ))}
) : !data || data.length === 0 ? (
Ainda não há dados suficientes para exibir a tendência. Continue alimentando as filas ou reduza o período.
) : (
{data.map((queue) => ( ))}
)}
) } function QueueSparklineCard({ queue, }: { queue: Required["queues"][number] }) { const sanitizedId = queue.id.replace(/[^a-zA-Z0-9_-]/g, "") const latest = queue.series.length > 0 ? queue.series[queue.series.length - 1] : null const net = queue.openedTotal - queue.resolvedTotal const netLabel = net === 0 ? "Estável" : `${net > 0 ? "+" : ""}${net} no período` const lastUpdated = latest ? formatDistanceToNow(new Date(latest.date), { addSuffix: true, locale: ptBR }) : null return (

{queue.name}

Última movimentação {lastUpdated ?? "—"}

0 ? "text-amber-600" : net < 0 ? "text-emerald-600" : "text-neutral-500" )} > {netLabel}

Entraram

{queue.openedTotal}

Resolvidos

{queue.resolvedTotal}

formatDateDM(new Date(value))} tickLine={false} axisLine={false} tickMargin={8} minTickGap={32} /> formatDateDMY(new Date(value as string))} valueFormatter={(value) => typeof value === "number" ? value.toLocaleString("pt-BR") : String(value ?? "") } /> } />
) } export function SlaAtRiskCard({ data, isLoading, }: { data?: { total: number; atRisk: number } isLoading: boolean }) { return ( SLA em risco Tickets com solução prevista nas próximas horas

{isLoading ? : data?.atRisk ?? 0}

de {isLoading ? "—" : data?.total ?? 0} tickets ativos

Foque nesses atendimentos para preservar o SLA. Utilize o filtro “em risco” na lista.
Priorizar agora
) } function ButtonLink({ href, children }: { href: string; children: ReactNode }) { return ( {children} ) }