"use client"; import { useMemo, useState } from "react"; import { convertLexicalToPlaintext } from "@payloadcms/richtext-lexical/plaintext"; import type { Ticket } from "@/payload-types"; import { Badge } from "@/components/ui/badge"; import { Item, ItemContent, ItemDescription, ItemMedia, ItemTitle } from "@/components/ui/item"; import { Input } from "@/components/ui/input"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { CreateTicketDialog } from "@/components/frontend/helpdesk/CreateTicketDialog"; import { TicketCard } from "@/components/frontend/helpdesk/TicketCard"; import { CLOSING_STATUSES, STATUS_STYLES, type TicketStatus } from "@/lib/tickets/ticketMeta"; import { InboxIcon, SearchIcon } from "lucide-react"; import { cn } from "@/lib/utils"; type TicketFilter = "all" | "open" | "resolved" | "closed" | "unassigned"; interface TicketsViewProps { readonly tickets: readonly Ticket[]; readonly currentUserId: number; readonly isStaff: boolean; } const OPEN_STATUSES: readonly TicketStatus[] = [ "open", "in_review", "planning", "in_progress", "waiting_on_reporter", "in_testing", ]; const FILTER_OPTIONS = [ { label: "All", value: "all" }, { label: "Open", value: "open" }, { label: "Resolved", value: "resolved" }, { label: "Closed", value: "closed" }, { label: "Unassigned", value: "unassigned" }, ] as const satisfies readonly { label: string; value: TicketFilter }[]; function isTicketFilter(value: string): value is TicketFilter { return FILTER_OPTIONS.some((option) => option.value === value); } function isOpenTicket(ticket: Ticket): boolean { return OPEN_STATUSES.includes(ticket.status); } function isUnassignedTicket(ticket: Ticket): boolean { return ( (ticket.assignee === null || ticket.assignee === undefined) && !CLOSING_STATUSES.includes(ticket.status) ); } export function TicketsView({ tickets, currentUserId: _currentUserId, isStaff }: TicketsViewProps) { const [activeFilter, setActiveFilter] = useState("all"); const [search, setSearch] = useState(""); const counts = useMemo>( () => ({ all: tickets.length, open: tickets.filter(isOpenTicket).length, resolved: tickets.filter((ticket) => ticket.status === "resolved").length, closed: tickets.filter((ticket) => ticket.status === "canceled" || ticket.status === "closed") .length, unassigned: tickets.filter(isUnassignedTicket).length, }), [tickets], ); const filteredTickets = useMemo(() => { const query = search.trim().toLowerCase(); return tickets.filter((ticket) => { const matchesFilter = activeFilter === "all" || (activeFilter === "open" && isOpenTicket(ticket)) || (activeFilter === "resolved" && ticket.status === "resolved") || (activeFilter === "closed" && (ticket.status === "canceled" || ticket.status === "closed")) || (activeFilter === "unassigned" && isUnassignedTicket(ticket)); if (!matchesFilter) return false; if (!query) return true; const description = convertLexicalToPlaintext({ data: ticket.description }); return `${ticket.title} ${description}`.toLowerCase().includes(query); }); }, [activeFilter, search, tickets]); const visibleFilters = isStaff ? FILTER_OPTIONS : FILTER_OPTIONS.filter((option) => option.value !== "unassigned"); // Helper to derive a small per-filter class from STATUS_STYLES. // STATUS_STYLES is keyed by TicketStatus and contains a space-separated // class string (e.g. "bg-emerald-50 text-emerald-700"). The original // implementation attempted to build a complex mapping but had type errors // and didn't return values from the map. Keep this simple and typed: pick // the second token (usually the foreground color) when available. const statusStyles = STATUS_STYLES as Record; function classForFilter(filter: TicketFilter): string { switch (filter) { case "open": return (statusStyles.open ?? "").split(" ")[1] ?? ""; case "resolved": return (statusStyles.resolved ?? "").split(" ")[1] ?? ""; case "closed": // prefer explicit closed style, fall back to canceled if present return (statusStyles.closed ?? statusStyles.canceled ?? "").split(" ")[1] ?? ""; case "unassigned": return "text-muted-foreground"; case "all": default: return ""; } } return (

{isStaff ? "Ticket queue" : "Your tickets"}

{tickets.length} total

{isStaff ? "Review incoming requests and keep members updated." : "Keep an eye on your open requests."}

{ if (isTicketFilter(value)) setActiveFilter(value); }} > {visibleFilters.map((option) => ( {option.label} {counts[option.value]} ))}
setSearch(event.target.value)} />
{filteredTickets.length === 0 ? ( {tickets.length === 0 ? "No tickets yet" : "No matching tickets"} {tickets.length === 0 ? "Create a ticket to send a request to the helpdesk." : "Try a different filter or search phrase."} ) : (
{filteredTickets.map((ticket) => ( ))}
)}
); }