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): Map { const headerMap = new Map(); headerRow.forEach((value, index) => { const normalized = normalizeText(value); if (normalized) { headerMap.set(normalized, index); } }); return headerMap; } function getCellValue( row: ReadonlyArray, headerMap: Map, 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 { const rows = await readWorksheetMatrix(workbookPath, DISPO_CHARGEABILITY_SHEET); const headerMap = buildHeaderMap(rows[0] ?? []); const warnings: string[] = []; const unresolved: ParsedUnresolvedRecord[] = []; const resourceByCanonicalId = new Map(); 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, }; }