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:
60
app/api/export/cornerstone/route.ts
Normal file
60
app/api/export/cornerstone/route.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { NextResponse, type NextRequest } from "next/server";
|
||||
import { baueCornerstoneZeile, CORNERSTONE_CSV, cornerstoneSpalten, type CornerstoneKontext } from "@/lib/cornerstone";
|
||||
import { loadKontierungen } from "@/lib/cost-centers";
|
||||
import { exportFilename, exportResponseHeaders, toCsv, toXlsx } from "@/lib/export";
|
||||
import { exportParameter, ladeExportMitarbeiter } from "@/lib/export-auswahl";
|
||||
import { todayIso } from "@/lib/format";
|
||||
import { requireHrUser } from "@/lib/auth/require-hr";
|
||||
import { withUser } from "@/lib/db";
|
||||
|
||||
// Der Export für Cornerstone — Spalten und Zuordnung stehen in
|
||||
// lib/cornerstone.ts.
|
||||
//
|
||||
// Dieselben Personen wie im vollständigen Export daneben, mit denselben
|
||||
// Filtern (lib/export-auswahl.ts). Wer nur die Aktiven einspielen will,
|
||||
// stellt das auf der Berichtsseite ein, bevor er exportiert.
|
||||
export async function GET(request: NextRequest) {
|
||||
const gate = await requireHrUser();
|
||||
if ("denied" in gate) return gate.denied;
|
||||
|
||||
const { format, asOf, filters } = exportParameter(request.nextUrl.searchParams);
|
||||
const stichtag = asOf ?? todayIso();
|
||||
|
||||
const { zeilen } = await withUser(gate.userId, async (tx) => {
|
||||
const { rows, lookups, orgMaps, managerNummer } = await ladeExportMitarbeiter(tx, { asOf, filters });
|
||||
|
||||
// Die beiden Angaben, die die gemeinsame Auswahl nicht mitbringt: die
|
||||
// Kostenstelle hängt an der Planstelle, der Wiedereintritt an der
|
||||
// Historie. Beide erst jetzt, weil erst jetzt feststeht, um wen es geht.
|
||||
const positionIds = [...new Set(rows.map((r) => r.position_id).filter((id): id is string => Boolean(id)))];
|
||||
const [kostenstelle, wiedereintritte] = await Promise.all([
|
||||
loadKontierungen(tx, { asOf: stichtag, positionIds }),
|
||||
tx
|
||||
.selectFrom("employee_history")
|
||||
.select("employee_id")
|
||||
.where("event_type", "=", "Wiedereintritt")
|
||||
.distinct()
|
||||
.execute(),
|
||||
]);
|
||||
|
||||
const kontext: CornerstoneKontext = {
|
||||
orgMaps,
|
||||
standortName: lookups.locationName,
|
||||
managerNummer,
|
||||
kostenstelle,
|
||||
wiedereingestellt: new Set(wiedereintritte.map((w) => w.employee_id)),
|
||||
stichtag,
|
||||
};
|
||||
|
||||
return { zeilen: rows.map((r) => baueCornerstoneZeile(r, kontext)) };
|
||||
});
|
||||
|
||||
const filename = exportFilename("cornerstone-export", format);
|
||||
// Form und Spalten kommen beide aus lib/cornerstone.ts — siehe dort.
|
||||
const body =
|
||||
format === "xlsx"
|
||||
? await toXlsx(zeilen, cornerstoneSpalten(), "Worksheet")
|
||||
: toCsv(zeilen, cornerstoneSpalten(), CORNERSTONE_CSV);
|
||||
// Derselbe Umweg über Blob wie in den anderen Exporten (TS#59417).
|
||||
return new NextResponse(new Blob([body as BlobPart]), { headers: exportResponseHeaders(filename, format) });
|
||||
}
|
||||
Reference in New Issue
Block a user