From 98120f3ca8890d6ed2f29ad74cba4483a963bc84 Mon Sep 17 00:00:00 2001 From: twotalesanimation <80506065+twotalesanimation@users.noreply.github.com> Date: Thu, 6 Aug 2026 09:13:16 +0200 Subject: [PATCH] CSV Group updates --- actions/shots.ts | 68 ++++++ .../[id]/import-edl/EdlImportClient.tsx | 222 +++++++++++++++++- lib/edl-utils.ts | 65 +++++ 3 files changed, 350 insertions(+), 5 deletions(-) diff --git a/actions/shots.ts b/actions/shots.ts index 549934a..796b487 100644 --- a/actions/shots.ts +++ b/actions/shots.ts @@ -958,6 +958,7 @@ export interface SimpleCsvRow { seqTimecodeStart: string; seqTimecodeEnd: string; description: string; + group: string; action: "create" | "update" | "skip"; } @@ -985,12 +986,20 @@ export async function importShotsFromSimpleCsv( }); if (existing) { + const shotGroupId = row.group?.trim() + ? (await db.shotGroup.upsert({ + where: { projectId_name: { projectId, name: row.group.trim() } }, + create: { projectId, name: row.group.trim() }, + update: {}, + })).id + : undefined; await db.shot.update({ where: { id: existing.id }, data: { description: row.description || null, seqTimecodeStart: row.seqTimecodeStart || null, seqTimecodeEnd: row.seqTimecodeEnd || null, + ...(shotGroupId ? { shotGroupId } : {}), }, }); updated.push(row.shotCode); @@ -1004,6 +1013,13 @@ export async function importShotsFromSimpleCsv( select: { shotNumber: true }, }); const shotNumber = (maxNum?.shotNumber ?? 0) + 10; + const shotGroupId = row.group?.trim() + ? (await db.shotGroup.upsert({ + where: { projectId_name: { projectId, name: row.group.trim() } }, + create: { projectId, name: row.group.trim() }, + update: {}, + })).id + : undefined; await db.shot.create({ data: { @@ -1015,6 +1031,7 @@ export async function importShotsFromSimpleCsv( description: row.description || null, seqTimecodeStart: row.seqTimecodeStart || null, seqTimecodeEnd: row.seqTimecodeEnd || null, + shotGroupId, }, }); created.push(row.shotCode); @@ -1031,6 +1048,57 @@ export async function importShotsFromSimpleCsv( return { created, updated, skipped, errors }; } +// ── Group Assignment ────────────────────────────────────────────────────────────────── + +export interface GroupAssignRow { + shotCode: string; + groupName: string; + exists: boolean; +} + +export async function assignShotGroups( + projectId: string, + rows: GroupAssignRow[] +): Promise<{ assigned: 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 assigned: string[] = []; + const skipped: string[] = []; + const errors: string[] = []; + + for (const row of rows) { + if (!row.exists) { skipped.push(row.shotCode); continue; } + try { + const shot = await db.shot.findFirst({ + where: { projectId, shotCode: row.shotCode }, + select: { id: true }, + }); + if (!shot) { skipped.push(row.shotCode); continue; } + + const group = await db.shotGroup.upsert({ + where: { projectId_name: { projectId, name: row.groupName.trim() } }, + create: { projectId, name: row.groupName.trim() }, + update: {}, + }); + + await db.shot.update({ + where: { id: shot.id }, + data: { shotGroupId: group.id }, + }); + assigned.push(row.shotCode); + } catch (e) { + errors.push(`${row.shotCode}: ${e instanceof Error ? e.message : "Unknown error"}`); + } + } + + revalidatePath(`/projects/${projectId}`); + return { assigned, 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 7398129..75b4cda 100644 --- a/app/(dashboard)/projects/[id]/import-edl/EdlImportClient.tsx +++ b/app/(dashboard)/projects/[id]/import-edl/EdlImportClient.tsx @@ -19,9 +19,9 @@ import { Film, FileSpreadsheet, } from "lucide-react"; -import { parseEdlCsv, parsePictureTrackerCsv, parseSimpleCsv } from "@/lib/edl-utils"; -import type { EdlImportRow, PictureTrackerRow, SimpleCsvRow } from "@/lib/edl-utils"; -import { importShotsFromEdl, updateShotsSeqTimecodes, importShotsFromSimpleCsv } from "@/actions/shots"; +import { parseEdlCsv, parsePictureTrackerCsv, parseSimpleCsv, parseGroupAssignCsv } from "@/lib/edl-utils"; +import type { EdlImportRow, PictureTrackerRow, SimpleCsvRow, GroupAssignRow } from "@/lib/edl-utils"; +import { importShotsFromEdl, updateShotsSeqTimecodes, importShotsFromSimpleCsv, assignShotGroups } from "@/actions/shots"; interface EdlImportClientProps { projectId: string; @@ -64,6 +64,15 @@ export function EdlImportClient({ projectId, projectName, existingShotCodes }: E const [scImporting, setScImporting] = useState(false); const [scResult, setScResult] = useState<{ created: string[]; updated: string[]; skipped: string[]; errors: string[] } | null>(null); + // ── Group Assignment state ─────────────────────────────────────────────── + const gaFileInputRef = useRef(null); + const [gaStep, setGaStep] = useState<"input" | "preview" | "result">("input"); + const [gaCsvText, setGaCsvText] = useState(""); + const [gaRows, setGaRows] = useState([]); + const [gaErrors, setGaErrors] = useState([]); + const [gaAssigning, setGaAssigning] = useState(false); + const [gaResult, setGaResult] = useState<{ assigned: string[]; skipped: string[]; errors: string[] } | null>(null); + // ── Picture Tracker state ───────────────────────────────────────────────── const ptFileInputRef = useRef(null); const [ptStep, setPtStep] = useState<"input" | "preview" | "result">("input"); @@ -193,6 +202,47 @@ export function EdlImportClient({ projectId, projectName, existingShotCodes }: E setScResult(null); }; + // ── Group Assignment handlers ──────────────────────────────────────────── + const handleGaFileUpload = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = (ev) => setGaCsvText(ev.target?.result as string ?? ""); + reader.readAsText(file); + e.target.value = ""; + }; + + const handleGaParse = useCallback(() => { + const { rows: parsed, errors } = parseGroupAssignCsv(gaCsvText, existingShotCodes); + setGaErrors(errors); + setGaRows(parsed); + if (parsed.length > 0) setGaStep("preview"); + }, [gaCsvText, existingShotCodes]); + + const handleGaAssign = async () => { + setGaAssigning(true); + try { + const res = await assignShotGroups(projectId, gaRows); + setGaResult(res); + setGaStep("result"); + if (res.assigned.length > 0) { + toast({ title: "Groups assigned", description: `${res.assigned.length} shot${res.assigned.length !== 1 ? "s" : ""} updated` }); + } + } catch (e) { + toast({ title: "Assignment failed", description: e instanceof Error ? e.message : undefined, variant: "destructive" }); + } finally { + setGaAssigning(false); + } + }; + + const handleGaReset = () => { + setGaStep("input"); + setGaCsvText(""); + setGaRows([]); + setGaErrors([]); + setGaResult(null); + }; + // ── Picture Tracker handlers ────────────────────────────────────────────── const handlePtFileUpload = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; @@ -572,18 +622,19 @@ export function EdlImportClient({ projectId, projectName, existingShotCodes }: E )}
-
+
Action Shot Code Seq TC In Seq TC Out + Group Description
{scRows.map((row, i) => { const ActionIcon = ACTION_ICONS[row.action]; return ( -
+
); @@ -678,6 +730,166 @@ export function EdlImportClient({ projectId, projectName, existingShotCodes }: E )}
+ {/* ── GROUP ASSIGNMENT PANEL ─────────────────────────────────────── */} +
+
+

Assign Shots to Groups

+

+ Bulk-assign existing shots to groups from a CSV with Shot Name and Group columns. Groups are created automatically if they don’t exist. +

+
+ + {gaStep === "input" && ( +
+
+
+

Paste CSV or upload file

+
+ + +
+
+ +