+52
-8
@@ -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}`);
|
||||
|
||||
@@ -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<number | null>(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<ReviewPlayerRef>(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({
|
||||
<XCircle className="h-3 w-3" />
|
||||
<span className="hidden sm:inline">Reject</span>
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
className="h-7 text-xs gap-1 bg-emerald-600 hover:bg-emerald-500 text-white"
|
||||
onClick={() => openApproval("APPROVED")}
|
||||
>
|
||||
<CheckCircle2 className="h-3 w-3" />
|
||||
<span className="hidden sm:inline">Approve</span>
|
||||
</Button>
|
||||
{version.shot?.id && (
|
||||
<Button
|
||||
size="sm"
|
||||
className="h-7 text-xs gap-1 bg-sky-600 hover:bg-sky-500 text-white"
|
||||
onClick={handleInternalApprove}
|
||||
disabled={isInternallyApproving}
|
||||
>
|
||||
<ShieldCheck className="h-3 w-3" />
|
||||
<span className="hidden sm:inline">
|
||||
{isInternallyApproving ? "Approving…" : "Approve Internally"}
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -338,9 +363,7 @@ export function ReviewPageClient({
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{approvalDialog.status === "APPROVED"
|
||||
? "Approve Version"
|
||||
: approvalDialog.status === "REJECTED"
|
||||
{approvalDialog.status === "REJECTED"
|
||||
? "Reject Version"
|
||||
: "Request Changes"}
|
||||
</DialogTitle>
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user