diff --git a/actions/shots.ts b/actions/shots.ts index 2efc24e..ededc53 100644 --- a/actions/shots.ts +++ b/actions/shots.ts @@ -873,6 +873,57 @@ export async function importShotsFromEdl( 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. * Resets shotApprovalStatus = PENDING, sharedWithClient = false. diff --git a/app/(dashboard)/projects/[id]/import-edl/EdlImportClient.tsx b/app/(dashboard)/projects/[id]/import-edl/EdlImportClient.tsx index 7fdcf7f..db956a9 100644 --- a/app/(dashboard)/projects/[id]/import-edl/EdlImportClient.tsx +++ b/app/(dashboard)/projects/[id]/import-edl/EdlImportClient.tsx @@ -18,9 +18,9 @@ import { Pencil, Film, } from "lucide-react"; -import { parseEdlCsv } from "@/lib/edl-utils"; -import type { EdlImportRow } from "@/lib/edl-utils"; -import { importShotsFromEdl } from "@/actions/shots"; +import { parseEdlCsv, parsePictureTrackerCsv } from "@/lib/edl-utils"; +import type { EdlImportRow, PictureTrackerRow } from "@/lib/edl-utils"; +import { importShotsFromEdl, updateShotsSeqTimecodes } from "@/actions/shots"; interface EdlImportClientProps { projectId: string; @@ -54,6 +54,15 @@ export function EdlImportClient({ projectId, projectName, existingShotCodes }: E const [importing, setImporting] = useState(false); const [result, setResult] = useState<{ created: string[]; updated: string[]; skipped: string[]; errors: string[] } | null>(null); + // ── Picture Tracker state ───────────────────────────────────────────────── + const ptFileInputRef = useRef(null); + const [ptStep, setPtStep] = useState<"input" | "preview" | "result">("input"); + const [ptCsvText, setPtCsvText] = useState(""); + const [ptRows, setPtRows] = useState([]); + const [ptErrors, setPtErrors] = useState([]); + const [ptImporting, setPtImporting] = useState(false); + const [ptResult, setPtResult] = useState<{ updated: string[]; skipped: string[]; errors: string[] } | null>(null); + const handleFileUpload = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; @@ -119,6 +128,54 @@ export function EdlImportClient({ projectId, projectName, existingShotCodes }: E setResult(null); }; + // ── Picture Tracker handlers ────────────────────────────────────────────── + const handlePtFileUpload = (e: React.ChangeEvent) => { + 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 = { create: rows.filter((r) => r.action === "create").length, update: rows.filter((r) => r.action === "update").length, @@ -368,6 +425,194 @@ export function EdlImportClient({ projectId, projectName, existingShotCodes }: E )} + + {/* ── PICTURE TRACKER PANEL ─────────────────────────────────────── */} +
+
+

Import Picture Tracker Timecodes

+

+ Paste a picture tracker CSV to update seqTimecodeStart / seqTimecodeEnd on existing shots only. +

+
+ + {/* PT STEP 1: Input */} + {ptStep === "input" && ( +
+
+
+

Paste CSV or upload file

+
+ + +
+
+ +