import { notFound, redirect } from "next/navigation"; import { getTranslations, getFormatter } from "next-intl/server"; import { getSessionUser } from "@/lib/session"; import { getSupportTicketById, listCommentsForTicket, } from "@/lib/db"; import { Card } from "@/components/ui/card"; import { BackLink } from "@/components/ui/back-link"; import { TicketStatusBadge } from "@/components/support/ticket-status-badge"; import { TicketCategoryLabel } from "@/components/support/ticket-category-label"; import { TicketThread } from "@/components/support/ticket-thread"; import { TicketAdminControls } from "@/components/support/ticket-admin-controls"; import { formatDateTime } from "@/lib/format"; /** * /support/[id] — single ticket detail. * * Same UI for customer and admin; admin gets an extra * `` block for changing status/category. The * customer side gets a "Close ticket" link if they want to mark it * resolved themselves. * * Authorization mirrors the API: customer sees their own; platform * admin sees any. 404 (not 403) when a customer accesses someone * else's ticket — don't leak existence. */ export default async function TicketDetailPage({ params, }: { params: Promise<{ id: string }>; }) { const user = await getSessionUser(); if (!user) redirect("/login"); const { id } = await params; const ticket = await getSupportTicketById(id); if (!ticket) notFound(); if (!user.isPlatform && ticket.zitadelUserId !== user.id) { notFound(); } const comments = await listCommentsForTicket(id); const t = await getTranslations("support"); const f = await getFormatter(); return (

{ticket.title}

· {t("openedBy", { name: ticket.contactName, when: formatDateTime(ticket.createdAt, f), })} · #{ticket.id.slice(0, 8)}
{/* Original ticket description, rendered as the first message in the thread. Visually distinct via the customer-author styling (handled inside ). */}
{ticket.contactName} {formatDateTime(ticket.createdAt, f)}
{ticket.description}
{user.isPlatform && ( )}
); }