import { and, asc, count, desc, eq, inArray, like, ne, or, type SQL, } from "drizzle-orm"; import { alias } from "drizzle-orm/mysql-core"; import { getTranslations } from "next-intl/server"; import { type TicketRow, TicketsTable, } from "@/app/admin/tickets/tickets-table"; import { StatusCard } from "@/components/admin/dashboard"; import { TicketQueueBanner } from "@/components/admin/ticket-queue-banner"; import { requireModPermission } from "@/lib/admin/guard"; import { fetchTicketQueueOpenCounts } from "@/lib/admin/ticket-queue-counts"; import { calcPagination, parseListParams } from "@/lib/admin-helpers"; import { db, User, WebsiteTicket, WebsiteTicketMessage } from "@/lib/db"; import { formatDate } from "@/lib/format-date"; import { PERMS } from "@/lib/permissions"; const Creator = alias(User, "ticket_creator"); const Assignee = alias(User, "ticket_assignee"); const SORT_COLS = { subject: WebsiteTicket.subject, status: WebsiteTicket.status, priority: WebsiteTicket.priority, assignee: WebsiteTicket.assigneeId, date: WebsiteTicket.createdAt, id: WebsiteTicket.id, } as const; export default async function ModTicketsDeskPage({ searchParams, }: { searchParams: Promise>; }) { await requireModPermission([PERMS.MOD_TICKETS_VIEW, PERMS.TICKETS_VIEW]); const t = await getTranslations("pages.admin.tickets"); const raw = await searchParams; const parsed = parseListParams(new URLSearchParams(raw)); const statusFilter = raw.filter_status || raw.status || "active"; const conditions: SQL[] = []; if (statusFilter === "active") { conditions.push(ne(WebsiteTicket.status, "closed")); } else if (statusFilter && statusFilter !== "all") { conditions.push(eq(WebsiteTicket.status, statusFilter)); } if (parsed.search.trim()) { const q = parsed.search.trim(); const asId = Number(q); const ors: SQL[] = [ like(WebsiteTicket.subject, `%${q}%`), like(WebsiteTicket.category, `%${q}%`), like(Creator.username, `%${q}%`), like(Assignee.username, `%${q}%`), ]; if (Number.isFinite(asId) && asId > 0) { ors.push(eq(WebsiteTicket.id, asId)); } conditions.push(or(...ors)!); } const where = conditions.length > 0 ? and(...conditions) : undefined; const sortKey = parsed.sort && parsed.sort in SORT_COLS ? (parsed.sort as keyof typeof SORT_COLS) : "date"; const sortCol = SORT_COLS[sortKey]; const finalOrder = parsed.sort && parsed.sort in SORT_COLS ? parsed.order === "asc" ? asc(sortCol) : desc(sortCol) : desc(WebsiteTicket.createdAt); const [total, openCount, inProgressCount, waitingCount, closedCount, queues] = await Promise.all([ db .select({ total: count() }) .from(WebsiteTicket) .leftJoin(Creator, eq(WebsiteTicket.creatorId, Creator.id)) .leftJoin(Assignee, eq(WebsiteTicket.assigneeId, Assignee.id)) .where(where) .then((rows) => rows[0]?.total ?? 0) .catch(() => 0), db .select({ total: count() }) .from(WebsiteTicket) .where(eq(WebsiteTicket.status, "open")) .then((rows) => rows[0]?.total ?? 0) .catch(() => 0), db .select({ total: count() }) .from(WebsiteTicket) .where(eq(WebsiteTicket.status, "in_progress")) .then((rows) => rows[0]?.total ?? 0) .catch(() => 0), db .select({ total: count() }) .from(WebsiteTicket) .where(eq(WebsiteTicket.status, "waiting")) .then((rows) => rows[0]?.total ?? 0) .catch(() => 0), db .select({ total: count() }) .from(WebsiteTicket) .where(eq(WebsiteTicket.status, "closed")) .then((rows) => rows[0]?.total ?? 0) .catch(() => 0), fetchTicketQueueOpenCounts(), ]); const pagination = calcPagination(total, parsed.page, parsed.perPage); const tickets = await db .select({ id: WebsiteTicket.id, subject: WebsiteTicket.subject, category: WebsiteTicket.category, status: WebsiteTicket.status, priority: WebsiteTicket.priority, creatorId: WebsiteTicket.creatorId, createdAt: WebsiteTicket.createdAt, creatorUsername: Creator.username, assigneeUsername: Assignee.username, }) .from(WebsiteTicket) .leftJoin(Creator, eq(WebsiteTicket.creatorId, Creator.id)) .leftJoin(Assignee, eq(WebsiteTicket.assigneeId, Assignee.id)) .where(where) .orderBy(finalOrder) .offset(pagination.offset) .limit(pagination.perPage) .catch(() => []); const ticketIds = tickets.map((ticket) => ticket.id); const msgRows = ticketIds.length > 0 ? await db .select({ ticketId: WebsiteTicketMessage.ticketId, total: count(), }) .from(WebsiteTicketMessage) .where(inArray(WebsiteTicketMessage.ticketId, ticketIds)) .groupBy(WebsiteTicketMessage.ticketId) .catch(() => []) : []; const msgMap = new Map(msgRows.map((m) => [m.ticketId, m.total])); const rows: TicketRow[] = tickets.map((ticket) => ({ id: ticket.id, subject: ticket.subject, creator: ticket.creatorUsername ?? "—", creatorId: ticket.creatorId, category: ticket.category, messages: msgMap.get(ticket.id) ?? 0, status: ticket.status, priority: ticket.priority, assignee: ticket.assigneeUsername ?? "—", date: formatDate(ticket.createdAt, "date"), })); return (
0 ? "warn" : "neutral"} icon="⏳" /> 0 ? "warn" : "neutral"} icon="🕒" />
{total === 0 && !parsed.search && statusFilter === "active" ? (
{t("noTickets")}
) : ( )}
); }