351 lines
9.2 KiB
TypeScript
351 lines
9.2 KiB
TypeScript
"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." };
|
||
}
|
||
}
|