Files
alpenwerk-hr/actions/positions.ts
Maximilian Stubhan a87c688c0a Show positions that do not exist yet, and let them be corrected
Two gaps in the positions view, both reported from use.

A position dated into the future was invisible. loadOpenPositions required
valid_from <= today, so a position decided now and effective at the quarter
boundary appeared nowhere until the day it began. The database already held
one — 60000824 "Neue Position", effective 01.09. — created through the
application and shown on no screen since.

Future positions now have their own section rather than joining the vacancy
list. They are a different statement: "nobody is here" and "this does not
exist yet" should not be counted together, and a position starting 01.10.
read as a vacancy nobody was filling.

Positions could only be created and deleted. Fixing a typo in the job title
meant deleting and recreating — with a new position number, which appears in
job postings, budgets and audit entries, and whose trail then breaks.
update_position keeps the number and records old and new values per field,
using the audit detail added earlier today.

Three things it refuses, as guards rather than remarks:

  - Moving an occupied position to another unit. That is a transfer, with
    history and reporting line, and belongs to the person — otherwise
    someone changes department silently.
  - Ending an occupied position, which would leave an assignment without
    one.
  - A second chief position in a unit, or an end before the start.

Verified against the live database, all rolled back: each guard fires with
its own message, the permitted edits go through, the audit entry carries the
changed fields. Open positions stay at 9 and the future one now appears in
its own section.

ESLint caught me priming the dialog's fields from an effect. Replaced by a
key on the component, so React rebuilds it per position and the fields
initialise from props — which also removes the flash of the previous
position's values on second open.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 12:26:12 +02:00

52 lines
1.5 KiB
TypeScript

"use server";
import { revalidatePath } from "next/cache";
import { currentUserId } from "@/lib/auth/session";
import { runMutation, type ActionResult } from "@/lib/db/rpc";
const POSITION_PATHS = ["/positions", "/orgchart", "/"];
async function callRpc(
fn: "create_position" | "update_position" | "delete_position",
payload: Record<string, unknown>,
revalidate: string[]
): Promise<ActionResult> {
const result = await runMutation(await currentUserId(), fn, payload);
if (!result.success) return result;
for (const path of revalidate) revalidatePath(path);
return result;
}
export async function createPosition(payload: {
org_unit_id: string;
job_title: string;
is_chief: boolean;
valid_from: string;
}): Promise<ActionResult> {
return callRpc("create_position", payload, POSITION_PATHS);
}
/**
* Ändert eine Planstelle.
*
* Weggelassene Felder bleiben, wie sie sind. `valid_to: null` beendet die
* Befristung ausdrücklich — deshalb ist es hier `string | null` und nicht
* optional: „nicht mitgeschickt" und „auf leer setzen" müssen unterscheidbar
* bleiben.
*/
export async function updatePosition(payload: {
position_id: string;
org_unit_id?: string;
job_title?: string;
is_chief?: boolean;
valid_from?: string;
valid_to?: string | null;
}): Promise<ActionResult> {
return callRpc("update_position", payload, POSITION_PATHS);
}
export async function deletePosition(positionId: string): Promise<ActionResult> {
return callRpc("delete_position", { position_id: positionId }, POSITION_PATHS);
}