"use client"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { Badge } from "@/components/ui/badge"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { cn } from "@/lib/utils"; import { getInitials } from "@/lib/utils"; import { MoreHorizontal, Clock, CheckCircle2, AlertCircle, Loader2, Eye, Play, RefreshCw, Layers, CalendarDays, Trash2, } from "lucide-react"; import { updateTaskStatus, deleteTask } from "@/actions/tasks"; import { useToast } from "@/components/ui/use-toast"; import { TaskStatus, TaskType } from "@prisma/client"; import { formatDistanceToNow } from "date-fns"; export const TASK_STATUS_CONFIG: Record< TaskStatus, { label: string; color: string; icon: React.ElementType } > = { TODO: { label: "To Do", color: "bg-zinc-500/10 text-zinc-400 border-zinc-500/20", icon: Clock }, IN_PROGRESS: { label: "In Progress", color: "bg-blue-500/10 text-blue-400 border-blue-500/20", icon: Loader2 }, INTERNAL_REVIEW: { label: "Internal Review", color: "bg-purple-500/10 text-purple-400 border-purple-500/20", icon: Eye }, CLIENT_REVIEW: { label: "Client Review", color: "bg-amber-500/10 text-amber-400 border-amber-500/20", icon: AlertCircle }, CHANGES: { label: "Changes", color: "bg-orange-500/10 text-orange-400 border-orange-500/20", icon: RefreshCw }, DONE: { label: "Done", color: "bg-emerald-500/10 text-emerald-400 border-emerald-500/20", icon: CheckCircle2 }, }; export const TASK_TYPE_LABELS: Record = { TRACK: "Track", ROTO: "Roto", KEY: "Key", COMP: "Comp", FX: "FX", LIGHTING: "Lighting", RENDER: "Render", ANIMATION: "Animation", MODEL: "Model", TEXTURE: "Texture", RIG: "Rig", LOOKDEV: "Lookdev", GENERAL: "General", }; const PRIORITY_DOT: Record = { LOW: "bg-zinc-500", NORMAL: "bg-blue-500", HIGH: "bg-amber-500", URGENT: "bg-red-500", }; interface TaskCardProps { task: { id: string; title: string; type: TaskType; status: TaskStatus; priority: string; dueDate: Date | null; assignedArtist?: { id: string; name: string | null; email: string; image: string | null } | null; _count?: { versions: number }; versions?: { id: string; versionNumber: number; approvalStatus: string; createdAt: Date }[]; }; projectId: string; canManage?: boolean; } export function TaskCard({ task, projectId, canManage = false }: TaskCardProps) { const { toast } = useToast(); const router = useRouter(); const statusCfg = TASK_STATUS_CONFIG[task.status]; const StatusIcon = statusCfg.icon; const latestVersion = task.versions?.[0]; const isOverdue = task.dueDate && new Date(task.dueDate) < new Date() && task.status !== "DONE"; const handleStatusChange = async (newStatus: TaskStatus) => { try { await updateTaskStatus(task.id, newStatus); router.refresh(); } catch { toast({ title: "Failed to update status", variant: "destructive" }); } }; const handleDelete = async () => { if (!confirm("Delete this task? All versions and comments will be lost.")) return; try { await deleteTask(task.id); router.refresh(); toast({ title: "Task deleted" }); } catch { toast({ title: "Failed to delete task", variant: "destructive" }); } }; return (
{/* Priority dot */}
{task.title} {TASK_TYPE_LABELS[task.type]}
{statusCfg.label} {task._count && task._count.versions > 0 && ( v{latestVersion?.versionNumber ?? task._count.versions} )} {task.dueDate && ( {isOverdue ? "Overdue ยท " : ""} {formatDistanceToNow(new Date(task.dueDate), { addSuffix: true })} )}
{task.assignedArtist && ( {getInitials(task.assignedArtist.name ?? task.assignedArtist.email)} )} {canManage && ( Open task {(Object.keys(TASK_STATUS_CONFIG) as TaskStatus[]) .filter((s) => s !== task.status) .map((s) => ( handleStatusChange(s)}> Move to {TASK_STATUS_CONFIG[s].label} ))} Delete )}
); }