rename(phase 1): CapaKraken → Nexus across code, UI, docs, CI (#61)
CI / Architecture Guardrails (push) Successful in 2m38s
CI / Assistant Split Regression (push) Successful in 3m33s
CI / Typecheck (push) Successful in 3m51s
CI / Lint (push) Successful in 5m2s
CI / E2E Tests (push) Has been cancelled
CI / Fresh-Linux Docker Deploy (push) Has been cancelled
CI / Release Images (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Architecture Guardrails (push) Successful in 2m38s
CI / Assistant Split Regression (push) Successful in 3m33s
CI / Typecheck (push) Successful in 3m51s
CI / Lint (push) Successful in 5m2s
CI / E2E Tests (push) Has been cancelled
CI / Fresh-Linux Docker Deploy (push) Has been cancelled
CI / Release Images (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
rename(phase 1): CapaKraken → Nexus across code, UI, docs, CI (#61) Co-authored-by: Hartmut Nörenberg <hn@hartmut-noerenberg.com> Co-committed-by: Hartmut Nörenberg <hn@hartmut-noerenberg.com>
This commit was merged in pull request #61.
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
import { DispoStagedRecordType } from "@capakraken/db";
|
||||
import { DispoStagedRecordType } from "@nexus/db";
|
||||
import {
|
||||
VacationType,
|
||||
normalizeCanonicalResourceIdentity,
|
||||
normalizeDispoRoleToken,
|
||||
normalizeDispoUtilizationToken,
|
||||
} from "@capakraken/shared";
|
||||
} from "@nexus/shared";
|
||||
import { readWorksheetMatrix, toColumnLetter, type WorksheetCellValue } from "./read-workbook.js";
|
||||
import {
|
||||
DISPO_PLANNING_SHEET,
|
||||
@@ -182,27 +182,37 @@ function isPlanningSummaryRow(row: ReadonlyArray<WorksheetCellValue>): boolean {
|
||||
const normalizedLabels = new Set(repeatedLabels.map((value) => value.toLowerCase()));
|
||||
const label = repeatedLabels[0] ?? null;
|
||||
|
||||
return normalizedLabels.size === 1 && label !== null && label.startsWith("(") && label.endsWith(")");
|
||||
return (
|
||||
normalizedLabels.size === 1 && label !== null && label.startsWith("(") && label.endsWith(")")
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
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) {
|
||||
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 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);
|
||||
toDateOnlyInBerlin(currentHeaderValue) ?? toDateOnlyInBerlin(nextHeaderValue);
|
||||
|
||||
if (!assignmentDate) {
|
||||
continue;
|
||||
@@ -236,10 +246,15 @@ function normalizePlanningToken(token: string): string {
|
||||
}
|
||||
|
||||
function extractBracketTokens(token: string): string[] {
|
||||
return Array.from(token.matchAll(/\[([^\]]+)\]/g), (match) => match[1]?.trim() ?? "").filter(Boolean);
|
||||
return Array.from(token.matchAll(/\[([^\]]+)\]/g), (match) => match[1]?.trim() ?? "").filter(
|
||||
Boolean,
|
||||
);
|
||||
}
|
||||
|
||||
function extractUtilizationToken(token: string): { utilizationToken: string | null; winProbability: number | null } {
|
||||
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) {
|
||||
@@ -263,7 +278,9 @@ function extractRoleToken(token: string, metadata: PlanningRowMetadata): string
|
||||
return explicitRoleToken;
|
||||
}
|
||||
|
||||
return deriveRoleTokens(metadata.chapter, metadata.typeOfWork, metadata.unitSpecificField)[0] ?? null;
|
||||
return (
|
||||
deriveRoleTokens(metadata.chapter, metadata.typeOfWork, metadata.unitSpecificField)[0] ?? null
|
||||
);
|
||||
}
|
||||
|
||||
function extractProjectKey(token: string): string | null {
|
||||
@@ -466,9 +483,10 @@ function buildAvailabilityAccumulator(
|
||||
} = {},
|
||||
): AvailabilityAccumulator {
|
||||
const percentage = input.percentage ?? parsePercentage(rawToken);
|
||||
const availableHours = percentage !== null
|
||||
? Math.round((percentage / 100) * 8 * 100) / 100
|
||||
: (input.availableHours ?? (8 - SLOT_HOURS));
|
||||
const availableHours =
|
||||
percentage !== null
|
||||
? Math.round((percentage / 100) * 8 * 100) / 100
|
||||
: (input.availableHours ?? 8 - SLOT_HOURS);
|
||||
const warnings = new Set<string>();
|
||||
if (input.warning) {
|
||||
warnings.add(input.warning);
|
||||
@@ -548,7 +566,9 @@ export async function parseDispoPlanningWorkbook(
|
||||
|
||||
if (normalizedToken.startsWith("[_AB]")) {
|
||||
const note = extractLabel(rawToken);
|
||||
const vacationType = note?.toLowerCase().includes("sick") ? VacationType.SICK : VacationType.ANNUAL;
|
||||
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) {
|
||||
@@ -577,11 +597,17 @@ export async function parseDispoPlanningWorkbook(
|
||||
existing.halfDayParts.add(column.halfDayPart);
|
||||
}
|
||||
} else {
|
||||
const vacation = buildVacationAccumulator(column, metadata, rawToken, VacationType.PUBLIC_HOLIDAY, {
|
||||
holidayName,
|
||||
isPublicHoliday: true,
|
||||
note: holidayName,
|
||||
});
|
||||
const vacation = buildVacationAccumulator(
|
||||
column,
|
||||
metadata,
|
||||
rawToken,
|
||||
VacationType.PUBLIC_HOLIDAY,
|
||||
{
|
||||
holidayName,
|
||||
isPublicHoliday: true,
|
||||
note: holidayName,
|
||||
},
|
||||
);
|
||||
vacation.sourceRow = rowNumber;
|
||||
vacations.set(key, vacation);
|
||||
}
|
||||
@@ -595,7 +621,9 @@ export async function parseDispoPlanningWorkbook(
|
||||
const existing = availabilityRules.get(key);
|
||||
if (existing) {
|
||||
const nextAvailability = buildAvailabilityAccumulator(column, metadata, rawToken, {
|
||||
...(naHandling.availableHours !== undefined ? { availableHours: naHandling.availableHours } : {}),
|
||||
...(naHandling.availableHours !== undefined
|
||||
? { availableHours: naHandling.availableHours }
|
||||
: {}),
|
||||
...(naHandling.percentage !== undefined ? { percentage: naHandling.percentage } : {}),
|
||||
...(naHandling.ruleType !== undefined ? { ruleType: naHandling.ruleType } : {}),
|
||||
...(naHandling.warning !== undefined ? { warning: naHandling.warning } : {}),
|
||||
@@ -607,7 +635,9 @@ export async function parseDispoPlanningWorkbook(
|
||||
}
|
||||
} else {
|
||||
const availabilityRule = buildAvailabilityAccumulator(column, metadata, rawToken, {
|
||||
...(naHandling.availableHours !== undefined ? { availableHours: naHandling.availableHours } : {}),
|
||||
...(naHandling.availableHours !== undefined
|
||||
? { availableHours: naHandling.availableHours }
|
||||
: {}),
|
||||
...(naHandling.percentage !== undefined ? { percentage: naHandling.percentage } : {}),
|
||||
...(naHandling.ruleType !== undefined ? { ruleType: naHandling.ruleType } : {}),
|
||||
...(naHandling.warning !== undefined ? { warning: naHandling.warning } : {}),
|
||||
@@ -673,7 +703,8 @@ export async function parseDispoPlanningWorkbook(
|
||||
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",
|
||||
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),
|
||||
@@ -716,30 +747,32 @@ export async function parseDispoPlanningWorkbook(
|
||||
}
|
||||
}
|
||||
|
||||
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 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,
|
||||
halfDayPart: entry.halfDayParts.size === 1 ? (Array.from(entry.halfDayParts)[0] ?? null) : null,
|
||||
holidayName: entry.holidayName,
|
||||
isHalfDay: entry.halfDayParts.size === 1,
|
||||
isPublicHoliday: entry.isPublicHoliday,
|
||||
@@ -753,7 +786,9 @@ export async function parseDispoPlanningWorkbook(
|
||||
warnings: Array.from(entry.warnings),
|
||||
}));
|
||||
|
||||
const parsedAvailabilityRules: ParsedPlanningAvailabilityRule[] = Array.from(availabilityRules.values()).map((entry) => ({
|
||||
const parsedAvailabilityRules: ParsedPlanningAvailabilityRule[] = Array.from(
|
||||
availabilityRules.values(),
|
||||
).map((entry) => ({
|
||||
availableHours: entry.availableHours,
|
||||
effectiveEndDate: entry.effectiveEndDate,
|
||||
effectiveStartDate: entry.effectiveStartDate,
|
||||
|
||||
Reference in New Issue
Block a user