// Die SAP-OM-Berichtslinie, abgeleitet aus dem Organisationsbaum. // // Dieselbe Regel steckt als om_reporting_lines() in der Datenbank. Zwei // Fassungen derselben Regel driften auseinander, deshalb prüft // tests/integration/om-reporting.test.ts beide gegen denselben Bestand. // Hier liegt sie zusätzlich, weil das Organigramm zu einem Stichtag ohnehin // im Anwendungscode gerechnet wird und ein Datenbank-Roundtrip je // Stichtagswechsel nichts brächte. // // Regel: // Wer eine gewöhnliche Planstelle innehat, berichtet an die Leitung der // eigenen Einheit. Wer selbst die Leitung innehat, an die Leitung der // übergeordneten Einheit. // // Aufwärtsregel: Ist diese Leitung unbesetzt oder langzeitabwesend, geht es // weiter nach oben. Eine unbesetzte Abteilungsleitung braucht damit keine // Sonderbehandlung — sie wird übersprungen. export type OmUnit = { id: string; parentId: string | null }; export type OmHolder = { employeeId: string; positionId: string; orgUnitId: string; isChief: boolean; /** Langzeitabwesend am Stichtag. */ absent: boolean; }; export type OmReportingLine = { employeeId: string; positionId: string; orgUnitId: string; isChief: boolean; /** Zuständige Leitung, auch wenn abwesend. Null, wenn es keine gibt. */ formalManagerId: string | null; /** Nächste besetzte und anwesende Leitung ab der zuständigen Einheit aufwärts. */ actingManagerId: string | null; }; /** * `units` und `holders` beschreiben den Stand zu genau einem Stichtag — * gültige Einheiten und laufende Besetzungen. Die Zeitlogik bleibt bewusst * draußen, damit diese Funktion nur eine Sache tut. */ export function resolveReportingLines(units: OmUnit[], holders: OmHolder[]): OmReportingLine[] { const parentOf = new Map(units.map((u) => [u.id, u.parentId])); const chiefOfUnit = new Map(); for (const h of holders) { if (h.isChief) chiefOfUnit.set(h.orgUnitId, h); } return holders.map((h) => { // Leitungen suchen ab der übergeordneten Einheit, alle anderen ab der // eigenen — sonst berichtete eine Leitung an sich selbst. const baseUnitId = h.isChief ? (parentOf.get(h.orgUnitId) ?? null) : h.orgUnitId; const formal = baseUnitId ? (chiefOfUnit.get(baseUnitId) ?? null) : null; // Aufwärts, bis eine besetzte und anwesende Leitung gefunden ist. Der // Zyklusschutz ist kein Selbstzweck: parent_id ist eine gewöhnliche // Spalte, und ein fehlerhafter Import kann einen Ring erzeugen. let actingManagerId: string | null = null; const seen = new Set(); let unitId: string | null = baseUnitId; while (unitId && !seen.has(unitId)) { seen.add(unitId); const chief = chiefOfUnit.get(unitId); if (chief && !chief.absent && chief.employeeId !== h.employeeId) { actingManagerId = chief.employeeId; break; } unitId = parentOf.get(unitId) ?? null; } return { employeeId: h.employeeId, positionId: h.positionId, orgUnitId: h.orgUnitId, isChief: h.isChief, formalManagerId: formal?.employeeId ?? null, actingManagerId, }; }); }