This commit is contained in:
@@ -14,6 +14,9 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { internallyApproveShot, shareWithClient } from "@/actions/shots";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import {
|
||||
Film,
|
||||
Loader2,
|
||||
@@ -21,6 +24,8 @@ import {
|
||||
AlertCircle,
|
||||
Clock,
|
||||
ExternalLink,
|
||||
ShieldCheck,
|
||||
Send,
|
||||
} from "lucide-react";
|
||||
|
||||
interface Project {
|
||||
@@ -46,12 +51,15 @@ interface PlaylistShot {
|
||||
shotCode: string;
|
||||
episode: string | null;
|
||||
status: string;
|
||||
shotApprovalStatus: string;
|
||||
sharedWithClient: boolean;
|
||||
thumbnailUrl: string | null;
|
||||
latestVersion: PlaylistVersion;
|
||||
}
|
||||
|
||||
interface PlaylistClientProps {
|
||||
projects: Project[];
|
||||
userRole: string;
|
||||
}
|
||||
|
||||
const APPROVAL_COLORS: Record<string, string> = {
|
||||
@@ -68,14 +76,18 @@ const APPROVAL_ICONS: Record<string, React.ElementType> = {
|
||||
NEEDS_CHANGES: AlertCircle,
|
||||
};
|
||||
|
||||
export function PlaylistClient({ projects }: PlaylistClientProps) {
|
||||
export function PlaylistClient({ projects, userRole }: PlaylistClientProps) {
|
||||
const playerRef = useRef<ReviewPlayerRef>(null);
|
||||
const reset = useReviewStore((s) => s.reset);
|
||||
const { toast } = useToast();
|
||||
|
||||
const [projectId, setProjectId] = useState<string | null>(null);
|
||||
const [shots, setShots] = useState<PlaylistShot[]>([]);
|
||||
const [activeShot, setActiveShot] = useState<PlaylistShot | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isActioning, setIsActioning] = useState(false);
|
||||
|
||||
const canApprove = ["ADMIN", "PRODUCER", "SUPERVISOR"].includes(userRole);
|
||||
|
||||
// Restore last project from localStorage on mount
|
||||
useEffect(() => {
|
||||
@@ -125,6 +137,46 @@ export function PlaylistClient({ projects }: PlaylistClientProps) {
|
||||
setActiveShot(shot);
|
||||
}, [reset]);
|
||||
|
||||
const refreshShots = useCallback(() => {
|
||||
if (!projectId) return;
|
||||
fetch(`/api/playlist?projectId=${projectId}`)
|
||||
.then((r) => r.json())
|
||||
.then((data) => {
|
||||
const list: PlaylistShot[] = data.shots ?? [];
|
||||
setShots(list);
|
||||
setActiveShot((prev) => prev ? (list.find((s) => s.id === prev.id) ?? prev) : null);
|
||||
})
|
||||
.catch(() => {});
|
||||
}, [projectId]);
|
||||
|
||||
const handleInternallyApprove = async () => {
|
||||
if (!activeShot) return;
|
||||
setIsActioning(true);
|
||||
try {
|
||||
await internallyApproveShot(activeShot.id);
|
||||
toast({ title: "Shot internally approved", description: "Status: Ready for Client" });
|
||||
refreshShots();
|
||||
} catch (e) {
|
||||
toast({ title: "Failed", description: (e as Error).message, variant: "destructive" });
|
||||
} finally {
|
||||
setIsActioning(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleShareWithClient = async () => {
|
||||
if (!activeShot) return;
|
||||
setIsActioning(true);
|
||||
try {
|
||||
await shareWithClient(activeShot.id);
|
||||
toast({ title: "Shared with client", description: "Status: Client Review" });
|
||||
refreshShots();
|
||||
} catch (e) {
|
||||
toast({ title: "Failed", description: (e as Error).message, variant: "destructive" });
|
||||
} finally {
|
||||
setIsActioning(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Group shots by episode for episodic projects
|
||||
const selectedProject = projects.find((p) => p.id === projectId);
|
||||
const isEpisodic = selectedProject?.projectType === "EPISODIC";
|
||||
@@ -173,6 +225,33 @@ export function PlaylistClient({ projects }: PlaylistClientProps) {
|
||||
})()}
|
||||
{activeShot.latestVersion.approvalStatus.replace(/_/g, " ")}
|
||||
</Badge>
|
||||
|
||||
{canApprove && activeShot.shotApprovalStatus === "PENDING" && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={isActioning}
|
||||
onClick={handleInternallyApprove}
|
||||
className="h-7 text-xs gap-1 text-purple-400 border-purple-500/30 hover:bg-purple-500/10"
|
||||
>
|
||||
<ShieldCheck className="h-3 w-3" />
|
||||
<span className="hidden sm:inline">Internally Approve</span>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{canApprove && activeShot.shotApprovalStatus === "INTERNALLY_APPROVED" && !activeShot.sharedWithClient && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={isActioning}
|
||||
onClick={handleShareWithClient}
|
||||
className="h-7 text-xs gap-1 text-amber-400 border-amber-500/30 hover:bg-amber-500/10"
|
||||
>
|
||||
<Send className="h-3 w-3" />
|
||||
<span className="hidden sm:inline">Share with Client</span>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Link
|
||||
href={`/review/${activeShot.latestVersion.id}`}
|
||||
className="text-zinc-500 hover:text-zinc-200 transition-colors"
|
||||
|
||||
@@ -15,5 +15,5 @@ export default async function PlaylistPage() {
|
||||
orderBy: { name: "asc" },
|
||||
});
|
||||
|
||||
return <PlaylistClient projects={projects} />;
|
||||
return <PlaylistClient projects={projects} userRole={session.user.role} />;
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ export async function GET(req: NextRequest) {
|
||||
shotCode: true,
|
||||
episode: true,
|
||||
status: true,
|
||||
shotApprovalStatus: true,
|
||||
sharedWithClient: true,
|
||||
thumbnailUrl: true,
|
||||
tasks: {
|
||||
select: {
|
||||
@@ -71,6 +73,8 @@ export async function GET(req: NextRequest) {
|
||||
shotCode: shot.shotCode,
|
||||
episode: shot.episode,
|
||||
status: shot.status,
|
||||
shotApprovalStatus: shot.shotApprovalStatus,
|
||||
sharedWithClient: shot.sharedWithClient,
|
||||
thumbnailUrl: shot.thumbnailUrl,
|
||||
latestVersion: {
|
||||
id: latest.id,
|
||||
|
||||
Reference in New Issue
Block a user