This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useState, useTransition } from "react";
|
||||
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";
|
||||
@@ -11,9 +12,12 @@ 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 } from "lucide-react";
|
||||
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";
|
||||
|
||||
@@ -61,6 +65,7 @@ interface ProjectTabsClientProps {
|
||||
tasks: any[];
|
||||
artists: Artist[];
|
||||
shotGroups: { id: string; name: string }[];
|
||||
episodeDueDates: { episode: string; dueDate: Date | string }[];
|
||||
canManage: boolean;
|
||||
}
|
||||
|
||||
@@ -75,14 +80,42 @@ export function ProjectTabsClient({
|
||||
tasks,
|
||||
artists,
|
||||
shotGroups,
|
||||
episodeDueDates,
|
||||
canManage,
|
||||
}: ProjectTabsClientProps) {
|
||||
const { toast } = useToast();
|
||||
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 [collapsedEpisodes, setCollapsedEpisodes] = useState<Set<string>>(new Set());
|
||||
const [editingEpisode, setEditingEpisode] = useState<string | null>(null);
|
||||
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) =>
|
||||
setCollapsedEpisodes((prev) => {
|
||||
@@ -211,23 +244,98 @@ export function ProjectTabsClient({
|
||||
<div className="space-y-4">
|
||||
{episodeGroups.map(([episode, episodeShots]) => {
|
||||
const collapsed = collapsedEpisodes.has(episode);
|
||||
const dueDate = episodeDueDateMap.get(episode);
|
||||
const isOverdue = dueDate && dueDate < new Date();
|
||||
const isEditing = editingEpisode === episode;
|
||||
return (
|
||||
<div key={episode}>
|
||||
<button
|
||||
onClick={() => toggleEpisode(episode)}
|
||||
className="flex items-center gap-2 w-full mb-3 group text-left"
|
||||
>
|
||||
{collapsed
|
||||
? <ChevronRight className="h-4 w-4 text-muted-foreground shrink-0" />
|
||||
: <ChevronDown className="h-4 w-4 text-muted-foreground shrink-0" />}
|
||||
<span className="font-semibold text-sm">
|
||||
Episode {episode}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground font-normal">
|
||||
{episodeShots.length} shot{episodeShots.length !== 1 ? "s" : ""}
|
||||
</span>
|
||||
<div className="flex items-center gap-2 w-full mb-3">
|
||||
<button
|
||||
onClick={() => toggleEpisode(episode)}
|
||||
className="flex items-center gap-2 shrink-0"
|
||||
>
|
||||
{collapsed
|
||||
? <ChevronRight className="h-4 w-4 text-muted-foreground" />
|
||||
: <ChevronDown className="h-4 w-4 text-muted-foreground" />}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => toggleEpisode(episode)}
|
||||
className="flex items-center gap-2 text-left"
|
||||
>
|
||||
<span className="font-semibold text-sm">
|
||||
Episode {episode}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground font-normal">
|
||||
{episodeShots.length} shot{episodeShots.length !== 1 ? "s" : ""}
|
||||
</span>
|
||||
</button>
|
||||
{/* Due date display / edit */}
|
||||
{isEditing ? (
|
||||
<div className="flex items-center gap-1 ml-2">
|
||||
<Input
|
||||
type="date"
|
||||
value={episodeDateInput}
|
||||
onChange={(e) => setEpisodeDateInput(e.target.value)}
|
||||
className="h-6 w-36 text-xs px-2 py-0"
|
||||
/>
|
||||
<button
|
||||
onClick={() => handleSaveEpisodeDate(episode)}
|
||||
disabled={isPendingDate}
|
||||
className="text-emerald-400 hover:text-emerald-300 p-0.5"
|
||||
>
|
||||
<Check className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setEditingEpisode(null); setEpisodeDateInput(""); }}
|
||||
className="text-zinc-500 hover:text-zinc-300 p-0.5"
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1.5 ml-2">
|
||||
{dueDate && (
|
||||
<span className={cn(
|
||||
"flex items-center gap-1 text-xs",
|
||||
isOverdue ? "text-red-400" : "text-zinc-400"
|
||||
)}>
|
||||
<Calendar className="h-3 w-3" />
|
||||
{format(dueDate, "d MMM yyyy")}
|
||||
</span>
|
||||
)}
|
||||
{canManage && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setEditingEpisode(episode);
|
||||
setEpisodeDateInput(
|
||||
dueDate ? format(dueDate, "yyyy-MM-dd") : ""
|
||||
);
|
||||
}}
|
||||
className="text-zinc-600 hover:text-zinc-300 p-0.5 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
title="Set due date"
|
||||
>
|
||||
<Pencil className="h-3 w-3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1 h-px bg-border ml-1" />
|
||||
</button>
|
||||
{/* Always-visible edit button for managers */}
|
||||
{canManage && !isEditing && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setEditingEpisode(episode);
|
||||
setEpisodeDateInput(
|
||||
dueDate ? format(dueDate, "yyyy-MM-dd") : ""
|
||||
);
|
||||
}}
|
||||
className="text-zinc-600 hover:text-zinc-400 transition-colors shrink-0"
|
||||
title="Set episode due date"
|
||||
>
|
||||
<Pencil className="h-3 w-3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{!collapsed && (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
|
||||
{episodeShots.map((shot) => (
|
||||
|
||||
@@ -106,11 +106,12 @@ async function getTeamMembers() {
|
||||
export default async function ProjectPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
const session = await auth();
|
||||
const [project, artists, clients, teamMembers] = await Promise.all([
|
||||
const [project, artists, clients, teamMembers, episodeDueDates] = await Promise.all([
|
||||
getProject(id),
|
||||
getProjectArtists(),
|
||||
getClients(),
|
||||
getTeamMembers(),
|
||||
db.episodeDueDate.findMany({ where: { projectId: id }, orderBy: { episode: "asc" } }),
|
||||
]);
|
||||
if (!project) notFound();
|
||||
|
||||
@@ -208,6 +209,7 @@ export default async function ProjectPage({ params }: { params: Promise<{ id: st
|
||||
tasks={project.tasks as any}
|
||||
artists={artists}
|
||||
shotGroups={project.shotGroups}
|
||||
episodeDueDates={episodeDueDates}
|
||||
canManage={!!canManage}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user