1
0
Fork 0
polaris-task-force/src/components/frontend/helpdesk/TicketCard.tsx

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>
);
}