"use client"; import { useState, useTransition, useEffect } from "react"; import Link from "next/link"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { ShotCard } from "@/components/shots/ShotCard"; import { NewShotDialog } from "@/components/shots/NewShotDialog"; import { ImportShotsDialog } from "@/components/shots/ImportShotsDialog"; import { AssetCard } from "@/components/assets/AssetCard"; import { NewAssetDialog } from "@/components/assets/NewAssetDialog"; import { TaskCard } from "@/components/tasks/TaskCard"; import { NewTaskDialog } from "@/components/tasks/NewTaskDialog"; import { KanbanBoard } from "@/components/tasks/KanbanBoard"; import { cn } from "@/lib/utils"; import { Film, Package, ListTodo, LayoutDashboard, Plus, Settings, FileUp, ChevronDown, ChevronRight, Calendar, Pencil, Check, X } from "lucide-react"; import type { ShotWithDetails } from "@/types"; import { ProjectSettingsTab } from "@/components/projects/ProjectSettingsTab"; import { setEpisodeDueDate } from "@/actions/episode-due-dates"; import { useToast } from "@/components/ui/use-toast"; import { format } from "date-fns"; type Tab = "shots" | "assets" | "tasks" | "kanban" | "settings"; interface Artist { id: string; name: string | null; email: string; } interface Client { id: string; company: string; } interface TeamMember { id: string; name: string | null; email: string; role: string; } interface ProjectTabsClientProps { projectId: string; projectType: "STANDARD" | "EPISODIC"; projectSettings: { id: string; name: string; code: string; showId: string; projectType: "STANDARD" | "EPISODIC"; description: string | null; status: string; clientId: string | null; producerId: string | null; supervisorId: string | null; dueDate: Date | null; startDate: Date | null; slackWebhook: string | null; slackChannel: string | null; }; clients: Client[]; teamMembers: TeamMember[]; shots: ShotWithDetails[]; assets: any[]; tasks: any[]; artists: Artist[]; shotGroups: { id: string; name: string }[]; episodeDueDates: { episode: string; dueDate: Date | string }[]; canManage: boolean; } export function ProjectTabsClient({ projectId, projectType, projectSettings, clients, teamMembers, shots, assets, tasks, artists, shotGroups, episodeDueDates, canManage, }: ProjectTabsClientProps) { const { toast } = useToast(); const [activeTab, setActiveTab] = useState("shots"); const [showNewShot, setShowNewShot] = useState(false); const [showImportShots, setShowImportShots] = useState(false); const [showNewAsset, setShowNewAsset] = useState(false); const [showNewTask, setShowNewTask] = useState(false); const [expandedEpisodes, setExpandedEpisodes] = useState>(new Set()); const [editingEpisode, setEditingEpisode] = useState(null); // Load saved episode expand state from localStorage on mount useEffect(() => { try { const saved = localStorage.getItem(`project:${projectId}:episodeExpand`); if (saved) setExpandedEpisodes(new Set(JSON.parse(saved) as string[])); } catch {} }, [projectId]); const [episodeDateInput, setEpisodeDateInput] = useState(""); const [isPendingDate, startDateTransition] = useTransition(); // Build a lookup map for episode due dates const episodeDueDateMap = new Map( episodeDueDates.map((e) => [e.episode, new Date(e.dueDate)]) ); const handleSaveEpisodeDate = (episode: string) => { startDateTransition(async () => { try { await setEpisodeDueDate({ projectId, episode, dueDate: episodeDateInput || null, }); toast({ title: `Due date ${episodeDateInput ? "set" : "cleared"} for Episode ${episode}` }); } catch { toast({ title: "Failed to save due date", variant: "destructive" }); } finally { setEditingEpisode(null); setEpisodeDateInput(""); } }); }; const toggleEpisode = (ep: string) => setExpandedEpisodes((prev) => { const next = new Set(prev); next.has(ep) ? next.delete(ep) : next.add(ep); try { localStorage.setItem(`project:${projectId}:episodeExpand`, JSON.stringify([...next])); } catch {} return next; }); // For episodic projects: group and sort shots by episode → scene → shotNumber const episodeGroups: [string, ShotWithDetails[]][] = projectType === "EPISODIC" ? (() => { const sorted = [...shots].sort((a, b) => { const ea = a.episode ?? ""; const eb = b.episode ?? ""; if (ea !== eb) return ea.localeCompare(eb, undefined, { numeric: true }); if (a.scene !== b.scene) return a.scene.localeCompare(b.scene, undefined, { numeric: true }); return a.shotNumber - b.shotNumber; }); const map = new Map(); for (const shot of sorted) { const key = shot.episode ?? "(No Episode)"; if (!map.has(key)) map.set(key, []); map.get(key)!.push(shot); } return Array.from(map.entries()); })() : []; // For standard projects: group by custom shot group when any exist const standardGroups: [string, ShotWithDetails[]][] = projectType === "STANDARD" && shots.some((s) => s.shotGroup) ? (() => { const sorted = [...shots].sort((a, b) => { const ga = a.shotGroup?.name ?? "\uFFFF"; const gb = b.shotGroup?.name ?? "\uFFFF"; if (ga !== gb) return ga.localeCompare(gb, undefined, { numeric: true }); if (a.scene !== b.scene) return a.scene.localeCompare(b.scene, undefined, { numeric: true }); return a.shotNumber - b.shotNumber; }); const map = new Map(); for (const shot of sorted) { const key = shot.shotGroup?.name ?? "(Ungrouped)"; if (!map.has(key)) map.set(key, []); map.get(key)!.push(shot); } // Move ungrouped to the end const ungrouped = map.get("(Ungrouped)"); if (ungrouped) { map.delete("(Ungrouped)"); map.set("(Ungrouped)", ungrouped); } return Array.from(map.entries()); })() : []; const tabs: { id: Tab; label: string; icon: React.ElementType; count: number; managerOnly?: boolean }[] = [ { id: "shots", label: "Shots", icon: Film, count: shots.length }, { id: "assets", label: "Assets", icon: Package, count: assets.length }, { id: "tasks", label: "All Tasks", icon: ListTodo, count: tasks.length }, { id: "kanban", label: "Kanban", icon: LayoutDashboard, count: 0 }, { id: "settings", label: "Settings", icon: Settings, count: 0, managerOnly: true }, ]; const visibleTabs = tabs.filter((t) => !t.managerOnly || canManage); return (
{/* Tab bar */}
{visibleTabs.map((tab) => { const Icon = tab.icon; return ( ); })}
{/* Context-sensitive add button */} {canManage && (
{activeTab === "shots" && (
)} {activeTab === "assets" && ( )} {(activeTab === "tasks" || activeTab === "kanban") && ( )}
)}
{/* Tab content */} {activeTab === "shots" && (
{shots.length === 0 ? ( ) : projectType === "EPISODIC" ? (
{episodeGroups.map(([episode, episodeShots]) => { const collapsed = !expandedEpisodes.has(episode); const dueDate = episodeDueDateMap.get(episode); const isOverdue = dueDate && dueDate < new Date(); const isEditing = editingEpisode === episode; return (
{/* Due date display / edit */} {isEditing ? (
setEpisodeDateInput(e.target.value)} className="h-6 w-36 text-xs px-2 py-0" />
) : (
{dueDate && ( {format(dueDate, "d MMM yyyy")} )} {canManage && ( )}
)}
{/* Always-visible edit button for managers */} {canManage && !isEditing && ( )}
{!collapsed && (
{episodeShots.map((shot) => ( ))}
)}
); })}
) : standardGroups.length > 0 ? (
{standardGroups.map(([groupName, groupShots]) => { const collapsed = !expandedEpisodes.has(groupName); return (
{!collapsed && (
{groupShots.map((shot) => ( ))}
)}
); })}
) : (
{shots.map((shot) => ( ))}
)}
)} {activeTab === "assets" && (
{assets.length === 0 ? ( ) : ( assets.map((asset) => ( )) )}
)} {activeTab === "tasks" && (
{tasks.length === 0 ? ( ) : ( tasks.map((task) => ( )) )}
)} {activeTab === "kanban" && ( )} {activeTab === "settings" && canManage && ( )} {/* Dialogs */} setShowNewShot(false)} onSuccess={() => setShowNewShot(false)} /> setShowImportShots(false)} onSuccess={() => setShowImportShots(false)} /> setShowNewAsset(false)} onSuccess={() => setShowNewAsset(false)} /> setShowNewTask(false)} onSuccess={() => setShowNewTask(false)} />
); } function EmptyState({ icon: Icon, label }: { icon: React.ElementType; label: string }) { return (

{label}

); }