chore(repo): initialize planarchy workspace

This commit is contained in:
2026-03-14 14:31:09 +01:00
commit dd55d0e78b
769 changed files with 166461 additions and 0 deletions
@@ -0,0 +1,206 @@
import { DispoStagedRecordType } from "@planarchy/db";
import { DISPO_CHARGEABILITY_SHEET, type ParsedChargeabilityResource, type ParsedChargeabilityWorkbook, type ParsedUnresolvedRecord, buildFallbackAccentureEmail, createAvailabilityFromFte, deriveCountryCodeFromMetroCity, deriveDisplayNameFromEnterpriseId, deriveNormalizedChapter, deriveRoleTokens, ensurePercentageValue, mapChargeabilityResourceType, normalizeNullableWorkbookValue, normalizeText, resolveCanonicalEnterpriseIdentity } from "./shared.js";
import { readWorksheetMatrix } from "./read-workbook.js";
const CHGFC_HEADERS = {
clientUnit: "MV Client Unit",
enterpriseId: "Enterprise ID",
fte: "FTE",
managementLevelGroup: "Management Level Group",
metroCity: "Metro City",
orgUnitLevel6: "Org Unit Level 6",
rawChapter: "MV Org Unit 1 / Chapter",
rawResourceType: "MV Ressource Type",
target: "Target (per Level)",
} as const;
function buildHeaderMap(headerRow: ReadonlyArray<unknown>): Map<string, number> {
const headerMap = new Map<string, number>();
headerRow.forEach((value, index) => {
const normalized = normalizeText(value);
if (normalized) {
headerMap.set(normalized, index);
}
});
return headerMap;
}
function getCellValue(
row: ReadonlyArray<unknown>,
headerMap: Map<string, number>,
headerName: string,
): unknown {
const index = headerMap.get(headerName);
if (index === undefined) {
return null;
}
return row[index] ?? null;
}
function buildResourceSignature(resource: ParsedChargeabilityResource): string {
return JSON.stringify({
chapter: resource.chapter,
chapterCode: resource.chapterCode,
chargeabilityTarget: resource.chargeabilityTarget,
clientUnitName: resource.clientUnitName,
countryCode: resource.countryCode,
fte: resource.fte,
managementLevelGroupName: resource.managementLevelGroupName,
metroCityName: resource.metroCityName,
resourceType: resource.resourceType,
roleTokens: resource.roleTokens,
});
}
export async function parseDispoChargeabilityWorkbook(
workbookPath: string,
): Promise<ParsedChargeabilityWorkbook> {
const rows = await readWorksheetMatrix(workbookPath, DISPO_CHARGEABILITY_SHEET);
const headerMap = buildHeaderMap(rows[0] ?? []);
const warnings: string[] = [];
const unresolved: ParsedUnresolvedRecord[] = [];
const resourceByCanonicalId = new Map<string, ParsedChargeabilityResource>();
for (let rowNumber = 2; rowNumber <= rows.length; rowNumber += 1) {
const row = rows[rowNumber - 1] ?? [];
const enterpriseIdValue = normalizeNullableWorkbookValue(
getCellValue(row, headerMap, CHGFC_HEADERS.enterpriseId),
);
if (!enterpriseIdValue) {
if (row.some((value) => normalizeText(value) !== null)) {
unresolved.push({
sourceRow: rowNumber,
sourceColumn: "A",
recordType: DispoStagedRecordType.RESOURCE,
resourceExternalId: null,
message: "Missing Enterprise ID in ChgFC row",
resolutionHint: "Populate Enterprise ID before staging resource data",
warnings: [],
normalizedData: {},
});
}
continue;
}
const canonicalExternalId = resolveCanonicalEnterpriseIdentity(enterpriseIdValue);
if (!canonicalExternalId) {
unresolved.push({
sourceRow: rowNumber,
sourceColumn: "A",
recordType: DispoStagedRecordType.RESOURCE,
resourceExternalId: enterpriseIdValue,
message: `Unable to normalize Enterprise ID "${enterpriseIdValue}"`,
resolutionHint: "Validate Enterprise ID formatting in ChgFC",
warnings: [],
normalizedData: {
enterpriseId: enterpriseIdValue,
},
});
continue;
}
const managementLevelGroupName = normalizeNullableWorkbookValue(
getCellValue(row, headerMap, CHGFC_HEADERS.managementLevelGroup),
);
const rawTarget = getCellValue(row, headerMap, CHGFC_HEADERS.target);
const fte = typeof getCellValue(row, headerMap, CHGFC_HEADERS.fte) === "number"
? Number(getCellValue(row, headerMap, CHGFC_HEADERS.fte))
: null;
const metroCityName = normalizeNullableWorkbookValue(
getCellValue(row, headerMap, CHGFC_HEADERS.metroCity),
);
const rawResourceType = normalizeNullableWorkbookValue(
getCellValue(row, headerMap, CHGFC_HEADERS.rawResourceType),
);
const levelSixName = normalizeNullableWorkbookValue(
getCellValue(row, headerMap, CHGFC_HEADERS.orgUnitLevel6),
);
const rawChapter = normalizeNullableWorkbookValue(
getCellValue(row, headerMap, CHGFC_HEADERS.rawChapter),
);
const clientUnitName = normalizeNullableWorkbookValue(
getCellValue(row, headerMap, CHGFC_HEADERS.clientUnit),
);
const roleTokens = deriveRoleTokens(levelSixName, rawChapter);
const normalizedChapter = deriveNormalizedChapter(rawChapter, roleTokens);
const resourceTypeResult = mapChargeabilityResourceType(rawResourceType);
const recordWarnings = resourceTypeResult.warning ? [resourceTypeResult.warning] : [];
const chargeabilityTarget =
typeof rawTarget === "number" ? ensurePercentageValue(rawTarget) : null;
const resource: ParsedChargeabilityResource = {
sourceRow: rowNumber,
canonicalExternalId,
enterpriseId: canonicalExternalId,
eid: canonicalExternalId,
displayName: deriveDisplayNameFromEnterpriseId(canonicalExternalId),
email: buildFallbackAccentureEmail(canonicalExternalId),
chapter: normalizedChapter.chapter,
chapterCode: normalizedChapter.chapterCode,
managementLevelGroupName,
managementLevelName: null,
countryCode: deriveCountryCodeFromMetroCity(metroCityName),
metroCityName,
clientUnitName,
rawResourceType,
resourceType: resourceTypeResult.resourceType,
chargeabilityTarget,
fte,
availability: createAvailabilityFromFte(fte),
roleTokens,
warnings: recordWarnings,
};
const existing = resourceByCanonicalId.get(canonicalExternalId);
if (!existing) {
resourceByCanonicalId.set(canonicalExternalId, resource);
continue;
}
const existingSignature = buildResourceSignature(existing);
const nextSignature = buildResourceSignature(resource);
if (existingSignature === nextSignature) {
existing.warnings.push(`Duplicate ChgFC row ${rowNumber} ignored for ${canonicalExternalId}`);
continue;
}
existing.warnings.push(`Conflicting duplicate ChgFC row ${rowNumber} found for ${canonicalExternalId}`);
unresolved.push({
sourceRow: rowNumber,
sourceColumn: "A",
recordType: DispoStagedRecordType.RESOURCE,
resourceExternalId: canonicalExternalId,
message: `Conflicting resource roster rows found for ${canonicalExternalId}`,
resolutionHint: "Resolve the differing ChgFC roster values before commit",
warnings: [...recordWarnings],
normalizedData: {
existing: {
sourceRow: existing.sourceRow,
chapter: existing.chapter,
clientUnitName: existing.clientUnitName,
fte: existing.fte,
metroCityName: existing.metroCityName,
},
conflicting: {
sourceRow: resource.sourceRow,
chapter: resource.chapter,
clientUnitName: resource.clientUnitName,
fte: resource.fte,
metroCityName: resource.metroCityName,
},
},
});
}
return {
resources: Array.from(resourceByCanonicalId.values()),
unresolved,
warnings,
};
}