103 lines
3.7 KiB
TypeScript
103 lines
3.7 KiB
TypeScript
import { redirect } from "next/navigation";
|
|
import Link from "next/link";
|
|
import { getTranslations, getFormatter } from "next-intl/server";
|
|
import { getSessionUser } from "@/lib/session";
|
|
import {
|
|
listSupportTicketsForUser,
|
|
listAllSupportTickets,
|
|
} from "@/lib/db";
|
|
import { Card } from "@/components/ui/card";
|
|
import { formatRelative } from "@/lib/format";
|
|
import { TicketStatusBadge } from "@/components/support/ticket-status-badge";
|
|
import { TicketCategoryLabel } from "@/components/support/ticket-category-label";
|
|
|
|
/**
|
|
* /support — ticket list.
|
|
*
|
|
* Customers see their own tickets only (per Feature 5: per-user
|
|
* scope, NOT per-org). Platform admins see the global queue. Same
|
|
* UI shell, different list source — the rendering logic is
|
|
* identical because the per-row data is the same shape.
|
|
*
|
|
* Sorting: newest activity first (the DB query already orders by
|
|
* updated_at DESC). Open tickets bubble to the top by virtue of
|
|
* having recent activity, but we don't sort by status; that's a
|
|
* filter the admin can add later if the queue grows.
|
|
*/
|
|
export async function generateMetadata() {
|
|
const t = await getTranslations("common");
|
|
return { title: t("support") };
|
|
}
|
|
|
|
export default async function SupportListPage() {
|
|
const user = await getSessionUser();
|
|
if (!user) redirect("/login");
|
|
const t = await getTranslations("support");
|
|
const f = await getFormatter();
|
|
|
|
const tickets = user.isPlatform
|
|
? await listAllSupportTickets()
|
|
: await listSupportTicketsForUser(user.id);
|
|
|
|
return (
|
|
<main className="max-w-5xl mx-auto px-6 py-8">
|
|
<div className="mb-8 animate-in flex items-end justify-between">
|
|
<div>
|
|
<h1 className="font-display text-2xl font-semibold accent-rule">
|
|
{user.isPlatform ? t("titleAdmin") : t("title")}
|
|
</h1>
|
|
<p className="text-sm text-text-secondary mt-3">
|
|
{user.isPlatform ? t("subtitleAdmin") : t("subtitle")}
|
|
</p>
|
|
</div>
|
|
{!user.isPlatform && (
|
|
<Link
|
|
href="/support/new"
|
|
className="text-sm font-medium px-4 py-2 rounded-lg bg-accent text-surface-0 hover:bg-accent/90 transition-colors"
|
|
>
|
|
{t("newTicket")}
|
|
</Link>
|
|
)}
|
|
</div>
|
|
|
|
{tickets.length === 0 ? (
|
|
<Card className="animate-in animate-in-delay-1">
|
|
<p className="text-sm text-text-secondary text-center py-6">
|
|
{user.isPlatform ? t("emptyAdmin") : t("empty")}
|
|
</p>
|
|
</Card>
|
|
) : (
|
|
<div className="space-y-2 animate-in animate-in-delay-1">
|
|
{tickets.map((tk) => (
|
|
<Link
|
|
key={tk.id}
|
|
href={`/support/${tk.id}`}
|
|
className="block rounded-xl border border-border bg-surface-1 p-4 hover:border-text-secondary transition-colors"
|
|
>
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div className="min-w-0">
|
|
<div className="font-medium text-text-primary truncate">
|
|
{tk.title}
|
|
</div>
|
|
<div className="text-xs text-text-muted mt-1 flex items-center gap-2">
|
|
<TicketCategoryLabel category={tk.category} />
|
|
<span>·</span>
|
|
<span>{formatRelative(tk.updatedAt, f)}</span>
|
|
{user.isPlatform && (
|
|
<>
|
|
<span>·</span>
|
|
<span className="font-mono">{tk.contactEmail}</span>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<TicketStatusBadge status={tk.status} />
|
|
</div>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
)}
|
|
</main>
|
|
);
|
|
}
|