19aeb2ba04
CI / Lint (push) Successful in 3m4s
CI / Typecheck (push) Successful in 3m6s
CI / Architecture Guardrails (push) Successful in 3m8s
CI / Assistant Split Regression (push) Successful in 3m48s
CI / Build (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Fresh-Linux Docker Deploy (push) Has been cancelled
CI / Release Images (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
rename(phase 3): compose/DB/infra + stray code refs capakraken → nexus (#62) Co-authored-by: Hartmut Nörenberg <hn@hartmut-noerenberg.com> Co-committed-by: Hartmut Nörenberg <hn@hartmut-noerenberg.com>
3219 lines
93 KiB
TypeScript
3219 lines
93 KiB
TypeScript
import {
|
||
AllocationStatus,
|
||
AllocationType,
|
||
BlueprintTarget,
|
||
FieldType,
|
||
OrderType,
|
||
ProjectStatus,
|
||
ResourceType,
|
||
SystemRole,
|
||
VacationStatus,
|
||
VacationType,
|
||
} from "@nexus/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.0,
|
||
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.0,
|
||
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.0,
|
||
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 Nexus 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@nexus.dev" },
|
||
update: { passwordHash: adminHash },
|
||
create: {
|
||
email: "admin@nexus.dev",
|
||
name: "Admin User",
|
||
passwordHash: adminHash,
|
||
systemRole: SystemRole.ADMIN,
|
||
},
|
||
});
|
||
|
||
const manager = await prisma.user.upsert({
|
||
where: { email: "manager@nexus.dev" },
|
||
update: { passwordHash: managerHash },
|
||
create: {
|
||
email: "manager@nexus.dev",
|
||
name: "Manager User",
|
||
passwordHash: managerHash,
|
||
systemRole: SystemRole.MANAGER,
|
||
},
|
||
});
|
||
|
||
const viewer = await prisma.user.upsert({
|
||
where: { email: "viewer@nexus.dev" },
|
||
update: { passwordHash: viewerHash },
|
||
create: {
|
||
email: "viewer@nexus.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}@nexus.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();
|
||
});
|