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 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, dependentsCounts: Map, asOf?: string ): ExportColumn[] { const columns: ExportColumn[] = [ { 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: "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; }