173 lines
7.6 KiB
TypeScript
173 lines
7.6 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useEffect } from "react"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "@/components/ui/table"
|
|
import { Badge } from "@/components/ui/badge"
|
|
import { Ticket, ticketsApi } from "@/lib/api"
|
|
import { toast } from "sonner"
|
|
import Link from "next/link"
|
|
import { MessageSquare, Trash2, Eye } from "lucide-react"
|
|
import { format } from "date-fns"
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog"
|
|
|
|
export default function AdminTicketsPage() {
|
|
const [tickets, setTickets] = useState<Ticket[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [deleteId, setDeleteId] = useState<string | null>(null)
|
|
|
|
const fetchTickets = async () => {
|
|
try {
|
|
setLoading(true)
|
|
const data = await ticketsApi.listAll()
|
|
setTickets(data)
|
|
} catch (error) {
|
|
console.error("Failed to fetch tickets", error)
|
|
toast.error("Failed to fetch tickets")
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
fetchTickets()
|
|
}, [])
|
|
|
|
const handleDelete = async () => {
|
|
if (!deleteId) return
|
|
try {
|
|
await ticketsApi.delete(deleteId)
|
|
toast.success("Ticket deleted")
|
|
setDeleteId(null)
|
|
fetchTickets()
|
|
} catch (error) {
|
|
console.error("Failed to delete ticket", error)
|
|
toast.error("Failed to delete ticket")
|
|
}
|
|
}
|
|
|
|
const getStatusBadge = (status: string) => {
|
|
switch (status) {
|
|
case "open": return <Badge variant="default" className="bg-green-500">Open</Badge>
|
|
case "in_progress": return <Badge variant="secondary" className="bg-yellow-500 text-white">In Progress</Badge>
|
|
case "closed": return <Badge variant="outline">Closed</Badge>
|
|
default: return <Badge variant="outline">{status}</Badge>
|
|
}
|
|
}
|
|
|
|
const getPriorityBadge = (priority: string) => {
|
|
switch (priority) {
|
|
case "high": return <Badge variant="destructive">High</Badge>
|
|
case "medium": return <Badge variant="secondary">Medium</Badge>
|
|
case "low": return <Badge variant="outline">Low</Badge>
|
|
default: return <Badge variant="outline">{priority}</Badge>
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex justify-between items-center">
|
|
<div>
|
|
<h1 className="text-3xl font-bold tracking-tight">Support Tickets (Admin)</h1>
|
|
<p className="text-muted-foreground">Manage all user support tickets.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>All Tickets</CardTitle>
|
|
<CardDescription>A list of all support tickets from users.</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>ID</TableHead>
|
|
<TableHead>Subject</TableHead>
|
|
<TableHead>User ID</TableHead>
|
|
<TableHead>Status</TableHead>
|
|
<TableHead>Priority</TableHead>
|
|
<TableHead>Created</TableHead>
|
|
<TableHead className="text-right">Actions</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{loading ? (
|
|
<TableRow>
|
|
<TableCell colSpan={7} className="text-center py-4">Loading...</TableCell>
|
|
</TableRow>
|
|
) : tickets.length === 0 ? (
|
|
<TableRow>
|
|
<TableCell colSpan={7} className="text-center py-4">No tickets found</TableCell>
|
|
</TableRow>
|
|
) : (
|
|
tickets.map((ticket) => (
|
|
<TableRow key={ticket.id}>
|
|
<TableCell className="font-mono text-xs max-w-[80px] truncate" title={ticket.id}>
|
|
{ticket.id.substring(0, 8)}...
|
|
</TableCell>
|
|
<TableCell className="font-medium">{ticket.subject}</TableCell>
|
|
<TableCell className="text-xs text-muted-foreground font-mono" title={ticket.userId}>
|
|
{ticket.userId}
|
|
</TableCell>
|
|
<TableCell>{getStatusBadge(ticket.status)}</TableCell>
|
|
<TableCell>{getPriorityBadge(ticket.priority)}</TableCell>
|
|
<TableCell>{format(new Date(ticket.createdAt), "MMM d, yyyy")}</TableCell>
|
|
<TableCell className="text-right">
|
|
<div className="flex justify-end gap-2">
|
|
<Link href={`./tickets/${ticket.id}`}>
|
|
<Button variant="ghost" size="sm">
|
|
<Eye className="h-4 w-4 mr-2" />
|
|
View
|
|
</Button>
|
|
</Link>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => setDeleteId(ticket.id)}
|
|
className="text-destructive hover:text-destructive"
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
))
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Dialog open={!!deleteId} onOpenChange={(open) => !open && setDeleteId(null)}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Delete Ticket</DialogTitle>
|
|
<DialogDescription>
|
|
Are you sure you want to delete this ticket? This action cannot be undone.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => setDeleteId(null)}>Cancel</Button>
|
|
<Button variant="destructive" onClick={handleDelete}>Delete</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
)
|
|
}
|