feat(application): complete dispo import operator flow
This commit is contained in:
@@ -101,6 +101,18 @@ interface AvailabilityAccumulator {
|
||||
warnings: Set<string>;
|
||||
}
|
||||
|
||||
interface NaTokenHandlingResult {
|
||||
availableHours?: number | null;
|
||||
isPublicHoliday: boolean;
|
||||
kind: "availability" | "assignment" | "skip" | "vacation";
|
||||
note?: string | null;
|
||||
percentage?: number | null;
|
||||
projectKey?: string | null;
|
||||
ruleType?: string;
|
||||
vacationType?: VacationType;
|
||||
warning?: string;
|
||||
}
|
||||
|
||||
interface ParsedAssignmentToken {
|
||||
chapterToken: string | null;
|
||||
isInternal: boolean;
|
||||
@@ -241,6 +253,14 @@ function extractProjectKey(token: string): string | null {
|
||||
return lastToken && lastToken.toLowerCase() !== "tbd" ? lastToken : null;
|
||||
}
|
||||
|
||||
function slugifyProjectKeyFragment(value: string): string {
|
||||
return value
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "")
|
||||
.slice(0, 48);
|
||||
}
|
||||
|
||||
function extractLabel(token: string): string | null {
|
||||
const stripped = token
|
||||
.replace(/^(2D|3D|PM|AD)\s+/i, "")
|
||||
@@ -269,6 +289,84 @@ function parsePercentage(value: string): number | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
function classifyNaPlanningToken(rawToken: string): NaTokenHandlingResult {
|
||||
const normalized = rawToken.toLowerCase();
|
||||
const label = extractLabel(rawToken);
|
||||
const { utilizationToken } = extractUtilizationToken(rawToken);
|
||||
|
||||
if (!rawToken.toUpperCase().startsWith("[_NA]")) {
|
||||
return {
|
||||
isPublicHoliday: false,
|
||||
kind: "assignment",
|
||||
};
|
||||
}
|
||||
|
||||
if (normalized.includes("public holiday")) {
|
||||
return {
|
||||
isPublicHoliday: true,
|
||||
kind: "vacation",
|
||||
note: label,
|
||||
vacationType: VacationType.PUBLIC_HOLIDAY,
|
||||
};
|
||||
}
|
||||
|
||||
if (normalized.includes("part-time")) {
|
||||
return {
|
||||
availableHours: null,
|
||||
isPublicHoliday: false,
|
||||
kind: "availability",
|
||||
percentage: parsePercentage(rawToken),
|
||||
ruleType: "PART_TIME",
|
||||
};
|
||||
}
|
||||
|
||||
if (normalized.includes("not bookable")) {
|
||||
return {
|
||||
availableHours: 0,
|
||||
isPublicHoliday: false,
|
||||
kind: "availability",
|
||||
percentage: 0,
|
||||
ruleType: "NOT_BOOKABLE",
|
||||
};
|
||||
}
|
||||
|
||||
if (normalized.includes("reduced spanish working hours")) {
|
||||
return {
|
||||
availableHours: 7,
|
||||
isPublicHoliday: false,
|
||||
kind: "availability",
|
||||
percentage: 87.5,
|
||||
ruleType: "REDUCED_SPANISH_WORKING_HOURS",
|
||||
warning: "Assumed 7h/day for reduced Spanish working hours",
|
||||
};
|
||||
}
|
||||
|
||||
if (normalized.includes("parental leave")) {
|
||||
return {
|
||||
isPublicHoliday: false,
|
||||
kind: "vacation",
|
||||
note: label,
|
||||
vacationType: VacationType.OTHER,
|
||||
};
|
||||
}
|
||||
|
||||
if (utilizationToken && utilizationToken !== "NA" && utilizationToken !== "UN") {
|
||||
const projectKey = label ? `NA-${slugifyProjectKeyFragment(label)}` : null;
|
||||
return {
|
||||
isPublicHoliday: false,
|
||||
kind: projectKey ? "assignment" : "skip",
|
||||
note: label,
|
||||
projectKey,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
isPublicHoliday: false,
|
||||
kind: "skip",
|
||||
note: label,
|
||||
};
|
||||
}
|
||||
|
||||
function buildAssignmentAccumulator(
|
||||
column: PlanningColumn,
|
||||
metadata: PlanningRowMetadata,
|
||||
@@ -278,7 +376,8 @@ function buildAssignmentAccumulator(
|
||||
const roleName = normalizeDispoRoleToken(roleToken);
|
||||
const { utilizationToken, winProbability } = extractUtilizationToken(rawToken);
|
||||
const utilizationCategoryCode = normalizeDispoUtilizationToken(utilizationToken);
|
||||
const projectKey = extractProjectKey(rawToken);
|
||||
const naHandling = classifyNaPlanningToken(rawToken);
|
||||
const projectKey = extractProjectKey(rawToken) ?? naHandling.projectKey ?? null;
|
||||
const isTbd = /\[tbd\]/i.test(rawToken);
|
||||
const isUnassigned = utilizationToken === "UN";
|
||||
const isInternal = ["MD", "MO", "PD"].includes(utilizationToken ?? "");
|
||||
@@ -340,11 +439,21 @@ function buildAvailabilityAccumulator(
|
||||
column: PlanningColumn,
|
||||
metadata: PlanningRowMetadata,
|
||||
rawToken: string,
|
||||
input: {
|
||||
availableHours?: number | null;
|
||||
percentage?: number | null;
|
||||
ruleType?: string;
|
||||
warning?: string;
|
||||
} = {},
|
||||
): AvailabilityAccumulator {
|
||||
const percentage = parsePercentage(rawToken);
|
||||
const percentage = input.percentage ?? parsePercentage(rawToken);
|
||||
const availableHours = percentage !== null
|
||||
? Math.round((percentage / 100) * 8 * 100) / 100
|
||||
: 8 - SLOT_HOURS;
|
||||
: (input.availableHours ?? (8 - SLOT_HOURS));
|
||||
const warnings = new Set<string>();
|
||||
if (input.warning) {
|
||||
warnings.add(input.warning);
|
||||
}
|
||||
|
||||
return {
|
||||
availableHours,
|
||||
@@ -355,9 +464,9 @@ function buildAvailabilityAccumulator(
|
||||
percentage,
|
||||
rawToken,
|
||||
resourceExternalId: metadata.eid,
|
||||
ruleType: "PART_TIME",
|
||||
ruleType: input.ruleType ?? "PART_TIME",
|
||||
sourceRow: 0,
|
||||
warnings: new Set<string>(),
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -388,7 +497,18 @@ export async function parseDispoPlanningWorkbook(
|
||||
};
|
||||
|
||||
for (const column of planningColumns) {
|
||||
const rawCellValue = normalizeNullableWorkbookValue(row[column.columnNumber - 1]);
|
||||
const worksheetValue = row[column.columnNumber - 1];
|
||||
if (
|
||||
worksheetValue === null ||
|
||||
worksheetValue === undefined ||
|
||||
typeof worksheetValue === "number" ||
|
||||
typeof worksheetValue === "boolean" ||
|
||||
worksheetValue instanceof Date
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const rawCellValue = normalizeNullableWorkbookValue(worksheetValue);
|
||||
if (!rawCellValue) {
|
||||
continue;
|
||||
}
|
||||
@@ -396,6 +516,10 @@ export async function parseDispoPlanningWorkbook(
|
||||
const rawToken = normalizePlanningToken(rawCellValue);
|
||||
const normalizedToken = rawToken.toUpperCase();
|
||||
|
||||
if (normalizedToken === "IN DAYS") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (normalizedToken === "[_NA] WEEKEND {NA}") {
|
||||
continue;
|
||||
}
|
||||
@@ -442,20 +566,69 @@ export async function parseDispoPlanningWorkbook(
|
||||
continue;
|
||||
}
|
||||
|
||||
if (normalizedToken.startsWith("[_NA]") && normalizedToken.includes("PART-TIME")) {
|
||||
const naHandling = classifyNaPlanningToken(rawToken);
|
||||
|
||||
if (naHandling.kind === "availability") {
|
||||
const key = `${metadata.eid}|${getDateKey(column.assignmentDate)}|PT|${rawToken}`;
|
||||
const existing = availabilityRules.get(key);
|
||||
if (existing) {
|
||||
existing.availableHours = buildAvailabilityAccumulator(column, metadata, rawToken).availableHours;
|
||||
existing.percentage = buildAvailabilityAccumulator(column, metadata, rawToken).percentage;
|
||||
const nextAvailability = buildAvailabilityAccumulator(column, metadata, rawToken, {
|
||||
availableHours: naHandling.availableHours,
|
||||
percentage: naHandling.percentage,
|
||||
ruleType: naHandling.ruleType,
|
||||
warning: naHandling.warning,
|
||||
});
|
||||
existing.availableHours = nextAvailability.availableHours;
|
||||
existing.percentage = nextAvailability.percentage;
|
||||
if (naHandling.warning) {
|
||||
existing.warnings.add(naHandling.warning);
|
||||
}
|
||||
} else {
|
||||
const availabilityRule = buildAvailabilityAccumulator(column, metadata, rawToken);
|
||||
const availabilityRule = buildAvailabilityAccumulator(column, metadata, rawToken, {
|
||||
availableHours: naHandling.availableHours,
|
||||
percentage: naHandling.percentage,
|
||||
ruleType: naHandling.ruleType,
|
||||
warning: naHandling.warning,
|
||||
});
|
||||
availabilityRule.sourceRow = rowNumber;
|
||||
availabilityRules.set(key, availabilityRule);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (naHandling.kind === "vacation" && naHandling.vacationType) {
|
||||
const key = `${metadata.eid}|${getDateKey(column.assignmentDate)}|VAC|${rawToken}`;
|
||||
const existing = vacations.get(key);
|
||||
if (existing) {
|
||||
existing.endDate = column.assignmentDate;
|
||||
if (column.halfDayPart) {
|
||||
existing.halfDayParts.add(column.halfDayPart);
|
||||
}
|
||||
} else {
|
||||
const vacation = buildVacationAccumulator(
|
||||
column,
|
||||
metadata,
|
||||
rawToken,
|
||||
naHandling.vacationType,
|
||||
{
|
||||
holidayName: naHandling.isPublicHoliday ? extractLabel(rawToken) : null,
|
||||
isPublicHoliday: naHandling.isPublicHoliday,
|
||||
note: naHandling.note ?? extractLabel(rawToken),
|
||||
},
|
||||
);
|
||||
vacation.sourceRow = rowNumber;
|
||||
if (naHandling.warning) {
|
||||
vacation.warnings.add(naHandling.warning);
|
||||
}
|
||||
vacations.set(key, vacation);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (naHandling.kind === "skip") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (normalizedToken.startsWith("[_UN]")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user