Reports: add charts to Produtividade (areas + channels), CSAT (bar), Backlog (pie+bar), Horas (stacked bar); deploy Convex reports agent productivity

This commit is contained in:
codex-bot 2025-10-21 13:35:06 -03:00
parent 67df0d4308
commit 68b897c30c
4 changed files with 157 additions and 40 deletions

View file

@ -14,6 +14,8 @@ import { Skeleton } from "@/components/ui/skeleton"
import { Badge } from "@/components/ui/badge"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { usePersistentCompanyFilter } from "@/lib/use-company-filter"
import { Pie, PieChart, Bar, BarChart, CartesianGrid, XAxis } from "recharts"
import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"
const PRIORITY_LABELS: Record<string, string> = {
LOW: "Baixa",
@ -161,18 +163,31 @@ export function BacklogReport() {
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3">
{Object.keys(data.priorityCounts).length === 0 ? (
<p className="rounded-lg border border-dashed border-slate-200 p-6 text-sm text-neutral-500">Sem dados para o período.</p>
) : (
<div className="grid gap-6 md:grid-cols-[1.2fr_1fr]">
<ChartContainer config={{}} className="aspect-auto h-[260px] w-full">
<PieChart>
<ChartTooltip content={<ChartTooltipContent hideLabel />} />
<Pie
data={(Object.entries(data.priorityCounts) as Array<[string, number]>).map(([priority, total]) => ({ name: PRIORITY_LABELS[priority] ?? priority, total, fill: `var(--chart-${{LOW:1,MEDIUM:2,HIGH:3,URGENT:4}[priority as keyof typeof PRIORITY_LABELS]||5})` }))}
dataKey="total"
nameKey="name"
label
/>
</PieChart>
</ChartContainer>
<ul className="space-y-3">
{(Object.entries(data.priorityCounts) as Array<[string, number]>).map(([priority, total]) => (
<div key={priority} className="flex items-center justify-between rounded-xl border border-slate-200 px-4 py-3">
<span className="text-sm font-medium text-neutral-800">
{PRIORITY_LABELS[priority] ?? priority}
</span>
<Badge variant="outline" className="rounded-full border-neutral-300 text-neutral-600">
{total} ticket{total === 1 ? "" : "s"}
</Badge>
</div>
<li key={priority} className="flex items-center justify-between rounded-xl border border-slate-200 px-4 py-3">
<span className="text-sm font-medium text-neutral-800">{PRIORITY_LABELS[priority] ?? priority}</span>
<Badge variant="outline" className="rounded-full border-neutral-300 text-neutral-600">{total} ticket{total === 1 ? "" : "s"}</Badge>
</li>
))}
</ul>
</div>
)}
</CardContent>
</Card>
@ -189,18 +204,14 @@ export function BacklogReport() {
Nenhuma fila com tickets abertos no momento.
</p>
) : (
<ul className="space-y-3">
{data.queueCounts.map((queue: { id: string; name: string; total: number }) => (
<li key={queue.id} className="flex items-center justify-between rounded-xl border border-slate-200 px-4 py-3">
<div className="flex flex-col">
<span className="text-sm font-medium text-neutral-900">{queue.name}</span>
</div>
<Badge variant="outline" className="rounded-full border-neutral-300 text-neutral-600">
{queue.total} em aberto
</Badge>
</li>
))}
</ul>
<ChartContainer config={{}} className="aspect-auto h-[260px] w-full">
<BarChart data={data.queueCounts.map((q: { name: string; total: number }) => ({ name: q.name, total: q.total }))}>
<CartesianGrid vertical={false} />
<XAxis dataKey="name" tickLine={false} axisLine={false} tickMargin={8} interval={0} angle={-30} height={60} />
<Bar dataKey="total" fill="var(--chart-5)" radius={[4, 4, 0, 0]} />
<ChartTooltip content={<ChartTooltipContent className="w-[180px]" nameKey="Abertos" />} />
</BarChart>
</ChartContainer>
)}
</CardContent>
</Card>

View file

@ -10,6 +10,8 @@ import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle }
import { Skeleton } from "@/components/ui/skeleton"
import { Badge } from "@/components/ui/badge"
import { useState } from "react"
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts"
import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"
import { Button } from "@/components/ui/button"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
@ -131,21 +133,18 @@ export function CsatReport() {
</CardDescription>
</CardHeader>
<CardContent>
<ul className="space-y-3">
{data.distribution.map((entry: { score: number; total: number }) => (
<li key={entry.score} className="flex items-center justify-between rounded-xl border border-slate-200 px-4 py-3">
<div className="flex items-center gap-3">
<Badge variant="outline" className="rounded-full border-neutral-300 text-neutral-600">
Nota {entry.score}
</Badge>
<span className="text-sm text-neutral-700">{entry.total} respostas</span>
</div>
<span className="text-sm font-medium text-neutral-900">
{data.totalSurveys === 0 ? "0%" : `${((entry.total / data.totalSurveys) * 100).toFixed(0)}%`}
</span>
</li>
))}
</ul>
{data.totalSurveys === 0 ? (
<p className="rounded-lg border border-dashed border-slate-200 p-6 text-sm text-neutral-500">Sem respostas no período.</p>
) : (
<ChartContainer config={{}} className="aspect-auto h-[260px] w-full">
<BarChart data={data.distribution.map((d: { score: number; total: number }) => ({ score: `Nota ${d.score}`, total: d.total }))}>
<CartesianGrid vertical={false} />
<XAxis dataKey="score" tickLine={false} axisLine={false} tickMargin={8} />
<Bar dataKey="total" fill="var(--chart-3)" radius={[4, 4, 0, 0]} />
<ChartTooltip content={<ChartTooltipContent className="w-[180px]" nameKey="Respostas" />} />
</BarChart>
</ChartContainer>
)}
</CardContent>
</Card>
</div>

View file

@ -15,6 +15,8 @@ import { Input } from "@/components/ui/input"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { usePersistentCompanyFilter } from "@/lib/use-company-filter"
import { Progress } from "@/components/ui/progress"
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts"
import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"
type HoursItem = {
companyId: string
@ -92,6 +94,34 @@ export function HoursReport() {
return (
<div className="space-y-6">
<Card className="border-slate-200">
<CardHeader>
<CardTitle>Top clientes por horas</CardTitle>
<CardDescription>Comparativo empilhado de horas internas x externas (top 10).</CardDescription>
</CardHeader>
<CardContent>
{!filteredWithComputed || filteredWithComputed.length === 0 ? (
<p className="rounded-lg border border-dashed border-slate-200 p-6 text-sm text-neutral-500">Sem dados para o período.</p>
) : (
<ChartContainer config={{}} className="aspect-auto h-[260px] w-full">
<BarChart
data={filteredWithComputed
.slice()
.sort((a, b) => b.total - a.total)
.slice(0, 10)
.map((r) => ({ name: r.name, internas: r.internal, externas: r.external }))}
>
<CartesianGrid vertical={false} />
<XAxis dataKey="name" tickLine={false} axisLine={false} tickMargin={8} interval={0} angle={-30} height={60} />
<Bar dataKey="internas" stackId="a" fill="var(--chart-1)" radius={[4, 4, 0, 0]} />
<Bar dataKey="externas" stackId="a" fill="var(--chart-2)" radius={[4, 4, 0, 0]} />
<ChartTooltip content={<ChartTooltipContent className="w-[200px]" />} />
</BarChart>
</ChartContainer>
)}
</CardContent>
</Card>
<Card className="border-slate-200">
<CardHeader>
<CardTitle>Horas</CardTitle>

View file

@ -15,7 +15,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { useState } from "react"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
import { usePersistentCompanyFilter } from "@/lib/use-company-filter"
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts"
import { Area, AreaChart, Bar, BarChart, CartesianGrid, XAxis } from "recharts"
import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"
function formatMinutes(value: number | null) {
@ -44,6 +44,20 @@ export function SlaReport() {
? ({ 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,
convexUserId
? ({ 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,
convexUserId
? ({ 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<string, number> }> } | undefined
const companies = useQuery(api.companies.list, convexUserId ? { tenantId, viewerId: convexUserId as Id<"users"> } : "skip") as Array<{ id: Id<"companies">; name: string }> | undefined
const queueTotal = useMemo(
@ -173,6 +187,69 @@ export function SlaReport() {
</CardContent>
</Card>
<Card className="border-slate-200">
<CardHeader>
<div className="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
<div>
<CardTitle className="text-lg font-semibold text-neutral-900">Abertos x Resolvidos</CardTitle>
<CardDescription className="text-neutral-600">Comparativo diário no período selecionado.</CardDescription>
</div>
</div>
</CardHeader>
<CardContent>
{!openedResolved || openedResolved.series.length === 0 ? (
<p className="rounded-lg border border-dashed border-slate-200 p-6 text-sm text-neutral-500">Sem dados para o período.</p>
) : (
<ChartContainer config={{}} className="aspect-auto h-[260px] w-full">
<AreaChart data={openedResolved.series}>
<defs>
<linearGradient id="fillOpened" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="var(--chart-1)" stopOpacity={0.8} />
<stop offset="95%" stopColor="var(--chart-1)" stopOpacity={0.1} />
</linearGradient>
<linearGradient id="fillResolved" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="var(--chart-2)" stopOpacity={0.8} />
<stop offset="95%" stopColor="var(--chart-2)" stopOpacity={0.1} />
</linearGradient>
</defs>
<CartesianGrid vertical={false} />
<XAxis dataKey="date" tickLine={false} axisLine={false} tickMargin={8} minTickGap={24} />
<ChartTooltip content={<ChartTooltipContent className="w-[180px]" />} />
<Area dataKey="opened" type="natural" fill="url(#fillOpened)" stroke="var(--chart-1)" name="Abertos" />
<Area dataKey="resolved" type="natural" fill="url(#fillResolved)" stroke="var(--chart-2)" name="Resolvidos" />
</AreaChart>
</ChartContainer>
)}
</CardContent>
</Card>
<Card className="border-slate-200">
<CardHeader>
<div className="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
<div>
<CardTitle className="text-lg font-semibold text-neutral-900">Volume por canal</CardTitle>
<CardDescription className="text-neutral-600">Distribuição diária por canal (empilhado).</CardDescription>
</div>
</div>
</CardHeader>
<CardContent>
{!channelsSeries || channelsSeries.points.length === 0 ? (
<p className="rounded-lg border border-dashed border-slate-200 p-6 text-sm text-neutral-500">Sem dados para o período.</p>
) : (
<ChartContainer config={{}} className="aspect-auto h-[260px] w-full">
<AreaChart data={channelsSeries.points.map((p) => ({ date: p.date, ...p.values }))}>
<CartesianGrid vertical={false} />
<XAxis dataKey="date" tickLine={false} axisLine={false} tickMargin={8} minTickGap={24} />
<ChartTooltip content={<ChartTooltipContent className="w-[220px]" />} />
{channelsSeries.channels.map((ch, idx) => (
<Area key={ch} dataKey={ch} type="natural" stackId="a" stroke={`var(--chart-${(idx % 5) + 1})`} fill={`var(--chart-${(idx % 5) + 1})`} />
))}
</AreaChart>
</ChartContainer>
)}
</CardContent>
</Card>
<Card className="border-slate-200">
<CardHeader>
<div className="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">