"use client"; import { useState, useEffect, use } from "react"; import { useRouter } from "next/navigation"; import Link from "next/link"; import { formatDistanceToNow } from "date-fns"; import { ArrowLeftIcon, SendIcon, PaperclipIcon } from "lucide-react"; import { getTicket, getTicketTransactions, getQueues, previewTicket, updateTicket, } from "@/lib/api"; import type { Ticket, Transaction, Queue, PreviewResult, UpdateResult, } from "@/lib/types"; import { Separator } from "@/components/ui/separator"; import { cn } from "@/lib/utils"; const STATUS_COLORS: Record = { new: "#8a8f98", open: "#7170ff", in_progress: "#f59e0b", resolved: "#22c55e", closed: "#6b7280", }; const STATUS_LABELS: Record = { new: "New", open: "Open", in_progress: "In progress", resolved: "Resolved", closed: "Closed", }; const ALL_STATUSES = ["new", "open", "in_progress", "resolved", "closed"]; function getInitial(name: string): string { if (!name) return "?"; return name.charAt(0).toUpperCase(); } function getInitialColor(name: string): string { const colors = ["#5e6ad2", "#7170ff", "#828fff", "#f59e0b", "#22c55e"]; let hash = 0; for (let i = 0; i < name.length; i++) { hash = name.charCodeAt(i) + ((hash << 5) - hash); } return colors[Math.abs(hash) % colors.length]; } function TransactionBubble({ tx, }: { tx: Transaction; }) { const isSystem = tx.transaction_type === "StatusChange" || tx.transaction_type === "SetOwner" || tx.transaction_type === "Create"; const isAgent = tx.transaction_type === "Correspond" || tx.transaction_type === "Comment"; if (isSystem) { const timeAgo = formatDistanceToNow(new Date(tx.created_at), { addSuffix: true, }); let message = ""; if (tx.transaction_type === "Create") { message = `Ticket created`; } else if (tx.transaction_type === "StatusChange") { const oldLabel = tx.old_value ? STATUS_LABELS[tx.old_value] || tx.old_value : "?"; const newLabel = tx.new_value ? STATUS_LABELS[tx.new_value] || tx.new_value : "?"; message = `${getInitial(tx.creator_id)} changed status from ${oldLabel} to ${newLabel}`; } else if (tx.transaction_type === "SetOwner") { message = tx.new_value ? `${getInitial(tx.creator_id)} assigned to ${tx.new_value}` : `${getInitial(tx.creator_id)} unassigned`; } else { message = tx.transaction_type; } return (
{message} · {timeAgo}
); } const isInternal = tx.transaction_type === "Comment"; const timeAgo = formatDistanceToNow(new Date(tx.created_at), { addSuffix: true, }); return (
{getInitial(tx.creator_id)}
{isInternal && (
Internal note
)}

{typeof tx.data === "object" && tx.data !== null && "body" in (tx.data as Record) ? String((tx.data as Record).body) : tx.transaction_type}

{timeAgo}

); } export default function TicketDetailPage({ params, }: { params: Promise<{ id: string }>; }) { const { id } = use(params); const router = useRouter(); const [ticket, setTicket] = useState(null); const [transactions, setTransactions] = useState([]); const [queue, setQueue] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); // Reply const [replyText, setReplyText] = useState(""); const [replyMode, setReplyMode] = useState<"public" | "internal">("public"); // Status change const [statusSelectOpen, setStatusSelectOpen] = useState(false); const [pendingStatus, setPendingStatus] = useState(null); const [preview, setPreview] = useState(null); const [previewLoading, setPreviewLoading] = useState(false); const [previewError, setPreviewError] = useState(null); const [applyLoading, setApplyLoading] = useState(false); const [scripResults, setScripResults] = useState(null); const fetchData = async () => { setLoading(true); setError(null); const [ticketRes, txRes, queuesRes] = await Promise.all([ getTicket(id), getTicketTransactions(id), getQueues(), ]); if (ticketRes.error) { setError(ticketRes.error); } else { setTicket(ticketRes.data); } if (txRes.error) { setError((prev) => prev || txRes.error); } else { setTransactions(txRes.data ?? []); } if (queuesRes.data && ticketRes.data) { const q = queuesRes.data.find((q) => q.id === ticketRes.data!.queue_id); if (q) setQueue(q); } setLoading(false); }; useEffect(() => { fetchData(); }, [id]); const handleStatusSelect = async (newStatus: string) => { setPendingStatus(newStatus); setStatusSelectOpen(false); setPreview(null); setPreviewError(null); setScripResults(null); if (newStatus === ticket?.status) return; setPreviewLoading(true); const { data, error } = await previewTicket(id, { status: newStatus }); setPreviewLoading(false); if (error) { setPreviewError(error); setPendingStatus(null); } else { setPreview(data); } }; const handleApplyStatus = async () => { if (!pendingStatus) return; setApplyLoading(true); setPreviewError(null); const { data, error } = await updateTicket(id, { status: pendingStatus }); setApplyLoading(false); if (error) { setPreviewError(error); } else if (data) { setTicket(data.ticket); setScripResults(data.scrip_results); setPreview(null); setPendingStatus(null); // Refresh transactions const txRes = await getTicketTransactions(id); if (txRes.data) setTransactions(txRes.data); } }; const handleCancelStatus = () => { setPendingStatus(null); setPreview(null); setPreviewError(null); setScripResults(null); }; if (loading) { return (
{Array.from({ length: 6 }).map((_, i) => (
))}
{Array.from({ length: 5 }).map((_, i) => (
))}
); } if (error && !ticket) { return (

{error}

); } if (!ticket) { return (

Ticket not found

); } const currentStatusColor = STATUS_COLORS[ticket.status] || STATUS_COLORS.new; const currentStatusLabel = STATUS_LABELS[ticket.status] || ticket.status; return (
{/* Left panel — conversation */}
{/* Header */}
All tickets
{/* Title */}

{ticket.subject}

{ticket.id.slice(0, 8)} · {queue?.name || ticket.queue_id}

{/* Conversation */}
{transactions.length === 0 && (

No activity yet

)} {transactions.map((tx) => ( ))}
{/* Reply box */}
{/* Toggle tabs */}