"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, 3–10 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." }; } }