chore(repo): initialize planarchy workspace
This commit is contained in:
@@ -0,0 +1,582 @@
|
||||
import { DispoStagedRecordType } from "@planarchy/db";
|
||||
import {
|
||||
VacationType,
|
||||
normalizeCanonicalResourceIdentity,
|
||||
normalizeDispoRoleToken,
|
||||
normalizeDispoUtilizationToken,
|
||||
} from "@planarchy/shared";
|
||||
import { readWorksheetMatrix, toColumnLetter, type WorksheetCellValue } from "./read-workbook.js";
|
||||
import {
|
||||
DISPO_PLANNING_SHEET,
|
||||
type ParsedPlanningAssignment,
|
||||
type ParsedPlanningAvailabilityRule,
|
||||
type ParsedPlanningVacation,
|
||||
type ParsedPlanningWorkbook,
|
||||
type ParsedUnresolvedRecord,
|
||||
deriveRoleTokens,
|
||||
normalizeNullableWorkbookValue,
|
||||
normalizeText,
|
||||
} from "./shared.js";
|
||||
|
||||
const DISPO_HEADER_ROW = 5;
|
||||
const DISPO_DATE_ROW = 2;
|
||||
const DISPO_SLOT_ROW = 3;
|
||||
const DISPO_DATA_START_ROW = 6;
|
||||
const DISPO_EID_COLUMN = 3;
|
||||
const DISPO_CHAPTER_COLUMN = 4;
|
||||
const DISPO_TYPE_OF_WORK_COLUMN = 5;
|
||||
const DISPO_UNIT_SPECIFIC_FIELD_COLUMN = 7;
|
||||
const DISPO_PLANNING_START_COLUMN = 11;
|
||||
const SLOT_HOURS = 4;
|
||||
const WEEKDAY_LABELS = new Set(["MO", "DI", "MI", "DO", "FR", "SA", "SO"]);
|
||||
const BERLIN_DATE_FORMATTER = new Intl.DateTimeFormat("en-CA", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
timeZone: "Europe/Berlin",
|
||||
year: "numeric",
|
||||
});
|
||||
|
||||
interface PlanningColumn {
|
||||
assignmentDate: Date;
|
||||
columnLetter: string;
|
||||
columnNumber: number;
|
||||
halfDayPart: "AFTERNOON" | "MORNING" | null;
|
||||
slotLabel: string;
|
||||
weekdayLabel: string | null;
|
||||
}
|
||||
|
||||
interface PlanningRowMetadata {
|
||||
chapter: string | null;
|
||||
eid: string;
|
||||
typeOfWork: string | null;
|
||||
unitSpecificField: string | null;
|
||||
}
|
||||
|
||||
interface AssignmentAccumulator {
|
||||
assignmentDate: Date;
|
||||
chapterToken: string | null;
|
||||
firstColumnNumber: number;
|
||||
hoursPerDay: number;
|
||||
isInternal: boolean;
|
||||
isTbd: boolean;
|
||||
isUnassigned: boolean;
|
||||
projectKey: string | null;
|
||||
rawToken: string;
|
||||
resourceExternalId: string;
|
||||
roleName: string | null;
|
||||
roleToken: string | null;
|
||||
slotCount: number;
|
||||
sourceRow: number;
|
||||
utilizationCategoryCode: string | null;
|
||||
warnings: Set<string>;
|
||||
winProbability: number | null;
|
||||
}
|
||||
|
||||
interface VacationAccumulator {
|
||||
endDate: Date;
|
||||
firstColumnNumber: number;
|
||||
halfDayParts: Set<string>;
|
||||
holidayName: string | null;
|
||||
isPublicHoliday: boolean;
|
||||
note: string | null;
|
||||
rawToken: string;
|
||||
resourceExternalId: string;
|
||||
sourceRow: number;
|
||||
startDate: Date;
|
||||
vacationType: VacationType;
|
||||
warnings: Set<string>;
|
||||
}
|
||||
|
||||
interface AvailabilityAccumulator {
|
||||
availableHours: number | null;
|
||||
effectiveEndDate: Date;
|
||||
effectiveStartDate: Date;
|
||||
firstColumnNumber: number;
|
||||
isResolved: boolean;
|
||||
percentage: number | null;
|
||||
rawToken: string;
|
||||
resourceExternalId: string;
|
||||
ruleType: string;
|
||||
sourceRow: number;
|
||||
warnings: Set<string>;
|
||||
}
|
||||
|
||||
interface ParsedAssignmentToken {
|
||||
chapterToken: string | null;
|
||||
isInternal: boolean;
|
||||
isTbd: boolean;
|
||||
isUnassigned: boolean;
|
||||
projectKey: string | null;
|
||||
roleName: string | null;
|
||||
roleToken: string | null;
|
||||
utilizationCategoryCode: string | null;
|
||||
winProbability: number | null;
|
||||
}
|
||||
|
||||
function isWeekdayLabel(value: string | null): boolean {
|
||||
return value !== null && WEEKDAY_LABELS.has(value.toUpperCase());
|
||||
}
|
||||
|
||||
function toDateOnlyInBerlin(value: WorksheetCellValue): Date | null {
|
||||
if (!(value instanceof Date)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parts = BERLIN_DATE_FORMATTER.formatToParts(value);
|
||||
const year = Number(parts.find((part) => part.type === "year")?.value);
|
||||
const month = Number(parts.find((part) => part.type === "month")?.value);
|
||||
const day = Number(parts.find((part) => part.type === "day")?.value);
|
||||
|
||||
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Date(Date.UTC(year, month - 1, day));
|
||||
}
|
||||
|
||||
function getDateKey(value: Date): string {
|
||||
return value.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function getSlotHalfDayPart(slotLabel: string | null): "AFTERNOON" | "MORNING" | null {
|
||||
const normalized = normalizeText(slotLabel)?.toLowerCase() ?? null;
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (normalized.includes("9.-13")) {
|
||||
return "MORNING";
|
||||
}
|
||||
if (normalized.includes("14.-18")) {
|
||||
return "AFTERNOON";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildPlanningColumns(rows: ReadonlyArray<ReadonlyArray<WorksheetCellValue>>) {
|
||||
const columns: PlanningColumn[] = [];
|
||||
const headerWidth = Math.max(rows[DISPO_DATE_ROW - 1]?.length ?? 0, rows[DISPO_SLOT_ROW - 1]?.length ?? 0);
|
||||
|
||||
for (let columnNumber = DISPO_PLANNING_START_COLUMN; columnNumber <= headerWidth; columnNumber += 1) {
|
||||
const slotLabel = normalizeNullableWorkbookValue(rows[DISPO_SLOT_ROW - 1]?.[columnNumber - 1]);
|
||||
if (!slotLabel) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const currentHeaderValue = rows[DISPO_DATE_ROW - 1]?.[columnNumber - 1] ?? null;
|
||||
const previousHeaderLabel = normalizeNullableWorkbookValue(rows[DISPO_DATE_ROW - 1]?.[columnNumber - 2]);
|
||||
const currentHeaderLabel = normalizeNullableWorkbookValue(currentHeaderValue);
|
||||
const nextHeaderValue = rows[DISPO_DATE_ROW - 1]?.[columnNumber] ?? null;
|
||||
|
||||
const assignmentDate =
|
||||
toDateOnlyInBerlin(currentHeaderValue) ??
|
||||
toDateOnlyInBerlin(nextHeaderValue);
|
||||
|
||||
if (!assignmentDate) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const weekdayLabel = isWeekdayLabel(currentHeaderLabel)
|
||||
? currentHeaderLabel
|
||||
: isWeekdayLabel(previousHeaderLabel)
|
||||
? previousHeaderLabel
|
||||
: null;
|
||||
|
||||
columns.push({
|
||||
assignmentDate,
|
||||
columnLetter: toColumnLetter(columnNumber),
|
||||
columnNumber,
|
||||
halfDayPart: getSlotHalfDayPart(slotLabel),
|
||||
slotLabel,
|
||||
weekdayLabel,
|
||||
});
|
||||
}
|
||||
|
||||
return columns;
|
||||
}
|
||||
|
||||
function normalizePlanningToken(token: string): string {
|
||||
return token
|
||||
.trim()
|
||||
.replace(/\s+/g, " ")
|
||||
.replace(/\s+(?:HB|SB)_?\s*$/i, "")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function extractBracketTokens(token: string): string[] {
|
||||
return Array.from(token.matchAll(/\[([^\]]+)\]/g), (match) => match[1]?.trim() ?? "").filter(Boolean);
|
||||
}
|
||||
|
||||
function extractUtilizationToken(token: string): { utilizationToken: string | null; winProbability: number | null } {
|
||||
const matches = Array.from(token.matchAll(/\{([A-Z]+)(\d{0,3})\}/gi));
|
||||
const lastMatch = matches.at(-1);
|
||||
if (!lastMatch) {
|
||||
return {
|
||||
utilizationToken: null,
|
||||
winProbability: null,
|
||||
};
|
||||
}
|
||||
|
||||
const utilizationToken = lastMatch[1]?.toUpperCase() ?? null;
|
||||
const winProbability = lastMatch[2] ? Number(lastMatch[2]) : null;
|
||||
return {
|
||||
utilizationToken,
|
||||
winProbability: Number.isFinite(winProbability) ? winProbability : null,
|
||||
};
|
||||
}
|
||||
|
||||
function extractRoleToken(token: string, metadata: PlanningRowMetadata): string | null {
|
||||
const explicitRoleToken = token.match(/^(2D|3D|PM|AD)\b/i)?.[1]?.toUpperCase() ?? null;
|
||||
if (explicitRoleToken) {
|
||||
return explicitRoleToken;
|
||||
}
|
||||
|
||||
return deriveRoleTokens(metadata.chapter, metadata.typeOfWork, metadata.unitSpecificField)[0] ?? null;
|
||||
}
|
||||
|
||||
function extractProjectKey(token: string): string | null {
|
||||
const bracketTokens = extractBracketTokens(token).filter((entry) => !entry.startsWith("_"));
|
||||
const lastToken = bracketTokens.at(-1) ?? null;
|
||||
return lastToken && lastToken.toLowerCase() !== "tbd" ? lastToken : null;
|
||||
}
|
||||
|
||||
function extractLabel(token: string): string | null {
|
||||
const stripped = token
|
||||
.replace(/^(2D|3D|PM|AD)\s+/i, "")
|
||||
.replace(/\[[^\]]+\]/g, " ")
|
||||
.replace(/\{[^}]+\}/g, " ")
|
||||
.replace(/\s+(?:HB|SB)_?\s*$/i, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
|
||||
return stripped.length > 0 ? stripped : null;
|
||||
}
|
||||
|
||||
function parsePercentage(value: string): number | null {
|
||||
const percentageMatch = value.match(/(\d+(?:[.,]\d+)?)\s*%/);
|
||||
if (percentageMatch) {
|
||||
const normalized = Number(percentageMatch[1]?.replace(",", "."));
|
||||
return Number.isFinite(normalized) ? normalized : null;
|
||||
}
|
||||
|
||||
const fteMatch = value.match(/FTE:\s*(\d+(?:[.,]\d+)?)/i);
|
||||
if (fteMatch) {
|
||||
const normalized = Number(fteMatch[1]?.replace(",", "."));
|
||||
return Number.isFinite(normalized) ? Math.round(normalized * 10000) / 100 : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildAssignmentAccumulator(
|
||||
column: PlanningColumn,
|
||||
metadata: PlanningRowMetadata,
|
||||
rawToken: string,
|
||||
): AssignmentAccumulator | null {
|
||||
const roleToken = extractRoleToken(rawToken, metadata);
|
||||
const roleName = normalizeDispoRoleToken(roleToken);
|
||||
const { utilizationToken, winProbability } = extractUtilizationToken(rawToken);
|
||||
const utilizationCategoryCode = normalizeDispoUtilizationToken(utilizationToken);
|
||||
const projectKey = extractProjectKey(rawToken);
|
||||
const isTbd = /\[tbd\]/i.test(rawToken);
|
||||
const isUnassigned = utilizationToken === "UN";
|
||||
const isInternal = ["MD", "MO", "PD"].includes(utilizationToken ?? "");
|
||||
|
||||
if (isUnassigned) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
assignmentDate: column.assignmentDate,
|
||||
chapterToken: roleToken,
|
||||
firstColumnNumber: column.columnNumber,
|
||||
hoursPerDay: SLOT_HOURS,
|
||||
isInternal,
|
||||
isTbd,
|
||||
isUnassigned: false,
|
||||
projectKey,
|
||||
rawToken,
|
||||
resourceExternalId: metadata.eid,
|
||||
roleName,
|
||||
roleToken,
|
||||
slotCount: 1,
|
||||
sourceRow: 0,
|
||||
utilizationCategoryCode,
|
||||
warnings: new Set<string>(),
|
||||
winProbability,
|
||||
};
|
||||
}
|
||||
|
||||
function buildVacationAccumulator(
|
||||
column: PlanningColumn,
|
||||
metadata: PlanningRowMetadata,
|
||||
rawToken: string,
|
||||
vacationType: VacationType,
|
||||
input: { holidayName?: string | null; isPublicHoliday: boolean; note?: string | null },
|
||||
): VacationAccumulator {
|
||||
const halfDayParts = new Set<string>();
|
||||
if (column.halfDayPart) {
|
||||
halfDayParts.add(column.halfDayPart);
|
||||
}
|
||||
|
||||
return {
|
||||
endDate: column.assignmentDate,
|
||||
firstColumnNumber: column.columnNumber,
|
||||
halfDayParts,
|
||||
holidayName: input.holidayName ?? null,
|
||||
isPublicHoliday: input.isPublicHoliday,
|
||||
note: input.note ?? null,
|
||||
rawToken,
|
||||
resourceExternalId: metadata.eid,
|
||||
sourceRow: 0,
|
||||
startDate: column.assignmentDate,
|
||||
vacationType,
|
||||
warnings: new Set<string>(),
|
||||
};
|
||||
}
|
||||
|
||||
function buildAvailabilityAccumulator(
|
||||
column: PlanningColumn,
|
||||
metadata: PlanningRowMetadata,
|
||||
rawToken: string,
|
||||
): AvailabilityAccumulator {
|
||||
const percentage = parsePercentage(rawToken);
|
||||
const availableHours = percentage !== null
|
||||
? Math.round((percentage / 100) * 8 * 100) / 100
|
||||
: 8 - SLOT_HOURS;
|
||||
|
||||
return {
|
||||
availableHours,
|
||||
effectiveEndDate: column.assignmentDate,
|
||||
effectiveStartDate: column.assignmentDate,
|
||||
firstColumnNumber: column.columnNumber,
|
||||
isResolved: false,
|
||||
percentage,
|
||||
rawToken,
|
||||
resourceExternalId: metadata.eid,
|
||||
ruleType: "PART_TIME",
|
||||
sourceRow: 0,
|
||||
warnings: new Set<string>(),
|
||||
};
|
||||
}
|
||||
|
||||
export async function parseDispoPlanningWorkbook(
|
||||
workbookPath: string,
|
||||
): Promise<ParsedPlanningWorkbook> {
|
||||
const rows = await readWorksheetMatrix(workbookPath, DISPO_PLANNING_SHEET);
|
||||
const planningColumns = buildPlanningColumns(rows);
|
||||
const assignments = new Map<string, AssignmentAccumulator>();
|
||||
const vacations = new Map<string, VacationAccumulator>();
|
||||
const availabilityRules = new Map<string, AvailabilityAccumulator>();
|
||||
const unresolved: ParsedUnresolvedRecord[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
for (let rowNumber = DISPO_DATA_START_ROW; rowNumber <= rows.length; rowNumber += 1) {
|
||||
const row = rows[rowNumber - 1] ?? [];
|
||||
const eid = normalizeNullableWorkbookValue(row[DISPO_EID_COLUMN - 1]);
|
||||
|
||||
if (!eid) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const metadata: PlanningRowMetadata = {
|
||||
chapter: normalizeNullableWorkbookValue(row[DISPO_CHAPTER_COLUMN - 1]),
|
||||
eid: normalizeCanonicalResourceIdentity(eid),
|
||||
typeOfWork: normalizeNullableWorkbookValue(row[DISPO_TYPE_OF_WORK_COLUMN - 1]),
|
||||
unitSpecificField: normalizeNullableWorkbookValue(row[DISPO_UNIT_SPECIFIC_FIELD_COLUMN - 1]),
|
||||
};
|
||||
|
||||
for (const column of planningColumns) {
|
||||
const rawCellValue = normalizeNullableWorkbookValue(row[column.columnNumber - 1]);
|
||||
if (!rawCellValue) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const rawToken = normalizePlanningToken(rawCellValue);
|
||||
const normalizedToken = rawToken.toUpperCase();
|
||||
|
||||
if (normalizedToken === "[_NA] WEEKEND {NA}") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (normalizedToken.startsWith("[_AB]")) {
|
||||
const note = extractLabel(rawToken);
|
||||
const vacationType = note?.toLowerCase().includes("sick") ? VacationType.SICK : VacationType.ANNUAL;
|
||||
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, vacationType, {
|
||||
isPublicHoliday: false,
|
||||
note,
|
||||
});
|
||||
vacation.sourceRow = rowNumber;
|
||||
vacations.set(key, vacation);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (normalizedToken.startsWith("[_NA]") && normalizedToken.includes("PUBLIC HOLIDAY")) {
|
||||
const holidayName = extractLabel(rawToken);
|
||||
const key = `${metadata.eid}|${getDateKey(column.assignmentDate)}|PH|${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, VacationType.PUBLIC_HOLIDAY, {
|
||||
holidayName,
|
||||
isPublicHoliday: true,
|
||||
note: holidayName,
|
||||
});
|
||||
vacation.sourceRow = rowNumber;
|
||||
vacations.set(key, vacation);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (normalizedToken.startsWith("[_NA]") && normalizedToken.includes("PART-TIME")) {
|
||||
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;
|
||||
} else {
|
||||
const availabilityRule = buildAvailabilityAccumulator(column, metadata, rawToken);
|
||||
availabilityRule.sourceRow = rowNumber;
|
||||
availabilityRules.set(key, availabilityRule);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (normalizedToken.startsWith("[_UN]")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const assignment = buildAssignmentAccumulator(column, metadata, rawToken);
|
||||
if (!assignment) {
|
||||
continue;
|
||||
}
|
||||
|
||||
assignment.sourceRow = rowNumber;
|
||||
if (!assignment.utilizationCategoryCode) {
|
||||
assignment.warnings.add(`Unable to resolve utilization category from token "${rawToken}"`);
|
||||
}
|
||||
|
||||
if (!assignment.projectKey && !assignment.isInternal && !assignment.isTbd) {
|
||||
unresolved.push({
|
||||
sourceRow: rowNumber,
|
||||
sourceColumn: column.columnLetter,
|
||||
recordType: DispoStagedRecordType.ASSIGNMENT,
|
||||
resourceExternalId: metadata.eid,
|
||||
projectKey: null,
|
||||
message: `Unable to resolve project key from planning token "${rawToken}"`,
|
||||
resolutionHint: "Add a WBS token or classify this cell as an internal bucket before commit",
|
||||
warnings: Array.from(assignment.warnings),
|
||||
normalizedData: {
|
||||
assignmentDate: getDateKey(column.assignmentDate),
|
||||
rawToken,
|
||||
roleToken: assignment.roleToken,
|
||||
utilizationCategoryCode: assignment.utilizationCategoryCode,
|
||||
},
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (assignment.isTbd) {
|
||||
unresolved.push({
|
||||
sourceRow: rowNumber,
|
||||
sourceColumn: column.columnLetter,
|
||||
recordType: DispoStagedRecordType.PROJECT,
|
||||
resourceExternalId: metadata.eid,
|
||||
projectKey: null,
|
||||
message: `Planning token "${rawToken}" references [tbd] and requires project resolution`,
|
||||
resolutionHint: "Resolve [tbd] rows to a real WBS/project before commit",
|
||||
warnings: Array.from(assignment.warnings),
|
||||
normalizedData: {
|
||||
assignmentDate: getDateKey(column.assignmentDate),
|
||||
rawToken,
|
||||
roleToken: assignment.roleToken,
|
||||
utilizationCategoryCode: assignment.utilizationCategoryCode,
|
||||
winProbability: assignment.winProbability,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const key = `${metadata.eid}|${getDateKey(column.assignmentDate)}|ASN|${rawToken}`;
|
||||
const existing = assignments.get(key);
|
||||
if (existing) {
|
||||
existing.hoursPerDay += SLOT_HOURS;
|
||||
existing.slotCount += 1;
|
||||
} else {
|
||||
assignments.set(key, assignment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const parsedAssignments: ParsedPlanningAssignment[] = Array.from(assignments.values()).map((entry) => ({
|
||||
assignmentDate: entry.assignmentDate,
|
||||
chapterToken: entry.chapterToken,
|
||||
hoursPerDay: entry.hoursPerDay,
|
||||
isInternal: entry.isInternal,
|
||||
isTbd: entry.isTbd,
|
||||
isUnassigned: entry.isUnassigned,
|
||||
percentage: entry.slotCount * 50,
|
||||
projectKey: entry.projectKey,
|
||||
rawToken: entry.rawToken,
|
||||
resourceExternalId: entry.resourceExternalId,
|
||||
roleName: entry.roleName,
|
||||
roleToken: entry.roleToken,
|
||||
slotFraction: entry.slotCount / 2,
|
||||
sourceColumn: toColumnLetter(entry.firstColumnNumber),
|
||||
sourceRow: entry.sourceRow,
|
||||
utilizationCategoryCode: entry.utilizationCategoryCode,
|
||||
warnings: Array.from(entry.warnings),
|
||||
winProbability: entry.winProbability,
|
||||
}));
|
||||
|
||||
const parsedVacations: ParsedPlanningVacation[] = Array.from(vacations.values()).map((entry) => ({
|
||||
endDate: entry.endDate,
|
||||
halfDayPart: entry.halfDayParts.size === 1 ? Array.from(entry.halfDayParts)[0] ?? null : null,
|
||||
holidayName: entry.holidayName,
|
||||
isHalfDay: entry.halfDayParts.size === 1,
|
||||
isPublicHoliday: entry.isPublicHoliday,
|
||||
note: entry.note,
|
||||
rawToken: entry.rawToken,
|
||||
resourceExternalId: entry.resourceExternalId,
|
||||
sourceColumn: toColumnLetter(entry.firstColumnNumber),
|
||||
sourceRow: entry.sourceRow,
|
||||
startDate: entry.startDate,
|
||||
vacationType: entry.vacationType,
|
||||
warnings: Array.from(entry.warnings),
|
||||
}));
|
||||
|
||||
const parsedAvailabilityRules: ParsedPlanningAvailabilityRule[] = Array.from(availabilityRules.values()).map((entry) => ({
|
||||
availableHours: entry.availableHours,
|
||||
effectiveEndDate: entry.effectiveEndDate,
|
||||
effectiveStartDate: entry.effectiveStartDate,
|
||||
isResolved: entry.isResolved,
|
||||
percentage: entry.percentage,
|
||||
rawToken: entry.rawToken,
|
||||
resourceExternalId: entry.resourceExternalId,
|
||||
ruleType: entry.ruleType,
|
||||
sourceColumn: toColumnLetter(entry.firstColumnNumber),
|
||||
sourceRow: entry.sourceRow,
|
||||
warnings: Array.from(entry.warnings),
|
||||
}));
|
||||
|
||||
return {
|
||||
assignments: parsedAssignments,
|
||||
availabilityRules: parsedAvailabilityRules,
|
||||
unresolved,
|
||||
vacations: parsedVacations,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user