Put the onboarding checklist where the file is
Some checks failed
CI / Lint, Typen, Tests, Build (push) Failing after 6m24s
CI / Integrationstests (echtes Postgres) (push) Failing after 5m51s

The list existed on paper: one printed sheet per entry, twenty-five
boxes. What is on it is known only to whoever holds the sheet — it
cannot be searched, cannot be covered for while someone is away, and
says nothing about who ticked what.

Not every box on the sheet is a checkbox, and the differences carry
meaning, so the field kind is derived from the thing rather than
flattened:

  Haken   — the normal case. The Meldezettel is there or it is not.
  Ja/Nein — Prämienanspruch had *two* boxes on the sheet, and that is
            not decoration: "nein" is a finding, "not asked yet" is not.
            One checkbox cannot say both.
  Text    — shoe, shirt and trouser size. The value is the point;
            ticked off it would be worthless.

Every item takes a comment, and every item records who last touched it
and when — the part the sheet could never do.

Saved on click, not on submit. A checklist is worked through over days,
between other things; a save button at the end is where half a morning
goes missing.

The items live in lib/onboarding.ts, not in a table: a checklist is a
company process, not a master record. Stored per person is only the
answer, under the item's key — so an item dropped later leaves its old
answers standing instead of taking them along, and a file from back then
stays readable.

A list is created by hire and rehire, in the same transaction as the
hire itself: a hire without a checklist would be a half-recorded hire.
Rehire only adds what is missing and never clears an old tick — what
genuinely has to be redone is HR's call, and a program deciding it would
be guessing. People hired before this feature have no list and get a
button to start one.

Checked against the real database end to end: hire creates 25 open
items; checkbox, ja/nein, size and comment all land; a comment-only edit
leaves the tick alone; rehire tops the list up and keeps what was done.
The probe employee was removed afterwards — audit rows first, since the
log has no delete policy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-17 15:40:02 +02:00
parent bd990b7f2c
commit 82d07f0d95
9 changed files with 800 additions and 8 deletions

View File

@@ -4,6 +4,7 @@ import { revalidatePath } from "next/cache";
import { currentUserId } from "@/lib/auth/session";
import { withUser } from "@/lib/db";
import { callFunction, runMutation, type ActionResult, type MutationFn } from "@/lib/db/rpc";
import { ONBOARDING_PUNKTE } from "@/lib/onboarding";
import type { CollectiveAgreement, DienstwagenArt, NoteCategory, RelationshipType, Weekday, WorkerType } from "@/lib/supabase/types";
async function callRpc(fn: MutationFn, payload: Record<string, unknown>, revalidate: string[]): Promise<ActionResult> {
@@ -67,9 +68,14 @@ export async function hireEmployee(payload: {
// Einzige Mutation, deren Rückgabewert gebraucht wird: die neue
// Personen-Kennung, damit die Oberfläche direkt auf die Akte springen kann.
try {
const employeeId = await withUser(await currentUserId(), (tx) =>
callFunction(tx, "hire_employee", payload as Record<string, unknown>)
);
const employeeId = await withUser(await currentUserId(), async (tx) => {
const id = (await callFunction(tx, "hire_employee", payload as Record<string, unknown>)) as string;
// In derselben Transaktion: eine Einstellung ohne Checkliste wäre eine
// halb erfasste Einstellung, und sie später nachzureichen hiesse, dass
// jemand daran denken muss.
await callFunction(tx, "start_onboarding", { employee_id: id, item_keys: ONBOARDING_PUNKTE.map((x) => x.key) });
return id;
});
revalidatePath("/employees");
revalidatePath("/");
revalidatePath("/positions");
@@ -160,7 +166,44 @@ export async function rehireEmployee(payload: {
rehire_date: string;
position_id: string;
}): Promise<ActionResult> {
return callRpc("rehire_employee", payload, [`/employees/${payload.employee_id}`, "/employees", "/"]);
try {
await withUser(await currentUserId(), async (tx) => {
await callFunction(tx, "rehire_employee", payload as unknown as Record<string, unknown>);
// Auch bei der Wiedereinstellung: Dienstzettel, Bankverbindung und
// E-Card sind wieder zu erledigen. Die Punkte von damals stehen noch da
// und bleiben stehen — start_onboarding legt nur an, was fehlt, statt
// einen alten Haken zu löschen. Was wirklich neu zu tun ist, entscheidet
// HR an der Liste; ein Programm an ihrer Stelle würde raten.
await callFunction(tx, "start_onboarding", {
employee_id: payload.employee_id,
item_keys: ONBOARDING_PUNKTE.map((x) => x.key),
});
});
} catch (err) {
return { success: false, error: err instanceof Error ? err.message : "Unbekannter Fehler." };
}
for (const path of [`/employees/${payload.employee_id}`, "/employees", "/"]) revalidatePath(path);
return { success: true };
}
/** Einen Punkt der Checkliste festhalten — Haken, Wert oder Kommentar. */
export async function setOnboardingTask(payload: {
employee_id: string;
item_key: string;
erledigt?: boolean;
wert?: string | null;
kommentar?: string | null;
}): Promise<ActionResult> {
return callRpc("set_onboarding_task", payload, [`/employees/${payload.employee_id}`]);
}
/** Legt die Checkliste nachträglich an — für Personen von vor dieser Liste. */
export async function startOnboarding(employeeId: string): Promise<ActionResult> {
return callRpc(
"start_onboarding",
{ employee_id: employeeId, item_keys: ONBOARDING_PUNKTE.map((x) => x.key) },
[`/employees/${employeeId}`]
);
}
export async function addEmployeeDependent(payload: {