Files
alpenwerk-hr/app/(app)/reports/page.tsx
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

154 lines
5.2 KiB
TypeScript

import { Suspense } from "react";
import { ReportsPageClient } from "@/components/reports/ReportsPageClient";
import {
aggregateEvents,
aggregateReport,
parseEventDateParam,
parseEventGroupDimension,
parseEventSplitDimension,
parseEventSubtype,
parseEventType,
parseGroupDimension,
parseIsoDateParam,
parseMeasure,
parseMode,
parseSplitDimension,
sumValues,
totalForRows,
} from "@/lib/reports";
import { parseCriteria } from "@/lib/report-criteria";
import { loadEventHistory, loadOrgLookups, loadSnapshotEmployees } from "@/lib/reports-data";
import { currentUserId } from "@/lib/auth/session";
import { withUser } from "@/lib/db";
// Nur die Parameter, die diese Seite selbst auswertet. Die Auswahlkriterien
// stehen ebenfalls in der Adresszeile, werden aber geschlossen von
// parseCriteria gelesen — siehe lib/report-criteria.ts.
type SearchParams = Record<string, string | string[] | undefined> & {
mode?: string;
measure?: string;
group?: string;
split?: string;
division?: string;
location?: string;
status?: string;
asOf?: string;
eventType?: string;
eventSubtype?: string;
from?: string;
to?: string;
};
export default async function ReportsPage({ searchParams }: { searchParams: Promise<SearchParams> }) {
const params = await searchParams;
const mode = parseMode(params.mode);
// Both modes are parsed up front so the data load can start before
// anything is awaited. They group by different dimension sets, so the
// parsed values stay separate rather than being widened into one type.
const asOf = parseIsoDateParam(params.asOf);
const eventGroup = parseEventGroupDimension(params.group);
const eventSplit = parseEventSplitDimension(params.split);
const eventType = parseEventType(params.eventType);
const eventSubtype = parseEventSubtype(params.eventSubtype, eventType);
const from = parseEventDateParam(params.from);
const to = parseEventDateParam(params.to);
const measure = parseMeasure(params.measure);
const group = parseGroupDimension(params.group);
const split = parseSplitDimension(params.split);
const criteria = parseCriteria((k) => (typeof params[k] === "string" ? (params[k] as string) : undefined));
// The report data depends on neither the org lookups nor on who is signed
// in, so all three go out together. Against a hosted database a round trip
// costs about as much as the query itself, which made this page's three
// sequential waves its dominant cost.
const userId = await currentUserId();
// Alles in einer Transaktion — dort gilt der Sitzungskontext, und der
// Lesestand ist über alle Abfragen hinweg derselbe. Vorher waren es drei
// Wellen nacheinander, was gegen eine entfernte Datenbank der teuerste
// Teil dieser Seite war.
const { lookups, units, locations, events, employees, savedReports } = await withUser(userId, async (tx) => {
const [{ lookups, units, locations }, events, employees, savedReports] = await Promise.all([
loadOrgLookups(tx),
mode === "events"
? loadEventHistory(tx, {
eventType: eventType ?? undefined,
eventSubtype: eventSubtype ?? undefined,
division: params.division,
location: params.location,
from,
to,
})
: Promise.resolve([]),
mode === "snapshot"
? loadSnapshotEmployees(tx, {
division: params.division,
location: params.location,
status: params.status,
criteria,
asOf,
})
: Promise.resolve([]),
userId
? tx
.selectFrom("saved_reports")
.select(["id", "name", "config"])
.where("created_by", "=", userId)
.orderBy("created_at", "desc")
.execute()
: Promise.resolve([]),
]);
return { lookups, units, locations, events, employees, savedReports };
});
if (mode === "events") {
const rows = aggregateEvents(events, eventGroup, eventSplit, lookups);
return (
<Suspense>
<ReportsPageClient
mode="events"
eventGroup={eventGroup}
eventSplit={eventSplit ?? ""}
eventType={eventType ?? ""}
eventSubtype={eventSubtype ?? ""}
eventFilters={{ division: params.division ?? "", location: params.location ?? "", from: from ?? "", to: to ?? "" }}
rows={rows}
total={sumValues(rows)}
recordCount={events.length}
units={units}
locations={locations}
savedReports={savedReports}
/>
</Suspense>
);
}
const rows = aggregateReport(employees, measure, group, split, lookups, asOf);
return (
<Suspense>
<ReportsPageClient
mode="snapshot"
measure={measure}
group={group}
split={split ?? ""}
asOf={asOf ?? ""}
filters={{
division: params.division ?? "",
location: params.location ?? "",
status: params.status ?? "",
}}
criteria={criteria}
rows={rows}
total={totalForRows(rows, measure)}
recordCount={employees.length}
units={units}
locations={locations}
savedReports={savedReports}
/>
</Suspense>
);
}