"use client"; import { useState } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { RefreshCw, Ticket, Plus, Send } from "lucide-react"; import { Card, CardContent, CardHeader } from "@/components/ui/Card"; import { Table, TableHead, TableBody, TableRow, Th, Td, EmptyState } from "@/components/ui/Table"; import { Badge } from "@/components/ui/Badge"; import { Button } from "@/components/ui/Button"; import { Input } from "@/components/ui/Input"; import { Modal } from "@/components/ui/Modal"; import { formatDate } from "@/lib/utils"; import api from "@/lib/api"; import { toast } from "sonner"; interface TicketComment { id: string; body: string; createdAt: string; sender?: { firstName: string; lastName: string }; author?: { firstName: string; lastName: string }; } interface TicketItem { id: string; ticketNumber?: string; subject: string; description?: string; type: string; priority: string; status: string; clientId?: string; client?: { id: string; firstName: string; lastName: string; accountNumber: string }; assignedTo?: { id: string; firstName: string; lastName: string } | string; createdAt: string; updatedAt?: string; comments?: TicketComment[]; } interface PaginatedResponse { data: T[]; meta: { total: number; page: number; limit: number; }; } const statusVariant: Record = { OPEN: "warning", IN_PROGRESS: "default" as any, RESOLVED: "success", CLOSED: "muted", }; const priorityVariant: Record = { HIGH: "danger", NORMAL: "muted", LOW: "muted", }; const statusFilters = ["", "OPEN", "IN_PROGRESS", "RESOLVED", "CLOSED"]; const typeFilters = ["", "SUPPORT", "BILLING", "INSTALLATION"]; export default function TicketsPage() { const qc = useQueryClient(); const [search, setSearch] = useState(""); const [statusFilter, setStatusFilter] = useState(""); const [typeFilter, setTypeFilter] = useState(""); const [page, setPage] = useState(1); const [selected, setSelected] = useState(null); const [showCreate, setShowCreate] = useState(false); const [comment, setComment] = useState(""); const [newForm, setNewForm] = useState({ subject: "", description: "", type: "SUPPORT", priority: "NORMAL", clientSearch: "", clientId: "" }); const { data, isLoading, refetch } = useQuery>({ queryKey: ["tickets", search, statusFilter, typeFilter, page], queryFn: async () => { const params = new URLSearchParams({ page: String(page), limit: "20" }); if (search) params.set("search", search); if (statusFilter) params.set("status", statusFilter); if (typeFilter) params.set("type", typeFilter); const res = await api.get>(`/api/v1/tickets?${params}`); return res.data; }, }); const { data: ticketDetail, refetch: refetchDetail } = useQuery({ queryKey: ["ticket", selected?.id], queryFn: async () => { const res = await api.get(`/api/v1/tickets/${selected!.id}`); return res.data; }, enabled: !!selected?.id, }); const { data: clientSearch = [], isFetching: searchingClients } = useQuery({ queryKey: ["client-search", newForm.clientSearch], queryFn: async () => { if (!newForm.clientSearch || newForm.clientSearch.length < 2) return []; const res = await api.get(`/api/v1/clients?search=${encodeURIComponent(newForm.clientSearch)}&limit=10`); return res.data?.data ?? []; }, enabled: newForm.clientSearch.length >= 2, }); const updateStatus = useMutation({ mutationFn: async ({ id, status }: { id: string; status: string }) => { await api.patch(`/api/v1/tickets/${id}`, { status }); }, onSuccess: () => { toast.success("Status updated"); refetch(); refetchDetail(); qc.invalidateQueries({ queryKey: ["ticket"] }); }, onError: (e: any) => toast.error(e.response?.data?.message ?? "Failed"), }); const addComment = useMutation({ mutationFn: async () => { await api.post(`/api/v1/tickets/${selected!.id}/messages`, { body: comment }); }, onSuccess: () => { toast.success("Comment added"); setComment(""); refetchDetail(); }, onError: (e: any) => toast.error(e.response?.data?.message ?? "Failed"), }); const createTicket = useMutation({ mutationFn: async () => { await api.post("/api/v1/tickets", { subject: newForm.subject, description: newForm.description, type: newForm.type, priority: newForm.priority, clientId: newForm.clientId || undefined, }); }, onSuccess: () => { toast.success("Ticket created!"); setShowCreate(false); setNewForm({ subject: "", description: "", type: "SUPPORT", priority: "NORMAL", clientSearch: "", clientId: "" }); qc.invalidateQueries({ queryKey: ["tickets"] }); }, onError: (e: any) => toast.error(e.response?.data?.message ?? "Failed to create ticket"), }); const tickets = data?.data ?? []; const total = data?.meta?.total ?? 0; const detail = ticketDetail ?? selected; const comments = (ticketDetail as any)?.messages ?? []; const nextStatus: Record = { OPEN: "IN_PROGRESS", IN_PROGRESS: "RESOLVED", RESOLVED: "CLOSED" }; return (

Tickets

{total} total tickets

{ setSearch(e.target.value); setPage(1); }} />
{statusFilters.map(s => ( ))}
{typeFilters.map(t => ( ))}
{isLoading ? ( Array.from({ length: 8 }).map((_, i) => ( )) ) : tickets.length === 0 ? ( } /> ) : tickets.map(t => ( setSelected(t)} className="cursor-pointer hover:bg-blue-50 transition-colors"> ))}
SubjectClientTypePriorityStatusCreated
{t.subject} {t.client ? `${t.client.firstName} ${t.client.lastName}` : "—"} {t.type} {t.priority} {t.status} {formatDate(t.createdAt)} View →
{total > 20 && (
Showing {(page - 1) * 20 + 1}–{Math.min(page * 20, total)} of {total}
)}
{/* Ticket Detail Modal */} setSelected(null)} title={`Ticket — ${detail?.subject ?? ""}`} className="max-w-2xl"> {detail && (
Client

{detail.client ? `${detail.client.firstName} ${detail.client.lastName}` : "—"}

Type

{detail.type}

Priority

{detail.priority}

Status

{detail.status}

Created

{formatDate(detail.createdAt)}

{detail.description &&
Description

{detail.description}

}
{/* Status actions */} {nextStatus[detail.status] && ( )} {/* Comments */}

Comments ({comments.length})

{comments.length === 0 ?

No comments yet.

: comments.map((c: TicketComment) => (

{(c.sender || c.author) ? `${(c.sender || c.author)!.firstName} ${(c.sender || c.author)!.lastName}` : "Staff"} · {formatDate(c.createdAt)}

{c.body}

)) }
setComment(e.target.value)} onKeyDown={e => e.key === "Enter" && !e.shiftKey && comment.trim() && addComment.mutate()} />
)}
{/* Create Ticket Modal */} setShowCreate(false)} title="New Ticket">
setNewForm(f => ({ ...f, subject: e.target.value }))} />