Add the Honestly survey export to Berichte

A participant list for the employee survey on honestly.de, offered as
CSV and Excel right under the full data export. Columns: Personalnummer,
Email, Firstname, Last Name, Language, Location, OU, OU+1, ... , Role.
Language is always "de", Role always "Respondee".

The org columns run bottom-up: OU is the person's own unit, OU+1 the one
above, up to the top node. Their number follows the deepest chain in
the file, shorter chains are padded with empty cells, and there is
always at least one OU column so the file keeps its shape.

It exports exactly the people the full export would, with the same
filters. To guarantee that, the selection moved out of the full export's
route into lib/export-auswahl.ts and both routes use it; the full
export's columns are untouched.

Email is employees.email, which is the private address and optional --
there is no work address in the schema. Missing addresses stay empty
rather than getting a placeholder that would receive an invitation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-18 15:04:45 +02:00
parent 7b7562799d
commit 779d6116b2
6 changed files with 405 additions and 92 deletions

View File

@@ -0,0 +1,29 @@
import { NextResponse, type NextRequest } from "next/server";
import { exportFilename, exportResponseHeaders, toCsv, toXlsx } from "@/lib/export";
import { exportParameter, ladeExportMitarbeiter } from "@/lib/export-auswahl";
import { baueHonestlyZeilen, honestlySpalten } from "@/lib/honestly";
import { requireHrUser } from "@/lib/auth/require-hr";
import { withUser } from "@/lib/db";
// Der Export für die Mitarbeiterbefragung bei Honestly — Spalten und feste
// Werte stehen in lib/honestly.ts.
//
// Dieselben Personen wie im vollständigen Export daneben, mit denselben
// Filtern (lib/export-auswahl.ts). Wer eine Befragung nur für einen Bereich
// oder nur für Aktive will, stellt das auf der Berichtsseite ein, bevor er
// exportiert.
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, orgMaps } = await withUser(gate.userId, (tx) => ladeExportMitarbeiter(tx, { asOf, filters }));
const { zeilen, tiefe } = baueHonestlyZeilen(rows, orgMaps, lookups.locationName);
const spalten = honestlySpalten(tiefe);
const filename = exportFilename("honestly-export", format);
const body = format === "xlsx" ? await toXlsx(zeilen, spalten, "Honestly") : toCsv(zeilen, spalten);
// Derselbe Umweg über Blob wie im vollständigen Export (TS#59417).
return new NextResponse(new Blob([body as BlobPart]), { headers: exportResponseHeaders(filename, format) });
}