"use client"; import { useState, useEffect } from "react"; import { useParams, useRouter } from "next/navigation"; import Link from "next/link"; import Image from "next/image"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { TaskList } from "@/components/tasks/TaskList"; import { Separator } from "@/components/ui/separator"; import { getInitials, cn, formatRelativeDate } from "@/lib/utils"; import { Film, ArrowLeft, Clock, AlertCircle, CheckCircle2, Settings, ListTodo, Video, Copy, ShieldCheck, Share2, Eye, EyeOff, MessageSquare, ExternalLink, XCircle, FileVideo, Pencil, Check, } from "lucide-react"; import { Input } from "@/components/ui/input"; import type { ShotWithDetails } from "@/types"; import { ShotSettingsTab } from "@/components/shots/ShotSettingsTab"; import { ShotExportsTab } from "@/components/shots/ShotExportsTab"; import { FootageViewer } from "@/components/shots/FootageViewer"; import { HighResUploadDialog } from "@/components/shots/HighResUploadDialog"; import { duplicateShot, internallyApproveShot, shareWithClient, unshareFromClient, unapproveShot, updateShotVersion } from "@/actions/shots"; const STATUS_CONFIG: Record< string, { label: string; className: string; Icon: React.ElementType } > = { WAITING: { label: "Waiting", className: "bg-zinc-500/10 text-zinc-400 border-zinc-500/20", Icon: Clock }, IN_PROGRESS: { label: "In Progress", className: "bg-blue-500/10 text-blue-400 border-blue-500/20", Icon: Film }, INTERNAL_REVIEW: { label: "Internal Review", className: "bg-purple-500/10 text-purple-400 border-purple-500/20", Icon: AlertCircle }, READY_FOR_CLIENT: { label: "Ready for Client", className: "bg-sky-500/10 text-sky-400 border-sky-500/20", Icon: ShieldCheck }, CLIENT_REVIEW: { label: "Client Review", className: "bg-indigo-500/10 text-indigo-400 border-indigo-500/20", Icon: Eye }, REVISIONS: { label: "Revisions", className: "bg-orange-500/10 text-orange-400 border-orange-500/20", Icon: AlertCircle }, COMPLETE: { label: "Complete", className: "bg-emerald-500/10 text-emerald-400 border-emerald-500/20", Icon: CheckCircle2 }, }; 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", }; const APPROVAL_ICONS: Record = { PENDING_REVIEW: Clock, APPROVED: CheckCircle2, REJECTED: XCircle, NEEDS_CHANGES: AlertCircle, }; const PRIORITY_CONFIG: Record = { LOW: { label: "Low", dot: "bg-zinc-400" }, NORMAL: { label: "Normal", dot: "bg-blue-400" }, HIGH: { label: "High", dot: "bg-amber-400" }, CRITICAL: { label: "Critical", dot: "bg-red-500" }, }; import { useToast } from "@/components/ui/use-toast"; export default function ShotDetailPage() { const params = useParams<{ id: string; shotId: string }>(); const router = useRouter(); const { toast } = useToast(); const [shot, setShot] = useState(null); const [projectName, setProjectName] = useState(""); const [loading, setLoading] = useState(true); const [canApprove, setCanApprove] = useState(false); const [canInternallyApprove, setCanInternallyApprove] = useState(false); const [tasks, setTasks] = useState([]); const [artists, setArtists] = useState([]); const [canManage, setCanManage] = useState(false); const [isDuplicating, setIsDuplicating] = useState(false); const [isActioning, setIsActioning] = useState(false); const [highResDialogOpen, setHighResDialogOpen] = useState(false); const [activeTab, setActiveTab] = useState<"tasks" | "reviews" | "footage" | "exports" | "settings">("tasks"); const [editingVersion, setEditingVersion] = useState(false); const [versionInput, setVersionInput] = useState(""); const [savingVersion, setSavingVersion] = useState(false); const fetchShot = async () => { try { const res = await fetch(`/api/shots/${params.shotId}?projectId=${params.id}`); if (res.status === 404) { router.push("/projects"); return; } const data = await res.json(); setShot(data.shot); setProjectName(data.projectName ?? ""); setCanApprove(data.canApprove ?? false); setCanInternallyApprove(data.canInternallyApprove ?? false); setTasks(data.tasks ?? []); setArtists(data.artists ?? []); setCanManage(data.canApprove ?? false); } catch { router.push("/projects"); } finally { setLoading(false); } }; useEffect(() => { fetchShot(); }, [params.shotId]); const handleInternalApprove = async () => { if (!shot) return; setIsActioning(true); try { await internallyApproveShot(shot.id); toast({ title: "Shot internally approved", description: "Status: Ready for Client" }); fetchShot(); } catch (e) { toast({ title: "Failed", description: e instanceof Error ? e.message : undefined, variant: "destructive" }); } finally { setIsActioning(false); } }; const handleShareWithClient = async () => { if (!shot) return; setIsActioning(true); try { await shareWithClient(shot.id); toast({ title: "Shared with client", description: "Status: Client Review" }); fetchShot(); } catch (e) { toast({ title: "Failed", description: e instanceof Error ? e.message : undefined, variant: "destructive" }); } finally { setIsActioning(false); } }; const handleUnshare = async () => { if (!shot) return; setIsActioning(true); try { await unshareFromClient(shot.id); toast({ title: "Removed from client review", description: "Status: Ready for Client" }); fetchShot(); } catch (e) { toast({ title: "Failed", description: e instanceof Error ? e.message : undefined, variant: "destructive" }); } finally { setIsActioning(false); } }; const handleUnapproveShot = async () => { if (!shot) return; if (!confirm("Undo client approval? The shot will return to Internally Approved.")) return; setIsActioning(true); try { await unapproveShot(shot.id); toast({ title: "Client approval undone", description: "Status: Ready for Client" }); fetchShot(); } catch (e) { toast({ title: "Failed", description: e instanceof Error ? e.message : undefined, variant: "destructive" }); } finally { setIsActioning(false); } }; const handleSaveVersion = async () => { if (!shot) return; const trimmed = versionInput.trim().toLowerCase(); if (!/^v\d{3}$/.test(trimmed)) { toast({ title: "Invalid format", description: "Version must be v### (e.g. v001)", variant: "destructive" }); return; } setSavingVersion(true); try { await updateShotVersion(shot.id, trimmed); toast({ title: `Version set to ${trimmed}` }); setEditingVersion(false); fetchShot(); } catch (e) { toast({ title: "Failed", description: e instanceof Error ? e.message : undefined, variant: "destructive" }); } finally { setSavingVersion(false); } }; const handleDuplicate = async () => { if (!shot) return; setIsDuplicating(true); try { const { shot: newShot } = await duplicateShot(shot.id); toast({ title: "Shot duplicated", description: `Created ${newShot.shotCode}` }); router.push(`/projects/${params.id}/shots/${newShot.id}`); } catch (e) { toast({ title: "Duplicate failed", description: e instanceof Error ? e.message : undefined, variant: "destructive" }); } finally { setIsDuplicating(false); } }; if (loading) { return (
); } if (!shot) return null; const statusCfg = STATUS_CONFIG[shot.status] ?? STATUS_CONFIG.WAITING; const priorityCfg = PRIORITY_CONFIG[shot.priority] ?? PRIORITY_CONFIG.NORMAL; const { Icon: StatusIcon } = statusCfg; return (
{/* Breadcrumb */}
Projects / {projectName} / {shot.shotCode}
{/* Header */}
{/* Thumbnail – cinema scope 2.39:1 */} {shot.thumbnailUrl && (
{shot.shotCode}
)}

{shot.shotCode}

{/* Shot version badge — click to edit for managers */} {canManage ? ( editingVersion ? (
setVersionInput(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") handleSaveVersion(); if (e.key === "Escape") setEditingVersion(false); }} className="h-7 w-20 font-mono text-xs px-2" placeholder="v001" maxLength={4} />
) : ( ) ) : ( {shot.shotVersion ?? "v001"} )} {shot.sequence && ( Seq: {shot.sequence} )}
{shot.description && (

{shot.description}

)}
{statusCfg.label} {/* Approval status badges */} {shot.shotApprovalStatus === "PENDING" && ( Pending Internal Approval )} {shot.shotApprovalStatus === "INTERNALLY_APPROVED" && ( Internally Approved )} {shot.shotApprovalStatus === "CLIENT_APPROVED" && ( Client Approved )} {/* Client sharing badge */} {["INTERNALLY_APPROVED", "CLIENT_APPROVED"].includes(shot.shotApprovalStatus) && ( {shot.sharedWithClient ? "Shared With Client" : "Not Shared"} )}
{priorityCfg.label} Priority
{shot.fps} fps {shot.artist && (
{getInitials(shot.artist.name ?? shot.artist.email)} {shot.artist.name ?? shot.artist.email}
)}
{/* EDL / Pull metadata */} {(shot.exrOutput || shot.sourceClip) && (
{shot.exrOutput && (
EXR Output {shot.exrOutput}
)} {shot.sourceClip && (
Source Clip {shot.sourceClip}
)} {(shot.timecodeStart || shot.timecodeEnd) && (
Timecode {shot.timecodeStart ?? ""} {shot.timecodeStart && shot.timecodeEnd && " → "} {shot.timecodeEnd ?? ""} {shot.clipDuration && ({shot.clipDuration})}
)}
)} {canManage && (
{/* Internal approval action */} {canInternallyApprove && shot.shotApprovalStatus === "PENDING" && ( )} {/* Share / Unshare with client */} {canInternallyApprove && ["INTERNALLY_APPROVED", "CLIENT_APPROVED"].includes(shot.shotApprovalStatus) && !shot.sharedWithClient && ( )} {canInternallyApprove && shot.sharedWithClient && ( )} {/* Undo client approval */} {canInternallyApprove && shot.shotApprovalStatus === "CLIENT_APPROVED" && ( )}
)}
{/* Tabs */}
{canManage && ( )}
{activeTab === "tasks" && ( )} {activeTab === "reviews" && (
{tasks.length === 0 ? (

No tasks yet — reviews will appear here once tasks are created.

) : ( tasks.map((task: any) => { const latestVersion = task.versions?.[0]; const latestApproval = latestVersion?.approvals?.[0]; const ApprovalIcon = latestVersion ? (APPROVAL_ICONS[latestVersion.approvalStatus] ?? Clock) : Clock; const approvalStyle = latestVersion ? (APPROVAL_STYLES[latestVersion.approvalStatus] ?? "") : ""; return (
{/* Task header */}

{task.title}

{latestVersion ? ( <> {latestVersion.approvalStatus.replace(/_/g, " ")} v{String(latestVersion.versionNumber).padStart(3, "0")} ) : ( No versions uploaded )}
{/* Latest approval */} {latestVersion && ( latestApproval ? (
{getInitials(latestApproval.user.name ?? "?")}
{latestApproval.user.name ?? "Reviewer"} {formatRelativeDate(latestApproval.createdAt)}
{latestApproval.notes ? (

“{latestApproval.notes}”

) : (

No comment left

)}
) : (

Awaiting review

) )}
); }) )}
)} {activeTab === "footage" && ( )} {activeTab === "exports" && } {activeTab === "settings" && canManage && ( )}
setHighResDialogOpen(false)} onSuccess={fetchShot} />
); }