"use client" import { useQuery } from "convex/react" import { IconMoodSmile, IconStars, IconMessageCircle2, IconTarget } 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, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Skeleton } from "@/components/ui/skeleton" import { Badge } from "@/components/ui/badge" import { useMemo, useState } from "react" import { Bar, BarChart, CartesianGrid, XAxis } from "recharts" import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart" import { SearchableCombobox, type SearchableComboboxOption } from "@/components/ui/searchable-combobox" import { usePersistentCompanyFilter } from "@/lib/use-company-filter" import { ReportsFilterToolbar } from "@/components/reports/report-filter-toolbar" import { ReportScheduleDrawer } from "@/components/reports/report-schedule-drawer" function formatScore(value: number | null) { if (value === null) return "—" return value.toFixed(2) } export function CsatReport() { const [companyId, setCompanyId] = usePersistentCompanyFilter("all") const [timeRange, setTimeRange] = useState("90d") const [schedulerOpen, setSchedulerOpen] = useState(false) const [dateFrom, setDateFrom] = useState(null) const [dateTo, setDateTo] = useState(null) const { session, convexUserId, isStaff, isAdmin } = useAuth() const tenantId = session?.user.tenantId ?? DEFAULT_TENANT_ID const enabled = Boolean(isStaff && convexUserId) const dateRangeFilters = useMemo(() => { const filters: { dateFrom?: string; dateTo?: string } = {} if (dateFrom) filters.dateFrom = dateFrom if (dateTo) filters.dateTo = dateTo return filters }, [dateFrom, dateTo]) const data = useQuery( api.reports.csatOverview, enabled ? ({ tenantId, viewerId: convexUserId as Id<"users">, range: timeRange, companyId: companyId === "all" ? undefined : (companyId as Id<"companies">), ...dateRangeFilters, }) : "skip" ) const companies = useQuery( api.companies.list, enabled ? { tenantId, viewerId: convexUserId as Id<"users"> } : "skip" ) as Array<{ id: Id<"companies">; name: string }> | undefined if (!data) { return (
{Array.from({ length: 3 }).map((_, index) => ( ))}
) } const companyOptions = (companies ?? []).map((company) => ({ value: company.id, label: company.name, })) const comboboxOptions: SearchableComboboxOption[] = [ { value: "all", label: "Todas as empresas" }, ...companyOptions, ] const handleCompanyChange = (value: string | null) => { setCompanyId(value ?? "all") } const selectedCompany = companyId === "all" ? "all" : companyId const averageScore = typeof data.averageScore === "number" ? data.averageScore : null const positiveRate = typeof data.positiveRate === "number" ? data.positiveRate : null const agentStats = Array.isArray(data.byAgent) ? data.byAgent : [] const topAgent = agentStats[0] ?? null const agentChartData = agentStats.map((agent: { agentName: string; averageScore: number | null; totalResponses: number; positiveRate: number | null }) => ({ agent: agent.agentName ?? "Sem responsável", average: agent.averageScore ?? 0, total: agent.totalResponses ?? 0, positive: agent.positiveRate ? Math.round(agent.positiveRate * 1000) / 10 : 0, })) return (
{isAdmin ? ( ) : null} handleCompanyChange(value)} companyOptions={comboboxOptions} timeRange={timeRange as "90d" | "30d" | "7d"} onTimeRangeChange={(value) => { setTimeRange(value) setDateFrom(null) setDateTo(null) }} dateFrom={dateFrom} dateTo={dateTo} onDateRangeChange={({ from, to }) => { setDateFrom(from) setDateTo(to) }} exportHref={`/api/reports/csat.xlsx?range=${timeRange}${ companyId !== "all" ? `&companyId=${companyId}` : "" }`} onOpenScheduler={isAdmin ? () => setSchedulerOpen(true) : undefined} />
CSAT médio Média das respostas recebidas. {formatScore(averageScore)} / 5 Total de respostas Avaliações coletadas nos tickets. {data.totalSurveys} Avaliações positivas Notas iguais ou superiores a 4. {positiveRate === null ? "—" : `${(positiveRate * 100).toFixed(1).replace(".0", "")}%`} Destaque do período Agente com melhor média no recorte selecionado. {topAgent ? ( <>

{topAgent.agentName}

{topAgent.averageScore ? `${topAgent.averageScore.toFixed(2)} / 5` : "Sem notas suficientes"}

{topAgent.totalResponses} avaliação{topAgent.totalResponses === 1 ? "" : "s"}

) : (

Ainda não há avaliações suficientes.

)}
Últimas avaliações Até 10 registros mais recentes enviados pelos usuários.
{data.recent.length === 0 ? (

Ainda não coletamos nenhuma avaliação no período selecionado.

) : ( data.recent.map( (item: { ticketId: string reference: number score: number maxScore?: number | null comment?: string | null receivedAt: number assigneeName?: string | null }) => { const normalized = item.maxScore && item.maxScore > 0 ? Math.round(((item.score / item.maxScore) * 5) * 10) / 10 : item.score const badgeLabel = item.maxScore && item.maxScore !== 5 ? `${item.score}/${item.maxScore}` : normalized.toFixed(1).replace(/\.0$/, "") return (
#{item.reference} {item.assigneeName ? ( {item.assigneeName} ) : null} {item.comment ? ( “{item.comment}” ) : null}
Nota {badgeLabel}
) } ) )}
Distribuição das notas Frequência de respostas para cada valor na escala de 1 a 5. {data.totalSurveys === 0 ? (

Sem respostas no período.

) : ( ({ score: `Nota ${d.score}`, total: d.total }))}> } /> )}
Desempenho por agente Média ponderada (1 a 5) e volume de avaliações recebidas por integrante da equipe. {agentChartData.length === 0 ? (

Ainda não há avaliações atreladas a agentes no período selecionado.

) : ( <> `Agente: ${label}`} valueFormatter={(value) => `${Number(value).toFixed(2)} / 5`} /> } />
{agentStats.slice(0, 6).map((agent: { agentName: string; averageScore: number | null; totalResponses: number; positiveRate: number | null }) => (
{agent.agentName} {agent.totalResponses} avaliação{agent.totalResponses === 1 ? "" : "s"}
{agent.averageScore ? `${agent.averageScore.toFixed(2)} / 5` : "—"}

{agent.positiveRate === null ? "—" : `${(agent.positiveRate * 100).toFixed(0)}% positivas`}

))}
)}
) }