From d7e37de18f1ff0c78c1f7d8f9bd8fd5bf327643b Mon Sep 17 00:00:00 2001 From: twotalesanimation <80506065+twotalesanimation@users.noreply.github.com> Date: Sun, 11 Oct 2026 18:10:51 +0200 Subject: [PATCH] feat(deliveries): choose which ready shots go into a package Each QC-passed shot in "Ready for delivery" gets a checkbox; all start selected, with a per-episode Select/Deselect all. Build sends the explicit exportIds ticked on screen, so a shot that turns ready after the page loaded never slips into a package unseen. Co-Authored-By: Claude Opus 5.5 --- .../pipeline/deliveries/DeliveriesClient.tsx | 97 ++++++++++++++++--- 1 file changed, 82 insertions(+), 15 deletions(-) diff --git a/app/(dashboard)/pipeline/deliveries/DeliveriesClient.tsx b/app/(dashboard)/pipeline/deliveries/DeliveriesClient.tsx index 8152b2b..b462e71 100644 --- a/app/(dashboard)/pipeline/deliveries/DeliveriesClient.tsx +++ b/app/(dashboard)/pipeline/deliveries/DeliveriesClient.tsx @@ -83,6 +83,9 @@ export function DeliveriesClient({ canBuild }: { canBuild: boolean }) { const [projectId, setProjectId] = useState(""); const [episode, setEpisode] = useState(""); const [building, setBuilding] = useState(false); + // Tracked as exclusions so every ready shot — including one that passes QC + // while the page is open — starts selected. + const [deselected, setDeselected] = useState>(() => new Set()); const [handoff, setHandoff] = useState<{ step: "upload" | "share"; packageId: string } | null>(null); const { data: projects } = useQuery({ @@ -116,14 +119,36 @@ export function DeliveriesClient({ canBuild }: { canBuild: boolean }) { refetchInterval: 5000, }); - async function build(ep: string) { - if (!projectId) return; + function toggleExport(exportId: string, selected: boolean) { + setDeselected((prev) => { + const next = new Set(prev); + if (selected) next.delete(exportId); + else next.add(exportId); + return next; + }); + } + + function setEpisodeSelected(exportIds: string[], selected: boolean) { + setDeselected((prev) => { + const next = new Set(prev); + for (const id of exportIds) { + if (selected) next.delete(id); + else next.add(id); + } + return next; + }); + } + + async function build(ep: string, exportIds: string[]) { + if (!projectId || exportIds.length === 0) return; setBuilding(true); try { + // Always send the explicit list: the package holds exactly the shots + // ticked on screen, never one that turned ready since the page loaded. const res = await fetch("/api/deliveries", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ projectId, episode: ep || null }), + body: JSON.stringify({ projectId, episode: ep || null, exportIds }), }); const body = await res.json().catch(() => ({})); if (!res.ok) { @@ -179,6 +204,7 @@ export function DeliveriesClient({ canBuild }: { canBuild: boolean }) { onChange={(e) => { setProjectId(e.target.value); setEpisode(""); + setDeselected(new Set()); }} className="rounded-md border border-zinc-800 bg-zinc-900 px-3 py-1.5 text-sm text-zinc-200" > @@ -219,30 +245,69 @@ export function DeliveriesClient({ canBuild }: { canBuild: boolean }) { Nothing has passed QC yet. Exports appear here once QC passes them. )} - {shown.map((ep) => ( + {shown.map((ep) => { + const allIds = ep.exports.map((x) => x.exportId); + const selectedIds = allIds.filter((id) => !deselected.has(id)); + const allSelected = selectedIds.length === allIds.length; + const selectedExports = ep.exports.filter((x) => !deselected.has(x.exportId)); + const missingChecksum = selectedExports.filter((x) => !x.hasChecksum).length; + return (
{ep.episode ? `Episode ${ep.episode}` : "No episode"} - Ready: {ep.count} shots - {ep.exports.some((x) => !x.hasChecksum) && ( + + {canBuild ? `Selected: ${selectedIds.length} of ${ep.count} shots` : `Ready: ${ep.count} shots`} + + {canBuild && selectedIds.length > 0 && ( + + {formatBytes(selectedExports.reduce((sum, x) => sum + (x.totalBytes ?? 0), 0))} + + )} + {missingChecksum > 0 && ( - {ep.exports.filter((x) => !x.hasChecksum).length} without a validation checksum + {missingChecksum} without a validation checksum )}
{canBuild && ( - + + + + )}
- {ep.exports.map((x) => ( -
+ {ep.exports.map((x) => { + const selected = !deselected.has(x.exportId); + return ( +
+ {canBuild && ( + toggleExport(x.exportId, e.target.checked)} + aria-label={`Include ${x.shotCode} in the package`} + /> + )} {x.shotCode} @@ -251,10 +316,12 @@ export function DeliveriesClient({ canBuild }: { canBuild: boolean }) { {formatBytes(x.totalBytes)} {!x.deliveryMovPath && no MOV}
- ))} + ); + })}
- ))} + ); + })}
)}