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

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:
2026-05-21 16:28:40 +02:00
committed by Hartmut
parent d9a7ec0338
commit b41c1d2501
943 changed files with 24548 additions and 16832 deletions
+24 -27
View File
@@ -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,
);
});
+102 -80
View File
@@ -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());
+40 -11
View File
@@ -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)),
},
]),
});
}
+17 -22
View File
@@ -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,
+1 -1
View File
@@ -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);
+13 -13
View File
@@ -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) {
+9 -9
View File
@@ -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)}`,
);
}
+96 -30
View File
@@ -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 },
+7 -10
View File
@@ -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,
},
+17 -10
View File
@@ -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
View File
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 = [
{
+234 -94
View File
@@ -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());
+35 -35
View File
@@ -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) => {