task approval updates
Deploy / deploy (push) Successful in 2m38s

This commit is contained in:
twotalesanimation
2026-06-11 15:55:57 +02:00
parent b640fa472c
commit 7788907a4a
3 changed files with 99 additions and 14 deletions
@@ -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>
+15
View File
@@ -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" });