import type { Tx } from "./db"; import { jsonArrayFrom, jsonObjectFrom, zeitstempel } from "./db/json"; import { besetzungenAbfrage, pickPlacements } from "./placement"; import { buildOrgMaps, orgMapsAbfragen, type OrgEb } from "./org"; import { offeneStellenAbfrage, resolveOpenPositions, type OffeneStelle } from "./positions"; import type { HistoryEventType } from "./supabase/types"; import type { AnstehendArt } from "./dashboard-filter"; // Was die Übersichtsseite liest — in zwei Rundreisen statt in dreizehn. // // Hier und nicht in der Seite, damit sich die Zahl der Rundreisen messen und // das Ergebnis gegen den alten Weg halten lässt, ohne eine React-Komponente // aufzubauen. export type DashboardParams = { userId: string | null; today: string; yearStart: string; yearEnd: string; bisIso: string; arten: AnstehendArt[]; }; export async function loadDashboardData(tx: Tx, p: DashboardParams) { const { userId, today, yearStart, yearEnd, bisIso, arten } = p; const zeigt = (art: AnstehendArt) => arten.includes(art); // Elf Abfragen standen hier in einem Promise.all, das keine // Gleichzeitigkeit war: eine Transaktion hängt an einer Verbindung, und // über eine Verbindung laufen Abfragen nacheinander. Bei rund 36 ms // Umlaufzeit war das eine knappe Sekunde Warten für Daten, die zusammen // keine 200 kB wiegen. Jetzt: eine Rundreise, und eine zweite für die // offenen Stellen, deren zweite Hälfte vom Ergebnis der ersten abhängt. // Der Weg dahin steht in lib/db/json.ts. const countIn = (eb: OrgEb, types: readonly HistoryEventType[]) => eb .selectFrom("employee_history") .select(({ fn }) => fn.countAll().as("anzahl")) .where("event_type", "in", [...types]) .where("event_date", ">=", yearStart) .where("event_date", "<=", yearEnd); const g = await tx .selectNoFrom((eb) => [ jsonArrayFrom( eb .selectFrom("hire_drafts") .select(["id", "step", "payload"]) .select((x) => zeitstempel(x.ref("updated_at")).as("updated_at")) .where("created_by", "=", userId ?? "") .orderBy("updated_at", "desc") ).as("drafts"), jsonArrayFrom( eb .selectFrom("employees") .select(["id", "weekly_hours", "entry_date", "exit_date", "karenz_start_date", "karenz_return_date"]) .orderBy("id") ).as("staffRows"), // Entries/exits count history events, which is what the linked report // counts too. `entry_date` would also sweep up rehires, whose event is // logged as 'Wiedereintritt' — the tile and its destination then showed // different numbers for the same year. jsonObjectFrom(countIn(eb, ["Eintritt", "Wiedereintritt"])).as("hiresYtd"), jsonObjectFrom(countIn(eb, ["Austritt"])).as("exitsYtd"), ...orgMapsAbfragen(eb), jsonArrayFrom(offeneStellenAbfrage(eb, today)).as("open"), jsonArrayFrom(besetzungenAbfrage(eb)).as("placementRows"), // Abgewählte Arten werden gar nicht erst gelesen — die Karte zeigt sie // ohnehin nicht, und eine Teilabfrage, deren Ergebnis niemand ansieht, // ist eine Teilabfrage zu viel. jsonArrayFrom( eb .selectFrom("employees") .select(["id", "first_name", "last_name", "entry_date"]) .where("status", "=", "Geplant") .where("entry_date", ">=", today) .where("entry_date", "<=", bisIso) .where((e) => e.lit(zeigt("hire"))) ).as("upcomingHires"), jsonArrayFrom( eb .selectFrom("employees") .select(["id", "first_name", "last_name", "exit_date"]) .where("exit_date", "is not", null) .where("exit_date", ">=", today) .where("exit_date", "<=", bisIso) .where((e) => e.lit(zeigt("exit"))) ).as("upcomingExits"), jsonArrayFrom( eb .selectFrom("employees") .select(["id", "first_name", "last_name", "karenz_return_date"]) .where("status", "=", "Karenz") .where("karenz_return_date", "is not", null) .where("karenz_return_date", ">=", today) .where("karenz_return_date", "<=", bisIso) .where((e) => e.lit(zeigt("return"))) ).as("upcomingReturns"), // Wiedervorlagen. Sie verhalten sich anders als der Rest dieser Karte: // ein Eintritt am Montag ist am Dienstag vorbei, eine Wiedervorlage // nicht. Deshalb gibt es hier **keine untere Grenze** — was fällig war // und nicht abgehakt wurde, bleibt stehen, bis jemand „Erledigt" klickt. // Eine Aufgabe, die von selbst aus der Liste rutscht, ist eine // vergessene Aufgabe. jsonArrayFrom( eb .selectFrom("employee_notes as n") .innerJoin("employees as e", "e.id", "n.employee_id") .select(["n.id", "n.employee_id", "n.due_date", "n.category", "n.note_text", "e.first_name", "e.last_name"]) .where("n.done", "=", false) .where("n.due_date", "is not", null) .where("n.due_date", "<=", bisIso) .where((x) => x.lit(zeigt("note"))) .orderBy("n.due_date") ).as("upcomingNotes"), jsonArrayFrom( eb .selectFrom("employee_history as h") .leftJoin("employees as e", "e.id", "h.employee_id") .select([ "h.id", "h.employee_id", "h.event_date", "h.event_type", "h.description", "e.first_name", "e.last_name", ]) .orderBy("h.event_date", "desc") .orderBy("h.created_at", "desc") .limit(10) ).as("history"), ]) .executeTakeFirstOrThrow(); const orgMaps = buildOrgMaps(g.units as never, g.locations as never); return { drafts: userId ? g.drafts : [], staffRows: g.staffRows, hiresYtd: Number(g.hiresYtd?.anzahl ?? 0), exitsYtd: Number(g.exitsYtd?.anzahl ?? 0), openPositions: await resolveOpenPositions(tx, orgMaps, g.open as OffeneStelle[], today), orgMaps, placements: pickPlacements(g.placementRows as never, today), upcomingHires: g.upcomingHires, upcomingExits: g.upcomingExits, upcomingReturns: g.upcomingReturns, upcomingNotes: g.upcomingNotes, history: g.history, }; }