import { AlertCircle, CheckCircle2, Clock, Loader2, Ticket, } from "lucide-react"; import Link from "next/link"; import { redirect } from "next/navigation"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { prisma } from "@/lib/prisma"; const STATUS_CONFIG = { open: { label: "Open", icon: AlertCircle, class: "bg-[var(--admin-info-subtle)] text-[var(--admin-info)]", }, in_progress: { label: "In Progress", icon: Loader2, class: "bg-[var(--admin-warning-subtle)] text-[var(--admin-warning)]", }, waiting: { label: "Waiting", icon: Clock, class: "bg-[var(--admin-warning-subtle)] text-[var(--admin-warning)]", }, closed: { label: "Closed", icon: CheckCircle2, class: "bg-muted text-muted-foreground", }, } as const; const PRIORITY_COLORS = { low: "", normal: "text-[var(--admin-info)]", high: "text-[var(--admin-warning)]", urgent: "text-[var(--admin-error)] font-bold", }; interface PageProps { searchParams: Promise<{ status?: string; page?: string }>; } export default async function AdminTicketsPage({ searchParams }: PageProps) { const { session, permissions } = await getAdminContext(); if (!canAccess(permissions, PERMS.TICKETS_VIEW, session.user.rank)) { redirect("/admin"); } const sp = await searchParams; const statusFilter = sp.status || "active"; const page = Math.max(1, Number(sp.page) || 1); const perPage = 20; const where = statusFilter === "active" ? { status: { not: "closed" } } : statusFilter === "all" ? {} : { status: statusFilter }; const [tickets, total, stats] = await Promise.all([ prisma.websiteTicket.findMany({ where, orderBy: [{ createdAt: "desc" }], skip: (page - 1) * perPage, take: perPage, include: { creator: { select: { id: true, username: true } }, assignee: { select: { id: true, username: true } }, _count: { select: { messages: true } }, }, }), prisma.websiteTicket.count({ where }), Promise.all([ prisma.websiteTicket.count({ where: { status: "open" } }), prisma.websiteTicket.count({ where: { status: "in_progress" } }), prisma.websiteTicket.count({ where: { status: "waiting" } }), prisma.websiteTicket.count({ where: { status: "closed" } }), ]), ]); const [openCount, inProgressCount, waitingCount, closedCount] = stats; const totalPages = Math.ceil(total / perPage); return (

Support Tickets

Manage user support requests

{/* Stats */}
Open
{openCount}
In Progress
{inProgressCount}
Waiting
{waitingCount}
Closed
{closedCount}
{/* Filter tabs */}
{[ { key: "active", label: "Active" }, { key: "open", label: "Open" }, { key: "in_progress", label: "In Progress" }, { key: "waiting", label: "Waiting" }, { key: "closed", label: "Closed" }, { key: "all", label: "All" }, ].map(({ key, label }) => ( {label} ))}
{/* Ticket list */}
Ticket Status Priority Assigned Date
{tickets.length === 0 ? (
No tickets found.
) : ( tickets.map((ticket) => { const status = STATUS_CONFIG[ticket.status as keyof typeof STATUS_CONFIG] ?? STATUS_CONFIG.open; const StatusIcon = status.icon; return (
#{ticket.id} {ticket.subject}
by {ticket.creator.username} ·{" "} {ticket._count.messages} msg · {ticket.category}
{status.label} {ticket.priority} {ticket.assignee?.username ?? "—"} {new Date(ticket.createdAt).toLocaleDateString()} ); }) )}
{/* Pagination */} {totalPages > 1 && (
{page > 1 && ( Previous )} Page {page} of {totalPages} {page < totalPages && ( Next )}
)}
); }