"use client"; import { useState, useRef } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { cn } from "@/lib/utils"; import { getInitials, formatRelativeDate, formatFileSize } from "@/lib/utils"; import { updateTask, updateTaskStatus } from "@/actions/tasks"; import { useToast } from "@/components/ui/use-toast"; import { TaskStatus, TaskType } from "@prisma/client"; import { formatDistanceToNow, format } from "date-fns"; import { ArrowLeft, CalendarDays, Clock, Film, Layers, Play, Upload, ChevronDown, CheckCircle2, Eye, RefreshCw, Loader2, AlertCircle, User, ExternalLink, Trash2, } from "lucide-react"; import { TASK_STATUS_CONFIG, TASK_TYPE_LABELS } from "@/components/tasks/TaskCard"; import { VersionUpload } from "@/components/versions/VersionUpload"; import { deleteVersion } from "@/actions/versions"; import type { CommentWithReplies } from "@/types"; import { CommentPanel } from "@/components/comments/CommentPanel"; const PRIORITY_COLORS: Record = { LOW: "text-zinc-400", NORMAL: "text-blue-400", HIGH: "text-amber-400", URGENT: "text-red-400", }; const APPROVAL_STYLES: Record = { PENDING_REVIEW: "bg-amber-500/10 text-amber-400 border-amber-500/20", APPROVED: "bg-emerald-500/10 text-emerald-400 border-emerald-500/20", REJECTED: "bg-red-500/10 text-red-400 border-red-500/20", NEEDS_CHANGES: "bg-orange-500/10 text-orange-400 border-orange-500/20", }; interface Artist { id: string; name: string | null; email: string; image: string | null; role: string; } interface Version { id: string; versionNumber: number; fileName: string; fileSize: bigint | null; fps: number; approvalStatus: string; isLatest: boolean; isClientVisible: boolean; createdAt: Date; notes: string | null; artist: { id: string; name: string | null; image: string | null; email: string } | null; _count: { comments: number }; approvals: { id: string; user: { id: string; name: string | null; role: string } }[]; comments: CommentWithReplies[]; } interface Task { id: string; title: string; description: string | null; type: TaskType; status: TaskStatus; priority: string; dueDate: Date | null; estimatedHours: number | null; projectId: string; assignedArtistId: string | null; assignedArtist: Artist | null; createdBy: { id: string; name: string | null; email: string }; project: { id: string; name: string; code: string }; shot: { id: string; shotCode: string; projectId: string } | null; asset: { id: string; assetCode: string; name: string; projectId: string } | null; versions: Version[]; } interface TaskDetailClientProps { task: Task; artists: Artist[]; currentUserId: string; canManage: boolean; canUpload: boolean; } export function TaskDetailClient({ task, artists, currentUserId, canManage, canUpload, }: TaskDetailClientProps) { const router = useRouter(); const { toast } = useToast(); const [showUpload, setShowUpload] = useState(false); const [updatingStatus, setUpdatingStatus] = useState(false); 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 parentLink = task.shot ? { label: task.shot.shotCode, href: `/projects/${task.project.id}` } : task.asset ? { label: `${task.asset.assetCode} — ${task.asset.name}`, href: `/projects/${task.project.id}` } : null; const handleStatusChange = async (newStatus: TaskStatus) => { setUpdatingStatus(true); try { await updateTaskStatus(task.id, newStatus); router.refresh(); } catch { toast({ title: "Failed to update status", variant: "destructive" }); } finally { setUpdatingStatus(false); } }; const handleAssigneeChange = async (artistId: string) => { try { await updateTask(task.id, { assignedArtistId: artistId === "__none__" ? null : artistId }); router.refresh(); toast({ title: "Assignment updated" }); } catch { toast({ title: "Failed to update assignment", variant: "destructive" }); } }; const handleDeleteVersion = async (versionId: string, label: string) => { if (!confirm(`Delete ${label}? This will permanently remove the file and all comments.`)) return; try { await deleteVersion(versionId); toast({ title: "Version deleted" }); router.refresh(); } catch (e) { toast({ title: "Delete failed", description: e instanceof Error ? e.message : undefined, variant: "destructive" }); } }; return (
{/* Breadcrumb */}
Projects / {task.project.name} {parentLink && ( <> / {parentLink.label} )} / {task.title}
{/* Main column */}
{/* Task header */}

{task.title}

{TASK_TYPE_LABELS[task.type]} {parentLink && ( <> · {parentLink.label} )}
{canUpload && ( )}
{task.description && (

{task.description}

)}
{/* Version history */}

Versions {task.versions.length > 0 && ( ({task.versions.length}) )}

{task.versions.length === 0 ? (

No versions uploaded yet

{canUpload && ( )}
) : (
{task.versions.map((v) => (
v{String(v.versionNumber).padStart(3, "0")} {v.isLatest && ( Latest )} {v.approvalStatus.replace(/_/g, " ")} {v.isClientVisible && ( Client Visible )}
{v.fileName} {v.fileSize != null && ( {formatFileSize(Number(v.fileSize))} )} {formatRelativeDate(new Date(v.createdAt))} {v._count.comments > 0 && ( {v._count.comments} comments )}
{v.artist && ( {getInitials(v.artist.name ?? v.artist.email)} )} {canManage && ( )}
))}
)}
{/* Comments on latest version */} {latestVersion && (

Comments

)}
{/* Sidebar */}
{/* Status */}

Status

{canManage ? ( {(Object.keys(TASK_STATUS_CONFIG) as TaskStatus[]).map((s) => { const cfg = TASK_STATUS_CONFIG[s]; const Icon = cfg.icon; return ( handleStatusChange(s)} className={cn(s === task.status && "font-medium")} > {cfg.label} ); })} ) : (
{statusCfg.label}
)} {/* Meta fields */}

Type

{TASK_TYPE_LABELS[task.type]}

Priority

{task.priority}

{task.dueDate && (

Due Date

{format(new Date(task.dueDate), "MMM d, yyyy")} {isOverdue && (Overdue)}

)} {task.estimatedHours && (

Estimated

{task.estimatedHours}h

)}
{/* Assignee */}

Assigned Artist

{canManage ? ( ) : task.assignedArtist ? (
{getInitials(task.assignedArtist.name ?? task.assignedArtist.email)}

{task.assignedArtist.name ?? task.assignedArtist.email}

{task.assignedArtist.role}

) : (

Unassigned

)}
{/* Project context */}

Project

{task.project.code} {task.project.name} {task.shot && (

Shot: {task.shot.shotCode}

)} {task.asset && (

Asset: {task.asset.assetCode} — {task.asset.name}

)}
{/* Version upload dialog */} {showUpload && ( setShowUpload(false)} onSuccess={() => { setShowUpload(false); router.refresh(); }} /> )}
); }