@@ -17,11 +17,13 @@ import {
|
|||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { getInitials, formatRelativeDate, formatFileSize } from "@/lib/utils";
|
import { getInitials, formatRelativeDate, formatFileSize } from "@/lib/utils";
|
||||||
import { updateTask, updateTaskStatus } from "@/actions/tasks";
|
import { updateTask, updateTaskStatus } from "@/actions/tasks";
|
||||||
|
import { submitApproval, unapproveVersion } from "@/actions/approvals";
|
||||||
import { useToast } from "@/components/ui/use-toast";
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
import { TaskStatus, TaskType } from "@prisma/client";
|
import { TaskStatus, TaskType } from "@prisma/client";
|
||||||
import { formatDistanceToNow, format } from "date-fns";
|
import { formatDistanceToNow, format } from "date-fns";
|
||||||
@@ -42,6 +44,8 @@ import {
|
|||||||
User,
|
User,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
Trash2,
|
Trash2,
|
||||||
|
XCircle,
|
||||||
|
MoreHorizontal,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { TASK_STATUS_CONFIG, TASK_TYPE_LABELS } from "@/components/tasks/TaskCard";
|
import { TASK_STATUS_CONFIG, TASK_TYPE_LABELS } from "@/components/tasks/TaskCard";
|
||||||
import { VersionUpload } from "@/components/versions/VersionUpload";
|
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 (
|
return (
|
||||||
<div className="min-h-screen bg-background">
|
<div className="min-h-screen bg-background">
|
||||||
<div className="max-w-5xl mx-auto p-6 space-y-6">
|
<div className="max-w-5xl mx-auto p-6 space-y-6">
|
||||||
@@ -339,10 +366,46 @@ export function TaskDetailClient({
|
|||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
{canManage && (
|
{canManage && (
|
||||||
<Button
|
<DropdownMenu>
|
||||||
variant="ghost"
|
<DropdownMenuTrigger asChild>
|
||||||
size="icon"
|
<Button variant="ghost" size="icon" className="h-7 w-7 text-muted-foreground">
|
||||||
className="h-7 w-7 text-muted-foreground hover:text-red-500"
|
<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={() =>
|
onClick={() =>
|
||||||
handleDeleteVersion(
|
handleDeleteVersion(
|
||||||
v.id,
|
v.id,
|
||||||
@@ -350,8 +413,11 @@ export function TaskDetailClient({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-3.5 w-3.5" />
|
<Trash2 className="h-3.5 w-3.5 mr-2" />
|
||||||
</Button>
|
Delete version
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -113,6 +113,21 @@ export async function POST(
|
|||||||
if (version.task) {
|
if (version.task) {
|
||||||
if (status === "APPROVED") {
|
if (status === "APPROVED") {
|
||||||
await db.task.update({ where: { id: version.task.id }, data: { status: "DONE" } });
|
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 {
|
} else {
|
||||||
await db.task.update({ where: { id: version.task.id }, data: { status: "CHANGES" } });
|
await db.task.update({ where: { id: version.task.id }, data: { status: "CHANGES" } });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -176,10 +176,14 @@ export default function ClientReviewPage({
|
|||||||
toast({
|
toast({
|
||||||
title:
|
title:
|
||||||
approvalDialog.status === "APPROVED"
|
approvalDialog.status === "APPROVED"
|
||||||
? "Version approved!"
|
? "Shot approved!"
|
||||||
: approvalDialog.status === "REJECTED"
|
: approvalDialog.status === "REJECTED"
|
||||||
? "Version rejected"
|
? "Version rejected"
|
||||||
: "Changes requested",
|
: "Changes requested",
|
||||||
|
description:
|
||||||
|
approvalDialog.status === "APPROVED"
|
||||||
|
? "The shot has been marked as client approved."
|
||||||
|
: undefined,
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
toast({ title: "Failed to submit decision", variant: "destructive" });
|
toast({ title: "Failed to submit decision", variant: "destructive" });
|
||||||
|
|||||||
Reference in New Issue
Block a user