"use client" import { useEffect, useMemo, useRef, useState } from "react" import Link from "next/link" import { formatDistanceStrict } from "date-fns" import { ptBR } from "date-fns/locale" import { LayoutGrid } from "lucide-react" import { IconBuildingOff, IconCategory, IconUserOff } from "@tabler/icons-react" import type { Ticket } from "@/lib/schemas/ticket" import { Badge } from "@/components/ui/badge" import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyTitle } from "@/components/ui/empty" import { cn } from "@/lib/utils" import { PrioritySelect } from "@/components/tickets/priority-select" import { getTicketStatusChipClass, getTicketStatusLabel } from "@/lib/ticket-status-style" import { EmptyIndicator } from "@/components/ui/empty-indicator" import { deriveServerOffset, toServerTimestamp } from "@/components/tickets/ticket-timer.utils" type TicketsBoardProps = { tickets: Ticket[] enteringIds?: Set } const SECOND = 1_000 const MINUTE = 60 * SECOND const HOUR = 60 * MINUTE const DAY = 24 * HOUR function getTimestamp(value: Date | number | string | null | undefined) { if (value == null) return null if (typeof value === "number") { return Number.isFinite(value) ? value : null } const parsed = value instanceof Date ? value.getTime() : new Date(value).getTime() return Number.isFinite(parsed) ? parsed : null } function getNextDelay(diff: number) { if (diff < MINUTE) { return SECOND } if (diff < HOUR) { const pastMinute = diff % MINUTE return pastMinute === 0 ? MINUTE : MINUTE - pastMinute } if (diff < DAY) { const pastHour = diff % HOUR return pastHour === 0 ? HOUR : HOUR - pastHour } const pastDay = diff % DAY return pastDay === 0 ? DAY : DAY - pastDay } function formatUpdated(date: Date | number | string, now: number) { const timestamp = getTimestamp(date) if (timestamp === null) return "—" return formatDistanceStrict(timestamp, now, { addSuffix: true, locale: ptBR }) } function formatQueueLabel(queue?: string | null) { if (!queue) { return { label: "Sem fila", title: "Sem fila" } } const normalized = queue.trim().toLowerCase() if (normalized.startsWith("laboratorio")) { return { label: "Lab", title: queue } } return { label: queue, title: queue } } function formatDuration(ms?: number | null) { 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` } export function TicketsBoard({ tickets, enteringIds }: TicketsBoardProps) { const [now, setNow] = useState(() => Date.now()) const serverOffsetRef = useRef(0) const ticketTimestamps = useMemo(() => { return tickets .map((ticket) => getTimestamp(ticket.updatedAt)) .filter((value): value is number => value !== null) }, [tickets]) useEffect(() => { if (ticketTimestamps.length === 0) { return } let minDelay = DAY for (const timestamp of ticketTimestamps) { const diff = Math.abs(now - timestamp) const candidate = Math.max(SECOND, getNextDelay(diff)) if (candidate < minDelay) { minDelay = candidate } } const timeoutId = window.setTimeout(() => setNow(Date.now()), minDelay) return () => window.clearTimeout(timeoutId) }, [ticketTimestamps, now]) 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 } if (!tickets.length) { return (
Nenhum ticket encontrado Ajuste os filtros ou crie um novo ticket para visualizar aqui na visão em quadro.
) } return (
{tickets.map((ticket) => { const isEntering = enteringIds?.has(ticket.id) const queueDisplay = formatQueueLabel(ticket.queue) const updatedTimestamp = getTimestamp(ticket.updatedAt) const updatedAbsoluteLabel = updatedTimestamp ? new Date(updatedTimestamp).toLocaleString("pt-BR", { dateStyle: "short", timeStyle: "short", }) : "—" return (
#{ticket.reference} {queueDisplay.label}
{getTicketStatusLabel(ticket.status)}
event.stopPropagation()} onClick={(event) => event.stopPropagation()} onKeyDown={(event) => event.stopPropagation()} >

{ticket.subject || "Sem assunto"}

Empresa
{ticket.company?.name ?? ( )}
Responsável
{ticket.assignee?.name ?? ( )}
Solicitante
{ticket.requester?.name ?? ticket.requester?.email ?? "—"}
Criado em
{(() => { const createdTimestamp = getTimestamp(ticket.createdAt) return createdTimestamp ? new Date(createdTimestamp).toLocaleDateString("pt-BR") : "—" })()}
Categoria:{" "} {ticket.category?.name ? ( {ticket.category.name} ) : ( )} Tempo:{" "} {formatDuration(getWorkedMs(ticket))} {ticket.workSummary?.activeSession ? ( Em andamento ) : null}
) })}
) }