- Updated CommandItem component to use rounded-lg for better aesthetics. - Modified DialogOverlay and DialogContent to improve backdrop and border radius. - Changed DropdownMenuItem, DropdownMenuCheckboxItem, and DropdownMenuRadioItem to use rounded-md for consistency. - Enhanced SelectItem with rounded-md for a more modern look. - Updated SheetOverlay to improve backdrop styling. - Adjusted Toaster component border radius for a more refined appearance. - Enhanced Table component with rounded-xl and shadow for better visual hierarchy. - Added assignment display feature in DeliverableTable and KanbanBoard components, showing assigned users with badges. - Updated deliverable service to include assignments in the data fetching process. - Created a new seed script for tracker data to facilitate testing and development.
695 lines
23 KiB
TypeScript
695 lines
23 KiB
TypeScript
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<string, string> = {
|
|
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<any>(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<any>(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<any>(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<any>(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<string, ProjectGroup>();
|
|
|
|
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<string, string> {
|
|
const statuses = new Map<string, string>();
|
|
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();
|
|
});
|