"use client"; import { useState } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { RefreshCw, Ticket, Plus, Send, RotateCcw, User, MapPin, Zap } from "lucide-react"; import { useAuthStore } from "@/lib/auth-store"; 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"]; // Previous status mapping (revert flow) const prevStatus: Record = { CLOSED: "RESOLVED", RESOLVED: "IN_PROGRESS", IN_PROGRESS: "OPEN", }; export default function TicketsPage() { const qc = useQueryClient(); const user = useAuthStore((s) => s.user); const [search, setSearch] = useState(""); const [statusFilter, setStatusFilter] = useState(""); const [typeFilter, setTypeFilter] = useState(""); const [assignedToMe, setAssignedToMe] = useState(false); 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, assignedToMe, 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); if (assignedToMe && user?.id) params.set("assignedToId", user.id); 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 activateClient = useMutation({ mutationFn: async (clientId: string) => { await api.patch(`/api/v1/clients/${clientId}`, { isActive: true }); }, onSuccess: () => { toast.success("Client activated! ๐ŸŽ‰"); refetch(); refetchDetail(); }, onError: (e: any) => toast.error(e.response?.data?.message ?? "Failed to activate client"), }); const revertStatus = useMutation({ mutationFn: async ({ id, status }: { id: string; status: string }) => { await api.patch(`/api/v1/tickets/${id}`, { status }); }, onSuccess: () => { toast.success("Status reverted"); refetch(); refetchDetail(); qc.invalidateQueries({ queryKey: ["ticket"] }); }, onError: (e: any) => toast.error(e.response?.data?.message ?? "Failed"), }); 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}

} {/* Installation: show Google Maps link using client address */} {detail.type === "INSTALLATION" && detail.client && ( )}
{/* Status actions */}
{nextStatus[detail.status] && ( )} {prevStatus[detail.status] && ( )} {/* Installation ticket RESOLVED โ†’ show Activate Client button */} {detail.type === "INSTALLATION" && detail.status === "RESOLVED" && detail.clientId && ( )}
{/* 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 }))} />