Files
alpenwerk-hr/actions/employees.ts
Maximilian Stubhan 8282d7f581 Rename Karenz to Langzeitabwesenheit and record its type
Karenz was doing duty as the name for every kind of extended absence, but
the cases behave differently in payroll and reporting — Wochenhilfe, a
Präsenzdienst, a long sick leave and a sabbatical are not the same thing.
The concept is now called Langzeitabwesenheit and carries which kind it is.

- employees.absence_type, constrained to the thirteen kinds. start_karenz
  stores it on both paths (written straight away, or parked in the
  pending_org_changes payload when the absence starts later);
  record_karenz_return and the karenz_return branch of
  apply_due_pending_changes clear it, so a returned employee does not keep
  looking like they are still away. It also reaches employee_history, the
  audit log and the employee export.
- The status enum value stays 'Karenz'. Postgres can rename an enum value in
  place, but every stored function body that spells it would then reference
  a value that no longer exists — a dozen functions across fifteen
  migrations, rewritten for a label. The mapping lives in lib/absence.ts
  instead, which is the single place the UI reads the display name from.
- Where a kind is recorded the chip shows it — "Bildungskarenz" says more
  than "Langzeitabwesenheit". Absences predating the field have none and
  fall back to the generic name rather than to a guess, and a value outside
  the list is dropped rather than echoed into the UI.
- The export prints the display name, not the raw enum: a payroll hand-off
  reading "Karenz" for what the app calls Langzeitabwesenheit only causes
  questions. Audit filter options keep their stored values and change only
  their labels.
- The seed spreads the twelve absences across the kinds; all of them being
  Karenz would leave any breakdown by kind invisible.
2026-07-25 14:34:28 +02:00

176 lines
6.0 KiB
TypeScript

