Files
kita-planer/src/app/dashboard/profil/actions.ts
T
2026-05-20 22:47:52 +02:00

351 lines
9.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use server";
import { revalidatePath } from "next/cache";
import { z } from "zod";
import { NotdienstPlanStatus } from "@prisma/client";
import { requireKitaSession } from "@/lib/auth-utils";
import {
CHILD_MAX_AGE_YEARS,
getChildBirthdateRange,
parseDateInput,
} from "@/lib/child-birthdate";
import { prisma } from "@/lib/prisma";
// Telefonnummern: führendes "+" optional, dann Ziffern, Leerzeichen, Bindestriche,
// Klammern oder Schrägstriche. Mindestens 6 Ziffern damit es plausibel ist.
// Schließt damit "abc-123-not-a-phone" aus, lässt aber internationale Formate zu.
const phonePattern = /^[+]?[0-9][\d\s\-()/.]{4,}$/;
// PLZ: Ziffern/Buchstaben/Leerzeichen/Bindestriche, 310 Zeichen.
// Kein striktes DE-Format (5 Ziffern), damit auch AT/CH/UK denkbar sind.
const postalCodePattern = /^[0-9A-Za-z][0-9A-Za-z\s-]{2,9}$/;
const childSchema = z
.object({
firstName: z.string().min(1, "Vorname ist erforderlich.").max(100).trim(),
lastName: z.string().min(1, "Nachname ist erforderlich.").max(100).trim(),
dateOfBirth: z.string().optional(),
})
.superRefine((data, ctx) => {
if (!data.dateOfBirth) return;
const parsed = parseDateInput(data.dateOfBirth);
if (!parsed) {
ctx.addIssue({
path: ["dateOfBirth"],
code: "custom",
message: "Bitte ein gültiges Datum angeben.",
});
return;
}
const { min, max } = getChildBirthdateRange();
if (data.dateOfBirth > max) {
ctx.addIssue({
path: ["dateOfBirth"],
code: "custom",
message: "Geburtsdatum darf nicht in der Zukunft liegen.",
});
}
if (data.dateOfBirth < min) {
ctx.addIssue({
path: ["dateOfBirth"],
code: "custom",
message: `Kinder dürfen maximal ${CHILD_MAX_AGE_YEARS} Jahre alt sein.`,
});
}
});
const familySchema = z.object({
familyName: z.string().min(1, "Familienname ist erforderlich.").max(120).trim(),
});
const contactSchema = z.object({
phone: z
.string()
.trim()
.max(50)
.optional()
.refine(
(v) => !v || phonePattern.test(v),
"Bitte eine gültige Telefonnummer angeben.",
),
street: z.string().trim().max(120).optional(),
postalCode: z
.string()
.trim()
.max(20)
.optional()
.refine(
(v) => !v || postalCodePattern.test(v),
"Bitte eine gültige Postleitzahl angeben.",
),
city: z.string().trim().max(100).optional(),
});
// Gibt die erste konkrete Field-Error-Message aus einem Zod-Fehler zurück,
// damit der User nicht nur "Ungültige Eingabedaten" sieht.
function firstZodMessage(
error: z.ZodError,
fallback: string,
): string {
const issue = error.issues[0];
return issue?.message || fallback;
}
async function requireOwnFamilyChild(
childId: string,
familyId: string | null,
kitaId: string,
) {
if (!familyId) return null;
return prisma.child.findFirst({
where: { id: childId, familyId, kitaId },
});
}
export async function createMyChild(rawPayload: unknown) {
const session = await requireKitaSession();
const parsed = childSchema.safeParse(rawPayload);
if (!session.user.familyId) {
return { error: "Dein Account ist noch keinem Haushalt zugeordnet." };
}
if (!parsed.success) {
return { error: firstZodMessage(parsed.error, "Ungültige Eingabedaten.") };
}
try {
await prisma.child.create({
data: {
kitaId: session.user.kitaId,
familyId: session.user.familyId,
firstName: parsed.data.firstName,
lastName: parsed.data.lastName,
dateOfBirth: parseDateInput(parsed.data.dateOfBirth),
},
});
revalidatePath("/dashboard/profil");
revalidatePath("/dashboard/families");
return { success: true };
} catch (error) {
console.error("Fehler beim Anlegen des Kindes:", error);
return { error: "Das Kind konnte nicht angelegt werden." };
}
}
export async function createMyFamily(rawPayload: unknown) {
const session = await requireKitaSession();
const parsed = familySchema.safeParse(rawPayload);
if (!parsed.success) {
return { error: "Bitte gib einen gültigen Familiennamen ein." };
}
if (session.user.familyId) {
return { error: "Dein Account ist bereits einem Haushalt zugeordnet." };
}
try {
await prisma.$transaction(async (tx) => {
const family = await tx.family.create({
data: {
kitaId: session.user.kitaId,
name: parsed.data.familyName,
},
});
await tx.user.update({
where: { id: session.user.id },
data: { familyId: family.id },
});
});
revalidatePath("/dashboard/profil");
revalidatePath("/dashboard/families");
revalidatePath("/dashboard/adressbuch");
revalidatePath("/dashboard");
return { success: true };
} catch (error) {
console.error("Fehler beim Anlegen des eigenen Haushalts:", error);
return { error: "Der Haushalt konnte nicht angelegt werden." };
}
}
export async function updateMyContact(rawPayload: unknown) {
const session = await requireKitaSession();
const parsed = contactSchema.safeParse(rawPayload);
if (!parsed.success) {
return { error: firstZodMessage(parsed.error, "Ungültige Kontaktdaten.") };
}
try {
await prisma.user.update({
where: { id: session.user.id },
data: {
phone: parsed.data.phone || null,
street: parsed.data.street || null,
postalCode: parsed.data.postalCode || null,
city: parsed.data.city || null,
},
});
revalidatePath("/dashboard/profil");
revalidatePath("/dashboard");
revalidatePath("/dashboard/adressbuch");
return { success: true };
} catch (error) {
console.error("Fehler beim Aktualisieren der Kontaktdaten:", error);
return { error: "Kontaktdaten konnten nicht gespeichert werden." };
}
}
export async function updateMyChild(childId: string, rawPayload: unknown) {
const session = await requireKitaSession();
const parsed = childSchema.safeParse(rawPayload);
if (!parsed.success) {
return { error: firstZodMessage(parsed.error, "Ungültige Eingabedaten.") };
}
try {
const child = await requireOwnFamilyChild(
childId,
session.user.familyId,
session.user.kitaId,
);
if (!child) {
return { error: "Dieses Kind gehört nicht zu deinem Haushalt." };
}
await prisma.child.update({
where: { id: childId },
data: {
firstName: parsed.data.firstName,
lastName: parsed.data.lastName,
dateOfBirth: parseDateInput(parsed.data.dateOfBirth),
},
});
revalidatePath("/dashboard/profil");
revalidatePath("/dashboard/families");
return { success: true };
} catch (error) {
console.error("Fehler beim Aktualisieren des Kindes:", error);
return { error: "Das Kind konnte nicht aktualisiert werden." };
}
}
export async function deleteMyChild(childId: string) {
const session = await requireKitaSession();
try {
const child = await requireOwnFamilyChild(
childId,
session.user.familyId,
session.user.kitaId,
);
if (!child) {
return { error: "Dieses Kind gehört nicht zu deinem Haushalt." };
}
await prisma.child.delete({
where: { id: childId },
});
revalidatePath("/dashboard/profil");
revalidatePath("/dashboard/families");
return { success: true };
} catch (error) {
console.error("Fehler beim Entfernen des Kindes:", error);
return { error: "Das Kind konnte nicht entfernt werden." };
}
}
export async function deleteMyAccount() {
const session = await requireKitaSession();
try {
const user = await prisma.user.findFirst({
where: {
id: session.user.id,
kitaId: session.user.kitaId,
},
select: {
id: true,
familyId: true,
},
});
if (!user) {
return { error: "Account wurde nicht gefunden." };
}
const today = new Date();
today.setHours(0, 0, 0, 0);
const activeNotdienstAssignments = user.familyId
? await prisma.notdienstAssignment.count({
where: {
kitaId: session.user.kitaId,
date: { gte: today },
plan: { status: NotdienstPlanStatus.PUBLISHED },
child: { familyId: user.familyId },
},
})
: 0;
if (activeNotdienstAssignments > 0) {
return {
error:
"Dein Account kann nicht gelöscht werden, solange aktive Notdienst-Verpflichtungen bestehen.",
};
}
if (!user.familyId) {
await prisma.user.delete({
where: { id: user.id },
});
return { success: true };
}
const family = await prisma.family.findFirst({
where: {
id: user.familyId,
kitaId: session.user.kitaId,
},
select: {
id: true,
users: {
select: { id: true },
},
},
});
if (!family) {
return { error: "Haushalt wurde nicht gefunden." };
}
if (family.users.length <= 1) {
await prisma.family.delete({
where: { id: family.id },
});
return { success: true };
}
await prisma.user.delete({
where: { id: user.id },
});
return { success: true };
} catch (error) {
console.error("Fehler beim Löschen des Accounts:", error);
return { error: "Ein Fehler ist beim Löschen aufgetreten." };
}
}