feat: complete notdienst planning, quick wins and notifications

This commit is contained in:
t.indorf
2026-10-12 15:34:09 +02:00
parent b915857951
commit 00f602a7aa
50 changed files with 2443 additions and 513 deletions
+46 -10
View File
@@ -24,6 +24,7 @@ import { serializableTransaction } from "@/lib/transaction";
import { rateLimit } from "@/lib/rate-limit";
import { prisma } from "@/lib/prisma";
import { lockEvent } from "@/lib/event-lock";
import { canRequestNotdienstSwap } from "@/lib/notdienst-swaps";
// =====================================================================
// /dashboard/kalender · Server Actions
@@ -127,7 +128,7 @@ async function getSwapAssignment(
select: {
name: true,
users: {
where: { role: UserRole.ELTERN, emailVerifiedAt: { not: null }, disabledAt: null, privacyPolicyAcceptedAt: { not: null } },
where: { role: { in: [UserRole.ELTERN, UserRole.ADMIN, UserRole.KOORDINATOR] }, emailVerifiedAt: { not: null }, disabledAt: null, privacyPolicyAcceptedAt: { not: null } },
select: {
email: true,
firstName: true,
@@ -335,11 +336,8 @@ function buildTerminData(
export async function createNotdienstSwapRequest(rawPayload: unknown) {
const session = await requireKitaSession();
if (!(await rateLimit(`createNotdienstSwapRequest:${session.user.kitaId}:${session.user.id}`, 30, 3600)).allowed) return { error: "Zu viele Anfragen. Bitte versuche es später erneut." };
if (session.user.role !== UserRole.ELTERN) {
return { error: "Nur Eltern können Notdienst-Tausche anfragen." };
}
if (!session.user.familyId) {
return { error: "Dein Account ist noch keinem Haushalt zugeordnet." };
if (!canRequestNotdienstSwap(session.user.role, session.user.familyId)) {
return { error: "Nur Eltern mit einem eigenen Haushalt können Notdienst-Tausche anfragen." };
}
const parsed = swapRequestSchema.safeParse(rawPayload);
@@ -467,7 +465,7 @@ export async function acceptNotdienstSwapRequest(requestIdRaw: unknown) {
select: {
name: true,
users: {
where: { role: UserRole.ELTERN, emailVerifiedAt: { not: null }, disabledAt: null, privacyPolicyAcceptedAt: { not: null } },
where: { role: { in: [UserRole.ELTERN, UserRole.ADMIN, UserRole.KOORDINATOR] }, emailVerifiedAt: { not: null }, disabledAt: null, privacyPolicyAcceptedAt: { not: null } },
select: { email: true, firstName: true, lastName: true },
},
},
@@ -493,7 +491,7 @@ export async function acceptNotdienstSwapRequest(requestIdRaw: unknown) {
select: {
name: true,
users: {
where: { role: UserRole.ELTERN, emailVerifiedAt: { not: null }, disabledAt: null, privacyPolicyAcceptedAt: { not: null } },
where: { role: { in: [UserRole.ELTERN, UserRole.ADMIN, UserRole.KOORDINATOR] }, emailVerifiedAt: { not: null }, disabledAt: null, privacyPolicyAcceptedAt: { not: null } },
select: { email: true, firstName: true, lastName: true },
},
},
@@ -636,7 +634,7 @@ export async function declineNotdienstSwapRequest(requestIdRaw: unknown) {
select: {
name: true,
users: {
where: { role: UserRole.ELTERN, emailVerifiedAt: { not: null }, disabledAt: null, privacyPolicyAcceptedAt: { not: null } },
where: { role: { in: [UserRole.ELTERN, UserRole.ADMIN, UserRole.KOORDINATOR] }, emailVerifiedAt: { not: null }, disabledAt: null, privacyPolicyAcceptedAt: { not: null } },
select: { email: true, firstName: true, lastName: true },
},
},
@@ -654,7 +652,7 @@ export async function declineNotdienstSwapRequest(requestIdRaw: unknown) {
select: {
name: true,
users: {
where: { role: UserRole.ELTERN, emailVerifiedAt: { not: null }, disabledAt: null, privacyPolicyAcceptedAt: { not: null } },
where: { role: { in: [UserRole.ELTERN, UserRole.ADMIN, UserRole.KOORDINATOR] }, emailVerifiedAt: { not: null }, disabledAt: null, privacyPolicyAcceptedAt: { not: null } },
select: { email: true, firstName: true, lastName: true },
},
},
@@ -1059,6 +1057,44 @@ export async function addMitbringsel(rawPayload: unknown) {
}
}
const updateMitbringselSchema = z.object({
itemId: z.string().min(1).max(100),
content: mitbringselSchema.shape.content,
});
export async function updateMitbringsel(rawPayload: unknown) {
const session = await requireKitaSession();
const parsed = updateMitbringselSchema.safeParse(rawPayload);
if (!parsed.success) return { error: "Bitte einen Beitrag mit 1 bis 500 Zeichen eingeben." };
if (!(await rateLimit(`updateMitbringsel:${session.user.kitaId}:${session.user.id}`, 30, 3600)).allowed) {
return { error: "Zu viele Änderungen. Bitte versuche es später erneut." };
}
try {
await serializableTransaction(async (tx) => {
const item = await tx.mitbringselItem.findFirst({
where: { id: parsed.data.itemId, kitaId: session.user.kitaId, userId: session.user.id },
select: { terminId: true },
});
if (!item) throw new UserFacingError("Du kannst nur eigene Mitbringbeiträge bearbeiten.");
await lockEvent(tx, item.terminId, session.user.kitaId);
const termin = await tx.termin.findFirst({
where: { id: item.terminId, kitaId: session.user.kitaId, status: TerminStatus.CONFIRMED, mitbringselListEnabled: true },
select: { id: true },
});
if (!termin) throw new UserFacingError("Die Mitbring-Liste ist für diesen Termin nicht mehr aktiv.");
await tx.mitbringselItem.update({
where: { id: parsed.data.itemId, kitaId: session.user.kitaId, userId: session.user.id },
data: { content: parsed.data.content },
});
});
revalidatePath("/dashboard/kalender");
return { success: true };
} catch (error) {
return { error: publicActionError(error, "Der Beitrag konnte nicht gespeichert werden.") };
}
}
export async function deleteMitbringsel(itemId: string) {
const session = await requireKitaSession();