import { NextResponse, type NextRequest } from "next/server"; import { exportFilename, exportResponseHeaders, toCsv, toXlsx, type ExportColumn } from "@/lib/export"; import { aggregateEvents, aggregateReport, EVENT_GROUP_LABELS, GROUP_LABELS, MEASURE_LABELS, sumValues, totalForRows, type EventGroupDimension, type GroupDimension, type Measure, type ReportRow, } from "@/lib/reports"; import { loadEventHistory, loadOrgLookups, loadSnapshotEmployees } from "@/lib/reports-data"; import { createClient } from "@/lib/supabase/server"; import type { HistoryEventType } from "@/lib/supabase/types"; // Exports exactly the pivot table currently on screen (same mode/measure or // event-type/group/split/filters, read from the query string the client // already keeps in the URL) as a flat table — one row per group, one column // per split value if a split is active. export async function GET(request: NextRequest) { const supabase = await createClient(); const { data: { user }, } = await supabase.auth.getUser(); if (!user) return NextResponse.json({ error: "Nicht angemeldet." }, { status: 401 }); const { data: profile } = await supabase.from("profiles").select("role, is_active").eq("id", user.id).maybeSingle(); if (profile?.role !== "hr" || profile?.is_active !== true) { return NextResponse.json({ error: "Nicht berechtigt." }, { status: 403 }); } const params = request.nextUrl.searchParams; const format = params.get("format") === "xlsx" ? "xlsx" : "csv"; const mode = params.get("mode") === "events" ? "events" : "snapshot"; const { lookups } = await loadOrgLookups(supabase); let rows: ReportRow[]; let columns: ExportColumn[]; let filenameBase: string; if (mode === "events") { const group = (params.get("group") as EventGroupDimension) || "event_type"; const split = (params.get("split") as EventGroupDimension) || null; const eventType = (params.get("eventType") as HistoryEventType) || undefined; const events = await loadEventHistory(supabase, { eventType, division: params.get("division") ?? undefined, location: params.get("location") ?? undefined, from: params.get("from") ?? undefined, to: params.get("to") ?? undefined, }); rows = aggregateEvents(events, group, split, lookups); columns = eventReportColumns(rows, group, split, sumValues(rows)); filenameBase = `ereignisse-${eventType ?? "alle"}-${group}`; } else { const measure = (params.get("measure") as Measure) || "headcount"; const group = (params.get("group") as GroupDimension) || "division"; const split = (params.get("split") as GroupDimension) || null; const asOf = params.get("asOf") ?? undefined; const employees = await loadSnapshotEmployees(supabase, { division: params.get("division") ?? undefined, location: params.get("location") ?? undefined, status: params.get("status") ?? undefined, employment: params.get("employment") ?? undefined, asOf, }); rows = aggregateReport(employees, measure, group, split, lookups, asOf); columns = snapshotReportColumns(rows, measure, group, split, totalForRows(rows, measure)); filenameBase = `bericht-${measure}-${group}`; } const filename = exportFilename(filenameBase, format); const body = format === "xlsx" ? await toXlsx(rows, columns, "Bericht") : 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 snapshotReportColumns( rows: ReportRow[], measure: Measure, group: GroupDimension, split: GroupDimension | null, total: number ): ExportColumn[] { const columns: ExportColumn[] = [{ header: GROUP_LABELS[group], get: (r) => r.key }]; if (split) { const splitKeys = Array.from(new Set(rows.flatMap((r) => r.split?.map((s) => s.key) ?? []))); for (const key of splitKeys) { columns.push({ header: key, get: (r) => Math.round((r.split?.find((s) => s.key === key)?.value ?? 0) * 100) / 100 }); } } columns.push( { header: MEASURE_LABELS[measure], get: (r) => Math.round(r.value * 100) / 100 }, { header: "Anzahl", get: (r) => r.count }, { header: "Anteil (%)", get: (r) => (total > 0 ? Math.round((r.value / total) * 1000) / 10 : 0) } ); return columns; } function eventReportColumns( rows: ReportRow[], group: EventGroupDimension, split: EventGroupDimension | null, total: number ): ExportColumn[] { const columns: ExportColumn[] = [{ header: EVENT_GROUP_LABELS[group], get: (r) => r.key }]; if (split) { const splitKeys = Array.from(new Set(rows.flatMap((r) => r.split?.map((s) => s.key) ?? []))); for (const key of splitKeys) { columns.push({ header: key, get: (r) => r.split?.find((s) => s.key === key)?.value ?? 0 }); } } columns.push( { header: "Anzahl", get: (r) => r.count }, { header: "Anteil (%)", get: (r) => (total > 0 ? Math.round((r.value / total) * 1000) / 10 : 0) } ); return columns; }