Phase 2/3: Employees list/detail + mutation RPCs + action panels

- supabase/functions.sql, functions_2.sql: Postgres RPCs for every
  employee/position/reorg mutation (hire, terminate, transfer, promote,
  start/adjust/return karenz, change data, rehire, create position, staff
  internally, apply/undo reorg). Each resolves manager_id server-side,
  writes history + audit atomically, and enforces hr_admin via
  require_hr_admin() (backed by the existing RLS policy).
- actions/employees.ts, positions.ts, reorg.ts: Server Actions wrapping
  the RPCs, returning success/error for client-side toast handling.
- Employees list (search/filter/pagination) and detail (4 tabs: Stammdaten,
  Vertrag & Gehalt, Organisation, Historie) reading from employees_directory.
- 6 action slide-over panels: Transfer, Promote, Karenz (start/adjust/
  return), Daten aendern (person+contract diffing), Terminate (with direct-
  report reparenting warning + offboarding checklist), Rehire.
- lib/org.ts: shared division/department/team/location lookups.

Verified live: promote mutation updates salary, writes history/audit, and
the detail page reflects it after refresh, no console errors.

Note: the spec's Karenz-verwalten panel only covers employees already on
Karenz; added a start-Karenz mode (Karenzbeginn/geplante Rueckkehr) to
cover the Aktiv-employee case implied by the header button but not
specified in the panel list.
This commit is contained in:
2026-07-13 22:07:38 +02:00
parent ef9852b09c
commit 366731ec85
21 changed files with 2316 additions and 1 deletions

48
lib/org.ts Normal file
View File

@@ -0,0 +1,48 @@
import type { SupabaseClient } from "@supabase/supabase-js";
import type { Database } from "./supabase/types";
type Division = Database["public"]["Tables"]["divisions"]["Row"];
type Department = Database["public"]["Tables"]["departments"]["Row"];
type Team = Database["public"]["Tables"]["teams"]["Row"];
type Location = Database["public"]["Tables"]["locations"]["Row"];
export type OrgMaps = {
divisions: Map<string, Division>;
departments: Map<string, Department>;
teams: Map<string, Team>;
locations: Map<string, Location>;
divisionList: Division[];
locationList: Location[];
};
// Org reference data is tiny (9 divisions / 16 departments / 35 teams / 5
// locations) — fetched whole and joined client-side rather than per-row.
export async function loadOrgMaps(supabase: SupabaseClient<Database>): Promise<OrgMaps> {
const [{ data: divisions }, { data: departments }, { data: teams }, { data: locations }] = await Promise.all([
supabase.from("divisions").select("*").order("name"),
supabase.from("departments").select("*"),
supabase.from("teams").select("*"),
supabase.from("locations").select("*").order("name"),
]);
return {
divisions: new Map((divisions ?? []).map((d) => [d.id, d])),
departments: new Map((departments ?? []).map((d) => [d.id, d])),
teams: new Map((teams ?? []).map((t) => [t.id, t])),
locations: new Map((locations ?? []).map((l) => [l.id, l])),
divisionList: divisions ?? [],
locationList: locations ?? [],
};
}
export function breadcrumbFor(orgMaps: OrgMaps, divisionId: string | null, teamId: string | null) {
const division = divisionId ? orgMaps.divisions.get(divisionId) : undefined;
const team = teamId ? orgMaps.teams.get(teamId) : undefined;
const department = team ? orgMaps.departments.get(team.department_id) : undefined;
return { division, department, team };
}
export function breadcrumbLabel(orgMaps: OrgMaps, divisionId: string | null, teamId: string | null): string {
const { division, department, team } = breadcrumbFor(orgMaps, divisionId, teamId);
return [division?.name, department?.name, team?.name].filter(Boolean).join(" ") || "";
}

View File

@@ -281,6 +281,20 @@ export type Database = {
};
};
};
Functions: Record<string, never>;
Functions: {
hire_employee: { Args: { payload: Record<string, unknown> }; Returns: string };
terminate_employee: { Args: { payload: Record<string, unknown> }; Returns: void };
transfer_employee: { Args: { payload: Record<string, unknown> }; Returns: void };
promote_employee: { Args: { payload: Record<string, unknown> }; Returns: void };
start_karenz: { Args: { payload: Record<string, unknown> }; Returns: void };
adjust_karenz_return: { Args: { payload: Record<string, unknown> }; Returns: void };
record_karenz_return: { Args: { payload: Record<string, unknown> }; Returns: void };
change_employee_data: { Args: { payload: Record<string, unknown> }; Returns: void };
rehire_employee: { Args: { payload: Record<string, unknown> }; Returns: void };
create_position: { Args: { payload: Record<string, unknown> }; Returns: string };
staff_position_internally: { Args: { payload: Record<string, unknown> }; Returns: void };
apply_reorg: { Args: { payload: Record<string, unknown> }; Returns: string };
undo_reorg: { Args: { payload: Record<string, unknown> }; Returns: void };
};
};
};