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

@ -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">