diff --git a/actions/shots.ts b/actions/shots.ts index bdad131..0e85ffc 100644 --- a/actions/shots.ts +++ b/actions/shots.ts @@ -20,6 +20,7 @@ const createShotSchema = z.object({ dueDate: z.string().optional(), thumbnailUrl: z.string().optional(), shotGroupName: z.string().max(100).optional(), + isKeyShot: z.boolean().default(false), }); export async function createShot(data: z.infer) { @@ -85,6 +86,7 @@ export async function createShot(data: z.infer) { frameEnd: parsed.frameEnd, dueDate: parsed.dueDate ? new Date(parsed.dueDate) : undefined, thumbnailUrl: parsed.thumbnailUrl, + isKeyShot: parsed.isKeyShot ?? false, shotGroupId: parsed.shotGroupName?.trim() ? (await db.shotGroup.upsert({ where: { projectId_name: { projectId: parsed.projectId, name: parsed.shotGroupName.trim() } }, @@ -487,6 +489,31 @@ export async function updateShotNotes(shotId: string, notes: string) { return { success: true }; } +// ── Toggle Key Shot ─────────────────────────────────────────────────────────── + +export async function toggleKeyShot(shotId: string, isKeyShot: boolean) { + const session = await auth(); + if (!session?.user) throw new Error("Unauthorized"); + if (!["ADMIN", "PRODUCER", "SUPERVISOR"].includes(session.user.role)) { + throw new Error("Insufficient permissions"); + } + + const shot = await db.shot.findUnique({ + where: { id: shotId }, + select: { projectId: true }, + }); + if (!shot) throw new Error("Shot not found"); + + await db.shot.update({ + where: { id: shotId }, + data: { isKeyShot }, + }); + + revalidatePath(`/projects/${shot.projectId}`); + revalidatePath(`/shot-status`); + return { success: true }; +} + export async function deleteShot(shotId: string) { const session = await auth(); if (!session?.user) throw new Error("Unauthorized"); diff --git a/app/(dashboard)/shot-status/ShotStatusClient.tsx b/app/(dashboard)/shot-status/ShotStatusClient.tsx index af3073d..0dec056 100644 --- a/app/(dashboard)/shot-status/ShotStatusClient.tsx +++ b/app/(dashboard)/shot-status/ShotStatusClient.tsx @@ -22,7 +22,7 @@ import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { cn } from "@/lib/utils"; -import { updateShotNotes, bulkUpdateDueDates } from "@/actions/shots"; +import { updateShotNotes, bulkUpdateDueDates, toggleKeyShot } from "@/actions/shots"; import { useToast } from "@/components/ui/use-toast"; import { Film, @@ -37,6 +37,7 @@ import { X, ShieldCheck, Eye, + Star, } from "lucide-react"; import { format } from "date-fns"; @@ -52,6 +53,7 @@ type ShotRow = { thumbnailUrl: string | null; notes: string | null; description: string | null; + isKeyShot: boolean; artist: { id: string; name: string | null; image: string | null; email: string } | null; }; @@ -261,6 +263,49 @@ function NotesCell({ ); } +// ── KeyShotToggle ───────────────────────────────────────────────────────────── + +function KeyShotToggle({ shot }: { shot: ShotRow }) { + const [optimistic, setOptimistic] = useState(shot.isKeyShot); + const [isPending, startTransition] = useTransition(); + const { toast } = useToast(); + + const handle = () => { + const next = !optimistic; + setOptimistic(next); + startTransition(async () => { + try { + await toggleKeyShot(shot.id, next); + } catch { + setOptimistic(!next); + toast({ title: "Failed to update key shot", variant: "destructive" }); + } + }); + }; + + return ( + + ); +} + function ShotTable({ shots, canManage, @@ -297,6 +342,12 @@ function ShotTable({ Status Due Date Notes + + + + Key + + @@ -308,7 +359,13 @@ function ShotTable({ const isSelected = selectedIds.has(shot.id); return ( - + {/* Checkbox */} {canManage && ( @@ -340,13 +397,18 @@ function ShotTable({ {/* Shot name */} - sessionStorage.setItem(SCROLL_KEY, String(window.scrollY))} - > - {shot.shotCode} - +
+ {shot.isKeyShot && ( + + )} + sessionStorage.setItem(SCROLL_KEY, String(window.scrollY))} + > + {shot.shotCode} + +
{shot.description && (

{shot.description} @@ -383,6 +445,11 @@ function ShotTable({ + + {/* Key shot toggle */} + + + ); })} diff --git a/app/(dashboard)/shot-status/page.tsx b/app/(dashboard)/shot-status/page.tsx index e086fea..4c6942e 100644 --- a/app/(dashboard)/shot-status/page.tsx +++ b/app/(dashboard)/shot-status/page.tsx @@ -39,6 +39,7 @@ async function getShotsForProject(projectId: string) { thumbnailUrl: true, notes: true, description: true, + isKeyShot: true, artist: { select: { id: true, name: true, image: true, email: true } }, }, }); diff --git a/components/shots/NewShotDialog.tsx b/components/shots/NewShotDialog.tsx index 9202d25..b304da9 100644 --- a/components/shots/NewShotDialog.tsx +++ b/components/shots/NewShotDialog.tsx @@ -25,6 +25,7 @@ import { } from "@/components/ui/select"; import { createShot } from "@/actions/shots"; import { useToast } from "@/components/ui/use-toast"; +import { Star } from "lucide-react"; const shotSchema = z.object({ scene: z.string().min(1, "Scene is required").max(50).regex(/^[A-Z0-9_]+$/i, "Alphanumeric and underscore only"), @@ -50,6 +51,7 @@ export function NewShotDialog({ projectId, projectType = "STANDARD", shotGroups const [isSubmitting, setIsSubmitting] = useState(false); const [thumbnailFile, setThumbnailFile] = useState(null); const [thumbnailPreview, setThumbnailPreview] = useState(null); + const [isKeyShot, setIsKeyShot] = useState(false); const { toast } = useToast(); const router = useRouter(); const isEpisodic = projectType === "EPISODIC"; @@ -101,11 +103,12 @@ export function NewShotDialog({ projectId, projectType = "STANDARD", shotGroups thumbnailUrl = uploadData.url; } - await createShot({ projectId, ...data, thumbnailUrl }); + await createShot({ projectId, ...data, thumbnailUrl, isKeyShot }); toast({ title: "Shot created" }); reset(); setThumbnailFile(null); setThumbnailPreview(null); + setIsKeyShot(false); router.refresh(); onSuccess?.(); onClose(); @@ -240,6 +243,32 @@ export function NewShotDialog({ projectId, projectType = "STANDARD", shotGroups + {/* Key shot toggle */} +

+
+ +
+

Key Shot

+

Prioritised and highlighted across the project

+
+
+ +
+