import type { SupabaseClient } from "@supabase/supabase-js"; import { fetchAllRows } from "./supabase/query"; import type { Database } from "./supabase/types"; // 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; }; const SELECT = "employee_id, valid_from, valid_to, om_positions!inner(id, position_number, org_unit_id, is_chief, jobs!inner(title))"; type Row = { employee_id: string; valid_from: string; valid_to: string | null; om_positions: { id: string; position_number: string; org_unit_id: string; is_chief: boolean; jobs: { title: string }; }; }; function toPlacement(row: Row, asOf: string): Placement { return { employeeId: row.employee_id, positionId: row.om_positions.id, positionNumber: row.om_positions.position_number, orgUnitId: row.om_positions.org_unit_id, isChief: row.om_positions.is_chief, jobTitle: row.om_positions.jobs.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( supabase: SupabaseClient, { asOf, employeeIds }: { asOf: string; employeeIds?: string[] } ): Promise> { if (employeeIds?.length === 0) return new Map(); const rows = await fetchAllRows(() => { const q = supabase.from("position_assignments").select(SELECT).order("employee_id"); return employeeIds ? q.in("employee_id", employeeIds) : q; }); return pickPlacements(rows as unknown 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; }; export async function loadReportingLines( supabase: SupabaseClient, asOf: string ): Promise> { const { data, error } = await supabase.rpc("om_reporting_lines", { p_as_of: asOf }); if (error) throw new Error(`Berichtslinie konnte nicht geladen werden: ${error.message}`); return new Map(((data ?? []) as ReportingLine[]).map((l) => [l.employee_id, l])); }