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, parseEventDateParam, parseEventGroupDimension, parseEventSplitDimension, parseEventType, parseGroupDimension, parseIsoDateParam, parseMeasure, parseMode, parseSplitDimension, sortKeysForDimension, sumValues, totalForRows, type EventGroupDimension, type GroupDimension, type Measure, type ReportRow, } from "@/lib/reports"; import { loadEventHistory, loadOrgLookups, loadSnapshotEmployees } from "@/lib/reports-data"; import { requireHrUser } from "@/lib/supabase/auth"; import { createClient } from "@/lib/supabase/server"; // 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 denied = await requireHrUser(supabase); if (denied) return denied; const params = request.nextUrl.searchParams; const format = params.get("format") === "xlsx" ? "xlsx" : "csv"; const mode = parseMode(params.get("mode")); const { lookups } = await loadOrgLookups(supabase); let rows: ReportRow[]; let columns: ExportColumn[]; let filenameBase: string; if (mode === "events") { const group = parseEventGroupDimension(params.get("group")); const split = parseEventSplitDimension(params.get("split")); const eventType = parseEventType(params.get("eventType")); const events = await loadEventHistory(supabase, { eventType: eventType ?? undefined, division: params.get("division") ?? undefined, location: params.get("location") ?? undefined, from: parseEventDateParam(params.get("from")), to: parseEventDateParam(params.get("to")), }); rows = aggregateEvents(events, group, split, lookups); columns = eventReportColumns(rows, group, split, sumValues(rows)); filenameBase = `ereignisse-${eventType ?? "alle"}-${group}`; } else { const measure = parseMeasure(params.get("measure")); const group = parseGroupDimension(params.get("group")); const split = parseSplitDimension(params.get("split")); const asOf = parseIsoDateParam(params.get("asOf")); 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 = sortKeysForDimension(Array.from(new Set(rows.flatMap((r) => r.split?.map((s) => s.key) ?? []))), split); 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; }