Cornerstone fuehrt jede Person unter einer eigenen Kennung. Der Export dorthin trug in "User ID" und "Username" bisher die Alpenwerk-UUID -- richtig, solange es nichts Besseres gab, aber nicht die Kennung, unter der Cornerstone die Person kennt. Jetzt steht dort diese Spalte. Freiwillig, weil die Zuordnungstabelle des Kunden fuer 434 der 784 Personen keine Kennung liefert. Eindeutig, weil eine Kennung genau einer Person gehoert. Als Text, weil fuehrende Nullen in einer numerischen Spalte verlorengingen. Ohne hinterlegte Kennung bleiben User ID und Username **leer**. Ein Rueckfall auf die UUID braechte zwei Kennungsarten in eine Datei, ohne dass es auffiele, und legte in Cornerstone eine zweite Person neben der bestehenden an. Eine fehlende Angabe soll fehlen; dafuer gibt es einen eigenen Test. Fuenf Funktionen mussten mit -- dieselbe Liste und derselbe Grund wie bei der Firmen-E-Mail: hire_employee und rehire_employee teilen sich den Schritt "Person", change_employee_data macht das Feld aenderbar, apply_due_pending_changes sorgt dafuer, dass eine datierte Aenderung nicht verfaellt, app_feld_karte haelt den Eintrag in der Historie richtigstellbar. Die Migration ist wieder erzeugt, nicht abgeschrieben, und prueft jede der fuenf einzeln. Erfasst wird das Feld in der Akte, in "Daten aendern", bei Einstellung und Wiedereintritt sowie ueber den Massenimport; es steht im Mitarbeiterexport und fuellt im Cornerstone-Export User ID und Username.
123 lines
7.5 KiB
TypeScript
123 lines
7.5 KiB
TypeScript
import { NextResponse, type NextRequest } from "next/server";
|
|
import { statusLabel } from "@/lib/absence";
|
|
import { exportFilename, exportResponseHeaders, toCsv, toXlsx, type ExportColumn } from "@/lib/export";
|
|
import { exportParameter, ladeExportMitarbeiter, type ExportMitarbeiter } from "@/lib/export-auswahl";
|
|
import { sortiereWochentage } from "@/lib/wochentage";
|
|
import { deriveStatusAsOf, type OrgLookups } from "@/lib/reports";
|
|
import { requireHrUser } from "@/lib/auth/require-hr";
|
|
import { withUser } from "@/lib/db";
|
|
|
|
type EmployeeRow = ExportMitarbeiter;
|
|
|
|
// Full raw data dump — every column on `employees`, not just the fields a
|
|
// pivot report groups by. Respects the same division/location/status/
|
|
// employment filters as the Berichte page. Wer darin steht, entscheidet
|
|
// lib/export-auswahl.ts — dieselbe Auswahl wie im Honestly-Export.
|
|
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 { rows, lookups, managerName, dependentsCounts } = await withUser(gate.userId, (tx) =>
|
|
ladeExportMitarbeiter(tx, { asOf, filters })
|
|
);
|
|
|
|
const columns = employeeExportColumns(lookups, managerName, dependentsCounts, asOf);
|
|
const filename = exportFilename("mitarbeiter-export", format);
|
|
|
|
const body = format === "xlsx" ? await toXlsx(rows, columns, "Mitarbeiter") : toCsv(rows, columns);
|
|
// TS 5.9's Uint8Array<ArrayBufferLike> vs DOM's BlobPart/ArrayBuffer<> generic
|
|
// mismatch (microsoft/TypeScript#59417) — a real Uint8Array works fine here.
|
|
return new NextResponse(new Blob([body as BlobPart]), { headers: exportResponseHeaders(filename, format) });
|
|
}
|
|
|
|
function employeeExportColumns(
|
|
lookups: OrgLookups,
|
|
managerName: Map<string, string>,
|
|
dependentsCounts: Map<string, number>,
|
|
asOf?: string
|
|
): ExportColumn<EmployeeRow>[] {
|
|
const columns: ExportColumn<EmployeeRow>[] = [
|
|
{ header: "Pers.-Nr.", get: (e) => e.personnel_number },
|
|
{ header: "Vorname", get: (e) => e.first_name },
|
|
{ header: "Nachname", get: (e) => e.last_name },
|
|
{ header: "Geschlecht", get: (e) => (e.gender === "m" ? "männlich" : "weiblich") },
|
|
{ header: "Geburtsdatum", get: (e) => e.birth_date, kind: "date" },
|
|
{ header: "SV-Nummer", get: (e) => e.sv_nummer },
|
|
{ header: "Staatsbürgerschaft", get: (e) => e.nationality },
|
|
{ header: "Aufenthaltstitel", get: (e) => e.hat_aufenthaltstitel },
|
|
{ header: "Aufenthaltstitel bis", get: (e) => e.aufenthaltstitel_bis, kind: "date" },
|
|
{ header: "Adresse", get: (e) => e.address },
|
|
{ header: "Postleitzahl", get: (e) => e.postal_code },
|
|
{ header: "Ort", get: (e) => e.city },
|
|
{ header: "Wohnsitzland", get: (e) => e.address_country },
|
|
{ header: "Private E-Mail", get: (e) => e.email },
|
|
{ header: "Firmen-E-Mail", get: (e) => e.company_email },
|
|
{ header: "Cornerstone-ID", get: (e) => e.cornerstone_id },
|
|
{ header: "Private Telefonnummer", get: (e) => e.phone },
|
|
{ header: "Bereich", get: (e) => (e.org_unit_id ? (lookups.divisionName.get(e.org_unit_id) ?? "") : "") },
|
|
{ header: "Abteilung", get: (e) => (e.org_unit_id ? (lookups.departmentName.get(e.org_unit_id) ?? "") : "") },
|
|
{ header: "Team", get: (e) => (e.org_unit_id ? (lookups.teamName.get(e.org_unit_id) ?? "") : "") },
|
|
{ header: "Standort", get: (e) => lookups.locationName.get(e.location_id) ?? "" },
|
|
{ header: "Position", get: (e) => e.job_title },
|
|
{ header: "Vorgesetzte:r", get: (e) => (e.manager_id ? (managerName.get(e.manager_id) ?? "") : "") },
|
|
{ header: "Planstelle", get: (e) => e.position_number },
|
|
{ header: "Leitungsplanstelle", get: (e) => e.is_chief },
|
|
{ header: "Beschäftigungsausmaß", get: (e) => e.employment_type },
|
|
{ header: "Wochenstunden", get: (e) => e.weekly_hours },
|
|
// Seit lib/wochentage.ts wird sortiert gespeichert; der Bestand kann aber
|
|
// noch unsortierte Zeilen aus der Zeit davor tragen. Hier bleibt es
|
|
// deshalb stehen — im Export kostet es nichts und macht die Spalte
|
|
// unabhängig davon, wann eine Zeile zuletzt gespeichert wurde.
|
|
{ header: "Arbeitstage", get: (e) => sortiereWochentage(e.work_days).join(", ") },
|
|
{ header: "Vertragsart", get: (e) => e.contract_type },
|
|
{ header: "Befristet bis", get: (e) => e.contract_end_date, kind: "date" },
|
|
{ header: "Beschäftigtengruppe", get: (e) => e.worker_type },
|
|
{ header: "Mitarbeiterart", get: (e) => e.mitarbeiterart },
|
|
{ header: "Kollektivvertrag", get: (e) => e.collective_agreement },
|
|
{ header: "Betriebsrat", get: (e) => e.is_betriebsrat },
|
|
{ header: "Dienstwagen", get: (e) => e.has_dienstwagen },
|
|
{ header: "Antriebsart", get: (e) => e.dienstwagen_art ?? "" },
|
|
{ header: "Besonderer Kündigungsschutz", get: (e) => e.has_kuendigungsschutz },
|
|
{ header: "Personenkreis", get: (e) => e.kuendigungsschutz_grund ?? "" },
|
|
{ header: "Kündigungsschutz ab", get: (e) => e.kuendigungsschutz_ab, kind: "date" },
|
|
{ header: "Kündigungsschutz bis", get: (e) => e.kuendigungsschutz_bis, kind: "date" },
|
|
// Der Bericht, den der Workshop verlangt hat (Anforderung 5a): wer ist
|
|
// begünstigt behindert, mit wieviel Prozent, ab und bis wann. Vier
|
|
// Spalten statt einer zusammengesetzten — in Excel wird danach gefiltert
|
|
// und summiert, und ein Text wie „50% seit 2019" liesse beides nicht zu.
|
|
{ header: "Begünstigt behindert", get: (e) => e.ist_beguenstigt_behindert },
|
|
{ header: "Grad der Behinderung (%)", get: (e) => e.behinderung_grad ?? "" },
|
|
{ header: "Bescheid ab", get: (e) => e.behinderung_ab, kind: "date" },
|
|
{ header: "Bescheid bis", get: (e) => e.behinderung_bis, kind: "date" },
|
|
{ header: "Teilzeitvariante", get: (e) => e.teilzeit_art ?? "" },
|
|
{ header: "Teilzeit bis", get: (e) => e.teilzeit_bis, kind: "date" },
|
|
{ header: "Notfallkontakt", get: (e) => e.emergency_contact_name ?? "" },
|
|
{ header: "Notfallkontakt Telefon", get: (e) => e.emergency_contact_phone ?? "" },
|
|
{ header: "Notfallkontakt Verhältnis", get: (e) => e.emergency_contact_relation ?? "" },
|
|
{ header: "Laterale Führung", get: (e) => e.is_laterale_fuehrung },
|
|
{ header: "C-Level", get: (e) => e.is_c_level },
|
|
{ header: "Paygrade", get: (e) => e.paygrade },
|
|
{ header: "Herkunft", get: (e) => e.source },
|
|
// The export shows the display name, not the raw enum value — a payroll
|
|
// hand-off saying "Karenz" for what the app calls Langzeitabwesenheit
|
|
// would just cause questions.
|
|
{ header: "Status", get: (e) => statusLabel(e.status) },
|
|
{ header: "Art der Langzeitabwesenheit", get: (e) => e.absence_type },
|
|
{ header: "Eintrittsdatum", get: (e) => e.entry_date, kind: "date" },
|
|
{ header: "Austrittsdatum", get: (e) => e.exit_date, kind: "date" },
|
|
{ header: "Austrittsgrund", get: (e) => e.exit_reason },
|
|
// Eigene Spalte neben dem Grund: aus ihm folgt sie nicht. Leer heisst
|
|
// „nicht erfasst" und nicht „weder noch".
|
|
{ header: "Freiwillig/unfreiwillig", get: (e) => e.austrittsart ?? "" },
|
|
{ header: "Abwesenheit ab", get: (e) => e.karenz_start_date, kind: "date" },
|
|
{ header: "Rückkehr geplant", get: (e) => e.karenz_return_date, kind: "date" },
|
|
{ header: "Anzahl Angehörige", get: (e) => dependentsCounts.get(e.id) ?? 0 },
|
|
];
|
|
if (asOf) {
|
|
const statusIndex = columns.findIndex((c) => c.header === "Status");
|
|
columns.splice(statusIndex + 1, 0, { header: `Status zum Stichtag (${asOf})`, get: (e) => deriveStatusAsOf(e, asOf) });
|
|
}
|
|
return columns;
|
|
}
|