Reworks the app from a two-role (hr_admin/manager) model to a single HR-only role gated by profiles.is_active, fixes transfer/promote/karenz/ reorg RPCs to actually defer future-dated changes via a new pending_org_changes table instead of writing them immediately (applied by a daily Vercel Cron route), makes reorg undo append-only instead of deleting history, adds Karenz-return and history-date integrity guards, deprecates the salary column, and adds explicit schema grants + perf indexes needed to run against a fresh (non-hosted) Postgres instance. Adds vitest unit + integration test suites (the latter against a real local Supabase instance) covering all of the above, plus lint/typecheck/ build wiring (`npm run check`).
58 lines
2.0 KiB
TypeScript
58 lines
2.0 KiB
TypeScript
"use server";
|
|
|
|
import { revalidatePath } from "next/cache";
|
|
import { createClient } from "@/lib/supabase/server";
|
|
|
|
type ActionResult = { success: boolean; error?: string };
|
|
|
|
export async function createPosition(payload: {
|
|
title: string;
|
|
superior_employee_id: string;
|
|
is_lead: boolean;
|
|
team_id?: string;
|
|
}): Promise<ActionResult> {
|
|
const supabase = await createClient();
|
|
const { error } = await supabase.rpc("create_position", { payload });
|
|
if (error) return { success: false, error: error.message };
|
|
revalidatePath("/positions");
|
|
revalidatePath("/orgchart");
|
|
revalidatePath("/");
|
|
return { success: true };
|
|
}
|
|
|
|
export async function staffPositionInternally(payload: {
|
|
position_id: string;
|
|
employee_id: string;
|
|
}): Promise<ActionResult> {
|
|
const supabase = await createClient();
|
|
const { error } = await supabase.rpc("staff_position_internally", { payload });
|
|
if (error) return { success: false, error: error.message };
|
|
revalidatePath("/positions");
|
|
revalidatePath("/orgchart");
|
|
revalidatePath("/employees");
|
|
revalidatePath(`/employees/${payload.employee_id}`);
|
|
revalidatePath("/");
|
|
return { success: true };
|
|
}
|
|
|
|
export type SuperiorSearchResult = { id: string; first_name: string; last_name: string; job_title: string; division_id: string };
|
|
|
|
// For "Position ausschreiben": superior lookup, filtered to team-leads when
|
|
// the new position is an IC role, or to division-heads/CEO when the new
|
|
// position is itself a team lead (§2).
|
|
export async function searchSuperiors(query: string, forLeadPosition: boolean): Promise<SuperiorSearchResult[]> {
|
|
const supabase = await createClient();
|
|
let q = supabase
|
|
.from("employees")
|
|
.select("id, first_name, last_name, job_title, division_id")
|
|
.eq("status", "Aktiv")
|
|
.limit(20);
|
|
q = forLeadPosition ? q.lte("org_level", 1) : q.eq("is_lead", true).eq("org_level", 2);
|
|
if (query.trim()) {
|
|
const term = query.trim();
|
|
q = q.or(`first_name.ilike.%${term}%,last_name.ilike.%${term}%,job_title.ilike.%${term}%`);
|
|
}
|
|
const { data } = await q;
|
|
return data ?? [];
|
|
}
|