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.
154 lines
5.2 KiB
TypeScript
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>
|
|
);
|
|
}
|