@@ -17,11 +17,13 @@ import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { getInitials, formatRelativeDate, formatFileSize } from "@/lib/utils";
|
||||
import { updateTask, updateTaskStatus } from "@/actions/tasks";
|
||||
import { submitApproval, unapproveVersion } from "@/actions/approvals";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { TaskStatus, TaskType } from "@prisma/client";
|
||||
import { formatDistanceToNow, format } from "date-fns";
|
||||
@@ -42,6 +44,8 @@ import {
|
||||
User,
|
||||
ExternalLink,
|
||||
Trash2,
|
||||
XCircle,
|
||||
MoreHorizontal,
|
||||
} from "lucide-react";
|
||||
import { TASK_STATUS_CONFIG, TASK_TYPE_LABELS } from "@/components/tasks/TaskCard";
|
||||
import { VersionUpload } from "@/components/versions/VersionUpload";
|
||||
@@ -195,6 +199,29 @@ export function TaskDetailClient({
|
||||
}
|
||||
};
|
||||
|
||||
const handleApproveVersion = async (versionId: string, status: "APPROVED" | "REJECTED" | "NEEDS_CHANGES") => {
|
||||
try {
|
||||
await submitApproval({ versionId, status, notes: "" });
|
||||
toast({
|
||||
title: status === "APPROVED" ? "Version approved" : status === "REJECTED" ? "Version rejected" : "Changes requested",
|
||||
});
|
||||
router.refresh();
|
||||
} catch (e) {
|
||||
toast({ title: "Failed", description: e instanceof Error ? e.message : undefined, variant: "destructive" });
|
||||
}
|
||||
};
|
||||
|
||||
const handleUnapproveVersion = async (versionId: string) => {
|
||||
if (!confirm("Undo approval? The version will return to Pending Review and the task will return to Internal Review.")) return;
|
||||
try {
|
||||
await unapproveVersion(versionId);
|
||||
toast({ title: "Approval undone", description: "Version reset to Pending Review" });
|
||||
router.refresh();
|
||||
} catch (e) {
|
||||
toast({ title: "Failed to unapprove", description: e instanceof Error ? e.message : undefined, variant: "destructive" });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="max-w-5xl mx-auto p-6 space-y-6">
|
||||
@@ -339,10 +366,46 @@ export function TaskDetailClient({
|
||||
</Button>
|
||||
</Link>
|
||||
{canManage && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 text-muted-foreground hover:text-red-500"
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-muted-foreground">
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
className="text-emerald-500"
|
||||
onClick={() => handleApproveVersion(v.id, "APPROVED")}
|
||||
>
|
||||
<CheckCircle2 className="h-3.5 w-3.5 mr-2" />
|
||||
Approve
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="text-orange-400"
|
||||
onClick={() => handleApproveVersion(v.id, "NEEDS_CHANGES")}
|
||||
>
|
||||
<AlertCircle className="h-3.5 w-3.5 mr-2" />
|
||||
Needs Changes
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="text-red-500"
|
||||
onClick={() => handleApproveVersion(v.id, "REJECTED")}
|
||||
>
|
||||
<XCircle className="h-3.5 w-3.5 mr-2" />
|
||||
Reject
|
||||
</DropdownMenuItem>
|
||||
{v.approvalStatus === "APPROVED" && (
|
||||
<DropdownMenuItem
|
||||
className="text-amber-500"
|
||||
onClick={() => handleUnapproveVersion(v.id)}
|
||||
>
|
||||
<AlertCircle className="h-3.5 w-3.5 mr-2" />
|
||||
Undo Approval
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="text-red-500 focus:text-red-500"
|
||||
onClick={() =>
|
||||
handleDeleteVersion(
|
||||
v.id,
|
||||
@@ -350,8 +413,11 @@ export function TaskDetailClient({
|
||||
)
|
||||
}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Trash2 className="h-3.5 w-3.5 mr-2" />
|
||||
Delete version
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -113,6 +113,21 @@ export async function POST(
|
||||
if (version.task) {
|
||||
if (status === "APPROVED") {
|
||||
await db.task.update({ where: { id: version.task.id }, data: { status: "DONE" } });
|
||||
|
||||
// If this version belongs to a shot that is shared with the client,
|
||||
// also approve the shot at the shot level so it moves to COMPLETE.
|
||||
if (version.task.shot) {
|
||||
const shot = await db.shot.findUnique({
|
||||
where: { id: version.task.shot.id },
|
||||
select: { sharedWithClient: true, shotApprovalStatus: true },
|
||||
});
|
||||
if (shot?.sharedWithClient && shot.shotApprovalStatus !== "CLIENT_APPROVED") {
|
||||
await db.shot.update({
|
||||
where: { id: version.task.shot.id },
|
||||
data: { shotApprovalStatus: "CLIENT_APPROVED", status: "COMPLETE" },
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await db.task.update({ where: { id: version.task.id }, data: { status: "CHANGES" } });
|
||||
}
|
||||
|
||||
@@ -176,10 +176,14 @@ export default function ClientReviewPage({
|
||||
toast({
|
||||
title:
|
||||
approvalDialog.status === "APPROVED"
|
||||
? "Version approved!"
|
||||
? "Shot approved!"
|
||||
: approvalDialog.status === "REJECTED"
|
||||
? "Version rejected"
|
||||
: "Changes requested",
|
||||
description:
|
||||
approvalDialog.status === "APPROVED"
|
||||
? "The shot has been marked as client approved."
|
||||
: undefined,
|
||||
});
|
||||
} catch {
|
||||
toast({ title: "Failed to submit decision", variant: "destructive" });
|
||||
|
||||
Reference in New Issue
Block a user