hp-prod-tracker/prisma/seed-tracker-data.ts
Leivur Djurhuus edcf31672e feat: enhance UI components and add assignment feature to deliverables
- 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.
2026-03-02 13:46:55 -06:00

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();
});