feat: add parent announcements and family or child event responses
This commit is contained in:
@@ -5,13 +5,17 @@ import { mkdir, rm, writeFile } from "fs/promises";
|
||||
import path from "path";
|
||||
import { createElement } from "react";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { Prisma, UserRole } from "@prisma/client";
|
||||
import { AnnouncementAudience, UserRole } from "@prisma/client";
|
||||
import { z } from "zod";
|
||||
|
||||
import { NewsEmail } from "@/emails/NewsEmail";
|
||||
import { requireKitaSession, requireRole } from "@/lib/auth-utils";
|
||||
import { getAppEmailConfigError, sendAppEmail } from "@/lib/mail";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import {
|
||||
announcementRecipientWhere,
|
||||
visibleAnnouncementAudiences,
|
||||
} from "@/lib/announcement-access";
|
||||
|
||||
const MAX_ATTACHMENT_SIZE = 10 * 1024 * 1024;
|
||||
const ALLOWED_TYPES = new Map([
|
||||
@@ -55,25 +59,15 @@ function validateAttachment(file: File) {
|
||||
return null;
|
||||
}
|
||||
|
||||
async function getRecipients(kitaId: string, audience: z.infer<typeof audienceSchema>) {
|
||||
const where: Prisma.UserWhereInput =
|
||||
audience === "ELTERN"
|
||||
? {
|
||||
kitaId,
|
||||
familyId: { not: null },
|
||||
emailVerifiedAt: { not: null },
|
||||
}
|
||||
: audience === "ERZIEHER"
|
||||
? {
|
||||
kitaId,
|
||||
role: UserRole.ERZIEHER,
|
||||
emailVerifiedAt: { not: null },
|
||||
}
|
||||
: {
|
||||
kitaId,
|
||||
emailVerifiedAt: { not: null },
|
||||
OR: [{ familyId: { not: null } }, { role: UserRole.ERZIEHER }],
|
||||
};
|
||||
async function getRecipients(
|
||||
kitaId: string,
|
||||
audience: z.infer<typeof audienceSchema>,
|
||||
) {
|
||||
const where = {
|
||||
kitaId,
|
||||
emailVerifiedAt: { not: null },
|
||||
...announcementRecipientWhere(audience as AnnouncementAudience),
|
||||
};
|
||||
|
||||
const users = await prisma.user.findMany({
|
||||
where,
|
||||
@@ -129,6 +123,7 @@ export async function createAnnouncement(formData: FormData) {
|
||||
kitaId,
|
||||
title: parsed.data.title,
|
||||
content: parsed.data.content,
|
||||
audience: parsed.data.audience,
|
||||
authorId: session.user.id,
|
||||
attachments: {
|
||||
create: attachmentInputs.map((attachment) => ({
|
||||
@@ -166,7 +161,10 @@ export async function createAnnouncement(formData: FormData) {
|
||||
rm(path.join(uploadDir, attachment.id), { force: true }),
|
||||
),
|
||||
);
|
||||
console.error("Ankündigungs-Anhang konnte nicht gespeichert werden:", error);
|
||||
console.error(
|
||||
"Ankündigungs-Anhang konnte nicht gespeichert werden:",
|
||||
error,
|
||||
);
|
||||
return { error: "Anhang konnte nicht gespeichert werden." };
|
||||
}
|
||||
}
|
||||
@@ -198,6 +196,7 @@ export async function createAnnouncement(formData: FormData) {
|
||||
|
||||
revalidatePath("/dashboard");
|
||||
revalidatePath("/dashboard/admin/news");
|
||||
revalidatePath("/dashboard/mitteilungen");
|
||||
|
||||
if (emailError) {
|
||||
return {
|
||||
@@ -216,6 +215,7 @@ export async function markAnnouncementRead(announcementId: string) {
|
||||
where: {
|
||||
id: announcementId,
|
||||
kitaId: session.user.kitaId,
|
||||
audience: { in: visibleAnnouncementAudiences(session.user) },
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
@@ -241,5 +241,6 @@ export async function markAnnouncementRead(announcementId: string) {
|
||||
});
|
||||
|
||||
revalidatePath("/dashboard");
|
||||
revalidatePath("/dashboard/mitteilungen");
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
+114
-77
@@ -3,20 +3,16 @@
|
||||
import { revalidatePath } from "next/cache";
|
||||
import {
|
||||
EventParticipationStatus,
|
||||
RsvpMode,
|
||||
TerminStatus,
|
||||
UserRole,
|
||||
} from "@prisma/client";
|
||||
import { z } from "zod";
|
||||
import { rsvpSubmissionSchema } from "@/lib/rsvp";
|
||||
import { lockEvent } from "@/lib/event-lock";
|
||||
|
||||
import { requireKitaSession, requireRole } from "@/lib/auth-utils";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
const submitRsvpSchema = z.object({
|
||||
eventId: z.string().min(1),
|
||||
childId: z.string().min(1),
|
||||
status: z.nativeEnum(EventParticipationStatus),
|
||||
});
|
||||
|
||||
export type EventParticipantDto = {
|
||||
childId: string;
|
||||
childName: string;
|
||||
@@ -24,79 +20,120 @@ export type EventParticipantDto = {
|
||||
status: EventParticipationStatus | null;
|
||||
};
|
||||
|
||||
export async function submitEventRsvp(rawPayload: unknown) {
|
||||
export async function submitEventRsvp(
|
||||
rawPayload: unknown,
|
||||
): Promise<{ error?: string; success?: boolean }> {
|
||||
const session = await requireKitaSession();
|
||||
const parsed = submitRsvpSchema.safeParse(rawPayload);
|
||||
|
||||
if (!parsed.success) {
|
||||
return { error: "Ungültige Eingabedaten." };
|
||||
const parsed = rsvpSubmissionSchema.safeParse(rawPayload);
|
||||
if (!parsed.success)
|
||||
return {
|
||||
error:
|
||||
"Bitte alle Antworten und bei Familienzusagen die Anzahl Erwachsener angeben.",
|
||||
};
|
||||
if (!session.user.familyId || session.user.role === UserRole.ERZIEHER) {
|
||||
return {
|
||||
error: "Für Rückmeldungen ist ein Elternkonto mit Haushalt erforderlich.",
|
||||
};
|
||||
}
|
||||
|
||||
if (!session.user.familyId) {
|
||||
return { error: "Dein Account ist noch keinem Haushalt zugeordnet." };
|
||||
const kitaId = session.user.kitaId;
|
||||
const familyId = session.user.familyId;
|
||||
const payload = parsed.data;
|
||||
try {
|
||||
const result = await prisma.$transaction(async (tx) => {
|
||||
await lockEvent(tx, payload.eventId, kitaId);
|
||||
const event = await tx.termin.findFirst({
|
||||
where: { id: payload.eventId, kitaId },
|
||||
select: {
|
||||
status: true,
|
||||
requiresRsvp: true,
|
||||
rsvpMode: true,
|
||||
rsvpDeadline: true,
|
||||
startDate: true,
|
||||
kita: { select: { terminModuleEnabled: true } },
|
||||
},
|
||||
});
|
||||
if (
|
||||
!event?.kita.terminModuleEnabled ||
|
||||
!event.requiresRsvp ||
|
||||
event.status !== TerminStatus.CONFIRMED
|
||||
) {
|
||||
return { error: "Für diesen Termin ist keine Rückmeldung aktiv." };
|
||||
}
|
||||
if (event.rsvpMode !== payload.mode)
|
||||
return {
|
||||
error: "Die Antwortart wurde geändert. Bitte lade den Termin erneut.",
|
||||
};
|
||||
if (payload.mode === RsvpMode.FAMILY) {
|
||||
const family = await tx.family.findFirst({
|
||||
where: { id: familyId, kitaId },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!family) return { error: "Haushalt wurde nicht gefunden." };
|
||||
if (
|
||||
(event.rsvpDeadline ?? event.startDate) <= new Date() ||
|
||||
event.startDate <= new Date()
|
||||
) {
|
||||
return { error: "Die Rückmeldefrist ist bereits abgelaufen." };
|
||||
}
|
||||
await tx.familyEventParticipation.upsert({
|
||||
where: { eventId_familyId: { eventId: payload.eventId, familyId } },
|
||||
create: {
|
||||
kitaId,
|
||||
eventId: payload.eventId,
|
||||
familyId,
|
||||
status: payload.status,
|
||||
adultCount: payload.adultCount,
|
||||
},
|
||||
update: { status: payload.status, adultCount: payload.adultCount },
|
||||
});
|
||||
} else {
|
||||
const children = await tx.child.findMany({
|
||||
where: { kitaId, familyId, active: true },
|
||||
select: { id: true },
|
||||
});
|
||||
const ids = new Set(children.map((child) => child.id));
|
||||
if (
|
||||
payload.responses.length !== ids.size ||
|
||||
payload.responses.some((row) => !ids.has(row.childId))
|
||||
) {
|
||||
return {
|
||||
error:
|
||||
"Bitte antworte für alle eigenen aktiven Kinder. Lade den Termin bei Änderungen erneut.",
|
||||
};
|
||||
}
|
||||
if (
|
||||
(event.rsvpDeadline ?? event.startDate) <= new Date() ||
|
||||
event.startDate <= new Date()
|
||||
) {
|
||||
return { error: "Die Rückmeldefrist ist bereits abgelaufen." };
|
||||
}
|
||||
for (const response of payload.responses) {
|
||||
await tx.eventParticipation.upsert({
|
||||
where: {
|
||||
eventId_childId: {
|
||||
eventId: payload.eventId,
|
||||
childId: response.childId,
|
||||
},
|
||||
},
|
||||
create: { kitaId, eventId: payload.eventId, ...response },
|
||||
update: { status: response.status },
|
||||
});
|
||||
}
|
||||
}
|
||||
return { success: true };
|
||||
});
|
||||
if (result.error) return result;
|
||||
revalidatePath("/dashboard");
|
||||
revalidatePath("/dashboard/kalender");
|
||||
revalidatePath("/dashboard/admin/kalender");
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error("Rückmeldung konnte nicht gespeichert werden:", error);
|
||||
return {
|
||||
error:
|
||||
"Rückmeldung konnte nicht gespeichert werden. Bitte versuche es erneut.",
|
||||
};
|
||||
}
|
||||
|
||||
const { eventId, childId, status } = parsed.data;
|
||||
|
||||
const event = await prisma.termin.findFirst({
|
||||
where: {
|
||||
id: eventId,
|
||||
kitaId: session.user.kitaId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
status: true,
|
||||
requiresRsvp: true,
|
||||
rsvpDeadline: true,
|
||||
startDate: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!event || !event.requiresRsvp || event.status !== TerminStatus.CONFIRMED) {
|
||||
return { error: "Für diesen Termin ist keine Rückmeldung aktiv." };
|
||||
}
|
||||
|
||||
const deadline = event.rsvpDeadline ?? event.startDate;
|
||||
if (deadline < new Date()) {
|
||||
return { error: "Die Rückmeldefrist ist bereits abgelaufen." };
|
||||
}
|
||||
|
||||
const child = await prisma.child.findFirst({
|
||||
where: {
|
||||
id: childId,
|
||||
kitaId: session.user.kitaId,
|
||||
familyId: session.user.familyId,
|
||||
active: true,
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!child) {
|
||||
return { error: "Dieses Kind gehört nicht zu deinem Haushalt." };
|
||||
}
|
||||
|
||||
await prisma.eventParticipation.upsert({
|
||||
where: {
|
||||
eventId_childId: {
|
||||
eventId,
|
||||
childId,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
kitaId: session.user.kitaId,
|
||||
eventId,
|
||||
childId,
|
||||
status,
|
||||
},
|
||||
update: {
|
||||
status,
|
||||
},
|
||||
});
|
||||
|
||||
revalidatePath("/dashboard");
|
||||
revalidatePath("/dashboard/kalender");
|
||||
revalidatePath("/dashboard/admin/kalender");
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
export async function getEventParticipants(
|
||||
|
||||
Reference in New Issue
Block a user