Files
alpenwerk-hr/app/api/export/events/route.ts
Andrei Laas 8d0c9b4b65 Workshop-Anforderungen, erster Teil: was ohne Migration geht
Anforderung 1 — Freiwilliger vs unfreiwilliger Austritt. Die Liste der
Beendigungsarten zieht aus TerminatePanel.tsx nach lib/beendigung.ts um: der
Berichtemanager braucht sie ebenso, und zwei Listen liefen auseinander. Zwei
neue Arten (Beendigung in der Probezeit, je Seite). Auf wessen Betreiben
beendet wurde, wird aus der Art **abgeleitet** und nicht daneben gespeichert
— als zweites freies Feld liesse sich "Entlassung, freiwillig" erfassen. Das
Dropdown im Formular schraenkt die Auswahl darunter ein.

Drei Gruppen statt zwei: Befristungsablauf geschieht auf niemandes
Betreiben, ein Nichtantritt ist kein Austritt. Beide einer Seite
zuzuschlagen wuerde jede Fluktuationsquote verfaelschen.

Anforderung 2 — Namensfilter in "Anstehend", ab neun Eintraegen.

Anforderung 3 — die zwei Unterschriftenfelder im gedruckten Blatt sind weg;
"Firmenfahrzeug" steht in beiden Checklisten. has_dienstwagen sagt, ob eines
zusteht, nicht ob es uebergeben wurde.

Anforderung 4 — "+794 weitere" ist ein Knopf geworden; die Namen waren
vorher nur ueber den Export erreichbar. Stammdatenaenderung und
Gehaltsanpassung stehen nicht mehr zur Auswahl: die eine entsteht bei jeder
geaenderten Telefonnummer, die andere ist ein totes Ereignis, seit das
Gehalt in Loga liegt. Neu ist der Untertyp — Beendigungsart beim Austritt,
Art der Abwesenheit bei der Langzeitabwesenheit, im Bericht und im Export.

Anforderung 10 — zwei Kacheln. "Aktives Dienstverhaeltnis" ist nicht
dasselbe wie "Aktive Mitarbeiter:innen": dort steht, wer heute arbeitet,
hier, mit wem ein Vertrag laeuft. Sichtbar waren 806 und 10, addieren musste
man selbst.
2026-09-15 22:35:47 +02:00

70 lines
3.4 KiB
TypeScript

import { NextResponse, type NextRequest } from "next/server";
import { exportFilename, exportResponseHeaders, toCsv, toXlsx, type ExportColumn } from "@/lib/export";
import {
EVENT_TYPE_LABELS,
parseEventDateParam,
parseEventSubtype,
parseEventType,
type OrgLookups,
type ReportEvent,
} from "@/lib/reports";
import { loadEventHistory, loadOrgLookups } from "@/lib/reports-data";
import { requireHrUser } from "@/lib/auth/require-hr";
import { withUser } from "@/lib/db";
// Full raw event-log dump — one row per employee_history entry in the
// selected period (default: current year), every event type unless one is
// picked, org columns resolved from each affected employee's current
// placement (see loadEventHistory).
export async function GET(request: NextRequest) {
const gate = await requireHrUser();
if ("denied" in gate) return gate.denied;
const params = request.nextUrl.searchParams;
const format = params.get("format") === "xlsx" ? "xlsx" : "csv";
const eventType = parseEventType(params.get("eventType"));
const { lookups, events } = await withUser(gate.userId, async (tx) => {
const [{ lookups }, events] = await Promise.all([
loadOrgLookups(tx),
loadEventHistory(tx, {
eventType: eventType ?? undefined,
// Ohne diese Zeile exportierte die Schaltfläche neben der Auswertung
// etwas anderes, als auf dem Bildschirm steht — die Einschränkung auf
// die Beendigungsart fiele beim Export still weg.
eventSubtype: parseEventSubtype(params.get("eventSubtype"), eventType) ?? undefined,
division: params.get("division") ?? undefined,
location: params.get("location") ?? undefined,
from: parseEventDateParam(params.get("from")),
to: parseEventDateParam(params.get("to")),
}),
]);
return { lookups, events };
});
const columns = eventExportColumns(lookups);
const filename = exportFilename(`ereignisse-${eventType ?? "alle"}`, format);
const body = format === "xlsx" ? await toXlsx(events, columns, "Ereignisse") : toCsv(events, columns);
// TS 5.9's Uint8Array<ArrayBufferLike> 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 eventExportColumns(lookups: OrgLookups): ExportColumn<ReportEvent>[] {
return [
{ header: "Datum", get: (e) => e.event_date, kind: "date" },
{ header: "Ereignistyp", get: (e) => EVENT_TYPE_LABELS[e.event_type] ?? e.event_type },
// Beendigungsart bzw. Art der Langzeitabwesenheit. Leer bei allen
// Ereignissen, die keinen Untertyp kennen — eine Versetzung hat keinen.
{ header: "Untertyp", get: (e) => e.subtype ?? "" },
{ header: "Vorname", get: (e) => e.first_name },
{ header: "Nachname", get: (e) => e.last_name },
{ header: "Position", get: (e) => e.job_title },
{ 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: "Beschreibung", get: (e) => e.description },
];
}