Add the Cornerstone export to Berichte

The 57 columns of the Cornerstone user import, in the given order and
spelling, offered as CSV and Excel under the Honestly report. Same
people and same filters as the full export.

Two things about the file itself, both of which would have failed
quietly. Cornerstone reads comma-separated, while toCsv writes the
semicolons and the BOM that German Excel wants -- the BOM would have
hidden inside the name of the first column, so "User ID" would have
matched nothing in the mapping. toCsv now takes the form as an argument
and keeps its old defaults; the Cornerstone form lives next to the
columns so a test can hold both. Quoting follows the delimiter now,
otherwise a comma in an address would split the row.

Dates go out day-first, matching the import setting. An ISO date is
read as a different, equally valid date and nobody notices.

Gender maps to Cornerstone's own values; anything unexpected becomes
"not specified" rather than empty, because an invalid value makes
Cornerstone reject the whole row, not just the field.

Status and Employment Status come from separate rules: somebody on
Karenz has a working account and is not working, and filling both from
one value gets one of the two wrong.

Four fields carry visible placeholders because the leading systems do
not supply their identifiers yet: Division ID, Doxis, Interflex,
LGVplus. Home Phone and Personal Email stay empty on purpose -- what
leaves the house is the business data.

Not verified against a running Cornerstone import, and not seen in a
browser; there is no database reachable here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-25 11:21:25 +02:00
parent e11544b8da
commit 478364e2d0
6 changed files with 659 additions and 9 deletions

View File

@@ -19,6 +19,7 @@ import type { Database } from "./types";
* über die Planstelle und die abgeleitete Berichtslinie. */
export type ExportMitarbeiter = Database["public"]["Tables"]["employees"]["Row"] & {
org_unit_id: string | null;
position_id: string | null;
position_number: string | null;
is_chief: boolean;
manager_id: string | null;
@@ -57,6 +58,8 @@ export async function ladeExportMitarbeiter(
lookups: OrgLookups;
orgMaps: OrgMaps;
managerName: Map<string, string>;
/** Personalnummer je Kennung — für Exporte, die über Nummern verweisen. */
managerNummer: Map<string, number>;
dependentsCounts: Map<string, number>;
}> {
const statuses = parseStatuses(filters.status);
@@ -83,7 +86,7 @@ export async function ladeExportMitarbeiter(
const [employees, lookupResult, allEmployees, dependentsCounts, placements, lines] = await Promise.all([
employeeQuery().execute(),
loadOrgLookups(tx),
tx.selectFrom("employees").select(["id", "first_name", "last_name"]).orderBy("id").execute(),
tx.selectFrom("employees").select(["id", "first_name", "last_name", "personnel_number"]).orderBy("id").execute(),
loadDependentsCounts(tx),
loadPlacements(tx, { asOf: stichtag }),
loadReportingLineMap(tx, stichtag),
@@ -103,6 +106,7 @@ export async function ladeExportMitarbeiter(
{
...e,
org_unit_id: orgUnitId,
position_id: placement?.current ? placement.positionId : null,
position_number: placement?.positionNumber ?? null,
is_chief: placement?.isChief ?? false,
manager_id: lines.get(e.id)?.acting_manager_id ?? null,
@@ -115,6 +119,7 @@ export async function ladeExportMitarbeiter(
lookups: lookupResult.lookups,
orgMaps,
managerName: new Map(allEmployees.map((e) => [e.id, fmtName(e.first_name, e.last_name)])),
managerNummer: new Map(allEmployees.map((e) => [e.id, e.personnel_number])),
dependentsCounts,
};
}