// Who actually leads a team while its lead is on a long-term absence. // // The rule: reporting rolls up to the next management level, and keeps // rolling up until it reaches somebody who is present. Someone on // Langzeitabwesenheit stays formally in charge — nothing is written to // employees.manager_id — so this is derived at read time and the stand-in is // always shown as such rather than silently replacing the real manager. export type ChainNode = { id: string; /** The recorded manager, absent or not. */ managerId: string | null; absent: boolean; }; export type ActingManagerResult = { /** Who to report to in practice — the nearest present manager. */ actingManagerId: string | null; /** * The recorded manager, when they differ from the acting one. Null when * nobody is standing in, so the UI can show the arrangement only when * there is one. */ coveredForId: string | null; }; /** * Walks up from `node`'s recorded manager until it finds one who is present. * * Falls back to the recorded manager when the whole chain above is absent * (or leads nowhere): re-rooting a team to the top of the chart would be a * bigger lie than showing the absent manager, and the absence is labelled * either way. */ export function resolveActingManager(node: ChainNode, byId: Map): ActingManagerResult { const formalId = node.managerId; if (!formalId) return { actingManagerId: null, coveredForId: null }; const formal = byId.get(formalId); if (!formal || !formal.absent) return { actingManagerId: formalId, coveredForId: null }; // Nothing in the schema forbids a manager_id cycle, so the walk has to // terminate on its own rather than trusting the data. const seen = new Set([node.id, formalId]); let current: ChainNode | undefined = formal; while (current?.managerId && !seen.has(current.managerId)) { seen.add(current.managerId); const next = byId.get(current.managerId); if (!next) break; if (!next.absent) return { actingManagerId: next.id, coveredForId: formalId }; current = next; } return { actingManagerId: formalId, coveredForId: null }; } /** Applies the walk to a whole set, returning one result per node id. */ export function resolveActingManagers(nodes: ChainNode[]): Map { const byId = new Map(nodes.map((n) => [n.id, n])); return new Map(nodes.map((n) => [n.id, resolveActingManager(n, byId)])); }