"use server"; import { revalidatePath } from "next/cache"; import { z } from "zod"; import { requireKitaSession } from "@/lib/auth-utils"; 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 = new Date(`${data.dateOfBirth}T00:00:00`); if (Number.isNaN(parsed.getTime())) { ctx.addIssue({ path: ["dateOfBirth"], code: "custom", message: "Bitte ein gültiges Datum angeben.", }); return; } // In der Zukunft geborene Kinder existieren nicht. const today = new Date(); today.setHours(23, 59, 59, 999); if (parsed > today) { ctx.addIssue({ path: ["dateOfBirth"], code: "custom", message: "Geburtsdatum darf nicht in der Zukunft liegen.", }); } // Schutz vor offensichtlichen Tippfehlern (1899 statt 1989 o.ä.). if (parsed.getFullYear() < 1900) { ctx.addIssue({ path: ["dateOfBirth"], code: "custom", message: "Bitte ein realistisches Geburtsdatum angeben.", }); } }); 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(), }); function parseDateInput(value?: string) { if (!value) return null; const date = new Date(`${value}T00:00:00`); return Number.isNaN(date.getTime()) ? null : date; } // 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." }; } 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." }; } }