"use client" import { useEffect, useRef, useState } from "react" import { useRouter } from "next/navigation" import { format, formatDistanceToNowStrict } from "date-fns" import { ptBR } from "date-fns/locale" import { type LucideIcon, Code, FileText, Mail, MessageCircle, MessageSquare, Phone } from "lucide-react" import type { Ticket, TicketChannel, TicketStatus } from "@/lib/schemas/ticket" import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" import { Badge } from "@/components/ui/badge" import { Card, CardContent } from "@/components/ui/card" import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@/components/ui/empty" import { NewTicketDialog } from "@/components/tickets/new-ticket-dialog" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table" import { PrioritySelect } from "@/components/tickets/priority-select" import { cn } from "@/lib/utils" import { deriveServerOffset, toServerTimestamp } from "@/components/tickets/ticket-timer.utils" const channelLabel: Record = { EMAIL: "E-mail", WHATSAPP: "WhatsApp", CHAT: "Chat", PHONE: "Telefone", API: "API", MANUAL: "Manual", } const channelIcon: Record = { EMAIL: Mail, WHATSAPP: MessageCircle, CHAT: MessageSquare, PHONE: Phone, API: Code, MANUAL: FileText, } const cellClass = "px-4 py-4 align-middle text-sm text-neutral-700 whitespace-normal first:pl-5 last:pr-6" const channelIconBadgeClass = "inline-flex size-8 items-center justify-center rounded-full border border-slate-200 bg-slate-50 text-neutral-700" const categoryChipClass = "inline-flex items-center gap-1 rounded-full bg-slate-200/60 px-2.5 py-1 text-[11px] font-medium text-neutral-700" const tableRowClass = "group border-b border-slate-100 text-sm transition-colors hover:bg-slate-100/70 last:border-none" const statusLabel: Record = { PENDING: "Pendente", AWAITING_ATTENDANCE: "Aguardando atendimento", PAUSED: "Pausado", RESOLVED: "Resolvido", } const statusTone: Record = { PENDING: "text-slate-700", AWAITING_ATTENDANCE: "text-sky-700", PAUSED: "text-violet-700", RESOLVED: "text-emerald-700", } function formatDuration(ms?: number) { if (!ms || ms <= 0) return "—" const totalSeconds = Math.floor(ms / 1000) const hours = Math.floor(totalSeconds / 3600) const minutes = Math.floor((totalSeconds % 3600) / 60) const seconds = totalSeconds % 60 if (hours > 0) { return `${hours}h ${minutes.toString().padStart(2, "0")}m` } if (minutes > 0) { return `${minutes}m ${seconds.toString().padStart(2, "0")}s` } return `${seconds}s` } function AssigneeCell({ ticket }: { ticket: Ticket }) { if (!ticket.assignee) { return Sem responsável } const initials = ticket.assignee.name .split(" ") .slice(0, 2) .map((part) => part[0]?.toUpperCase()) .join("") return (
{initials}
{ticket.assignee.name} {ticket.assignee.teams?.[0] ?? "Agente"}
) } export type TicketsTableProps = { tickets?: Ticket[] } export function TicketsTable({ tickets }: TicketsTableProps) { const safeTickets = tickets ?? [] const [now, setNow] = useState(() => Date.now()) const serverOffsetRef = useRef(0) const router = useRouter() useEffect(() => { const interval = setInterval(() => { setNow(Date.now()) }, 1000) return () => clearInterval(interval) }, []) useEffect(() => { const candidates = (tickets ?? []) .map((ticket) => (typeof ticket.workSummary?.serverNow === "number" ? ticket.workSummary.serverNow : null)) .filter((value): value is number => value !== null) if (candidates.length === 0) return const latestServerNow = candidates[candidates.length - 1] serverOffsetRef.current = deriveServerOffset({ currentOffset: serverOffsetRef.current, localNow: Date.now(), serverNow: latestServerNow, }) }, [tickets]) const getWorkedMs = (ticket: Ticket) => { const base = ticket.workSummary?.totalWorkedMs ?? 0 const activeStart = ticket.workSummary?.activeSession?.startedAt if (activeStart instanceof Date) { const alignedNow = toServerTimestamp(now, serverOffsetRef.current) return base + Math.max(0, alignedNow - activeStart.getTime()) } return base } return (
Ticket Assunto Fila Canal Empresa Prioridade Status Tempo Responsável Atualizado {safeTickets.map((ticket) => { const ChannelIcon = channelIcon[ticket.channel] ?? MessageCircle return ( router.push(`/tickets/${ticket.id}`)} onKeyDown={(event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault() router.push(`/tickets/${ticket.id}`) } }} >
#{ticket.reference} {ticket.queue ?? "Sem fila"}
{ticket.subject} {ticket.summary ?? "Sem resumo"}
{ticket.requester.name} {ticket.category ? ( {ticket.category.name} {ticket.subcategory ? • {ticket.subcategory.name} : null} ) : ( Sem categoria )}
{statusLabel[ticket.status]} {ticket.metrics?.timeWaitingMinutes ? ( Em espera há {ticket.metrics.timeWaitingMinutes} min ) : null}
{`há cerca de ${formatDistanceToNowStrict(ticket.updatedAt, { locale: ptBR })}`} {format(ticket.updatedAt, "dd/MM/yyyy HH:mm")}
) })}
{safeTickets.length === 0 && ( Nenhum ticket encontrado Ajuste os filtros ou crie um novo ticket. )}
) }