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,
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>
+15
View File
@@ -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" });