share settings updated
Deploy / deploy (push) Failing after 1m43s

This commit is contained in:
twotalesanimation
2026-06-11 13:42:26 +02:00
parent c9c192d2b3
commit e801476574
2 changed files with 93 additions and 28 deletions
+52 -8
View File
@@ -613,6 +613,8 @@ export async function internallyApproveShot(shotId: string) {
/** /**
* Producer/Supervisor/Admin: share an internally-approved shot with the client. * Producer/Supervisor/Admin: share an internally-approved shot with the client.
* Sets sharedWithClient = true → status becomes CLIENT_REVIEW. * 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) { export async function shareWithClient(shotId: string) {
const session = await auth(); const session = await auth();
@@ -623,16 +625,43 @@ export async function shareWithClient(shotId: string) {
const shot = await db.shot.findUnique({ const shot = await db.shot.findUnique({
where: { id: shotId }, 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) throw new Error("Shot not found");
if (shot.shotApprovalStatus !== "INTERNALLY_APPROVED") { if (shot.shotApprovalStatus !== "INTERNALLY_APPROVED") {
throw new Error("Shot must be internally approved before sharing with client"); throw new Error("Shot must be internally approved before sharing with client");
} }
await db.shot.update({ const now = new Date();
where: { id: shotId },
data: { sharedWithClient: true, status: "CLIENT_REVIEW" }, 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}`); revalidatePath(`/projects/${shot.projectId}`);
@@ -644,6 +673,7 @@ export async function shareWithClient(shotId: string) {
/** /**
* Producer/Supervisor/Admin: remove a shot from client review. * Producer/Supervisor/Admin: remove a shot from client review.
* Sets sharedWithClient = false → status becomes READY_FOR_CLIENT. * 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) { export async function unshareFromClient(shotId: string) {
const session = await auth(); const session = await auth();
@@ -654,13 +684,27 @@ export async function unshareFromClient(shotId: string) {
const shot = await db.shot.findUnique({ const shot = await db.shot.findUnique({
where: { id: shotId }, where: { id: shotId },
select: { projectId: true }, select: {
projectId: true,
tasks: { select: { id: true } },
},
}); });
if (!shot) throw new Error("Shot not found"); if (!shot) throw new Error("Shot not found");
await db.shot.update({ await db.$transaction(async (tx) => {
where: { id: shotId }, await tx.shot.update({
data: { sharedWithClient: false, status: "READY_FOR_CLIENT" }, 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}`); revalidatePath(`/projects/${shot.projectId}`);
+41 -20
View File
@@ -25,6 +25,7 @@ import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { submitApproval } from "@/actions/approvals"; import { submitApproval } from "@/actions/approvals";
import { internallyApproveShot } from "@/actions/shots";
import { useToast } from "@/components/ui/use-toast"; import { useToast } from "@/components/ui/use-toast";
import { ShareWithClientButton } from "@/components/versions/ShareWithClientButton"; import { ShareWithClientButton } from "@/components/versions/ShareWithClientButton";
import { import {
@@ -34,6 +35,7 @@ import {
ChevronDown, ChevronDown,
ArrowLeft, ArrowLeft,
Film, Film,
ShieldCheck,
} from "lucide-react"; } from "lucide-react";
interface ReviewPageClientProps { interface ReviewPageClientProps {
@@ -110,10 +112,11 @@ export function ReviewPageClient({
const [pendingFrame, setPendingFrame] = useState<number | null>(null); const [pendingFrame, setPendingFrame] = useState<number | null>(null);
const [approvalDialog, setApprovalDialog] = useState<{ const [approvalDialog, setApprovalDialog] = useState<{
open: boolean; open: boolean;
status: "APPROVED" | "REJECTED" | "NEEDS_CHANGES" | null; status: "REJECTED" | "NEEDS_CHANGES" | null;
}>({ open: false, status: null }); }>({ open: false, status: null });
const [approvalNotes, setApprovalNotes] = useState(""); const [approvalNotes, setApprovalNotes] = useState("");
const [isSubmittingApproval, setIsSubmittingApproval] = useState(false); const [isSubmittingApproval, setIsSubmittingApproval] = useState(false);
const [isInternallyApproving, setIsInternallyApproving] = useState(false);
const [showVersions, setShowVersions] = useState(false); const [showVersions, setShowVersions] = useState(false);
const playerRef = useRef<ReviewPlayerRef>(null); const playerRef = useRef<ReviewPlayerRef>(null);
@@ -165,9 +168,7 @@ export function ReviewPageClient({
}); });
toast({ toast({
title: title:
approvalDialog.status === "APPROVED" approvalDialog.status === "REJECTED"
? "Version approved!"
: approvalDialog.status === "REJECTED"
? "Version rejected" ? "Version rejected"
: "Changes requested", : "Changes requested",
}); });
@@ -176,7 +177,7 @@ export function ReviewPageClient({
router.refresh(); router.refresh();
} catch (err) { } catch (err) {
toast({ toast({
title: "Failed to submit approval", title: "Failed to submit",
description: (err as Error).message, description: (err as Error).message,
variant: "destructive", 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 }); setApprovalDialog({ open: true, status });
}; };
@@ -288,14 +308,19 @@ export function ReviewPageClient({
<XCircle className="h-3 w-3" /> <XCircle className="h-3 w-3" />
<span className="hidden sm:inline">Reject</span> <span className="hidden sm:inline">Reject</span>
</Button> </Button>
<Button {version.shot?.id && (
size="sm" <Button
className="h-7 text-xs gap-1 bg-emerald-600 hover:bg-emerald-500 text-white" size="sm"
onClick={() => openApproval("APPROVED")} className="h-7 text-xs gap-1 bg-sky-600 hover:bg-sky-500 text-white"
> onClick={handleInternalApprove}
<CheckCircle2 className="h-3 w-3" /> disabled={isInternallyApproving}
<span className="hidden sm:inline">Approve</span> >
</Button> <ShieldCheck className="h-3 w-3" />
<span className="hidden sm:inline">
{isInternallyApproving ? "Approving…" : "Approve Internally"}
</span>
</Button>
)}
</div> </div>
)} )}
</div> </div>
@@ -338,9 +363,7 @@ export function ReviewPageClient({
<DialogContent className="sm:max-w-md"> <DialogContent className="sm:max-w-md">
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>
{approvalDialog.status === "APPROVED" {approvalDialog.status === "REJECTED"
? "Approve Version"
: approvalDialog.status === "REJECTED"
? "Reject Version" ? "Reject Version"
: "Request Changes"} : "Request Changes"}
</DialogTitle> </DialogTitle>
@@ -372,9 +395,7 @@ export function ReviewPageClient({
onClick={handleApprovalSubmit} onClick={handleApprovalSubmit}
disabled={isSubmittingApproval} disabled={isSubmittingApproval}
className={ className={
approvalDialog.status === "APPROVED" approvalDialog.status === "REJECTED"
? "bg-emerald-600 hover:bg-emerald-500 text-white"
: approvalDialog.status === "REJECTED"
? "bg-red-600 hover:bg-red-500 text-white" ? "bg-red-600 hover:bg-red-500 text-white"
: "bg-orange-600 hover:bg-orange-500 text-white" : "bg-orange-600 hover:bg-orange-500 text-white"
} }