"use client" import { useEffect, useMemo, useState } from "react" import Link from "next/link" import { usePaginatedQuery, useQuery } from "convex/react" import { format, formatDistanceToNowStrict } from "date-fns" import { ptBR } from "date-fns/locale" import { IconUserOff } from "@tabler/icons-react" import type { Id } from "@/convex/_generated/dataModel" import { api } from "@/convex/_generated/api" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { DatePicker } from "@/components/ui/date-picker" import { cn } from "@/lib/utils" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import { Spinner } from "@/components/ui/spinner" import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyTitle } from "@/components/ui/empty" import { TicketStatusBadge } from "@/components/tickets/status-badge" import type { TicketPriority, TicketStatus } from "@/lib/schemas/ticket" import { EmptyIndicator } from "@/components/ui/empty-indicator" type DeviceTicketHistoryItem = { id: string reference: number subject: string status: TicketStatus priority: TicketPriority | string updatedAt: number createdAt: number queue: string | null requester: { name: string | null; email: string | null } | null assignee: { name: string | null; email: string | null } | null } type DeviceTicketsHistoryArgs = { machineId: Id<"machines"> status?: "open" | "resolved" priority?: string search?: string from?: number to?: number } type DeviceTicketsHistoryStats = { total: number openCount: number resolvedCount: number } type PeriodPreset = "7d" | "30d" | "90d" | "year" | "all" | "custom" function startOfDayMs(date: Date) { const copy = new Date(date) copy.setHours(0, 0, 0, 0) return copy.getTime() } function endOfDayMs(date: Date) { const copy = new Date(date) copy.setHours(23, 59, 59, 999) return copy.getTime() } function parseDateInput(value: string) { if (!value) return null const parsed = new Date(`${value}T00:00:00`) if (Number.isNaN(parsed.getTime())) { return null } return parsed } function computeRange(preset: PeriodPreset, customFrom: string, customTo: string) { if (preset === "all") { return { from: null, to: null } } if (preset === "custom") { const fromDate = parseDateInput(customFrom) const toDate = parseDateInput(customTo) return { from: fromDate ? startOfDayMs(fromDate) : null, to: toDate ? endOfDayMs(toDate) : null, } } const now = new Date() const end = endOfDayMs(now) const start = new Date(now) switch (preset) { case "7d": start.setDate(start.getDate() - 6) break case "30d": start.setDate(start.getDate() - 29) break case "90d": start.setDate(start.getDate() - 89) break case "year": start.setMonth(0, 1) start.setHours(0, 0, 0, 0) break default: break } return { from: startOfDayMs(start), to: end } } function formatRelativeTime(timestamp?: number | null) { if (!timestamp || timestamp <= 0) return "—" return formatDistanceToNowStrict(timestamp, { addSuffix: true, locale: ptBR }) } function formatAbsoluteTime(timestamp?: number | null) { if (!timestamp || timestamp <= 0) return "—" return format(timestamp, "dd/MM/yyyy HH:mm", { locale: ptBR }) } function getPriorityMeta(priority: TicketPriority | string | null | undefined) { const normalized = (priority ?? "MEDIUM").toString().toUpperCase() switch (normalized) { case "LOW": return { label: "Baixa", badgeClass: "bg-emerald-100 text-emerald-700 border border-emerald-200" } case "MEDIUM": return { label: "Média", badgeClass: "bg-sky-100 text-sky-700 border border-sky-200" } case "HIGH": return { label: "Alta", badgeClass: "bg-amber-100 text-amber-700 border border-amber-200" } case "URGENT": return { label: "Urgente", badgeClass: "bg-rose-100 text-rose-700 border border-rose-200" } default: return { label: normalized, badgeClass: "bg-slate-100 text-slate-700 border border-slate-200" } } } export function DeviceTicketsHistoryClient({ tenantId: _tenantId, deviceId }: { tenantId: string; deviceId: string }) { const [statusFilter, setStatusFilter] = useState<"all" | "open" | "resolved">("all") const [priorityFilter, setPriorityFilter] = useState("ALL") const [periodPreset, setPeriodPreset] = useState("90d") const [customFrom, setCustomFrom] = useState("") const [customTo, setCustomTo] = useState("") const [searchValue, setSearchValue] = useState("") const [debouncedSearch, setDebouncedSearch] = useState("") useEffect(() => { const timer = setTimeout(() => { setDebouncedSearch(searchValue.trim()) }, 300) return () => clearTimeout(timer) }, [searchValue]) useEffect(() => { if (periodPreset !== "custom") { setCustomFrom("") setCustomTo("") } }, [periodPreset]) const range = useMemo(() => computeRange(periodPreset, customFrom, customTo), [periodPreset, customFrom, customTo]) const queryArgs = useMemo(() => { const args: DeviceTicketsHistoryArgs = { machineId: deviceId as Id<"machines">, } if (statusFilter !== "all") { args.status = statusFilter } if (priorityFilter !== "ALL") { args.priority = priorityFilter } if (debouncedSearch) { args.search = debouncedSearch } if (range.from !== null) { args.from = range.from } if (range.to !== null) { args.to = range.to } return args }, [debouncedSearch, deviceId, priorityFilter, range.from, range.to, statusFilter]) const { results: tickets, status: paginationStatus, loadMore } = usePaginatedQuery( api.devices.listTicketsHistory, queryArgs, { initialNumItems: 25 } ) const stats = useQuery(api.devices.getTicketsHistoryStats, queryArgs) as DeviceTicketsHistoryStats | undefined const totalTickets = stats?.total ?? 0 const openTickets = stats?.openCount ?? 0 const resolvedTickets = stats?.resolvedCount ?? 0 const isLoadingFirstPage = paginationStatus === "LoadingFirstPage" const isLoadingMore = paginationStatus === "LoadingMore" const canLoadMore = paginationStatus === "CanLoadMore" const resetFilters = () => { setStatusFilter("all") setPriorityFilter("ALL") setPeriodPreset("90d") setCustomFrom("") setCustomTo("") setSearchValue("") setDebouncedSearch("") } return (

Chamados no período

{stats ? ( totalTickets ) : ( Atualizando... )}

Em aberto

{stats ? openTickets : "—"}

Resolvidos

{stats ? resolvedTickets : "—"}

setSearchValue(event.target.value)} placeholder="Buscar por assunto, #ID, solicitante ou responsável" className="sm:max-w-sm" />
{periodPreset === "custom" ? (
setCustomFrom(value ?? "")} className="sm:w-[160px]" placeholder="Início" /> setCustomTo(value ?? "")} className="sm:w-[160px]" placeholder="Fim" />
) : null}
{isLoadingFirstPage ? (
Carregando chamados...
) : tickets.length === 0 ? ( Nenhum chamado encontrado Ajuste os filtros ou expanda o período para visualizar o histórico de chamados desta dispositivo. ) : ( <>
Ticket Status Prioridade Última atualização Responsável {tickets.map((ticket) => { const priorityMeta = getPriorityMeta(ticket.priority) const requesterLabel = ticket.requester?.name ?? ticket.requester?.email ?? "Solicitante não informado" const updatedLabel = formatRelativeTime(ticket.updatedAt) const updatedAbsolute = formatAbsoluteTime(ticket.updatedAt) return (
#{ticket.reference} · {ticket.subject}
{requesterLabel} {ticket.queue ? ( {ticket.queue} ) : null} Aberto em {formatAbsoluteTime(ticket.createdAt)}
{priorityMeta.label}
{updatedLabel} {updatedAbsolute}
{ticket.assignee ? ( <> {ticket.assignee.name ?? ticket.assignee.email ?? "—"} {ticket.assignee.email ? ( {ticket.assignee.email} ) : null} ) : ( )}
) })}
{canLoadMore || isLoadingMore ? ( ) : ( Todos os chamados filtrados foram exibidos. )}
)}
) }