From 5f3c89119a80154011c9f17436083542a97f86fb Mon Sep 17 00:00:00 2001 From: twotalesanimation <80506065+twotalesanimation@users.noreply.github.com> Date: Thu, 6 Aug 2026 08:36:26 +0200 Subject: [PATCH] Further CSV Import method added --- .gitignore | 3 + actions/shots.ts | 80 ++++++ .../[id]/import-edl/EdlImportClient.tsx | 258 +++++++++++++++++- lib/edl-utils.ts | 77 ++++++ 4 files changed, 415 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 86d6fe6..057b60a 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,6 @@ yarn-error.log* .vercel *.tsbuildinfo next-env.d.ts + +# Local tooling +RenderWorker/ diff --git a/actions/shots.ts b/actions/shots.ts index 0e85ffc..549934a 100644 --- a/actions/shots.ts +++ b/actions/shots.ts @@ -951,6 +951,86 @@ export async function updateShotsSeqTimecodes( return { updated, skipped, errors }; } +// ── Simple CSV shot import ──────────────────────────────────────────────────── + +export interface SimpleCsvRow { + shotCode: string; + seqTimecodeStart: string; + seqTimecodeEnd: string; + description: string; + action: "create" | "update" | "skip"; +} + +export async function importShotsFromSimpleCsv( + projectId: string, + rows: SimpleCsvRow[] +): Promise<{ created: string[]; 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 created: string[] = []; + const updated: string[] = []; + const skipped: string[] = []; + const errors: string[] = []; + + for (const row of rows) { + if (row.action === "skip") { skipped.push(row.shotCode); continue; } + try { + const existing = await db.shot.findFirst({ + where: { projectId, shotCode: row.shotCode }, + select: { id: true }, + }); + + if (existing) { + await db.shot.update({ + where: { id: existing.id }, + data: { + description: row.description || null, + seqTimecodeStart: row.seqTimecodeStart || null, + seqTimecodeEnd: row.seqTimecodeEnd || null, + }, + }); + updated.push(row.shotCode); + } else if (row.action === "create") { + const parts = row.shotCode.split("_"); + const scene = parts.length >= 3 ? parts[2] : (parts[1] ?? "000"); + const episode = parts.length >= 4 ? parts[1] : null; + const maxNum = await db.shot.findFirst({ + where: { projectId, scene, episode }, + orderBy: { shotNumber: "desc" }, + select: { shotNumber: true }, + }); + const shotNumber = (maxNum?.shotNumber ?? 0) + 10; + + await db.shot.create({ + data: { + projectId, + shotCode: row.shotCode, + scene, + episode, + shotNumber, + description: row.description || null, + seqTimecodeStart: row.seqTimecodeStart || null, + seqTimecodeEnd: row.seqTimecodeEnd || null, + }, + }); + created.push(row.shotCode); + } else { + // action === "update" but shot not found + skipped.push(row.shotCode); + } + } catch (e) { + errors.push(`${row.shotCode}: ${e instanceof Error ? e.message : "Unknown error"}`); + } + } + + revalidatePath(`/projects/${projectId}`); + return { created, 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 db956a9..7398129 100644 --- a/app/(dashboard)/projects/[id]/import-edl/EdlImportClient.tsx +++ b/app/(dashboard)/projects/[id]/import-edl/EdlImportClient.tsx @@ -17,10 +17,11 @@ import { FilePlus2, Pencil, Film, + FileSpreadsheet, } from "lucide-react"; -import { parseEdlCsv, parsePictureTrackerCsv } from "@/lib/edl-utils"; -import type { EdlImportRow, PictureTrackerRow } from "@/lib/edl-utils"; -import { importShotsFromEdl, updateShotsSeqTimecodes } from "@/actions/shots"; +import { parseEdlCsv, parsePictureTrackerCsv, parseSimpleCsv } from "@/lib/edl-utils"; +import type { EdlImportRow, PictureTrackerRow, SimpleCsvRow } from "@/lib/edl-utils"; +import { importShotsFromEdl, updateShotsSeqTimecodes, importShotsFromSimpleCsv } from "@/actions/shots"; interface EdlImportClientProps { projectId: string; @@ -54,6 +55,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); + // ── Simple CSV state ──────────────────────────────────────────────────────── + const scFileInputRef = useRef(null); + const [scStep, setScStep] = useState<"input" | "preview" | "result">("input"); + const [scCsvText, setScCsvText] = useState(""); + const [scRows, setScRows] = useState([]); + const [scErrors, setScErrors] = useState([]); + const [scImporting, setScImporting] = useState(false); + const [scResult, setScResult] = 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"); @@ -128,6 +138,61 @@ export function EdlImportClient({ projectId, projectName, existingShotCodes }: E setResult(null); }; + // ── Simple CSV handlers ───────────────────────────────────────────────────── + const handleScFileUpload = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = (ev) => setScCsvText(ev.target?.result as string ?? ""); + reader.readAsText(file); + e.target.value = ""; + }; + + const handleScParse = useCallback(() => { + const { rows: parsed, errors } = parseSimpleCsv(scCsvText, existingShotCodes); + setScErrors(errors); + setScRows(parsed); + if (parsed.length > 0) setScStep("preview"); + }, [scCsvText, existingShotCodes]); + + const toggleScAction = (idx: number) => { + setScRows((prev) => + prev.map((r, i) => { + if (i !== idx) return r; + const cycle: SimpleCsvRow["action"][] = ["create", "update", "skip"]; + const next = cycle[(cycle.indexOf(r.action) + 1) % cycle.length]; + return { ...r, action: next }; + }) + ); + }; + + const handleScImport = async () => { + setScImporting(true); + try { + const res = await importShotsFromSimpleCsv(projectId, scRows); + setScResult(res); + setScStep("result"); + if (res.created.length + res.updated.length > 0) { + toast({ + title: "Import complete", + description: `${res.created.length} created, ${res.updated.length} updated`, + }); + } + } catch (e) { + toast({ title: "Import failed", description: e instanceof Error ? e.message : undefined, variant: "destructive" }); + } finally { + setScImporting(false); + } + }; + + const handleScReset = () => { + setScStep("input"); + setScCsvText(""); + setScRows([]); + setScErrors([]); + setScResult(null); + }; + // ── Picture Tracker handlers ────────────────────────────────────────────── const handlePtFileUpload = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; @@ -426,6 +491,193 @@ export function EdlImportClient({ projectId, projectName, existingShotCodes }: E )} + {/* ── SIMPLE CSV PANEL ──────────────────────────────────────────── */} +
+
+

Import Shots from Simple CSV

+

+ Create or update shots from a CSV with Shot Name, Time Code In, Time Code Out, Description. +

+
+ + {scStep === "input" && ( +
+
+
+

Paste CSV or upload file

+
+ + +
+
+ +