Files

253 lines
6.5 KiB
TypeScript

"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 };
}