feat: add parent announcements and family or child event responses
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
||||
NotdienstAlertStatus,
|
||||
NotdienstPlanStatus,
|
||||
NotdienstSwapStatus,
|
||||
RsvpMode,
|
||||
TerminStatus,
|
||||
TerminType,
|
||||
UserRole,
|
||||
@@ -18,6 +19,7 @@ import { NotdienstSwapEmail } from "@/emails/NotdienstSwapEmail";
|
||||
import { requireKitaSession, requireRole } from "@/lib/auth-utils";
|
||||
import { sendAppEmail } from "@/lib/mail";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { lockEvent } from "@/lib/event-lock";
|
||||
|
||||
// =====================================================================
|
||||
// /dashboard/kalender · Server Actions
|
||||
@@ -30,8 +32,9 @@ const terminSchema = z.object({
|
||||
startDate: z.string().datetime(),
|
||||
endDate: z.string().datetime(),
|
||||
allDay: z.boolean().default(false),
|
||||
mitbringselListEnabled: z.boolean().default(false),
|
||||
requiresRsvp: z.boolean().default(false),
|
||||
mitbringselListEnabled: z.boolean().optional(),
|
||||
requiresRsvp: z.boolean().optional(),
|
||||
rsvpMode: z.nativeEnum(RsvpMode).optional(),
|
||||
rsvpDeadline: z.string().datetime().nullable().optional(),
|
||||
participantInfo: z.string().trim().max(1000).optional(),
|
||||
});
|
||||
@@ -274,13 +277,16 @@ async function notifyFamilyAboutSwap({
|
||||
);
|
||||
}
|
||||
|
||||
function buildTerminData(data: z.infer<typeof terminSchema>) {
|
||||
function buildTerminData(
|
||||
data: z.infer<typeof terminSchema>,
|
||||
allowPast = false,
|
||||
) {
|
||||
const startDate = new Date(data.startDate);
|
||||
const endDate = new Date(data.endDate);
|
||||
const now = new Date();
|
||||
now.setSeconds(0, 0);
|
||||
|
||||
if (startDate < now) {
|
||||
if (!allowPast && startDate < now) {
|
||||
return {
|
||||
error: "Der Startzeitpunkt darf nicht in der Vergangenheit liegen.",
|
||||
};
|
||||
@@ -290,6 +296,16 @@ function buildTerminData(data: z.infer<typeof terminSchema>) {
|
||||
return { error: "Das Enddatum darf nicht vor dem Startdatum liegen." };
|
||||
}
|
||||
|
||||
if (
|
||||
data.requiresRsvp &&
|
||||
data.rsvpDeadline &&
|
||||
new Date(data.rsvpDeadline) > startDate
|
||||
) {
|
||||
return {
|
||||
error: "Die Rückmeldefrist darf nicht nach dem Terminbeginn liegen.",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
data: {
|
||||
title: data.title,
|
||||
@@ -298,8 +314,9 @@ function buildTerminData(data: z.infer<typeof terminSchema>) {
|
||||
startDate,
|
||||
endDate,
|
||||
allDay: data.allDay,
|
||||
mitbringselListEnabled: data.mitbringselListEnabled,
|
||||
requiresRsvp: data.requiresRsvp,
|
||||
mitbringselListEnabled: data.mitbringselListEnabled ?? false,
|
||||
requiresRsvp: data.requiresRsvp ?? false,
|
||||
rsvpMode: data.rsvpMode ?? RsvpMode.CHILD,
|
||||
rsvpDeadline:
|
||||
data.requiresRsvp && data.rsvpDeadline
|
||||
? new Date(data.rsvpDeadline)
|
||||
@@ -852,6 +869,7 @@ export async function createTerminAdmin(rawPayload: unknown) {
|
||||
|
||||
revalidatePath("/dashboard/kalender");
|
||||
revalidatePath("/dashboard/admin/kalender");
|
||||
revalidatePath("/dashboard");
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("Fehler beim Erstellen des Termins:", error);
|
||||
@@ -859,7 +877,10 @@ export async function createTerminAdmin(rawPayload: unknown) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateTerminAdmin(terminId: string, rawPayload: unknown) {
|
||||
export async function updateTerminAdmin(
|
||||
terminId: string,
|
||||
rawPayload: unknown,
|
||||
): Promise<{ error?: string; success?: boolean }> {
|
||||
const session = await requireRole([UserRole.ADMIN, UserRole.KOORDINATOR]);
|
||||
const kitaId = session.user.kitaId;
|
||||
|
||||
@@ -872,19 +893,50 @@ export async function updateTerminAdmin(terminId: string, rawPayload: unknown) {
|
||||
return { error: "Ungültige Eingabedaten." };
|
||||
}
|
||||
|
||||
const terminData = buildTerminData(parsed.data);
|
||||
if ("error" in terminData) {
|
||||
return { error: terminData.error };
|
||||
}
|
||||
|
||||
try {
|
||||
await prisma.termin.update({
|
||||
where: {
|
||||
id: terminId,
|
||||
kitaId,
|
||||
},
|
||||
data: terminData.data,
|
||||
const result = await prisma.$transaction(async (tx) => {
|
||||
await lockEvent(tx, terminId, kitaId);
|
||||
const existing = await tx.termin.findFirst({
|
||||
where: { id: terminId, kitaId },
|
||||
include: {
|
||||
_count: {
|
||||
select: { participations: true, familyParticipations: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!existing) return { error: "Termin wurde nicht gefunden." };
|
||||
const data = {
|
||||
...parsed.data,
|
||||
mitbringselListEnabled:
|
||||
parsed.data.mitbringselListEnabled ?? existing.mitbringselListEnabled,
|
||||
requiresRsvp: parsed.data.requiresRsvp ?? existing.requiresRsvp,
|
||||
rsvpMode: parsed.data.rsvpMode ?? existing.rsvpMode,
|
||||
rsvpDeadline:
|
||||
parsed.data.rsvpDeadline === undefined
|
||||
? (existing.rsvpDeadline?.toISOString() ?? null)
|
||||
: parsed.data.rsvpDeadline,
|
||||
participantInfo:
|
||||
parsed.data.participantInfo ?? existing.participantInfo ?? "",
|
||||
};
|
||||
if (
|
||||
data.rsvpMode !== existing.rsvpMode &&
|
||||
existing._count.participations + existing._count.familyParticipations >
|
||||
0
|
||||
) {
|
||||
return {
|
||||
error:
|
||||
"Die Antwortart kann nach der ersten Rückmeldung nicht mehr geändert werden.",
|
||||
};
|
||||
}
|
||||
const terminData = buildTerminData(data, true);
|
||||
if ("error" in terminData) return { error: terminData.error };
|
||||
await tx.termin.update({
|
||||
where: { id: terminId, kitaId },
|
||||
data: terminData.data,
|
||||
});
|
||||
return { success: true };
|
||||
});
|
||||
if (result.error) return result;
|
||||
|
||||
revalidatePath("/dashboard/kalender");
|
||||
revalidatePath("/dashboard/admin/kalender");
|
||||
@@ -918,6 +970,7 @@ export async function approveTermin(terminId: string) {
|
||||
|
||||
revalidatePath("/dashboard/kalender");
|
||||
revalidatePath("/dashboard/admin/kalender");
|
||||
revalidatePath("/dashboard");
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("Fehler beim Bestätigen des Termins:", error);
|
||||
@@ -948,6 +1001,7 @@ export async function rejectTermin(terminId: string, reason?: string) {
|
||||
|
||||
revalidatePath("/dashboard/kalender");
|
||||
revalidatePath("/dashboard/admin/kalender");
|
||||
revalidatePath("/dashboard");
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("Fehler beim Ablehnen des Termins:", error);
|
||||
@@ -978,6 +1032,7 @@ export async function toggleMitbringselList(
|
||||
|
||||
revalidatePath("/dashboard/kalender");
|
||||
revalidatePath("/dashboard/admin/kalender");
|
||||
revalidatePath("/dashboard");
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("Fehler beim Umschalten der Mitbring-Liste:", error);
|
||||
|
||||
Reference in New Issue
Block a user