import type { WeekdayAvailability } from "@planarchy/shared"; import { createWeekdayAvailabilityFromFte, normalizeDispoRoleToken, } from "@planarchy/shared"; import type { TxClient, MergedStagedResource } from "./commit-dispo-batch-types.js"; import { deriveRoleTokens } from "./shared.js"; function asNullableString(value: unknown): string | null { return typeof value === "string" && value.trim().length > 0 ? value : null; } function normalizeFallbackRoleName(value: string): string { return value.replace(/\s+/g, " ").trim(); } function isFiniteNumber(value: unknown): value is number { return typeof value === "number" && Number.isFinite(value); } function asObject(value: unknown): Record { return value && typeof value === "object" && !Array.isArray(value) ? (value as Record) : {}; } function mergeScalar( current: T | null, incoming: T | null | undefined, ): T | null { return incoming ?? current; } export function inferRoleNameFromResource(resource: MergedStagedResource): string | null { const explicitRoleName = Array.from(resource.roleTokens) .map((token) => normalizeDispoRoleToken(token)) .find((roleName): roleName is string => Boolean(roleName)); if (explicitRoleName) { return explicitRoleName; } const derivedRoleName = deriveRoleTokens( resource.chapter, asNullableString(resource.rawPayload.department), asNullableString(resource.rawPayload.mainSkillset), asNullableString(resource.rawPayload.sapOrgUnitLevelSix), asNullableString(resource.rawPayload.sapOrgUnitLevelSeven), asNullableString(resource.rawPayload.sapEmployeeName), ) .map((token) => normalizeDispoRoleToken(token)) .find((roleName): roleName is string => Boolean(roleName)); if (derivedRoleName) { return derivedRoleName; } if (resource.chapter === "Art Direction") { return "Art Director"; } if (resource.chapter === "Project Management") { return "Project Manager"; } const fallbackRoleLabel = asNullableString(resource.rawPayload.department) ?? asNullableString(resource.rawPayload.mainSkillset) ?? asNullableString(resource.chapter) ?? asNullableString(resource.rawPayload.sapOrgUnitLevelSeven) ?? asNullableString(resource.rawPayload.sapOrgUnitLevelSix); return fallbackRoleLabel ? normalizeFallbackRoleName(fallbackRoleLabel) : null; } export function mergeStagedResources( rows: Awaited>, ): Map { const sourcePriority = new Map([ ["CHARGEABILITY", 1], ["ROSTER", 2], ]); const ordered = [...rows].sort( (left, right) => (sourcePriority.get(left.sourceKind) ?? 0) - (sourcePriority.get(right.sourceKind) ?? 0), ); const merged = new Map(); for (const row of ordered) { const existing = merged.get(row.canonicalExternalId); const rawPayload = asObject(row.rawPayload); if (!existing) { merged.set(row.canonicalExternalId, { availability: row.availability ?? null, canonicalExternalId: row.canonicalExternalId, chapter: row.chapter ?? null, chargeabilityTarget: row.chargeabilityTarget ?? null, clientUnitName: row.clientUnitName ?? null, countryCode: row.countryCode ?? null, displayName: row.displayName ?? null, email: row.email ?? null, fte: row.fte ?? null, lcrCents: row.lcrCents ?? null, managementLevelGroupName: row.managementLevelGroupName ?? null, managementLevelName: row.managementLevelName ?? null, metroCityName: row.metroCityName ?? null, rawPayload, resourceType: row.resourceType ?? null, roleTokens: new Set(row.roleTokens), sourceKinds: [row.sourceKind], ucrCents: row.ucrCents ?? null, vacationDaysPerYear: isFiniteNumber(rawPayload.vacationDaysPerYear) ? rawPayload.vacationDaysPerYear : null, warnings: [...row.warnings], }); continue; } existing.availability = mergeScalar(existing.availability, row.availability); existing.chapter = mergeScalar(existing.chapter, row.chapter); existing.chargeabilityTarget = mergeScalar(existing.chargeabilityTarget, row.chargeabilityTarget); existing.clientUnitName = mergeScalar(existing.clientUnitName, row.clientUnitName); existing.countryCode = mergeScalar(existing.countryCode, row.countryCode); existing.displayName = mergeScalar(existing.displayName, row.displayName); existing.email = mergeScalar(existing.email, row.email); existing.fte = mergeScalar(existing.fte, row.fte); existing.lcrCents = mergeScalar(existing.lcrCents, row.lcrCents); existing.managementLevelGroupName = mergeScalar( existing.managementLevelGroupName, row.managementLevelGroupName, ); existing.managementLevelName = mergeScalar(existing.managementLevelName, row.managementLevelName); existing.metroCityName = mergeScalar(existing.metroCityName, row.metroCityName); existing.resourceType = mergeScalar(existing.resourceType, row.resourceType); existing.ucrCents = mergeScalar(existing.ucrCents, row.ucrCents); if (existing.availability === null && row.availability !== null) { existing.availability = row.availability; } for (const roleToken of row.roleTokens) { existing.roleTokens.add(roleToken); } existing.sourceKinds.push(row.sourceKind); existing.warnings.push(...row.warnings); existing.rawPayload = { ...existing.rawPayload, ...rawPayload, }; if (isFiniteNumber(rawPayload.vacationDaysPerYear)) { existing.vacationDaysPerYear = rawPayload.vacationDaysPerYear; } } return merged; } export function parseWeekdayAvailability( value: unknown, fallbackFte: number | null, ): WeekdayAvailability { const fallback = createWeekdayAvailabilityFromFte(fallbackFte ?? 1); const source = asObject(value); return { monday: isFiniteNumber(source.monday) ? source.monday : fallback.monday, tuesday: isFiniteNumber(source.tuesday) ? source.tuesday : fallback.tuesday, wednesday: isFiniteNumber(source.wednesday) ? source.wednesday : fallback.wednesday, thursday: isFiniteNumber(source.thursday) ? source.thursday : fallback.thursday, friday: isFiniteNumber(source.friday) ? source.friday : fallback.friday, }; } export interface ReferenceDataMaps { clientIdByCode: Map; clientIdByName: Map; countryIdByCode: Map; managementLevelGroupByName: Map; managementLevelIdByName: Map; metroCityIdByName: Map; orgUnitIdByLevelAndName: Map; roleIdByName: Map; utilizationCategoryIdByCode: Map; } export function buildReferenceDataMaps(data: { clients: { id: string; code: string | null; name: string }[]; countries: { id: string; code: string }[]; managementLevelGroups: { id: string; name: string; targetPercentage: number }[]; managementLevels: { id: string; name: string }[]; metroCities: { id: string; name: string }[]; orgUnits: { id: string; level: number; name: string }[]; roles: { id: string; name: string }[]; utilizationCategories: { id: string; code: string }[]; }): ReferenceDataMaps { return { clientIdByCode: new Map( data.clients.filter((client) => client.code).map((client) => [client.code!, client.id]), ), clientIdByName: new Map( data.clients.map((client) => [client.name.toLowerCase(), client.id]), ), countryIdByCode: new Map( data.countries.map((country) => [country.code, country.id]), ), managementLevelGroupByName: new Map( data.managementLevelGroups.map((group) => [group.name, group]), ), managementLevelIdByName: new Map( data.managementLevels.map((level) => [level.name, level.id]), ), metroCityIdByName: new Map( data.metroCities.map((metroCity) => [metroCity.name.toLowerCase(), metroCity.id]), ), orgUnitIdByLevelAndName: new Map( data.orgUnits.map((orgUnit) => [`${orgUnit.level}:${orgUnit.name.toLowerCase()}`, orgUnit.id]), ), roleIdByName: new Map( data.roles.map((role) => [role.name, role.id]), ), utilizationCategoryIdByCode: new Map( data.utilizationCategories.map((category) => [category.code, category.id]), ), }; }