"use client" import { useMemo, useState } from "react" import { useQuery } from "convex/react" import { Bar, BarChart, CartesianGrid, XAxis } from "recharts" 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 } from "@/components/ui/card" import { ReportsFilterToolbar } from "@/components/reports/report-filter-toolbar" import { SearchableCombobox, type SearchableComboboxOption } from "@/components/ui/searchable-combobox" import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart" import { formatDateDM } from "@/lib/utils" import { Skeleton } from "@/components/ui/skeleton" type MachineCategoryDailyItem = { date: string machineId: string | null machineHostname: string | null companyId: string | null companyName: string | null categoryId: string | null categoryName: string total: number } type MachineCategoryReportData = { rangeDays: number items: MachineCategoryDailyItem[] } export function MachineCategoryReport() { const [timeRange, setTimeRange] = useState("30d") const [companyId, setCompanyId] = usePersistentCompanyFilter("all") const { session, convexUserId, isStaff } = useAuth() const tenantId = session?.user.tenantId ?? DEFAULT_TENANT_ID const canView = Boolean(isStaff) const enabled = Boolean(canView && convexUserId) const data = useQuery( api.reports.ticketsByMachineAndCategory, enabled ? ({ tenantId, viewerId: convexUserId as Id<"users">, range: timeRange, companyId: companyId === "all" ? undefined : (companyId as Id<"companies">), } as const) : "skip" ) as MachineCategoryReportData | undefined const companies = useQuery( api.companies.list, enabled ? { tenantId, viewerId: convexUserId as Id<"users"> } : "skip" ) as Array<{ id: Id<"companies">; name: string }> | undefined const companyOptions = useMemo(() => { const base: SearchableComboboxOption[] = [{ value: "all", label: "Todas as empresas" }] if (!companies || companies.length === 0) { return base } const sorted = [...companies].sort((a, b) => a.name.localeCompare(b.name, "pt-BR")) return [ base[0], ...sorted.map((company) => ({ value: company.id, label: company.name, })), ] }, [companies]) const items = useMemo(() => data?.items ?? [], [data]) const totals = useMemo( () => items.reduce( (acc, item) => { acc.totalTickets += item.total acc.machines.add(item.machineId ?? item.machineHostname ?? "sem-maquina") acc.categories.add(item.categoryName) return acc }, { totalTickets: 0, machines: new Set(), categories: new Set(), } ), [items] ) const dailySeries = useMemo( () => { const map = new Map() for (const item of items) { const current = map.get(item.date) ?? 0 map.set(item.date, current + item.total) } return Array.from(map.entries()) .map(([date, total]) => ({ date, total })) .sort((a, b) => a.date.localeCompare(b.date)) }, [items] ) const tableRows = useMemo( () => [...items].sort((a, b) => { if (a.date !== b.date) return b.date.localeCompare(a.date) const machineA = (a.machineHostname ?? "").toLowerCase() const machineB = (b.machineHostname ?? "").toLowerCase() if (machineA !== machineB) return machineA.localeCompare(machineB) return a.categoryName.localeCompare(b.categoryName, "pt-BR") }), [items] ) if (!canView) { return ( Máquinas x categorias Este relatório está disponível apenas para a equipe interna. ) } if (!data) { return (
) } return (
setCompanyId(value)} companyOptions={companyOptions} timeRange={timeRange as "90d" | "30d" | "7d"} onTimeRangeChange={(value) => setTimeRange(value)} />
Chamados analisados Total de tickets com máquina vinculada no período. {totals.totalTickets} Máquinas únicas Quantidade de dispositivos diferentes com chamados no período. {totals.machines.size} Categorias Categorias distintas associadas aos tickets dessas máquinas. {totals.categories.size}
Volume diário por máquina (total) Quantidade de chamados com máquina vinculada, somando todas as categorias, por dia. {dailySeries.length === 0 ? (

Nenhum ticket com máquina vinculada foi encontrado para o período selecionado.

) : ( formatDateDM(new Date(String(value)))} /> formatDateDM(new Date(String(value)))} /> } /> )}
Detalhamento diário por máquina e categoria Cada linha representa o total de chamados abertos em uma data específica, agrupados por máquina e categoria. {tableRows.length === 0 ? (

Nenhum ticket com máquina vinculada foi encontrado para o período selecionado.

) : (
{tableRows.map((row, index) => { const machineLabel = row.machineHostname && row.machineHostname.trim().length > 0 ? row.machineHostname : "Sem hostname" const companyLabel = row.companyName ?? "Sem empresa" return ( ) })}
Data Máquina Empresa Categoria Chamados
{formatDateDM(new Date(`${row.date}T00:00:00Z`))} {machineLabel} {companyLabel} {row.categoryName} {row.total}
)}
) }