chore(repo): initialize planarchy workspace
This commit is contained in:
@@ -0,0 +1,570 @@
|
||||
import { DispoStagedRecordType, ResourceType } from "@planarchy/db";
|
||||
import { createWeekdayAvailabilityFromFte } from "@planarchy/shared";
|
||||
import {
|
||||
parseResourceRosterMasterWorkbook,
|
||||
type ParsedResourceRosterLevelAverage,
|
||||
type ParsedResourceRosterMasterWorkbook,
|
||||
type ParsedResourceRosterRate,
|
||||
} from "./parse-resource-roster-master-workbook.js";
|
||||
import {
|
||||
DISPO_ROSTER_SAP_SHEET,
|
||||
DISPO_ROSTER_SHEET,
|
||||
type ParsedRosterResource,
|
||||
type ParsedRosterWorkbook,
|
||||
type ParsedUnresolvedRecord,
|
||||
buildFallbackAccentureEmail,
|
||||
deriveCountryCodeFromMetroCity,
|
||||
deriveDisplayNameFromEnterpriseId,
|
||||
deriveNormalizedChapter,
|
||||
deriveRoleTokens,
|
||||
isPseudoDemandResourceIdentity,
|
||||
mapChargeabilityResourceType,
|
||||
normalizeNullableWorkbookValue,
|
||||
normalizeText,
|
||||
resolveCanonicalEnterpriseIdentity,
|
||||
} from "./shared.js";
|
||||
import { readWorksheetMatrix } from "./read-workbook.js";
|
||||
|
||||
const ROSTER_HEADERS = {
|
||||
clientUnit: "MV Client Unit",
|
||||
dailyWorkingHoursPerFte: "Daily Working Hours/FTE",
|
||||
department: "MV Org Unit 2 / Department",
|
||||
eid: "EID",
|
||||
firstDayInDispo: "First day in dispo",
|
||||
fte: "FTE",
|
||||
lastDayInDispo: "Last day in dispo",
|
||||
mainSkillset: "MV Main Skillset",
|
||||
managementLevel: "Management Level",
|
||||
managementLevelGroup: "Management Level Group",
|
||||
metroCity: "Metro City",
|
||||
rawChapter: "MV Org Unit 1 / Chapter",
|
||||
rawResourceType: "MV Ressource Type",
|
||||
resourceHoursPerWeek: "Resource Hours/Week",
|
||||
vacationDaysPerYear: "Vacation days / year",
|
||||
} as const;
|
||||
|
||||
const SAP_HEADERS = {
|
||||
employeeEmail: "Employee Email",
|
||||
employeeName: "Employee Name",
|
||||
enterpriseId: "Enterprise ID",
|
||||
fte: "FTE",
|
||||
managementLevel: "Management Level",
|
||||
managementLevelGroup: "Management Level Group",
|
||||
metroCity: "Metro City",
|
||||
orgUnitLevel5: "Org Unit Level 5",
|
||||
orgUnitLevel6: "Org Unit Level 6",
|
||||
orgUnitLevel7: "Org Unit Level 7",
|
||||
} as const;
|
||||
|
||||
interface RosterSourceRow {
|
||||
canonicalExternalId: string;
|
||||
clientUnitName: string | null;
|
||||
dailyWorkingHoursPerFte: number | null;
|
||||
department: string | null;
|
||||
fte: number | null;
|
||||
mainSkillset: string | null;
|
||||
managementLevelGroupName: string | null;
|
||||
managementLevelName: string | null;
|
||||
metroCityName: string | null;
|
||||
rawChapter: string | null;
|
||||
rawResourceType: string | null;
|
||||
resourceHoursPerWeek: number | null;
|
||||
rowNumber: number;
|
||||
vacationDaysPerYear: number | null;
|
||||
firstDayInDispo: Date | null;
|
||||
lastDayInDispo: Date | null;
|
||||
}
|
||||
|
||||
interface SapSourceRow {
|
||||
canonicalExternalId: string;
|
||||
employeeEmail: string | null;
|
||||
employeeName: string | null;
|
||||
fte: number | null;
|
||||
managementLevelGroupName: string | null;
|
||||
managementLevelName: string | null;
|
||||
metroCityName: string | null;
|
||||
orgUnitLevelFive: string | null;
|
||||
orgUnitLevelSix: string | null;
|
||||
orgUnitLevelSeven: string | null;
|
||||
rowNumber: number;
|
||||
}
|
||||
|
||||
interface ParseDispoRosterWorkbookOptions {
|
||||
costWorkbookPath?: string;
|
||||
}
|
||||
|
||||
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 parseOptionalNumber(value: unknown): number | null {
|
||||
if (typeof value === "number" && Number.isFinite(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
const normalized = normalizeNullableWorkbookValue(value);
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsed = Number(normalized.replace(",", "."));
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
function parseOptionalDate(value: unknown): Date | null {
|
||||
if (value instanceof Date && !Number.isNaN(value.valueOf())) {
|
||||
return value;
|
||||
}
|
||||
|
||||
const normalized = normalizeNullableWorkbookValue(value);
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsed = new Date(normalized);
|
||||
return Number.isNaN(parsed.valueOf()) ? null : parsed;
|
||||
}
|
||||
|
||||
function normalizeSapDisplayName(value: string | null): string | null {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalized = value
|
||||
.split(",")
|
||||
.map((part) => normalizeText(part))
|
||||
.filter((part): part is string => Boolean(part));
|
||||
|
||||
if (normalized.length === 2) {
|
||||
return `${normalized[1]} ${normalized[0]}`;
|
||||
}
|
||||
|
||||
return normalizeText(value);
|
||||
}
|
||||
|
||||
function buildResourceWarnings(
|
||||
resourceType: ResourceType,
|
||||
resourceTypeWarning: string | null,
|
||||
roster: RosterSourceRow | null,
|
||||
sap: SapSourceRow | null,
|
||||
): string[] {
|
||||
const warnings: string[] = [];
|
||||
|
||||
if (resourceTypeWarning) {
|
||||
warnings.push(resourceTypeWarning);
|
||||
}
|
||||
if (!roster) {
|
||||
warnings.push("Missing DispoRoster row; resource imported from SAP_data only");
|
||||
}
|
||||
if (!sap) {
|
||||
warnings.push("Missing SAP_data row; email and display name fall back to derived values");
|
||||
}
|
||||
if (resourceType === ResourceType.FREELANCER && !roster?.dailyWorkingHoursPerFte) {
|
||||
warnings.push("Freelancer row has no daily working hours value; defaulting to 8h/day");
|
||||
}
|
||||
|
||||
return warnings;
|
||||
}
|
||||
|
||||
function shouldExcludeImportedResource(resource: {
|
||||
canonicalExternalId: string;
|
||||
sourceEmail: string | null;
|
||||
managementLevelName: string | null;
|
||||
}) {
|
||||
return !resource.sourceEmail && !resource.managementLevelName;
|
||||
}
|
||||
|
||||
function applyRateResolution(input: {
|
||||
canonicalExternalId: string;
|
||||
level: string | null;
|
||||
rateRecord: ParsedResourceRosterRate | null;
|
||||
levelAverage: ParsedResourceRosterLevelAverage | null;
|
||||
warnings: string[];
|
||||
}) {
|
||||
const { canonicalExternalId, level, rateRecord, levelAverage, warnings } = input;
|
||||
|
||||
const exactLcr = rateRecord?.lcrCents ?? null;
|
||||
const exactUcr = rateRecord?.ucrCents ?? null;
|
||||
const fallbackLcr = levelAverage?.lcrCents ?? null;
|
||||
const fallbackUcr = levelAverage?.ucrCents ?? null;
|
||||
|
||||
const lcrCents = exactLcr ?? fallbackLcr;
|
||||
const ucrCents = exactUcr ?? fallbackUcr;
|
||||
|
||||
if (!rateRecord) {
|
||||
if (levelAverage && lcrCents !== null && ucrCents !== null) {
|
||||
warnings.push(
|
||||
`Applied level-average rates for ${canonicalExternalId} using management level ${levelAverage.level}`,
|
||||
);
|
||||
return {
|
||||
lcrCents,
|
||||
ucrCents,
|
||||
rateResolution: "LEVEL_AVERAGE" as const,
|
||||
rateResolutionLevel: levelAverage.level,
|
||||
};
|
||||
}
|
||||
|
||||
warnings.push(
|
||||
level
|
||||
? `Missing rate row for ${canonicalExternalId}; no usable level-average rate found for ${level}`
|
||||
: `Missing rate row for ${canonicalExternalId}; management level unavailable for fallback`,
|
||||
);
|
||||
return {
|
||||
lcrCents,
|
||||
ucrCents,
|
||||
rateResolution: "MISSING" as const,
|
||||
rateResolutionLevel: levelAverage?.level ?? level ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
if (exactLcr !== null && exactUcr !== null) {
|
||||
return {
|
||||
lcrCents,
|
||||
ucrCents,
|
||||
rateResolution: "EXACT" as const,
|
||||
rateResolutionLevel: rateRecord.level ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
if (levelAverage && lcrCents !== null && ucrCents !== null) {
|
||||
warnings.push(
|
||||
`Completed incomplete rate row for ${canonicalExternalId} with level-average rates from ${levelAverage.level}`,
|
||||
);
|
||||
return {
|
||||
lcrCents,
|
||||
ucrCents,
|
||||
rateResolution: "LEVEL_AVERAGE" as const,
|
||||
rateResolutionLevel: levelAverage.level,
|
||||
};
|
||||
}
|
||||
|
||||
warnings.push(`Incomplete rate row for ${canonicalExternalId} could not be fully resolved`);
|
||||
return {
|
||||
lcrCents,
|
||||
ucrCents,
|
||||
rateResolution: "MISSING" as const,
|
||||
rateResolutionLevel: rateRecord.level ?? level ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
export async function parseDispoRosterWorkbook(
|
||||
workbookPath: string,
|
||||
options: ParseDispoRosterWorkbookOptions = {},
|
||||
): Promise<ParsedRosterWorkbook> {
|
||||
const [rosterRows, sapRows] = await Promise.all([
|
||||
readWorksheetMatrix(workbookPath, DISPO_ROSTER_SHEET),
|
||||
readWorksheetMatrix(workbookPath, DISPO_ROSTER_SAP_SHEET),
|
||||
]);
|
||||
const rateWorkbook: ParsedResourceRosterMasterWorkbook | null = options.costWorkbookPath
|
||||
? await parseResourceRosterMasterWorkbook(options.costWorkbookPath)
|
||||
: null;
|
||||
const rosterHeaderMap = buildHeaderMap(rosterRows[0] ?? []);
|
||||
const sapHeaderMap = buildHeaderMap(sapRows[1] ?? []);
|
||||
|
||||
const warnings: string[] = [...(rateWorkbook?.warnings ?? [])];
|
||||
const unresolved: ParsedUnresolvedRecord[] = [];
|
||||
const rosterById = new Map<string, RosterSourceRow>();
|
||||
const sapById = new Map<string, SapSourceRow>();
|
||||
let ignoredPseudoDemandRows = 0;
|
||||
|
||||
for (let rowNumber = 2; rowNumber <= rosterRows.length; rowNumber += 1) {
|
||||
const row = rosterRows[rowNumber - 1] ?? [];
|
||||
const eidValue = normalizeNullableWorkbookValue(getCellValue(row, rosterHeaderMap, ROSTER_HEADERS.eid));
|
||||
if (!eidValue) {
|
||||
if (row.some((value) => normalizeText(value) !== null)) {
|
||||
unresolved.push({
|
||||
sourceRow: rowNumber,
|
||||
sourceColumn: "A",
|
||||
recordType: DispoStagedRecordType.RESOURCE,
|
||||
resourceExternalId: null,
|
||||
message: "Missing EID in DispoRoster row",
|
||||
resolutionHint: "Populate EID before staging roster resource data",
|
||||
warnings: [],
|
||||
normalizedData: {},
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isPseudoDemandResourceIdentity(eidValue)) {
|
||||
ignoredPseudoDemandRows += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const canonicalExternalId = resolveCanonicalEnterpriseIdentity(eidValue);
|
||||
if (!canonicalExternalId) {
|
||||
unresolved.push({
|
||||
sourceRow: rowNumber,
|
||||
sourceColumn: "A",
|
||||
recordType: DispoStagedRecordType.RESOURCE,
|
||||
resourceExternalId: eidValue,
|
||||
message: `Unable to normalize EID "${eidValue}"`,
|
||||
resolutionHint: "Validate EID formatting in DispoRoster",
|
||||
warnings: [],
|
||||
normalizedData: { eid: eidValue },
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (rosterById.has(canonicalExternalId)) {
|
||||
unresolved.push({
|
||||
sourceRow: rowNumber,
|
||||
sourceColumn: "A",
|
||||
recordType: DispoStagedRecordType.RESOURCE,
|
||||
resourceExternalId: canonicalExternalId,
|
||||
message: `Duplicate DispoRoster row found for ${canonicalExternalId}`,
|
||||
resolutionHint: "Keep exactly one operational roster row per EID",
|
||||
warnings: [],
|
||||
normalizedData: { eid: canonicalExternalId },
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
rosterById.set(canonicalExternalId, {
|
||||
canonicalExternalId,
|
||||
rowNumber,
|
||||
metroCityName: normalizeNullableWorkbookValue(
|
||||
getCellValue(row, rosterHeaderMap, ROSTER_HEADERS.metroCity),
|
||||
),
|
||||
managementLevelGroupName: normalizeNullableWorkbookValue(
|
||||
getCellValue(row, rosterHeaderMap, ROSTER_HEADERS.managementLevelGroup),
|
||||
),
|
||||
managementLevelName: normalizeNullableWorkbookValue(
|
||||
getCellValue(row, rosterHeaderMap, ROSTER_HEADERS.managementLevel),
|
||||
),
|
||||
fte: parseOptionalNumber(getCellValue(row, rosterHeaderMap, ROSTER_HEADERS.fte)),
|
||||
dailyWorkingHoursPerFte: parseOptionalNumber(
|
||||
getCellValue(row, rosterHeaderMap, ROSTER_HEADERS.dailyWorkingHoursPerFte),
|
||||
),
|
||||
resourceHoursPerWeek: parseOptionalNumber(
|
||||
getCellValue(row, rosterHeaderMap, ROSTER_HEADERS.resourceHoursPerWeek),
|
||||
),
|
||||
rawResourceType: normalizeNullableWorkbookValue(
|
||||
getCellValue(row, rosterHeaderMap, ROSTER_HEADERS.rawResourceType),
|
||||
),
|
||||
clientUnitName: normalizeNullableWorkbookValue(
|
||||
getCellValue(row, rosterHeaderMap, ROSTER_HEADERS.clientUnit),
|
||||
),
|
||||
rawChapter: normalizeNullableWorkbookValue(
|
||||
getCellValue(row, rosterHeaderMap, ROSTER_HEADERS.rawChapter),
|
||||
),
|
||||
department: normalizeNullableWorkbookValue(
|
||||
getCellValue(row, rosterHeaderMap, ROSTER_HEADERS.department),
|
||||
),
|
||||
mainSkillset: normalizeNullableWorkbookValue(
|
||||
getCellValue(row, rosterHeaderMap, ROSTER_HEADERS.mainSkillset),
|
||||
),
|
||||
firstDayInDispo: parseOptionalDate(
|
||||
getCellValue(row, rosterHeaderMap, ROSTER_HEADERS.firstDayInDispo),
|
||||
),
|
||||
lastDayInDispo: parseOptionalDate(
|
||||
getCellValue(row, rosterHeaderMap, ROSTER_HEADERS.lastDayInDispo),
|
||||
),
|
||||
vacationDaysPerYear: parseOptionalNumber(
|
||||
getCellValue(row, rosterHeaderMap, ROSTER_HEADERS.vacationDaysPerYear),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
for (let rowNumber = 3; rowNumber <= sapRows.length; rowNumber += 1) {
|
||||
const row = sapRows[rowNumber - 1] ?? [];
|
||||
const enterpriseIdValue = normalizeNullableWorkbookValue(
|
||||
getCellValue(row, sapHeaderMap, SAP_HEADERS.enterpriseId),
|
||||
);
|
||||
if (!enterpriseIdValue) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const canonicalExternalId = resolveCanonicalEnterpriseIdentity(enterpriseIdValue);
|
||||
if (!canonicalExternalId) {
|
||||
unresolved.push({
|
||||
sourceRow: rowNumber,
|
||||
sourceColumn: "C",
|
||||
recordType: DispoStagedRecordType.RESOURCE,
|
||||
resourceExternalId: enterpriseIdValue,
|
||||
message: `Unable to normalize Enterprise ID "${enterpriseIdValue}"`,
|
||||
resolutionHint: "Validate Enterprise ID formatting in SAP_data",
|
||||
warnings: [],
|
||||
normalizedData: { enterpriseId: enterpriseIdValue },
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sapById.has(canonicalExternalId)) {
|
||||
unresolved.push({
|
||||
sourceRow: rowNumber,
|
||||
sourceColumn: "C",
|
||||
recordType: DispoStagedRecordType.RESOURCE,
|
||||
resourceExternalId: canonicalExternalId,
|
||||
message: `Duplicate SAP_data row found for ${canonicalExternalId}`,
|
||||
resolutionHint: "Keep exactly one SAP_data row per Enterprise ID",
|
||||
warnings: [],
|
||||
normalizedData: { enterpriseId: canonicalExternalId },
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
sapById.set(canonicalExternalId, {
|
||||
canonicalExternalId,
|
||||
rowNumber,
|
||||
employeeName: normalizeNullableWorkbookValue(
|
||||
getCellValue(row, sapHeaderMap, SAP_HEADERS.employeeName),
|
||||
),
|
||||
employeeEmail: normalizeNullableWorkbookValue(
|
||||
getCellValue(row, sapHeaderMap, SAP_HEADERS.employeeEmail),
|
||||
)?.toLowerCase() ?? null,
|
||||
metroCityName: normalizeNullableWorkbookValue(
|
||||
getCellValue(row, sapHeaderMap, SAP_HEADERS.metroCity),
|
||||
),
|
||||
managementLevelGroupName: normalizeNullableWorkbookValue(
|
||||
getCellValue(row, sapHeaderMap, SAP_HEADERS.managementLevelGroup),
|
||||
),
|
||||
managementLevelName: normalizeNullableWorkbookValue(
|
||||
getCellValue(row, sapHeaderMap, SAP_HEADERS.managementLevel),
|
||||
),
|
||||
orgUnitLevelFive: normalizeNullableWorkbookValue(
|
||||
getCellValue(row, sapHeaderMap, SAP_HEADERS.orgUnitLevel5),
|
||||
),
|
||||
orgUnitLevelSix: normalizeNullableWorkbookValue(
|
||||
getCellValue(row, sapHeaderMap, SAP_HEADERS.orgUnitLevel6),
|
||||
),
|
||||
orgUnitLevelSeven: normalizeNullableWorkbookValue(
|
||||
getCellValue(row, sapHeaderMap, SAP_HEADERS.orgUnitLevel7),
|
||||
),
|
||||
fte: parseOptionalNumber(getCellValue(row, sapHeaderMap, SAP_HEADERS.fte)),
|
||||
});
|
||||
}
|
||||
|
||||
const resourceIds = new Set<string>([...rosterById.keys(), ...sapById.keys()]);
|
||||
const resources: ParsedRosterResource[] = [];
|
||||
const excludedCanonicalExternalIds = new Set<string>();
|
||||
|
||||
for (const canonicalExternalId of resourceIds) {
|
||||
const roster = rosterById.get(canonicalExternalId) ?? null;
|
||||
const sap = sapById.get(canonicalExternalId) ?? null;
|
||||
const roleTokens = deriveRoleTokens(
|
||||
roster?.department,
|
||||
roster?.rawChapter,
|
||||
roster?.mainSkillset,
|
||||
sap?.orgUnitLevelSix,
|
||||
sap?.orgUnitLevelSeven,
|
||||
);
|
||||
const normalizedChapter = deriveNormalizedChapter(roster?.rawChapter ?? null, roleTokens);
|
||||
const resourceTypeResult = mapChargeabilityResourceType(roster?.rawResourceType ?? null);
|
||||
const resourceType =
|
||||
roster?.rawResourceType || sap ? resourceTypeResult.resourceType : ResourceType.EMPLOYEE;
|
||||
const fte = sap?.fte ?? roster?.fte ?? null;
|
||||
const dailyWorkingHoursPerFte = roster?.dailyWorkingHoursPerFte ?? null;
|
||||
const displayName =
|
||||
normalizeSapDisplayName(sap?.employeeName ?? null) ??
|
||||
deriveDisplayNameFromEnterpriseId(canonicalExternalId);
|
||||
const metroCityName = sap?.metroCityName ?? roster?.metroCityName ?? null;
|
||||
const managementLevelName =
|
||||
sap?.managementLevelName ?? roster?.managementLevelName ?? null;
|
||||
const resourceWarnings = buildResourceWarnings(resourceType, resourceTypeResult.warning, roster, sap);
|
||||
const rateResolution = applyRateResolution({
|
||||
canonicalExternalId,
|
||||
level: managementLevelName,
|
||||
rateRecord: rateWorkbook?.rates.get(canonicalExternalId) ?? null,
|
||||
levelAverage: managementLevelName
|
||||
? rateWorkbook?.levelAverages.get(managementLevelName) ?? null
|
||||
: null,
|
||||
warnings: resourceWarnings,
|
||||
});
|
||||
|
||||
const resource: ParsedRosterResource = {
|
||||
sourceRow: roster?.rowNumber ?? sap?.rowNumber ?? 0,
|
||||
sourceSheet: roster ? DISPO_ROSTER_SHEET : DISPO_ROSTER_SAP_SHEET,
|
||||
canonicalExternalId,
|
||||
enterpriseId: canonicalExternalId,
|
||||
eid: canonicalExternalId,
|
||||
displayName,
|
||||
email: sap?.employeeEmail ?? buildFallbackAccentureEmail(canonicalExternalId),
|
||||
chapter: normalizedChapter.chapter,
|
||||
chapterCode: normalizedChapter.chapterCode,
|
||||
managementLevelGroupName: sap?.managementLevelGroupName ?? roster?.managementLevelGroupName ?? null,
|
||||
managementLevelName,
|
||||
countryCode: deriveCountryCodeFromMetroCity(metroCityName),
|
||||
metroCityName,
|
||||
clientUnitName: roster?.clientUnitName ?? null,
|
||||
rawResourceType: roster?.rawResourceType ?? null,
|
||||
resourceType,
|
||||
fte,
|
||||
lcrCents: rateResolution.lcrCents,
|
||||
ucrCents: rateResolution.ucrCents,
|
||||
rateResolution: rateResolution.rateResolution,
|
||||
rateResolutionLevel: rateResolution.rateResolutionLevel,
|
||||
availability: createWeekdayAvailabilityFromFte(
|
||||
fte ?? 1,
|
||||
dailyWorkingHoursPerFte ?? 8,
|
||||
) as unknown as ParsedRosterResource["availability"],
|
||||
roleTokens,
|
||||
dailyWorkingHoursPerFte,
|
||||
department: roster?.department ?? null,
|
||||
mainSkillset: roster?.mainSkillset ?? null,
|
||||
resourceHoursPerWeek: roster?.resourceHoursPerWeek ?? null,
|
||||
firstDayInDispo: roster?.firstDayInDispo ?? null,
|
||||
lastDayInDispo: roster?.lastDayInDispo ?? null,
|
||||
vacationDaysPerYear: roster?.vacationDaysPerYear ?? null,
|
||||
sapEmployeeName: sap?.employeeName ?? null,
|
||||
sapOrgUnitLevelFive: sap?.orgUnitLevelFive ?? null,
|
||||
sapOrgUnitLevelSix: sap?.orgUnitLevelSix ?? null,
|
||||
sapOrgUnitLevelSeven: sap?.orgUnitLevelSeven ?? null,
|
||||
warnings: resourceWarnings,
|
||||
};
|
||||
|
||||
if (
|
||||
shouldExcludeImportedResource({
|
||||
canonicalExternalId,
|
||||
sourceEmail: sap?.employeeEmail ?? null,
|
||||
managementLevelName,
|
||||
})
|
||||
) {
|
||||
excludedCanonicalExternalIds.add(canonicalExternalId);
|
||||
warnings.push(
|
||||
`Excluded ${canonicalExternalId} from import because neither email nor management level is present in the supplied sources`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
resources.push(resource);
|
||||
}
|
||||
|
||||
if (ignoredPseudoDemandRows > 0) {
|
||||
warnings.push(`Ignored ${ignoredPseudoDemandRows} pseudo-demand rows from DispoRoster`);
|
||||
}
|
||||
|
||||
resources.sort((left, right) => left.canonicalExternalId.localeCompare(right.canonicalExternalId));
|
||||
|
||||
return {
|
||||
excludedCanonicalExternalIds: Array.from(excludedCanonicalExternalIds).sort((left, right) =>
|
||||
left.localeCompare(right),
|
||||
),
|
||||
resources,
|
||||
unresolved,
|
||||
warnings,
|
||||
ignoredPseudoDemandRows,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user