import "dotenv/config"; import * as XLSX from "xlsx"; import * as path from "path"; import * as fs from "fs"; import { PrismaPg } from "@prisma/adapter-pg"; import { PrismaClient } from "../src/generated/prisma/client"; const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL! }); const prisma = new PrismaClient({ adapter }); // ─── Type → Pipeline Stage Slug Mapping ────────────────── const TYPE_TO_STAGE_SLUG: Record = { catalog: "catalog-images", "catalog ": "catalog-images", "early catalog images": "early-images", "early catalog": "early-images", hero: "hero-images", "hero image": "hero-images", "hero imagery": "hero-images", "hero ": "hero-images", "hero images": "hero-images", packaging: "packaging-images", pkg: "packaging-images", "pkg ": "packaging-images", photocomp: "photocomps", "photocomp ": "photocomps", lifestyle: "photocomps", "lifestyle ": "photocomps", "360 spin": "360-spin-animations", "annotated spin": "360-spin-animations", "360 annotated": "360-spin-animations", "360 annotated ": "360-spin-animations", "simple spin": "360-spin-animations", "dynamic spin": "dynamic-spin", animations: "dynamic-spin", "animations ": "dynamic-spin", "annotated + dynamic": "dynamic-spin", "product video": "dynamic-spin", "product video ": "dynamic-spin", refresh: "catalog-images", "screen swap": "catalog-images", exploded: "hero-images", "cad update": "model-prep", }; // ─── Status Mapping ────────────────────────────────────── function mapTeamStatus(raw: string | undefined): { deliverableStatus: string; isDelivered: boolean; } { if (!raw) return { deliverableStatus: "IN_PROGRESS", isDelivered: false }; const s = raw.toString().trim().toLowerCase(); if ( s.includes("delivered") || s.includes("dam") || s === "dam done" || s === "dam initiated" ) return { deliverableStatus: "APPROVED", isDelivered: true }; if (s.includes("cancel")) return { deliverableStatus: "ON_HOLD", isDelivered: false }; if (s.includes("hold") || s === "on hold") return { deliverableStatus: "ON_HOLD", isDelivered: false }; if ( s.includes("wip") || s.includes("in progress") || s.includes("clays") || s.includes("lighting") || s.includes("started") || s.includes("prep") ) return { deliverableStatus: "IN_PROGRESS", isDelivered: false }; if ( s.includes("review") || s.includes("pending") || s.includes("under review") ) return { deliverableStatus: "IN_REVIEW", isDelivered: false }; if (s.includes("tba") || s.includes("tbs") || s.includes("tbi")) return { deliverableStatus: "NOT_STARTED", isDelivered: false }; if (s === "to be started" || s === "brief pending") return { deliverableStatus: "NOT_STARTED", isDelivered: false }; return { deliverableStatus: "IN_PROGRESS", isDelivered: false }; } function mapQ2Status(raw: string | undefined): { deliverableStatus: string; isDelivered: boolean; } { if (!raw) return { deliverableStatus: "IN_PROGRESS", isDelivered: false }; const s = raw.toString().trim().toLowerCase(); if (s.includes("delivered")) return { deliverableStatus: "APPROVED", isDelivered: true }; if (s.includes("hold")) return { deliverableStatus: "ON_HOLD", isDelivered: false }; if ( s.includes("in progress") || s.includes("wip") || s.includes("clays") || s.includes("lighting") || s.includes("under prep") ) return { deliverableStatus: "IN_PROGRESS", isDelivered: false }; if (s.includes("model prep")) return { deliverableStatus: "IN_PROGRESS", isDelivered: false }; if ( s.includes("to be started") || s.includes("tba") || s.includes("tbi") || s.includes("brief pending") || s.includes("share quote") || s.includes("quotes") ) return { deliverableStatus: "NOT_STARTED", isDelivered: false }; if (s.includes("aug due")) return { deliverableStatus: "NOT_STARTED", isDelivered: false }; return { deliverableStatus: "IN_PROGRESS", isDelivered: false }; } // ─── Parse Date ────────────────────────────────────────── function parseDate(raw: any): Date | null { if (!raw) return null; if (raw instanceof Date) return raw; const s = raw.toString().trim(); if (!s || s === "TBD" || s === "TBA" || s === "N.R") return null; const d = new Date(s); return isNaN(d.getTime()) ? null : d; } // ─── Parse Cost ────────────────────────────────────────── function parseCost(raw: any): number | null { if (raw == null) return null; const s = raw.toString().replace(/[$,\s]/g, ""); const match = s.match(/^[\d.]+/); if (!match) return null; const n = parseFloat(match[0]); return isNaN(n) ? null : n; } // ─── Row Interface ─────────────────────────────────────── interface TrackerRow { quarter: string; wfName: string; bu: string; formFactor: string; codeName: string; type: string; cmfSku: string; assetCount: number | null; dueDate: Date | null; remark: string; artist: string; teamStatus: string; requestor: string; producer: string; omgCode: string; cost: number | null; bmtId: string; deliverableStatus: string; isDelivered: boolean; wfInputDate: Date | null; requestedDueDate: Date | null; deliveryDate: Date | null; } // ─── Project Group ─────────────────────────────────────── interface ProjectGroup { wfName: string; bu: string; formFactor: string; codeName: string; quarter: string; requestor: string; producer: string; omgCode: string; bmtId: string; totalCost: number; dueDate: Date | null; isCompleted: boolean; rows: TrackerRow[]; } // ─── Sheet Parsers ─────────────────────────────────────── function parseQ4WIP(workbook: XLSX.WorkBook): TrackerRow[] { const sheet = workbook.Sheets["Q4_WIP_Projects"]; if (!sheet) return []; const json = XLSX.utils.sheet_to_json(sheet); const rows: TrackerRow[] = []; for (const r of json) { const wf = r["WF"]?.toString().trim(); if (!wf) continue; const { deliverableStatus, isDelivered } = mapTeamStatus(r["Team Status"]); rows.push({ quarter: r["Qtr"]?.toString() || "Q4", wfName: wf, bu: r["BU"]?.toString().trim() || "", formFactor: r["Form Factor"]?.toString().trim() || "", codeName: r["Code Names"]?.toString().trim() || "", type: r["Type"]?.toString().trim() || "Catalog", cmfSku: r["CMF/Sku"]?.toString().trim() || "", assetCount: r["#Asset"] ? parseInt(r["#Asset"]) || null : null, dueDate: parseDate(r["Due Date"]), remark: r["Remark"]?.toString().trim() || "", artist: r["Artist"]?.toString().trim() || "", teamStatus: r["Team Status"]?.toString().trim() || "", requestor: r["Requestor"]?.toString().trim() || "", producer: r["Producer"]?.toString().trim() || "", omgCode: r["OMG Code"]?.toString().trim() || "", cost: parseCost(r["Costs "]), bmtId: r["BMT ID"]?.toString().trim() || "", deliverableStatus, isDelivered, wfInputDate: null, requestedDueDate: null, deliveryDate: null, }); } return rows; } function parseQ2_2026(workbook: XLSX.WorkBook): TrackerRow[] { const sheet = workbook.Sheets["Q2_2026"]; if (!sheet) return []; const json = XLSX.utils.sheet_to_json(sheet); const rows: TrackerRow[] = []; for (const r of json) { const wf = r["WF"]?.toString().trim(); if (!wf) continue; const { deliverableStatus, isDelivered } = mapQ2Status(r[" Status"]); rows.push({ quarter: r["Qtr"]?.toString() || "FY26Q2", wfName: wf, bu: r["BU"]?.toString().trim() || "", formFactor: r["Form Factor"]?.toString().trim() || "", codeName: r["Code Names"]?.toString().trim() || "", type: r["Type"]?.toString().trim() || "Catalog", cmfSku: r["CMF/Sku"]?.toString().trim() || "", assetCount: r["#Asset"] ? parseInt(r["#Asset"]) || null : null, dueDate: parseDate(r["Delivery Date"]) || parseDate(r["Requested Due Date"]), remark: r["Remark"]?.toString().trim() || "", artist: r["Artist"]?.toString().trim() || "", teamStatus: r[" Status"]?.toString().trim() || "", requestor: r["Requestor"]?.toString().trim() || "", producer: r["Producer"]?.toString().trim() || "", omgCode: r["OMG Code"]?.toString().trim() || "", cost: parseCost(r["Costs "]), bmtId: r["BMT ID"]?.toString().trim() || "", deliverableStatus, isDelivered, wfInputDate: parseDate(r["WF Input Date"]), requestedDueDate: parseDate(r["Requested Due Date"]), deliveryDate: parseDate(r["Delivery Date"]), }); } return rows; } function parseDelivered(workbook: XLSX.WorkBook): TrackerRow[] { const sheet = workbook.Sheets["Qs_Delivered_Q3_Delivered_Tab"]; if (!sheet) return []; const json = XLSX.utils.sheet_to_json(sheet); const rows: TrackerRow[] = []; for (const r of json) { const wf = r["WF"]?.toString().trim(); if (!wf) continue; rows.push({ quarter: r["Qtr"]?.toString() || "Q3", wfName: wf, bu: r["BU"]?.toString().trim() || "", formFactor: r["Form Factor"]?.toString().trim() || "", codeName: r["Code Names"]?.toString().trim() || "", type: r["Type"]?.toString().trim() || "Catalog", cmfSku: r["CMF/Sku"]?.toString().trim() || "", assetCount: r["#Asset"] ? parseInt(r["#Asset"]) || null : null, dueDate: parseDate(r["Due Date"]), remark: r["Remark"]?.toString().trim() || "", artist: r["Artist"]?.toString().trim() || "", teamStatus: r["Team Status"]?.toString().trim() || "Delivered", requestor: r["Requestor"]?.toString().trim() || "", producer: r["Producer"]?.toString().trim() || "", omgCode: r["OMG Code"]?.toString().trim() || "", cost: parseCost(r["Costs "]), bmtId: r["BMT ID"]?.toString().trim() || "", deliverableStatus: "APPROVED", isDelivered: true, wfInputDate: null, requestedDueDate: null, deliveryDate: parseDate(r["Due Date"]), }); } return rows; } function parseAllSpins(workbook: XLSX.WorkBook): TrackerRow[] { const sheet = workbook.Sheets["All_Spins Project"]; if (!sheet) return []; const json = XLSX.utils.sheet_to_json(sheet); const rows: TrackerRow[] = []; for (const r of json) { const wf = r["WF/OMG"]?.toString().trim(); if (!wf) continue; const status = r["Status"]?.toString().trim() || ""; const isDelivered = status.toLowerCase().includes("delivered"); rows.push({ quarter: r["Qtr"]?.toString() || "", wfName: wf, bu: r["BU"]?.toString().trim() || "", formFactor: r["Form Factor"]?.toString().trim() || "", codeName: r["Code Names"]?.toString().trim() || "", type: r["Type"]?.toString().trim() || "360 Spin", cmfSku: r["CMF"]?.toString().trim() || "", assetCount: r["#Assets"] ? parseInt(r["#Assets"]) || null : null, dueDate: parseDate(r["Req Due Date"]) || parseDate(r["Planned Delivery Date"]), remark: r["Remarks"]?.toString().trim() || "", artist: r["Artist"]?.toString().trim() || "", teamStatus: status, requestor: r["Requestor"]?.toString().trim() || "", producer: r["Producer"]?.toString().trim() || "", omgCode: r["OMG Code"]?.toString().trim() || "", cost: parseCost(r["Amoumt"]), bmtId: r["BMT"]?.toString().trim() || "", deliverableStatus: isDelivered ? "APPROVED" : "IN_PROGRESS", isDelivered, wfInputDate: parseDate(r["WF Input Date"]), requestedDueDate: parseDate(r["Req Due Date"]), deliveryDate: parseDate(r["Planned Delivery Date"]), }); } return rows; } // ─── Group Rows into Projects ──────────────────────────── function groupIntoProjects(rows: TrackerRow[]): ProjectGroup[] { const map = new Map(); for (const row of rows) { // Use WF name as the project key const key = row.wfName.toLowerCase().replace(/\s+/g, " ").trim(); if (!key) continue; const existing = map.get(key); if (existing) { existing.rows.push(row); if (row.cost) existing.totalCost += row.cost; if (!existing.dueDate && row.dueDate) existing.dueDate = row.dueDate; if (row.dueDate && existing.dueDate && row.dueDate > existing.dueDate) { existing.dueDate = row.dueDate; } // Project is only completed if ALL rows are delivered if (!row.isDelivered) existing.isCompleted = false; } else { map.set(key, { wfName: row.wfName, bu: row.bu, formFactor: row.formFactor, codeName: row.codeName, quarter: row.quarter, requestor: row.requestor, producer: row.producer, omgCode: row.omgCode, bmtId: row.bmtId, totalCost: row.cost || 0, dueDate: row.dueDate, isCompleted: row.isDelivered, rows: [row], }); } } return Array.from(map.values()); } // ─── Generate Project Code ─────────────────────────────── let projectCounter = 0; function generateProjectCode(group: ProjectGroup): string { projectCounter++; const buCode = group.bu ? group.bu.replace(/[^A-Za-z]/g, "").substring(0, 3).toUpperCase() : "HP"; const qtr = group.quarter.replace(/[^A-Za-z0-9]/g, "").toUpperCase(); return `${buCode}-${qtr}-${String(projectCounter).padStart(3, "0")}`; } // ─── Determine Pipeline Stage Statuses ─────────────────── function getStageStatuses( row: TrackerRow, stageTemplates: { id: string; slug: string; order: number }[] ): Map { const statuses = new Map(); const typeLower = (row.type || "catalog").toLowerCase().trim(); const activeSlug = TYPE_TO_STAGE_SLUG[typeLower] || "catalog-images"; // Find the order of the active stage const activeTemplate = stageTemplates.find((t) => t.slug === activeSlug); const activeOrder = activeTemplate?.order || 5; for (const template of stageTemplates) { if (row.isDelivered) { // For delivered items: all stages up to and including the active one are DELIVERED if (template.order <= activeOrder) { statuses.set(template.slug, "DELIVERED"); } else { statuses.set(template.slug, "SKIPPED"); } } else if (row.deliverableStatus === "NOT_STARTED") { if (template.order === 1) { statuses.set(template.slug, "NOT_STARTED"); } else { statuses.set(template.slug, "BLOCKED"); } } else if (row.deliverableStatus === "ON_HOLD") { if (template.order <= 2) { statuses.set(template.slug, "APPROVED"); } else { statuses.set(template.slug, "BLOCKED"); } } else { // IN_PROGRESS or IN_REVIEW if (template.order < activeOrder - 1) { statuses.set(template.slug, "DELIVERED"); } else if (template.order === activeOrder - 1) { statuses.set(template.slug, "APPROVED"); } else if (template.order === activeOrder) { if (row.deliverableStatus === "IN_REVIEW") { statuses.set(template.slug, "IN_REVIEW"); } else { statuses.set(template.slug, "IN_PROGRESS"); } } else { statuses.set(template.slug, "BLOCKED"); } } } return statuses; } // ─── Main ──────────────────────────────────────────────── async function main() { const excelPath = path.resolve( __dirname, "../assets/temp/Master_CG Tracker (1).xlsx" ); if (!fs.existsSync(excelPath)) { console.error(`Excel file not found: ${excelPath}`); process.exit(1); } console.log("Reading Excel tracker..."); const workbook = XLSX.readFile(excelPath, { cellDates: true }); // Parse all sheets const q4Rows = parseQ4WIP(workbook); const q2Rows = parseQ2_2026(workbook); const deliveredRows = parseDelivered(workbook); const spinRows = parseAllSpins(workbook); console.log(` Q4 WIP: ${q4Rows.length} rows`); console.log(` Q2 2026: ${q2Rows.length} rows`); console.log(` Delivered: ${deliveredRows.length} rows`); console.log(` Spins: ${spinRows.length} rows`); const allRows = [...q2Rows, ...q4Rows, ...deliveredRows, ...spinRows]; const projects = groupIntoProjects(allRows); console.log(`\nGrouped into ${projects.length} projects`); // Get pipeline stage templates const templates = await prisma.pipelineStageTemplate.findMany({ include: { dependsOn: true }, orderBy: { order: "asc" }, }); if (templates.length === 0) { console.error( "No pipeline stage templates found. Run the base seed first (prisma db seed)." ); process.exit(1); } console.log(`Found ${templates.length} pipeline stage templates`); // Get the dev organization const org = await prisma.organization.findFirst({ where: { id: "dev-org-001" }, }); if (!org) { console.error( "Dev organization not found. Run the base seed first (prisma db seed)." ); process.exit(1); } // Clear existing projects (to allow re-runs) const existingCount = await prisma.project.count({ where: { organizationId: org.id }, }); if (existingCount > 0) { console.log(`\nClearing ${existingCount} existing projects...`); await prisma.project.deleteMany({ where: { organizationId: org.id } }); } // Create projects and deliverables let totalDeliverables = 0; let totalStages = 0; let createdProjects = 0; for (const group of projects) { // Skip projects with empty names or too-short names if (group.wfName.length < 3) continue; // Determine project status const hasOnHold = group.rows.some( (r) => r.deliverableStatus === "ON_HOLD" ); const allDelivered = group.rows.every((r) => r.isDelivered); const allNotStarted = group.rows.every( (r) => r.deliverableStatus === "NOT_STARTED" ); let projectStatus: string; if (allDelivered) { projectStatus = "COMPLETED"; } else if (hasOnHold && group.rows.every((r) => r.deliverableStatus === "ON_HOLD" || r.isDelivered)) { projectStatus = "ON_HOLD"; } else { projectStatus = "ACTIVE"; } // Determine priority based on cost or due date proximity let priority = "MEDIUM"; if (group.totalCost > 20000) priority = "HIGH"; else if (group.totalCost > 40000) priority = "URGENT"; else if (group.totalCost < 3000 && group.totalCost > 0) priority = "LOW"; const projectCode = generateProjectCode(group); try { const project = await prisma.project.create({ data: { projectCode, name: group.wfName, status: projectStatus as any, priority: priority as any, businessUnit: group.bu || null, formFactor: group.formFactor || null, codeName: group.codeName || null, quarter: group.quarter || null, requestor: group.requestor || null, omgCode: group.omgCode || null, bmtId: group.bmtId || null, estimatedCost: group.totalCost > 0 ? group.totalCost : null, dueDate: group.dueDate, organizationId: org.id, }, }); // Create deliverables for each row in this project for (const row of group.rows) { const delivName = row.codeName ? `${row.codeName} - ${row.type || "Catalog"}` : `${group.wfName} - ${row.type || "Catalog"}`; const deliverable = await prisma.deliverable.create({ data: { projectId: project.id, name: delivName.length > 200 ? delivName.substring(0, 200) : delivName, status: row.deliverableStatus as any, priority: priority as any, dueDate: row.dueDate, notes: row.remark || null, cmfSku: row.cmfSku || null, assetCount: row.assetCount, requestedDueDate: row.requestedDueDate, plannedDeliveryDate: row.deliveryDate, actualDeliveryDate: row.isDelivered ? row.deliveryDate : null, wfInputDate: row.wfInputDate, }, }); // Create pipeline stages with appropriate statuses const stageStatuses = getStageStatuses(row, templates as any); const stageData = templates.map((template) => { const status = (stageStatuses.get(template.slug) as any) || "BLOCKED"; // Set dates based on status let startDate: Date | null = null; let completedDate: Date | null = null; if ( status === "DELIVERED" || status === "APPROVED" || status === "SKIPPED" ) { // For completed stages, set reasonable dates startDate = row.dueDate ? new Date( row.dueDate.getTime() - (template.estimatedDays || 3) * 86400000 * 2 ) : null; completedDate = row.dueDate ? new Date( row.dueDate.getTime() - (10 - template.order) * 86400000 ) : null; } else if (status === "IN_PROGRESS" || status === "IN_REVIEW") { startDate = new Date( Date.now() - (template.estimatedDays || 2) * 86400000 ); } return { deliverableId: deliverable.id, templateId: template.id, status, dueDate: row.dueDate, startDate, completedDate, }; }); await prisma.deliverableStage.createMany({ data: stageData }); totalStages += stageData.length; totalDeliverables++; } createdProjects++; } catch (err: any) { // Skip duplicates or other errors silently if (!err.message?.includes("Unique constraint")) { console.error( ` Error creating project "${group.wfName}": ${err.message}` ); } } } console.log(`\n=== Seed Complete ===`); console.log(` Projects created: ${createdProjects}`); console.log(` Deliverables created: ${totalDeliverables}`); console.log(` Stages created: ${totalStages}`); // Print summary by status const statusCounts = await prisma.project.groupBy({ by: ["status"], _count: true, where: { organizationId: org.id }, }); console.log(`\n Project Status Breakdown:`); for (const s of statusCounts) { console.log(` ${s.status}: ${s._count}`); } const delivStatusCounts = await prisma.deliverable.groupBy({ by: ["status"], _count: true, }); console.log(`\n Deliverable Status Breakdown:`); for (const s of delivStatusCounts) { console.log(` ${s.status}: ${s._count}`); } } main() .catch((e) => { console.error(e); process.exit(1); }) .finally(async () => { await prisma.$disconnect(); });