This commit is contained in:
@@ -9,8 +9,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
import { TaskList } from "@/components/tasks/TaskList";
|
import { TaskList } from "@/components/tasks/TaskList";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { getInitials } from "@/lib/utils";
|
import { getInitials, cn, formatRelativeDate } from "@/lib/utils";
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import {
|
import {
|
||||||
Film,
|
Film,
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
@@ -25,6 +24,9 @@ import {
|
|||||||
Share2,
|
Share2,
|
||||||
Eye,
|
Eye,
|
||||||
EyeOff,
|
EyeOff,
|
||||||
|
MessageSquare,
|
||||||
|
ExternalLink,
|
||||||
|
XCircle,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import type { ShotWithDetails } from "@/types";
|
import type { ShotWithDetails } from "@/types";
|
||||||
import { ShotSettingsTab } from "@/components/shots/ShotSettingsTab";
|
import { ShotSettingsTab } from "@/components/shots/ShotSettingsTab";
|
||||||
@@ -44,6 +46,20 @@ const STATUS_CONFIG: Record<
|
|||||||
COMPLETE: { label: "Complete", className: "bg-emerald-500/10 text-emerald-400 border-emerald-500/20", Icon: CheckCircle2 },
|
COMPLETE: { label: "Complete", className: "bg-emerald-500/10 text-emerald-400 border-emerald-500/20", Icon: CheckCircle2 },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const APPROVAL_STYLES: Record<string, string> = {
|
||||||
|
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<string, React.ElementType> = {
|
||||||
|
PENDING_REVIEW: Clock,
|
||||||
|
APPROVED: CheckCircle2,
|
||||||
|
REJECTED: XCircle,
|
||||||
|
NEEDS_CHANGES: AlertCircle,
|
||||||
|
};
|
||||||
|
|
||||||
const PRIORITY_CONFIG: Record<string, { label: string; dot: string }> = {
|
const PRIORITY_CONFIG: Record<string, { label: string; dot: string }> = {
|
||||||
LOW: { label: "Low", dot: "bg-zinc-400" },
|
LOW: { label: "Low", dot: "bg-zinc-400" },
|
||||||
NORMAL: { label: "Normal", dot: "bg-blue-400" },
|
NORMAL: { label: "Normal", dot: "bg-blue-400" },
|
||||||
@@ -67,7 +83,7 @@ export default function ShotDetailPage() {
|
|||||||
const [canManage, setCanManage] = useState(false);
|
const [canManage, setCanManage] = useState(false);
|
||||||
const [isDuplicating, setIsDuplicating] = useState(false);
|
const [isDuplicating, setIsDuplicating] = useState(false);
|
||||||
const [isActioning, setIsActioning] = useState(false);
|
const [isActioning, setIsActioning] = useState(false);
|
||||||
const [activeTab, setActiveTab] = useState<"tasks" | "footage" | "settings">("tasks");
|
const [activeTab, setActiveTab] = useState<"tasks" | "reviews" | "footage" | "settings">("tasks");
|
||||||
|
|
||||||
const fetchShot = async () => {
|
const fetchShot = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -377,6 +393,18 @@ export default function ShotDetailPage() {
|
|||||||
<ListTodo className="h-4 w-4" />
|
<ListTodo className="h-4 w-4" />
|
||||||
Tasks
|
Tasks
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab("reviews")}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2 px-4 py-2.5 text-sm font-medium border-b-2 transition-colors -mb-px",
|
||||||
|
activeTab === "reviews"
|
||||||
|
? "border-amber-500 text-amber-400"
|
||||||
|
: "border-transparent text-zinc-500 hover:text-zinc-300"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<MessageSquare className="h-4 w-4" />
|
||||||
|
Reviews
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab("footage")}
|
onClick={() => setActiveTab("footage")}
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -416,6 +444,86 @@ export default function ShotDetailPage() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{activeTab === "reviews" && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{tasks.length === 0 ? (
|
||||||
|
<div className="flex flex-col items-center justify-center py-16 gap-2 text-muted-foreground">
|
||||||
|
<MessageSquare className="h-8 w-8 opacity-30" />
|
||||||
|
<p className="text-sm">No tasks yet — reviews will appear here once tasks are created.</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
tasks.map((task: any) => {
|
||||||
|
const latestVersion = task.versions?.[0];
|
||||||
|
const latestApproval = latestVersion?.approvals?.[0];
|
||||||
|
const ApprovalIcon = latestVersion ? (APPROVAL_ICONS[latestVersion.approvalStatus] ?? Clock) : Clock;
|
||||||
|
const approvalStyle = latestVersion ? (APPROVAL_STYLES[latestVersion.approvalStatus] ?? "") : "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={task.id} className="rounded-lg border border-border bg-card p-4 space-y-3">
|
||||||
|
{/* Task header */}
|
||||||
|
<div className="flex items-center gap-3 flex-wrap">
|
||||||
|
<p className="text-sm font-medium flex-1 min-w-0 truncate">{task.title}</p>
|
||||||
|
{latestVersion ? (
|
||||||
|
<>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className={cn("text-xs gap-1", approvalStyle)}
|
||||||
|
>
|
||||||
|
<ApprovalIcon className="h-3 w-3" />
|
||||||
|
{latestVersion.approvalStatus.replace(/_/g, " ")}
|
||||||
|
</Badge>
|
||||||
|
<Link
|
||||||
|
href={`/review/${latestVersion.id}`}
|
||||||
|
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors font-mono"
|
||||||
|
>
|
||||||
|
v{String(latestVersion.versionNumber).padStart(3, "0")}
|
||||||
|
<ExternalLink className="h-3 w-3" />
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span className="text-xs text-muted-foreground">No versions uploaded</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Latest approval */}
|
||||||
|
{latestVersion && (
|
||||||
|
latestApproval ? (
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Avatar className="h-7 w-7 shrink-0">
|
||||||
|
<AvatarImage src={latestApproval.user.image ?? undefined} />
|
||||||
|
<AvatarFallback className="text-[10px] bg-primary/10 text-primary">
|
||||||
|
{getInitials(latestApproval.user.name ?? "?")}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div className="flex-1 min-w-0 space-y-0.5">
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{latestApproval.user.name ?? "Reviewer"}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{formatRelativeDate(latestApproval.createdAt)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{latestApproval.notes ? (
|
||||||
|
<p className="text-sm text-muted-foreground italic leading-relaxed">
|
||||||
|
“{latestApproval.notes}”
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<p className="text-xs text-muted-foreground/60">No comment left</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-xs text-muted-foreground pl-1">Awaiting review</p>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{activeTab === "footage" && (
|
{activeTab === "footage" && (
|
||||||
<FootageViewer
|
<FootageViewer
|
||||||
shot={shot}
|
shot={shot}
|
||||||
|
|||||||
@@ -51,7 +51,23 @@ export async function GET(
|
|||||||
versions: {
|
versions: {
|
||||||
take: 1,
|
take: 1,
|
||||||
orderBy: { versionNumber: "desc" },
|
orderBy: { versionNumber: "desc" },
|
||||||
select: { id: true, versionNumber: true, approvalStatus: true, createdAt: true },
|
select: {
|
||||||
|
id: true,
|
||||||
|
versionNumber: true,
|
||||||
|
approvalStatus: true,
|
||||||
|
createdAt: true,
|
||||||
|
approvals: {
|
||||||
|
take: 1,
|
||||||
|
orderBy: { createdAt: "desc" },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
status: true,
|
||||||
|
notes: true,
|
||||||
|
createdAt: true,
|
||||||
|
user: { select: { id: true, name: true, image: true } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|||||||
Reference in New Issue
Block a user