Files
CapaKraken/packages/application/src/use-cases/dispo-import/parse-resource-roster-master-workbook.ts
T
Hartmut cd78f72f33 chore: full technical rename planarchy → capakraken
Complete rename of all technical identifiers across the codebase:

Package names (11 packages):
- @planarchy/* → @capakraken/* in all package.json, tsconfig, imports

Import statements: 277 files, 548 occurrences replaced

Database & Docker:
- PostgreSQL user/db: planarchy → capakraken
- Docker volumes: planarchy_pgdata → capakraken_pgdata
- Connection strings updated in docker-compose, .env, CI

CI/CD:
- GitHub Actions workflow: all filter commands updated
- Test database credentials updated

Infrastructure:
- Redis channel: planarchy:sse → capakraken:sse
- Logger service name: planarchy-api → capakraken-api
- Anonymization seed updated
- Start/stop/restart scripts updated

Test data:
- Seed emails: @planarchy.dev → @capakraken.dev
- E2E test credentials: all 11 spec files updated
- Email defaults: @planarchy.app → @capakraken.app
- localStorage keys: planarchy_* → capakraken_*

Documentation: 30+ .md files updated

Verification:
- pnpm install: workspace resolution works
- TypeScript: only pre-existing TS2589 (no new errors)
- Engine: 310/310 tests pass
- Staffing: 37/37 tests pass

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-27 13:18:09 +01:00

179 lines
5.4 KiB
TypeScript

import { normalizeCanonicalResourceIdentity } from "@capakraken/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,
};
}