+14
-32
@@ -23,7 +23,6 @@ var HANDLE_FRAMES = 8;
|
|||||||
lastActionText: null,
|
lastActionText: null,
|
||||||
pipelineStatusText: null,
|
pipelineStatusText: null,
|
||||||
urgentCheckbox: null,
|
urgentCheckbox: null,
|
||||||
vfxScopeField: null,
|
|
||||||
submissionNoteField: null
|
submissionNoteField: null
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2452,9 +2451,6 @@ var HANDLE_FRAMES = 8;
|
|||||||
if (pipelineState.prefilledShotCode === shotCode) {
|
if (pipelineState.prefilledShotCode === shotCode) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (uiState.vfxScopeField) {
|
|
||||||
uiState.vfxScopeField.text = (exportInfo && exportInfo.vfxScope) ? exportInfo.vfxScope : "";
|
|
||||||
}
|
|
||||||
if (uiState.submissionNoteField) {
|
if (uiState.submissionNoteField) {
|
||||||
uiState.submissionNoteField.text = (exportInfo && exportInfo.submissionNote) ? exportInfo.submissionNote : "";
|
uiState.submissionNoteField.text = (exportInfo && exportInfo.submissionNote) ? exportInfo.submissionNote : "";
|
||||||
}
|
}
|
||||||
@@ -2534,13 +2530,12 @@ var HANDLE_FRAMES = 8;
|
|||||||
if (urgent) {
|
if (urgent) {
|
||||||
body.priority = 20;
|
body.priority = 20;
|
||||||
}
|
}
|
||||||
// Sent every time so edits stick; omitting them would make the server
|
// submissionNote is a per-export override; if blank the server falls back to the shot's slate default.
|
||||||
// inherit the previous submission's values instead.
|
|
||||||
if (uiState.vfxScopeField) {
|
|
||||||
body.vfxScope = uiState.vfxScopeField.text;
|
|
||||||
}
|
|
||||||
if (uiState.submissionNoteField) {
|
if (uiState.submissionNoteField) {
|
||||||
body.submissionNote = uiState.submissionNoteField.text;
|
var note = uiState.submissionNoteField.text;
|
||||||
|
if (note && note.trim()) {
|
||||||
|
body.submissionNote = note;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return postJSON(BASE_URL + "/api/ext/exports", body);
|
return postJSON(BASE_URL + "/api/ext/exports", body);
|
||||||
}
|
}
|
||||||
@@ -2671,10 +2666,6 @@ var HANDLE_FRAMES = 8;
|
|||||||
var shotBuilderGroup;
|
var shotBuilderGroup;
|
||||||
var shotBuilderButton;
|
var shotBuilderButton;
|
||||||
var pipelinePanel;
|
var pipelinePanel;
|
||||||
var slateScopeGroup;
|
|
||||||
var slateScopeLabel;
|
|
||||||
var slateNoteGroup;
|
|
||||||
var slateNoteLabel;
|
|
||||||
var pipelineRow1;
|
var pipelineRow1;
|
||||||
var pipelineRow2;
|
var pipelineRow2;
|
||||||
var queueExportButton;
|
var queueExportButton;
|
||||||
@@ -2753,24 +2744,6 @@ var HANDLE_FRAMES = 8;
|
|||||||
|
|
||||||
uiState.pipelineStatusText = pipelinePanel.add("statictext", undefined, "Export status: not checked");
|
uiState.pipelineStatusText = pipelinePanel.add("statictext", undefined, "Export status: not checked");
|
||||||
|
|
||||||
slateScopeGroup = pipelinePanel.add("group");
|
|
||||||
slateScopeGroup.orientation = "row";
|
|
||||||
slateScopeGroup.alignChildren = ["fill", "center"];
|
|
||||||
slateScopeGroup.spacing = 6;
|
|
||||||
slateScopeLabel = slateScopeGroup.add("statictext", undefined, "VFX Scope:");
|
|
||||||
slateScopeLabel.preferredSize = [90, 20];
|
|
||||||
uiState.vfxScopeField = slateScopeGroup.add("edittext", undefined, "");
|
|
||||||
uiState.vfxScopeField.preferredSize = [220, 22];
|
|
||||||
|
|
||||||
slateNoteGroup = pipelinePanel.add("group");
|
|
||||||
slateNoteGroup.orientation = "row";
|
|
||||||
slateNoteGroup.alignChildren = ["fill", "top"];
|
|
||||||
slateNoteGroup.spacing = 6;
|
|
||||||
slateNoteLabel = slateNoteGroup.add("statictext", undefined, "Submission Note:");
|
|
||||||
slateNoteLabel.preferredSize = [90, 20];
|
|
||||||
uiState.submissionNoteField = slateNoteGroup.add("edittext", undefined, "", { multiline: true });
|
|
||||||
uiState.submissionNoteField.preferredSize = [220, 48];
|
|
||||||
|
|
||||||
pipelineRow1 = pipelinePanel.add("group");
|
pipelineRow1 = pipelinePanel.add("group");
|
||||||
pipelineRow1.orientation = "row";
|
pipelineRow1.orientation = "row";
|
||||||
pipelineRow1.alignChildren = ["fill", "center"];
|
pipelineRow1.alignChildren = ["fill", "center"];
|
||||||
@@ -2778,6 +2751,15 @@ var HANDLE_FRAMES = 8;
|
|||||||
queueExportButton = pipelineRow1.add("button", undefined, "Queue Export");
|
queueExportButton = pipelineRow1.add("button", undefined, "Queue Export");
|
||||||
uiState.urgentCheckbox = pipelineRow1.add("checkbox", undefined, "Urgent");
|
uiState.urgentCheckbox = pipelineRow1.add("checkbox", undefined, "Urgent");
|
||||||
|
|
||||||
|
var slateNoteGroup = pipelinePanel.add("group");
|
||||||
|
slateNoteGroup.orientation = "row";
|
||||||
|
slateNoteGroup.alignChildren = ["fill", "top"];
|
||||||
|
slateNoteGroup.spacing = 6;
|
||||||
|
var slateNoteLabel = slateNoteGroup.add("statictext", undefined, "Sub. Note:");
|
||||||
|
slateNoteLabel.preferredSize = [70, 20];
|
||||||
|
uiState.submissionNoteField = slateNoteGroup.add("edittext", undefined, "", { multiline: false });
|
||||||
|
uiState.submissionNoteField.preferredSize = [240, 22];
|
||||||
|
|
||||||
pipelineRow2 = pipelinePanel.add("group");
|
pipelineRow2 = pipelinePanel.add("group");
|
||||||
pipelineRow2.orientation = "row";
|
pipelineRow2.orientation = "row";
|
||||||
pipelineRow2.alignChildren = ["fill", "center"];
|
pipelineRow2.alignChildren = ["fill", "center"];
|
||||||
|
|||||||
@@ -489,6 +489,40 @@ export async function updateShotNotes(shotId: string, notes: string) {
|
|||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Update Shot Slate Fields ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export async function updateShotSlate(
|
||||||
|
shotId: string,
|
||||||
|
data: {
|
||||||
|
slateType?: string | null;
|
||||||
|
slateDescription?: string | null;
|
||||||
|
slateVfxScope?: string | null;
|
||||||
|
slateSubmissionNote?: string | null;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
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: {
|
||||||
|
slateType: data.slateType !== undefined ? (data.slateType?.trim() || null) : undefined,
|
||||||
|
slateDescription: data.slateDescription !== undefined ? (data.slateDescription?.trim() || null) : undefined,
|
||||||
|
slateVfxScope: data.slateVfxScope !== undefined ? (data.slateVfxScope?.trim() || null) : undefined,
|
||||||
|
slateSubmissionNote: data.slateSubmissionNote !== undefined ? (data.slateSubmissionNote?.trim() || null) : undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
revalidatePath(`/projects/${shot.projectId}`);
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
// ── Toggle Key Shot ───────────────────────────────────────────────────────────
|
// ── Toggle Key Shot ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export async function toggleKeyShot(shotId: string, isKeyShot: boolean) {
|
export async function toggleKeyShot(shotId: string, isKeyShot: boolean) {
|
||||||
|
|||||||
@@ -30,11 +30,13 @@ import {
|
|||||||
FileVideo,
|
FileVideo,
|
||||||
Pencil,
|
Pencil,
|
||||||
Check,
|
Check,
|
||||||
|
LayoutList,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import type { ShotWithDetails } from "@/types";
|
import type { ShotWithDetails } from "@/types";
|
||||||
import { ShotSettingsTab } from "@/components/shots/ShotSettingsTab";
|
import { ShotSettingsTab } from "@/components/shots/ShotSettingsTab";
|
||||||
import { ShotExportsTab } from "@/components/shots/ShotExportsTab";
|
import { ShotExportsTab } from "@/components/shots/ShotExportsTab";
|
||||||
|
import { ShotSlateTab } from "@/components/shots/ShotSlateTab";
|
||||||
import { FootageViewer } from "@/components/shots/FootageViewer";
|
import { FootageViewer } from "@/components/shots/FootageViewer";
|
||||||
import { HighResUploadDialog } from "@/components/shots/HighResUploadDialog";
|
import { HighResUploadDialog } from "@/components/shots/HighResUploadDialog";
|
||||||
import { duplicateShot, internallyApproveShot, shareWithClient, unshareFromClient, unapproveShot, updateShotVersion } from "@/actions/shots";
|
import { duplicateShot, internallyApproveShot, shareWithClient, unshareFromClient, unapproveShot, updateShotVersion } from "@/actions/shots";
|
||||||
@@ -90,7 +92,7 @@ export default function ShotDetailPage() {
|
|||||||
const [isDuplicating, setIsDuplicating] = useState(false);
|
const [isDuplicating, setIsDuplicating] = useState(false);
|
||||||
const [isActioning, setIsActioning] = useState(false);
|
const [isActioning, setIsActioning] = useState(false);
|
||||||
const [highResDialogOpen, setHighResDialogOpen] = useState(false);
|
const [highResDialogOpen, setHighResDialogOpen] = useState(false);
|
||||||
const [activeTab, setActiveTab] = useState<"tasks" | "reviews" | "footage" | "exports" | "settings">("tasks");
|
const [activeTab, setActiveTab] = useState<"tasks" | "slate" | "reviews" | "footage" | "exports" | "settings">("tasks");
|
||||||
const [editingVersion, setEditingVersion] = useState(false);
|
const [editingVersion, setEditingVersion] = useState(false);
|
||||||
const [versionInput, setVersionInput] = useState("");
|
const [versionInput, setVersionInput] = useState("");
|
||||||
const [savingVersion, setSavingVersion] = useState(false);
|
const [savingVersion, setSavingVersion] = useState(false);
|
||||||
@@ -520,6 +522,18 @@ export default function ShotDetailPage() {
|
|||||||
<ListTodo className="h-4 w-4" />
|
<ListTodo className="h-4 w-4" />
|
||||||
Tasks
|
Tasks
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab("slate")}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2 px-4 py-2.5 text-sm font-medium border-b-2 transition-colors -mb-px",
|
||||||
|
activeTab === "slate"
|
||||||
|
? "border-amber-500 text-amber-400"
|
||||||
|
: "border-transparent text-zinc-500 hover:text-zinc-300"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<LayoutList className="h-4 w-4" />
|
||||||
|
Slate
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab("reviews")}
|
onClick={() => setActiveTab("reviews")}
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -583,6 +597,10 @@ export default function ShotDetailPage() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{activeTab === "slate" && (
|
||||||
|
<ShotSlateTab shot={shot} projectName={projectName} onSaved={fetchShot} />
|
||||||
|
)}
|
||||||
|
|
||||||
{activeTab === "reviews" && (
|
{activeTab === "reviews" && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{tasks.length === 0 ? (
|
{tasks.length === 0 ? (
|
||||||
|
|||||||
@@ -0,0 +1,190 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useTransition } from "react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { Film, Save } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
|
import { updateShotSlate } from "@/actions/shots";
|
||||||
|
import type { ShotWithDetails } from "@/types";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
shot: ShotWithDetails;
|
||||||
|
projectName: string;
|
||||||
|
onSaved: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Left-side slate row: label + editable/static value */
|
||||||
|
function SlateRow({ label, children }: { label: string; children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div className="flex gap-0 border-b border-zinc-800/60 last:border-0">
|
||||||
|
<div className="w-36 shrink-0 py-3 px-4 text-right text-[11px] font-semibold text-zinc-500 uppercase tracking-widest self-start pt-3.5">
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 py-2.5 px-3 text-sm text-zinc-100">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Right-side metadata row */
|
||||||
|
function MetaRow({ label, value }: { label: string; value: string }) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between border-b border-zinc-800/50 last:border-0 py-1.5 px-3">
|
||||||
|
<span className="text-[11px] text-zinc-500">{label}</span>
|
||||||
|
<span className="text-[11px] text-zinc-300 text-right max-w-[55%] truncate">{value || "—"}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ShotSlateTab({ shot, projectName, onSaved }: Props) {
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [isPending, startTransition] = useTransition();
|
||||||
|
|
||||||
|
const [slateType, setSlateType] = useState(shot.slateType ?? "");
|
||||||
|
const [slateDescription, setSlateDescription] = useState(shot.slateDescription ?? shot.description ?? "");
|
||||||
|
const [slateVfxScope, setSlateVfxScope] = useState(shot.slateVfxScope ?? "");
|
||||||
|
const [slateSubmissionNote, setSlateSubmissionNote] = useState(shot.slateSubmissionNote ?? "");
|
||||||
|
|
||||||
|
const today = new Date();
|
||||||
|
const dateStr = `${today.getFullYear()}/${String(today.getMonth() + 1).padStart(2, "0")}/${String(today.getDate()).padStart(2, "0")}`;
|
||||||
|
const versionName = `${shot.shotCode}_cmp_TT_${shot.shotVersion ?? "v001"}`;
|
||||||
|
const frames =
|
||||||
|
shot.frameStart != null && shot.frameEnd != null
|
||||||
|
? String(shot.frameEnd - shot.frameStart + 1)
|
||||||
|
: "—";
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
startTransition(async () => {
|
||||||
|
try {
|
||||||
|
await updateShotSlate(shot.id, {
|
||||||
|
slateType: slateType.trim() || null,
|
||||||
|
slateDescription: slateDescription.trim() || null,
|
||||||
|
slateVfxScope: slateVfxScope.trim() || null,
|
||||||
|
slateSubmissionNote: slateSubmissionNote.trim() || null,
|
||||||
|
});
|
||||||
|
toast({ title: "Slate saved" });
|
||||||
|
onSaved();
|
||||||
|
} catch (e) {
|
||||||
|
toast({ title: "Failed to save", description: e instanceof Error ? e.message : undefined, variant: "destructive" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Slate document */}
|
||||||
|
<div className="rounded-xl overflow-hidden border border-zinc-800 bg-zinc-950 max-w-4xl">
|
||||||
|
{/* Top accent bar */}
|
||||||
|
<div className="h-1 bg-red-600" />
|
||||||
|
|
||||||
|
<div className="flex">
|
||||||
|
{/* ── Left column: editorial data ─────────────────────────────── */}
|
||||||
|
<div className="flex-1 border-r border-zinc-800">
|
||||||
|
{/* Header row */}
|
||||||
|
<div className="flex border-b border-zinc-800">
|
||||||
|
<div className="flex-1 px-4 py-3 border-r border-zinc-800">
|
||||||
|
<p className="text-[10px] text-zinc-500 uppercase tracking-widest mb-0.5">Show</p>
|
||||||
|
<p className="text-base font-bold text-white tracking-wide">{projectName.toUpperCase()}</p>
|
||||||
|
</div>
|
||||||
|
<div className="px-4 py-3 flex items-center gap-2">
|
||||||
|
<p className="text-[10px] text-zinc-500 uppercase tracking-widest">Submitting For</p>
|
||||||
|
<span className="ml-1 px-2 py-0.5 rounded bg-red-600/20 border border-red-600/40 text-red-400 text-xs font-bold tracking-widest">REVIEW</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Auto-filled rows */}
|
||||||
|
<SlateRow label="Version Name">
|
||||||
|
<span className="font-mono text-amber-400">{versionName}</span>
|
||||||
|
</SlateRow>
|
||||||
|
<SlateRow label="Date">
|
||||||
|
<span className="text-zinc-300">{dateStr}</span>
|
||||||
|
</SlateRow>
|
||||||
|
|
||||||
|
{/* Editable rows */}
|
||||||
|
<SlateRow label="Shot Type">
|
||||||
|
<Input
|
||||||
|
value={slateType}
|
||||||
|
onChange={(e) => setSlateType(e.target.value)}
|
||||||
|
placeholder="e.g. 2D COMP"
|
||||||
|
className="h-7 bg-zinc-900 border-zinc-700 text-sm font-medium text-zinc-100 placeholder:text-zinc-600"
|
||||||
|
/>
|
||||||
|
</SlateRow>
|
||||||
|
|
||||||
|
<SlateRow label="Shot Description">
|
||||||
|
<Textarea
|
||||||
|
value={slateDescription}
|
||||||
|
onChange={(e) => setSlateDescription(e.target.value)}
|
||||||
|
placeholder={shot.description ?? "Shot description for slate…"}
|
||||||
|
rows={2}
|
||||||
|
className="bg-zinc-900 border-zinc-700 text-sm text-zinc-100 placeholder:text-zinc-600 resize-none min-h-[56px]"
|
||||||
|
/>
|
||||||
|
</SlateRow>
|
||||||
|
|
||||||
|
<SlateRow label="VFX Scope">
|
||||||
|
<Textarea
|
||||||
|
value={slateVfxScope}
|
||||||
|
onChange={(e) => setSlateVfxScope(e.target.value)}
|
||||||
|
placeholder="VFX scope of work…"
|
||||||
|
rows={3}
|
||||||
|
className="bg-zinc-900 border-zinc-700 text-sm text-zinc-100 placeholder:text-zinc-600 resize-none min-h-[72px]"
|
||||||
|
/>
|
||||||
|
</SlateRow>
|
||||||
|
|
||||||
|
<SlateRow label="Submission Note">
|
||||||
|
<Input
|
||||||
|
value={slateSubmissionNote}
|
||||||
|
onChange={(e) => setSlateSubmissionNote(e.target.value)}
|
||||||
|
placeholder="e.g. Done"
|
||||||
|
className="h-7 bg-zinc-900 border-zinc-700 text-sm text-zinc-100 placeholder:text-zinc-600"
|
||||||
|
/>
|
||||||
|
</SlateRow>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ── Right column: thumbnail + metadata ──────────────────────── */}
|
||||||
|
<div className="w-56 shrink-0 flex flex-col">
|
||||||
|
{/* Thumbnail */}
|
||||||
|
<div className="aspect-video bg-zinc-900 border-b border-zinc-800 overflow-hidden flex items-center justify-center">
|
||||||
|
{shot.thumbnailUrl ? (
|
||||||
|
<Image
|
||||||
|
src={shot.thumbnailUrl}
|
||||||
|
alt={shot.shotCode}
|
||||||
|
width={224}
|
||||||
|
height={126}
|
||||||
|
className="object-cover w-full h-full"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Film className="h-8 w-8 text-zinc-700" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Metadata */}
|
||||||
|
<div className="flex-1 text-right">
|
||||||
|
<div className="px-3 py-2 border-b border-zinc-800">
|
||||||
|
<p className="text-[10px] text-zinc-500 mb-0.5">Vendor</p>
|
||||||
|
<p className="text-[11px] text-zinc-200 font-medium leading-tight">TWO TALES ANIMATION (PTY) LTD</p>
|
||||||
|
</div>
|
||||||
|
<MetaRow label="Shot Name" value={shot.shotCode} />
|
||||||
|
<MetaRow label="Episode" value={shot.episode ?? "—"} />
|
||||||
|
<MetaRow label="Seq Name" value={shot.sequence ?? "—"} />
|
||||||
|
<MetaRow label="Scene" value={shot.scene} />
|
||||||
|
<MetaRow label="Frames" value={frames} />
|
||||||
|
<MetaRow label="Media Color" value="w/SHOW LUT" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Save button */}
|
||||||
|
<div className="flex justify-end max-w-4xl">
|
||||||
|
<Button onClick={handleSave} disabled={isPending} className="gap-2">
|
||||||
|
<Save className="h-4 w-4" />
|
||||||
|
{isPending ? "Saving…" : "Save Slate"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -137,21 +137,17 @@ export async function createExport(input: CreateExportInput) {
|
|||||||
});
|
});
|
||||||
const freshShot = await tx.shot.findUniqueOrThrow({
|
const freshShot = await tx.shot.findUniqueOrThrow({
|
||||||
where: { id: shot.id },
|
where: { id: shot.id },
|
||||||
select: { shotVersion: true, exrOutput: true },
|
select: { shotVersion: true, exrOutput: true, slateVfxScope: true, slateSubmissionNote: true },
|
||||||
});
|
});
|
||||||
// Slate fields carry forward from the shot's previous submission
|
// vfxScope: caller override → shot's slate field
|
||||||
// unless the caller supplies new ones.
|
const vfxScope = input.vfxScope !== undefined ? input.vfxScope : (freshShot.slateVfxScope ?? null);
|
||||||
const previous = await tx.export.findFirst({
|
// submissionNote: caller override (per-export) → shot's slate default
|
||||||
where: { shotId: shot.id },
|
|
||||||
orderBy: { versionNumber: "desc" },
|
|
||||||
select: { vfxScope: true, submissionNote: true },
|
|
||||||
});
|
|
||||||
const vfxScope = input.vfxScope !== undefined ? input.vfxScope : (previous?.vfxScope ?? null);
|
|
||||||
const submissionNote =
|
const submissionNote =
|
||||||
input.submissionNote !== undefined ? input.submissionNote : (previous?.submissionNote ?? null);
|
input.submissionNote !== undefined ? input.submissionNote : (freshShot.slateSubmissionNote ?? null);
|
||||||
const versionNumber =
|
// versionNumber is an internal sequence for DB uniqueness only; output files
|
||||||
Math.max(latest._max.versionNumber ?? 0, parseVersionString(freshShot.shotVersion)) + 1;
|
// use the shot's current (manually-set) shotVersion.
|
||||||
const versionString = formatVersionString(versionNumber);
|
const versionNumber = (latest._max.versionNumber ?? 0) + 1;
|
||||||
|
const versionString = freshShot.shotVersion ?? formatVersionString(versionNumber);
|
||||||
const exrBase = nextExrOutputBase(freshShot.exrOutput, shot.shotCode, versionString);
|
const exrBase = nextExrOutputBase(freshShot.exrOutput, shot.shotCode, versionString);
|
||||||
|
|
||||||
const outputDir =
|
const outputDir =
|
||||||
@@ -236,13 +232,6 @@ export async function createExport(input: CreateExportInput) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mirror the legacy panel PATCH: Shot stays the canonical "current version"
|
|
||||||
const updatedShot = await tx.shot.update({
|
|
||||||
where: { id: shot.id },
|
|
||||||
data: { shotVersion: versionString, exrOutput: exrBase },
|
|
||||||
select: { id: true, shotVersion: true, exrOutput: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
export: {
|
export: {
|
||||||
id: created.id,
|
id: created.id,
|
||||||
@@ -256,7 +245,7 @@ export async function createExport(input: CreateExportInput) {
|
|||||||
submissionNote,
|
submissionNote,
|
||||||
},
|
},
|
||||||
renderJob: { id: renderJob.id, attempt: renderJob.attempt, priority: renderJob.priority },
|
renderJob: { id: renderJob.id, attempt: renderJob.attempt, priority: renderJob.priority },
|
||||||
shot: updatedShot,
|
shot: { id: shot.id, shotVersion: versionString, exrOutput: exrBase },
|
||||||
superseded: toSupersede.map((s) => s.id),
|
superseded: toSupersede.map((s) => s.id),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-- AlterTable: add Netflix slate fields to shots
|
||||||
|
ALTER TABLE "shots" ADD COLUMN "slateType" TEXT;
|
||||||
|
ALTER TABLE "shots" ADD COLUMN "slateDescription" TEXT;
|
||||||
|
ALTER TABLE "shots" ADD COLUMN "slateVfxScope" TEXT;
|
||||||
|
ALTER TABLE "shots" ADD COLUMN "slateSubmissionNote" TEXT;
|
||||||
@@ -418,6 +418,11 @@ model Shot {
|
|||||||
shotVersion String @default("v001")
|
shotVersion String @default("v001")
|
||||||
// Key shot flag — highlighted for prioritisation
|
// Key shot flag — highlighted for prioritisation
|
||||||
isKeyShot Boolean @default(false)
|
isKeyShot Boolean @default(false)
|
||||||
|
// Netflix slate fields — managed via the Slate tab, used by the preview pipeline
|
||||||
|
slateType String?
|
||||||
|
slateDescription String? @db.Text
|
||||||
|
slateVfxScope String? @db.Text
|
||||||
|
slateSubmissionNote String? @db.Text
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
|||||||
@@ -181,6 +181,11 @@ export interface ShotWithDetails {
|
|||||||
highResFilename: string | null;
|
highResFilename: string | null;
|
||||||
// Shot-level version tracking
|
// Shot-level version tracking
|
||||||
shotVersion: string;
|
shotVersion: string;
|
||||||
|
// Netflix slate fields
|
||||||
|
slateType: string | null;
|
||||||
|
slateDescription: string | null;
|
||||||
|
slateVfxScope: string | null;
|
||||||
|
slateSubmissionNote: string | null;
|
||||||
shotGroup: { id: string; name: string } | null;
|
shotGroup: { id: string; name: string } | null;
|
||||||
footagePlates: {
|
footagePlates: {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user