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>
67 lines
2.7 KiB
TypeScript
67 lines
2.7 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";
|
|
|
|
/**
|
|
* Eine Kollegin oder einen Kollegen hinzuwählen oder abwählen.
|
|
*
|
|
* Die Auswahl steuert zwei Listen: die Notizen in der Glocke und die
|
|
* Entwürfe auf der Übersicht. Der Name der Funktion nennt nur die erste,
|
|
* weil die Einstellung in der Glocke sitzt — was sie bewirkt, steht an der
|
|
* Einstellung selbst.
|
|
*
|
|
* 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 `colleague_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("colleague_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("colleague_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 };
|
|
}
|