Files
alpenwerk-hr/lib/entwuerfe.ts
Maximilian Stubhan eeaf210e78
All checks were successful
CI / Lint, Typen, Tests, Build (push) Successful in 11m47s
CI / Migrationen auf leerer Datenbank (push) Successful in 10m10s
Let the same choice open notes and drafts
The picker in the bell now governs both lists, so note_subscriptions is
renamed to colleague_subscriptions -- a name that only mentions notes would
mislead the next reader.

Reading and writing a draft now reach differently far. hire_drafts_owner
(for all) is split into four policies: select lets in your own drafts and
those of the people you added, while insert/update/delete stay with the
owner. A draft is unfinished work with no lock and no history; two people
writing into the same row would overwrite each other silently.

That split forces a change in the actions: a policy does not reject a write,
it lets it hit no rows. saveHireDraft and deleteHireDraft now read the row
count instead of reporting success over a row that never changed.

The card shows a foreign draft with its author and without Fortsetzen or
Loeschen -- offering a button that reliably ends in a database error is a
promise without cover.

check-schema-types.mjs learns `alter table ... rename to`; without it the
drift check reports one rename as two errors.

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

85 lines
3.5 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. Leer bei den eigenen: der Name sagte dort nichts. */
autor: string | null;
};
/**
* 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) => {
const vonMir = userId !== null && row.created_by === userId;
return {
id: row.id,
step: row.step,
payload: row.payload,
updated_at: row.updated_at,
vonMir,
// Ohne Namen die E-Mail — sonst stünde an einem fremden Entwurf nichts
// ausser dem Hinweis, dass er fremd ist.
autor: vonMir ? null : row.author_name?.trim() || row.author_email || "Unbekannt",
};
});
}