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>
52 lines
1.5 KiB
TypeScript
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);
|
|
}
|
|
|