Files
alpenwerk-hr/actions/positions.ts
Maximilian Stubhan 901c5c426e Consolidation pass: HR-only access, effective-dated mutations, data integrity guards, test suite
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`).
2026-07-14 20:32:20 +02:00

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 ?? [];
}