import { NextResponse, type NextRequest } from "next/server"; import { exportFilename, exportResponseHeaders, toCsv, toXlsx, type ExportColumn } from "@/lib/export"; import { deriveStatusAsOf, parseIsoDateParam, parseStatuses, type OrgLookups } from "@/lib/reports"; import { loadDependentsCounts, loadOrgLookups, type ReportFilters } from "@/lib/reports-data"; import { requireHrUser } from "@/lib/supabase/auth"; import { fetchAllRows } from "@/lib/supabase/query"; import { createClient } from "@/lib/supabase/server"; import type { Database, EmploymentType, Weekday } from "@/lib/supabase/types"; type EmployeeRow = Database["public"]["Tables"]["employees"]["Row"]; // 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. With `asOf` (Stichtag), status // filtering happens against the *derived* status as of that date rather // than the live `status` column — see deriveStatusAsOf. export async function GET(request: NextRequest) { const supabase = await createClient(); const denied = await requireHrUser(supabase); if (denied) return denied; const params = request.nextUrl.searchParams; const format = params.get("format") === "xlsx" ? "xlsx" : "csv"; const asOf = parseIsoDateParam(params.get("asOf")); const filters: ReportFilters = { division: params.get("division") ?? undefined, location: params.get("location") ?? undefined, status: params.get("status") ?? undefined, employment: params.get("employment") ?? undefined, }; const statuses = parseStatuses(filters.status); function employeeQuery() { let query = supabase.from("employees").select("*").order("last_name").order("id"); if (filters.division) query = query.eq("division_id", filters.division); if (filters.location) query = query.eq("location_id", filters.location); if (filters.employment) query = query.eq("employment_type", filters.employment as EmploymentType); if (!asOf) query = query.in("status", statuses); return query; } const [employees, { lookups }, allEmployees, dependentsCounts] = await Promise.all([ fetchAllRows(employeeQuery), loadOrgLookups(supabase), fetchAllRows(() => supabase.from("employees").select("id, first_name, last_name").order("id")), loadDependentsCounts(supabase), ]); const managerName = new Map(allEmployees.map((e) => [e.id, `${e.first_name} ${e.last_name}`])); const rows = asOf ? employees.filter((e) => statuses.includes(deriveStatusAsOf(e, asOf))) : employees; 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) }); } const WEEKDAY_ORDER: Weekday[] = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"]; 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: "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: "E-Mail", get: (e) => e.email }, { header: "Telefon", get: (e) => e.phone }, { header: "Bereich", get: (e) => lookups.divisionName.get(e.division_id) ?? "" }, { header: "Abteilung", get: (e) => (e.team_id ? (lookups.departmentNameByTeam.get(e.team_id) ?? "") : "") }, { header: "Team", get: (e) => (e.team_id ? (lookups.teamName.get(e.team_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: "Führungskraft", get: (e) => e.is_lead }, { header: "Org-Level", get: (e) => e.org_level }, { header: "Beschäftigungsausmaß", get: (e) => e.employment_type }, { header: "Wochenstunden", get: (e) => e.weekly_hours }, // work_days is stored in click order (see RoleEmploymentFields), not // guaranteed chronological — re-sort Mo→So for the export. { header: "Arbeitstage", get: (e) => [...e.work_days].sort((a, b) => WEEKDAY_ORDER.indexOf(a as Weekday) - WEEKDAY_ORDER.indexOf(b as Weekday)).join(", ") }, { header: "Vertragsart", get: (e) => e.contract_type }, { header: "Befristet bis", get: (e) => e.contract_end_date, kind: "date" }, { header: "Angestellte:r / Arbeiter:in", get: (e) => e.worker_type }, { header: "Kollektivvertrag", get: (e) => e.collective_agreement }, { header: "Betriebsrat", get: (e) => e.is_betriebsrat }, { header: "Dienstwagen", get: (e) => e.has_dienstwagen }, { 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 }, { header: "Status", get: (e) => e.status }, { 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 }, { header: "Karenzbeginn", get: (e) => e.karenz_start_date, kind: "date" }, { header: "Karenz-Rückkehrdatum", 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; }