feat(mandant): Betriebsmodus, Kundenverwaltung und Einstellungen (#22)
Zwei Betriebsmodi, ein Codepfad. src/server/mandant.ts ist die eine Stelle, an der die Mandantenregel steht. Der Review fand drei Blocker: die Lesemodelle wandten die Mandantensicht nicht an, der Protokolleintrag des Moduswechsels stand außerhalb der Transaktion, und Prüfung und Vollzug lagen so weit auseinander, dass zwei gleichzeitige Wechsel beide durchliefen. Alle drei geschlossen, der letzte mit einem Advisory Lock. 336 Tests. Closes #8 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit was merged in pull request #22.
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { z } from "zod";
|
||||
import { Betriebsmodus } from "@/generated/prisma/enums";
|
||||
import {
|
||||
EinstellungsFehler,
|
||||
NamensregelnSchema,
|
||||
einstellungenSpeichern,
|
||||
modusWechseln,
|
||||
} from "@/server/services/einstellungen";
|
||||
import { rechtErforderlich } from "@/server/auth/sitzungspflicht";
|
||||
|
||||
/** Server Actions der Einstellungen (#008). Recht prüfen, Zod, Service rufen. */
|
||||
|
||||
export type AktionsErgebnis = { readonly fehler?: string; readonly erfolg?: string };
|
||||
|
||||
const EinstellungsSchema = z.object({
|
||||
eigeneOrganisationName: z.string().trim().min(1, "Die eigene Organisation braucht einen Namen."),
|
||||
gesperrteOu: z.string().trim().optional(),
|
||||
namensregeln: NamensregelnSchema,
|
||||
});
|
||||
|
||||
export async function einstellungenSpeichernAktion(
|
||||
_vorher: AktionsErgebnis,
|
||||
formular: FormData,
|
||||
): Promise<AktionsErgebnis> {
|
||||
const { benutzer } = await rechtErforderlich("verwaltung");
|
||||
|
||||
const eingabe = EinstellungsSchema.safeParse({
|
||||
eigeneOrganisationName: formular.get("eigeneOrganisationName"),
|
||||
gesperrteOu: formular.get("gesperrteOu") ?? undefined,
|
||||
namensregeln: {
|
||||
upn: formular.get("regel_upn") ?? undefined,
|
||||
sam: formular.get("regel_sam") ?? undefined,
|
||||
mail: formular.get("regel_mail") ?? undefined,
|
||||
hostname: formular.get("regel_hostname") ?? undefined,
|
||||
},
|
||||
});
|
||||
if (!eingabe.success) {
|
||||
return { fehler: eingabe.error.issues[0]?.message ?? "Die Eingabe ist unvollständig." };
|
||||
}
|
||||
|
||||
try {
|
||||
await einstellungenSpeichern(
|
||||
{
|
||||
eigeneOrganisationName: eingabe.data.eigeneOrganisationName,
|
||||
namensregeln: eingabe.data.namensregeln,
|
||||
gesperrteOu: eingabe.data.gesperrteOu ?? null,
|
||||
},
|
||||
benutzer.id,
|
||||
);
|
||||
revalidatePath("/admin");
|
||||
return { erfolg: "Gespeichert." };
|
||||
} catch (fehler) {
|
||||
if (fehler instanceof EinstellungsFehler) return { fehler: fehler.message };
|
||||
console.error("Einstellungen speichern fehlgeschlagen:", fehler);
|
||||
return { fehler: "Die Einstellungen konnten nicht gespeichert werden." };
|
||||
}
|
||||
}
|
||||
|
||||
const WechselSchema = z.object({
|
||||
ziel: z.enum(Betriebsmodus),
|
||||
// Der Bestaetigungsdialog verlangt ein ausdrueckliches Haekchen: der Wechsel
|
||||
// aendert, was die ganze Anwendung zeigt.
|
||||
bestaetigt: z.literal("ja", { message: "Bitte den Wechsel ausdrücklich bestätigen." }),
|
||||
name: z.string().trim().optional(),
|
||||
kuerzel: z.string().trim().optional(),
|
||||
});
|
||||
|
||||
export async function modusWechselnAktion(
|
||||
_vorher: AktionsErgebnis,
|
||||
formular: FormData,
|
||||
): Promise<AktionsErgebnis> {
|
||||
const { benutzer } = await rechtErforderlich("verwaltung");
|
||||
|
||||
const eingabe = WechselSchema.safeParse({
|
||||
ziel: formular.get("ziel"),
|
||||
bestaetigt: formular.get("bestaetigt"),
|
||||
name: formular.get("name") ?? undefined,
|
||||
kuerzel: formular.get("kuerzel") ?? undefined,
|
||||
});
|
||||
if (!eingabe.success) {
|
||||
return { fehler: eingabe.error.issues[0]?.message ?? "Die Eingabe ist unvollständig." };
|
||||
}
|
||||
|
||||
try {
|
||||
await modusWechseln(
|
||||
eingabe.data.ziel,
|
||||
{ name: eingabe.data.name, kuerzel: eingabe.data.kuerzel },
|
||||
benutzer.id,
|
||||
);
|
||||
// Der Modus aendert Navigation und Beschriftungen ueberall.
|
||||
revalidatePath("/", "layout");
|
||||
return {
|
||||
erfolg:
|
||||
eingabe.data.ziel === Betriebsmodus.DIENSTLEISTUNG
|
||||
? "Der Betriebsmodus ist jetzt Dienstleistung. Kunden sind ab sofort eine sichtbare Ebene."
|
||||
: "Der Betriebsmodus ist jetzt Intern. Die Kundenebene ist ausgeblendet.",
|
||||
};
|
||||
} catch (fehler) {
|
||||
if (fehler instanceof EinstellungsFehler) return { fehler: fehler.message };
|
||||
console.error("Moduswechsel fehlgeschlagen:", fehler);
|
||||
return { fehler: "Der Moduswechsel ist fehlgeschlagen." };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user