chore(repo): initialize planarchy workspace
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user