rename(phase 1): CapaKraken → Nexus across code, UI, docs, CI (#61)
CI / Architecture Guardrails (push) Successful in 2m38s
CI / Assistant Split Regression (push) Successful in 3m33s
CI / Typecheck (push) Successful in 3m51s
CI / Lint (push) Successful in 5m2s
CI / E2E Tests (push) Has been cancelled
CI / Fresh-Linux Docker Deploy (push) Has been cancelled
CI / Release Images (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Architecture Guardrails (push) Successful in 2m38s
CI / Assistant Split Regression (push) Successful in 3m33s
CI / Typecheck (push) Successful in 3m51s
CI / Lint (push) Successful in 5m2s
CI / E2E Tests (push) Has been cancelled
CI / Fresh-Linux Docker Deploy (push) Has been cancelled
CI / Release Images (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
rename(phase 1): CapaKraken → Nexus across code, UI, docs, CI (#61) Co-authored-by: Hartmut Nörenberg <hn@hartmut-noerenberg.com> Co-committed-by: Hartmut Nörenberg <hn@hartmut-noerenberg.com>
This commit was merged in pull request #61.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { assertDestructiveDbAllowed } from "./destructive-db-guard.js";
|
||||
import { assertCapaKrakenDbTarget, assertSafeSeedTarget } from "./safe-destructive-env.js";
|
||||
import { assertNexusDbTarget, assertSafeSeedTarget } from "./safe-destructive-env.js";
|
||||
|
||||
const ORIGINAL_ENV = { ...process.env };
|
||||
|
||||
@@ -46,10 +46,11 @@ test("assertDestructiveDbAllowed rejects protected live database names even if a
|
||||
});
|
||||
|
||||
assert.throws(
|
||||
() => assertDestructiveDbAllowed({
|
||||
commandName: "db:test",
|
||||
allowedDatabaseNames: ["capakraken"],
|
||||
}),
|
||||
() =>
|
||||
assertDestructiveDbAllowed({
|
||||
commandName: "db:test",
|
||||
allowedDatabaseNames: ["capakraken"],
|
||||
}),
|
||||
/explicitly protected/u,
|
||||
);
|
||||
});
|
||||
@@ -62,10 +63,11 @@ test("assertDestructiveDbAllowed rejects missing confirmation", () => {
|
||||
});
|
||||
|
||||
assert.throws(
|
||||
() => assertDestructiveDbAllowed({
|
||||
commandName: "db:test",
|
||||
allowedDatabaseNames: ["capakraken_e2e"],
|
||||
}),
|
||||
() =>
|
||||
assertDestructiveDbAllowed({
|
||||
commandName: "db:test",
|
||||
allowedDatabaseNames: ["capakraken_e2e"],
|
||||
}),
|
||||
/CONFIRM_DESTRUCTIVE_DB_NAME=capakraken_e2e/u,
|
||||
);
|
||||
});
|
||||
@@ -78,10 +80,11 @@ test("assertDestructiveDbAllowed rejects missing destructive allow flag", () =>
|
||||
});
|
||||
|
||||
assert.throws(
|
||||
() => assertDestructiveDbAllowed({
|
||||
commandName: "db:test",
|
||||
allowedDatabaseNames: ["capakraken_ci"],
|
||||
}),
|
||||
() =>
|
||||
assertDestructiveDbAllowed({
|
||||
commandName: "db:test",
|
||||
allowedDatabaseNames: ["capakraken_ci"],
|
||||
}),
|
||||
/ALLOW_DESTRUCTIVE_DB_TOOLS=true/u,
|
||||
);
|
||||
});
|
||||
@@ -93,40 +96,34 @@ test("assertSafeSeedTarget rejects unexpected legacy disposable databases", () =
|
||||
CONFIRM_DESTRUCTIVE_DB_NAME: "legacy_test",
|
||||
});
|
||||
|
||||
assert.throws(
|
||||
() => assertSafeSeedTarget("db:seed"),
|
||||
/not in the destructive-tool allowlist/u,
|
||||
);
|
||||
assert.throws(() => assertSafeSeedTarget("db:seed"), /not in the destructive-tool allowlist/u);
|
||||
});
|
||||
|
||||
test("assertCapaKrakenDbTarget accepts non-destructive capakraken targets", () => {
|
||||
test("assertNexusDbTarget accepts non-destructive capakraken targets", () => {
|
||||
setEnv({
|
||||
DATABASE_URL: "postgresql://tester:secret@localhost:5432/capakraken_dev",
|
||||
});
|
||||
|
||||
const target = assertCapaKrakenDbTarget("db:seed:holidays");
|
||||
const target = assertNexusDbTarget("db:seed:holidays");
|
||||
|
||||
assert.equal(target.databaseName, "capakraken_dev");
|
||||
});
|
||||
|
||||
test("assertCapaKrakenDbTarget rejects legacy non-capakraken targets", () => {
|
||||
test("assertNexusDbTarget rejects legacy non-capakraken targets", () => {
|
||||
setEnv({
|
||||
DATABASE_URL: "postgresql://tester:secret@localhost:5432/legacy_non_capakraken",
|
||||
});
|
||||
|
||||
assert.throws(
|
||||
() => assertCapaKrakenDbTarget("db:seed:holidays"),
|
||||
/not a valid CapaKraken target/u,
|
||||
);
|
||||
assert.throws(() => assertNexusDbTarget("db:seed:holidays"), /not a valid Nexus target/u);
|
||||
});
|
||||
|
||||
test("assertCapaKrakenDbTarget explains missing env loading clearly", () => {
|
||||
test("assertNexusDbTarget explains missing env loading clearly", () => {
|
||||
setEnv({
|
||||
DATABASE_URL: undefined,
|
||||
});
|
||||
|
||||
assert.throws(
|
||||
() => assertCapaKrakenDbTarget("db:update:blueprints"),
|
||||
/Run the command through the CapaKraken env wrappers/u,
|
||||
() => assertNexusDbTarget("db:update:blueprints"),
|
||||
/Run the command through the Nexus env wrappers/u,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* Generate samples/CapaKrakenExamples.xlsx from the live database.
|
||||
* Generate samples/NexusExamples.xlsx from the live database.
|
||||
*
|
||||
* Run from repo root:
|
||||
* pnpm --filter @capakraken/db db:excel
|
||||
* pnpm --filter @nexus/db db:excel
|
||||
*/
|
||||
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
@@ -10,10 +10,10 @@ import ExcelJS from "exceljs";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { loadWorkspaceEnv } from "./load-workspace-env.js";
|
||||
import { assertCapaKrakenDbTarget } from "./safe-destructive-env.js";
|
||||
import { assertNexusDbTarget } from "./safe-destructive-env.js";
|
||||
|
||||
loadWorkspaceEnv();
|
||||
assertCapaKrakenDbTarget("db:excel");
|
||||
assertNexusDbTarget("db:excel");
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
@@ -21,20 +21,20 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
// ─── Colour palette ──────────────────────────────────────────────────────────
|
||||
|
||||
const COLORS = {
|
||||
headerBg: "FF1E3A5F", // dark navy
|
||||
headerFg: "FFFFFFFF",
|
||||
altRow: "FFF5F7FA",
|
||||
border: "FFCDD5E0",
|
||||
headerBg: "FF1E3A5F", // dark navy
|
||||
headerFg: "FFFFFFFF",
|
||||
altRow: "FFF5F7FA",
|
||||
border: "FFCDD5E0",
|
||||
// status
|
||||
completed: "FFD1FAE5",
|
||||
active: "FFDBEAFE",
|
||||
onHold: "FFFEF3C7",
|
||||
draft: "FFF3F4F6",
|
||||
cancelled: "FFFEE2E2",
|
||||
completed: "FFD1FAE5",
|
||||
active: "FFDBEAFE",
|
||||
onHold: "FFFEF3C7",
|
||||
draft: "FFF3F4F6",
|
||||
cancelled: "FFFEE2E2",
|
||||
// allocation status
|
||||
allocActive: "FFDBEAFE",
|
||||
allocActive: "FFDBEAFE",
|
||||
allocConfirmed: "FFD1FAE5",
|
||||
allocProposed: "FFFEF3C7",
|
||||
allocProposed: "FFFEF3C7",
|
||||
allocCompleted: "FFF3F4F6",
|
||||
};
|
||||
|
||||
@@ -55,8 +55,8 @@ function applyHeader(ws: ExcelJS.Worksheet, columns: ExcelJS.Column[]) {
|
||||
ws.columns = columns;
|
||||
const headerRow: AnyRow = ws.getRow(1);
|
||||
headerRow.eachCell((cell) => {
|
||||
cell.font = { bold: true, color: { argb: COLORS.headerFg }, size: 10 };
|
||||
cell.fill = { type: "pattern", pattern: "solid", fgColor: { argb: COLORS.headerBg } };
|
||||
cell.font = { bold: true, color: { argb: COLORS.headerFg }, size: 10 };
|
||||
cell.fill = { type: "pattern", pattern: "solid", fgColor: { argb: COLORS.headerBg } };
|
||||
cell.border = {
|
||||
bottom: { style: "thin", color: { argb: COLORS.border } },
|
||||
};
|
||||
@@ -73,9 +73,9 @@ function styleDataRows(ws: ExcelJS.Worksheet, startRow: number, totalRows: numbe
|
||||
if (isAlt) {
|
||||
cell.fill = { type: "pattern", pattern: "solid", fgColor: { argb: COLORS.altRow } };
|
||||
}
|
||||
cell.font = { size: 10 };
|
||||
cell.font = { size: 10 };
|
||||
cell.alignment = { vertical: "middle" };
|
||||
cell.border = {
|
||||
cell.border = {
|
||||
bottom: { style: "hair", color: { argb: COLORS.border } },
|
||||
};
|
||||
});
|
||||
@@ -89,22 +89,33 @@ function highlightCell(cell: ExcelJS.Cell, argb: string) {
|
||||
|
||||
function statusColor(status: string): string {
|
||||
switch (status) {
|
||||
case "COMPLETED": return COLORS.completed;
|
||||
case "ACTIVE": return COLORS.active;
|
||||
case "ON_HOLD": return COLORS.onHold;
|
||||
case "DRAFT": return COLORS.draft;
|
||||
case "CANCELLED": return COLORS.cancelled;
|
||||
default: return COLORS.draft;
|
||||
case "COMPLETED":
|
||||
return COLORS.completed;
|
||||
case "ACTIVE":
|
||||
return COLORS.active;
|
||||
case "ON_HOLD":
|
||||
return COLORS.onHold;
|
||||
case "DRAFT":
|
||||
return COLORS.draft;
|
||||
case "CANCELLED":
|
||||
return COLORS.cancelled;
|
||||
default:
|
||||
return COLORS.draft;
|
||||
}
|
||||
}
|
||||
|
||||
function allocStatusColor(status: string): string {
|
||||
switch (status) {
|
||||
case "ACTIVE": return COLORS.allocActive;
|
||||
case "CONFIRMED": return COLORS.allocConfirmed;
|
||||
case "PROPOSED": return COLORS.allocProposed;
|
||||
case "COMPLETED": return COLORS.allocCompleted;
|
||||
default: return COLORS.draft;
|
||||
case "ACTIVE":
|
||||
return COLORS.allocActive;
|
||||
case "CONFIRMED":
|
||||
return COLORS.allocConfirmed;
|
||||
case "PROPOSED":
|
||||
return COLORS.allocProposed;
|
||||
case "COMPLETED":
|
||||
return COLORS.allocCompleted;
|
||||
default:
|
||||
return COLORS.draft;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,29 +129,37 @@ async function buildResourcesSheet(wb: ExcelJS.Workbook) {
|
||||
});
|
||||
|
||||
applyHeader(ws, [
|
||||
{ header: "EID", key: "eid", width: 22 },
|
||||
{ header: "Display Name", key: "displayName", width: 24 },
|
||||
{ header: "Chapter", key: "chapter", width: 30 },
|
||||
{ header: "Type of Work", key: "typeOfWork", width: 30 },
|
||||
{ header: "City", key: "city", width: 16 },
|
||||
{ header: "Employee Type", key: "empType", width: 16 },
|
||||
{ header: "LCR (€/h)", key: "lcr", width: 12 },
|
||||
{ header: "UCR (€/h)", key: "ucr", width: 12 },
|
||||
{ header: "Chargeability %", key: "chargeability",width: 16 },
|
||||
{ header: "Mon h", key: "mon", width: 8 },
|
||||
{ header: "Tue h", key: "tue", width: 8 },
|
||||
{ header: "Wed h", key: "wed", width: 8 },
|
||||
{ header: "Thu h", key: "thu", width: 8 },
|
||||
{ header: "Fri h", key: "fri", width: 8 },
|
||||
{ header: "Skills", key: "skills", width: 40 },
|
||||
{ header: "EID", key: "eid", width: 22 },
|
||||
{ header: "Display Name", key: "displayName", width: 24 },
|
||||
{ header: "Chapter", key: "chapter", width: 30 },
|
||||
{ header: "Type of Work", key: "typeOfWork", width: 30 },
|
||||
{ header: "City", key: "city", width: 16 },
|
||||
{ header: "Employee Type", key: "empType", width: 16 },
|
||||
{ header: "LCR (€/h)", key: "lcr", width: 12 },
|
||||
{ header: "UCR (€/h)", key: "ucr", width: 12 },
|
||||
{ header: "Chargeability %", key: "chargeability", width: 16 },
|
||||
{ header: "Mon h", key: "mon", width: 8 },
|
||||
{ header: "Tue h", key: "tue", width: 8 },
|
||||
{ header: "Wed h", key: "wed", width: 8 },
|
||||
{ header: "Thu h", key: "thu", width: 8 },
|
||||
{ header: "Fri h", key: "fri", width: 8 },
|
||||
{ header: "Skills", key: "skills", width: 40 },
|
||||
] as ExcelJS.Column[]);
|
||||
|
||||
let rowIdx = 2;
|
||||
for (const res of resources) {
|
||||
const avail = res.availability as {
|
||||
monday: number; tuesday: number; wednesday: number; thursday: number; friday: number;
|
||||
monday: number;
|
||||
tuesday: number;
|
||||
wednesday: number;
|
||||
thursday: number;
|
||||
friday: number;
|
||||
};
|
||||
const dynFields = res.dynamicFields as {
|
||||
workType?: string;
|
||||
city?: string;
|
||||
employeeType?: string;
|
||||
};
|
||||
const dynFields = res.dynamicFields as { workType?: string; city?: string; employeeType?: string };
|
||||
const skills = (res.skills as Array<{ skill: string; proficiency: number }> | null) ?? [];
|
||||
const skillStr = skills.map((s) => `${s.skill} (${s.proficiency})`).join(", ");
|
||||
|
||||
@@ -177,17 +196,17 @@ async function buildProjectsSheet(wb: ExcelJS.Workbook) {
|
||||
});
|
||||
|
||||
applyHeader(ws, [
|
||||
{ header: "Short Code", key: "code", width: 14 },
|
||||
{ header: "Name", key: "name", width: 46 },
|
||||
{ header: "Client", key: "client", width: 14 },
|
||||
{ header: "Order Type", key: "orderType", width: 14 },
|
||||
{ header: "Alloc Type", key: "allocType", width: 12 },
|
||||
{ header: "Status", key: "status", width: 14 },
|
||||
{ header: "Start Date", key: "startDate", width: 14 },
|
||||
{ header: "End Date", key: "endDate", width: 14 },
|
||||
{ header: "Win Prob %", key: "winProb", width: 12 },
|
||||
{ header: "Budget (€)", key: "budget", width: 16 },
|
||||
{ header: "Responsible", key: "responsible", width: 24 },
|
||||
{ header: "Short Code", key: "code", width: 14 },
|
||||
{ header: "Name", key: "name", width: 46 },
|
||||
{ header: "Client", key: "client", width: 14 },
|
||||
{ header: "Order Type", key: "orderType", width: 14 },
|
||||
{ header: "Alloc Type", key: "allocType", width: 12 },
|
||||
{ header: "Status", key: "status", width: 14 },
|
||||
{ header: "Start Date", key: "startDate", width: 14 },
|
||||
{ header: "End Date", key: "endDate", width: 14 },
|
||||
{ header: "Win Prob %", key: "winProb", width: 12 },
|
||||
{ header: "Budget (€)", key: "budget", width: 16 },
|
||||
{ header: "Responsible", key: "responsible", width: 24 },
|
||||
] as ExcelJS.Column[]);
|
||||
|
||||
let rowIdx = 2;
|
||||
@@ -283,17 +302,17 @@ async function buildAllocationsSheet(wb: ExcelJS.Workbook) {
|
||||
});
|
||||
|
||||
applyHeader(ws, [
|
||||
{ header: "Project Code", key: "projCode", width: 14 },
|
||||
{ header: "Project Name", key: "projName", width: 40 },
|
||||
{ header: "EID", key: "eid", width: 20 },
|
||||
{ header: "Resource Name", key: "resource", width: 24 },
|
||||
{ header: "Chapter", key: "chapter", width: 28 },
|
||||
{ header: "Role", key: "role", width: 20 },
|
||||
{ header: "Start Date", key: "startDate", width: 14 },
|
||||
{ header: "End Date", key: "endDate", width: 14 },
|
||||
{ header: "h/Day", key: "hours", width: 8 },
|
||||
{ header: "Daily Cost (€)", key: "dailyCost", width: 16 },
|
||||
{ header: "Status", key: "status", width: 14 },
|
||||
{ header: "Project Code", key: "projCode", width: 14 },
|
||||
{ header: "Project Name", key: "projName", width: 40 },
|
||||
{ header: "EID", key: "eid", width: 20 },
|
||||
{ header: "Resource Name", key: "resource", width: 24 },
|
||||
{ header: "Chapter", key: "chapter", width: 28 },
|
||||
{ header: "Role", key: "role", width: 20 },
|
||||
{ header: "Start Date", key: "startDate", width: 14 },
|
||||
{ header: "End Date", key: "endDate", width: 14 },
|
||||
{ header: "h/Day", key: "hours", width: 8 },
|
||||
{ header: "Daily Cost (€)", key: "dailyCost", width: 16 },
|
||||
{ header: "Status", key: "status", width: 14 },
|
||||
] as ExcelJS.Column[]);
|
||||
|
||||
let rowIdx = 2;
|
||||
@@ -338,29 +357,29 @@ async function buildSummarySheet(wb: ExcelJS.Workbook) {
|
||||
] as ExcelJS.Column[];
|
||||
|
||||
const title = ws.getCell("A1");
|
||||
title.value = "CapaKraken — Seed Data Summary";
|
||||
title.font = { bold: true, size: 14, color: { argb: COLORS.headerBg } };
|
||||
title.value = "Nexus — Seed Data Summary";
|
||||
title.font = { bold: true, size: 14, color: { argb: COLORS.headerBg } };
|
||||
ws.mergeCells("A1:B1");
|
||||
ws.getRow(1).height = 28;
|
||||
|
||||
const generated = ws.getCell("A2");
|
||||
generated.value = `Generated: ${new Date().toISOString().split("T")[0]}`;
|
||||
generated.font = { italic: true, size: 10, color: { argb: "FF666666" } };
|
||||
generated.font = { italic: true, size: 10, color: { argb: "FF666666" } };
|
||||
ws.mergeCells("A2:B2");
|
||||
ws.getRow(2).height = 18;
|
||||
|
||||
const rows: [string, number | string][] = [
|
||||
["Resources", resourceCount],
|
||||
["Projects", projectCount],
|
||||
["Resources", resourceCount],
|
||||
["Projects", projectCount],
|
||||
["Allocations", allocationCount],
|
||||
];
|
||||
|
||||
rows.forEach(([label, value], i) => {
|
||||
const row = ws.getRow(4 + i);
|
||||
row.getCell(1).value = label;
|
||||
row.getCell(1).font = { bold: true, size: 10 };
|
||||
row.getCell(1).font = { bold: true, size: 10 };
|
||||
row.getCell(2).value = value;
|
||||
row.getCell(2).font = { size: 10 };
|
||||
row.getCell(2).font = { size: 10 };
|
||||
row.height = 20;
|
||||
});
|
||||
}
|
||||
@@ -371,8 +390,8 @@ async function main() {
|
||||
console.log("Connecting to database...");
|
||||
|
||||
const wb = new ExcelJS.Workbook();
|
||||
wb.creator = "CapaKraken";
|
||||
wb.created = new Date();
|
||||
wb.creator = "Nexus";
|
||||
wb.created = new Date();
|
||||
wb.modified = new Date();
|
||||
|
||||
console.log("Building sheets...");
|
||||
@@ -381,11 +400,14 @@ async function main() {
|
||||
await buildProjectsSheet(wb);
|
||||
await buildAllocationsSheet(wb);
|
||||
|
||||
const outPath = path.resolve(__dirname, "../../../samples/CapaKrakenExamples.xlsx");
|
||||
const outPath = path.resolve(__dirname, "../../../samples/NexusExamples.xlsx");
|
||||
await wb.xlsx.writeFile(outPath);
|
||||
console.log(`Excel written to: ${outPath}`);
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => { console.error(e); process.exit(1); })
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(() => prisma.$disconnect());
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getPublicHolidays, toIsoDate } from "@capakraken/shared";
|
||||
import { getPublicHolidays, toIsoDate } from "@nexus/shared";
|
||||
|
||||
export type HolidayCalendarSeedScope = "COUNTRY" | "STATE" | "CITY";
|
||||
|
||||
@@ -62,10 +62,15 @@ function computeEasterSunday(year: number): Date {
|
||||
return dateUtc(year, month, day);
|
||||
}
|
||||
|
||||
function nthWeekdayOfMonth(year: number, month: number, weekday: number, occurrence: number): string {
|
||||
function nthWeekdayOfMonth(
|
||||
year: number,
|
||||
month: number,
|
||||
weekday: number,
|
||||
occurrence: number,
|
||||
): string {
|
||||
const firstDay = dateUtc(year, month, 1);
|
||||
const delta = (weekday - firstDay.getUTCDay() + 7) % 7;
|
||||
const day = 1 + delta + ((occurrence - 1) * 7);
|
||||
const day = 1 + delta + (occurrence - 1) * 7;
|
||||
return toIsoDate(dateUtc(year, month, day));
|
||||
}
|
||||
|
||||
@@ -90,7 +95,10 @@ function observedUsFixedHoliday(year: number, month: number, day: number): strin
|
||||
return toIsoDate(holiday);
|
||||
}
|
||||
|
||||
function buildEntries(years: number[], definitions: SimpleHolidayDefinition[]): HolidayCalendarSeedEntry[] {
|
||||
function buildEntries(
|
||||
years: number[],
|
||||
definitions: SimpleHolidayDefinition[],
|
||||
): HolidayCalendarSeedEntry[] {
|
||||
const entries = new Map<string, HolidayCalendarSeedEntry>();
|
||||
|
||||
for (const year of years) {
|
||||
@@ -139,7 +147,10 @@ function buildSpanishCountryEntries(years: number[]): HolidayCalendarSeedEntry[]
|
||||
return buildEntries(years, [
|
||||
{ name: "Ano Nuevo", resolveDate: (year) => toIsoDate(dateUtc(year, 1, 1)) },
|
||||
{ name: "Epifania del Senor", resolveDate: (year) => toIsoDate(dateUtc(year, 1, 6)) },
|
||||
{ name: "Viernes Santo", resolveDate: (year) => toIsoDate(addDays(computeEasterSunday(year), -2)) },
|
||||
{
|
||||
name: "Viernes Santo",
|
||||
resolveDate: (year) => toIsoDate(addDays(computeEasterSunday(year), -2)),
|
||||
},
|
||||
{ name: "Fiesta del Trabajo", resolveDate: (year) => toIsoDate(dateUtc(year, 5, 1)) },
|
||||
{ name: "Asuncion de la Virgen", resolveDate: (year) => toIsoDate(dateUtc(year, 8, 15)) },
|
||||
{ name: "Fiesta Nacional de Espana", resolveDate: (year) => toIsoDate(dateUtc(year, 10, 12)) },
|
||||
@@ -153,7 +164,10 @@ function buildSpanishCountryEntries(years: number[]): HolidayCalendarSeedEntry[]
|
||||
function buildIndianCountryEntries(years: number[]): HolidayCalendarSeedEntry[] {
|
||||
return buildEntries(years, [
|
||||
{ name: "Republic Day", resolveDate: (year) => toIsoDate(dateUtc(year, 1, 26)) },
|
||||
{ name: "Good Friday", resolveDate: (year) => toIsoDate(addDays(computeEasterSunday(year), -2)) },
|
||||
{
|
||||
name: "Good Friday",
|
||||
resolveDate: (year) => toIsoDate(addDays(computeEasterSunday(year), -2)),
|
||||
},
|
||||
{ name: "Independence Day", resolveDate: (year) => toIsoDate(dateUtc(year, 8, 15)) },
|
||||
{ name: "Gandhi Jayanti", resolveDate: (year) => toIsoDate(dateUtc(year, 10, 2)) },
|
||||
]);
|
||||
@@ -175,7 +189,9 @@ function normalizeCountryCodes(countryCodes: string[]): Set<string> {
|
||||
return new Set(countryCodes.map((countryCode) => countryCode.trim().toUpperCase()));
|
||||
}
|
||||
|
||||
function normalizeCityLookup(availableCitiesByCountry: Record<string, string[]>): Map<string, Set<string>> {
|
||||
function normalizeCityLookup(
|
||||
availableCitiesByCountry: Record<string, string[]>,
|
||||
): Map<string, Set<string>> {
|
||||
const lookup = new Map<string, Set<string>>();
|
||||
|
||||
for (const [countryCode, cityNames] of Object.entries(availableCitiesByCountry)) {
|
||||
@@ -185,7 +201,11 @@ function normalizeCityLookup(availableCitiesByCountry: Record<string, string[]>)
|
||||
return lookup;
|
||||
}
|
||||
|
||||
function hasCity(cityLookup: Map<string, Set<string>>, countryCode: string, cityName: string): boolean {
|
||||
function hasCity(
|
||||
cityLookup: Map<string, Set<string>>,
|
||||
countryCode: string,
|
||||
cityName: string,
|
||||
): boolean {
|
||||
return cityLookup.get(countryCode)?.has(cityName) ?? false;
|
||||
}
|
||||
|
||||
@@ -204,7 +224,10 @@ function germanStateDisplayName(stateCode: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
function buildCityEntries(years: number[], definitions: SimpleHolidayDefinition[]): HolidayCalendarSeedEntry[] {
|
||||
function buildCityEntries(
|
||||
years: number[],
|
||||
definitions: SimpleHolidayDefinition[],
|
||||
): HolidayCalendarSeedEntry[] {
|
||||
return buildEntries(years, definitions);
|
||||
}
|
||||
|
||||
@@ -253,7 +276,10 @@ export function buildHolidayCalendarSeedDefinitions(
|
||||
cityName: "Augsburg",
|
||||
priority: CITY_PRIORITY,
|
||||
entries: buildCityEntries(context.years, [
|
||||
{ name: "Augsburger Friedensfest", resolveDate: (year) => toIsoDate(dateUtc(year, 8, 8)) },
|
||||
{
|
||||
name: "Augsburger Friedensfest",
|
||||
resolveDate: (year) => toIsoDate(dateUtc(year, 8, 8)),
|
||||
},
|
||||
]),
|
||||
});
|
||||
}
|
||||
@@ -277,7 +303,10 @@ export function buildHolidayCalendarSeedDefinitions(
|
||||
priority: CITY_PRIORITY,
|
||||
entries: buildEntries(context.years, [
|
||||
{ name: "San Isidro", resolveDate: (year) => toIsoDate(dateUtc(year, 5, 15)) },
|
||||
{ name: "Nuestra Senora de la Almudena", resolveDate: (year) => toIsoDate(dateUtc(year, 11, 9)) },
|
||||
{
|
||||
name: "Nuestra Senora de la Almudena",
|
||||
resolveDate: (year) => toIsoDate(dateUtc(year, 11, 9)),
|
||||
},
|
||||
]),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { resolve } from "node:path";
|
||||
import { PrismaClient, StagedRecordStatus } from "@prisma/client";
|
||||
import { loadWorkspaceEnv, resolveWorkspacePath } from "./load-workspace-env.js";
|
||||
import { assertCapaKrakenDbTarget } from "./safe-destructive-env.js";
|
||||
import { assertNexusDbTarget } from "./safe-destructive-env.js";
|
||||
|
||||
loadWorkspaceEnv();
|
||||
|
||||
@@ -13,21 +13,13 @@ const DEFAULT_REFERENCE_WORKBOOK = resolveWorkspacePath(
|
||||
"Dispov2",
|
||||
"MandatoryDispoCategories_V3.xlsx",
|
||||
);
|
||||
const DEFAULT_PLANNING_WORKBOOK = resolveWorkspacePath(
|
||||
"samples",
|
||||
"Dispov2",
|
||||
"DISPO_2026.xlsx",
|
||||
);
|
||||
const DEFAULT_PLANNING_WORKBOOK = resolveWorkspacePath("samples", "Dispov2", "DISPO_2026.xlsx");
|
||||
const DEFAULT_CHARGEABILITY_WORKBOOK = resolveWorkspacePath(
|
||||
"samples",
|
||||
"Dispov2",
|
||||
"20260309_Bi-Weekly_Chargeability_Reporting_Content_Production_V0.943_4Hartmut.xlsx",
|
||||
);
|
||||
const DEFAULT_ROSTER_WORKBOOK = resolveWorkspacePath(
|
||||
"samples",
|
||||
"Dispov2",
|
||||
"MV_DispoRoster.xlsx",
|
||||
);
|
||||
const DEFAULT_ROSTER_WORKBOOK = resolveWorkspacePath("samples", "Dispov2", "MV_DispoRoster.xlsx");
|
||||
const DEFAULT_COST_WORKBOOK = resolveWorkspacePath(
|
||||
"samples",
|
||||
"Dispov2",
|
||||
@@ -253,7 +245,7 @@ export function parseImportDispoBatchArgs(argv: string[]): ImportDispoBatchOptio
|
||||
|
||||
function buildHelpText() {
|
||||
return [
|
||||
"Usage: pnpm --filter @capakraken/db db:import:dispo [options]",
|
||||
"Usage: pnpm --filter @nexus/db db:import:dispo [options]",
|
||||
"",
|
||||
"Options:",
|
||||
" --reference-workbook <path> Override MandatoryDispoCategories workbook",
|
||||
@@ -330,11 +322,7 @@ async function loadUnresolvedPreview(
|
||||
importBatchId: batchId,
|
||||
status: StagedRecordStatus.UNRESOLVED,
|
||||
},
|
||||
orderBy: [
|
||||
{ recordType: "asc" },
|
||||
{ sourceSheet: "asc" },
|
||||
{ sourceRow: "asc" },
|
||||
],
|
||||
orderBy: [{ recordType: "asc" }, { sourceSheet: "asc" }, { sourceRow: "asc" }],
|
||||
take: limit,
|
||||
select: {
|
||||
message: true,
|
||||
@@ -365,21 +353,28 @@ function printUnresolvedPreview(records: ReadonlyArray<UnresolvedPreviewRecord>,
|
||||
}
|
||||
}
|
||||
|
||||
function ensureCommitAllowed(options: ImportDispoBatchOptions, readiness: DispoImportReadinessReport) {
|
||||
function ensureCommitAllowed(
|
||||
options: ImportDispoBatchOptions,
|
||||
readiness: DispoImportReadinessReport,
|
||||
) {
|
||||
if (options.strictSourceData) {
|
||||
if (!readiness.canCommitWithStrictSourceData) {
|
||||
throw new Error("Readiness is not strict-source-data clean. Re-run without --strict-source-data or fix blockers.");
|
||||
throw new Error(
|
||||
"Readiness is not strict-source-data clean. Re-run without --strict-source-data or fix blockers.",
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!readiness.canCommitWithFallbacks) {
|
||||
throw new Error("Readiness has unresolved blocker issues that are not covered by the agreed fallback rules.");
|
||||
throw new Error(
|
||||
"Readiness has unresolved blocker issues that are not covered by the agreed fallback rules.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function runImportDispoBatch(options: ImportDispoBatchOptions) {
|
||||
assertCapaKrakenDbTarget("db:import:dispo");
|
||||
assertNexusDbTarget("db:import:dispo");
|
||||
const dispoImport = await loadDispoImportModule();
|
||||
|
||||
printWorkbookSources(options);
|
||||
@@ -422,7 +417,7 @@ export async function runImportDispoBatch(options: ImportDispoBatchOptions) {
|
||||
ensureCommitAllowed(options, stageResult.readiness);
|
||||
|
||||
console.log("");
|
||||
console.log("Committing staged rows into live CapaKraken tables...");
|
||||
console.log("Committing staged rows into live Nexus tables...");
|
||||
|
||||
const commitResult = await dispoImport.commitDispoImportBatch(prisma, {
|
||||
allowTbdUnresolved: options.allowTbdUnresolved,
|
||||
|
||||
@@ -14,7 +14,7 @@ function clearEnv() {
|
||||
}
|
||||
|
||||
function withTempWorkspace(run: (workspaceRoot: string) => void) {
|
||||
const workspaceRoot = mkdtempSync(join(tmpdir(), "capakraken-env-"));
|
||||
const workspaceRoot = mkdtempSync(join(tmpdir(), "nexus-env-"));
|
||||
|
||||
try {
|
||||
run(workspaceRoot);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { execFileSync } from "node:child_process";
|
||||
import { mkdirSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { hash } from "@node-rs/argon2";
|
||||
import { SystemRole } from "@capakraken/shared";
|
||||
import { SystemRole } from "@nexus/shared";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { assertDestructiveDbAllowed } from "./destructive-db-guard.js";
|
||||
import { loadWorkspaceEnv, resolveWorkspacePath } from "./load-workspace-env.js";
|
||||
@@ -27,9 +27,9 @@ function parseArgs(argv: string[]): ResetOptions {
|
||||
force: false,
|
||||
skipBackup: false,
|
||||
backupDir: DEFAULT_BACKUP_DIR,
|
||||
adminEmail: "admin@capakraken.dev",
|
||||
adminEmail: "admin@nexus.dev",
|
||||
adminPassword: "admin123",
|
||||
adminName: "CapaKraken Admin",
|
||||
adminName: "Nexus Admin",
|
||||
};
|
||||
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
@@ -77,21 +77,17 @@ function createTimestamp() {
|
||||
}
|
||||
|
||||
function quoteIdentifier(identifier: string): string {
|
||||
return `"${identifier.replace(/"/g, "\"\"")}"`;
|
||||
return `"${identifier.replace(/"/g, '""')}"`;
|
||||
}
|
||||
|
||||
function createDatabaseBackup(databaseUrl: string, backupDir: string): string {
|
||||
mkdirSync(backupDir, { recursive: true });
|
||||
|
||||
const backupPath = resolve(backupDir, `dispo-reset-${createTimestamp()}.dump`);
|
||||
execFileSync(
|
||||
"pg_dump",
|
||||
["--format=custom", "--file", backupPath, databaseUrl],
|
||||
{
|
||||
stdio: "inherit",
|
||||
env: process.env,
|
||||
},
|
||||
);
|
||||
execFileSync("pg_dump", ["--format=custom", "--file", backupPath, databaseUrl], {
|
||||
stdio: "inherit",
|
||||
env: process.env,
|
||||
});
|
||||
|
||||
return backupPath;
|
||||
}
|
||||
@@ -193,7 +189,11 @@ async function main() {
|
||||
const truncatedTables = await truncatePublicTables();
|
||||
console.log(`Truncated ${truncatedTables.length} public tables.`);
|
||||
|
||||
const admin = await bootstrapPlatform(options.adminEmail, options.adminPassword, options.adminName);
|
||||
const admin = await bootstrapPlatform(
|
||||
options.adminEmail,
|
||||
options.adminPassword,
|
||||
options.adminName,
|
||||
);
|
||||
console.log(`Bootstrap admin created: ${admin.email}`);
|
||||
|
||||
if (backupPath) {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { assertDestructiveDbAllowed, formatTarget, parseDatabaseUrl } from "./destructive-db-guard.js";
|
||||
import {
|
||||
assertDestructiveDbAllowed,
|
||||
formatTarget,
|
||||
parseDatabaseUrl,
|
||||
} from "./destructive-db-guard.js";
|
||||
|
||||
const TEST_DATABASE_NAMES = [
|
||||
"capakraken_test",
|
||||
"capakraken_e2e",
|
||||
"capakraken_ci",
|
||||
];
|
||||
const TEST_DATABASE_NAMES = ["capakraken_test", "capakraken_e2e", "capakraken_ci"];
|
||||
|
||||
export function assertSafeSeedTarget(commandName: string) {
|
||||
return assertDestructiveDbAllowed({
|
||||
@@ -13,12 +13,12 @@ export function assertSafeSeedTarget(commandName: string) {
|
||||
});
|
||||
}
|
||||
|
||||
export function assertCapaKrakenDbTarget(commandName: string) {
|
||||
export function assertNexusDbTarget(commandName: string) {
|
||||
const rawUrl = process.env.DATABASE_URL;
|
||||
|
||||
if (!rawUrl) {
|
||||
throw new Error(
|
||||
`${commandName} aborted: DATABASE_URL is not configured. Run the command through the CapaKraken env wrappers so the workspace env files are loaded.`,
|
||||
`${commandName} aborted: DATABASE_URL is not configured. Run the command through the Nexus env wrappers so the workspace env files are loaded.`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ export function assertCapaKrakenDbTarget(commandName: string) {
|
||||
|
||||
if (!target.databaseName.startsWith("capakraken")) {
|
||||
throw new Error(
|
||||
`${commandName} aborted: database '${target.databaseName}' is not a valid CapaKraken target. Target=${formatTarget(target)}`,
|
||||
`${commandName} aborted: database '${target.databaseName}' is not a valid Nexus target. Target=${formatTarget(target)}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,7 @@
|
||||
*
|
||||
* Run: npx tsx src/seed-dispo-v2.ts
|
||||
*/
|
||||
import {
|
||||
DISPO_REQUIRED_ROLE_SEEDS,
|
||||
DISPO_UTILIZATION_CATEGORIES,
|
||||
} from "@capakraken/shared";
|
||||
import { DISPO_REQUIRED_ROLE_SEEDS, DISPO_UTILIZATION_CATEGORIES } from "@nexus/shared";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { loadWorkspaceEnv } from "./load-workspace-env.js";
|
||||
import { assertSafeSeedTarget } from "./safe-destructive-env.js";
|
||||
@@ -24,7 +21,21 @@ async function main() {
|
||||
|
||||
const countries = [
|
||||
{ code: "CR", name: "Costa Rica", dailyWorkingHours: 8, cities: ["Costa Rica"] },
|
||||
{ code: "DE", name: "Germany", dailyWorkingHours: 8, cities: ["Augsburg", "Berlin", "Bonn", "Frankfurt", "Hamburg", "Koeln", "Muenchen", "Stuttgart"] },
|
||||
{
|
||||
code: "DE",
|
||||
name: "Germany",
|
||||
dailyWorkingHours: 8,
|
||||
cities: [
|
||||
"Augsburg",
|
||||
"Berlin",
|
||||
"Bonn",
|
||||
"Frankfurt",
|
||||
"Hamburg",
|
||||
"Koeln",
|
||||
"Muenchen",
|
||||
"Stuttgart",
|
||||
],
|
||||
},
|
||||
{ code: "HU", name: "Hungary", dailyWorkingHours: 8, cities: ["Hungary"] },
|
||||
{ code: "IN", name: "India", dailyWorkingHours: 9, cities: ["India"] },
|
||||
{ code: "IT", name: "Italy", dailyWorkingHours: 8, cities: ["Italy"] },
|
||||
@@ -68,16 +79,22 @@ async function main() {
|
||||
|
||||
// ─── Org Unit Hierarchy ───────────────────────────────────────────────────
|
||||
|
||||
const l5 = await prisma.orgUnit.upsert({
|
||||
where: { parentId_name: { parentId: null as unknown as string, name: "Content Production" } },
|
||||
update: {},
|
||||
create: { name: "Content Production", level: 5, sortOrder: 1 },
|
||||
}).catch(async () => {
|
||||
// parentId_name unique doesn't work with null parentId in all Prisma versions
|
||||
const existing = await prisma.orgUnit.findFirst({ where: { name: "Content Production", level: 5, parentId: null } });
|
||||
if (existing) return existing;
|
||||
return prisma.orgUnit.create({ data: { name: "Content Production", level: 5, sortOrder: 1 } });
|
||||
});
|
||||
const l5 = await prisma.orgUnit
|
||||
.upsert({
|
||||
where: { parentId_name: { parentId: null as unknown as string, name: "Content Production" } },
|
||||
update: {},
|
||||
create: { name: "Content Production", level: 5, sortOrder: 1 },
|
||||
})
|
||||
.catch(async () => {
|
||||
// parentId_name unique doesn't work with null parentId in all Prisma versions
|
||||
const existing = await prisma.orgUnit.findFirst({
|
||||
where: { name: "Content Production", level: 5, parentId: null },
|
||||
});
|
||||
if (existing) return existing;
|
||||
return prisma.orgUnit.create({
|
||||
data: { name: "Content Production", level: 5, sortOrder: 1 },
|
||||
});
|
||||
});
|
||||
|
||||
const l6Data = [
|
||||
{ name: "CGI Content", sortOrder: 1 },
|
||||
@@ -98,10 +115,8 @@ async function main() {
|
||||
{ name: "CGI Development", sortOrder: 1 },
|
||||
{ name: "IT Development", sortOrder: 2 },
|
||||
],
|
||||
"Creative Content Production": [
|
||||
{ name: "Creative Content Production", sortOrder: 1 },
|
||||
],
|
||||
"VFX": [
|
||||
"Creative Content Production": [{ name: "Creative Content Production", sortOrder: 1 }],
|
||||
VFX: [
|
||||
{ name: "2D & Art Direction", sortOrder: 1 },
|
||||
{ name: "3D", sortOrder: 2 },
|
||||
{ name: "Program/Delivery Mgmt & Other", sortOrder: 3 },
|
||||
@@ -109,14 +124,18 @@ async function main() {
|
||||
};
|
||||
|
||||
for (const l6Item of l6Data) {
|
||||
let l6 = await prisma.orgUnit.findFirst({ where: { name: l6Item.name, level: 6, parentId: l5.id } });
|
||||
let l6 = await prisma.orgUnit.findFirst({
|
||||
where: { name: l6Item.name, level: 6, parentId: l5.id },
|
||||
});
|
||||
if (!l6) {
|
||||
l6 = await prisma.orgUnit.create({
|
||||
data: { name: l6Item.name, level: 6, parentId: l5.id, sortOrder: l6Item.sortOrder },
|
||||
});
|
||||
}
|
||||
for (const l7Item of l7Data[l6Item.name] ?? []) {
|
||||
const existing = await prisma.orgUnit.findFirst({ where: { name: l7Item.name, level: 7, parentId: l6.id } });
|
||||
const existing = await prisma.orgUnit.findFirst({
|
||||
where: { name: l7Item.name, level: 7, parentId: l6.id },
|
||||
});
|
||||
if (!existing) {
|
||||
await prisma.orgUnit.create({
|
||||
data: { name: l7Item.name, level: 7, parentId: l6.id, sortOrder: l7Item.sortOrder },
|
||||
@@ -152,11 +171,31 @@ async function main() {
|
||||
|
||||
const mgmtGroups = [
|
||||
{ name: "Accenture Leadership", targetPercentage: 0.365, sortOrder: 1, levels: [] as string[] },
|
||||
{ name: "Senior Manager", targetPercentage: 0.546, sortOrder: 2, levels: ["5-Associate Director", "6-Senior Manager"] },
|
||||
{
|
||||
name: "Senior Manager",
|
||||
targetPercentage: 0.546,
|
||||
sortOrder: 2,
|
||||
levels: ["5-Associate Director", "6-Senior Manager"],
|
||||
},
|
||||
{ name: "Manager", targetPercentage: 0.747, sortOrder: 3, levels: ["7-Manager"] },
|
||||
{ name: "Consultant", targetPercentage: 0.808, sortOrder: 4, levels: ["8-Associate Manager", "9-Team Lead/Consultant"] },
|
||||
{ name: "Analyst", targetPercentage: 0.805, sortOrder: 5, levels: ["10-Senior Analyst", "11-Analyst"] },
|
||||
{ name: "Associate", targetPercentage: 0.770, sortOrder: 6, levels: ["12-Associate", "13-New Associate"] },
|
||||
{
|
||||
name: "Consultant",
|
||||
targetPercentage: 0.808,
|
||||
sortOrder: 4,
|
||||
levels: ["8-Associate Manager", "9-Team Lead/Consultant"],
|
||||
},
|
||||
{
|
||||
name: "Analyst",
|
||||
targetPercentage: 0.805,
|
||||
sortOrder: 5,
|
||||
levels: ["10-Senior Analyst", "11-Analyst"],
|
||||
},
|
||||
{
|
||||
name: "Associate",
|
||||
targetPercentage: 0.77,
|
||||
sortOrder: 6,
|
||||
levels: ["12-Associate", "13-New Associate"],
|
||||
},
|
||||
];
|
||||
|
||||
for (const grp of mgmtGroups) {
|
||||
@@ -179,14 +218,39 @@ async function main() {
|
||||
|
||||
const clients: { name: string; code?: string; children: string[] }[] = [
|
||||
{ name: "BMW", code: "BMW", children: ["BMW AG"] },
|
||||
{ name: "VOLKSWAGEN", code: "VW", children: ["Audi Business Innovation GmbH", "Dr. Ing. h.c. F. Porsche AG", "MAN Truck & Bus SE", "Volkswagen AG"] },
|
||||
{ name: "DAIMLER", code: "DAIMLER", children: ["antoni garage GmbH & Co. KG", "Mercedes-Benz AG"] },
|
||||
{ name: "EXOR-STELLANTIS", code: "STELLANTIS", children: ["AUTOMOBILES PEUGEOT", "FCA Italy S.p.A.", "Ferrari S.p.A", "MASERATI SPA A SOCIO UNICO"] },
|
||||
{
|
||||
name: "VOLKSWAGEN",
|
||||
code: "VW",
|
||||
children: [
|
||||
"Audi Business Innovation GmbH",
|
||||
"Dr. Ing. h.c. F. Porsche AG",
|
||||
"MAN Truck & Bus SE",
|
||||
"Volkswagen AG",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "DAIMLER",
|
||||
code: "DAIMLER",
|
||||
children: ["antoni garage GmbH & Co. KG", "Mercedes-Benz AG"],
|
||||
},
|
||||
{
|
||||
name: "EXOR-STELLANTIS",
|
||||
code: "STELLANTIS",
|
||||
children: [
|
||||
"AUTOMOBILES PEUGEOT",
|
||||
"FCA Italy S.p.A.",
|
||||
"Ferrari S.p.A",
|
||||
"MASERATI SPA A SOCIO UNICO",
|
||||
],
|
||||
},
|
||||
{ name: "TATA MOTORS GROUP", code: "JLR", children: ["Jaguar Land Rover"] },
|
||||
{ name: "SCHWARZ GROUP", code: "LIDL", children: ["Lidl Stiftung & Co. KG"] },
|
||||
{ name: "INA-HOLDING SCHAEFFLER GMBH & CO KG", children: ["Schaeffler Technologies"] },
|
||||
{ name: "AIRBUS GROUP", children: ["Airbus SAS"] },
|
||||
{ name: "ALDI EINKAUF GMBH & CO. OHG", children: ["ALDI Einkauf SE & co. oHG", "ALDI SUED Dienstleistungs-SE & Co.oH"] },
|
||||
{
|
||||
name: "ALDI EINKAUF GMBH & CO. OHG",
|
||||
children: ["ALDI Einkauf SE & co. oHG", "ALDI SUED Dienstleistungs-SE & Co.oH"],
|
||||
},
|
||||
{ name: "ARLA", children: ["Arla Foods amba"] },
|
||||
{ name: "BLANC & FISCHER FAMILIENHOLDING", children: ["BLANCO GmbH + Co KG"] },
|
||||
{ name: "BMDS & ITZBUND ASG", children: ["Bundesministerium der Finanzen"] },
|
||||
@@ -229,7 +293,9 @@ async function main() {
|
||||
});
|
||||
}
|
||||
for (const childName of c.children) {
|
||||
const existing = await prisma.client.findFirst({ where: { name: childName, parentId: master.id } });
|
||||
const existing = await prisma.client.findFirst({
|
||||
where: { name: childName, parentId: master.id },
|
||||
});
|
||||
if (!existing) {
|
||||
await prisma.client.create({
|
||||
data: { name: childName, parentId: master.id },
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { PrismaClient, type HolidayCalendarEntry } from "@prisma/client";
|
||||
import { buildHolidayCalendarSeedDefinitions } from "./holiday-calendar-seed-data.js";
|
||||
import { loadWorkspaceEnv } from "./load-workspace-env.js";
|
||||
import { assertCapaKrakenDbTarget } from "./safe-destructive-env.js";
|
||||
import { assertNexusDbTarget } from "./safe-destructive-env.js";
|
||||
|
||||
loadWorkspaceEnv();
|
||||
|
||||
@@ -33,8 +33,8 @@ async function findScopedCalendar(input: {
|
||||
where: {
|
||||
countryId: input.countryId,
|
||||
scopeType: input.scopeType,
|
||||
stateCode: input.scopeType === "STATE" ? input.stateCode ?? null : null,
|
||||
metroCityId: input.scopeType === "CITY" ? input.metroCityId ?? null : null,
|
||||
stateCode: input.scopeType === "STATE" ? (input.stateCode ?? null) : null,
|
||||
metroCityId: input.scopeType === "CITY" ? (input.metroCityId ?? null) : null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
@@ -50,7 +50,7 @@ async function findScopedCalendar(input: {
|
||||
}
|
||||
|
||||
async function main() {
|
||||
assertCapaKrakenDbTarget("db:seed:holidays");
|
||||
assertNexusDbTarget("db:seed:holidays");
|
||||
console.log("Seeding holiday calendars for 2026-2027...");
|
||||
|
||||
const countries = await prisma.country.findMany({
|
||||
@@ -81,10 +81,7 @@ async function main() {
|
||||
const definitions = buildHolidayCalendarSeedDefinitions({
|
||||
availableCountryCodes: countries.map((country) => country.code),
|
||||
availableCitiesByCountry: Object.fromEntries(
|
||||
countries.map((country) => [
|
||||
country.code,
|
||||
country.metroCities.map((city) => city.name),
|
||||
]),
|
||||
countries.map((country) => [country.code, country.metroCities.map((city) => city.name)]),
|
||||
),
|
||||
activeGermanStates: activeGermanStatesRows
|
||||
.map((row) => row.federalState)
|
||||
@@ -135,8 +132,8 @@ async function main() {
|
||||
name: definition.name,
|
||||
scopeType: definition.scopeType,
|
||||
countryId: country.id,
|
||||
stateCode: definition.scopeType === "STATE" ? definition.stateCode ?? null : null,
|
||||
metroCityId: definition.scopeType === "CITY" ? metroCity?.id ?? null : null,
|
||||
stateCode: definition.scopeType === "STATE" ? (definition.stateCode ?? null) : null,
|
||||
metroCityId: definition.scopeType === "CITY" ? (metroCity?.id ?? null) : null,
|
||||
priority: definition.priority,
|
||||
isActive: true,
|
||||
},
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { PrismaClient, type Prisma } from "@prisma/client";
|
||||
import { getHolidayDemoCityNamesByCountry, getHolidayDemoProfileForIndex } from "./holiday-demo-profiles.js";
|
||||
import {
|
||||
getHolidayDemoCityNamesByCountry,
|
||||
getHolidayDemoProfileForIndex,
|
||||
} from "./holiday-demo-profiles.js";
|
||||
import { loadWorkspaceEnv } from "./load-workspace-env.js";
|
||||
import { assertCapaKrakenDbTarget } from "./safe-destructive-env.js";
|
||||
import { assertNexusDbTarget } from "./safe-destructive-env.js";
|
||||
|
||||
loadWorkspaceEnv();
|
||||
|
||||
@@ -12,7 +15,9 @@ type CountryRecord = {
|
||||
code: string;
|
||||
};
|
||||
|
||||
function asJsonObject(value: Prisma.JsonValue | null | undefined): Record<string, Prisma.JsonValue> {
|
||||
function asJsonObject(
|
||||
value: Prisma.JsonValue | null | undefined,
|
||||
): Record<string, Prisma.JsonValue> {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
return {};
|
||||
}
|
||||
@@ -53,7 +58,7 @@ async function ensureCities(countryByCode: Map<string, CountryRecord>) {
|
||||
}
|
||||
|
||||
async function main() {
|
||||
assertCapaKrakenDbTarget("db:seed:holiday-demo-resources");
|
||||
assertNexusDbTarget("db:seed:holiday-demo-resources");
|
||||
console.log("Normalizing active resources for holiday demo profiles...");
|
||||
|
||||
const countrySeeds = [
|
||||
@@ -65,12 +70,14 @@ async function main() {
|
||||
const countries = [];
|
||||
|
||||
for (const countrySeed of countrySeeds) {
|
||||
countries.push(await prisma.country.upsert({
|
||||
where: { code: countrySeed.code },
|
||||
update: { name: countrySeed.name, dailyWorkingHours: countrySeed.dailyWorkingHours },
|
||||
create: countrySeed,
|
||||
select: { id: true, code: true },
|
||||
}));
|
||||
countries.push(
|
||||
await prisma.country.upsert({
|
||||
where: { code: countrySeed.code },
|
||||
update: { name: countrySeed.name, dailyWorkingHours: countrySeed.dailyWorkingHours },
|
||||
create: countrySeed,
|
||||
select: { id: true, code: true },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const countryByCode = new Map(countries.map((country) => [country.code, country] as const));
|
||||
|
||||
+2300
-385
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
import { ROLE_DEFAULT_PERMISSIONS, SystemRole } from "@capakraken/shared";
|
||||
import { ROLE_DEFAULT_PERMISSIONS, SystemRole } from "@nexus/shared";
|
||||
|
||||
export const SYSTEM_ROLE_CONFIG_DEFAULTS = [
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { BlueprintTarget, FieldType } from "@capakraken/shared";
|
||||
import { BlueprintTarget, FieldType } from "@nexus/shared";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { loadWorkspaceEnv } from "./load-workspace-env.js";
|
||||
import { assertCapaKrakenDbTarget } from "./safe-destructive-env.js";
|
||||
import { assertNexusDbTarget } from "./safe-destructive-env.js";
|
||||
|
||||
loadWorkspaceEnv();
|
||||
|
||||
@@ -11,10 +11,10 @@ const prisma = new PrismaClient();
|
||||
|
||||
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" },
|
||||
{ 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 = [
|
||||
@@ -26,7 +26,7 @@ const frameRateOptions = [
|
||||
|
||||
const colorSpaceOptions = [
|
||||
{ value: "sRGB_Rec709", label: "sRGB / Rec.709" },
|
||||
{ value: "DCI-P3", label: "DCI-P3 (Cinema)" },
|
||||
{ value: "DCI-P3", label: "DCI-P3 (Cinema)" },
|
||||
{ value: "Rec2020_HDR", label: "Rec.2020 / HDR" },
|
||||
];
|
||||
|
||||
@@ -70,10 +70,10 @@ const resourceFieldDefs = [
|
||||
order: 3,
|
||||
group: "Basic Info",
|
||||
options: [
|
||||
{ value: "Employee", label: "Employee" },
|
||||
{ value: "Employee", label: "Employee" },
|
||||
{ value: "Freelancer", label: "Freelancer" },
|
||||
{ value: "Intern", label: "Intern" },
|
||||
{ value: "External", label: "External Contractor" },
|
||||
{ value: "Intern", label: "Intern" },
|
||||
{ value: "External", label: "External Contractor" },
|
||||
],
|
||||
},
|
||||
// Group: Work Setup
|
||||
@@ -95,15 +95,15 @@ const resourceFieldDefs = [
|
||||
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", 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)" },
|
||||
{ value: "UTC+8", label: "UTC+8 (CST/HKT)" },
|
||||
{ value: "UTC+9", label: "UTC+9 (JST)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -115,18 +115,18 @@ const resourceFieldDefs = [
|
||||
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: "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" },
|
||||
{ value: "ZBrush", label: "ZBrush" },
|
||||
{ value: "PremierePro", label: "Premiere Pro" },
|
||||
{ value: "DaVinciResolve", label: "DaVinci Resolve" },
|
||||
],
|
||||
},
|
||||
// Group: Background
|
||||
@@ -205,7 +205,7 @@ const projectFieldDefs = [
|
||||
order: 2,
|
||||
group: "Client & Billing",
|
||||
options: [
|
||||
{ value: "Confidential", label: "Confidential" },
|
||||
{ value: "Confidential", label: "Confidential" },
|
||||
{ value: "Not Confidential", label: "Not Confidential" },
|
||||
],
|
||||
},
|
||||
@@ -334,7 +334,7 @@ const content3DFieldDefs = [
|
||||
order: 2,
|
||||
group: "Client & Billing",
|
||||
options: [
|
||||
{ value: "Confidential", label: "Confidential" },
|
||||
{ value: "Confidential", label: "Confidential" },
|
||||
{ value: "Not Confidential", label: "Not Confidential" },
|
||||
],
|
||||
},
|
||||
@@ -367,13 +367,13 @@ const content3DFieldDefs = [
|
||||
order: 5,
|
||||
group: "Technical Specs",
|
||||
options: [
|
||||
{ value: "Arnold", label: "Arnold" },
|
||||
{ value: "VRay", label: "V-Ray" },
|
||||
{ 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" },
|
||||
{ value: "Cycles", label: "Cycles (Blender)" },
|
||||
{ value: "Octane", label: "Octane" },
|
||||
{ value: "Corona", label: "Corona" },
|
||||
{ value: "KeyShot", label: "KeyShot" },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -387,9 +387,9 @@ const content3DFieldDefs = [
|
||||
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" },
|
||||
{ value: "CloudAWS", label: "Cloud (AWS)" },
|
||||
{ value: "CloudGCP", label: "Cloud (GCP)" },
|
||||
{ value: "Hybrid", label: "Hybrid" },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -456,9 +456,9 @@ const content3DFieldDefs = [
|
||||
order: 12,
|
||||
group: "Asset Scope",
|
||||
options: [
|
||||
{ value: "Low", label: "Low" },
|
||||
{ value: "Medium", label: "Medium" },
|
||||
{ value: "High", label: "High" },
|
||||
{ value: "Low", label: "Low" },
|
||||
{ value: "Medium", label: "Medium" },
|
||||
{ value: "High", label: "High" },
|
||||
{ value: "VeryHigh", label: "Very High" },
|
||||
],
|
||||
},
|
||||
@@ -534,12 +534,48 @@ const content3DFieldDefs = [
|
||||
];
|
||||
|
||||
const rolePresets3D = [
|
||||
{ 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 },
|
||||
{
|
||||
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,
|
||||
},
|
||||
];
|
||||
|
||||
// ─── 4. Animation Production ────────────────────────────────────────────────
|
||||
@@ -575,7 +611,7 @@ const animationFieldDefs = [
|
||||
order: 2,
|
||||
group: "Client & Billing",
|
||||
options: [
|
||||
{ value: "Confidential", label: "Confidential" },
|
||||
{ value: "Confidential", label: "Confidential" },
|
||||
{ value: "Not Confidential", label: "Not Confidential" },
|
||||
],
|
||||
},
|
||||
@@ -608,12 +644,12 @@ const animationFieldDefs = [
|
||||
order: 5,
|
||||
group: "Animation Specs",
|
||||
options: [
|
||||
{ value: "Realistic", label: "Realistic / Photoreal" },
|
||||
{ value: "Stylized", label: "Stylized" },
|
||||
{ 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" },
|
||||
{ value: "Procedural", label: "Procedural" },
|
||||
{ value: "CelShaded", label: "Cel-Shaded / Toon" },
|
||||
{ value: "Mixed", label: "Mixed / Hybrid" },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -682,10 +718,10 @@ const animationFieldDefs = [
|
||||
order: 12,
|
||||
group: "Characters & Rigging",
|
||||
options: [
|
||||
{ value: "Simple", label: "Simple (basic bones)" },
|
||||
{ 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)" },
|
||||
{ value: "Complex", label: "Complex (full-body IK)" },
|
||||
{ value: "Hero", label: "Hero (simulation + secondary motion)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -760,12 +796,48 @@ const animationFieldDefs = [
|
||||
];
|
||||
|
||||
const rolePresetsAnimation = [
|
||||
{ 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 },
|
||||
{
|
||||
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,
|
||||
},
|
||||
];
|
||||
|
||||
// ─── 5. VFX / Compositing (new) ─────────────────────────────────────────────
|
||||
@@ -801,7 +873,7 @@ const vfxFieldDefs = [
|
||||
order: 2,
|
||||
group: "Client & Billing",
|
||||
options: [
|
||||
{ value: "Confidential", label: "Confidential" },
|
||||
{ value: "Confidential", label: "Confidential" },
|
||||
{ value: "Not Confidential", label: "Not Confidential" },
|
||||
],
|
||||
},
|
||||
@@ -834,12 +906,12 @@ const vfxFieldDefs = [
|
||||
order: 5,
|
||||
group: "VFX Specs",
|
||||
options: [
|
||||
{ value: "GreenScreen", label: "Green Screen / Chroma Key" },
|
||||
{ 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: "Rotoscoping", label: "Rotoscoping" },
|
||||
{ value: "ParticleFX", label: "Particle FX" },
|
||||
{ value: "FluidSim", label: "Fluid Simulation" },
|
||||
{ value: "MattePainting", label: "Matte Painting" },
|
||||
{ value: "TitleSequence", label: "Title Sequence" },
|
||||
],
|
||||
@@ -862,9 +934,9 @@ const vfxFieldDefs = [
|
||||
order: 7,
|
||||
group: "VFX Specs",
|
||||
options: [
|
||||
{ value: "Low", label: "Low (cleanup / grade)" },
|
||||
{ value: "Medium", label: "Medium (integration)" },
|
||||
{ value: "High", label: "High (simulation)" },
|
||||
{ value: "Low", label: "Low (cleanup / grade)" },
|
||||
{ value: "Medium", label: "Medium (integration)" },
|
||||
{ value: "High", label: "High (simulation)" },
|
||||
{ value: "Photoreal", label: "Photoreal" },
|
||||
],
|
||||
},
|
||||
@@ -960,12 +1032,48 @@ const vfxFieldDefs = [
|
||||
];
|
||||
|
||||
const rolePresetsVFX = [
|
||||
{ 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 },
|
||||
{
|
||||
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,
|
||||
},
|
||||
];
|
||||
|
||||
// ─── 6. Motion Design (new) ─────────────────────────────────────────────────
|
||||
@@ -1001,7 +1109,7 @@ const motionDesignFieldDefs = [
|
||||
order: 2,
|
||||
group: "Client & Billing",
|
||||
options: [
|
||||
{ value: "Confidential", label: "Confidential" },
|
||||
{ value: "Confidential", label: "Confidential" },
|
||||
{ value: "Not Confidential", label: "Not Confidential" },
|
||||
],
|
||||
},
|
||||
@@ -1034,11 +1142,11 @@ const motionDesignFieldDefs = [
|
||||
order: 5,
|
||||
group: "Motion Specs",
|
||||
options: [
|
||||
{ value: "Flat2D", label: "2D Flat / Icon Animation" },
|
||||
{ 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" },
|
||||
{ value: "TwoHalfD", label: "2.5D" },
|
||||
{ value: "ThreeD", label: "3D Motion" },
|
||||
{ value: "Mixed", label: "Mixed / Hybrid" },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -1059,11 +1167,11 @@ const motionDesignFieldDefs = [
|
||||
order: 7,
|
||||
group: "Motion Specs",
|
||||
options: [
|
||||
{ value: "Social_916", label: "Social (9:16 vertical)" },
|
||||
{ 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" },
|
||||
{ value: "Square_11", label: "Square (1:1)" },
|
||||
{ value: "Banner", label: "Banner / Display Ads" },
|
||||
{ value: "Custom", label: "Custom" },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -1168,17 +1276,47 @@ const motionDesignFieldDefs = [
|
||||
];
|
||||
|
||||
const rolePresetsMotion = [
|
||||
{ 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 },
|
||||
{
|
||||
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,
|
||||
},
|
||||
];
|
||||
|
||||
// ─── Main ───────────────────────────────────────────────────────────────────
|
||||
|
||||
async function main() {
|
||||
assertCapaKrakenDbTarget("db:update:blueprints");
|
||||
assertNexusDbTarget("db:update:blueprints");
|
||||
console.log("Starting blueprint update...\n");
|
||||
|
||||
// Blueprints to update in-place (by name — preserves PKs and FKs)
|
||||
@@ -1274,4 +1412,6 @@ async function main() {
|
||||
console.log("\nDone.");
|
||||
}
|
||||
|
||||
main().catch(console.error).finally(() => prisma.$disconnect());
|
||||
main()
|
||||
.catch(console.error)
|
||||
.finally(() => prisma.$disconnect());
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Updates CapaKrakenExamples.xlsx with missing columns and documentation.
|
||||
* Adds: Display Name, Email, Skills, CapaKraken Notes columns to EID sheet.
|
||||
* Adds: Start Date, End Date, Status, CapaKraken Notes columns to Projects sheet.
|
||||
* Updates NexusExamples.xlsx with missing columns and documentation.
|
||||
* Adds: Display Name, Email, Skills, Nexus Notes columns to EID sheet.
|
||||
* Adds: Start Date, End Date, Status, Nexus Notes columns to Projects sheet.
|
||||
*/
|
||||
|
||||
import ExcelJS from "exceljs";
|
||||
@@ -11,7 +11,7 @@ import { dirname, join } from "path";
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const EXCEL_PATH = join(__dirname, "../../../samples/CapaKrakenExamples.xlsx");
|
||||
const EXCEL_PATH = join(__dirname, "../../../samples/NexusExamples.xlsx");
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -36,8 +36,8 @@ function computeSkillLabel(chapter, typeOfWork) {
|
||||
return typeOfWork;
|
||||
}
|
||||
|
||||
function computeCapaKrakenEid(eid) {
|
||||
// In CapaKraken the EID stays as firstname.lastname (unique key)
|
||||
function computeNexusEid(eid) {
|
||||
// In Nexus the EID stays as firstname.lastname (unique key)
|
||||
return eid;
|
||||
}
|
||||
|
||||
@@ -139,28 +139,28 @@ async function main() {
|
||||
// Column N: Display Name
|
||||
// Column O: Email
|
||||
// Column P: Skills (derived)
|
||||
// Column Q: Description / Notes for CapaKraken
|
||||
// Column Q: Description / Notes for Nexus
|
||||
|
||||
const newEidCols = [
|
||||
{
|
||||
col: 14, // N
|
||||
header: "Display Name\n(auto-generated)",
|
||||
doc: "Full display name derived from EID (firstname.lastname → Firstname Lastname). Used as the person's name in CapaKraken.",
|
||||
doc: "Full display name derived from EID (firstname.lastname → Firstname Lastname). Used as the person's name in Nexus.",
|
||||
},
|
||||
{
|
||||
col: 15, // O
|
||||
header: "Email\n(generated)",
|
||||
doc: "Generated email: firstname.lastname@capakraken.example. Required unique field in CapaKraken. Replace with real email in production.",
|
||||
doc: "Generated email: firstname.lastname@capakraken.example. Required unique field in Nexus. Replace with real email in production.",
|
||||
},
|
||||
{
|
||||
col: 16, // P
|
||||
header: "Skills\n(derived from Chapter)",
|
||||
doc: "Skill tags assigned based on Chapter + Type of Work. Format: 'SkillA | SkillB'. Stored as JSON array in CapaKraken with proficiency 1-5. Senior (LCR >= 118) -> 5, Mid-Senior (LCR >= 95) -> 4, Mid -> 3.",
|
||||
doc: "Skill tags assigned based on Chapter + Type of Work. Format: 'SkillA | SkillB'. Stored as JSON array in Nexus with proficiency 1-5. Senior (LCR >= 118) -> 5, Mid-Senior (LCR >= 95) -> 4, Mid -> 3.",
|
||||
},
|
||||
{
|
||||
col: 17, // Q
|
||||
header: "CapaKraken Notes",
|
||||
doc: "How data maps to CapaKraken:\n- EID = unique key (col A)\n- Chapter = chapter field\n- LCR / UCR -> multiply by 100 for integer cents (EUR85.00 -> 8500)\n- Hours fraction x 8 = daily availability hours\n- Chargeability -> multiply by 100 for % (0.75 -> 75%)\n- Employee type, City, Client Unit -> stored in dynamicFields JSONB",
|
||||
header: "Nexus Notes",
|
||||
doc: "How data maps to Nexus:\n- EID = unique key (col A)\n- Chapter = chapter field\n- LCR / UCR -> multiply by 100 for integer cents (EUR85.00 -> 8500)\n- Hours fraction x 8 = daily availability hours\n- Chargeability -> multiply by 100 for % (0.75 -> 75%)\n- Employee type, City, Client Unit -> stored in dynamicFields JSONB",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -214,18 +214,18 @@ async function main() {
|
||||
|
||||
// Also add doc notes to existing header columns A-M in row 1
|
||||
const eidExistingDocs = [
|
||||
"Unique identifier. Used as EID in CapaKraken (no EMP-XXX prefix needed). e.g. steve.rogers",
|
||||
"Team / department. Maps to 'chapter' field in CapaKraken.",
|
||||
"Unique identifier. Used as EID in Nexus (no EMP-XXX prefix needed). e.g. steve.rogers",
|
||||
"Team / department. Maps to 'chapter' field in Nexus.",
|
||||
"Specialization within chapter. Stored in dynamicFields.workType.",
|
||||
"Assigned client account. Stored in dynamicFields.clientUnit.",
|
||||
"Unit-specific field. Currently unused — can be stored in dynamicFields.",
|
||||
"Office city location. Stored in dynamicFields.city.",
|
||||
"Employment type: Employee or Freelancer. Stored in dynamicFields.employeeType.",
|
||||
"Loaded Cost Rate (LCR) in EUR/h. Multiply × 100 for CapaKraken cents. e.g. 133.77 → 13377",
|
||||
"Loaded Cost Rate (LCR) in EUR/h. Multiply × 100 for Nexus cents. e.g. 133.77 → 13377",
|
||||
"Unloaded/Utilization Cost Rate (UCR) in EUR/h. Multiply × 100 for cents.",
|
||||
"FTE fraction (1.0 = 40h/week, 0.8 = 4 days, 0.5 = 20h/week). Combined with col K for availability JSON.",
|
||||
"Available weekdays. 'all' = Mon-Fri. Specific days listed = only those days active at 8h.",
|
||||
"Chargeability target as decimal. Multiply × 100 for CapaKraken % (0.75 → 75%).",
|
||||
"Chargeability target as decimal. Multiply × 100 for Nexus % (0.75 → 75%).",
|
||||
"(unused)",
|
||||
];
|
||||
for (let c = 1; c <= 13; c++) {
|
||||
@@ -254,12 +254,12 @@ async function main() {
|
||||
{
|
||||
col: 19, // S
|
||||
header: "Status\n(derived)",
|
||||
doc: "CapaKraken status derived from 'is ordered' + win probability + date:\n• COMPLETED: ordered + 100% + past dates\n• ACTIVE: ordered + 100% + current/future\n• ON_HOLD: ordered but paused\n• DRAFT: not ordered or low win probability",
|
||||
doc: "Nexus status derived from 'is ordered' + win probability + date:\n• COMPLETED: ordered + 100% + past dates\n• ACTIVE: ordered + 100% + current/future\n• ON_HOLD: ordered but paused\n• DRAFT: not ordered or low win probability",
|
||||
},
|
||||
{
|
||||
col: 20, // T
|
||||
header: "CapaKraken Notes",
|
||||
doc: "How data maps to CapaKraken:\n• Col C (short code) → shortCode (unique key)\n• Col B → name\n• BD/CH/UN → OrderType: BD / CHARGEABLE / INTERNAL\n• Internal/External → allocationType: INT / EXT\n• Resource Costs (col I) × 100 = budgetCents in CapaKraken\n• Col H (chargability %) → stored in dynamicFields.chargeabilityPercent\n• Col J (person hours) → stored in dynamicFields.personHoursSold\n• Col O (classification) → stored in dynamicFields.classification",
|
||||
header: "Nexus Notes",
|
||||
doc: "How data maps to Nexus:\n• Col C (short code) → shortCode (unique key)\n• Col B → name\n• BD/CH/UN → OrderType: BD / CHARGEABLE / INTERNAL\n• Internal/External → allocationType: INT / EXT\n• Resource Costs (col I) × 100 = budgetCents in Nexus\n• Col H (chargability %) → stored in dynamicFields.chargeabilityPercent\n• Col J (person hours) → stored in dynamicFields.personHoursSold\n• Col O (classification) → stored in dynamicFields.classification",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -286,21 +286,21 @@ async function main() {
|
||||
|
||||
const projExistingDocs = [
|
||||
"Client Unit tag. e.g. [DAI]=Daimler, [PAG]=Porsche AG, [BMW], [JLR]=Jaguar Land Rover. Stored in dynamicFields.clientUnit.",
|
||||
"Full project name → maps to CapaKraken 'name' field.",
|
||||
"Short internal project code (5-6 chars) → maps to CapaKraken 'shortCode' (unique key). e.g. JLFJFL",
|
||||
"Full project name → maps to Nexus 'name' field.",
|
||||
"Short internal project code (5-6 chars) → maps to Nexus 'shortCode' (unique key). e.g. JLFJFL",
|
||||
"'yes'/'no' — whether the project is formally ordered. Drives status: yes+100% → ACTIVE/COMPLETED.",
|
||||
"Order type: BD=Business Development, CH=Chargeable, UN=Internal/Unordered. Maps to CapaKraken OrderType.",
|
||||
"Win probability 0-100. Used in CapaKraken 'winProbability' field for pipeline forecasting.",
|
||||
"Allocation type: Internal → INT, External → EXT. Maps to CapaKraken 'allocationType' field.",
|
||||
"Order type: BD=Business Development, CH=Chargeable, UN=Internal/Unordered. Maps to Nexus OrderType.",
|
||||
"Win probability 0-100. Used in Nexus 'winProbability' field for pipeline forecasting.",
|
||||
"Allocation type: Internal → INT, External → EXT. Maps to Nexus 'allocationType' field.",
|
||||
"Chargeability % as decimal. Stored in dynamicFields.chargeabilityPercent.",
|
||||
"Planned resource cost in EUR. Multiply × 100 for CapaKraken budgetCents. e.g. 78799 → 7879900 cents.",
|
||||
"Planned resource cost in EUR. Multiply × 100 for Nexus budgetCents. e.g. 78799 → 7879900 cents.",
|
||||
"Person hours planned/sold. Stored in dynamicFields.personHoursSold for budget tracking.",
|
||||
"Team staffing (empty in source). Would list assigned EIDs. Handled by Allocations in CapaKraken.",
|
||||
"Team staffing (empty in source). Would list assigned EIDs. Handled by Allocations in Nexus.",
|
||||
"Day project sold (empty in source). Could be stored as dynamicFields.dateSold.",
|
||||
"Project start date (empty in source → synthesized in col Q). Maps to CapaKraken 'startDate'.",
|
||||
"Project end date (empty in source → synthesized in col R). Maps to CapaKraken 'endDate'.",
|
||||
"Project start date (empty in source → synthesized in col Q). Maps to Nexus 'startDate'.",
|
||||
"Project end date (empty in source → synthesized in col R). Maps to Nexus 'endDate'.",
|
||||
"Confidentiality: Confidential / Not Confidential. Stored in dynamicFields.classification.",
|
||||
"Responsible EID / Owner (empty in source). Would map to a PM allocation in CapaKraken.",
|
||||
"Responsible EID / Owner (empty in source). Would map to a PM allocation in Nexus.",
|
||||
];
|
||||
for (let c = 1; c <= 16; c++) {
|
||||
const doc = projExistingDocs[c - 1];
|
||||
@@ -329,15 +329,15 @@ async function main() {
|
||||
projSheet.getColumn(19).width = 16;
|
||||
projSheet.getColumn(20).width = 50;
|
||||
|
||||
// ─── Add a "CapaKraken Data Model" sheet ──────────────────────────────────
|
||||
// ─── Add a "Nexus Data Model" sheet ──────────────────────────────────
|
||||
|
||||
let modelSheet = workbook.getWorksheet("CapaKraken Data Model");
|
||||
let modelSheet = workbook.getWorksheet("Nexus Data Model");
|
||||
if (!modelSheet) {
|
||||
modelSheet = workbook.addWorksheet("CapaKraken Data Model");
|
||||
modelSheet = workbook.addWorksheet("Nexus Data Model");
|
||||
}
|
||||
|
||||
const modelData = [
|
||||
["CapaKraken Data Model — Field Reference", "", "", "", ""],
|
||||
["Nexus Data Model — Field Reference", "", "", "", ""],
|
||||
["", "", "", "", ""],
|
||||
["RESOURCE FIELDS", "", "", "", ""],
|
||||
["Field", "Type", "Required", "Example", "Description"],
|
||||
@@ -393,7 +393,7 @@ async function main() {
|
||||
// Style title row
|
||||
const titleCell = modelSheet.getCell(1, 1);
|
||||
titleCell.font = { bold: true, size: 14, color: { argb: "FF4F46E5" } };
|
||||
titleCell.value = "CapaKraken Data Model — Field Reference";
|
||||
titleCell.value = "Nexus Data Model — Field Reference";
|
||||
|
||||
// Style section headers and field headers
|
||||
const sectionRows = [3, 17, 31];
|
||||
@@ -421,7 +421,7 @@ async function main() {
|
||||
console.log(`✅ Excel updated: ${EXCEL_PATH}`);
|
||||
console.log(" - EID_Informationen: added Display Name, Email, Skills, Notes columns");
|
||||
console.log(" - Projektinfomartionen: added Start Date, End Date, Status, Notes columns");
|
||||
console.log(" - Added new sheet: 'CapaKraken Data Model' (field reference)");
|
||||
console.log(" - Added new sheet: 'Nexus Data Model' (field reference)");
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
|
||||
Reference in New Issue
Block a user