246 lines
7.7 KiB
TypeScript
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>
|
|
);
|
|
}
|