1286 lines
84 KiB
TypeScript
1286 lines
84 KiB
TypeScript
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";
|
||
|
||
loadWorkspaceEnv();
|
||
|
||
const prisma = new PrismaClient();
|
||
|
||
// ─── 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}`);
|
||
|
||
// ── 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();
|
||
});
|