SAP OM: org units, jobs, positions, and a derived reporting line
The org structure was three fixed tables — divisions -> departments -> teams — with people hanging directly off them and a hand-maintained manager_id. The depth was therefore wired into the schema: an Abteilungsleitung could not exist without a migration, and a team directly under a Bereich not at all. That is what this replaces. The SAP OM object types, one table each: O org_units recursive over parent_id C jobs catalogue, so many positions can share a job S om_positions belongs to exactly one org unit P employees existing table A012 om_positions.is_chief "ist Leiter von" A008 position_assignments "Inhaber ist", time-dependent Two consequences worth stating, because they are the point of the exercise: - GF/Bereich/Abteilung/Team are now a label (unit_type), not a structure. Adding a fifth level, or hanging a team straight off a Bereich, becomes a data question rather than a migration. - Nobody hangs off an org unit any more: person -> position -> unit. A vacancy stops being its own concept — it is a position with no current assignment. The reporting line is derived rather than stored: an ordinary position reports to the chief of its own unit, a chief to the chief of the parent unit, and if that chief is vacant or on a long-term absence it keeps climbing. An unfilled Abteilungsleitung therefore needs no special case — it is simply skipped. Both ids come back, formal and acting, so the UI can show a stand-in as a stand-in instead of passing it off as the real manager. The rule exists twice, as om_reporting_lines() in SQL and resolveReportingLines() in TypeScript, because the as-of chart computes it per date in the app and a round trip per date change would buy nothing. Two copies drift silently — the org chart would just show a different manager than the export — so an integration test runs both over the whole roster and requires identical answers, plus that every line terminates at the top. Unit tests cover the rule itself: unfilled levels, several absent levels in a row, nobody above, a chief who also leads the parent unit, and a cycle in parent_id, which is an ordinary column an import could get wrong. Additive so far. The old tables still stand and the app still reads them; the cut-over follows.
This commit is contained in:
@@ -18,6 +18,8 @@ export type NoteCategory = "Allgemein" | "Vertraulich" | "Personalgespräch" | "
|
||||
// union (not a string literal) so a future hr_admin/hr_user split, if ever
|
||||
// technically required, is a type-level addition, not a rewrite.
|
||||
export type ProfileRole = "hr";
|
||||
/** Etikett einer Organisationseinheit; die Struktur steckt in parent_id. */
|
||||
export type OrgUnitType = "Gesellschaft" | "Bereich" | "Abteilung" | "Team";
|
||||
export type HistoryEventType =
|
||||
| "Eintritt"
|
||||
| "Beförderung"
|
||||
@@ -395,6 +397,86 @@ export type Database = {
|
||||
};
|
||||
// Written exclusively by trg_track_employee_assignment; RLS grants HR
|
||||
// read access only, hence no Insert/Update shapes worth modelling.
|
||||
// ── SAP-OM-Modell ──────────────────────────────────────────
|
||||
// O: rekursiv über parent_id, unit_type ist nur ein Etikett.
|
||||
org_units: NoRelationships & {
|
||||
Row: {
|
||||
id: string;
|
||||
org_number: string;
|
||||
name: string;
|
||||
parent_id: string | null;
|
||||
unit_type: OrgUnitType;
|
||||
valid_from: string;
|
||||
valid_to: string | null;
|
||||
created_at: string;
|
||||
};
|
||||
Insert: {
|
||||
id?: string;
|
||||
org_number: string;
|
||||
name: string;
|
||||
parent_id?: string | null;
|
||||
unit_type: OrgUnitType;
|
||||
valid_from?: string;
|
||||
valid_to?: string | null;
|
||||
created_at?: string;
|
||||
};
|
||||
Update: Partial<Database["public"]["Tables"]["org_units"]["Insert"]>;
|
||||
};
|
||||
// C: Katalog der Tätigkeiten.
|
||||
jobs: NoRelationships & {
|
||||
Row: { id: string; code: string; title: string; created_at: string };
|
||||
Insert: { id?: string; code: string; title: string; created_at?: string };
|
||||
Update: Partial<Database["public"]["Tables"]["jobs"]["Insert"]>;
|
||||
};
|
||||
// S: Planstelle. Heisst om_positions, weil `positions` noch die alte
|
||||
// Tabelle für offene Stellen ist, bis der Umstieg abgeschlossen ist.
|
||||
om_positions: {
|
||||
Row: {
|
||||
id: string;
|
||||
position_number: string;
|
||||
org_unit_id: string;
|
||||
job_id: string;
|
||||
is_chief: boolean;
|
||||
valid_from: string;
|
||||
valid_to: string | null;
|
||||
created_at: string;
|
||||
};
|
||||
Insert: {
|
||||
id?: string;
|
||||
position_number: string;
|
||||
org_unit_id: string;
|
||||
job_id: string;
|
||||
is_chief?: boolean;
|
||||
valid_from?: string;
|
||||
valid_to?: string | null;
|
||||
created_at?: string;
|
||||
};
|
||||
Update: Partial<Database["public"]["Tables"]["om_positions"]["Insert"]>;
|
||||
Relationships: [];
|
||||
};
|
||||
// A008: Person besetzt Planstelle, zeitabhängig.
|
||||
position_assignments: {
|
||||
Row: {
|
||||
id: string;
|
||||
position_id: string;
|
||||
employee_id: string;
|
||||
valid_from: string;
|
||||
valid_to: string | null;
|
||||
created_at: string;
|
||||
};
|
||||
Insert: {
|
||||
id?: string;
|
||||
position_id: string;
|
||||
employee_id: string;
|
||||
valid_from: string;
|
||||
valid_to?: string | null;
|
||||
created_at?: string;
|
||||
};
|
||||
Update: Partial<Database["public"]["Tables"]["position_assignments"]["Insert"]>;
|
||||
// Einbettung auf die Planstelle, damit die Berichtslinie in einer
|
||||
// Abfrage geladen werden kann.
|
||||
Relationships: [{ foreignKeyName: "position_assignments_position_id_fkey"; columns: ["position_id"]; referencedRelation: "om_positions"; referencedColumns: ["id"] }];
|
||||
};
|
||||
employee_assignments: NoRelationships & {
|
||||
Row: {
|
||||
id: string;
|
||||
@@ -435,6 +517,17 @@ export type Database = {
|
||||
apply_reorg: { Args: { payload: Record<string, unknown> }; Returns: string };
|
||||
undo_reorg: { Args: { payload: Record<string, unknown> }; Returns: void };
|
||||
apply_due_pending_changes: { Args: Record<string, never>; Returns: number };
|
||||
om_reporting_lines: {
|
||||
Args: { p_as_of?: string };
|
||||
Returns: {
|
||||
employee_id: string;
|
||||
position_id: string;
|
||||
org_unit_id: string;
|
||||
is_chief: boolean;
|
||||
formal_manager_id: string | null;
|
||||
acting_manager_id: string | null;
|
||||
}[];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user