"use client"; import { useState, useEffect, useRef, useCallback } from "react"; import Image from "next/image"; import Link from "next/link"; import { ReviewPlayer, type ReviewPlayerRef } from "@/components/player/ReviewPlayer"; import { useReviewStore } from "@/hooks/use-review-player"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { internallyApproveShot, shareWithClient } from "@/actions/shots"; import { useToast } from "@/components/ui/use-toast"; import { Film, Loader2, CheckCircle2, AlertCircle, Clock, ExternalLink, ShieldCheck, Send, } from "lucide-react"; interface Project { id: string; name: string; code: string; projectType: string; } interface PlaylistVersion { id: string; versionNumber: number; fileUrl: string; thumbnailUrl: string | null; posterUrl: string | null; fps: number; approvalStatus: string; taskTitle: string; } interface PlaylistShot { id: string; shotCode: string; episode: string | null; status: string; shotApprovalStatus: string; sharedWithClient: boolean; thumbnailUrl: string | null; latestVersion: PlaylistVersion; } interface PlaylistClientProps { projects: Project[]; userRole: string; } const APPROVAL_COLORS: 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: AlertCircle, NEEDS_CHANGES: AlertCircle, }; export function PlaylistClient({ projects, userRole }: PlaylistClientProps) { const playerRef = useRef(null); const reset = useReviewStore((s) => s.reset); const { toast } = useToast(); const [projectId, setProjectId] = useState(null); const [shots, setShots] = useState([]); const [activeShot, setActiveShot] = useState(null); const [isLoading, setIsLoading] = useState(false); const [isActioning, setIsActioning] = useState(false); const canApprove = ["ADMIN", "PRODUCER", "SUPERVISOR"].includes(userRole); // Restore last project from localStorage on mount useEffect(() => { const saved = localStorage.getItem("playlist:lastProjectId"); if (saved && projects.some((p) => p.id === saved)) { setProjectId(saved); } }, [projects]); // Fetch shots whenever project changes useEffect(() => { if (!projectId) { setShots([]); setActiveShot(null); return; } let cancelled = false; setIsLoading(true); fetch(`/api/playlist?projectId=${projectId}`) .then((r) => r.json()) .then((data) => { if (cancelled) return; const list: PlaylistShot[] = data.shots ?? []; setShots(list); // Auto-select the first shot, or keep the current one if it still exists setActiveShot((prev) => { const stillExists = prev && list.some((s) => s.id === prev.id); return stillExists ? prev : (list[0] ?? null); }); }) .catch(() => { if (!cancelled) setShots([]); }) .finally(() => { if (!cancelled) setIsLoading(false); }); return () => { cancelled = true; }; }, [projectId]); const handleProjectChange = (id: string) => { localStorage.setItem("playlist:lastProjectId", id); setProjectId(id); }; const handleSelectShot = useCallback((shot: PlaylistShot) => { reset(); setActiveShot(shot); }, [reset]); const refreshShots = useCallback(() => { if (!projectId) return; fetch(`/api/playlist?projectId=${projectId}`) .then((r) => r.json()) .then((data) => { const list: PlaylistShot[] = data.shots ?? []; setShots(list); setActiveShot((prev) => prev ? (list.find((s) => s.id === prev.id) ?? prev) : null); }) .catch(() => {}); }, [projectId]); const handleInternallyApprove = async () => { if (!activeShot) return; setIsActioning(true); try { await internallyApproveShot(activeShot.id); toast({ title: "Shot internally approved", description: "Status: Ready for Client" }); refreshShots(); } catch (e) { toast({ title: "Failed", description: (e as Error).message, variant: "destructive" }); } finally { setIsActioning(false); } }; const handleShareWithClient = async () => { if (!activeShot) return; setIsActioning(true); try { await shareWithClient(activeShot.id); toast({ title: "Shared with client", description: "Status: Client Review" }); refreshShots(); } catch (e) { toast({ title: "Failed", description: (e as Error).message, variant: "destructive" }); } finally { setIsActioning(false); } }; // Group shots by episode for episodic projects const selectedProject = projects.find((p) => p.id === projectId); const isEpisodic = selectedProject?.projectType === "EPISODIC"; const episodeGroups: [string, PlaylistShot[]][] = isEpisodic ? (() => { const map = new Map(); for (const shot of shots) { const key = shot.episode ?? "(No Episode)"; if (!map.has(key)) map.set(key, []); map.get(key)!.push(shot); } return Array.from(map.entries()); })() : []; return (
{/* Toolbar */}
Playlist
{activeShot && (
{activeShot.shotCode} {(() => { const Icon = APPROVAL_ICONS[activeShot.latestVersion.approvalStatus] ?? Clock; return ; })()} {activeShot.latestVersion.approvalStatus.replace(/_/g, " ")} {canApprove && activeShot.shotApprovalStatus === "PENDING" && ( )} {canApprove && activeShot.shotApprovalStatus === "INTERNALLY_APPROVED" && !activeShot.sharedWithClient && ( )}
)}
{/* Body: player + shot panel */}
{/* Player */}
{activeShot ? ( ) : (

{!projectId ? "Select a project to start" : isLoading ? "" : "No shots with video versions found"}

{isLoading && }
)}
{/* Shot panel */}
Shots {shots.length > 0 && ( {shots.length} )}
{isLoading ? (
) : !projectId ? (

Select a project

) : shots.length === 0 ? (

No video versions found

) : isEpisodic ? (
{episodeGroups.map(([episode, episodeShots]) => (
Ep {episode}
{episodeShots.map((shot) => ( handleSelectShot(shot)} /> ))}
))}
) : (
{shots.map((shot) => ( handleSelectShot(shot)} /> ))}
)}
); } function ShotThumbnail({ shot, isActive, onClick, }: { shot: PlaylistShot; isActive: boolean; onClick: () => void; }) { const thumb = shot.latestVersion.thumbnailUrl ?? shot.latestVersion.posterUrl ?? shot.thumbnailUrl; const ApprovalIcon = APPROVAL_ICONS[shot.latestVersion.approvalStatus] ?? Clock; return ( ); }