"use server";
import { revalidatePath } from "next/cache";
import { sanitizeIlikeTerm } from "@/lib/supabase/query";
import { createClient } from "@/lib/supabase/server";
import type { CollectiveAgreement, Database, NoteCategory, RelationshipType, Weekday, WorkerType } from "@/lib/supabase/types";
type ActionResult = { success: boolean; error?: string };
type MutationFn = keyof Database["public"]["Functions"];
async function callRpc(fn: MutationFn, payload: Record<string, unknown>, revalidate: string[]): Promise<ActionResult> {
const supabase = await createClient();
const { error } = await supabase.rpc(fn, { payload });
if (error) return { success: false, error: error.message };
for (const path of revalidate) revalidatePath(path);
return { success: true };
}
export async function hireEmployee(payload: {
first_name: string;
last_name: string;
title_prefix?: string[];
title_suffix?: string[];
gender: "m" | "w";
birth_date: string;
sv_nummer?: string;
phone?: string;
position_id?: string;
team_id?: string;
job_title?: string;
location_id: string;
entry_date: string;
contract_type?: "unbefristet" | "befristet";
contract_end_date?: string;
employment_type?: "Vollzeit" | "Teilzeit";
weekly_hours?: number;
paygrade?: "A" | "B" | "C" | "D" | "E" | "F";
source: "Intern" | "Extern";
worker_type?: WorkerType;
collective_agreement?: CollectiveAgreement;
work_days?: Weekday[];
is_betriebsrat?: boolean;
has_dienstwagen?: boolean;
is_laterale_fuehrung?: boolean;
is_c_level?: boolean;
}): Promise<ActionResult & { employeeId?: string }> {
const supabase = await createClient();
const { data, error } = await supabase.rpc("hire_employee", { payload });
if (error) return { success: false, error: error.message };
revalidatePath("/employees");
revalidatePath("/");
revalidatePath("/positions");
return { success: true, employeeId: data as string };
}
export async function terminateEmployee(payload: {
employee_id: string;
exit_date: string;
exit_reason: string;
note?: string;
}): Promise<ActionResult> {
return callRpc("terminate_employee", payload, [`/employees/${payload.employee_id}`, "/employees", "/"]);
}
export async function transferEmployee(payload: {
employee_id: string;
effective_date: string;
new_team_id: string;
new_title?: string;
}): Promise<ActionResult> {
return callRpc("transfer_employee", payload, [`/employees/${payload.employee_id}`, "/employees"]);
}
export async function promoteEmployee(payload: {
employee_id: string;
effective_date: string;
new_title: string;
new_paygrade?: "A" | "B" | "C" | "D" | "E" | "F";
}): Promise<ActionResult> {
return callRpc("promote_employee", payload, [`/employees/${payload.employee_id}`, "/employees"]);
}
export async function startKarenz(payload: {
employee_id: string;
karenz_start_date: string;
planned_return_date: string;
absence_type: string;
note?: string;
}): Promise<ActionResult> {
return callRpc("start_karenz", payload, [`/employees/${payload.employee_id}`, "/employees", "/"]);
}
export async function adjustKarenzReturn(payload: {
employee_id: string;
new_return_date: string;
note?: string;
}): Promise<ActionResult> {
return callRpc("adjust_karenz_return", payload, [`/employees/${payload.employee_id}`]);
}
export async function recordKarenzReturn(payload: {
employee_id: string;
return_date: string;
employment_mode: "unverändert" | "Vollzeit" | "Teilzeit";
weekly_hours?: number;
}): Promise<ActionResult> {
return callRpc("record_karenz_return", payload, [`/employees/${payload.employee_id}`, "/employees", "/"]);
}
export async function changeEmployeeData(payload: {
employee_id: string;
effective_date: string;
person: Record<string, unknown>;
contract: Record<string, unknown>;
role: Record<string, unknown>;
}): Promise<ActionResult> {
return callRpc("change_employee_data", payload, [`/employees/${payload.employee_id}`, "/employees"]);
}
export async function rehireEmployee(payload: { employee_id: string; rehire_date: string }): Promise<ActionResult> {
return callRpc("rehire_employee", payload, [`/employees/${payload.employee_id}`, "/employees", "/"]);
}
export async function addEmployeeDependent(payload: {
employee_id: string;
first_name: string;
last_name: string;
relationship: RelationshipType;
sv_nummer?: string;
birth_date: string;
effective_date: string;
}): Promise<ActionResult> {
return callRpc("add_employee_dependent", payload, [`/employees/${payload.employee_id}`]);
}
export async function deleteEmployeeDependent(payload: {
dependent_id: string;
employee_id: string;
effective_date: string;
}): Promise<ActionResult> {
return callRpc("delete_employee_dependent", payload, [`/employees/${payload.employee_id}`]);
}
export async function addEmployeeNote(payload: {
employee_id: string;
category: NoteCategory;
note_text: string;
due_date?: string;
}): Promise<ActionResult> {
return callRpc("add_employee_note", payload, [`/employees/${payload.employee_id}`, "/"]);
}
export async function completeEmployeeNote(payload: { note_id: string; employee_id: string }): Promise<ActionResult> {
return callRpc("complete_employee_note", payload, [`/employees/${payload.employee_id}`, "/"]);
}
export type EmployeeSearchResult = { id: string; first_name: string; last_name: string; job_title: string; team_id: string | null };
// Shared by "Position besetzen" (staff an open position) and the reorg
// workbench's "Mitarbeiter:in(nen)" multi-select — both search active/
// on-leave employees by name or title.
export async function searchActiveEmployees(query: string): Promise<EmployeeSearchResult[]> {
const supabase = await createClient();
let q = supabase
.from("employees")
.select("id, first_name, last_name, job_title, team_id")
.in("status", ["Aktiv", "Karenz"])
.limit(20);
if (query.trim()) {
const term = sanitizeIlikeTerm(query.trim());
q = q.or(`first_name.ilike.%${term}%,last_name.ilike.%${term}%,job_title.ilike.%${term}%`);
}
const { data } = await q;
return data ?? [];
}