Files
CapaKraken/packages/db/src/update-excel.mjs
T

431 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Updates PlanarchyExamples.xlsx with missing columns and documentation.
* Adds: Display Name, Email, Skills, Planarchy Notes columns to EID sheet.
* Adds: Start Date, End Date, Status, Planarchy Notes columns to Projects sheet.
*/
import ExcelJS from "exceljs";
import { fileURLToPath } from "url";
import { dirname, join } from "path";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const EXCEL_PATH = join(__dirname, "../../../samples/PlanarchyExamples.xlsx");
// ─── Helpers ─────────────────────────────────────────────────────────────────
function toDisplayName(eid) {
return eid
.split(".")
.map((p) => p.charAt(0).toUpperCase() + p.slice(1))
.join(" ");
}
function toEmail(eid) {
return `${eid}@planarchy.example`;
}
function computeSkillLabel(chapter, typeOfWork) {
if (chapter === "Project Management") return "Project Manager | Scrum Master";
if (chapter === "Digital Content Production") return "Unreal Engine | 3D Lighting | 3D Modeling";
if (chapter === "Product Data Management") return "Visualization Logic | Quality Assurance";
if (chapter === "Art Direction") return "Art Direction | 2D Compositing";
if (chapter === "CGI-Dev" && typeOfWork === "Dev Unreal") return "Unreal Dev | Technical Architect";
if (chapter === "CGI-Dev" && typeOfWork === "Dev Frontend") return "Frontend Dev | Backend Dev";
return typeOfWork;
}
function computePlanarchyEid(eid) {
// In Planarchy the EID stays as firstname.lastname (unique key)
return eid;
}
// Project date table (matches seed.ts)
const PROJECT_DATES = {
JLFJFL: ["2024-01-15", "2024-05-31", "COMPLETED"],
JZTFG: ["2024-02-01", "2024-09-30", "COMPLETED"],
UZGHD: ["2024-03-01", "2024-07-31", "COMPLETED"],
DSFGHF: ["2024-04-01", "2024-12-31", "COMPLETED"],
TZUZG: ["2024-05-15", "2025-01-31", "COMPLETED"],
KLJLOH: ["2024-06-01", "2024-10-31", "COMPLETED"],
FGHJJ: ["2024-07-01", "2025-01-31", "COMPLETED"],
FFGHG: ["2024-08-01", "2024-12-31", "COMPLETED"],
ARGFH: ["2024-09-01", "2025-03-31", "COMPLETED"],
ERTGR: ["2024-10-01", "2025-01-31", "COMPLETED"],
HFGRT: ["2024-11-01", "2025-06-30", "COMPLETED"],
WERTF: ["2024-12-01", "2025-04-30", "COMPLETED"],
RDFGH: ["2025-01-15", "2025-08-31", "ACTIVE"],
HJZGJ: ["2025-02-01", "2025-10-31", "ACTIVE"],
SRTZRT: ["2025-03-01", "2025-09-30", "DRAFT"],
STTHG: ["2025-04-01", "2026-01-31", "ACTIVE"],
EDDGG: ["2025-05-01", "2025-12-31", "ACTIVE"],
FGHHGF: ["2025-06-01", "2026-03-31", "ACTIVE"],
AFDGH: ["2025-07-01", "2025-12-31", "ACTIVE"],
HTZUG: ["2025-08-01", "2026-04-30", "ON_HOLD"],
MUIUF: ["2025-09-01", "2026-06-30", "ACTIVE"],
FGJUO: ["2025-10-01", "2026-02-28", "ACTIVE"],
ZUITJZ: ["2025-11-01", "2026-08-31", "ACTIVE"],
SACVFH: ["2025-12-01", "2026-06-30", "ACTIVE"],
EWFSFH: ["2026-01-15", "2026-09-30", "ACTIVE"],
NTZJJ: ["2026-02-01", "2026-10-31", "ACTIVE"],
EFGTZ: ["2026-03-01", "2026-12-31", "ACTIVE"],
SJUIL: ["2026-04-01", "2026-11-30", "DRAFT"],
SAERZ: ["2026-05-01", "2027-01-31", "DRAFT"],
HJUOP: ["2026-06-01", "2027-03-31", "ACTIVE"],
AERGH: ["2026-07-01", "2026-12-31", "DRAFT"],
POUILZ: ["2026-08-01", "2027-02-28", "DRAFT"],
EWERZ: ["2026-09-01", "2027-06-30", "DRAFT"],
HZIOP: ["2026-10-01", "2027-04-30", "DRAFT"],
DGHBN: ["2026-11-01", "2027-05-31", "DRAFT"],
KUOOL: ["2026-12-01", "2027-09-30", "ACTIVE"],
};
const HEADER_FILL = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FF4F46E5" }, // indigo-600
};
const HEADER_FONT = { bold: true, color: { argb: "FFFFFFFF" }, size: 10 };
const DOC_FILL = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FFF0F9FF" }, // light blue
};
const DOC_FONT = { italic: true, color: { argb: "FF475569" }, size: 9 };
const NEW_FILL = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FFEEF2FF" }, // indigo-50
};
function styleNewHeader(cell, label) {
cell.value = label;
cell.fill = HEADER_FILL;
cell.font = HEADER_FONT;
cell.alignment = { wrapText: true, vertical: "middle" };
cell.border = { bottom: { style: "thin", color: { argb: "FF6366F1" } } };
}
function styleDocCell(cell, value) {
cell.value = value;
cell.fill = DOC_FILL;
cell.font = DOC_FONT;
cell.alignment = { wrapText: true, vertical: "top" };
}
function styleDataCell(cell, value) {
cell.value = value;
cell.fill = NEW_FILL;
cell.font = { size: 10 };
cell.alignment = { wrapText: false, vertical: "middle" };
}
// ─── Main ─────────────────────────────────────────────────────────────────────
async function main() {
const workbook = new ExcelJS.Workbook();
await workbook.xlsx.readFile(EXCEL_PATH);
// ─── EID_Informationen ───────────────────────────────────────────────────
const eidSheet = workbook.getWorksheet("EID_Informationen");
if (!eidSheet) throw new Error("Sheet EID_Informationen not found");
// Add a "Documentation" row 2 below headers explaining each column
// First, let's add new columns to the right: N, O, P, Q
const eidLastCol = 14; // N
// Column N: Display Name
// Column O: Email
// Column P: Skills (derived)
// Column Q: Description / Notes for Planarchy
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 Planarchy.",
},
{
col: 15, // O
header: "Email\n(generated)",
doc: "Generated email: firstname.lastname@planarchy.example. Required unique field in Planarchy. 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 Planarchy with proficiency 1-5. Senior (LCR ≥ 118) → 5, Mid-Senior (LCR ≥ 95) → 4, Mid → 3.",
},
{
col: 17, // Q
header: "Planarchy Notes",
doc: "How data maps to Planarchy:\n• EID = unique key (col A)\n• Chapter = chapter field\n• LCR / UCR → multiply by 100 for integer cents (€85.00 → 8500)\n• Hours fraction × 8 = daily availability hours\n• Chargeability → multiply by 100 for % (0.75 → 75%)\n• Employee type, City, Client Unit → stored in dynamicFields JSONB",
},
];
// Add row 1 column headers for new columns
const eidHeaderRow = eidSheet.getRow(1);
for (const { col, header } of newEidCols) {
styleNewHeader(eidSheet.getCell(1, col), header);
}
// Add documentation row after header (insert at row 2, shifting data down)
eidSheet.spliceRows(2, 0, []); // insert empty row
const eidDocRow = eidSheet.getRow(2);
eidDocRow.height = 55;
// Add doc text to new columns
for (const { col, doc } of newEidCols) {
styleDocCell(eidSheet.getCell(2, col), doc);
}
// Style existing header cells for doc row
for (let c = 1; c <= 13; c++) {
const cell = eidSheet.getCell(2, c);
cell.fill = DOC_FILL;
cell.font = DOC_FONT;
cell.alignment = { wrapText: true, vertical: "top" };
}
eidSheet.getCell(2, 1).value = "← Column documentation (row 2)";
// Now fill data rows (row 3 onward = data rows 1-36)
for (let r = 3; r <= 38; r++) {
const row = eidSheet.getRow(r);
const eid = row.getCell(1).value?.toString().trim();
if (!eid) continue;
const chapter = row.getCell(2).value?.toString().trim() ?? "";
const typeOfWork = row.getCell(3).value?.toString().trim() ?? "";
const lcr = parseFloat(row.getCell(8).value?.toString() ?? "0");
styleDataCell(eidSheet.getCell(r, 14), toDisplayName(eid));
styleDataCell(eidSheet.getCell(r, 15), toEmail(eid));
styleDataCell(eidSheet.getCell(r, 16), computeSkillLabel(chapter, typeOfWork));
if (r === 3) {
styleDataCell(eidSheet.getCell(r, 17), "See header row notes →");
}
}
// Set column widths
eidSheet.getColumn(14).width = 22;
eidSheet.getColumn(15).width = 34;
eidSheet.getColumn(16).width = 36;
eidSheet.getColumn(17).width = 50;
// Also add doc notes to existing header columns A-M in row 1
const eidExistingDocs = [
"Unique identifier. Used as EID in Planarchy (no EMP-XXX prefix needed). e.g. steve.rogers",
"Team / department. Maps to 'chapter' field in Planarchy.",
"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 Planarchy 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 Planarchy % (0.75 → 75%).",
"(unused)",
];
for (let c = 1; c <= 13; c++) {
const doc = eidExistingDocs[c - 1];
if (doc) {
eidSheet.getCell(2, c).value = doc;
}
}
// ─── Projektinfomartionen ────────────────────────────────────────────────
const projSheet = workbook.getWorksheet("Projektinfomartionen");
if (!projSheet) throw new Error("Sheet Projektinfomartionen not found");
const newProjCols = [
{
col: 17, // Q
header: "Start Date\n(synthesized)",
doc: "Synthesized project start date (YYYY-MM-DD). Source Excel had no dates — spread across 2024-2027 based on win probability and project order.",
},
{
col: 18, // R
header: "End Date\n(synthesized)",
doc: "Synthesized end date (YYYY-MM-DD). Duration based on resource costs planned and person hours. Ranges from 3-12 months.",
},
{
col: 19, // S
header: "Status\n(derived)",
doc: "Planarchy 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: "Planarchy Notes",
doc: "How data maps to Planarchy:\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 Planarchy\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",
},
];
// Add header row columns
const projHeaderRow = projSheet.getRow(1);
for (const { col, header } of newProjCols) {
styleNewHeader(projSheet.getCell(1, col), header);
}
// Insert documentation row 2
projSheet.spliceRows(2, 0, []);
const projDocRow = projSheet.getRow(2);
projDocRow.height = 55;
for (const { col, doc } of newProjCols) {
styleDocCell(projSheet.getCell(2, col), doc);
}
projSheet.getCell(2, 1).value = "← Column documentation (row 2)";
for (let c = 1; c <= 16; c++) {
const cell = projSheet.getCell(2, c);
cell.fill = DOC_FILL;
cell.font = DOC_FONT;
cell.alignment = { wrapText: true, vertical: "top" };
}
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 Planarchy 'name' field.",
"Short internal project code (5-6 chars) → maps to Planarchy '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 Planarchy OrderType.",
"Win probability 0-100. Used in Planarchy 'winProbability' field for pipeline forecasting.",
"Allocation type: Internal → INT, External → EXT. Maps to Planarchy 'allocationType' field.",
"Chargeability % as decimal. Stored in dynamicFields.chargeabilityPercent.",
"Planned resource cost in EUR. Multiply × 100 for Planarchy 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 Planarchy.",
"Day project sold (empty in source). Could be stored as dynamicFields.dateSold.",
"Project start date (empty in source → synthesized in col Q). Maps to Planarchy 'startDate'.",
"Project end date (empty in source → synthesized in col R). Maps to Planarchy 'endDate'.",
"Confidentiality: Confidential / Not Confidential. Stored in dynamicFields.classification.",
"Responsible EID / Owner (empty in source). Would map to a PM allocation in Planarchy.",
];
for (let c = 1; c <= 16; c++) {
const doc = projExistingDocs[c - 1];
if (doc) projSheet.getCell(2, c).value = doc;
}
// Fill data rows
for (let r = 3; r <= 38; r++) {
const row = projSheet.getRow(r);
const shortCode = row.getCell(3).value?.toString().trim();
if (!shortCode) continue;
const dates = PROJECT_DATES[shortCode];
if (dates) {
styleDataCell(projSheet.getCell(r, 17), dates[0]);
styleDataCell(projSheet.getCell(r, 18), dates[1]);
styleDataCell(projSheet.getCell(r, 19), dates[2]);
}
if (r === 3) {
styleDataCell(projSheet.getCell(r, 20), "See header row notes →");
}
}
projSheet.getColumn(17).width = 18;
projSheet.getColumn(18).width = 18;
projSheet.getColumn(19).width = 16;
projSheet.getColumn(20).width = 50;
// ─── Add a "Planarchy Data Model" sheet ──────────────────────────────────
let modelSheet = workbook.getWorksheet("Planarchy Data Model");
if (!modelSheet) {
modelSheet = workbook.addWorksheet("Planarchy Data Model");
}
const modelData = [
["Planarchy Data Model — Field Reference", "", "", "", ""],
["", "", "", "", ""],
["RESOURCE FIELDS", "", "", "", ""],
["Field", "Type", "Required", "Example", "Description"],
["eid", "String (unique)", "Yes", "steve.rogers", "Employee identifier. Must be unique. Shown in UI as EID badge."],
["displayName", "String", "Yes", "Steve Rogers", "Full display name shown in all views."],
["email", "String (unique)", "Yes", "steve.rogers@company.com", "Unique email address. Used for login if resource is also a user."],
["chapter", "String", "No", "Project Management", "Team or department. Used for grouping in dashboard and filters."],
["lcrCents", "Integer (cents)", "Yes", "13377", "Loaded Cost Rate in euro-cents (€/h × 100). e.g. €133.77/h → 13377"],
["ucrCents", "Integer (cents)", "Yes", "7465", "Unloaded/Utilization Cost Rate in euro-cents. e.g. €74.65/h → 7465"],
["currency", "String", "Yes", "EUR", "Currency code for LCR/UCR. Default EUR."],
["chargeabilityTarget", "Float (0-100)", "Yes", "75", "Target chargeability percentage. 75 = 75% of available hours should be chargeable."],
["availability", "JSON", "Yes", "{monday:8, ...}", "Available hours per weekday. Partial days for part-time. e.g. 4h = half day."],
["skills", "JSON Array", "No", "[{skill:'Unreal Engine', proficiency:4}]", "Skill entries with proficiency 1(Junior)5(Expert). Optional category and yearsExperience."],
["isActive", "Boolean", "Yes", "true", "Active resources appear in planning. Deactivated resources are hidden but kept in history."],
["dynamicFields", "JSON", "No", "{city:'Stuttgart', ...}", "Additional fields defined by Blueprint (e.g. city, clientUnit, workType, employeeType)."],
["", "", "", "", ""],
["PROJECT FIELDS", "", "", "", ""],
["Field", "Type", "Required", "Example", "Description"],
["shortCode", "String (unique)", "Yes", "STTHG", "Short internal project code. Unique key. Shown as monospace badge in UI."],
["name", "String", "Yes", "Spider-Man: Homecoming (2017)", "Full project name."],
["orderType", "Enum", "Yes", "CHARGEABLE", "BD = Business Development, CHARGEABLE = billable client work, INTERNAL = internal, OVERHEAD = overhead."],
["allocationType", "Enum", "Yes", "EXT", "INT = Internal allocation (internal team), EXT = External (client-facing)."],
["winProbability", "Integer (0-100)", "Yes", "100", "Probability this project proceeds. Used in pipeline reports. 100 = confirmed."],
["budgetCents", "Integer (cents)", "Yes", "6789900", "Total project budget in euro-cents. e.g. €67,899 → 6789900. Used for utilization tracking."],
["startDate", "Date", "Yes", "2025-04-01", "Project start date (ISO format)."],
["endDate", "Date", "Yes", "2026-01-31", "Project end date (ISO format)."],
["status", "Enum", "Yes", "ACTIVE", "DRAFT / ACTIVE / ON_HOLD / COMPLETED / CANCELLED. Drives timeline filtering."],
["staffingReqs", "JSON Array", "No", "[{role:'3D Artist', fteCount:2}]", "Staffing requirements. Used in Demand View widget to show gaps vs actual allocations."],
["dynamicFields", "JSON", "No", "{clientUnit:'[DAI]', ...}", "Blueprint fields: clientUnit, personHoursSold, classification, etc."],
["", "", "", "", ""],
["ALLOCATION FIELDS", "", "", "", ""],
["Field", "Type", "Required", "Example", "Description"],
["resourceId", "String (FK)", "Yes", "(cuid)", "Reference to Resource.id"],
["projectId", "String (FK)", "Yes", "(cuid)", "Reference to Project.id"],
["startDate", "Date", "Yes", "2025-04-01", "Start of allocation (often same as project start)."],
["endDate", "Date", "Yes", "2026-01-31", "End of allocation (can be partial project duration)."],
["hoursPerDay", "Float", "Yes", "8", "Hours per working day. 8 = full day. Can exceed 8 (overtime, shown in amber)."],
["percentage", "Float (0-100)", "Yes", "100", "Allocation percentage (100 = full-time, 50 = half-time)."],
["role", "String", "Yes", "Project Manager", "Role on this project (free text)."],
["dailyCostCents", "Integer (cents)", "Yes", "106960", "LCR × hoursPerDay × 100. Computed automatically on create."],
["status", "Enum", "Yes", "ACTIVE", "PROPOSED / CONFIRMED / ACTIVE / COMPLETED / CANCELLED"],
];
modelSheet.addRows(modelData);
// Style the model sheet
modelSheet.getColumn(1).width = 28;
modelSheet.getColumn(2).width = 22;
modelSheet.getColumn(3).width = 12;
modelSheet.getColumn(4).width = 35;
modelSheet.getColumn(5).width = 72;
// Style title row
const titleCell = modelSheet.getCell(1, 1);
titleCell.font = { bold: true, size: 14, color: { argb: "FF4F46E5" } };
titleCell.value = "Planarchy Data Model — Field Reference";
// Style section headers and field headers
const sectionRows = [3, 17, 31];
const fieldHeaderRows = [4, 18, 32];
for (const r of sectionRows) {
for (let c = 1; c <= 5; c++) {
const cell = modelSheet.getCell(r, c);
cell.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "FF4F46E5" } };
cell.font = { bold: true, color: { argb: "FFFFFFFF" }, size: 11 };
}
}
for (const r of fieldHeaderRows) {
for (let c = 1; c <= 5; c++) {
const cell = modelSheet.getCell(r, c);
cell.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "FFE0E7FF" } };
cell.font = { bold: true, color: { argb: "FF3730A3" }, size: 10 };
}
}
// ─── Save ────────────────────────────────────────────────────────────────
await workbook.xlsx.writeFile(EXCEL_PATH);
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: 'Planarchy Data Model' (field reference)");
}
main().catch((err) => {
console.error(err);
process.exit(1);
});