Moved approvals comments and views to shots page
Deploy / deploy (push) Successful in 2m36s

This commit is contained in:
twotalesanimation
2026-06-12 08:14:34 +02:00
parent d56f3b3ad5
commit 3248b8596a
2 changed files with 128 additions and 4 deletions
@@ -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">
&ldquo;{latestApproval.notes}&rdquo;
</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}
+17 -1
View File
@@ -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 } },
},
},
},
},
},
}),