562 lines
19 KiB
TypeScript
562 lines
19 KiB
TypeScript
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)) {
|
|
warnings.push(`Ignoring DispoRoster row ${rowNumber} because EID is missing`);
|
|
}
|
|
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,
|
|
};
|
|
}
|