From 88b65c3e1527fd4661498ba9e4343c1eb2761991 Mon Sep 17 00:00:00 2001 From: Esdras Renan Date: Tue, 7 Oct 2025 17:28:01 -0300 Subject: [PATCH] =?UTF-8?q?views:=20criar=20p=C3=A1gina=20/views=20com=20g?= =?UTF-8?q?r=C3=A1ficos=20(Canais=20movido=20do=20dashboard,=20CSAT=20dist?= =?UTF-8?q?ribui=C3=A7=C3=A3o,=20Filas=20abertas);=20dashboard:=20trocar?= =?UTF-8?q?=20por=20gr=C3=A1fico=20Abertos=20x=20Resolvidos=20(=C3=BAltimo?= =?UTF-8?q?s=207/30/90=20dias);=20reports:=20nova=20query=20openedResolved?= =?UTF-8?q?ByDay?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex/reports.ts | 46 ++++++ src/app/dashboard/page.tsx | 4 +- src/app/views/page.tsx | 22 +++ .../charts/chart-opened-resolved.tsx | 112 +++++++++++++++ src/components/charts/views-charts.tsx | 135 ++++++++++++++++++ 5 files changed, 317 insertions(+), 2 deletions(-) create mode 100644 src/app/views/page.tsx create mode 100644 src/components/charts/chart-opened-resolved.tsx create mode 100644 src/components/charts/views-charts.tsx diff --git a/convex/reports.ts b/convex/reports.ts index 698f6f8..0d5ae75 100644 --- a/convex/reports.ts +++ b/convex/reports.ts @@ -226,6 +226,52 @@ export const csatOverview = query({ }, }); +export const openedResolvedByDay = query({ + args: { tenantId: v.string(), viewerId: v.id("users"), range: v.optional(v.string()), companyId: v.optional(v.id("companies")) }, + handler: async (ctx, { tenantId, viewerId, range, companyId }) => { + const viewer = await requireStaff(ctx, viewerId, tenantId); + let tickets = await fetchScopedTickets(ctx, tenantId, viewer); + if (companyId) tickets = tickets.filter((t) => t.companyId === companyId) + + const days = range === "7d" ? 7 : range === "30d" ? 30 : 90; + const end = new Date(); + end.setUTCHours(0, 0, 0, 0); + const endMs = end.getTime() + ONE_DAY_MS; + const startMs = endMs - days * ONE_DAY_MS; + + const opened: Record = {} + const resolved: Record = {} + + // pre-fill buckets + for (let i = days - 1; i >= 0; i--) { + const d = new Date(endMs - (i + 1) * ONE_DAY_MS) + const key = formatDateKey(d.getTime()) + opened[key] = 0 + resolved[key] = 0 + } + + for (const t of tickets) { + if (t.createdAt >= startMs && t.createdAt < endMs) { + const key = formatDateKey(t.createdAt) + opened[key] = (opened[key] ?? 0) + 1 + } + if (t.resolvedAt && t.resolvedAt >= startMs && t.resolvedAt < endMs) { + const key = formatDateKey(t.resolvedAt) + resolved[key] = (resolved[key] ?? 0) + 1 + } + } + + const series: Array<{ date: string; opened: number; resolved: number }> = [] + for (let i = days - 1; i >= 0; i--) { + const d = new Date(endMs - (i + 1) * ONE_DAY_MS) + const key = formatDateKey(d.getTime()) + series.push({ date: key, opened: opened[key] ?? 0, resolved: resolved[key] ?? 0 }) + } + + return { rangeDays: days, series } + }, +}) + export const backlogOverview = query({ args: { tenantId: v.string(), viewerId: v.id("users"), range: v.optional(v.string()), companyId: v.optional(v.id("companies")) }, handler: async (ctx, { tenantId, viewerId, range, companyId }) => { diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 82acb67..e94fee0 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -3,7 +3,7 @@ import { SectionCards } from "@/components/section-cards" import { SiteHeader } from "@/components/site-header" import { RecentTicketsPanel } from "@/components/tickets/recent-tickets-panel" import { TicketQueueSummaryCards } from "@/components/tickets/ticket-queue-summary" -import { ChartAreaInteractive } from "@/components/chart-area-interactive" +import { ChartOpenedResolved } from "@/components/charts/chart-opened-resolved" import { NewTicketDialogDeferred } from "@/components/tickets/new-ticket-dialog.client" export default function Dashboard() { @@ -20,7 +20,7 @@ export default function Dashboard() { >
- +
diff --git a/src/app/views/page.tsx b/src/app/views/page.tsx new file mode 100644 index 0000000..58f9f33 --- /dev/null +++ b/src/app/views/page.tsx @@ -0,0 +1,22 @@ +import { AppShell } from "@/components/app-shell" +import { SiteHeader } from "@/components/site-header" +import { ChartAreaInteractive } from "@/components/chart-area-interactive" +import { ViewsCharts } from "@/components/charts/views-charts" + +export const dynamic = "force-dynamic" + +export default function ViewsPage() { + return ( + } + > +
+ {/* Canais por período (movido do dashboard) */} + + {/* Conjunto extra de gráficos relevantes (CSAT, filas) */} + +
+
+ ) +} + diff --git a/src/components/charts/chart-opened-resolved.tsx b/src/components/charts/chart-opened-resolved.tsx new file mode 100644 index 0000000..45aeda0 --- /dev/null +++ b/src/components/charts/chart-opened-resolved.tsx @@ -0,0 +1,112 @@ +"use client" + +import * as React from "react" +import { CartesianGrid, Line, LineChart, XAxis } from "recharts" +import { useQuery } from "convex/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 { usePersistentCompanyFilter } from "@/lib/use-company-filter" +import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group" +import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart" +import { Skeleton } from "@/components/ui/skeleton" + +type SeriesPoint = { date: string; opened: number; resolved: number } + +const chartConfig = { + opened: { label: "Abertos", color: "var(--chart-1)" }, + resolved: { label: "Resolvidos", color: "var(--chart-2)" }, +} as const + +export function ChartOpenedResolved() { + const [timeRange, setTimeRange] = React.useState("30d") + const [companyId, setCompanyId] = usePersistentCompanyFilter("all") + const { session, convexUserId } = useAuth() + const tenantId = session?.user.tenantId ?? DEFAULT_TENANT_ID + + const data = 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: SeriesPoint[] } | undefined + + const companies = useQuery(api.companies.list, convexUserId ? { tenantId, viewerId: convexUserId as Id<"users"> } : "skip") as Array<{ id: Id<"companies">; name: string }> | undefined + + if (!data) { + return + } + + return ( + + + Abertos x Resolvidos + + Evolução diária nos últimos {data.rangeDays} dias + + +
+ + + 90 dias + 30 dias + 7 dias + +
+
+
+ + {data.series.length === 0 ? ( +
+ Sem dados suficientes no período selecionado. +
+ ) : ( + + + + + } + /> + + + + + )} +
+
+ ) +} + diff --git a/src/components/charts/views-charts.tsx b/src/components/charts/views-charts.tsx new file mode 100644 index 0000000..300109f --- /dev/null +++ b/src/components/charts/views-charts.tsx @@ -0,0 +1,135 @@ +"use client" + +import * as React from "react" +import { Pie, PieChart, LabelList, Bar, BarChart, CartesianGrid, XAxis } from "recharts" +import { useQuery } from "convex/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 { usePersistentCompanyFilter } from "@/lib/use-company-filter" +import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardAction } from "@/components/ui/card" +import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { Skeleton } from "@/components/ui/skeleton" + +export function ViewsCharts() { + return ( +
+ + +
+ ) +} + +function CsatPie() { + const [companyId, setCompanyId] = usePersistentCompanyFilter("all") + const { session, convexUserId } = useAuth() + const tenantId = session?.user.tenantId ?? DEFAULT_TENANT_ID + const data = useQuery( + api.reports.csatOverview, + convexUserId + ? ({ tenantId, viewerId: convexUserId as Id<"users">, companyId: companyId === "all" ? undefined : (companyId as Id<"companies">) }) + : "skip" + ) as { totalSurveys: number; distribution: { score: number; total: number }[] } | undefined + const companies = useQuery(api.companies.list, convexUserId ? { tenantId, viewerId: convexUserId as Id<"users"> } : "skip") as Array<{ id: Id<"companies">; name: string }> | undefined + + if (!data) return + const chartData = (data.distribution ?? []).map((d) => ({ score: `Nota ${d.score}`, total: d.total })) + const chartConfig: any = { total: { label: "Respostas" } } + + return ( + + + CSAT - Distribuição + Frequência de respostas por nota + + + + + + {chartData.length === 0 ? ( +
+ Sem respostas no período. +
+ ) : ( + + + } /> + + + + + + )} +
+
+ ) +} + +function QueuesOpenBar() { + const [companyId, setCompanyId] = usePersistentCompanyFilter("all") + const { session, convexUserId } = useAuth() + const tenantId = session?.user.tenantId ?? DEFAULT_TENANT_ID + const data = useQuery( + api.reports.slaOverview, + convexUserId + ? ({ tenantId, viewerId: convexUserId as Id<"users">, companyId: companyId === "all" ? undefined : (companyId as Id<"companies">) }) + : "skip" + ) as { queueBreakdown: { id: string; name: string; open: number }[] } | undefined + const companies = useQuery(api.companies.list, convexUserId ? { tenantId, viewerId: convexUserId as Id<"users"> } : "skip") as Array<{ id: Id<"companies">; name: string }> | undefined + + if (!data) return + const chartData = (data.queueBreakdown ?? []).map((q) => ({ queue: q.name, open: q.open })) + const chartConfig: any = { open: { label: "Abertos" } } + + return ( + + + Filas com maior volume aberto + Distribuição atual por fila + + + + + + {chartData.length === 0 ? ( +
+ Sem filas com tickets abertos. +
+ ) : ( + + + + + } /> + + + + )} +
+
+ ) +} +