"use client"; import { useState } from "react"; import { useRouter } from "next/navigation"; import { useTranslations, useFormatter } from "next-intl"; import { Card } from "@/components/ui/card"; import { formatDateTime } from "@/lib/format"; import type { SupportTicketComment, SupportTicketStatus } from "@/types"; interface Props { ticketId: string; ticketStatus: SupportTicketStatus; comments: SupportTicketComment[]; isPlatform: boolean; /** True when the viewer is the customer who created this ticket. */ isOwnTicket: boolean; } /** * Thread of comments + reply box. Customer-side viewers see a * "Close ticket" button as well, mapping to the customer-self-close * path on the PATCH endpoint. * * Reply submission: posts the comment, then router.refresh() so the * server-rendered page re-fetches and renders the new entry. Avoids * duplicating the comment-rendering logic on the client. * * Empty body submissions are blocked at HTML level (required) AND * by the API; we trust both layers. */ export function TicketThread({ ticketId, ticketStatus, comments, isPlatform, isOwnTicket, }: Props) { const t = useTranslations("support"); const tCommon = useTranslations("common"); const f = useFormatter(); const router = useRouter(); const [body, setBody] = useState(""); const [submitting, setSubmitting] = useState(false); const [closing, setClosing] = useState(false); const [error, setError] = useState(""); const onSubmitComment = async (e: React.FormEvent) => { e.preventDefault(); setSubmitting(true); setError(""); try { const res = await fetch( `/api/support/tickets/${encodeURIComponent(ticketId)}/comments`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ body }), } ); if (!res.ok) { const data = await res.json().catch(() => ({})); throw new Error(data.error || t("commentFailed")); } setBody(""); router.refresh(); } catch (e: any) { setError(e.message); } finally { setSubmitting(false); } }; // Customer-self-close: confirms because it's a state change, then // hits PATCH with status=resolved. The API allows this for // own-ticket regardless of role; the button only shows when the // ticket is in a non-resolved state. const onCustomerClose = async () => { if (!confirm(t("confirmClose"))) return; setClosing(true); setError(""); try { const res = await fetch( `/api/support/tickets/${encodeURIComponent(ticketId)}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: "resolved" }), } ); if (!res.ok) { const data = await res.json().catch(() => ({})); throw new Error(data.error || t("closeFailed")); } router.refresh(); } catch (e: any) { setError(e.message); } finally { setClosing(false); } }; const isResolved = ticketStatus === "resolved"; const canCustomerClose = isOwnTicket && !isResolved; return ( <> {comments.map((c) => ( {c.authorName} {c.authorKind === "admin" && ( {t("authorTagAdmin")} )} {formatDateTime(c.createdAt, f)} {c.body} ))} {isResolved && ( {t("resolvedBanner")} )} {/* Reply box. Visible regardless of status — customer can reply even on a resolved ticket (which auto-reopens it server-side). The semantic is "reply means the ticket is alive again", which is friendlier than blocking the reply. */} {t("replyLabel")} setBody(e.target.value)} placeholder={ isResolved && isOwnTicket ? t("replyPlaceholderReopen") : t("replyPlaceholder") } className="w-full px-3 py-2 rounded-lg border border-border bg-surface-2 text-text-primary text-sm focus:outline-none focus:border-text-secondary" /> {error && ( {error} )} {canCustomerClose ? ( {closing ? tCommon("loading") : t("closeTicket")} ) : ( )} {submitting ? tCommon("loading") : t("sendReply")} > ); }
{t("resolvedBanner")}