Files
vfxreview/app/(dashboard)/projects/[id]/ProjectTabsClient.tsx
T
twotalesanimation 0fbe856dce Initial commit
2026-05-19 22:20:29 +02:00

246 lines
7.7 KiB
TypeScript

"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
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 } from "lucide-react";
import type { ShotWithDetails } from "@/types";
import { ProjectSettingsTab } from "@/components/projects/ProjectSettingsTab";
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[];
canManage: boolean;
}
export function ProjectTabsClient({
projectId,
projectType,
projectSettings,
clients,
teamMembers,
shots,
assets,
tasks,
artists,
canManage,
}: ProjectTabsClientProps) {
const [activeTab, setActiveTab] = useState<Tab>("shots");
const [showNewShot, setShowNewShot] = useState(false);
const [showImportShots, setShowImportShots] = useState(false);
const [showNewAsset, setShowNewAsset] = useState(false);
const [showNewTask, setShowNewTask] = useState(false);
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 (
<div className="space-y-4">
{/* Tab bar */}
<div className="flex items-center justify-between border-b border-border pb-0">
<div className="flex">
{visibleTabs.map((tab) => {
const Icon = tab.icon;
return (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={cn(
"flex items-center gap-2 px-4 py-2.5 text-sm font-medium border-b-2 transition-colors -mb-px",
activeTab === tab.id
? "border-amber-500 text-amber-400"
: "border-transparent text-zinc-500 hover:text-zinc-300"
)}
>
<Icon className="h-4 w-4" />
{tab.label}
{tab.count > 0 && (
<span className="text-xs text-zinc-500">{tab.count}</span>
)}
</button>
);
})}
</div>
{/* Context-sensitive add button */}
{canManage && (
<div className="pb-1">
{activeTab === "shots" && (
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" className="gap-2 h-8" onClick={() => setShowImportShots(true)}>
<FileUp className="h-3.5 w-3.5" /> Import CSV
</Button>
<Button size="sm" className="gap-2 h-8" onClick={() => setShowNewShot(true)}>
<Plus className="h-3.5 w-3.5" /> New Shot
</Button>
</div>
)}
{activeTab === "assets" && (
<Button size="sm" className="gap-2 h-8" onClick={() => setShowNewAsset(true)}>
<Plus className="h-3.5 w-3.5" /> New Asset
</Button>
)}
{(activeTab === "tasks" || activeTab === "kanban") && (
<Button size="sm" className="gap-2 h-8" onClick={() => setShowNewTask(true)}>
<Plus className="h-3.5 w-3.5" /> New Task
</Button>
)}
</div>
)}
</div>
{/* Tab content */}
{activeTab === "shots" && (
<div>
{shots.length === 0 ? (
<EmptyState icon={Film} label="No shots yet" />
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
{shots.map((shot) => (
<ShotCard key={shot.id} shot={shot} projectId={projectId} />
))}
</div>
)}
</div>
)}
{activeTab === "assets" && (
<div className="space-y-2">
{assets.length === 0 ? (
<EmptyState icon={Package} label="No assets yet" />
) : (
assets.map((asset) => (
<AssetCard
key={asset.id}
asset={asset}
projectId={projectId}
artists={artists}
canManage={canManage}
/>
))
)}
</div>
)}
{activeTab === "tasks" && (
<div className="space-y-1">
{tasks.length === 0 ? (
<EmptyState icon={ListTodo} label="No tasks yet" />
) : (
tasks.map((task) => (
<TaskCard key={task.id} task={task} projectId={projectId} canManage={canManage} />
))
)}
</div>
)}
{activeTab === "kanban" && (
<KanbanBoard tasks={tasks} projectId={projectId} artists={artists} />
)}
{activeTab === "settings" && canManage && (
<ProjectSettingsTab
project={projectSettings}
clients={clients}
teamMembers={teamMembers}
/>
)}
{/* Dialogs */}
<NewShotDialog
projectId={projectId}
projectType={projectType}
open={showNewShot}
onClose={() => setShowNewShot(false)}
onSuccess={() => setShowNewShot(false)}
/>
<ImportShotsDialog
projectId={projectId}
projectType={projectType}
open={showImportShots}
onClose={() => setShowImportShots(false)}
onSuccess={() => setShowImportShots(false)}
/>
<NewAssetDialog
projectId={projectId}
open={showNewAsset}
onClose={() => setShowNewAsset(false)}
onSuccess={() => setShowNewAsset(false)}
/>
<NewTaskDialog
projectId={projectId}
artists={artists}
open={showNewTask}
onClose={() => setShowNewTask(false)}
onSuccess={() => setShowNewTask(false)}
/>
</div>
);
}
function EmptyState({ icon: Icon, label }: { icon: React.ElementType; label: string }) {
return (
<div className="text-center py-10 text-muted-foreground border border-dashed border-border rounded-lg">
<Icon className="h-8 w-8 mx-auto mb-3 opacity-30" />
<p className="text-sm">{label}</p>
</div>
);
}