import { sql, type Tx } from "./db"; // Wo jemand in der Organisation steht, steht nicht mehr auf der Person. Es // ergibt sich aus der Planstelle, die sie zum Stichtag innehat: // // employees ──A008──> position_assignments ──> om_positions ──> org_units // └────────> jobs // // Das ist der Grund, warum es diese Datei gibt: die Verkettung braucht es an // einem Dutzend Stellen, und sie zeitrichtig aufzulösen ist die Arbeit. export type Placement = { employeeId: string; positionId: string; positionNumber: string; orgUnitId: string; isChief: boolean; jobTitle: string; validFrom: string; validTo: string | null; /** Die Besetzung läuft am Stichtag; sonst ist es die zuletzt beendete. */ current: boolean; }; type Row = { employee_id: string; valid_from: string; valid_to: string | null; position_id: string; position_number: string; org_unit_id: string; is_chief: boolean; job_title: string; }; function toPlacement(row: Row, asOf: string): Placement { return { employeeId: row.employee_id, positionId: row.position_id, positionNumber: row.position_number, orgUnitId: row.org_unit_id, isChief: row.is_chief, jobTitle: row.job_title, validFrom: row.valid_from, validTo: row.valid_to, current: row.valid_from <= asOf && (row.valid_to === null || row.valid_to > asOf), }; } /** * Die am Stichtag laufende Besetzung je Person — und für alle, die zu dem * Zeitpunkt keine hatten, die zuletzt beendete. Ohne diesen Rückfall stünde * bei jeder ausgetretenen Person „–" statt der Stelle, die sie innehatte. */ export function pickPlacements(rows: Row[], asOf: string): Map { const byEmployee = new Map(); for (const row of rows) { const p = toPlacement(row, asOf); const best = byEmployee.get(p.employeeId); if (!best) { byEmployee.set(p.employeeId, p); continue; } // Laufend schlägt beendet; unter beendeten gewinnt die jüngste. if (p.current && !best.current) byEmployee.set(p.employeeId, p); else if (p.current === best.current && p.validFrom > best.validFrom) byEmployee.set(p.employeeId, p); } return byEmployee; } export async function loadPlacements( tx: Tx, { asOf, employeeIds }: { asOf: string; employeeIds?: string[] } ): Promise> { if (employeeIds?.length === 0) return new Map(); // Ein Join statt einer eingebetteten Ressource. Und ohne die // 1000-Zeilen-Grenze von PostgREST fällt das seitenweise Nachladen weg, // das es dafür brauchte. let q = tx .selectFrom("position_assignments as pa") .innerJoin("om_positions as p", "p.id", "pa.position_id") .innerJoin("jobs as j", "j.id", "p.job_id") .select([ "pa.employee_id", "pa.valid_from", "pa.valid_to", "p.id as position_id", "p.position_number", "p.org_unit_id", "p.is_chief", "j.title as job_title", ]) .orderBy("pa.employee_id"); if (employeeIds) q = q.where("pa.employee_id", "in", employeeIds); return pickPlacements((await q.execute()) as Row[], asOf); } // ── Abgeleitete Berichtslinie ────────────────────────────────────── // Sie steht nirgends als Spalte; om_reporting_lines() rechnet sie aus dem // Baum aus. formal_manager_id ist die zuständige Leitung, acting_manager_id // die nächste besetzte und anwesende darüber — beides, damit sich in der // Oberfläche zeigen lässt, dass eine Vertretung im Spiel ist, statt sie // stillschweigend als die echte Führungskraft auszugeben. export type ReportingLine = { employee_id: string; position_id: string; org_unit_id: string; is_chief: boolean; formal_manager_id: string | null; acting_manager_id: string | null; }; /** * `filter` schränkt die Funktion selbst ein, nicht das Ergebnis im Speicher — * bei der Detailseite wandern damit neun Zeilen über die Leitung statt * achthundert. */ export async function loadReportingLines( tx: Tx, asOf: string, filter?: { employeeId?: string; actingManagerId?: string } ): Promise { const conditions = [sql`true`]; if (filter?.employeeId) conditions.push(sql`employee_id = ${filter.employeeId}::uuid`); if (filter?.actingManagerId) conditions.push(sql`acting_manager_id = ${filter.actingManagerId}::uuid`); const result = await sql` select * from om_reporting_lines(${asOf}::date) where ${sql.join(conditions, sql` and `)} `.execute(tx); return result.rows; } /** Wie loadReportingLines, aber als Karte über die Personen-Kennung. */ export async function loadReportingLineMap(tx: Tx, asOf: string): Promise> { const lines = await loadReportingLines(tx, asOf); return new Map(lines.map((l) => [l.employee_id, l])); }