253 lines
6.5 KiB
TypeScript
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 };
|
|
}
|