"use client" import { useEffect, useMemo, useState } from "react" import { format, formatDistanceToNow } from "date-fns" import { ptBR } from "date-fns/locale" import { IconClock, IconPlayerPause, IconPlayerPlay } from "@tabler/icons-react" import { useMutation, useQuery } from "convex/react" import { toast } from "sonner" // @ts-expect-error Convex generates JS module without TS definitions import { api } from "@/convex/_generated/api" import { useAuth } from "@/lib/auth-client" import type { Doc, Id } from "@/convex/_generated/dataModel" import type { TicketWithDetails, TicketQueueSummary, TicketStatus } from "@/lib/schemas/ticket" import { Badge } from "@/components/ui/badge" import { Separator } from "@/components/ui/separator" import { PrioritySelect } from "@/components/tickets/priority-select" import { DeleteTicketDialog } from "@/components/tickets/delete-ticket-dialog" import { StatusSelect } from "@/components/tickets/status-select" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { useTicketCategories } from "@/hooks/use-ticket-categories" interface TicketHeaderProps { ticket: TicketWithDetails } const cardClass = "relative space-y-4 rounded-2xl border border-slate-200 bg-white p-6 shadow-sm" const referenceBadgeClass = "inline-flex h-9 items-center gap-2 rounded-full border border-slate-200 bg-white px-3 text-sm font-semibold text-neutral-700" const startButtonClass = "inline-flex items-center gap-1 rounded-lg border border-black bg-black px-3 py-1.5 text-sm font-semibold text-white transition hover:bg-[#18181b]/85 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-black/30" const pauseButtonClass = "inline-flex items-center gap-1 rounded-lg border border-black bg-black px-3 py-1.5 text-sm font-semibold text-white transition hover:bg-[#18181b]/85 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#18181b]/30" const editButtonClass = "inline-flex items-center gap-1 rounded-lg border border-black bg-black px-3 py-1.5 text-sm font-semibold text-white transition hover:bg-[#18181b]/85 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#18181b]/30" const selectTriggerClass = "h-8 w-full rounded-lg border border-slate-300 bg-white px-3 text-left text-sm font-medium text-neutral-800 shadow-sm focus:ring-0 data-[state=open]:border-[#00d6eb]" const smallSelectTriggerClass = "h-8 w-full rounded-lg border border-slate-300 bg-white px-3 text-left text-sm font-medium text-neutral-800 shadow-sm focus:ring-0 data-[state=open]:border-[#00d6eb]" const sectionLabelClass = "text-xs font-semibold uppercase tracking-wide text-neutral-500" const sectionValueClass = "font-medium text-neutral-900" const subtleBadgeClass = "inline-flex items-center rounded-full border border-slate-200 bg-slate-50 px-2.5 py-0.5 text-[11px] font-medium text-neutral-600" const EMPTY_CATEGORY_VALUE = "__none__" const EMPTY_SUBCATEGORY_VALUE = "__none__" function formatDuration(durationMs: number) { if (durationMs <= 0) return "0s" const totalSeconds = Math.floor(durationMs / 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 TicketSummaryHeader({ ticket }: TicketHeaderProps) { const { convexUserId, role } = useAuth() const isManager = role === "manager" const changeAssignee = useMutation(api.tickets.changeAssignee) const changeQueue = useMutation(api.tickets.changeQueue) const updateSubject = useMutation(api.tickets.updateSubject) const updateSummary = useMutation(api.tickets.updateSummary) const startWork = useMutation(api.tickets.startWork) const pauseWork = useMutation(api.tickets.pauseWork) const updateCategories = useMutation(api.tickets.updateCategories) const agents = (useQuery(api.users.listAgents, { tenantId: ticket.tenantId }) as Doc<"users">[] | undefined) ?? [] const queueArgs = convexUserId ? { tenantId: ticket.tenantId, viewerId: convexUserId as Id<"users"> } : "skip" const queues = ( useQuery(convexUserId ? api.queues.summary : "skip", queueArgs) as TicketQueueSummary[] | undefined ) ?? [] const { categories, isLoading: categoriesLoading } = useTicketCategories(ticket.tenantId) const [status] = useState(ticket.status) const workSummaryRemote = useQuery( api.tickets.workSummary, convexUserId ? { ticketId: ticket.id as Id<"tickets">, viewerId: convexUserId as Id<"users"> } : "skip" ) as | { ticketId: Id<"tickets"> totalWorkedMs: number activeSession: { id: Id<"ticketWorkSessions">; agentId: Id<"users">; startedAt: number } | null } | null | undefined const [editing, setEditing] = useState(false) const [subject, setSubject] = useState(ticket.subject) const [summary, setSummary] = useState(ticket.summary ?? "") const [categorySelection, setCategorySelection] = useState<{ categoryId: string; subcategoryId: string }>( { categoryId: ticket.category?.id ?? "", subcategoryId: ticket.subcategory?.id ?? "", } ) const [saving, setSaving] = useState(false) const selectedCategoryId = categorySelection.categoryId const selectedSubcategoryId = categorySelection.subcategoryId const dirty = useMemo( () => subject !== ticket.subject || (summary ?? "") !== (ticket.summary ?? ""), [subject, summary, ticket.subject, ticket.summary] ) const currentCategoryId = ticket.category?.id ?? "" const currentSubcategoryId = ticket.subcategory?.id ?? "" const categoryDirty = useMemo(() => { return selectedCategoryId !== currentCategoryId || selectedSubcategoryId !== currentSubcategoryId }, [selectedCategoryId, selectedSubcategoryId, currentCategoryId, currentSubcategoryId]) const formDirty = dirty || categoryDirty const activeCategory = useMemo( () => categories.find((category) => category.id === selectedCategoryId) ?? null, [categories, selectedCategoryId] ) const secondaryOptions = useMemo(() => activeCategory?.secondary ?? [], [activeCategory]) async function handleSave() { if (!convexUserId || !formDirty) { setEditing(false) return } setSaving(true) try { if (categoryDirty && !isManager) { toast.loading("Atualizando categoria...", { id: "ticket-category" }) try { await updateCategories({ ticketId: ticket.id as Id<"tickets">, categoryId: selectedCategoryId ? (selectedCategoryId as Id<"ticketCategories">) : null, subcategoryId: selectedSubcategoryId ? (selectedSubcategoryId as Id<"ticketSubcategories">) : null, actorId: convexUserId as Id<"users">, }) toast.success("Categoria atualizada!", { id: "ticket-category" }) } catch (categoryError) { toast.error("Não foi possível atualizar a categoria.", { id: "ticket-category" }) setCategorySelection({ categoryId: currentCategoryId, subcategoryId: currentSubcategoryId, }) throw categoryError } } else if (categoryDirty && isManager) { setCategorySelection({ categoryId: currentCategoryId, subcategoryId: currentSubcategoryId, }) } if (dirty) { toast.loading("Salvando alterações...", { id: "save-header" }) if (subject !== ticket.subject) { await updateSubject({ ticketId: ticket.id as Id<"tickets">, subject: subject.trim(), actorId: convexUserId as Id<"users">, }) } if ((summary ?? "") !== (ticket.summary ?? "")) { await updateSummary({ ticketId: ticket.id as Id<"tickets">, summary: (summary ?? "").trim(), actorId: convexUserId as Id<"users">, }) } toast.success("Cabeçalho atualizado!", { id: "save-header" }) } setEditing(false) } catch { toast.error("Não foi possível salvar.", { id: "save-header" }) } finally { setSaving(false) } } function handleCancel() { setSubject(ticket.subject) setSummary(ticket.summary ?? "") setCategorySelection({ categoryId: currentCategoryId, subcategoryId: currentSubcategoryId, }) setEditing(false) } useEffect(() => { if (editing) return setCategorySelection({ categoryId: ticket.category?.id ?? "", subcategoryId: ticket.subcategory?.id ?? "", }) }, [editing, ticket.category?.id, ticket.subcategory?.id]) useEffect(() => { if (!editing) return if (!selectedCategoryId) { if (selectedSubcategoryId) { setCategorySelection((prev) => ({ ...prev, subcategoryId: "" })) } return } const stillValid = secondaryOptions.some((option) => option.id === selectedSubcategoryId) if (!stillValid && selectedSubcategoryId) { setCategorySelection((prev) => ({ ...prev, subcategoryId: "" })) } }, [editing, secondaryOptions, selectedCategoryId, selectedSubcategoryId]) const workSummary = useMemo(() => { if (workSummaryRemote !== undefined) return workSummaryRemote ?? null if (!ticket.workSummary) return null return { ticketId: ticket.id as Id<"tickets">, totalWorkedMs: ticket.workSummary.totalWorkedMs, activeSession: ticket.workSummary.activeSession ? { id: ticket.workSummary.activeSession.id as Id<"ticketWorkSessions">, agentId: ticket.workSummary.activeSession.agentId as Id<"users">, startedAt: ticket.workSummary.activeSession.startedAt.getTime(), } : null, } }, [ticket.id, ticket.workSummary, workSummaryRemote]) const isPlaying = Boolean(workSummary?.activeSession) const [now, setNow] = useState(() => Date.now()) useEffect(() => { if (!workSummary?.activeSession) return const interval = setInterval(() => { setNow(Date.now()) }, 1000) return () => clearInterval(interval) }, [workSummary?.activeSession]) const currentSessionMs = workSummary?.activeSession ? Math.max(0, now - workSummary.activeSession.startedAt) : 0 const totalWorkedMs = workSummary ? workSummary.totalWorkedMs + currentSessionMs : 0 const formattedTotalWorked = useMemo(() => formatDuration(totalWorkedMs), [totalWorkedMs]) const updatedRelative = useMemo( () => formatDistanceToNow(ticket.updatedAt, { addSuffix: true, locale: ptBR }), [ticket.updatedAt] ) return (
{workSummary ? ( Tempo total: {formattedTotalWorked} ) : null} {!editing ? ( ) : null} } />
#{ticket.reference}
{editing ? (
setSubject(e.target.value)} className="h-10 rounded-lg border border-slate-300 text-lg font-semibold text-neutral-900" />