diff --git a/actions/shots.ts b/actions/shots.ts index 0627ccd..5233667 100644 --- a/actions/shots.ts +++ b/actions/shots.ts @@ -613,6 +613,8 @@ export async function internallyApproveShot(shotId: string) { /** * Producer/Supervisor/Admin: share an internally-approved shot with the client. * Sets sharedWithClient = true → status becomes CLIENT_REVIEW. + * Also marks the latest version of every task on this shot as isClientVisible = true + * so the client portal can display them. */ export async function shareWithClient(shotId: string) { const session = await auth(); @@ -623,16 +625,43 @@ export async function shareWithClient(shotId: string) { const shot = await db.shot.findUnique({ where: { id: shotId }, - select: { projectId: true, shotApprovalStatus: true }, + select: { + projectId: true, + shotApprovalStatus: true, + tasks: { select: { id: true } }, + }, }); if (!shot) throw new Error("Shot not found"); if (shot.shotApprovalStatus !== "INTERNALLY_APPROVED") { throw new Error("Shot must be internally approved before sharing with client"); } - await db.shot.update({ - where: { id: shotId }, - data: { sharedWithClient: true, status: "CLIENT_REVIEW" }, + const now = new Date(); + + await db.$transaction(async (tx) => { + // Mark shot as shared + await tx.shot.update({ + where: { id: shotId }, + data: { sharedWithClient: true, status: "CLIENT_REVIEW" }, + }); + + // For each task, make the latest version client-visible + for (const task of shot.tasks) { + const latest = await tx.version.findFirst({ + where: { taskId: task.id, isLatest: true }, + select: { id: true, isClientVisible: true }, + }); + if (latest && !latest.isClientVisible) { + await tx.version.update({ + where: { id: latest.id }, + data: { + isClientVisible: true, + sharedAt: now, + sharedById: session.user.id, + }, + }); + } + } }); revalidatePath(`/projects/${shot.projectId}`); @@ -644,6 +673,7 @@ export async function shareWithClient(shotId: string) { /** * Producer/Supervisor/Admin: remove a shot from client review. * Sets sharedWithClient = false → status becomes READY_FOR_CLIENT. + * Also hides all task versions that were made visible by shareWithClient. */ export async function unshareFromClient(shotId: string) { const session = await auth(); @@ -654,13 +684,27 @@ export async function unshareFromClient(shotId: string) { const shot = await db.shot.findUnique({ where: { id: shotId }, - select: { projectId: true }, + select: { + projectId: true, + tasks: { select: { id: true } }, + }, }); if (!shot) throw new Error("Shot not found"); - await db.shot.update({ - where: { id: shotId }, - data: { sharedWithClient: false, status: "READY_FOR_CLIENT" }, + await db.$transaction(async (tx) => { + await tx.shot.update({ + where: { id: shotId }, + data: { sharedWithClient: false, status: "READY_FOR_CLIENT" }, + }); + + // Hide all client-visible versions on this shot's tasks + const taskIds = shot.tasks.map((t) => t.id); + if (taskIds.length > 0) { + await tx.version.updateMany({ + where: { taskId: { in: taskIds }, isClientVisible: true }, + data: { isClientVisible: false }, + }); + } }); revalidatePath(`/projects/${shot.projectId}`); diff --git a/app/review/[versionId]/ReviewPageClient.tsx b/app/review/[versionId]/ReviewPageClient.tsx index fc3174d..e52eda7 100644 --- a/app/review/[versionId]/ReviewPageClient.tsx +++ b/app/review/[versionId]/ReviewPageClient.tsx @@ -25,6 +25,7 @@ import { Textarea } from "@/components/ui/textarea"; import { Label } from "@/components/ui/label"; import { cn } from "@/lib/utils"; import { submitApproval } from "@/actions/approvals"; +import { internallyApproveShot } from "@/actions/shots"; import { useToast } from "@/components/ui/use-toast"; import { ShareWithClientButton } from "@/components/versions/ShareWithClientButton"; import { @@ -34,6 +35,7 @@ import { ChevronDown, ArrowLeft, Film, + ShieldCheck, } from "lucide-react"; interface ReviewPageClientProps { @@ -110,10 +112,11 @@ export function ReviewPageClient({ const [pendingFrame, setPendingFrame] = useState(null); const [approvalDialog, setApprovalDialog] = useState<{ open: boolean; - status: "APPROVED" | "REJECTED" | "NEEDS_CHANGES" | null; + status: "REJECTED" | "NEEDS_CHANGES" | null; }>({ open: false, status: null }); const [approvalNotes, setApprovalNotes] = useState(""); const [isSubmittingApproval, setIsSubmittingApproval] = useState(false); + const [isInternallyApproving, setIsInternallyApproving] = useState(false); const [showVersions, setShowVersions] = useState(false); const playerRef = useRef(null); @@ -165,9 +168,7 @@ export function ReviewPageClient({ }); toast({ title: - approvalDialog.status === "APPROVED" - ? "Version approved!" - : approvalDialog.status === "REJECTED" + approvalDialog.status === "REJECTED" ? "Version rejected" : "Changes requested", }); @@ -176,7 +177,7 @@ export function ReviewPageClient({ router.refresh(); } catch (err) { toast({ - title: "Failed to submit approval", + title: "Failed to submit", description: (err as Error).message, variant: "destructive", }); @@ -185,7 +186,26 @@ export function ReviewPageClient({ } }; - const openApproval = (status: "APPROVED" | "REJECTED" | "NEEDS_CHANGES") => { + const handleInternalApprove = async () => { + const shotId = version.shot?.id; + if (!shotId) return; + setIsInternallyApproving(true); + try { + await internallyApproveShot(shotId); + toast({ title: "Shot internally approved", description: "Status set to Ready for Client" }); + router.refresh(); + } catch (err) { + toast({ + title: "Failed to approve", + description: (err as Error).message, + variant: "destructive", + }); + } finally { + setIsInternallyApproving(false); + } + }; + + const openApproval = (status: "REJECTED" | "NEEDS_CHANGES") => { setApprovalDialog({ open: true, status }); }; @@ -288,14 +308,19 @@ export function ReviewPageClient({ Reject - + {version.shot?.id && ( + + )} )} @@ -338,9 +363,7 @@ export function ReviewPageClient({ - {approvalDialog.status === "APPROVED" - ? "Approve Version" - : approvalDialog.status === "REJECTED" + {approvalDialog.status === "REJECTED" ? "Reject Version" : "Request Changes"} @@ -372,9 +395,7 @@ export function ReviewPageClient({ onClick={handleApprovalSubmit} disabled={isSubmittingApproval} className={ - approvalDialog.status === "APPROVED" - ? "bg-emerald-600 hover:bg-emerald-500 text-white" - : approvalDialog.status === "REJECTED" + approvalDialog.status === "REJECTED" ? "bg-red-600 hover:bg-red-500 text-white" : "bg-orange-600 hover:bg-orange-500 text-white" }