feat: sync convex companies and dashboard metrics

This commit is contained in:
Esdras Renan 2025-10-18 21:13:20 -03:00
parent 4f52114b48
commit 7a3eca9361
10 changed files with 356 additions and 19 deletions

View file

@ -0,0 +1,143 @@
"use client"
import * as React from "react"
import { Bar, BarChart, CartesianGrid, Cell, 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 {
ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
import { Skeleton } from "@/components/ui/skeleton"
type PriorityKey = "LOW" | "MEDIUM" | "HIGH" | "URGENT"
const PRIORITY_CONFIG: Record<PriorityKey, { label: string; color: string }> = {
LOW: { label: "Baixa", color: "var(--chart-4)" },
MEDIUM: { label: "Média", color: "var(--chart-3)" },
HIGH: { label: "Alta", color: "var(--chart-2)" },
URGENT: { label: "Crítica", color: "var(--chart-1)" },
}
const PRIORITY_ORDER: PriorityKey[] = ["URGENT", "HIGH", "MEDIUM", "LOW"]
export function ChartOpenByPriority() {
const [timeRange, setTimeRange] = React.useState("30d")
const [companyId, setCompanyId] = usePersistentCompanyFilter("all")
const { session, convexUserId, isStaff } = useAuth()
const tenantId = session?.user.tenantId ?? DEFAULT_TENANT_ID
const enabled = Boolean(isStaff && convexUserId)
const report = useQuery(
api.reports.backlogOverview,
enabled
? ({
tenantId,
viewerId: convexUserId as Id<"users">,
range: timeRange,
companyId: companyId === "all" ? undefined : (companyId as Id<"companies">),
})
: "skip"
) as { priorityCounts: Record<string, number> } | undefined
const companies = useQuery(
api.companies.list,
enabled ? { tenantId, viewerId: convexUserId as Id<"users"> } : "skip"
) as Array<{ id: Id<"companies">; name: string }> | undefined
if (!report) {
return <Skeleton className="h-[300px] w-full" />
}
const chartData = PRIORITY_ORDER.map((key) => ({
priority: PRIORITY_CONFIG[key].label,
value: report.priorityCounts?.[key] ?? 0,
fill: PRIORITY_CONFIG[key].color,
}))
const totalTickets = chartData.reduce((sum, item) => sum + item.value, 0)
const chartConfig: ChartConfig = {
value: {
label: "Tickets em atendimento",
},
}
return (
<Card className="@container/card">
<CardHeader>
<CardTitle>Tickets em andamento por prioridade</CardTitle>
<CardDescription>Distribuição de tickets iniciados e ainda abertos</CardDescription>
<CardAction>
<div className="flex flex-wrap items-center justify-end gap-2 md:gap-3">
<Select value={companyId} onValueChange={setCompanyId}>
<SelectTrigger className="w-48">
<SelectValue placeholder="Todas as empresas" />
</SelectTrigger>
<SelectContent className="rounded-xl">
<SelectItem value="all">Todas as empresas</SelectItem>
{(companies ?? []).map((c) => (
<SelectItem key={c.id} value={c.id}>
{c.name}
</SelectItem>
))}
</SelectContent>
</Select>
<ToggleGroup
type="single"
value={timeRange}
onValueChange={(value) => value && setTimeRange(value)}
variant="outline"
className="hidden *:data-[slot=toggle-group-item]:!px-4 @[640px]/card:flex"
>
<ToggleGroupItem value="90d">90 dias</ToggleGroupItem>
<ToggleGroupItem value="30d">30 dias</ToggleGroupItem>
<ToggleGroupItem value="7d">7 dias</ToggleGroupItem>
</ToggleGroup>
</div>
</CardAction>
</CardHeader>
<CardContent className="px-2 pb-6 pt-4 sm:px-6">
{totalTickets === 0 ? (
<div className="flex h-[260px] items-center justify-center rounded-xl border border-dashed border-border/60 text-sm text-muted-foreground">
Sem tickets em andamento no período selecionado.
</div>
) : (
<ChartContainer config={chartConfig} className="aspect-auto h-[260px] w-full">
<BarChart data={chartData} margin={{ top: 12, left: 12, right: 12 }}>
<CartesianGrid vertical={false} />
<XAxis dataKey="priority" tickLine={false} axisLine={false} tickMargin={8} />
<ChartTooltip
cursor={false}
content={<ChartTooltipContent indicator="dot" nameKey="value" labelKey="priority" />}
/>
<Bar dataKey="value" radius={8}>
{chartData.map((entry) => (
<Cell key={`cell-${entry.priority}`} fill={entry.fill} />
))}
</Bar>
</BarChart>
</ChartContainer>
)}
</CardContent>
</Card>
)
}

View file

@ -38,9 +38,23 @@ export function SectionCards() {
dashboardEnabled ? { tenantId, viewerId: convexUserId as Id<"users"> } : "skip"
)
const inProgressSummary = useMemo(() => {
if (dashboard?.inProgress) {
return dashboard.inProgress
}
if (dashboard?.newTickets) {
return {
current: dashboard.newTickets.last24h,
previousSnapshot: dashboard.newTickets.previous24h,
trendPercentage: dashboard.newTickets.trendPercentage,
}
}
return null
}, [dashboard])
const trendInfo = useMemo(() => {
if (!dashboard?.newTickets) return { value: null, label: "Aguardando dados", icon: IconTrendingUp }
const trend = dashboard.newTickets.trendPercentage
if (!inProgressSummary) return { value: null, label: "Aguardando dados", icon: IconTrendingUp }
const trend = inProgressSummary.trendPercentage
if (trend === null) {
return { value: null, label: "Sem histórico", icon: IconTrendingUp }
}
@ -48,7 +62,7 @@ export function SectionCards() {
const icon = positive ? IconTrendingUp : IconTrendingDown
const label = `${positive ? "+" : ""}${trend.toFixed(1)}%`
return { value: trend, label, icon }
}, [dashboard])
}, [inProgressSummary])
const responseDelta = useMemo(() => {
if (!dashboard?.firstResponse) return { delta: null, label: "Sem dados", positive: false }
@ -88,9 +102,9 @@ export function SectionCards() {
<div className="grid grid-cols-1 gap-4 px-4 sm:grid-cols-2 xl:grid-cols-4 xl:px-8">
<Card className="@container/card border border-border/60 bg-gradient-to-br from-white/90 via-white to-primary/5 p-5 shadow-sm">
<CardHeader className="gap-3">
<CardDescription>Tickets novos</CardDescription>
<CardDescription>Tickets em atendimento</CardDescription>
<CardTitle className="text-3xl font-semibold tabular-nums">
{dashboard ? dashboard.newTickets.last24h : <Skeleton className="h-8 w-20" />}
{inProgressSummary ? inProgressSummary.current : <Skeleton className="h-8 w-20" />}
</CardTitle>
<CardAction>
<Badge
@ -109,10 +123,10 @@ export function SectionCards() {
{trendInfo.value === null
? "Aguardando histórico"
: trendInfo.value >= 0
? "Volume acima do período anterior"
: "Volume abaixo do período anterior"}
? "Mais tickets em andamento que no período anterior"
: "Menos tickets em andamento que no período anterior"}
</div>
<span>Comparação com as 24h anteriores.</span>
<span>Considera tickets iniciados (com 1ª resposta) e ainda sem resolução. Comparação com o mesmo horário das últimas 24h.</span>
</CardFooter>
</Card>