import type { Tx } from "./db"; import { jsonArrayFrom, jsonObjectFrom, zeitstempel } from "./db/json"; import { todayIso } from "./format"; import { baueOffeneNotizen, offeneNotizenAbfrage, type NotizZeile, type OpenNote } from "./notes"; import { buildOrgMaps, orgMapsAbfragen, type Location } from "./org"; import { offeneStellenAbfrage, resolveOpenPositions, type OffeneStelle, type OpenPositionResolved, } from "./positions"; // Was die Hülle jeder Seite braucht — in zwei Rundreisen statt in sechs. // // Vorher stand das im Layout selbst, als Promise.all, das wie Gleichzeitigkeit // aussah und keine war: eine Transaktion hängt an einer Verbindung, und über // eine Verbindung laufen Abfragen nacheinander. Bei rund 36 ms Umlaufzeit // kostete diese Hülle — die **jede** Seite mitlädt — eine halbe Sekunde // Warten für ein paar Kilobyte. Der Weg dahin steht in lib/db/json.ts. // // Hier und nicht im Layout, damit sich die Zahl der Rundreisen messen lässt, // ohne eine React-Komponente aufzubauen. export type ShellData = { profile: { full_name: string | null; email: string | null; role: string | null; is_active: boolean | null }; openPositions: OpenPositionResolved[]; locations: Location[]; drafts: { id: string; step: number; payload: Record; updated_at: string }[]; openNotes: OpenNote[]; }; /** * `null` heisst: angemeldet, aber nicht als HR freigeschaltet. * * Die Zugangsprüfung fragt gleichzeitig mit dem Rest statt davor. Das liest * ein paar Zeilen mehr, als eine gesperrte Person sehen dürfte, wirft sie aber * weg, ohne sie je auszuliefern — und die eigentliche Grenze ist ohnehin RLS, * nicht die Reihenfolge hier. */ export async function loadShellData(tx: Tx, userId: string): Promise { const asOf = todayIso(); const gelesen = await tx .selectNoFrom((eb) => [ jsonObjectFrom( eb.selectFrom("profiles").select(["full_name", "email", "role", "is_active"]).where("id", "=", userId) ).as("profile"), ...orgMapsAbfragen(eb), jsonArrayFrom(offeneStellenAbfrage(eb, asOf)).as("open"), jsonArrayFrom(offeneNotizenAbfrage(eb)).as("notes"), 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"), ]) .executeTakeFirstOrThrow(); const profile = gelesen.profile; if (profile?.role !== "hr" || profile?.is_active !== true) return null; const orgMaps = buildOrgMaps(gelesen.units as never, gelesen.locations as never); return { profile, // Die zweite Rundreise: was sie fragt, hängt davon ab, welche Stellen // offen sind — das lässt sich nicht in die erste ziehen. openPositions: await resolveOpenPositions(tx, orgMaps, gelesen.open as OffeneStelle[], asOf), locations: gelesen.locations as Location[], drafts: gelesen.drafts as ShellData["drafts"], openNotes: baueOffeneNotizen(gelesen.notes as NotizZeile[]), }; }