This commit is contained in:
@@ -9,8 +9,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { TaskList } from "@/components/tasks/TaskList";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { getInitials } from "@/lib/utils";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { getInitials, cn, formatRelativeDate } from "@/lib/utils";
|
||||
import {
|
||||
Film,
|
||||
ArrowLeft,
|
||||
@@ -25,6 +24,9 @@ import {
|
||||
Share2,
|
||||
Eye,
|
||||
EyeOff,
|
||||
MessageSquare,
|
||||
ExternalLink,
|
||||
XCircle,
|
||||
} from "lucide-react";
|
||||
import type { ShotWithDetails } from "@/types";
|
||||
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 },
|
||||
};
|
||||
|
||||
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 }> = {
|
||||
LOW: { label: "Low", dot: "bg-zinc-400" },
|
||||
NORMAL: { label: "Normal", dot: "bg-blue-400" },
|
||||
@@ -67,7 +83,7 @@ export default function ShotDetailPage() {
|
||||
const [canManage, setCanManage] = useState(false);
|
||||
const [isDuplicating, setIsDuplicating] = 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 () => {
|
||||
try {
|
||||
@@ -377,6 +393,18 @@ export default function ShotDetailPage() {
|
||||
<ListTodo className="h-4 w-4" />
|
||||
Tasks
|
||||
</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
|
||||
onClick={() => setActiveTab("footage")}
|
||||
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" && (
|
||||
<FootageViewer
|
||||
shot={shot}
|
||||
|
||||
@@ -51,7 +51,23 @@ export async function GET(
|
||||
versions: {
|
||||
take: 1,
|
||||
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