- Drag-and-drop widget arrangement (main two-column zone + sidebar) with per-widget span resize, persisted to Users.preferences.dashboard.layout via saveDashboardLayout (migration included) - New personal widgets: Wallet, Open Negotiations, My Tickets (View all), Recent Notifications (per-row mark as read) - Widgets that are empty or not visible to the viewer are omitted so they never render as empty drag targets - Registers both pending migrations and regenerates payload types/schema
79 lines
2.8 KiB
TypeScript
79 lines
2.8 KiB
TypeScript
import Link from "next/link";
|
|
import type { Ticket } from "@/payload-types";
|
|
import { Item, ItemContent, ItemHeader, ItemTitle } from "@/components/ui/item";
|
|
import { ChevronRightIcon, LifeBuoyIcon } from "lucide-react";
|
|
import {
|
|
CLOSING_STATUSES,
|
|
STATUS_STYLES,
|
|
statusLabel,
|
|
} from "@/lib/tickets/ticketMeta";
|
|
|
|
interface MyTicketsProps {
|
|
readonly tickets: Ticket[];
|
|
readonly openCount: number;
|
|
}
|
|
|
|
function timeAgo(dateStr: string): string {
|
|
const now = new Date();
|
|
const date = new Date(dateStr);
|
|
const diffMs = now.getTime() - date.getTime();
|
|
const diffMins = Math.floor(diffMs / 60000);
|
|
if (diffMins < 1) return "just now";
|
|
if (diffMins < 60) return `${diffMins}m ago`;
|
|
const diffHours = Math.floor(diffMins / 60);
|
|
if (diffHours < 24) return `${diffHours}h ago`;
|
|
const diffDays = Math.floor(diffHours / 24);
|
|
return `${diffDays}d ago`;
|
|
}
|
|
|
|
export function MyTickets({ tickets, openCount }: MyTicketsProps) {
|
|
return (
|
|
<Item variant="outline">
|
|
<ItemHeader>
|
|
<div className="flex items-center gap-2">
|
|
<LifeBuoyIcon className="size-4 text-muted-foreground" />
|
|
<ItemTitle>My Tickets</ItemTitle>
|
|
{openCount > 0 && (
|
|
<span className="text-xs text-muted-foreground">{openCount} open</span>
|
|
)}
|
|
</div>
|
|
<Link
|
|
href="/helpdesk"
|
|
className="text-xs text-muted-foreground transition-colors hover:text-primary"
|
|
>
|
|
View all
|
|
</Link>
|
|
</ItemHeader>
|
|
<ItemContent>
|
|
{tickets.length === 0 ? (
|
|
<p className="text-sm italic text-muted-foreground">No tickets yet.</p>
|
|
) : (
|
|
<div className="flex flex-col gap-1">
|
|
{tickets.map((ticket) => (
|
|
<Link
|
|
key={ticket.id}
|
|
href={`/helpdesk/${ticket.id}`}
|
|
className="group flex items-center gap-3 rounded-md p-2 transition-colors hover:bg-muted/50"
|
|
>
|
|
<span
|
|
className={`shrink-0 rounded border px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wider ${STATUS_STYLES[ticket.status]}`}
|
|
>
|
|
{statusLabel(ticket.status)}
|
|
</span>
|
|
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
|
<span className="truncate text-sm font-medium transition-colors group-hover:text-primary">
|
|
{ticket.title}
|
|
</span>
|
|
<span className="text-xs text-muted-foreground">
|
|
Updated {timeAgo(ticket.updatedAt)}
|
|
</span>
|
|
</div>
|
|
<ChevronRightIcon className="size-3 shrink-0 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100" />
|
|
</Link>
|
|
))}
|
|
</div>
|
|
)}
|
|
</ItemContent>
|
|
</Item>
|
|
);
|
|
}
|