Files
CapaKraken/packages/db/src/seed.ts
T

1304 lines
85 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
AllocationStatus,
AllocationType,
BlueprintTarget,
FieldType,
OrderType,
ProjectStatus,
ResourceType,
SystemRole,
VacationStatus,
VacationType,
} from "@capakraken/shared";
import { PrismaClient, type Prisma, type Resource, type Project } from "@prisma/client";
import { hash } from "@node-rs/argon2";
import { getHolidayDemoProfileForIndex } from "./holiday-demo-profiles.js";
import { loadWorkspaceEnv } from "./load-workspace-env.js";
import { assertSafeSeedTarget } from "./safe-destructive-env.js";
import { buildSystemRoleConfigSeedData } from "./system-role-config-defaults.js";
loadWorkspaceEnv();
const prisma = new PrismaClient();
async function seedSystemRoleConfigs() {
for (const config of buildSystemRoleConfigSeedData()) {
await prisma.systemRoleConfig.upsert({
where: { role: config.role },
update: {
label: config.label,
description: config.description,
defaultPermissions: config.defaultPermissions,
color: config.color,
sortOrder: config.sortOrder,
},
create: config,
});
}
}
// ─── Skill helpers ─────────────────────────────────────────────────────────────
interface SkillEntry {
skill: string;
proficiency: number;
category: string;
}
function computeProficiency(lcr: number): number {
if (lcr >= 118) return 5;
if (lcr >= 95) return 4;
return 3;
}
function computeSkills(chapter: string, typeOfWork: string, lcr: number): SkillEntry[] {
const prof = computeProficiency(lcr);
const p2 = Math.max(2, prof - 1);
if (chapter === "Project Management" && typeOfWork === "Project Management") {
return [
{ skill: "Project Manager", proficiency: prof, category: "PM" },
{ skill: "Scrum Master", proficiency: p2, category: "PM" },
];
}
if (chapter === "Digital Content Production" && typeOfWork === "Unreal Engine Layerstack") {
return [
{ skill: "Unreal Engine", proficiency: prof, category: "3D" },
{ skill: "3D Lighting", proficiency: p2, category: "3D" },
{ skill: "3D Modeling", proficiency: p2, category: "3D" },
];
}
if (chapter === "Product Data Management" && typeOfWork === "Vis Logic") {
return [
{ skill: "Visualization Logic", proficiency: prof, category: "PDM" },
{ skill: "Quality Assurance", proficiency: p2, category: "PDM" },
];
}
if (chapter === "Art Direction" && typeOfWork === "Art Direction") {
return [
{ skill: "Art Direction", proficiency: prof, category: "Art" },
{ skill: "2D Compositing", proficiency: p2, category: "Art" },
];
}
if (chapter === "CGI-Dev" && typeOfWork === "Dev Unreal") {
return [
{ skill: "Unreal Dev", proficiency: prof, category: "Dev" },
{ skill: "Technical Architect", proficiency: p2, category: "Dev" },
];
}
if (chapter === "CGI-Dev" && typeOfWork === "Dev Frontend") {
return [
{ skill: "Frontend Dev", proficiency: prof, category: "Dev" },
{ skill: "Backend Dev", proficiency: p2, category: "Dev" },
];
}
return [{ skill: typeOfWork, proficiency: prof, category: chapter }];
}
// ─── Availability helpers ───────────────────────────────────────────────────────
interface WeekdayAvailability {
monday: number;
tuesday: number;
wednesday: number;
thursday: number;
friday: number;
}
function computeAvailability(fraction: number, availDays: string): WeekdayAvailability {
const full = 8;
const half = Math.round(full * fraction);
if (availDays === "all") {
if (fraction === 1.0) {
return { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8 };
}
if (fraction === 0.5) {
return { monday: 4, tuesday: 4, wednesday: 4, thursday: 4, friday: 4 };
}
if (fraction === 0.8) {
return { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 0 };
}
return { monday: half, tuesday: half, wednesday: half, thursday: half, friday: half };
}
if (availDays === "tue,wed,thu,fri") {
return { monday: 0, tuesday: 8, wednesday: 8, thursday: 8, friday: 8 };
}
if (availDays === "mon,tue,thu,fri") {
return { monday: 8, tuesday: 8, wednesday: 0, thursday: 8, friday: 8 };
}
if (availDays === "mon,tue,wed,thu") {
return { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 0 };
}
return { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8 };
}
// ─── Resource data ─────────────────────────────────────────────────────────────
// [eid, chapter, typeOfWork, clientUnit, city, employeeType, lcr, ucr, fraction, availDays, chargeability]
type ResourceRow = [
string,
string,
string,
string,
string,
string,
number,
number,
number,
string,
number,
];
const RESOURCE_DATA: ResourceRow[] = [
// ── Project Management ──
["steve.rogers", "Project Management", "Project Management", "Porsche AG", "Stuttgart", "Employee", 133.77, 74.65, 1.0, "all", 0.75],
["bruce.banner", "Project Management", "Project Management", "Daimler", "Stuttgart", "Employee", 118.35, 88.52, 1.0, "all", 0.75],
["natasha.romanoff","Project Management", "Project Management", "Italy (FER/MAS)", "Stuttgart", "Employee", 133.77, 74.65, 1.0, "all", 1.0 ],
["charles.xavier", "Project Management", "Project Management", "Moving Image", "Muenchen", "Employee", 96.77, 65.24, 1.0, "all", 0.75],
["scott.summers", "Project Management", "Project Management", "Moving Image", "Stuttgart", "Employee", 65.00, 88.52, 0.8, "mon,tue,thu,fri", 0.75],
["ororo.munroe", "Project Management", "Project Management", "Moving Image", "Stuttgart", "Employee", 118.35, 88.52, 1.0, "all", 0.75],
["gamora.gamora", "Project Management", "Project Management", "Moving Image", "Hamburg", "Employee", 67.00, 88.52, 0.8, "mon,tue,wed,thu", 1.0 ],
// ── 3D / Digital Content Production ──
["tony.stark", "Digital Content Production","Unreal Engine Layerstack", "Italy (FER/MAS)", "Hamburg", "Employee", 96.77, 65.24, 1.0, "all", 0.75],
["thor.odinson", "Digital Content Production","Unreal Engine Layerstack", "Daimler", "Stuttgart", "Employee", 76.64, 55.98, 1.0, "all", 0.75],
["sam.wilson", "Digital Content Production","Unreal Engine Layerstack", "Porsche AG", "Stuttgart", "Employee", 133.77, 74.65, 1.0, "all", 0.75],
["james.barnes", "Digital Content Production","Unreal Engine Layerstack", "Porsche AG", "Hamburg", "Employee", 118.35, 88.52, 1.0, "all", 0.75],
["wanda.maximoff", "Digital Content Production","Unreal Engine Layerstack", "BMW", "Stuttgart", "Employee", 76.64, 55.98, 0.8, "tue,wed,thu,fri", 0.75],
["peter.parker", "Digital Content Production","Unreal Engine Layerstack", "BMW", "Hamburg", "Employee", 96.77, 65.24, 1.0, "all", 0.75],
["miles.morales", "Digital Content Production","Unreal Engine Layerstack", "Cross-Unit", "Stuttgart", "Employee", 76.64, 55.98, 1.0, "all", 0.86],
["jessica.drew", "Digital Content Production","Unreal Engine Layerstack", "BMW", "Stuttgart", "Employee", 76.64, 55.98, 1.0, "all", 0.75],
["logan.howlett", "Digital Content Production","Unreal Engine Layerstack", "BMW", "Hamburg", "Employee", 66.67, 49.18, 1.0, "all", 0.75],
["erik.lehnsherr", "Digital Content Production","Unreal Engine Layerstack", "Daimler", "Hamburg", "Employee", 66.67, 49.18, 1.0, "all", 1.0 ],
["jean.grey", "Digital Content Production","Unreal Engine Layerstack", "Daimler", "Muenchen", "Employee", 133.77, 74.65, 1.0, "all", 0.75],
["stephen.strange", "Digital Content Production","Unreal Engine Layerstack", "Porsche AG", "Muenchen", "Employee", 66.67, 49.18, 1.0, "all", 0.75],
["wong.wong", "Digital Content Production","Unreal Engine Layerstack", "Porsche AG", "Stuttgart", "Employee", 66.67, 49.18, 0.5, "all", 0.75],
// ── Art Direction ──
["vision.vision", "Art Direction", "Art Direction", "BMW", "Stuttgart", "Employee", 66.67, 49.18, 1.0, "all", 0.75],
["drax.drax", "Art Direction", "Art Direction", "Daimler", "Stuttgart", "Employee", 96.77, 65.24, 1.0, "all", 0.75],
["rocket.rocket", "Art Direction", "Art Direction", "Cross-Unit", "Stuttgart", "Employee", 118.35, 88.52, 0.8, "mon,tue,wed,thu", 0.75],
["groot.groot", "Art Direction", "Art Direction", "Jaguar Land Rover", "Hamburg", "Employee", 118.35, 88.52, 1.0, "all", 0.75],
["tchalla.tchalla", "Art Direction", "Art Direction", "Jaguar Land Rover", "Stuttgart", "Employee", 96.77, 65.24, 1.0, "all", 0.9 ],
// ── Product Data Management / Compositor ──
["clint.barton", "Product Data Management", "Vis Logic", "Cross-Unit", "Muenchen", "Employee", 76.64, 55.98, 1.0, "all", 0.9 ],
["hank.mccoy", "Product Data Management", "Vis Logic", "Porsche AG", "Muenchen", "Employee", 133.77, 74.65, 1.0, "all", 1.0 ],
["anna.marie", "Product Data Management", "Vis Logic", "Daimler", "Hamburg", "Employee", 118.35, 88.52, 1.0, "all", 0.86],
["peter.quill", "Product Data Management", "Vis Logic", "Moving Image", "Stuttgart", "Employee", 95.00, 88.52, 1.0, "all", 0.75],
// ── CGI-Dev Unreal ──
["matt.murdock", "CGI-Dev", "Dev Unreal", "Daimler", "Muenchen", "Employee", 118.35, 88.52, 1.0, "all", 1.0 ],
["carl.lucas", "CGI-Dev", "Dev Unreal", "Daimler", "Muenchen", "Employee", 133.77, 74.65, 0.5, "all", 0.75],
["jessica.jones", "CGI-Dev", "Dev Unreal", "Porsche AG", "Stuttgart", "Employee", 76.64, 55.98, 1.0, "all", 0.85],
["danny.rand", "CGI-Dev", "Dev Unreal", "Porsche AG", "Stuttgart", "Employee", 118.35, 88.52, 1.0, "all", 0.75],
// ── CGI-Dev Frontend ──
["frank.castle", "CGI-Dev", "Dev Frontend", "Daimler", "Hamburg", "Employee", 96.77, 65.24, 0.5, "all", 0.75],
["carol.danvers", "CGI-Dev", "Dev Frontend", "Moving Image", "Stuttgart", "Freelancer", 76.64, 55.98, 1.0, "all", 0.85],
["kamala.khan", "CGI-Dev", "Dev Frontend", "Jaguar Land Rover", "Stuttgart", "Freelancer", 76.64, 55.98, 1.0, "all", 0.75],
];
// ─── Project data ──────────────────────────────────────────────────────────────
// [shortCode, name, clientTag, isOrdered, orderTypeStr, winProb, allocTypeStr, budgetCents]
type ProjectRow = [string, string, string, boolean, string, number, string, number];
const PROJECT_DATA: ProjectRow[] = [
// ── Completed 3D Stills ────────────────────────────────────────────────────
["PORS24A", "Porsche 911 GT3 Campaign Stills", "PAG", true, "CHARGEABLE", 100, "EXT", 11500000],
["BMW24B", "BMW M5 Competition Pack", "BMW", true, "CHARGEABLE", 100, "EXT", 8800000],
["JLR24C", "Jaguar F-Type Reveal Stills", "JLR", true, "CHARGEABLE", 100, "EXT", 6700000],
["DAI24E", "Mercedes AMG Night Edition Stills", "DAI", true, "CHARGEABLE", 100, "EXT", 9200000],
["JLR24G", "Defender Trail Series Stills", "JLR", true, "BD", 100, "EXT", 5500000],
["AUDI25C", "Audi Q8 e-tron Campaign Stills", "PAG", true, "CHARGEABLE", 100, "EXT", 10500000],
["JLR25E", "Range Rover Lifestyle Stills", "JLR", true, "CHARGEABLE", 100, "EXT", 7200000],
["DAI25F", "Mercedes S-Class Interior Viz", "DAI", true, "CHARGEABLE", 100, "EXT", 9800000],
["BMW25H", "BMW M3 Competition Still Pack", "BMW", true, "CHARGEABLE", 100, "EXT", 8500000],
// ── Completed Animated Movies ──────────────────────────────────────────────
["HERO24D", "Heritage Brand Film Stuttgart", "PAG", true, "CHARGEABLE", 100, "EXT", 28500000],
["BMW24F", "BMW iX Vision Brand Film", "BMW", true, "CHARGEABLE", 100, "EXT", 34000000],
["DAI24H", "AMG GT Campaign 2024", "DAI", true, "CHARGEABLE", 100, "EXT", 21000000],
["JLR25A", "Defender Chronicles Pilot Film", "JLR", true, "CHARGEABLE", 100, "EXT", 43500000],
["PAG25B", "Porsche 75 Years Anniversary Film", "PAG", true, "CHARGEABLE", 100, "EXT", 44500000],
["BMW25D", "MINI Electric Short Film", "BMW", true, "CHARGEABLE", 100, "EXT", 19500000],
// ── Completed Pipeline / Internal ─────────────────────────────────────────
["DEV24A", "Render Farm v3 Migration", "INT", true, "OVERHEAD", 100, "INT", 5200000],
["DEV25A", "Houdini UE5 Pipeline Bridge", "INT", true, "INTERNAL", 100, "INT", 7800000],
// ── Active Animated Movies ─────────────────────────────────────────────────
["PAG25G", "Porsche Taycan Sport Film", "PAG", true, "CHARGEABLE", 100, "EXT", 32000000],
["JLR26A", "Jaguar EV Launch Film", "JLR", true, "CHARGEABLE", 100, "EXT", 38500000],
// ── Active Pipeline ────────────────────────────────────────────────────────
["DEV25B", "Asset Library & Pipeline Rebuild", "INT", true, "OVERHEAD", 100, "INT", 6500000],
// ── Active 3D Stills ───────────────────────────────────────────────────────
["DAI26B", "AMG EV Reveal Campaign Stills", "DAI", true, "CHARGEABLE", 100, "EXT", 12500000],
["PAG26C", "Porsche Carrera Grand Viz", "PAG", true, "CHARGEABLE", 100, "EXT", 11000000],
// ── Draft 3D Stills ────────────────────────────────────────────────────────
["BMW26B", "BMW X5 Facelift Campaign", "BMW", false, "CHARGEABLE", 80, "EXT", 9500000],
["BMW26C", "MINI Aceman Launch Campaign", "BMW", false, "CHARGEABLE", 75, "EXT", 7800000],
// ── Draft Animated Movies ──────────────────────────────────────────────────
["JLR26B", "Defender 90 Anniversary Film", "JLR", false, "CHARGEABLE", 90, "EXT", 41000000],
["PAG26D", "Porsche Mission X Concept Film", "PAG", false, "BD", 60, "EXT", 45000000],
["DAI26C", "Mercedes G-Class Offroad Film", "DAI", false, "CHARGEABLE", 85, "EXT", 29500000],
// ── Draft Pipeline ─────────────────────────────────────────────────────────
["DEV26A", "UE5 Realtime Pipeline v4", "INT", false, "INTERNAL", 100, "INT", 8500000],
];
// shortCode → [startDate, endDate, status]
const PROJECT_DATES: Record<string, [string, string, ProjectStatus]> = {
// Completed 3D Stills
PORS24A: ["2024-02-05", "2024-03-15", ProjectStatus.COMPLETED],
BMW24B: ["2024-03-18", "2024-05-03", ProjectStatus.COMPLETED],
JLR24C: ["2024-04-08", "2024-05-17", ProjectStatus.COMPLETED],
DAI24E: ["2024-06-03", "2024-07-12", ProjectStatus.COMPLETED],
JLR24G: ["2024-09-02", "2024-10-11", ProjectStatus.COMPLETED],
AUDI25C: ["2025-05-05", "2025-06-20", ProjectStatus.COMPLETED],
JLR25E: ["2025-08-04", "2025-09-26", ProjectStatus.COMPLETED],
DAI25F: ["2025-09-01", "2025-10-17", ProjectStatus.COMPLETED],
BMW25H: ["2025-11-03", "2025-12-31", ProjectStatus.COMPLETED],
// Completed Animated Movies
HERO24D: ["2024-05-06", "2024-09-27", ProjectStatus.COMPLETED],
BMW24F: ["2024-08-05", "2024-12-20", ProjectStatus.COMPLETED],
DAI24H: ["2024-10-07", "2025-01-31", ProjectStatus.COMPLETED],
JLR25A: ["2025-02-03", "2025-09-30", ProjectStatus.COMPLETED],
PAG25B: ["2025-04-01", "2025-12-19", ProjectStatus.COMPLETED],
BMW25D: ["2025-07-07", "2025-11-28", ProjectStatus.COMPLETED],
// Completed Pipeline
DEV24A: ["2024-07-01", "2024-08-30", ProjectStatus.COMPLETED],
DEV25A: ["2025-06-02", "2025-09-30", ProjectStatus.COMPLETED],
// Active (currently running — today is 2026-03-04)
PAG25G: ["2025-10-01", "2026-03-31", ProjectStatus.ACTIVE],
JLR26A: ["2026-01-05", "2026-07-31", ProjectStatus.ACTIVE],
DEV25B: ["2025-11-03", "2026-04-30", ProjectStatus.ACTIVE],
DAI26B: ["2026-02-02", "2026-04-17", ProjectStatus.ACTIVE],
PAG26C: ["2026-02-16", "2026-04-24", ProjectStatus.ACTIVE],
// Draft / Planned
BMW26B: ["2026-05-04", "2026-06-26", ProjectStatus.DRAFT],
BMW26C: ["2026-10-01", "2026-11-27", ProjectStatus.DRAFT],
JLR26B: ["2026-06-01", "2026-11-30", ProjectStatus.DRAFT],
PAG26D: ["2026-07-01", "2027-01-31", ProjectStatus.DRAFT],
DAI26C: ["2026-09-01", "2027-02-28", ProjectStatus.DRAFT],
DEV26A: ["2026-05-04", "2026-08-28", ProjectStatus.DRAFT],
};
function parseOrderType(s: string): OrderType {
switch (s) {
case "BD": return OrderType.BD;
case "CHARGEABLE": return OrderType.CHARGEABLE;
case "INTERNAL": return OrderType.INTERNAL;
case "OVERHEAD": return OrderType.OVERHEAD;
default: return OrderType.CHARGEABLE;
}
}
function parseAllocationType(s: string): AllocationType {
return s === "INT" ? AllocationType.INT : AllocationType.EXT;
}
// ─── Main ──────────────────────────────────────────────────────────────────────
async function main() {
const target = assertSafeSeedTarget("db:seed");
console.warn(`Seeding CapaKraken example data into ${target.databaseName} (${target.hostname}${target.port ? `:${target.port}` : ""})...`);
// ── 1. Delete all data (keep users) ────────────────────────────────────────
console.warn(`Deleting existing data from disposable seed target '${target.databaseName}'...`);
await prisma.auditLog.deleteMany({});
await prisma.notification.deleteMany({});
// Estimates (deep hierarchy)
await prisma.estimateExport.deleteMany({});
await prisma.estimateMetric.deleteMany({});
await prisma.estimateDemandLine.deleteMany({});
await prisma.estimateAssumption.deleteMany({});
await prisma.resourceCostSnapshot.deleteMany({});
await prisma.estimateVersion.deleteMany({});
await prisma.estimate.deleteMany({});
// Assignments + demands before projects/resources
await prisma.assignment.deleteMany({});
await prisma.demandRequirement.deleteMany({});
await prisma.resourceRole.deleteMany({});
await prisma.vacation.deleteMany({});
await prisma.vacationEntitlement.deleteMany({});
await prisma.calculationRule.deleteMany({});
await prisma.project.deleteMany({});
await prisma.resource.deleteMany({});
await prisma.role.deleteMany({});
await prisma.blueprint.deleteMany({});
// Dispo v2 entities (children before parents)
await prisma.managementLevel.deleteMany({});
await prisma.managementLevelGroup.deleteMany({});
await prisma.metroCity.deleteMany({});
await prisma.country.deleteMany({});
await prisma.orgUnit.deleteMany({});
await prisma.utilizationCategory.deleteMany({});
await prisma.client.deleteMany({});
console.warn("Existing data deleted.");
// ── 2. Upsert users ────────────────────────────────────────────────────────
const adminHash = await hash("admin123");
const managerHash = await hash("manager123");
const viewerHash = await hash("viewer123");
const admin = await prisma.user.upsert({
where: { email: "admin@capakraken.dev" },
update: { passwordHash: adminHash },
create: { email: "admin@capakraken.dev", name: "Admin User", passwordHash: adminHash, systemRole: SystemRole.ADMIN },
});
const manager = await prisma.user.upsert({
where: { email: "manager@capakraken.dev" },
update: { passwordHash: managerHash },
create: { email: "manager@capakraken.dev", name: "Manager User", passwordHash: managerHash, systemRole: SystemRole.MANAGER },
});
const viewer = await prisma.user.upsert({
where: { email: "viewer@capakraken.dev" },
update: { passwordHash: viewerHash },
create: { email: "viewer@capakraken.dev", name: "Viewer User", passwordHash: viewerHash, systemRole: SystemRole.VIEWER },
});
console.warn(`Users: admin=${admin.id}, manager=${manager.id}, viewer=${viewer.id}`);
await seedSystemRoleConfigs();
// ── 2b. Create Dispo v2 entities ──────────────────────────────────────────
// Countries + Metro Cities
const countryDE = await prisma.country.create({
data: { code: "DE", name: "Germany", dailyWorkingHours: 8.0 },
});
const countryIN = await prisma.country.create({
data: { code: "IN", name: "India", dailyWorkingHours: 9.0 },
});
const countryES = await prisma.country.create({
data: {
code: "ES",
name: "Spain",
dailyWorkingHours: 8.0,
scheduleRules: {
type: "spain",
regularHours: 8.5,
fridayHours: 7,
summerHours: 7,
summerPeriod: { from: "06-15", to: "09-15" },
} as unknown as Prisma.InputJsonValue,
},
});
const countryUS = await prisma.country.create({
data: { code: "US", name: "United States", dailyWorkingHours: 8.0 },
});
const cityMap = new Map<string, string>(); // cityName → id
for (const [countryId, cities] of [
[countryDE.id, ["Augsburg", "Berlin", "Hamburg", "Koeln", "Muenchen", "Stuttgart"]],
[countryIN.id, ["Bangalore", "Mumbai"]],
[countryES.id, ["Madrid", "Barcelona"]],
[countryUS.id, ["New York", "Los Angeles"]],
] as [string, string[]][]) {
for (const cityName of cities) {
const city = await prisma.metroCity.create({
data: { name: cityName, countryId },
});
cityMap.set(cityName, city.id);
}
}
console.warn(`Countries: 4 created, Metro Cities: ${cityMap.size} created`);
// Org Units (3-level hierarchy: L5 → L6 → L7)
const orgUnitMap = new Map<string, string>(); // name → id
const l5 = await prisma.orgUnit.create({
data: { name: "Studio Services", level: 5, sortOrder: 1 },
});
orgUnitMap.set("Studio Services", l5.id);
const l6ContentProd = await prisma.orgUnit.create({
data: { name: "Content Production", level: 6, parentId: l5.id, sortOrder: 1 },
});
orgUnitMap.set("Content Production", l6ContentProd.id);
for (const [name, order] of [["3D Visualization", 1], ["Animation", 2], ["Art Direction", 3]] as [string, number][]) {
const unit = await prisma.orgUnit.create({
data: { name, level: 7, parentId: l6ContentProd.id, sortOrder: order },
});
orgUnitMap.set(name, unit.id);
}
const l6Tech = await prisma.orgUnit.create({
data: { name: "Technology", level: 6, parentId: l5.id, sortOrder: 2 },
});
orgUnitMap.set("Technology", l6Tech.id);
for (const [name, order] of [["CGI Development", 1], ["Pipeline Engineering", 2]] as [string, number][]) {
const unit = await prisma.orgUnit.create({
data: { name, level: 7, parentId: l6Tech.id, sortOrder: order },
});
orgUnitMap.set(name, unit.id);
}
const l6Ops = await prisma.orgUnit.create({
data: { name: "Operations", level: 6, parentId: l5.id, sortOrder: 3 },
});
orgUnitMap.set("Operations", l6Ops.id);
for (const [name, order] of [["Product Data Management", 1], ["Project Management", 2]] as [string, number][]) {
const unit = await prisma.orgUnit.create({
data: { name, level: 7, parentId: l6Ops.id, sortOrder: order },
});
orgUnitMap.set(name, unit.id);
}
console.warn(`Org Units: ${orgUnitMap.size} created`);
// Utilization Categories
const utilCatMap = new Map<string, string>(); // code → id
const UTIL_CATS = [
{ code: "Chg", name: "Chargeable", isDefault: true, sortOrder: 1 },
{ code: "BD", name: "Business Development", sortOrder: 2 },
{ code: "MD&I", name: "Market Development & Innovation", sortOrder: 3 },
{ code: "M&O", name: "Management & Operations", sortOrder: 4 },
{ code: "PD&R", name: "People Development & Recruiting", sortOrder: 5 },
{ code: "Absence", name: "Absence", sortOrder: 6 },
];
for (const cat of UTIL_CATS) {
const created = await prisma.utilizationCategory.create({ data: cat });
utilCatMap.set(cat.code, created.id);
}
console.warn(`Utilization Categories: ${utilCatMap.size} created`);
// Management Level Groups + Levels
const mgmtGroupMap = new Map<string, string>(); // groupName → id
const mgmtLevelMap = new Map<string, string>(); // levelName → id
const MGMT_GROUPS = [
{ name: "Accenture Leadership", target: 0.365, levels: ["CL10-Managing Director", "CL9-Senior Managing Director"] },
{ name: "Senior Manager", target: 0.546, levels: ["CL8-Senior Manager"] },
{ name: "Manager", target: 0.747, levels: ["CL7-Manager"] },
{ name: "Consultant", target: 0.808, levels: ["CL6-Consultant", "CL5-Senior Analyst"] },
{ name: "Analyst", target: 0.805, levels: ["CL4-Analyst"] },
{ name: "Associate", target: 0.77, levels: ["CL3-Associate", "CL2-Junior Associate"] },
];
for (const g of MGMT_GROUPS) {
const group = await prisma.managementLevelGroup.create({
data: { name: g.name, targetPercentage: g.target, sortOrder: MGMT_GROUPS.indexOf(g) + 1 },
});
mgmtGroupMap.set(g.name, group.id);
for (const levelName of g.levels) {
const level = await prisma.managementLevel.create({
data: { name: levelName, groupId: group.id },
});
mgmtLevelMap.set(levelName, level.id);
}
}
console.warn(`Management Level Groups: ${mgmtGroupMap.size}, Levels: ${mgmtLevelMap.size} created`);
// Clients (hierarchy: Master → Entity)
const clientMap = new Map<string, string>(); // name → id
const CLIENT_TREE = [
{ master: "Automotive OEM", entities: ["Porsche AG", "BMW Group", "Daimler / Mercedes", "Jaguar Land Rover"] },
{ master: "Moving Image & Digital", entities: ["Moving Image", "Cross-Unit"] },
{ master: "Internal", entities: ["Internal Projects"] },
];
for (const tree of CLIENT_TREE) {
const master = await prisma.client.create({
data: { name: tree.master, sortOrder: CLIENT_TREE.indexOf(tree) + 1 },
});
clientMap.set(tree.master, master.id);
for (const entityName of tree.entities) {
const entity = await prisma.client.create({
data: { name: entityName, parentId: master.id, sortOrder: tree.entities.indexOf(entityName) + 1 },
});
clientMap.set(entityName, entity.id);
}
}
console.warn(`Clients: ${clientMap.size} created`);
// ── 3. Create blueprints ───────────────────────────────────────────────────
// Shared option sets
const deliveryFormatOptions = [
{ value: "WebHD_1080p", label: "Web HD (1080p)" },
{ value: "4K_UHD", label: "4K UHD (3840×2160)" },
{ value: "8K", label: "8K (7680×4320)" },
{ value: "DCP", label: "DCP (Cinema)" },
{ value: "Custom", label: "Custom / TBD" },
];
const frameRateOptions = [
{ value: "24", label: "24 fps (Film)" },
{ value: "25", label: "25 fps (PAL)" },
{ value: "30", label: "30 fps (NTSC)" },
{ value: "60", label: "60 fps (HFR)" },
];
const colorSpaceOptions = [
{ value: "sRGB_Rec709", label: "sRGB / Rec.709" },
{ value: "DCI-P3", label: "DCI-P3 (Cinema)" },
{ value: "Rec2020_HDR", label: "Rec.2020 / HDR" },
];
const classificationOptions = [
{ value: "Confidential", label: "Confidential" },
{ value: "Not Confidential", label: "Not Confidential" },
];
const resourceBlueprint = await prisma.blueprint.create({
data: {
name: "Studio Resource Blueprint",
target: BlueprintTarget.RESOURCE,
description: "Standard fields for 3D studio resources",
fieldDefs: [
// Group: Basic Info
{ id: "fd-client-unit", label: "Client Unit / Account", key: "clientUnit", type: FieldType.TEXT, required: false, order: 0, group: "Basic Info" },
{ id: "fd-work-type", label: "Type of Work / Specialization", key: "workType", type: FieldType.TEXT, required: false, order: 1, group: "Basic Info" },
{ id: "fd-city", label: "Office Location (Metro City)", key: "city", type: FieldType.TEXT, required: false, order: 2, group: "Basic Info" },
{ id: "fd-employee-type", label: "Employee Type", key: "employeeType", type: FieldType.SELECT, required: false, order: 3, group: "Basic Info",
options: [
{ value: "Employee", label: "Employee" },
{ value: "Freelancer", label: "Freelancer" },
{ value: "Intern", label: "Intern" },
{ value: "External", label: "External Contractor" },
],
},
// Group: Work Setup
{ id: "fd-remote-eligible", label: "Eligible for Remote Work", key: "remoteEligible", type: FieldType.BOOLEAN, required: false, order: 4, group: "Work Setup" },
{ id: "fd-timezone", label: "Timezone", key: "timezone", type: FieldType.SELECT, required: false, order: 5, group: "Work Setup",
options: [
{ value: "UTC-5", label: "UTC-5 (EST)" },
{ value: "UTC-4", label: "UTC-4 (EDT)" },
{ value: "UTC+0", label: "UTC+0 (GMT/WET)" },
{ value: "UTC+1", label: "UTC+1 (CET)" },
{ value: "UTC+2", label: "UTC+2 (EET/CEST)" },
{ value: "UTC+3", label: "UTC+3 (MSK)" },
{ value: "UTC+5.5", label: "UTC+5:30 (IST)" },
{ value: "UTC+8", label: "UTC+8 (CST/HKT)" },
{ value: "UTC+9", label: "UTC+9 (JST)" },
],
},
{ id: "fd-primary-software", label: "Primary Software", key: "primarySoftware", type: FieldType.MULTI_SELECT, required: false, order: 6, group: "Work Setup",
options: [
{ value: "Maya", label: "Maya" },
{ value: "Cinema4D", label: "Cinema 4D" },
{ value: "Houdini", label: "Houdini" },
{ value: "Blender", label: "Blender" },
{ value: "UnrealEngine", label: "Unreal Engine" },
{ value: "AfterEffects", label: "After Effects" },
{ value: "Nuke", label: "Nuke" },
{ value: "Photoshop", label: "Photoshop" },
{ value: "SubstancePainter", label: "Substance Painter" },
{ value: "ZBrush", label: "ZBrush" },
{ value: "PremierePro", label: "Premiere Pro" },
{ value: "DaVinciResolve", label: "DaVinci Resolve" },
],
},
// Group: Background
{ id: "fd-years-experience", label: "Years of Industry Experience", key: "yearsOfExperience", type: FieldType.NUMBER, required: false, order: 7, group: "Background", description: "Total years working in the industry" },
{ id: "fd-languages", label: "Spoken Languages", key: "spokenLanguages", type: FieldType.TEXT, required: false, order: 8, group: "Background", placeholder: "e.g. English, German, French" },
{ id: "fd-portfolio-notes", label: "Portfolio / Work Notes", key: "portfolioNotes", type: FieldType.TEXTAREA, required: false, order: 9, group: "Background", description: "Links or notes about portfolio / notable projects" },
{ id: "fd-internal-notes", label: "Internal Notes", key: "internalNotes", type: FieldType.TEXTAREA, required: false, order: 10, group: "Background", description: "HR / team notes (not visible to the resource)" },
],
defaults: {},
validationRules: [],
},
});
const projectBlueprint = await prisma.blueprint.create({
data: {
name: "Studio Project Blueprint",
target: BlueprintTarget.PROJECT,
description: "Standard fields for client and internal projects",
fieldDefs: [
// Group: Client & Billing
{ id: "fd-proj-client-unit", label: "Client Unit Tag", key: "clientUnit", type: FieldType.TEXT, required: false, order: 0, group: "Client & Billing", placeholder: "e.g. [DAI], [BMW]" },
{ id: "fd-person-hours-sold", label: "Person Hours Sold", key: "personHoursSold", type: FieldType.NUMBER, required: false, order: 1, group: "Client & Billing", description: "Planned billable person hours agreed with client" },
{ id: "fd-classification", label: "Classification", key: "classification", type: FieldType.SELECT, required: false, order: 2, group: "Client & Billing", options: classificationOptions },
{ id: "fd-client-contact", label: "Client Contact / Account Mgr", key: "clientContact", type: FieldType.TEXT, required: false, order: 3, group: "Client & Billing" },
{ id: "fd-crm-reference", label: "CRM Reference / Opportunity", key: "crmReference", type: FieldType.TEXT, required: false, order: 4, group: "Client & Billing", description: "CRM opportunity ID or ticket reference" },
// Group: Delivery
{ id: "fd-delivery-deadline", label: "Final Delivery Date", key: "deliveryDeadline", type: FieldType.DATE, required: false, order: 5, group: "Delivery" },
{ id: "fd-delivery-format", label: "Delivery Format", key: "deliveryFormat", type: FieldType.SELECT, required: false, order: 6, group: "Delivery", options: deliveryFormatOptions },
{ id: "fd-frame-rate", label: "Frame Rate", key: "frameRate", type: FieldType.SELECT, required: false, order: 7, group: "Delivery", options: frameRateOptions },
{ id: "fd-color-space", label: "Color Space", key: "colorSpace", type: FieldType.SELECT, required: false, order: 8, group: "Delivery", options: colorSpaceOptions },
// Group: Scope
{ id: "fd-approval-rounds", label: "Client Approval Rounds", key: "clientApprovalRounds", type: FieldType.NUMBER, required: false, order: 9, group: "Scope", description: "Estimated number of client review cycles" },
{ id: "fd-revision-budget", label: "Revision Budget (hours)", key: "revisionBudgetHours", type: FieldType.NUMBER, required: false, order: 10, group: "Scope", description: "Person-hours reserved for revisions and corrections" },
{ id: "fd-notes", label: "Project Notes", key: "notes", type: FieldType.TEXTAREA, required: false, order: 11, group: "Scope", description: "Technical or creative notes / special requirements" },
],
defaults: { classification: "Not Confidential" },
validationRules: [],
},
});
const blueprint3D = await prisma.blueprint.create({
data: {
name: "3D Content Production",
target: BlueprintTarget.PROJECT,
description: "Blueprint for 3D rendering and stills projects",
fieldDefs: [
// Group: Client & Billing
{ id: "fd-3d-client-unit", label: "Client Unit Tag", key: "clientUnit", type: FieldType.TEXT, required: false, order: 0, group: "Client & Billing", placeholder: "e.g. [DAI], [BMW]" },
{ id: "fd-3d-hours-sold", label: "Person Hours Sold", key: "personHoursSold", type: FieldType.NUMBER, required: false, order: 1, group: "Client & Billing", description: "Planned billable person hours agreed with client" },
{ id: "fd-3d-classification", label: "Classification", key: "classification", type: FieldType.SELECT, required: false, order: 2, group: "Client & Billing", options: classificationOptions },
{ id: "fd-3d-client-contact", label: "Client Contact / Account Mgr", key: "clientContact", type: FieldType.TEXT, required: false, order: 3, group: "Client & Billing" },
{ id: "fd-3d-crm", label: "CRM Reference / Opportunity", key: "crmReference", type: FieldType.TEXT, required: false, order: 4, group: "Client & Billing", description: "CRM opportunity ID or ticket reference" },
// Group: Technical Specs
{ id: "fd-3d-render-engine", label: "Render Engine", key: "renderEngine", type: FieldType.SELECT, required: false, order: 5, group: "Technical Specs",
options: [
{ value: "Arnold", label: "Arnold" },
{ value: "VRay", label: "V-Ray" },
{ value: "Redshift", label: "Redshift" },
{ value: "Cycles", label: "Cycles (Blender)" },
{ value: "Octane", label: "Octane" },
{ value: "Corona", label: "Corona" },
{ value: "KeyShot", label: "KeyShot" },
],
},
{ id: "fd-3d-render-farm", label: "Render Farm", key: "renderFarm", type: FieldType.SELECT, required: false, order: 6, group: "Technical Specs",
options: [
{ value: "InhouseCPU", label: "In-house CPU" },
{ value: "InhouseGPU", label: "In-house GPU" },
{ value: "CloudAWS", label: "Cloud (AWS)" },
{ value: "CloudGCP", label: "Cloud (GCP)" },
{ value: "Hybrid", label: "Hybrid" },
],
},
{ id: "fd-3d-delivery-format", label: "Delivery Format", key: "deliveryFormat", type: FieldType.SELECT, required: false, order: 7, group: "Technical Specs", options: deliveryFormatOptions },
{ id: "fd-3d-frame-rate", label: "Frame Rate", key: "frameRate", type: FieldType.SELECT, required: false, order: 8, group: "Technical Specs", options: frameRateOptions },
{ id: "fd-3d-color-space", label: "Color Space", key: "colorSpace", type: FieldType.SELECT, required: false, order: 9, group: "Technical Specs", options: colorSpaceOptions },
{ id: "fd-3d-texture-res", label: "Texture Resolution", key: "textureResolution", type: FieldType.SELECT, required: false, order: 10, group: "Technical Specs",
options: [
{ value: "2K", label: "2K Textures" },
{ value: "4K", label: "4K Textures" },
{ value: "8K", label: "8K Textures" },
],
},
// Group: Asset Scope
{ id: "fd-3d-num-assets", label: "Number of Assets", key: "numberOfAssets", type: FieldType.NUMBER, required: false, order: 11, group: "Asset Scope", description: "Total 3D models / assets to produce" },
{ id: "fd-3d-asset-complexity", label: "Asset Complexity", key: "assetComplexity", type: FieldType.SELECT, required: false, order: 12, group: "Asset Scope",
options: [
{ value: "Low", label: "Low" },
{ value: "Medium", label: "Medium" },
{ value: "High", label: "High" },
{ value: "VeryHigh", label: "Very High" },
],
},
{ id: "fd-3d-lighting-setups", label: "Lighting Setups", key: "numberOfLightingSetups", type: FieldType.NUMBER, required: false, order: 13, group: "Asset Scope", description: "Number of distinct lighting scenarios" },
{ id: "fd-3d-render-hours", label: "Render Hours / Frame", key: "estimatedRenderHoursPerFrame", type: FieldType.NUMBER, required: false, order: 14, group: "Asset Scope" },
{ id: "fd-3d-iterations", label: "Iterations per Asset", key: "numberOfIterations", type: FieldType.NUMBER, required: false, order: 15, group: "Asset Scope" },
{ id: "fd-3d-post-processing", label: "Post-Processing Required", key: "postProcessingRequired", type: FieldType.BOOLEAN, required: false, order: 16, group: "Asset Scope", description: "Compositing / retouching needed" },
// Group: Scope
{ id: "fd-3d-scope-approval-rounds", label: "Client Approval Rounds", key: "clientApprovalRounds", type: FieldType.NUMBER, required: false, order: 17, group: "Scope", description: "Estimated number of client review cycles" },
{ id: "fd-3d-scope-revision-budget", label: "Revision Budget (hours)", key: "revisionBudgetHours", type: FieldType.NUMBER, required: false, order: 18, group: "Scope", description: "Person-hours reserved for revisions and corrections" },
{ id: "fd-3d-scope-notes", label: "Project Notes", key: "notes", type: FieldType.TEXTAREA, required: false, order: 19, group: "Scope", description: "Technical or creative notes / special requirements" },
],
defaults: { classification: "Not Confidential" },
validationRules: [],
rolePresets: [
{ id: "rp-3d-pm", role: "Project Manager", requiredSkills: ["Project Manager"], hoursPerDay: 4, headcount: 1 },
{ id: "rp-3d-lead", role: "3D Lead", requiredSkills: ["3D Modeling", "3D Lighting"], hoursPerDay: 8, headcount: 1 },
{ id: "rp-3d-art", role: "3D Artist", requiredSkills: ["3D Modeling"], hoursPerDay: 8, headcount: 2 },
{ id: "rp-3d-comp", role: "Compositor", requiredSkills: ["Compositing"], hoursPerDay: 8, headcount: 1 },
{ id: "rp-3d-ad", role: "Art Director", requiredSkills: ["Art Direction"], hoursPerDay: 4, headcount: 1 },
{ id: "rp-3d-td", role: "Technical Director", requiredSkills: ["Pipeline", "3D Modeling"], hoursPerDay: 6, headcount: 1 },
],
},
});
const blueprintAnimation = await prisma.blueprint.create({
data: {
name: "Animation Production",
target: BlueprintTarget.PROJECT,
description: "Blueprint for animated films and motion projects",
fieldDefs: [
// Group: Client & Billing
{ id: "fd-anim-client-unit", label: "Client Unit Tag", key: "clientUnit", type: FieldType.TEXT, required: false, order: 0, group: "Client & Billing", placeholder: "e.g. [DAI], [BMW]" },
{ id: "fd-anim-hours-sold", label: "Person Hours Sold", key: "personHoursSold", type: FieldType.NUMBER, required: false, order: 1, group: "Client & Billing", description: "Planned billable person hours agreed with client" },
{ id: "fd-anim-classification", label: "Classification", key: "classification", type: FieldType.SELECT, required: false, order: 2, group: "Client & Billing", options: classificationOptions },
{ id: "fd-anim-client-contact", label: "Client Contact / Account Mgr", key: "clientContact", type: FieldType.TEXT, required: false, order: 3, group: "Client & Billing" },
{ id: "fd-anim-crm", label: "CRM Reference / Opportunity", key: "crmReference", type: FieldType.TEXT, required: false, order: 4, group: "Client & Billing", description: "CRM opportunity ID or ticket reference" },
// Group: Animation Specs
{ id: "fd-anim-style", label: "Animation Style", key: "animationStyle", type: FieldType.SELECT, required: false, order: 5, group: "Animation Specs",
options: [
{ value: "Realistic", label: "Realistic / Photoreal" },
{ value: "Stylized", label: "Stylized" },
{ value: "MotionCapture", label: "Motion Capture" },
{ value: "Procedural", label: "Procedural" },
{ value: "CelShaded", label: "Cel-Shaded / Toon" },
{ value: "Mixed", label: "Mixed / Hybrid" },
],
},
{ id: "fd-anim-duration", label: "Duration (seconds)", key: "durationSeconds", type: FieldType.NUMBER, required: false, order: 6, group: "Animation Specs" },
{ id: "fd-anim-scenes", label: "Number of Scenes", key: "sceneCount", type: FieldType.NUMBER, required: false, order: 7, group: "Animation Specs" },
{ id: "fd-anim-shots", label: "Number of Shots", key: "shotCount", type: FieldType.NUMBER, required: false, order: 8, group: "Animation Specs" },
{ id: "fd-anim-delivery", label: "Delivery Format", key: "deliveryFormat", type: FieldType.SELECT, required: false, order: 9, group: "Animation Specs", options: deliveryFormatOptions },
{ id: "fd-anim-fps", label: "Frame Rate", key: "frameRate", type: FieldType.SELECT, required: false, order: 10, group: "Animation Specs", options: frameRateOptions },
// Group: Characters & Rigging
{ id: "fd-anim-chars", label: "Number of Characters", key: "characterCount", type: FieldType.NUMBER, required: false, order: 11, group: "Characters & Rigging" },
{ id: "fd-anim-rig-complexity", label: "Rig Complexity", key: "rigComplexity", type: FieldType.SELECT, required: false, order: 12, group: "Characters & Rigging",
options: [
{ value: "Simple", label: "Simple (basic bones)" },
{ value: "Standard", label: "Standard (FK/IK)" },
{ value: "Complex", label: "Complex (full-body IK)" },
{ value: "Hero", label: "Hero (simulation + secondary motion)" },
],
},
{ id: "fd-anim-mocap", label: "Motion Capture Required", key: "mocapRequired", type: FieldType.BOOLEAN, required: false, order: 13, group: "Characters & Rigging" },
{ id: "fd-anim-storyboard", label: "Storyboard Provided", key: "storyboardProvided", type: FieldType.BOOLEAN, required: false, order: 14, group: "Characters & Rigging", description: "Client provides storyboard" },
// Group: Post & Audio
{ id: "fd-anim-music", label: "Music / Sound Design", key: "musicPostRequired", type: FieldType.BOOLEAN, required: false, order: 15, group: "Post & Audio" },
{ id: "fd-anim-colgrade", label: "Color Grading", key: "colorGradingRequired", type: FieldType.BOOLEAN, required: false, order: 16, group: "Post & Audio" },
// Group: Scope
{ id: "fd-anim-scope-approval-rounds", label: "Client Approval Rounds", key: "clientApprovalRounds", type: FieldType.NUMBER, required: false, order: 17, group: "Scope", description: "Estimated number of client review cycles" },
{ id: "fd-anim-scope-revision-budget", label: "Revision Budget (hours)", key: "revisionBudgetHours", type: FieldType.NUMBER, required: false, order: 18, group: "Scope", description: "Person-hours reserved for revisions and corrections" },
{ id: "fd-anim-scope-notes", label: "Project Notes", key: "notes", type: FieldType.TEXTAREA, required: false, order: 19, group: "Scope", description: "Technical or creative notes / special requirements" },
],
defaults: { classification: "Not Confidential" },
validationRules: [],
rolePresets: [
{ id: "rp-anim-pm", role: "Project Manager", requiredSkills: ["Project Manager"], hoursPerDay: 4, headcount: 1 },
{ id: "rp-anim-dir", role: "Animation Director", requiredSkills: ["Animation", "Unreal Engine"], hoursPerDay: 8, headcount: 1 },
{ id: "rp-anim-anim", role: "Animator", requiredSkills: ["Animation"], hoursPerDay: 8, headcount: 2 },
{ id: "rp-anim-rig", role: "Rigger / TD", requiredSkills: ["Rigging"], hoursPerDay: 8, headcount: 1 },
{ id: "rp-anim-comp", role: "Compositor", requiredSkills: ["Compositing"], hoursPerDay: 8, headcount: 1 },
{ id: "rp-anim-ad", role: "Art Director", requiredSkills: ["Art Direction"], hoursPerDay: 4, headcount: 1 },
],
},
});
const blueprintVFX = await prisma.blueprint.create({
data: {
name: "VFX / Compositing",
target: BlueprintTarget.PROJECT,
description: "Blueprint for VFX and compositing projects",
fieldDefs: [
// Group: Client & Billing
{ id: "fd-vfx-client-unit", label: "Client Unit Tag", key: "clientUnit", type: FieldType.TEXT, required: false, order: 0, group: "Client & Billing", placeholder: "e.g. [DAI], [BMW]" },
{ id: "fd-vfx-hours-sold", label: "Person Hours Sold", key: "personHoursSold", type: FieldType.NUMBER, required: false, order: 1, group: "Client & Billing", description: "Planned billable person hours agreed with client" },
{ id: "fd-vfx-classification", label: "Classification", key: "classification", type: FieldType.SELECT, required: false, order: 2, group: "Client & Billing", options: classificationOptions },
{ id: "fd-vfx-client-contact", label: "Client Contact / Account Mgr", key: "clientContact", type: FieldType.TEXT, required: false, order: 3, group: "Client & Billing" },
{ id: "fd-vfx-crm", label: "CRM Reference / Opportunity", key: "crmReference", type: FieldType.TEXT, required: false, order: 4, group: "Client & Billing", description: "CRM opportunity ID or ticket reference" },
// Group: VFX Specs
{ id: "fd-vfx-type", label: "VFX Type(s)", key: "vfxType", type: FieldType.MULTI_SELECT, required: false, order: 5, group: "VFX Specs",
options: [
{ value: "GreenScreen", label: "Green Screen / Chroma Key" },
{ value: "CGIIntegration", label: "CGI Integration" },
{ value: "MotionTracking", label: "Motion Tracking" },
{ value: "Rotoscoping", label: "Rotoscoping" },
{ value: "ParticleFX", label: "Particle FX" },
{ value: "FluidSim", label: "Fluid Simulation" },
{ value: "MattePainting", label: "Matte Painting" },
{ value: "TitleSequence", label: "Title Sequence" },
],
},
{ id: "fd-vfx-shot-count", label: "Number of VFX Shots", key: "shotCount", type: FieldType.NUMBER, required: false, order: 6, group: "VFX Specs" },
{ id: "fd-vfx-complexity", label: "Avg. Shot Complexity", key: "avgShotComplexity", type: FieldType.SELECT, required: false, order: 7, group: "VFX Specs",
options: [
{ value: "Low", label: "Low (cleanup / grade)" },
{ value: "Medium", label: "Medium (integration)" },
{ value: "High", label: "High (simulation)" },
{ value: "Photoreal", label: "Photoreal" },
],
},
{ id: "fd-vfx-delivery", label: "Delivery Format", key: "deliveryFormat", type: FieldType.SELECT, required: false, order: 8, group: "VFX Specs", options: deliveryFormatOptions },
{ id: "fd-vfx-fps", label: "Frame Rate", key: "frameRate", type: FieldType.SELECT, required: false, order: 9, group: "VFX Specs", options: frameRateOptions },
{ id: "fd-vfx-color-space", label: "Color Space", key: "colorSpace", type: FieldType.SELECT, required: false, order: 10, group: "VFX Specs", options: colorSpaceOptions },
// Group: Source Material
{ id: "fd-vfx-footage", label: "Footage Provided by Client", key: "footageProvided", type: FieldType.BOOLEAN, required: false, order: 11, group: "Source Material" },
{ id: "fd-vfx-audio-sync", label: "Audio Sync Required", key: "audioSyncRequired", type: FieldType.BOOLEAN, required: false, order: 12, group: "Source Material" },
{ id: "fd-vfx-onset-sup", label: "VFX Supervisor On Set", key: "hasOnSetVFXSup", type: FieldType.BOOLEAN, required: false, order: 13, group: "Source Material" },
// Group: Scope
{ id: "fd-vfx-scope-approval-rounds", label: "Client Approval Rounds", key: "clientApprovalRounds", type: FieldType.NUMBER, required: false, order: 14, group: "Scope", description: "Estimated number of client review cycles" },
{ id: "fd-vfx-scope-revision-budget", label: "Revision Budget (hours)", key: "revisionBudgetHours", type: FieldType.NUMBER, required: false, order: 15, group: "Scope", description: "Person-hours reserved for revisions and corrections" },
{ id: "fd-vfx-scope-notes", label: "Project Notes", key: "notes", type: FieldType.TEXTAREA, required: false, order: 16, group: "Scope", description: "Technical or creative notes / special requirements" },
],
defaults: { classification: "Not Confidential" },
validationRules: [],
rolePresets: [
{ id: "rp-vfx-producer", role: "VFX Producer", requiredSkills: ["Project Manager"], hoursPerDay: 4, headcount: 1 },
{ id: "rp-vfx-sup", role: "VFX Supervisor", requiredSkills: ["Compositing", "Art Direction"], hoursPerDay: 6, headcount: 1 },
{ id: "rp-vfx-sr-comp", role: "Senior Compositor", requiredSkills: ["Compositing", "Nuke"], hoursPerDay: 8, headcount: 2 },
{ id: "rp-vfx-comp", role: "Compositor", requiredSkills: ["Compositing"], hoursPerDay: 8, headcount: 1 },
{ id: "rp-vfx-roto", role: "Roto / Paint Artist", requiredSkills: ["Rotoscoping"], hoursPerDay: 8, headcount: 1 },
{ id: "rp-vfx-tracker", role: "Motion Tracker", requiredSkills: ["Motion Tracking"], hoursPerDay: 8, headcount: 1 },
],
},
});
const blueprintMotion = await prisma.blueprint.create({
data: {
name: "Motion Design",
target: BlueprintTarget.PROJECT,
description: "Blueprint for motion graphics and animation",
fieldDefs: [
// Group: Client & Billing
{ id: "fd-mog-client-unit", label: "Client Unit Tag", key: "clientUnit", type: FieldType.TEXT, required: false, order: 0, group: "Client & Billing", placeholder: "e.g. [DAI], [BMW]" },
{ id: "fd-mog-hours-sold", label: "Person Hours Sold", key: "personHoursSold", type: FieldType.NUMBER, required: false, order: 1, group: "Client & Billing", description: "Planned billable person hours agreed with client" },
{ id: "fd-mog-classification", label: "Classification", key: "classification", type: FieldType.SELECT, required: false, order: 2, group: "Client & Billing", options: classificationOptions },
{ id: "fd-mog-client-contact", label: "Client Contact / Account Mgr", key: "clientContact", type: FieldType.TEXT, required: false, order: 3, group: "Client & Billing" },
{ id: "fd-mog-crm", label: "CRM Reference / Opportunity", key: "crmReference", type: FieldType.TEXT, required: false, order: 4, group: "Client & Billing", description: "CRM opportunity ID or ticket reference" },
// Group: Motion Specs
{ id: "fd-mog-style", label: "Motion Style", key: "motionStyle", type: FieldType.SELECT, required: false, order: 5, group: "Motion Specs",
options: [
{ value: "Flat2D", label: "2D Flat / Icon Animation" },
{ value: "KineticType", label: "Kinetic Typography" },
{ value: "TwoHalfD", label: "2.5D" },
{ value: "ThreeD", label: "3D Motion" },
{ value: "Mixed", label: "Mixed / Hybrid" },
],
},
{ id: "fd-mog-duration", label: "Duration (seconds)", key: "durationSeconds", type: FieldType.NUMBER, required: false, order: 6, group: "Motion Specs" },
{ id: "fd-mog-formats", label: "Delivery Formats", key: "deliveryFormats", type: FieldType.MULTI_SELECT, required: false, order: 7, group: "Motion Specs",
options: [
{ value: "Social_916", label: "Social (9:16 vertical)" },
{ value: "Landscape_169", label: "Landscape (16:9)" },
{ value: "Square_11", label: "Square (1:1)" },
{ value: "Banner", label: "Banner / Display Ads" },
{ value: "Custom", label: "Custom" },
],
},
{ id: "fd-mog-variants", label: "Number of Variants", key: "numberOfVariants", type: FieldType.NUMBER, required: false, order: 8, group: "Motion Specs", description: "Format / language / size variants" },
// Group: Assets & Design
{ id: "fd-mog-design-system", label: "Client Design System Provided", key: "hasDesignSystem", type: FieldType.BOOLEAN, required: false, order: 9, group: "Assets & Design", description: "Brand guidelines, fonts, and color palette provided" },
{ id: "fd-mog-storyboard", label: "Storyboard / Animatic Required",key: "storyboardRequired", type: FieldType.BOOLEAN, required: false, order: 10, group: "Assets & Design" },
{ id: "fd-mog-assets", label: "Design Assets Provided", key: "assetsProvided", type: FieldType.BOOLEAN, required: false, order: 11, group: "Assets & Design", description: "Logos, images, and fonts provided by client" },
// Group: Post & Audio
{ id: "fd-mog-voiceover", label: "Voice-Over Required", key: "voiceoverRequired", type: FieldType.BOOLEAN, required: false, order: 12, group: "Post & Audio" },
{ id: "fd-mog-music", label: "Music Licensing Required", key: "musicLicensingRequired", type: FieldType.BOOLEAN, required: false, order: 13, group: "Post & Audio" },
{ id: "fd-mog-sound", label: "Custom Sound Design", key: "soundDesignRequired", type: FieldType.BOOLEAN, required: false, order: 14, group: "Post & Audio" },
// Group: Scope
{ id: "fd-mog-scope-approval-rounds", label: "Client Approval Rounds", key: "clientApprovalRounds", type: FieldType.NUMBER, required: false, order: 15, group: "Scope", description: "Estimated number of client review cycles" },
{ id: "fd-mog-scope-revision-budget", label: "Revision Budget (hours)", key: "revisionBudgetHours", type: FieldType.NUMBER, required: false, order: 16, group: "Scope", description: "Person-hours reserved for revisions and corrections" },
{ id: "fd-mog-scope-notes", label: "Project Notes", key: "notes", type: FieldType.TEXTAREA, required: false, order: 17, group: "Scope", description: "Technical or creative notes / special requirements" },
],
defaults: { classification: "Not Confidential" },
validationRules: [],
rolePresets: [
{ id: "rp-mog-designer", role: "Motion Designer", requiredSkills: ["After Effects", "Motion Design"], hoursPerDay: 8, headcount: 2 },
{ id: "rp-mog-sr-designer", role: "Senior Motion Designer", requiredSkills: ["After Effects", "Motion Design", "Art Direction"], hoursPerDay: 8, headcount: 1 },
{ id: "rp-mog-ad", role: "Art Director", requiredSkills: ["Art Direction"], hoursPerDay: 4, headcount: 1 },
{ id: "rp-mog-producer", role: "Producer", requiredSkills: ["Project Manager"], hoursPerDay: 4, headcount: 1 },
{ id: "rp-mog-sound", role: "Sound Designer", requiredSkills: ["Sound Design"], hoursPerDay: 4, headcount: 1 },
],
},
});
console.warn(`Blueprints: resource=${resourceBlueprint.id}, project=${projectBlueprint.id}, 3D=${blueprint3D.id}, animation=${blueprintAnimation.id}, vfx=${blueprintVFX.id}, motion=${blueprintMotion.id}`);
// ── 4. Create roles ────────────────────────────────────────────────────────
const ROLE_SEED = [
{ name: "Project Manager", color: "#6366f1", description: "Project lead and client liaison" },
{ name: "3D Lead", color: "#8b5cf6", description: "Senior 3D artist and technical lead" },
{ name: "3D Artist", color: "#a78bfa", description: "3D modeling, texturing, lighting" },
{ name: "Art Director", color: "#ec4899", description: "Visual direction and brand alignment" },
{ name: "Unreal Dev", color: "#14b8a6", description: "Real-time engine development (UE5)" },
{ name: "Frontend Dev", color: "#22d3ee", description: "Web & tool frontend development" },
{ name: "PDM Specialist", color: "#f59e0b", description: "Product data management and compositing" },
{ name: "3D Generalist", color: "#84cc16", description: "General 3D production support" },
{ name: "3D Tech Tester", color: "#f97316", description: "Pipeline and render QA testing" },
];
const roleMap = new Map<string, string>(); // name → id
for (const r of ROLE_SEED) {
const role = await prisma.role.create({
data: { name: r.name, color: r.color, description: r.description, isActive: true },
});
roleMap.set(r.name, role.id);
}
console.warn(`Roles: ${roleMap.size} created`);
// ── 5. Create resources ────────────────────────────────────────────────────
const resourceMap = new Map<string, Resource>();
const countryIdByCode = new Map<string, string>([
["DE", countryDE.id],
["ES", countryES.id],
["IN", countryIN.id],
["US", countryUS.id],
]);
for (const [index, row] of RESOURCE_DATA.entries()) {
const [eid, chapter, typeOfWork, clientUnit, , employeeType, lcr, ucr, fraction, availDays, chargeability] = row;
const holidayProfile = getHolidayDemoProfileForIndex(index);
const displayName = eid
.split(".")
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join(" ");
const email = `${eid}@capakraken.example`;
const lcrCents = Math.round(lcr * 100);
const ucrCents = Math.round(ucr * 100);
const availability = computeAvailability(fraction, availDays);
const skills = computeSkills(chapter, typeOfWork, lcr);
// Dispo v2: resolve FKs
const resCountryId = countryIdByCode.get(holidayProfile.countryCode) ?? countryDE.id;
const resMetroCityId = cityMap.get(holidayProfile.cityName) ?? null;
// chapter → orgUnit mapping
const chapterToOrgUnit: Record<string, string> = {
"Project Management": "Project Management",
"Digital Content Production": "3D Visualization",
"Art Direction": "Art Direction",
"Product Data Management": "Product Data Management",
"CGI-Dev": "CGI Development",
};
const resOrgUnitId = orgUnitMap.get(chapterToOrgUnit[chapter] ?? "") ?? null;
// lcr → management level mapping
let resMgmtGroupName: string;
let resMgmtLevelName: string;
if (lcr >= 130) {
resMgmtGroupName = "Consultant"; resMgmtLevelName = "CL6-Consultant";
} else if (lcr >= 115) {
resMgmtGroupName = "Manager"; resMgmtLevelName = "CL7-Manager";
} else if (lcr >= 90) {
resMgmtGroupName = "Consultant"; resMgmtLevelName = "CL5-Senior Analyst";
} else if (lcr >= 75) {
resMgmtGroupName = "Analyst"; resMgmtLevelName = "CL4-Analyst";
} else {
resMgmtGroupName = "Associate"; resMgmtLevelName = "CL3-Associate";
}
const resMgmtGroupId = mgmtGroupMap.get(resMgmtGroupName) ?? null;
const resMgmtLevelId = mgmtLevelMap.get(resMgmtLevelName) ?? null;
// resourceType
const resResourceType = employeeType === "Freelancer" ? ResourceType.FREELANCER : ResourceType.EMPLOYEE;
// clientUnit → client mapping
const clientUnitToClient: Record<string, string> = {
"Porsche AG": "Porsche AG",
"BMW": "BMW Group",
"Daimler": "Daimler / Mercedes",
"Jaguar Land Rover": "Jaguar Land Rover",
"Moving Image": "Moving Image",
"Cross-Unit": "Cross-Unit",
"Italy (FER/MAS)": "Porsche AG", // mapped to Porsche AG (Porsche/Ferrari/Maserati group)
};
const resClientUnitId = clientMap.get(clientUnitToClient[clientUnit] ?? "") ?? null;
const resource = await prisma.resource.create({
data: {
eid,
displayName,
email,
chapter,
lcrCents,
ucrCents,
currency: "EUR",
chargeabilityTarget: chargeability * 100,
availability: availability as unknown as Prisma.InputJsonValue,
skills: skills as unknown as Prisma.InputJsonValue,
dynamicFields: {
clientUnit,
workType: typeOfWork,
city: holidayProfile.cityName,
employeeType,
holidayCountryCode: holidayProfile.countryCode,
holidayStateCode: holidayProfile.stateCode,
},
blueprintId: resourceBlueprint.id,
countryId: resCountryId,
federalState: holidayProfile.stateCode,
...(resMetroCityId ? { metroCityId: resMetroCityId } : {}),
...(resOrgUnitId ? { orgUnitId: resOrgUnitId } : {}),
...(resMgmtGroupId ? { managementLevelGroupId: resMgmtGroupId } : {}),
...(resMgmtLevelId ? { managementLevelId: resMgmtLevelId } : {}),
resourceType: resResourceType,
fte: fraction,
chgResponsibility: true,
enterpriseId: eid,
...(resClientUnitId ? { clientUnitId: resClientUnitId } : {}),
},
});
resourceMap.set(eid, resource);
}
console.warn(`Resources: ${resourceMap.size} created`);
// ── 5. Create projects ─────────────────────────────────────────────────────
const projectMap = new Map<string, Project>();
for (const row of PROJECT_DATA) {
const [shortCode, name, clientTag, , orderTypeStr, winProb, allocTypeStr, budgetCents] = row;
const dates = PROJECT_DATES[shortCode];
if (!dates) {
console.warn(`WARNING: no dates for project ${shortCode}, skipping`);
continue;
}
const [startDateStr, endDateStr, status] = dates;
// Dispo v2: utilizationCategory mapping
const orderTypeToUtilCat: Record<string, string> = {
"CHARGEABLE": "Chg",
"BD": "BD",
"INTERNAL": "M&O",
"OVERHEAD": "M&O",
};
const projUtilCatId = utilCatMap.get(orderTypeToUtilCat[orderTypeStr] ?? "") ?? null;
// Dispo v2: client mapping
const clientTagToClient: Record<string, string> = {
"PAG": "Porsche AG",
"BMW": "BMW Group",
"DAI": "Daimler / Mercedes",
"JLR": "Jaguar Land Rover",
"INT": "Internal Projects",
};
const projClientId = clientMap.get(clientTagToClient[clientTag] ?? "") ?? null;
const project = await prisma.project.create({
data: {
shortCode,
name,
orderType: parseOrderType(orderTypeStr),
allocationType: parseAllocationType(allocTypeStr),
winProbability: winProb,
budgetCents,
startDate: new Date(startDateStr),
endDate: new Date(endDateStr),
status,
staffingReqs: [],
dynamicFields: { clientUnit: clientTag },
blueprintId: projectBlueprint.id,
...(projUtilCatId ? { utilizationCategoryId: projUtilCatId } : {}),
...(projClientId ? { clientId: projClientId } : {}),
},
});
projectMap.set(shortCode, project);
}
console.warn(`Projects: ${projectMap.size} created`);
// ── 6. Create demand requirements + assignments ───────────────────────────
// Seed planning data for active and draft projects
// Helper: compute dailyCostCents from resource LCR and hoursPerDay
function seedDailyCost(lcrCents: number, hoursPerDay: number): number {
return Math.round(lcrCents * hoursPerDay);
}
// Demand requirements (open demand / placeholder roles)
interface DemandSeed {
projectCode: string;
roleName: string;
start: string;
end: string;
hoursPerDay: number;
percentage: number;
headcount: number;
status: AllocationStatus;
}
const DEMAND_SEEDS: DemandSeed[] = [
// PAG25G: Porsche Taycan Sport Film — needs 2 more 3D artists
{ projectCode: "PAG25G", roleName: "3D Artist", start: "2026-03-16", end: "2026-03-31", hoursPerDay: 8, percentage: 100, headcount: 2, status: AllocationStatus.PROPOSED },
// JLR26A: Jaguar EV Launch Film — open Art Director + PDM slot
{ projectCode: "JLR26A", roleName: "Art Director", start: "2026-02-02", end: "2026-05-29", hoursPerDay: 8, percentage: 100, headcount: 1, status: AllocationStatus.CONFIRMED },
{ projectCode: "JLR26A", roleName: "PDM Specialist", start: "2026-03-02", end: "2026-06-30", hoursPerDay: 4, percentage: 50, headcount: 1, status: AllocationStatus.PROPOSED },
// DAI26B: AMG EV Reveal — needs Unreal Dev
{ projectCode: "DAI26B", roleName: "Unreal Dev", start: "2026-02-16", end: "2026-04-17", hoursPerDay: 8, percentage: 100, headcount: 1, status: AllocationStatus.PROPOSED },
// BMW26B: Draft — early demand planning
{ projectCode: "BMW26B", roleName: "3D Lead", start: "2026-05-04", end: "2026-06-26", hoursPerDay: 8, percentage: 100, headcount: 1, status: AllocationStatus.PROPOSED },
{ projectCode: "BMW26B", roleName: "3D Artist", start: "2026-05-04", end: "2026-06-26", hoursPerDay: 8, percentage: 100, headcount: 3, status: AllocationStatus.PROPOSED },
{ projectCode: "BMW26B", roleName: "Art Director", start: "2026-05-04", end: "2026-06-26", hoursPerDay: 4, percentage: 50, headcount: 1, status: AllocationStatus.PROPOSED },
// JLR26B: Draft film — early demand
{ projectCode: "JLR26B", roleName: "Project Manager",start: "2026-06-01", end: "2026-11-30", hoursPerDay: 8, percentage: 100, headcount: 1, status: AllocationStatus.PROPOSED },
{ projectCode: "JLR26B", roleName: "3D Artist", start: "2026-06-01", end: "2026-11-30", hoursPerDay: 8, percentage: 100, headcount: 4, status: AllocationStatus.PROPOSED },
// DEV25B: Pipeline rebuild — open frontend dev slot
{ projectCode: "DEV25B", roleName: "Frontend Dev", start: "2026-01-05", end: "2026-04-30", hoursPerDay: 8, percentage: 100, headcount: 1, status: AllocationStatus.PROPOSED },
];
let demandCount = 0;
for (const d of DEMAND_SEEDS) {
const project = projectMap.get(d.projectCode);
const roleId = roleMap.get(d.roleName);
if (!project) { console.warn(`WARN: demand skip — project ${d.projectCode} not found`); continue; }
await prisma.demandRequirement.create({
data: {
projectId: project.id,
startDate: new Date(d.start),
endDate: new Date(d.end),
hoursPerDay: d.hoursPerDay,
percentage: d.percentage,
role: d.roleName,
...(roleId ? { roleId } : {}),
headcount: d.headcount,
status: d.status,
metadata: {},
},
});
demandCount++;
}
console.warn(`Demand requirements: ${demandCount} created`);
// Assignments (resource-backed allocations)
interface AssignmentSeed {
eid: string;
projectCode: string;
roleName: string;
start: string;
end: string;
hoursPerDay: number;
percentage: number;
status: AllocationStatus;
}
const ASSIGNMENT_SEEDS: AssignmentSeed[] = [
// PAG25G: Porsche Taycan Sport Film
{ eid: "steve.rogers", projectCode: "PAG25G", roleName: "Project Manager", start: "2025-10-01", end: "2026-03-31", hoursPerDay: 8, percentage: 100, status: AllocationStatus.ACTIVE },
{ eid: "tony.stark", projectCode: "PAG25G", roleName: "3D Artist", start: "2025-10-13", end: "2026-03-31", hoursPerDay: 8, percentage: 100, status: AllocationStatus.ACTIVE },
{ eid: "sam.wilson", projectCode: "PAG25G", roleName: "3D Artist", start: "2025-11-03", end: "2026-03-31", hoursPerDay: 8, percentage: 100, status: AllocationStatus.ACTIVE },
{ eid: "vision.vision", projectCode: "PAG25G", roleName: "Art Director", start: "2025-10-01", end: "2026-03-31", hoursPerDay: 4, percentage: 50, status: AllocationStatus.ACTIVE },
{ eid: "hank.mccoy", projectCode: "PAG25G", roleName: "PDM Specialist", start: "2026-01-05", end: "2026-03-31", hoursPerDay: 8, percentage: 100, status: AllocationStatus.CONFIRMED },
// JLR26A: Jaguar EV Launch Film
{ eid: "natasha.romanoff",projectCode: "JLR26A", roleName: "Project Manager", start: "2026-01-05", end: "2026-07-31", hoursPerDay: 8, percentage: 100, status: AllocationStatus.ACTIVE },
{ eid: "james.barnes", projectCode: "JLR26A", roleName: "3D Artist", start: "2026-01-05", end: "2026-07-31", hoursPerDay: 8, percentage: 100, status: AllocationStatus.ACTIVE },
{ eid: "peter.parker", projectCode: "JLR26A", roleName: "3D Artist", start: "2026-01-19", end: "2026-07-31", hoursPerDay: 8, percentage: 100, status: AllocationStatus.ACTIVE },
{ eid: "miles.morales", projectCode: "JLR26A", roleName: "3D Artist", start: "2026-02-02", end: "2026-07-31", hoursPerDay: 8, percentage: 100, status: AllocationStatus.CONFIRMED },
{ eid: "groot.groot", projectCode: "JLR26A", roleName: "Art Director", start: "2026-01-05", end: "2026-07-31", hoursPerDay: 8, percentage: 100, status: AllocationStatus.ACTIVE },
{ eid: "clint.barton", projectCode: "JLR26A", roleName: "PDM Specialist", start: "2026-02-16", end: "2026-06-30", hoursPerDay: 8, percentage: 100, status: AllocationStatus.CONFIRMED },
// DEV25B: Asset Library & Pipeline Rebuild
{ eid: "matt.murdock", projectCode: "DEV25B", roleName: "Unreal Dev", start: "2025-11-03", end: "2026-04-30", hoursPerDay: 8, percentage: 100, status: AllocationStatus.ACTIVE },
{ eid: "jessica.jones", projectCode: "DEV25B", roleName: "Unreal Dev", start: "2026-01-05", end: "2026-04-30", hoursPerDay: 8, percentage: 100, status: AllocationStatus.CONFIRMED },
{ eid: "frank.castle", projectCode: "DEV25B", roleName: "Frontend Dev", start: "2025-11-03", end: "2026-04-30", hoursPerDay: 4, percentage: 50, status: AllocationStatus.ACTIVE },
// DAI26B: AMG EV Reveal Campaign Stills
{ eid: "bruce.banner", projectCode: "DAI26B", roleName: "Project Manager", start: "2026-02-02", end: "2026-04-17", hoursPerDay: 4, percentage: 50, status: AllocationStatus.ACTIVE },
{ eid: "thor.odinson", projectCode: "DAI26B", roleName: "3D Artist", start: "2026-02-02", end: "2026-04-17", hoursPerDay: 8, percentage: 100, status: AllocationStatus.ACTIVE },
{ eid: "erik.lehnsherr", projectCode: "DAI26B", roleName: "3D Artist", start: "2026-02-16", end: "2026-04-17", hoursPerDay: 8, percentage: 100, status: AllocationStatus.CONFIRMED },
{ eid: "drax.drax", projectCode: "DAI26B", roleName: "Art Director", start: "2026-02-02", end: "2026-04-17", hoursPerDay: 4, percentage: 50, status: AllocationStatus.ACTIVE },
{ eid: "anna.marie", projectCode: "DAI26B", roleName: "PDM Specialist", start: "2026-03-02", end: "2026-04-17", hoursPerDay: 8, percentage: 100, status: AllocationStatus.CONFIRMED },
// PAG26C: Porsche Carrera Grand Viz
{ eid: "charles.xavier", projectCode: "PAG26C", roleName: "Project Manager", start: "2026-02-16", end: "2026-04-24", hoursPerDay: 8, percentage: 100, status: AllocationStatus.ACTIVE },
{ eid: "jean.grey", projectCode: "PAG26C", roleName: "3D Artist", start: "2026-02-16", end: "2026-04-24", hoursPerDay: 8, percentage: 100, status: AllocationStatus.ACTIVE },
{ eid: "stephen.strange", projectCode: "PAG26C", roleName: "3D Artist", start: "2026-02-16", end: "2026-04-24", hoursPerDay: 8, percentage: 100, status: AllocationStatus.CONFIRMED },
{ eid: "jessica.drew", projectCode: "PAG26C", roleName: "3D Artist", start: "2026-03-02", end: "2026-04-24", hoursPerDay: 8, percentage: 100, status: AllocationStatus.CONFIRMED },
{ eid: "tchalla.tchalla", projectCode: "PAG26C", roleName: "Art Director", start: "2026-02-16", end: "2026-04-24", hoursPerDay: 8, percentage: 100, status: AllocationStatus.ACTIVE },
{ eid: "peter.quill", projectCode: "PAG26C", roleName: "PDM Specialist", start: "2026-03-02", end: "2026-04-24", hoursPerDay: 4, percentage: 50, status: AllocationStatus.PROPOSED },
// Cross-project partial allocations
{ eid: "wanda.maximoff", projectCode: "DAI26B", roleName: "3D Artist", start: "2026-03-03", end: "2026-04-17", hoursPerDay: 6, percentage: 75, status: AllocationStatus.PROPOSED },
{ eid: "logan.howlett", projectCode: "PAG26C", roleName: "3D Artist", start: "2026-03-16", end: "2026-04-24", hoursPerDay: 8, percentage: 100, status: AllocationStatus.PROPOSED },
{ eid: "wong.wong", projectCode: "PAG25G", roleName: "3D Artist", start: "2026-01-05", end: "2026-03-31", hoursPerDay: 4, percentage: 50, status: AllocationStatus.CONFIRMED },
{ eid: "danny.rand", projectCode: "JLR26A", roleName: "Unreal Dev", start: "2026-03-02", end: "2026-07-31", hoursPerDay: 8, percentage: 100, status: AllocationStatus.PROPOSED },
{ eid: "scott.lang", projectCode: "PAG25G", roleName: "3D Generalist", start: "2026-01-19", end: "2026-03-31", hoursPerDay: 4, percentage: 50, status: AllocationStatus.CONFIRMED },
// Draft project pre-allocations
{ eid: "carol.danvers", projectCode: "DEV26A", roleName: "Frontend Dev", start: "2026-05-04", end: "2026-08-28", hoursPerDay: 8, percentage: 100, status: AllocationStatus.PROPOSED },
{ eid: "kamala.khan", projectCode: "DEV26A", roleName: "Frontend Dev", start: "2026-05-04", end: "2026-08-28", hoursPerDay: 8, percentage: 100, status: AllocationStatus.PROPOSED },
];
let assignmentCount = 0;
for (const a of ASSIGNMENT_SEEDS) {
const resource = resourceMap.get(a.eid);
const project = projectMap.get(a.projectCode);
const roleId = roleMap.get(a.roleName);
if (!resource || !project) {
console.warn(`WARN: assignment skip — ${a.eid}/${a.projectCode} not found`);
continue;
}
const dailyCost = seedDailyCost(resource.lcrCents, a.hoursPerDay);
await prisma.assignment.create({
data: {
resourceId: resource.id,
projectId: project.id,
startDate: new Date(a.start),
endDate: new Date(a.end),
hoursPerDay: a.hoursPerDay,
percentage: a.percentage,
role: a.roleName,
...(roleId ? { roleId } : {}),
dailyCostCents: dailyCost,
status: a.status,
metadata: {},
},
});
assignmentCount++;
}
console.warn(`Assignments: ${assignmentCount} created`);
// ── Vacations ──────────────────────────────────────────────────────────────
const vacationEntries: Array<{
eid: string;
type: VacationType;
status: VacationStatus;
start: string;
end: string;
note?: string;
}> = [
{ eid: "steve.rogers", type: VacationType.ANNUAL, status: VacationStatus.APPROVED, start: "2026-04-07", end: "2026-04-11", note: "Easter break" },
{ eid: "tony.stark", type: VacationType.ANNUAL, status: VacationStatus.APPROVED, start: "2026-05-25", end: "2026-06-06", note: "Summer vacation" },
{ eid: "natasha.romanoff", type: VacationType.SICK, status: VacationStatus.APPROVED, start: "2026-03-10", end: "2026-03-12" },
{ eid: "bruce.banner", type: VacationType.ANNUAL, status: VacationStatus.PENDING, start: "2026-06-15", end: "2026-06-26", note: "Conference + holiday" },
{ eid: "thor.odinson", type: VacationType.PUBLIC_HOLIDAY, status: VacationStatus.APPROVED, start: "2026-05-01", end: "2026-05-01", note: "Labour Day" },
{ eid: "sam.wilson", type: VacationType.ANNUAL, status: VacationStatus.APPROVED, start: "2026-07-20", end: "2026-08-07", note: "Summer vacation" },
{ eid: "peter.parker", type: VacationType.SICK, status: VacationStatus.APPROVED, start: "2026-03-18", end: "2026-03-19" },
{ eid: "wanda.maximoff", type: VacationType.ANNUAL, status: VacationStatus.APPROVED, start: "2026-04-14", end: "2026-04-17", note: "Long weekend" },
{ eid: "scott.lang", type: VacationType.ANNUAL, status: VacationStatus.PENDING, start: "2026-08-17", end: "2026-08-28", note: "Family trip" },
{ eid: "james.barnes", type: VacationType.OTHER, status: VacationStatus.APPROVED, start: "2026-03-26", end: "2026-03-27", note: "Personal day" },
];
let vacationCount = 0;
for (const v of vacationEntries) {
const res = resourceMap.get(v.eid);
if (!res) continue;
const isApproved = v.status === VacationStatus.APPROVED;
await prisma.vacation.create({
data: {
resourceId: res.id,
type: v.type,
status: v.status,
startDate: new Date(v.start),
endDate: new Date(v.end),
...(v.note ? { note: v.note } : {}),
requestedById: manager.id,
...(isApproved ? { approvedById: manager.id, approvedAt: new Date() } : {}),
},
});
vacationCount++;
}
console.warn(`Vacations: ${vacationCount} created`);
// ── Calculation Rules (default set) ──────────────────────────────────────────
await prisma.calculationRule.createMany({
data: [
{
name: "Urlaub — Person chargeable, Projekt nicht belastet",
description: "Vacation days count toward chargeability but are not charged to the project.",
triggerType: "VACATION",
costEffect: "ZERO",
chargeabilityEffect: "COUNT",
priority: 0,
isActive: true,
},
{
name: "Krankheit — Person chargeable, Projekt nicht belastet",
description: "Sick days count toward chargeability but are not charged to the project.",
triggerType: "SICK",
costEffect: "ZERO",
chargeabilityEffect: "COUNT",
priority: 0,
isActive: true,
},
{
name: "Feiertag — kein Effekt",
description: "Public holidays are neither chargeable nor charged to projects.",
triggerType: "PUBLIC_HOLIDAY",
costEffect: "ZERO",
chargeabilityEffect: "SKIP",
priority: 0,
isActive: true,
},
],
});
console.warn("Calculation rules: 3 default rules created");
console.warn("Seed complete!");
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(() => {
void prisma.$disconnect();
});