"use client"; import { useState } from "react"; import Image from "next/image"; import Link from "next/link"; import { toast } from "sonner"; import { ExternalLink, FileText, MessageSquare } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Textarea } from "@/components/ui/textarea"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; type TicketStatus = "open" | "triaged" | "closed"; type Attachment = { id: string; contentType: string; url: string }; type Comment = { id: string; authorType: "user" | "admin" | "gitea"; body: string; createdAt: string }; const AUTHOR_LABEL: Record = { user: "User", admin: "Admin", gitea: "Gitea" }; type Ticket = { id: string; userEmail: string; username: string | null; type: string; title: string; description: string; status: TicketStatus; giteaIssueUrl: string | null; giteaError: string | null; createdAt: string; attachments: Attachment[]; }; function isImage(contentType: string) { return contentType.startsWith("image/"); } function formatDateTime(iso: string) { return new Date(iso).toLocaleString(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", }); } const statusVariant: Record = { open: "default", triaged: "secondary", closed: "outline", }; export function AdminSupportManager({ initialTickets }: { initialTickets: Ticket[] }) { const [tickets, setTickets] = useState(initialTickets); const [retrying, setRetrying] = useState(null); const [expandedId, setExpandedId] = useState(null); const [comments, setComments] = useState>({}); const [commentDraft, setCommentDraft] = useState(""); const [sendingComment, setSendingComment] = useState(false); async function toggleConversation(ticketId: string) { if (expandedId === ticketId) { setExpandedId(null); return; } setExpandedId(ticketId); setCommentDraft(""); if (!comments[ticketId]) { try { const res = await fetch(`/api/v1/admin/support/${ticketId}/comments`); if (res.ok) { const data = (await res.json()) as Comment[]; setComments((prev) => ({ ...prev, [ticketId]: data })); } } catch { // leave the section empty — the send box below still works } } } async function sendComment(ticketId: string) { const body = commentDraft.trim(); if (!body) return; setSendingComment(true); try { const res = await fetch(`/api/v1/admin/support/${ticketId}/comments`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ body }), }); if (!res.ok) throw new Error(); const comment = (await res.json()) as Comment; setComments((prev) => ({ ...prev, [ticketId]: [...(prev[ticketId] ?? []), comment] })); setCommentDraft(""); } catch { toast.error("Failed to send comment"); } finally { setSendingComment(false); } } async function handleStatusChange(id: string, status: TicketStatus) { try { const res = await fetch(`/api/v1/admin/support/${id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status }), }); if (!res.ok) throw new Error(); setTickets((prev) => prev.map((t) => (t.id === id ? { ...t, status } : t))); } catch { toast.error("Failed to update status"); } } async function handleRetryGitea(id: string) { setRetrying(id); try { const res = await fetch(`/api/v1/admin/support/${id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ retryGitea: true }), }); if (!res.ok) throw new Error(); // Re-fetch the single ticket's fields isn't wired up server-side, so // reload the list to pick up the new giteaIssueUrl/giteaError. const listRes = await fetch("/api/v1/admin/support"); if (listRes.ok) { const data = (await listRes.json()) as Ticket[]; setTickets(data); } toast.success("Retried Gitea issue creation"); } catch { toast.error("Retry failed"); } finally { setRetrying(null); } } if (tickets.length === 0) { return

No support tickets yet.

; } return (
{tickets.map((ticket) => (

{ticket.title}

{ticket.username ? `@${ticket.username}` : ticket.userEmail} · {formatDateTime(ticket.createdAt)}

{ticket.description}

{ticket.attachments.length > 0 && (
{ticket.attachments.map((a) => ( {isImage(a.contentType) ? ( Attachment ) : (
)} ))}
)}
{ticket.type} {ticket.status} {ticket.giteaIssueUrl ? ( View Gitea issue ) : ( <> {ticket.giteaError && ( Gitea issue failed )} )}
{expandedId === ticket.id && (
{(comments[ticket.id] ?? []).length === 0 ? (

No comments yet.

) : (
{(comments[ticket.id] ?? []).map((c) => (
{AUTHOR_LABEL[c.authorType]}

{c.body}

))}
)}