From 7788907a4ae9dc60876480566e7c9d18217b957e Mon Sep 17 00:00:00 2001 From: twotalesanimation <80506065+twotalesanimation@users.noreply.github.com> Date: Thu, 11 Jun 2026 15:55:57 +0200 Subject: [PATCH] task approval updates --- .../tasks/[taskId]/TaskDetailClient.tsx | 92 ++++++++++++++++--- app/api/client/[token]/approve/route.ts | 15 +++ .../[token]/review/[versionId]/page.tsx | 6 +- 3 files changed, 99 insertions(+), 14 deletions(-) diff --git a/app/(dashboard)/tasks/[taskId]/TaskDetailClient.tsx b/app/(dashboard)/tasks/[taskId]/TaskDetailClient.tsx index 0902fd5..0efa2a5 100644 --- a/app/(dashboard)/tasks/[taskId]/TaskDetailClient.tsx +++ b/app/(dashboard)/tasks/[taskId]/TaskDetailClient.tsx @@ -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 (
@@ -339,19 +366,58 @@ export function TaskDetailClient({ {canManage && ( - + + + + + + handleApproveVersion(v.id, "APPROVED")} + > + + Approve + + handleApproveVersion(v.id, "NEEDS_CHANGES")} + > + + Needs Changes + + handleApproveVersion(v.id, "REJECTED")} + > + + Reject + + {v.approvalStatus === "APPROVED" && ( + handleUnapproveVersion(v.id)} + > + + Undo Approval + + )} + + + handleDeleteVersion( + v.id, + `v${String(v.versionNumber).padStart(3, "0")}` + ) + } + > + + Delete version + + + )}
diff --git a/app/api/client/[token]/approve/route.ts b/app/api/client/[token]/approve/route.ts index e456c5f..2d8b0cd 100644 --- a/app/api/client/[token]/approve/route.ts +++ b/app/api/client/[token]/approve/route.ts @@ -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" } }); } diff --git a/app/client/[token]/review/[versionId]/page.tsx b/app/client/[token]/review/[versionId]/page.tsx index 733ea17..a9bcf0d 100644 --- a/app/client/[token]/review/[versionId]/page.tsx +++ b/app/client/[token]/review/[versionId]/page.tsx @@ -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" });