Files
alpenwerk-hr/lib/entwuerfe.ts
Maximilian Stubhan 2838919e42
All checks were successful
CI / Lint, Typen, Tests, Build (push) Successful in 11m12s
CI / Migrationen auf leerer Datenbank (push) Successful in 10m6s
@
Name every draft, and put Vertrag before Angehoerige

Two things the dashboard and the hire wizard were getting wrong.

The drafts card named the author only on other people's drafts. With
foreign and own rows side by side that reads as an inconsistency, not as
information: the eye has to work out that a missing name means "mine".
Now every row says it, "von mir" on the own ones -- the same wording the
notes in the bell already use.

In the wizard, Angehoerige stood before Vertrag. What a contract is made
of -- entry date, working days, a fixed term -- is on paper before the
conversation happens; relatives the person brings along, often on the
first day. The optional step came before the one the hire rests on.

Swapping them meant touching the part that would have broken silently:
the per-step validation was a positional list that had to line up with
STEP_LABELS by hand. Reordered labels alone would have left the checks
where they were -- "Weiter" on Vertrag would have validated the
relatives and waved an empty entry date through, until the database
refused it at the end. The checks are keyed by step name now, so they
travel with the step.

Drafts saved before this land on the step number they stored, which now
points at a different page. Nothing is lost -- the payload carries every
field -- but somebody resuming an older draft may open on Vertrag where
they left Angehoerige.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@
2026-09-10 16:35:20 +02:00

84 lines
3.4 KiB
TypeScript

import { sql } from "kysely";
import { zeitstempel } from "./db/json";
import { istHinzugewaehlt } from "./kollegen";
import type { OrgEb } from "./org";
// Die angefangenen Neueinstellungen auf der Übersicht.
//
// Bis September 2026 sah jede Person nur die eigenen. Seit die Auswahl in der
// Glocke auch hierfür gilt (lib/kollegen.ts), kommen die Entwürfe der
// hinzugewählten Kolleg:innen dazu — damit eine angefangene Einstellung nicht
// liegen bleibt, weil die eine Person im Urlaub ist und die andere nicht weiss,
// dass es sie gibt.
//
// **Nur ansehen.** Fortsetzen und Löschen bleiben bei der Person, von der der
// Entwurf stammt; die Regeln hire_drafts_update und hire_drafts_delete lassen
// nichts anderes zu. Ein Entwurf ist unfertige Arbeit ohne Sperre und ohne
// Historie — zwei Personen, die abwechselnd hineinschreiben, überschreiben
// einander lautlos.
export type EntwurfZeile = {
id: string;
step: number;
payload: Record<string, unknown>;
updated_at: string;
created_by: string | null;
author_name: string | null;
author_email: string | null;
};
export type Entwurf = {
id: string;
step: number;
payload: Record<string, unknown>;
updated_at: string;
/** Ob der Entwurf von der angemeldeten Person stammt — nur dann darf sie ihn anfassen. */
vonMir: boolean;
/** Wer ihn angefangen hat — steht an jeder Zeile, auch an den eigenen. */
autor: string;
};
/**
* Die Entwürfe als *Teilabfrage* — zum Einhängen in die eine Abfrage, die die
* Übersicht ohnehin stellt (lib/db/json.ts).
*
* Die Bedingung wiederholt, was `hire_drafts_select` ohnehin durchlässt. Das
* ist Absicht: die Regel in der Datenbank ist die Grenze, aber sie steht in
* einer Migration und lässt sich von hier aus nicht lesen. Steht sie auch in
* der Abfrage, sagt der Quelltext, was die Liste zeigt — und ein Test kann es
* festhalten. Fällt eine der beiden aus, bleibt die andere.
*
* `created_by` kann leer sein (die Person wurde gelöscht). Solche Zeilen zeigt
* niemand: anders als bei einer Notiz steht in einem Entwurf nichts, was ohne
* die Person, die ihn angefangen hat, noch weiterginge.
*/
export function entwuerfeAbfrage(eb: OrgEb, userId: string) {
return eb
.selectFrom("hire_drafts as d")
.leftJoin("profiles as p", "p.id", "d.created_by")
.select(["d.id", "d.step", "d.payload", "d.created_by"])
.select(["p.full_name as author_name", "p.email as author_email"])
.select((x) => zeitstempel(x.ref("d.updated_at")).as("updated_at"))
.where((e) => e.or([e("d.created_by", "=", userId), istHinzugewaehlt(userId, "d.created_by")]))
// Die eigenen zuerst. Vor der Freigabe standen dort nur sie; wer seinen
// halbfertigen Entwurf sucht, soll ihn nicht zwischen fremden suchen.
.orderBy(sql`case when "d"."created_by" = ${userId} then 0 else 1 end`)
.orderBy("d.updated_at", "desc");
}
/** Der reine Teil: aus den Zeilen die Entwürfe mit lesbarem Verfasser. */
export function baueEntwuerfe(rows: EntwurfZeile[], userId: string | null): Entwurf[] {
return rows.map((row) => {
return {
id: row.id,
step: row.step,
payload: row.payload,
updated_at: row.updated_at,
vonMir: userId !== null && row.created_by === userId,
// Ohne Namen die E-Mail — sonst stünde an einem Entwurf nichts ausser
// dem Hinweis, dass er von jemandem ist.
autor: row.author_name?.trim() || row.author_email || "Unbekannt",
};
});
}