import { sql, type Expression, type SqlBool } from "kysely"; import type { Tx } from "./db"; import { jsonArrayFrom, zeitstempel } from "./db/json"; import { fmtName } from "./format"; import type { OrgEb } from "./org"; import type { Database } from "./types"; export type OpenNote = Database["public"]["Tables"]["employee_notes"]["Row"] & { employeeName: string; }; /** Was die Teilabfrage liefert: die Notiz plus den Namen aus dem Join. */ export type NotizZeile = Omit & { created_at: string; done_at: string | null; first_name: string | null; last_name: string | null; }; // „Meine Notizen" (Topbar-Glocke): das gemeinsame, mitarbeiterübergreifende // Postfach aller noch nicht erledigten HR-Notizen — unabhängig davon, zu wem // sie gehören (mit Nutzer abgestimmt). // // Seit note_mutes lässt sich einschränken, *wessen* Notizen man sehen will. // Der Standard bleibt „alle": wer niemanden abwählt, sieht denselben Topf wie // vorher. // // Früher zwei Abfragen, in JavaScript zusammengeführt, weil die API-Schicht // für eine einzelne verschachtelte Abfrage keine Verknüpfung anbot. Am // direkten Zugang ist es schlicht ein Join. /** * Wessen Notizen diese Person sehen will. * * Der Standard ist unverändert: alle. Wer niemanden abgewählt hat, sieht * weiterhin den gemeinsamen Topf — die Einstellung engt ein, sie öffnet * nichts. * * Zwei Dinge, die zusammengehören: * * 1. **Die eigenen Notizen immer.** Sie sind der Grund, warum es die Glocke * gibt. `note_mutes` lässt einen Selbstbezug ohnehin nicht zu; die * Bedingung steht hier trotzdem, damit die Zusage nicht davon abhängt, * dass eine Prüfbedingung an anderer Stelle bestehen bleibt. * * 2. **Notizen ohne Verfasser bleiben sichtbar.** `author_user_id` kann leer * sein. Eine Notiz auszublenden, weil niemand weiss, von wem sie ist, * wäre genau der stille Verlust, den diese Liste verhindern soll. * * Steht als eigener Ausdruck hier und nicht in der jeweiligen Seite, weil * ihn zwei Stellen brauchen: die Glocke in der Kopfzeile und die Karte * „Anstehend" auf der Übersicht. Zwei Fassungen derselben Regel driften * auseinander, und die Übersicht zeigte dann etwas anderes als die Glocke. * * Der Vorgabewert für `spalte` setzt voraus, dass die Notiztabelle als `n` * eingebunden ist — so machen es beide Aufrufer. Wer anders aliasiert, gibt * den Namen mit. */ export function sichtbareNotizen(userId: string, spalte = "n.author_user_id"): Expression { const verfasser = sql.ref(spalte); return sql`( ${verfasser} is null or ${verfasser} = ${userId} or not exists ( select 1 from note_mutes m where m.user_id = ${userId} and m.muted_user_id = ${verfasser}))`; } /** * Die offenen Notizen als *Teilabfrage* — zum Einhängen in die eine Abfrage, * die eine Seite ohnehin stellt (lib/db/json.ts). * * Die beiden Zeitstempel gehen durch zeitstempel(): innerhalb von JSON * formatiert Postgres sie anders als der Treiber es sonst täte, und der * Unterschied fällt erst beim Vergleichen auf. */ export function offeneNotizenAbfrage(eb: OrgEb, userId: string) { return eb .selectFrom("employee_notes as n") .leftJoin("employees as e", "e.id", "n.employee_id") .select([ "n.id", "n.employee_id", "n.author_user_id", "n.author_name", "n.category", "n.note_text", "n.due_date", "n.done", "n.done_by", "e.first_name", "e.last_name", ]) .select((x) => [zeitstempel(x.ref("n.created_at")).as("created_at"), zeitstempel(x.ref("n.done_at")).as("done_at")]) .where("n.done", "=", false) .where(sichtbareNotizen(userId)) .orderBy("n.created_at", "desc"); } /** Der reine Teil: aus den Zeilen die Notizen mit lesbarem Namen. */ export function baueOffeneNotizen(rows: NotizZeile[]): OpenNote[] { return rows.map((row) => { const { first_name, last_name, ...note } = row; return { ...(note as Database["public"]["Tables"]["employee_notes"]["Row"]), employeeName: first_name && last_name ? fmtName(first_name, last_name) : "Unbekannt", }; }); } export async function loadOpenNotes(tx: Tx, userId: string): Promise { const { notes } = await tx .selectNoFrom((eb) => [jsonArrayFrom(offeneNotizenAbfrage(eb, userId)).as("notes")]) .executeTakeFirstOrThrow(); return baueOffeneNotizen(notes as NotizZeile[]); }