Files
alpenwerk-hr/actions/notes.ts
Maximilian Stubhan e8e675fd07
All checks were successful
CI / Lint, Typen, Tests, Build (push) Successful in 11m36s
CI / Migrationen auf leerer Datenbank (push) Successful in 10m10s
Turn the note filter around: yours by default, colleagues added
Yesterday's version had it the other way — everyone visible, untick to
hide. The decision from the business side is the opposite: you see your
own notes, and you tick the colleagues you also want. So note_mutes
becomes note_subscriptions and the predicate flips from `not exists` to
`exists`.

The existing rows are not carried over. The meaning inverts rather than
the sign: converting faithfully ("everyone except the muted") would write
almost the whole roster into the new table and reproduce exactly the state
the change is meant to end. Anyone opening the setting tomorrow would
think it had not taken effect. The table is a day old; what is lost is a
few ticks from trying it out.

What this costs is worth saying plainly: the silent case that could not
happen under exceptions can happen now. Do not tick a colleague and you
will not see her follow-ups — not while she is on holiday either. That is
the flip side of the decision, and it is written down in the migration
rather than discovered later.

Each note now says who wrote it. Own notes read "von mir" rather than
repeating your own name, which would sit on every second line and tell
nobody anything. The flag is computed on the server: the user id is
already there, and threading it through four components for one word is a
poor trade. The counter on the button follows the same turn — "+2" for
what you added, nothing when you added nothing.

Verified: 21 tests, five mutation-checked (restoring `not exists`,
dropping the own-notes clause, inverting the default, hiding the author,
and printing your own name instead of "von mir" each turn them red). 489
tests, typecheck, lint, schema drift and build clean. The migration is
reviewed but not run — no reachable database here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 11:36:38 +02:00

62 lines
2.4 KiB
TypeScript

"use server";
import { revalidatePath } from "next/cache";
import { requireUserId } from "@/lib/auth/session";
import { withUser } from "@/lib/db";
import type { ActionResult } from "@/lib/db/rpc";
/**
* Notizen einer Kollegin oder eines Kollegen hinzuwählen oder abwählen.
*
* Kein Aufruf einer SQL-Funktion und kein Protokolleintrag, anders als bei
* allem, was Personaldaten ändert: das hier ist eine persönliche
* Anzeigeeinstellung. Ein Prüfprotokoll, das jeden Haken mitschreibt, machte
* die Suche nach echten Änderungen mühsamer, ohne etwas nachzuweisen.
* Dasselbe Muster wie bei gespeicherten Auswertungen und Entwürfen
* (actions/reports.ts, actions/hireDrafts.ts).
*
* Abgesichert ist es trotzdem: die Regel `note_subscriptions_owner` lässt nur Zeilen
* zu, deren `user_id` die angemeldete Person ist. Eine fremde Einstellung
* liesse sich auch mit erfundenen Werten nicht schreiben.
*/
export async function setNotizSichtbarkeit(payload: {
kollegeId: string;
sichtbar: boolean;
}): Promise<ActionResult> {
const userId = await requireUserId();
// Die eigenen Notizen sind ohnehin immer dabei. Die Prüfbedingung der
// Tabelle weist das ab; hier kommt die Meldung heraus, die jemand lesen
// kann, statt einer Verletzungsmeldung aus der Datenbank.
if (payload.kollegeId === userId) {
return { success: false, error: "Die eigenen Notizen sind immer dabei." };
}
try {
await withUser(userId, async (tx) => {
if (payload.sichtbar) {
// `on conflict do nothing`: zweimal dasselbe Hinzuwählen ist kein
// Fehler, sondern derselbe Wunsch — etwa wenn zwei Reiter offen sind.
await tx
.insertInto("note_subscriptions")
.values({ user_id: userId, author_user_id: payload.kollegeId })
.onConflict((oc) => oc.columns(["user_id", "author_user_id"]).doNothing())
.execute();
} else {
await tx
.deleteFrom("note_subscriptions")
.where("user_id", "=", userId)
.where("author_user_id", "=", payload.kollegeId)
.execute();
}
});
} catch (err) {
return { success: false, error: err instanceof Error ? err.message : "Unbekannter Fehler." };
}
// Die Glocke steckt in der Hülle jeder Seite, die Karte „Anstehend" auf der
// Übersicht. Beide zeigen dieselbe Menge und müssen gemeinsam nachziehen.
revalidatePath("/", "layout");
return { success: true };
}