feat: add household emergency duty alarms and deadline worker
This commit is contained in:
@@ -51,11 +51,15 @@ const activeNotdienstAlertStatuses = [
|
||||
|
||||
function getTodayUtcDateOnly() {
|
||||
const today = new Date();
|
||||
return new Date(Date.UTC(today.getFullYear(), today.getMonth(), today.getDate()));
|
||||
return new Date(
|
||||
Date.UTC(today.getFullYear(), today.getMonth(), today.getDate()),
|
||||
);
|
||||
}
|
||||
|
||||
function getBaseUrl() {
|
||||
return process.env.NEXTAUTH_URL ?? process.env.AUTH_URL ?? "http://localhost:3000";
|
||||
return (
|
||||
process.env.NEXTAUTH_URL ?? process.env.AUTH_URL ?? "http://localhost:3000"
|
||||
);
|
||||
}
|
||||
|
||||
function formatNotdienstDate(date: Date) {
|
||||
@@ -68,7 +72,7 @@ function revalidateCalendarAndNotdienst() {
|
||||
revalidatePath("/dashboard/notdienst/plan");
|
||||
revalidatePath("/dashboard/notdienst");
|
||||
revalidatePath("/dashboard");
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// Ignore if called outside Next.js context
|
||||
}
|
||||
}
|
||||
@@ -94,6 +98,7 @@ type SwapAssignmentWithFamily = {
|
||||
alerts: {
|
||||
id: string;
|
||||
}[];
|
||||
alarms?: { id: string }[];
|
||||
};
|
||||
|
||||
async function getSwapAssignment(
|
||||
@@ -126,7 +131,11 @@ async function getSwapAssignment(
|
||||
},
|
||||
},
|
||||
alerts: {
|
||||
where: { status: { in: activeNotdienstAlertStatuses } },
|
||||
where: { alarmId: null, status: { in: activeNotdienstAlertStatuses } },
|
||||
select: { id: true },
|
||||
},
|
||||
alarms: {
|
||||
where: { activeAssignmentId: { not: null } },
|
||||
select: { id: true },
|
||||
},
|
||||
},
|
||||
@@ -171,7 +180,12 @@ function validateSwapAssignmentPair({
|
||||
return "Du kannst keinen Tausch mit deinem eigenen Haushalt anfragen.";
|
||||
}
|
||||
|
||||
if (targetAssignment.alerts.length > 0 || offeredAssignment.alerts.length > 0) {
|
||||
if (
|
||||
targetAssignment.alerts.length > 0 ||
|
||||
offeredAssignment.alerts.length > 0 ||
|
||||
targetAssignment.alarms?.length ||
|
||||
offeredAssignment.alarms?.length
|
||||
) {
|
||||
return "Notdienste mit aktiver Alarmierung können nicht getauscht werden.";
|
||||
}
|
||||
|
||||
@@ -215,7 +229,10 @@ async function sendSwapMail({
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
console.error("Notdienst-Tausch-Mail konnte nicht versendet werden:", result.error);
|
||||
console.error(
|
||||
"Notdienst-Tausch-Mail konnte nicht versendet werden:",
|
||||
result.error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,7 +281,9 @@ function buildTerminData(data: z.infer<typeof terminSchema>) {
|
||||
now.setSeconds(0, 0);
|
||||
|
||||
if (startDate < now) {
|
||||
return { error: "Der Startzeitpunkt darf nicht in der Vergangenheit liegen." };
|
||||
return {
|
||||
error: "Der Startzeitpunkt darf nicht in der Vergangenheit liegen.",
|
||||
};
|
||||
}
|
||||
|
||||
if (endDate < startDate) {
|
||||
@@ -286,9 +305,7 @@ function buildTerminData(data: z.infer<typeof terminSchema>) {
|
||||
? new Date(data.rsvpDeadline)
|
||||
: null,
|
||||
participantInfo:
|
||||
data.requiresRsvp && data.participantInfo
|
||||
? data.participantInfo
|
||||
: null,
|
||||
data.requiresRsvp && data.participantInfo ? data.participantInfo : null,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -347,7 +364,10 @@ export async function createNotdienstSwapRequest(rawPayload: unknown) {
|
||||
return { error: "Notdienst nicht gefunden." };
|
||||
}
|
||||
if (existingRequest) {
|
||||
return { error: "Für einen dieser Notdienste gibt es bereits eine offene Tauschanfrage." };
|
||||
return {
|
||||
error:
|
||||
"Für einen dieser Notdienste gibt es bereits eine offene Tauschanfrage.",
|
||||
};
|
||||
}
|
||||
|
||||
await prisma.notdienstSwapRequest.create({
|
||||
@@ -394,128 +414,170 @@ export async function acceptNotdienstSwapRequest(requestIdRaw: unknown) {
|
||||
const today = getTodayUtcDateOnly();
|
||||
|
||||
try {
|
||||
const result = await prisma.$transaction(async (tx) => {
|
||||
const request = await tx.notdienstSwapRequest.findFirst({
|
||||
where: {
|
||||
id: parsed.data,
|
||||
kitaId: session.user.kitaId,
|
||||
status: NotdienstSwapStatus.PENDING,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
requesterFamilyId: true,
|
||||
targetAssignmentId: true,
|
||||
offeredAssignmentId: true,
|
||||
targetAssignment: {
|
||||
select: {
|
||||
id: true,
|
||||
childId: true,
|
||||
date: true,
|
||||
plan: { select: { status: true } },
|
||||
child: {
|
||||
select: {
|
||||
familyId: true,
|
||||
family: {
|
||||
select: {
|
||||
name: true,
|
||||
users: {
|
||||
where: { role: UserRole.ELTERN },
|
||||
select: { email: true, firstName: true, lastName: true },
|
||||
const result = await prisma.$transaction(
|
||||
async (tx) => {
|
||||
const request = await tx.notdienstSwapRequest.findFirst({
|
||||
where: {
|
||||
id: parsed.data,
|
||||
kitaId: session.user.kitaId,
|
||||
status: NotdienstSwapStatus.PENDING,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
requesterFamilyId: true,
|
||||
targetAssignmentId: true,
|
||||
offeredAssignmentId: true,
|
||||
targetAssignment: {
|
||||
select: {
|
||||
id: true,
|
||||
childId: true,
|
||||
date: true,
|
||||
plan: { select: { status: true } },
|
||||
child: {
|
||||
select: {
|
||||
familyId: true,
|
||||
family: {
|
||||
select: {
|
||||
name: true,
|
||||
users: {
|
||||
where: { role: UserRole.ELTERN },
|
||||
select: {
|
||||
email: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
alerts: {
|
||||
where: { status: { in: activeNotdienstAlertStatuses } },
|
||||
select: { id: true },
|
||||
alerts: {
|
||||
where: {
|
||||
alarmId: null,
|
||||
status: { in: activeNotdienstAlertStatuses },
|
||||
},
|
||||
select: { id: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
offeredAssignment: {
|
||||
select: {
|
||||
id: true,
|
||||
childId: true,
|
||||
date: true,
|
||||
plan: { select: { status: true } },
|
||||
child: {
|
||||
select: {
|
||||
familyId: true,
|
||||
family: {
|
||||
select: {
|
||||
name: true,
|
||||
users: {
|
||||
where: { role: UserRole.ELTERN },
|
||||
select: { email: true, firstName: true, lastName: true },
|
||||
offeredAssignment: {
|
||||
select: {
|
||||
id: true,
|
||||
childId: true,
|
||||
date: true,
|
||||
plan: { select: { status: true } },
|
||||
child: {
|
||||
select: {
|
||||
familyId: true,
|
||||
family: {
|
||||
select: {
|
||||
name: true,
|
||||
users: {
|
||||
where: { role: UserRole.ELTERN },
|
||||
select: {
|
||||
email: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
alerts: {
|
||||
where: { status: { in: activeNotdienstAlertStatuses } },
|
||||
select: { id: true },
|
||||
alerts: {
|
||||
where: {
|
||||
alarmId: null,
|
||||
status: { in: activeNotdienstAlertStatuses },
|
||||
},
|
||||
select: { id: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
if (!request) {
|
||||
throw new Error("Tauschanfrage nicht gefunden oder nicht mehr offen.");
|
||||
}
|
||||
if (!request) {
|
||||
throw new Error(
|
||||
"Tauschanfrage nicht gefunden oder nicht mehr offen.",
|
||||
);
|
||||
}
|
||||
|
||||
if (request.targetAssignment.child.familyId !== session.user.familyId) {
|
||||
throw new Error("Nur der angefragte Haushalt kann diese Anfrage annehmen.");
|
||||
}
|
||||
if (
|
||||
await tx.notdienstAlarm.count({
|
||||
where: {
|
||||
activeAssignmentId: {
|
||||
in: [request.targetAssignmentId, request.offeredAssignmentId],
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
throw new Error(
|
||||
"Notdienste mit aktiver Alarmierung können nicht getauscht werden.",
|
||||
);
|
||||
|
||||
const validationError = validateSwapAssignmentPair({
|
||||
targetAssignment: request.targetAssignment,
|
||||
offeredAssignment: request.offeredAssignment,
|
||||
requesterFamilyId: request.requesterFamilyId,
|
||||
today,
|
||||
});
|
||||
if (validationError) {
|
||||
throw new Error(validationError);
|
||||
}
|
||||
if (request.targetAssignment.child.familyId !== session.user.familyId) {
|
||||
throw new Error(
|
||||
"Nur der angefragte Haushalt kann diese Anfrage annehmen.",
|
||||
);
|
||||
}
|
||||
|
||||
await tx.notdienstAssignment.update({
|
||||
where: { id: request.targetAssignmentId },
|
||||
data: { childId: request.offeredAssignment.childId },
|
||||
});
|
||||
await tx.notdienstAssignment.update({
|
||||
where: { id: request.offeredAssignmentId },
|
||||
data: { childId: request.targetAssignment.childId },
|
||||
});
|
||||
await tx.notdienstSwapRequest.update({
|
||||
where: { id: request.id },
|
||||
data: {
|
||||
status: NotdienstSwapStatus.ACCEPTED,
|
||||
respondedById: session.user.id,
|
||||
respondedAt: new Date(),
|
||||
},
|
||||
});
|
||||
await tx.notdienstSwapRequest.updateMany({
|
||||
where: {
|
||||
id: { not: request.id },
|
||||
kitaId: session.user.kitaId,
|
||||
status: NotdienstSwapStatus.PENDING,
|
||||
OR: [
|
||||
{ targetAssignmentId: { in: [request.targetAssignmentId, request.offeredAssignmentId] } },
|
||||
{ offeredAssignmentId: { in: [request.targetAssignmentId, request.offeredAssignmentId] } },
|
||||
],
|
||||
},
|
||||
data: {
|
||||
status: NotdienstSwapStatus.CANCELLED,
|
||||
respondedAt: new Date(),
|
||||
},
|
||||
});
|
||||
const validationError = validateSwapAssignmentPair({
|
||||
targetAssignment: request.targetAssignment,
|
||||
offeredAssignment: request.offeredAssignment,
|
||||
requesterFamilyId: request.requesterFamilyId,
|
||||
today,
|
||||
});
|
||||
if (validationError) {
|
||||
throw new Error(validationError);
|
||||
}
|
||||
|
||||
return {
|
||||
targetAssignment: request.targetAssignment,
|
||||
offeredAssignment: request.offeredAssignment,
|
||||
};
|
||||
});
|
||||
await tx.notdienstAssignment.update({
|
||||
where: { id: request.targetAssignmentId },
|
||||
data: { childId: request.offeredAssignment.childId },
|
||||
});
|
||||
await tx.notdienstAssignment.update({
|
||||
where: { id: request.offeredAssignmentId },
|
||||
data: { childId: request.targetAssignment.childId },
|
||||
});
|
||||
await tx.notdienstSwapRequest.update({
|
||||
where: { id: request.id },
|
||||
data: {
|
||||
status: NotdienstSwapStatus.ACCEPTED,
|
||||
respondedById: session.user.id,
|
||||
respondedAt: new Date(),
|
||||
},
|
||||
});
|
||||
await tx.notdienstSwapRequest.updateMany({
|
||||
where: {
|
||||
id: { not: request.id },
|
||||
kitaId: session.user.kitaId,
|
||||
status: NotdienstSwapStatus.PENDING,
|
||||
OR: [
|
||||
{
|
||||
targetAssignmentId: {
|
||||
in: [request.targetAssignmentId, request.offeredAssignmentId],
|
||||
},
|
||||
},
|
||||
{
|
||||
offeredAssignmentId: {
|
||||
in: [request.targetAssignmentId, request.offeredAssignmentId],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
data: {
|
||||
status: NotdienstSwapStatus.CANCELLED,
|
||||
respondedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
targetAssignment: request.targetAssignment,
|
||||
offeredAssignment: request.offeredAssignment,
|
||||
};
|
||||
},
|
||||
{ isolationLevel: "Serializable" },
|
||||
);
|
||||
|
||||
const kita = await prisma.kita.findUnique({
|
||||
where: { id: session.user.kitaId },
|
||||
@@ -550,7 +612,10 @@ export async function acceptNotdienstSwapRequest(requestIdRaw: unknown) {
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return {
|
||||
error: error instanceof Error ? error.message : "Tauschanfrage konnte nicht angenommen werden.",
|
||||
error:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Tauschanfrage konnte nicht angenommen werden.",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -619,7 +684,9 @@ export async function declineNotdienstSwapRequest(requestIdRaw: unknown) {
|
||||
return { error: "Tauschanfrage nicht gefunden oder nicht mehr offen." };
|
||||
}
|
||||
if (request.targetAssignment.child.familyId !== session.user.familyId) {
|
||||
return { error: "Nur der angefragte Haushalt kann diese Anfrage ablehnen." };
|
||||
return {
|
||||
error: "Nur der angefragte Haushalt kann diese Anfrage ablehnen.",
|
||||
};
|
||||
}
|
||||
|
||||
await prisma.notdienstSwapRequest.update({
|
||||
@@ -706,19 +773,22 @@ export async function cancelNotdienstSwapRequest(requestIdRaw: unknown) {
|
||||
revalidateCalendarAndNotdienst();
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("Fehler beim Zurückziehen der Notdienst-Tauschanfrage:", error);
|
||||
console.error(
|
||||
"Fehler beim Zurückziehen der Notdienst-Tauschanfrage:",
|
||||
error,
|
||||
);
|
||||
return { error: "Tauschanfrage konnte nicht zurückgezogen werden." };
|
||||
}
|
||||
}
|
||||
|
||||
export async function createTerminRequest(rawPayload: unknown) {
|
||||
const session = await requireKitaSession();
|
||||
|
||||
|
||||
const parsed = terminSchema.safeParse(rawPayload);
|
||||
if (!parsed.success) {
|
||||
return { error: "Ungültige Eingabedaten." };
|
||||
}
|
||||
|
||||
|
||||
const data = parsed.data;
|
||||
const terminData = buildTerminData(data);
|
||||
|
||||
@@ -755,12 +825,12 @@ export async function createTerminAdmin(rawPayload: unknown) {
|
||||
if (!kitaId) {
|
||||
return { error: "Kein Mandant zugeordnet." };
|
||||
}
|
||||
|
||||
|
||||
const parsed = terminSchema.safeParse(rawPayload);
|
||||
if (!parsed.success) {
|
||||
return { error: "Ungültige Eingabedaten." };
|
||||
}
|
||||
|
||||
|
||||
const data = parsed.data;
|
||||
const terminData = buildTerminData(data);
|
||||
|
||||
@@ -885,7 +955,10 @@ export async function rejectTermin(terminId: string, reason?: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function toggleMitbringselList(terminId: string, enabled: boolean) {
|
||||
export async function toggleMitbringselList(
|
||||
terminId: string,
|
||||
enabled: boolean,
|
||||
) {
|
||||
const session = await requireRole([UserRole.ADMIN, UserRole.KOORDINATOR]);
|
||||
const kitaId = session.user.kitaId;
|
||||
if (!kitaId) {
|
||||
@@ -919,12 +992,12 @@ const mitbringselSchema = z.object({
|
||||
|
||||
export async function addMitbringsel(rawPayload: unknown) {
|
||||
const session = await requireKitaSession();
|
||||
|
||||
|
||||
const parsed = mitbringselSchema.safeParse(rawPayload);
|
||||
if (!parsed.success) {
|
||||
return { error: "Ungültige Eingabedaten." };
|
||||
}
|
||||
|
||||
|
||||
const { terminId, content } = parsed.data;
|
||||
|
||||
try {
|
||||
@@ -933,7 +1006,11 @@ export async function addMitbringsel(rawPayload: unknown) {
|
||||
where: { id: terminId, kitaId: session.user.kitaId },
|
||||
});
|
||||
|
||||
if (!termin || termin.status !== TerminStatus.CONFIRMED || !termin.mitbringselListEnabled) {
|
||||
if (
|
||||
!termin ||
|
||||
termin.status !== TerminStatus.CONFIRMED ||
|
||||
!termin.mitbringselListEnabled
|
||||
) {
|
||||
return { error: "Mitbring-Liste ist für diesen Termin nicht aktiv." };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user