77 lines
2.9 KiB
TypeScript
77 lines
2.9 KiB
TypeScript
import Link from "next/link";
|
|
import type { Ticket, User } from "@/payload-types";
|
|
import { convertLexicalToPlaintext } from "@payloadcms/richtext-lexical/plaintext";
|
|
import {
|
|
Item,
|
|
ItemContent,
|
|
ItemDescription,
|
|
ItemFooter,
|
|
ItemHeader,
|
|
ItemTitle,
|
|
} from "@/components/ui/item";
|
|
import { TicketBadges, TicketCategoryBadge } from "@/components/frontend/helpdesk/TicketBadges";
|
|
|
|
interface TicketCardProps {
|
|
readonly ticket: Ticket;
|
|
}
|
|
|
|
type UserRelation = number | User | null | undefined;
|
|
|
|
function resolveUser(relation: UserRelation): User | null {
|
|
return typeof relation === "object" && relation ? relation : null;
|
|
}
|
|
|
|
function userName(relation: UserRelation, fallback: string): string {
|
|
const user = resolveUser(relation);
|
|
if (user) return user.payloadDisplayName || user.username || user.displayName;
|
|
if (typeof relation === "number") return `User #${relation}`;
|
|
return fallback;
|
|
}
|
|
|
|
function timeAgo(iso: string): string {
|
|
const diff = Date.now() - new Date(iso).getTime();
|
|
const minutes = Math.floor(diff / 60000);
|
|
if (minutes < 1) return "just now";
|
|
if (minutes < 60) return `${minutes}m ago`;
|
|
const hours = Math.floor(minutes / 60);
|
|
if (hours < 24) return `${hours}h ago`;
|
|
const days = Math.floor(hours / 24);
|
|
return `${days}d ago`;
|
|
}
|
|
|
|
export function TicketCard({ ticket }: TicketCardProps) {
|
|
const description = convertLexicalToPlaintext({ data: ticket.description });
|
|
const reporterName = userName(ticket.reporter, "Unknown reporter");
|
|
const assigneeName = userName(ticket.assignee, "Unassigned");
|
|
const hasAssignee = ticket.assignee !== null && ticket.assignee !== undefined;
|
|
|
|
return (
|
|
<Link href={`/helpdesk/${ticket.id}`} className="group block h-full">
|
|
<Item
|
|
variant="outline"
|
|
className="h-full min-h-48 transition-colors group-hover:bg-accent/50"
|
|
>
|
|
<ItemHeader className="items-start">
|
|
<TicketCategoryBadge category={ticket.category} />
|
|
<span className="font-mono text-[10px] text-muted-foreground">#{ticket.id}</span>
|
|
</ItemHeader>
|
|
<ItemContent className="w-full min-w-0 flex-1 gap-3">
|
|
<ItemTitle className="line-clamp-2 min-w-0 max-w-full text-base font-semibold">
|
|
{ticket.title}
|
|
</ItemTitle>
|
|
<ItemDescription>{description || "No description provided."}</ItemDescription>
|
|
<TicketBadges ticket={ticket} />
|
|
</ItemContent>
|
|
<ItemFooter className="w-full flex-wrap items-end">
|
|
<div className="flex min-w-0 flex-1 flex-wrap gap-x-3 gap-y-1 text-xs text-muted-foreground">
|
|
<span>opened by {reporterName}</span>
|
|
{hasAssignee && <span>assigned to {assigneeName}</span>}
|
|
</div>
|
|
<span className="shrink-0 text-right font-mono text-[10px] text-muted-foreground">
|
|
{timeAgo(ticket.updatedAt)}
|
|
</span>
|
|
</ItemFooter>
|
|
</Item>
|
|
</Link>
|
|
);
|
|
}
|