"use server"; import { createElement } from "react"; import { revalidatePath } from "next/cache"; import { DutyAssignmentStatus, UserRole } from "@prisma/client"; import { endOfWeek, format, startOfDay, startOfWeek } from "date-fns"; import { de } from "date-fns/locale"; import { dateOnlySchema } from "@/lib/date-schema"; import { z } from "zod"; import { DutyReminderEmail } from "@/emails/DutyReminderEmail"; import { requireRole } from "@/lib/auth-utils"; import { getAppEmailConfigError, sendAppEmail } from "@/lib/mail"; import { rateLimit } from "@/lib/rate-limit"; import { prisma } from "@/lib/prisma"; const dutyTypeSchema = z.object({ name: z.string().trim().min(2).max(60), description: z.string().trim().max(240).optional(), }); const assignmentSchema = z.object({ dutyTypeId: z.string().min(1), familyId: z.string().nullable(), startDate: dateOnlySchema, endDate: dateOnlySchema, }); async function requireDutyAdmin() { const session = await requireRole([UserRole.ADMIN, UserRole.SUPERADMIN]); if (!session.user.kitaId) { throw new Error("Kein Mandant zugeordnet."); } return { session, kitaId: session.user.kitaId }; } function parseDateOnly(value: string) { const date = new Date(value); if (Number.isNaN(date.getTime())) { throw new Error("Ungueltiges Datum."); } return startOfDay(date); } export async function createDutyType(formData: FormData) { const { kitaId } = await requireDutyAdmin(); const parsed = dutyTypeSchema.safeParse({ name: formData.get("name"), description: formData.get("description") || undefined, }); if (!parsed.success) { return; } try { await prisma.dutyType.create({ data: { kitaId, name: parsed.data.name, description: parsed.data.description, }, }); revalidatePath("/dashboard/admin/dienste"); } catch (error) { console.error("Fehler beim Erstellen des Diensttyps:", error); } } export async function deleteDutyType(formData: FormData) { const { kitaId } = await requireDutyAdmin(); const dutyTypeId = String(formData.get("dutyTypeId") ?? ""); if (!dutyTypeId) { return; } await prisma.dutyType.deleteMany({ where: { id: dutyTypeId, kitaId }, }); revalidatePath("/dashboard/admin/dienste"); revalidatePath("/dashboard/kalender"); revalidatePath("/dashboard"); } export async function upsertDutyAssignment(rawPayload: unknown) { const { kitaId } = await requireDutyAdmin(); const parsed = assignmentSchema.safeParse(rawPayload); if (!parsed.success) { return { error: "Ungueltige Eingabedaten." }; } const startDate = parseDateOnly(parsed.data.startDate); const endDate = parseDateOnly(parsed.data.endDate); if (endDate < startDate) { return { error: "Das Enddatum liegt vor dem Startdatum." }; } const dutyType = await prisma.dutyType.findFirst({ where: { id: parsed.data.dutyTypeId, kitaId }, select: { id: true }, }); if (!dutyType) { return { error: "Diensttyp gehoert nicht zu dieser Kita." }; } if (!parsed.data.familyId) { await prisma.dutyAssignment.deleteMany({ where: { kitaId, dutyTypeId: parsed.data.dutyTypeId, startDate, }, }); revalidatePath("/dashboard/admin/dienste"); revalidatePath("/dashboard/kalender"); revalidatePath("/dashboard"); return { success: true }; } const family = await prisma.family.findFirst({ where: { id: parsed.data.familyId, kitaId }, select: { id: true }, }); if (!family) { return { error: "Familie gehoert nicht zu dieser Kita." }; } await prisma.dutyAssignment.upsert({ where: { kitaId_dutyTypeId_startDate: { kitaId, dutyTypeId: parsed.data.dutyTypeId, startDate, }, }, create: { kitaId, dutyTypeId: parsed.data.dutyTypeId, familyId: parsed.data.familyId, startDate, endDate, status: DutyAssignmentStatus.PLANNED, }, update: { familyId: parsed.data.familyId, endDate, status: DutyAssignmentStatus.PLANNED, reminderSentAt: null, }, }); revalidatePath("/dashboard/admin/dienste"); revalidatePath("/dashboard/kalender"); revalidatePath("/dashboard"); return { success: true }; } export async function checkAndSendDutyReminders() { const { kitaId } = await requireDutyAdmin(); if (!(await rateLimit(`duty-reminders:${kitaId}`, 3, 3600)).allowed) return { error: "Bitte versuche es später erneut." }; const configError = getAppEmailConfigError(); if (configError) { return { error: configError }; } const now = new Date(); const weekStart = startOfWeek(now, { weekStartsOn: 1 }); const weekEnd = endOfWeek(now, { weekStartsOn: 1 }); const assignments = await prisma.dutyAssignment.findMany({ where: { kitaId, status: DutyAssignmentStatus.PLANNED, reminderSentAt: null, startDate: { gte: startOfDay(weekStart), lte: startOfDay(weekEnd), }, }, select: { id: true, startDate: true, endDate: true, dutyType: { select: { name: true, }, }, family: { select: { name: true, users: { where: { emailVerifiedAt: { not: null }, disabledAt: null, privacyPolicyAcceptedAt: { not: null }, }, select: { email: true, }, }, }, }, }, }); let sent = 0; let skipped = 0; const errors: string[] = []; for (const assignment of assignments) { const recipients = assignment.family.users.map((user) => user.email); if (recipients.length === 0) { skipped += 1; continue; } const weekLabel = `${format(assignment.startDate, "dd.MM.", { locale: de, })} - ${format(assignment.endDate, "dd.MM.yyyy", { locale: de })}`; const result = await sendAppEmail({ to: recipients, subject: `Erinnerung: ${assignment.dutyType.name} diese Woche`, react: createElement(DutyReminderEmail, { familyName: assignment.family.name, dutyName: assignment.dutyType.name, weekLabel, }), }); if (!result.success) { errors.push(`${assignment.family.name}: ${result.error}`); continue; } await prisma.dutyAssignment.update({ where: { id: assignment.id }, data: { reminderSentAt: new Date() }, }); sent += recipients.length; } revalidatePath("/dashboard/admin/dienste"); return { success: true, sent, skipped, errors }; }