Die Akte oeffnet jetzt in rund 100 ms statt in 1,5 s: vorher person 4ms, hauptabfrage 1554ms, nachschlag 11ms, gesamt 1569ms nachher person 2ms, hauptabfrage 79ms, nachschlag 7ms, gesamt 87ms Gefunden hat das der vorlaeufige console.info in dieser Datei, nicht das Nachdenken darueber. Behoben haben es zwei Migrationen: die Policies pruefen die Rechte einmal je Abfrage (20260924200000), und om_reporting_lines laeuft als security definer mit require_hr_admin in der ersten Zeile (20260924220000). Nebenbei die Behauptung im Kopf der Datei richtiggestellt: dort standen "rund 36 ms Umlaufzeit" als Begruendung dafuer, die Zahl der Rundreisen zu druecken. Nachgemessen sind es 0,5 ms. Die Rundreisen waren nie das Problem.
171 lines
7.2 KiB
TypeScript
171 lines
7.2 KiB
TypeScript
import type { Tx } from "./db";
|
|
import { kontierungZum, kontierungenAbfrage, type KontierungsZeile } from "./cost-centers";
|
|
import { jsonArrayFrom, zeitstempel } from "./db/json";
|
|
import { todayIso } from "./format";
|
|
import { buildOrgMaps, orgMapsAbfragen } from "./org";
|
|
import { berichtslinienAbfrage, besetzungenAbfrage, orgAsOf, pickPlacements } from "./placement";
|
|
import { offeneStellenAbfrage, resolveOpenPositions, type OffeneStelle } from "./positions";
|
|
|
|
// Was die Mitarbeiterakte liest — in vier Rundreisen statt in zwölf.
|
|
//
|
|
// Vorher: die Person, dann acht Dinge in einem Promise.all, dann die Namen der
|
|
// Beteiligten. Das Promise.all war keine Gleichzeitigkeit — eine Transaktion
|
|
// hängt an einer Verbindung (lib/db/json.ts).
|
|
//
|
|
// Hier stand „bei rund 36 ms Umlaufzeit". Nachgemessen sind es 0,5 ms; die
|
|
// Zahl der Rundreisen ist also nicht der Hebel, für den sie hier gehalten
|
|
// wurde. Teuer war die Akte aus einem anderen Grund — 1,5 s, davon 1,4 s in
|
|
// om_reporting_lines unter Zeilenschutz (siehe Migration 20260924220000).
|
|
// Jetzt liegt die ganze Seite bei rund 100 ms.
|
|
//
|
|
// Die drei Tabellen mit `selectAll()` stehen hier ausgeschrieben. Das ist
|
|
// keine Umständlichkeit: innerhalb von JSON formatiert Postgres timestamptz
|
|
// anders als der Treiber sonst, und `created_at` entscheidet in lib/history.ts
|
|
// darüber, was später passiert ist. Ausgeschrieben ist sichtbar, welche Spalte
|
|
// durch zeitstempel() geht — bei selectAll() wäre es unsichtbar falsch.
|
|
|
|
export async function loadEmployeeDetail(tx: Tx, id: string, today: string = todayIso()) {
|
|
// Die Person zuerst, allein: erst aus Eintritt und Austritt ergibt sich der
|
|
// Stichtag, zu dem ihre Organisation überhaupt eine Antwort hat. Eine
|
|
// Person, die am 01.09. anfängt, hat heute keine laufende Besetzung — zu
|
|
// heute gefragt lieferte om_reporting_lines() nichts, und die Akte
|
|
// behauptete „Keine Führungskraft", obwohl das Team eine hat.
|
|
const employee = await tx.selectFrom("employees").selectAll().where("id", "=", id).executeTakeFirst();
|
|
if (!employee) return null;
|
|
const asOf = orgAsOf(employee, today);
|
|
|
|
const g = await tx
|
|
.selectNoFrom((eb) => [
|
|
// Vorgesetzte und direkte Berichte stehen nirgends als Spalte — sie
|
|
// kommen aus om_reporting_lines().
|
|
//
|
|
// **Einmal** gerufen, nicht zweimal. Hier standen zwei Teilabfragen mit
|
|
// je einem Filter, in der Annahme, der Filter schränke die Funktion
|
|
// ein. Das tut er nicht: wegen des `with recursive` in ihrem Körper
|
|
// lässt sie sich nicht einbetten, sie rechnet jedes Mal alle Zeilen aus
|
|
// und der Filter wirft sie danach weg. Auf dem Server gemessen:
|
|
//
|
|
// explain analyze select * from om_reporting_lines(current_date)
|
|
// where employee_id = …;
|
|
// → Rows Removed by Filter: 783, Execution Time: 69 ms
|
|
//
|
|
// Zwei Filter hiessen also zweimal dieselbe Rechnung. Jetzt wandern
|
|
// einmal alle Zeilen herüber — ein paar hundert Kilobyte über eine
|
|
// Verbindung im selben Netz — und die Auswahl trifft der Aufrufer.
|
|
jsonArrayFrom(berichtslinienAbfrage(eb, asOf)).as("lines"),
|
|
|
|
jsonArrayFrom(
|
|
eb
|
|
.selectFrom("employee_history")
|
|
.select(["id", "employee_id", "event_date", "event_type", "description", "changes", "pending_id"])
|
|
.select((x) => zeitstempel(x.ref("created_at")).as("created_at"))
|
|
.where("employee_id", "=", id)
|
|
.orderBy("event_date", "desc")
|
|
.orderBy("created_at", "desc")
|
|
).as("history"),
|
|
|
|
jsonArrayFrom(
|
|
eb
|
|
.selectFrom("employee_dependents")
|
|
.select(["id", "employee_id", "first_name", "last_name", "relationship", "sv_nummer", "birth_date"])
|
|
.select((x) => zeitstempel(x.ref("created_at")).as("created_at"))
|
|
.where("employee_id", "=", id)
|
|
.orderBy("created_at")
|
|
).as("dependents"),
|
|
|
|
jsonArrayFrom(
|
|
eb
|
|
.selectFrom("employee_notes")
|
|
.select([
|
|
"id",
|
|
"employee_id",
|
|
"author_user_id",
|
|
"author_name",
|
|
"category",
|
|
"note_text",
|
|
"due_date",
|
|
"done",
|
|
"done_by",
|
|
])
|
|
.select((x) => [
|
|
zeitstempel(x.ref("created_at")).as("created_at"),
|
|
zeitstempel(x.ref("done_at")).as("done_at"),
|
|
])
|
|
.where("employee_id", "=", id)
|
|
.orderBy("created_at", "desc")
|
|
).as("notes"),
|
|
|
|
...orgMapsAbfragen(eb),
|
|
jsonArrayFrom(besetzungenAbfrage(eb, [id])).as("placementRows"),
|
|
// Die Kontierungen der Planstellen dieser Person. Welche gilt, hängt am
|
|
// Stichtag — und der ergibt sich erst aus Eintritt und Austritt, steht
|
|
// hier also schon fest.
|
|
jsonArrayFrom(
|
|
kontierungenAbfrage(eb).where("z.position_id", "in", (e) =>
|
|
e.selectFrom("position_assignments as pa").select("pa.position_id").where("pa.employee_id", "=", id)
|
|
)
|
|
).as("costRows"),
|
|
jsonArrayFrom(
|
|
eb
|
|
.selectFrom("onboarding_tasks")
|
|
.select(["item_key", "erledigt", "wert", "kommentar", "updated_by_name"])
|
|
.select((x) => zeitstempel(x.ref("updated_at")).as("updated_at"))
|
|
.where("employee_id", "=", id)
|
|
).as("onboarding"),
|
|
jsonArrayFrom(
|
|
eb
|
|
.selectFrom("offboarding_tasks")
|
|
.select(["item_key", "erledigt", "wert", "kommentar", "updated_by_name"])
|
|
.select((x) => zeitstempel(x.ref("updated_at")).as("updated_at"))
|
|
.where("employee_id", "=", id)
|
|
).as("offboarding"),
|
|
// Die offenen Planstellen bleiben bei heute: sie gehören zur
|
|
// Organisation, nicht zu dieser Person.
|
|
jsonArrayFrom(offeneStellenAbfrage(eb, today)).as("open"),
|
|
])
|
|
.executeTakeFirstOrThrow();
|
|
|
|
const orgMaps = buildOrgMaps(g.units as never, g.locations as never);
|
|
const line = g.lines.find((l) => l.employee_id === id) ?? null;
|
|
const reports = g.lines.filter((l) => l.acting_manager_id === id);
|
|
const placement = pickPlacements(g.placementRows as never, asOf).get(id) ?? null;
|
|
|
|
// Namen für die beteiligten Personen in einem Zug: die Vertretung, die
|
|
// formal zuständige Leitung und die direkten Berichte.
|
|
const relatedIds = Array.from(
|
|
new Set(
|
|
[line?.acting_manager_id, line?.formal_manager_id, ...reports.map((r) => r.employee_id)].filter(
|
|
(x): x is string => Boolean(x)
|
|
)
|
|
)
|
|
);
|
|
|
|
const [relatedRows, openPositions] = await Promise.all([
|
|
relatedIds.length
|
|
? tx
|
|
.selectFrom("employees")
|
|
.select(["id", "first_name", "last_name", "job_title", "status"])
|
|
.where("id", "in", relatedIds)
|
|
.execute()
|
|
: Promise.resolve([]),
|
|
resolveOpenPositions(tx, orgMaps, g.open as OffeneStelle[], today),
|
|
]);
|
|
|
|
return {
|
|
employee,
|
|
line,
|
|
reports,
|
|
history: g.history,
|
|
dependents: g.dependents,
|
|
notes: g.notes,
|
|
orgMaps,
|
|
placement,
|
|
// Die Kostenstelle des Sitzes, auf dem die Person am Stichtag sitzt.
|
|
kostenstelle: placement ? (kontierungZum(g.costRows as KontierungsZeile[], asOf).get(placement.positionId) ?? null) : null,
|
|
onboarding: g.onboarding,
|
|
offboarding: g.offboarding,
|
|
openPositions,
|
|
byId: new Map(relatedRows.map((e) => [e.id, e])),
|
|
};
|
|
}
|