chore(repo): initialize planarchy workspace

This commit is contained in:
2026-03-14 14:31:09 +01:00
commit dd55d0e78b
769 changed files with 166461 additions and 0 deletions
@@ -0,0 +1,178 @@
import { normalizeCanonicalResourceIdentity } from "@planarchy/shared";
import { readWorksheetMatrix } from "./read-workbook.js";
import { normalizeNullableWorkbookValue, normalizeText } from "./shared.js";
const RESOURCE_ROSTER_MASTER_SHEET = "Dispo Namen";
const HEADERS = {
chapter: "Chapter",
employeeName: "Mitarbeiter (laut Dispo)",
experience: "Experience",
fte: "FTE",
lcr: "LCR (EUR)",
level: "Level",
location: "Location",
status: "Status",
typeOfWork: "Type of work",
ucr: "UCR (EUR)",
} as const;
export interface ParsedResourceRosterRate {
canonicalExternalId: string;
chapter: string | null;
experience: string | null;
fte: number | null;
lcrCents: number | null;
level: string | null;
location: string | null;
sourceRow: number;
status: string | null;
typeOfWork: string | null;
ucrCents: number | null;
warnings: string[];
}
export interface ParsedResourceRosterLevelAverage {
lcrCents: number | null;
level: string;
sampleCount: number;
ucrCents: number | null;
}
export interface ParsedResourceRosterMasterWorkbook {
levelAverages: Map<string, ParsedResourceRosterLevelAverage>;
rates: Map<string, ParsedResourceRosterRate>;
warnings: 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 toCents(value: number | null): number | null {
return value === null ? null : Math.round(value * 100);
}
export async function parseResourceRosterMasterWorkbook(
workbookPath: string,
): Promise<ParsedResourceRosterMasterWorkbook> {
const rows = await readWorksheetMatrix(workbookPath, RESOURCE_ROSTER_MASTER_SHEET);
const headerMap = buildHeaderMap(rows[0] ?? []);
const warnings: string[] = [];
const rates = new Map<string, ParsedResourceRosterRate>();
const levelBuckets = new Map<string, { lcr: number[]; ucr: number[] }>();
for (let rowNumber = 2; rowNumber <= rows.length; rowNumber += 1) {
const row = rows[rowNumber - 1] ?? [];
const employeeName = normalizeNullableWorkbookValue(
getCellValue(row, headerMap, HEADERS.employeeName),
);
if (!employeeName) {
continue;
}
const canonicalExternalId = normalizeCanonicalResourceIdentity(employeeName);
const level = normalizeNullableWorkbookValue(getCellValue(row, headerMap, HEADERS.level));
const ucrValue = parseOptionalNumber(getCellValue(row, headerMap, HEADERS.ucr));
const lcrValue = parseOptionalNumber(getCellValue(row, headerMap, HEADERS.lcr));
const recordWarnings: string[] = [];
if (rates.has(canonicalExternalId)) {
recordWarnings.push(`Duplicate rate row ${rowNumber} ignored for ${canonicalExternalId}`);
warnings.push(recordWarnings[0] ?? `Duplicate rate row ${rowNumber} ignored`);
continue;
}
if (ucrValue === null || lcrValue === null) {
recordWarnings.push(`Incomplete rate row for ${canonicalExternalId}`);
}
rates.set(canonicalExternalId, {
canonicalExternalId,
sourceRow: rowNumber,
ucrCents: toCents(ucrValue),
lcrCents: toCents(lcrValue),
fte: parseOptionalNumber(getCellValue(row, headerMap, HEADERS.fte)),
level,
typeOfWork: normalizeNullableWorkbookValue(getCellValue(row, headerMap, HEADERS.typeOfWork)),
chapter: normalizeNullableWorkbookValue(getCellValue(row, headerMap, HEADERS.chapter)),
location: normalizeNullableWorkbookValue(getCellValue(row, headerMap, HEADERS.location)),
status: normalizeNullableWorkbookValue(getCellValue(row, headerMap, HEADERS.status)),
experience: normalizeNullableWorkbookValue(getCellValue(row, headerMap, HEADERS.experience)),
warnings: recordWarnings,
});
if (level) {
const bucket = levelBuckets.get(level) ?? { lcr: [], ucr: [] };
if (lcrValue !== null) {
bucket.lcr.push(lcrValue);
}
if (ucrValue !== null) {
bucket.ucr.push(ucrValue);
}
levelBuckets.set(level, bucket);
}
}
const levelAverages = new Map<string, ParsedResourceRosterLevelAverage>();
for (const [level, bucket] of levelBuckets.entries()) {
const lcrAverage =
bucket.lcr.length > 0
? Math.round((bucket.lcr.reduce((sum, value) => sum + value, 0) / bucket.lcr.length) * 100)
: null;
const ucrAverage =
bucket.ucr.length > 0
? Math.round((bucket.ucr.reduce((sum, value) => sum + value, 0) / bucket.ucr.length) * 100)
: null;
levelAverages.set(level, {
level,
sampleCount: Math.max(bucket.lcr.length, bucket.ucr.length),
lcrCents: lcrAverage,
ucrCents: ucrAverage,
});
}
return {
rates,
levelAverages,
warnings,
};
}