@@ -873,6 +873,57 @@ export async function importShotsFromEdl(
|
|||||||
return { created, updated, skipped, errors };
|
return { created, updated, skipped, errors };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Picture Tracker / Sequence Timecode Update ────────────────────────────────
|
||||||
|
|
||||||
|
export interface SeqTimecodeRow {
|
||||||
|
shotCode: string;
|
||||||
|
seqTimecodeStart: string;
|
||||||
|
seqTimecodeEnd: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateShotsSeqTimecodes(
|
||||||
|
projectId: string,
|
||||||
|
rows: SeqTimecodeRow[]
|
||||||
|
): Promise<{ updated: string[]; skipped: string[]; errors: string[] }> {
|
||||||
|
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 updated: string[] = [];
|
||||||
|
const skipped: string[] = [];
|
||||||
|
const errors: string[] = [];
|
||||||
|
|
||||||
|
for (const row of rows) {
|
||||||
|
try {
|
||||||
|
const existing = await db.shot.findFirst({
|
||||||
|
where: { projectId, shotCode: row.shotCode },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
skipped.push(row.shotCode);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.shot.update({
|
||||||
|
where: { id: existing.id },
|
||||||
|
data: {
|
||||||
|
seqTimecodeStart: row.seqTimecodeStart || null,
|
||||||
|
seqTimecodeEnd: row.seqTimecodeEnd || null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
updated.push(row.shotCode);
|
||||||
|
} catch (e) {
|
||||||
|
errors.push(`${row.shotCode}: ${e instanceof Error ? e.message : "Unknown error"}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
revalidatePath(`/projects/${projectId}`);
|
||||||
|
return { updated, skipped, errors };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal: handle client requesting changes on a shot.
|
* Internal: handle client requesting changes on a shot.
|
||||||
* Resets shotApprovalStatus = PENDING, sharedWithClient = false.
|
* Resets shotApprovalStatus = PENDING, sharedWithClient = false.
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ import {
|
|||||||
Pencil,
|
Pencil,
|
||||||
Film,
|
Film,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { parseEdlCsv } from "@/lib/edl-utils";
|
import { parseEdlCsv, parsePictureTrackerCsv } from "@/lib/edl-utils";
|
||||||
import type { EdlImportRow } from "@/lib/edl-utils";
|
import type { EdlImportRow, PictureTrackerRow } from "@/lib/edl-utils";
|
||||||
import { importShotsFromEdl } from "@/actions/shots";
|
import { importShotsFromEdl, updateShotsSeqTimecodes } from "@/actions/shots";
|
||||||
|
|
||||||
interface EdlImportClientProps {
|
interface EdlImportClientProps {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@@ -54,6 +54,15 @@ export function EdlImportClient({ projectId, projectName, existingShotCodes }: E
|
|||||||
const [importing, setImporting] = useState(false);
|
const [importing, setImporting] = useState(false);
|
||||||
const [result, setResult] = useState<{ created: string[]; updated: string[]; skipped: string[]; errors: string[] } | null>(null);
|
const [result, setResult] = useState<{ created: string[]; updated: string[]; skipped: string[]; errors: string[] } | null>(null);
|
||||||
|
|
||||||
|
// ── Picture Tracker state ─────────────────────────────────────────────────
|
||||||
|
const ptFileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const [ptStep, setPtStep] = useState<"input" | "preview" | "result">("input");
|
||||||
|
const [ptCsvText, setPtCsvText] = useState("");
|
||||||
|
const [ptRows, setPtRows] = useState<PictureTrackerRow[]>([]);
|
||||||
|
const [ptErrors, setPtErrors] = useState<string[]>([]);
|
||||||
|
const [ptImporting, setPtImporting] = useState(false);
|
||||||
|
const [ptResult, setPtResult] = useState<{ updated: string[]; skipped: string[]; errors: string[] } | null>(null);
|
||||||
|
|
||||||
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
@@ -119,6 +128,54 @@ export function EdlImportClient({ projectId, projectName, existingShotCodes }: E
|
|||||||
setResult(null);
|
setResult(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── Picture Tracker handlers ──────────────────────────────────────────────
|
||||||
|
const handlePtFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (ev) => setPtCsvText(ev.target?.result as string ?? "");
|
||||||
|
reader.readAsText(file);
|
||||||
|
e.target.value = "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePtParse = useCallback(() => {
|
||||||
|
const { rows: parsed, errors } = parsePictureTrackerCsv(ptCsvText, existingShotCodes);
|
||||||
|
setPtErrors(errors);
|
||||||
|
setPtRows(parsed);
|
||||||
|
if (parsed.length > 0) setPtStep("preview");
|
||||||
|
}, [ptCsvText, existingShotCodes]);
|
||||||
|
|
||||||
|
const handlePtImport = async () => {
|
||||||
|
setPtImporting(true);
|
||||||
|
try {
|
||||||
|
const toUpdate = ptRows.filter((r) => r.exists);
|
||||||
|
const res = await updateShotsSeqTimecodes(projectId, toUpdate);
|
||||||
|
// Rows that parsed but didn't exist are also skipped
|
||||||
|
const notFound = ptRows.filter((r) => !r.exists).map((r) => r.shotCode);
|
||||||
|
const combined = { ...res, skipped: [...notFound, ...res.skipped] };
|
||||||
|
setPtResult(combined);
|
||||||
|
setPtStep("result");
|
||||||
|
if (res.updated.length > 0) {
|
||||||
|
toast({
|
||||||
|
title: "Timecodes updated",
|
||||||
|
description: `${res.updated.length} shot${res.updated.length !== 1 ? "s" : ""} updated`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
toast({ title: "Update failed", description: e instanceof Error ? e.message : undefined, variant: "destructive" });
|
||||||
|
} finally {
|
||||||
|
setPtImporting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePtReset = () => {
|
||||||
|
setPtStep("input");
|
||||||
|
setPtCsvText("");
|
||||||
|
setPtRows([]);
|
||||||
|
setPtErrors([]);
|
||||||
|
setPtResult(null);
|
||||||
|
};
|
||||||
|
|
||||||
const counts = {
|
const counts = {
|
||||||
create: rows.filter((r) => r.action === "create").length,
|
create: rows.filter((r) => r.action === "create").length,
|
||||||
update: rows.filter((r) => r.action === "update").length,
|
update: rows.filter((r) => r.action === "update").length,
|
||||||
@@ -368,6 +425,194 @@ export function EdlImportClient({ projectId, projectName, existingShotCodes }: E
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* ── PICTURE TRACKER PANEL ─────────────────────────────────────── */}
|
||||||
|
<div className="pt-6 border-t border-zinc-800 space-y-4">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-base font-semibold text-white">Import Picture Tracker Timecodes</h2>
|
||||||
|
<p className="text-sm text-zinc-500 mt-0.5">
|
||||||
|
Paste a picture tracker CSV to update <span className="font-mono text-zinc-300">seqTimecodeStart</span> / <span className="font-mono text-zinc-300">seqTimecodeEnd</span> on existing shots only.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* PT STEP 1: Input */}
|
||||||
|
{ptStep === "input" && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="rounded-xl border border-zinc-800 bg-zinc-900 p-5 space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<p className="text-sm font-medium text-zinc-300">Paste CSV or upload file</p>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
ref={ptFileInputRef}
|
||||||
|
type="file"
|
||||||
|
accept=".csv,text/csv"
|
||||||
|
className="hidden"
|
||||||
|
onChange={handlePtFileUpload}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="gap-1.5 h-7"
|
||||||
|
onClick={() => ptFileInputRef.current?.click()}
|
||||||
|
>
|
||||||
|
<Upload className="h-3.5 w-3.5" />
|
||||||
|
Upload .csv
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Textarea
|
||||||
|
value={ptCsvText}
|
||||||
|
onChange={(e) => setPtCsvText(e.target.value)}
|
||||||
|
placeholder={`#,Thumbnail,Shot Name,TC In,TC Out,Duration,...\n1,,UNG_106_004_010,01:00:37:16,01:00:53:01,00:00:15:09,...`}
|
||||||
|
className="font-mono text-xs min-h-[200px] bg-zinc-950 border-zinc-700 resize-y"
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="rounded-lg bg-zinc-950 border border-zinc-800 p-3 text-xs text-zinc-400 space-y-1">
|
||||||
|
<p className="font-medium text-zinc-300">Expected format</p>
|
||||||
|
<p>Columns required: <span className="font-mono text-amber-400">Shot Name</span>, <span className="font-mono text-amber-400">TC In</span>, <span className="font-mono text-amber-400">TC Out</span></p>
|
||||||
|
<p>Only shots that already exist in this project will be updated. Duplicate shot rows are ignored (first TC In/Out wins).</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button onClick={handlePtParse} disabled={!ptCsvText.trim()} className="gap-2">
|
||||||
|
<Film className="h-4 w-4" />
|
||||||
|
Parse & Preview
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* PT STEP 2: Preview */}
|
||||||
|
{ptStep === "preview" && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
|
<div className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-blue-500/10 border border-blue-500/20 text-blue-400 text-sm">
|
||||||
|
<Pencil className="h-3.5 w-3.5" />
|
||||||
|
{ptRows.filter((r) => r.exists).length} to update
|
||||||
|
</div>
|
||||||
|
{ptRows.filter((r) => !r.exists).length > 0 && (
|
||||||
|
<div className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-zinc-500/10 border border-zinc-500/20 text-zinc-400 text-sm">
|
||||||
|
<SkipForward className="h-3.5 w-3.5" />
|
||||||
|
{ptRows.filter((r) => !r.exists).length} not found (will skip)
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{ptErrors.length > 0 && (
|
||||||
|
<div className="rounded-lg bg-red-500/10 border border-red-500/20 p-3 space-y-1">
|
||||||
|
<p className="text-xs font-medium text-red-400 flex items-center gap-1.5">
|
||||||
|
<AlertCircle className="h-3.5 w-3.5" /> {ptErrors.length} parse warning{ptErrors.length !== 1 ? "s" : ""}
|
||||||
|
</p>
|
||||||
|
{ptErrors.map((e, i) => <p key={i} className="text-xs text-red-300 pl-5">{e}</p>)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="rounded-xl border border-zinc-800 overflow-hidden">
|
||||||
|
<div className="grid grid-cols-[1fr_1fr_1fr_auto] text-xs font-medium text-zinc-500 uppercase tracking-wider bg-zinc-900 px-4 py-2.5 gap-4 border-b border-zinc-800">
|
||||||
|
<span>Shot Code</span>
|
||||||
|
<span>Seq TC In</span>
|
||||||
|
<span>Seq TC Out</span>
|
||||||
|
<span>Status</span>
|
||||||
|
</div>
|
||||||
|
<div className="divide-y divide-zinc-800/60 bg-zinc-950/40 max-h-[400px] overflow-y-auto">
|
||||||
|
{ptRows.map((row, i) => (
|
||||||
|
<div key={i} className="grid grid-cols-[1fr_1fr_1fr_auto] items-center gap-4 px-4 py-3 text-sm">
|
||||||
|
<span className="font-mono text-xs text-zinc-200">{row.shotCode}</span>
|
||||||
|
<span className="font-mono text-xs text-zinc-300">{row.seqTimecodeStart}</span>
|
||||||
|
<span className="font-mono text-xs text-zinc-300">{row.seqTimecodeEnd}</span>
|
||||||
|
{row.exists ? (
|
||||||
|
<span className="inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded border bg-blue-500/10 text-blue-400 border-blue-500/20">
|
||||||
|
<Pencil className="h-3 w-3" /> update
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded border bg-zinc-500/10 text-zinc-400 border-zinc-500/20">
|
||||||
|
<SkipForward className="h-3 w-3" /> skip
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Button variant="outline" onClick={handlePtReset}>
|
||||||
|
<ArrowLeft className="h-4 w-4 mr-1.5" /> Back
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handlePtImport}
|
||||||
|
disabled={ptImporting || ptRows.filter((r) => r.exists).length === 0}
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
{ptImporting ? <RefreshCw className="h-4 w-4 animate-spin" /> : <Upload className="h-4 w-4" />}
|
||||||
|
{ptImporting
|
||||||
|
? "Updating…"
|
||||||
|
: `Update ${ptRows.filter((r) => r.exists).length} shot${ptRows.filter((r) => r.exists).length !== 1 ? "s" : ""}`}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* PT STEP 3: Result */}
|
||||||
|
{ptStep === "result" && ptResult && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="rounded-xl border border-zinc-800 bg-zinc-900 p-6 space-y-5">
|
||||||
|
<h2 className="text-base font-semibold text-white flex items-center gap-2">
|
||||||
|
<CheckCircle2 className="h-5 w-5 text-emerald-400" />
|
||||||
|
Timecode update complete
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{ptResult.updated.length > 0 && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-xs font-medium text-blue-400 uppercase tracking-wide">
|
||||||
|
Updated ({ptResult.updated.length})
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
{ptResult.updated.map((c) => (
|
||||||
|
<span key={c} className="font-mono text-xs px-2 py-0.5 rounded bg-blue-500/10 border border-blue-500/20 text-blue-300">{c}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{ptResult.skipped.length > 0 && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-xs font-medium text-zinc-500 uppercase tracking-wide">
|
||||||
|
Skipped / not found ({ptResult.skipped.length})
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
{ptResult.skipped.map((c) => (
|
||||||
|
<span key={c} className="font-mono text-xs px-2 py-0.5 rounded bg-zinc-800 border border-zinc-700 text-zinc-400">{c}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{ptResult.errors.length > 0 && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-xs font-medium text-red-400 uppercase tracking-wide flex items-center gap-1.5">
|
||||||
|
<AlertCircle className="h-3.5 w-3.5" /> Errors ({ptResult.errors.length})
|
||||||
|
</p>
|
||||||
|
{ptResult.errors.map((e, i) => (
|
||||||
|
<p key={i} className="text-xs text-red-300 pl-5">{e}</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Button variant="outline" onClick={handlePtReset}>
|
||||||
|
Import Another
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => router.push(`/projects/${projectId}`)}>
|
||||||
|
Back to Project
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -97,3 +97,90 @@ export function parseEdlCsv(csvText: string): { rows: EdlImportRow[]; errors: st
|
|||||||
|
|
||||||
return { rows, errors };
|
return { rows, errors };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Picture Tracker CSV Parser ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface PictureTrackerRow {
|
||||||
|
shotCode: string;
|
||||||
|
seqTimecodeStart: string;
|
||||||
|
seqTimecodeEnd: string;
|
||||||
|
/** true when shotCode matched an existing shot in the project */
|
||||||
|
exists: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TC values exported by some tools contain invalid values like "aN:aN:aN:aN"
|
||||||
|
* or "#ERROR!". Reject anything that doesn't look like HH:MM:SS:FF.
|
||||||
|
*/
|
||||||
|
function isValidTimecode(tc: string): boolean {
|
||||||
|
return /^\d{2}:\d{2}:\d{2}[:;]\d{2}$/.test(tc.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a picture tracker CSV.
|
||||||
|
*
|
||||||
|
* Expected header (col indices may vary, but we find by name):
|
||||||
|
* #, Thumbnail, Shot Name, TC In, TC Out, Duration, ...
|
||||||
|
*
|
||||||
|
* Only the FIRST TC In/Out for each unique Shot Name is used
|
||||||
|
* (subsequent rows for the same shot are ignored).
|
||||||
|
*
|
||||||
|
* @param csvText raw CSV text
|
||||||
|
* @param existingCodes set of shotCodes already in the project
|
||||||
|
*/
|
||||||
|
export function parsePictureTrackerCsv(
|
||||||
|
csvText: string,
|
||||||
|
existingCodes: Set<string>
|
||||||
|
): { rows: PictureTrackerRow[]; errors: string[] } {
|
||||||
|
const lines = csvText.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
||||||
|
if (lines.length < 2) {
|
||||||
|
return { rows: [], errors: ["CSV must have a header row and at least one data row"] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawHeaders = parseCsvLine(lines[0]).map((h) => h.toLowerCase().trim());
|
||||||
|
const col = (name: string) => rawHeaders.indexOf(name);
|
||||||
|
|
||||||
|
const shotNameIdx = col("shot name");
|
||||||
|
const tcInIdx = col("tc in");
|
||||||
|
const tcOutIdx = col("tc out");
|
||||||
|
|
||||||
|
if (shotNameIdx === -1) {
|
||||||
|
return { rows: [], errors: ['CSV must contain a "Shot Name" column'] };
|
||||||
|
}
|
||||||
|
if (tcInIdx === -1 || tcOutIdx === -1) {
|
||||||
|
return { rows: [], errors: ['CSV must contain "TC In" and "TC Out" columns'] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows: PictureTrackerRow[] = [];
|
||||||
|
const errors: string[] = [];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
|
||||||
|
for (let i = 1; i < lines.length; i++) {
|
||||||
|
const cells = parseCsvLine(lines[i]);
|
||||||
|
const get = (idx: number) => (idx < cells.length ? cells[idx] ?? "" : "").trim();
|
||||||
|
|
||||||
|
const shotCode = get(shotNameIdx);
|
||||||
|
if (!shotCode) continue;
|
||||||
|
|
||||||
|
// Only use the first occurrence per shot
|
||||||
|
if (seen.has(shotCode)) continue;
|
||||||
|
|
||||||
|
const tcIn = get(tcInIdx);
|
||||||
|
const tcOut = get(tcOutIdx);
|
||||||
|
|
||||||
|
if (!isValidTimecode(tcIn) || !isValidTimecode(tcOut)) {
|
||||||
|
errors.push(`Row ${i + 1} (${shotCode}): invalid timecode "${tcIn}" / "${tcOut}" — skipped`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
seen.add(shotCode);
|
||||||
|
rows.push({
|
||||||
|
shotCode,
|
||||||
|
seqTimecodeStart: tcIn,
|
||||||
|
seqTimecodeEnd: tcOut,
|
||||||
|
exists: existingCodes.has(shotCode),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { rows, errors };
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- Add sequence/picture lock timecode fields to shots
|
||||||
|
ALTER TABLE "shots" ADD COLUMN "seqTimecodeStart" TEXT;
|
||||||
|
ALTER TABLE "shots" ADD COLUMN "seqTimecodeEnd" TEXT;
|
||||||
@@ -303,6 +303,9 @@ model Shot {
|
|||||||
timecodeEnd String?
|
timecodeEnd String?
|
||||||
clipDuration String?
|
clipDuration String?
|
||||||
exrOutput String?
|
exrOutput String?
|
||||||
|
// Sequence / picture lock timecodes
|
||||||
|
seqTimecodeStart String?
|
||||||
|
seqTimecodeEnd String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
|||||||
@@ -174,6 +174,9 @@ export interface ShotWithDetails {
|
|||||||
timecodeEnd: string | null;
|
timecodeEnd: string | null;
|
||||||
clipDuration: string | null;
|
clipDuration: string | null;
|
||||||
exrOutput: string | null;
|
exrOutput: string | null;
|
||||||
|
// Sequence / picture lock timecodes
|
||||||
|
seqTimecodeStart: string | null;
|
||||||
|
seqTimecodeEnd: 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