Roll reporting up past an absent manager, and say so on both sides
While somebody is on a long-term absence their reports report to the next
management level, and it keeps rolling up until it reaches somebody present.
Derived at read time in lib/acting-manager.ts rather than written to
employees.manager_id: the absent person stays formally in charge, so the
stand-in has to be visible as a stand-in rather than quietly replacing them.
Both ids therefore travel to the UI, and both sides carry a badge — the
absent person ("Abwesend · Vertretung: X") and anyone now reporting
elsewhere ("Vertretung für Y").
Three cases the walk has to survive, all covered by tests:
- Several absent levels in a row — it keeps climbing, and still names the
*recorded* manager as the one being covered for, not the level skipped.
- Everyone above absent — it stops and keeps the recorded manager. Re-rooting
a team to the top of the chart would distort more than showing an absent
manager whose absence is labelled anyway.
- A manager_id cycle, which nothing in the schema forbids.
An absent lead needs no separate deputy field: their stand-in is simply
their own acting manager, the same one their reports moved to.
This commit is contained in:
62
lib/acting-manager.ts
Normal file
62
lib/acting-manager.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
// 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<string, ChainNode>): 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<string>([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<string, ActingManagerResult> {
|
||||
const byId = new Map(nodes.map((n) => [n.id, n]));
|
||||
return new Map(nodes.map((n) => [n.id, resolveActingManager(n, byId)]));
|
||||
}
|
||||
Reference in New Issue
Block a user