fix(review): Share with Client in the player shares the shot
Deploy / deploy (push) Successful in 4m14s
Deploy / deploy (push) Successful in 4m14s
The review player's button only made the viewed version client-visible. It never set shot.sharedWithClient, and the client portal lists only shared shots (and only each task's latest version), so the button reported "Shared with Client" while the client saw nothing. It also skipped the internal approval the shot page requires. For a version that belongs to a shot (directly or through its task), the button now runs the shot page's shareWithClient: shot marked shared, latest version of each task visible, status to Client Review. It is disabled with an explanation until the shot is internally approved, and the player's Approve Internally button now appears for task versions too, only while the shot awaits approval. Asset versions keep the per-version share. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -56,6 +56,8 @@ interface ReviewPageClientProps {
|
||||
shot?: {
|
||||
id: string;
|
||||
shotCode: string;
|
||||
shotApprovalStatus: string;
|
||||
sharedWithClient: boolean;
|
||||
project: { id: string; name: string; code: string };
|
||||
versions: {
|
||||
id: string;
|
||||
@@ -73,7 +75,7 @@ interface ReviewPageClientProps {
|
||||
id: string;
|
||||
title: string;
|
||||
project: { id: string; name: string; code: string };
|
||||
shot?: { id: string; shotCode: string } | null;
|
||||
shot?: { id: string; shotCode: string; shotApprovalStatus: string; sharedWithClient: boolean } | null;
|
||||
asset?: { id: string; assetCode: string; name: string } | null;
|
||||
versions: {
|
||||
id: string;
|
||||
@@ -127,6 +129,11 @@ export function ReviewPageClient({
|
||||
const [commentsOpen, setCommentsOpen] = useState(true);
|
||||
|
||||
const playerRef = useRef<ReviewPlayerRef>(null);
|
||||
|
||||
// The shot this version belongs to, directly or through its task. Approval
|
||||
// and client sharing are shot-level: the client portal only lists shots
|
||||
// marked sharedWithClient, so sharing has to go through the shot.
|
||||
const reviewShot = version.shot ?? version.task?.shot ?? null;
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
|
||||
@@ -194,7 +201,7 @@ export function ReviewPageClient({
|
||||
};
|
||||
|
||||
const handleInternalApprove = async () => {
|
||||
const shotId = version.shot?.id;
|
||||
const shotId = reviewShot?.id;
|
||||
if (!shotId) return;
|
||||
setIsInternallyApproving(true);
|
||||
try {
|
||||
@@ -295,7 +302,8 @@ export function ReviewPageClient({
|
||||
<ShareWithClientButton
|
||||
versionId={version.id}
|
||||
isAlreadyShared={version.isClientVisible}
|
||||
onShared={() => {/* router.refresh() handled inside component */}}
|
||||
shot={reviewShot}
|
||||
onShared={() => router.refresh()}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -320,7 +328,8 @@ export function ReviewPageClient({
|
||||
<XCircle className="h-3 w-3" />
|
||||
<span className="hidden sm:inline">Reject</span>
|
||||
</Button>
|
||||
{version.shot?.id && (
|
||||
{/* Same rule as the shot page: only while the shot awaits internal approval. */}
|
||||
{reviewShot?.shotApprovalStatus === "PENDING" && (
|
||||
<Button
|
||||
size="sm"
|
||||
className="h-7 text-xs gap-1 bg-sky-600 hover:bg-sky-500 text-white"
|
||||
|
||||
@@ -45,7 +45,7 @@ async function getReviewData(versionId: string) {
|
||||
task: {
|
||||
include: {
|
||||
project: { select: { id: true, name: true, code: true } },
|
||||
shot: { select: { id: true, shotCode: true } },
|
||||
shot: { select: { id: true, shotCode: true, shotApprovalStatus: true, sharedWithClient: true } },
|
||||
asset: { select: { id: true, assetCode: true, name: true } },
|
||||
versions: {
|
||||
orderBy: { versionNumber: "desc" },
|
||||
|
||||
@@ -4,36 +4,56 @@ import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { shareVersionWithClient } from "@/actions/versions";
|
||||
import { shareWithClient } from "@/actions/shots";
|
||||
import { Send, CheckCircle2 } from "lucide-react";
|
||||
|
||||
interface ShareWithClientButtonProps {
|
||||
versionId: string;
|
||||
isAlreadyShared?: boolean;
|
||||
/**
|
||||
* The shot the version belongs to, if any. Sharing is then shot-level —
|
||||
* the same action as the shot page's button — because the client portal
|
||||
* only lists shots marked sharedWithClient; making just this version
|
||||
* visible left the client seeing nothing.
|
||||
*/
|
||||
shot?: { id: string; shotApprovalStatus: string; sharedWithClient: boolean } | null;
|
||||
onShared?: () => void;
|
||||
size?: "sm" | "default";
|
||||
}
|
||||
|
||||
const SHAREABLE = ["INTERNALLY_APPROVED", "CLIENT_APPROVED"];
|
||||
|
||||
export function ShareWithClientButton({
|
||||
versionId,
|
||||
isAlreadyShared = false,
|
||||
shot,
|
||||
onShared,
|
||||
size = "sm",
|
||||
}: ShareWithClientButtonProps) {
|
||||
const [shared, setShared] = useState(isAlreadyShared);
|
||||
const [sharedHere, setSharedHere] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { toast } = useToast();
|
||||
|
||||
// Read from props so a refresh after approving or unsharing elsewhere shows.
|
||||
const shared = sharedHere || (shot ? shot.sharedWithClient : isAlreadyShared);
|
||||
const needsApproval = !!shot && !SHAREABLE.includes(shot.shotApprovalStatus);
|
||||
|
||||
const handleShare = async () => {
|
||||
if (shared) return;
|
||||
if (shared || needsApproval) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
await shareVersionWithClient(versionId);
|
||||
setShared(true);
|
||||
toast({ title: "Version shared with client" });
|
||||
if (shot) {
|
||||
await shareWithClient(shot.id);
|
||||
toast({ title: "Shot shared with client", description: "The latest version of each task is now in client review" });
|
||||
} else {
|
||||
await shareVersionWithClient(versionId);
|
||||
toast({ title: "Version shared with client" });
|
||||
}
|
||||
setSharedHere(true);
|
||||
onShared?.();
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: "Failed to share version",
|
||||
title: "Failed to share",
|
||||
description: (err as Error).message,
|
||||
variant: "destructive",
|
||||
});
|
||||
@@ -56,11 +76,11 @@ export function ShareWithClientButton({
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
const button = (
|
||||
<Button
|
||||
size={size}
|
||||
variant="outline"
|
||||
disabled={loading}
|
||||
disabled={loading || needsApproval}
|
||||
onClick={handleShare}
|
||||
className="h-7 text-xs gap-1 text-amber-400 border-amber-500/30 hover:bg-amber-500/10"
|
||||
>
|
||||
@@ -68,4 +88,13 @@ export function ShareWithClientButton({
|
||||
<span className="hidden sm:inline">{loading ? "Sharing…" : "Share with Client"}</span>
|
||||
</Button>
|
||||
);
|
||||
|
||||
// A disabled button takes no pointer events, so the explanation sits on a wrapper.
|
||||
return needsApproval ? (
|
||||
<span title="Approve the shot internally before sharing it with the client" className="inline-flex">
|
||||
{button}
|
||||
</span>
|
||||
) : (
|
||||
button
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user