Added approval buttons to playlist
Deploy / deploy (push) Successful in 2m45s

This commit is contained in:
twotalesanimation
2026-06-12 07:53:07 +02:00
parent 7788907a4a
commit d56f3b3ad5
3 changed files with 85 additions and 2 deletions
+80 -1
View File
@@ -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"
+1 -1
View File
@@ -15,5 +15,5 @@ export default async function PlaylistPage() {
orderBy: { name: "asc" },
});
return <PlaylistClient projects={projects} />;
return <PlaylistClient projects={projects} userRole={session.user.role} />;
}
+4
View File
@@ -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,