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." };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { z } from "zod";
|
||||
import { KundenFehler, kundeAnlegen, kundeArchivieren, kundeBearbeiten } from "@/server/services/kunden";
|
||||
import { rechtErforderlich } from "@/server/auth/sitzungspflicht";
|
||||
|
||||
/** Server Actions der Kundenverwaltung (#008). Recht prüfen, Zod, Service rufen. */
|
||||
|
||||
export type AktionsErgebnis = { readonly fehler?: string; readonly erfolg?: string };
|
||||
|
||||
const NeuSchema = z.object({
|
||||
name: z.string().trim().min(1, "Bitte einen Namen angeben."),
|
||||
kuerzel: z.string().trim().min(1, "Bitte ein Kürzel angeben."),
|
||||
notizen: z.string().trim().optional(),
|
||||
});
|
||||
|
||||
export async function kundeAnlegenAktion(
|
||||
_vorher: AktionsErgebnis,
|
||||
formular: FormData,
|
||||
): Promise<AktionsErgebnis> {
|
||||
const { benutzer } = await rechtErforderlich("verwaltung");
|
||||
|
||||
const eingabe = NeuSchema.safeParse({
|
||||
name: formular.get("name"),
|
||||
kuerzel: formular.get("kuerzel"),
|
||||
notizen: formular.get("notizen") ?? undefined,
|
||||
});
|
||||
if (!eingabe.success) {
|
||||
return { fehler: eingabe.error.issues[0]?.message ?? "Die Eingabe ist unvollständig." };
|
||||
}
|
||||
|
||||
try {
|
||||
const kunde = await kundeAnlegen(eingabe.data, benutzer.id);
|
||||
revalidatePath("/kunden");
|
||||
return { erfolg: `${kunde.name} (${kunde.kuerzel}) wurde angelegt.` };
|
||||
} catch (fehler) {
|
||||
if (fehler instanceof KundenFehler) return { fehler: fehler.message };
|
||||
console.error("Kunde anlegen fehlgeschlagen:", fehler);
|
||||
return { fehler: "Der Kunde konnte nicht angelegt werden." };
|
||||
}
|
||||
}
|
||||
|
||||
const BearbeitenSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
name: z.string().trim().min(1, "Bitte einen Namen angeben."),
|
||||
kuerzel: z.string().trim().min(1, "Bitte ein Kürzel angeben."),
|
||||
notizen: z.string().trim().optional(),
|
||||
});
|
||||
|
||||
export async function kundeBearbeitenAktion(
|
||||
_vorher: AktionsErgebnis,
|
||||
formular: FormData,
|
||||
): Promise<AktionsErgebnis> {
|
||||
const { benutzer } = await rechtErforderlich("verwaltung");
|
||||
|
||||
const eingabe = BearbeitenSchema.safeParse({
|
||||
id: formular.get("id"),
|
||||
name: formular.get("name"),
|
||||
kuerzel: formular.get("kuerzel"),
|
||||
notizen: formular.get("notizen") ?? undefined,
|
||||
});
|
||||
if (!eingabe.success) {
|
||||
return { fehler: eingabe.error.issues[0]?.message ?? "Die Eingabe ist unvollständig." };
|
||||
}
|
||||
|
||||
try {
|
||||
await kundeBearbeiten(
|
||||
eingabe.data.id,
|
||||
{
|
||||
name: eingabe.data.name,
|
||||
kuerzel: eingabe.data.kuerzel,
|
||||
notizen: eingabe.data.notizen ?? null,
|
||||
},
|
||||
benutzer.id,
|
||||
);
|
||||
revalidatePath("/kunden");
|
||||
return { erfolg: "Gespeichert." };
|
||||
} catch (fehler) {
|
||||
if (fehler instanceof KundenFehler) return { fehler: fehler.message };
|
||||
console.error("Kunde bearbeiten fehlgeschlagen:", fehler);
|
||||
return { fehler: "Die Änderung konnte nicht gespeichert werden." };
|
||||
}
|
||||
}
|
||||
|
||||
export async function kundeArchivierenAktion(
|
||||
_vorher: AktionsErgebnis,
|
||||
formular: FormData,
|
||||
): Promise<AktionsErgebnis> {
|
||||
const { benutzer } = await rechtErforderlich("verwaltung");
|
||||
const eingabe = z
|
||||
.object({ id: z.string().min(1), archiviert: z.enum(["true", "false"]) })
|
||||
.safeParse({ id: formular.get("id"), archiviert: formular.get("archiviert") });
|
||||
if (!eingabe.success) {
|
||||
return { fehler: "Die Eingabe ist unvollständig." };
|
||||
}
|
||||
|
||||
try {
|
||||
await kundeArchivieren(eingabe.data.id, eingabe.data.archiviert === "true", benutzer.id);
|
||||
revalidatePath("/kunden");
|
||||
return {};
|
||||
} catch (fehler) {
|
||||
// Der Versuch, die eigene Organisation zu archivieren, hat eine sorgfaeltig
|
||||
// formulierte Begruendung – die soll der Admin sehen und keine Fehlerseite.
|
||||
if (fehler instanceof KundenFehler) return { fehler: fehler.message };
|
||||
console.error("Kunde archivieren fehlgeschlagen:", fehler);
|
||||
return { fehler: "Die Änderung konnte nicht gespeichert werden." };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user