Add educator email invitations and staff notification preferences

This commit is contained in:
t.indorf
2026-10-08 11:32:38 +02:00
parent 043fed2e46
commit 90b8843cda
15 changed files with 700 additions and 39 deletions
+89 -2
View File
@@ -28,6 +28,7 @@ export type StaffNotice = {
toAdmins: boolean;
};
type CommandResult = {
emailIds?: string[];
notice?: StaffNotice;
invite?: { email: string; name: string; token: string; kitaName: string };
};
@@ -214,6 +215,33 @@ export async function executeStaffCommand(
throw new Error(
"Dein Konto hat keinen Zugriff auf den Personalbereich.",
);
if (command === "emailPreference") {
if (actor.role !== UserRole.ERZIEHER)
throw new Error(
"Diese Einstellung ist für Fachkräfte vorgesehen.",
);
await tx.user.update({
where: { id: actor.id },
data: {
staffDecisionEmails: z.boolean().parse(data.enabled),
},
});
return {};
}
if (command === "emailRetry") {
const jobs = await tx.staffEmail.findMany({
where: {
profile: { kitaId: actor.kitaId },
...(actor.role === UserRole.ADMIN ? {} : { userId: actor.id }),
sentAt: null,
skippedAt: null,
},
select: { id: true },
orderBy: { createdAt: "asc" },
take: 50,
});
return { emailIds: jobs.map((job) => job.id) };
}
if (command === "holiday") {
requireAdmin(actor);
const input = z
@@ -356,6 +384,7 @@ export async function executeStaffCommand(
const notice = async (
text: string,
toAdmins: boolean,
email?: { message: string; query: string },
): Promise<CommandResult> => {
const recipients = await tx.user.findMany({
where: {
@@ -364,7 +393,7 @@ export async function executeStaffCommand(
? { role: UserRole.ADMIN }
: { id: profile.userId ?? "" }),
},
select: { id: true },
select: { id: true, role: true, staffDecisionEmails: true },
});
await tx.staffNotification.createMany({
data: recipients.map((user) => ({
@@ -373,7 +402,28 @@ export async function executeStaffCommand(
message: `${profile.educator.firstName} ${profile.educator.lastName}: ${text}`,
})),
});
return { notice: { profileId, text, toAdmins } };
const emailIds: string[] = [];
if (email) {
for (const user of recipients) {
if (
!toAdmins &&
(user.role !== UserRole.ERZIEHER || !user.staffDecisionEmails)
)
continue;
const job = await tx.staffEmail.create({
data: {
profileId,
userId: user.id,
toAdmins,
subject: "Kita-Planer: Hinweis im Personalbereich",
message: `${profile.educator.firstName} ${profile.educator.lastName}: ${email.message}`,
path: `/dashboard/personal?educator=${encodeURIComponent(profile.educatorId)}&${email.query}`,
},
});
emailIds.push(job.id);
}
}
return { notice: { profileId, text, toAdmins }, emailIds };
};
if (command === "noticeRead") {
await tx.staffNotification.updateMany({
@@ -446,7 +496,18 @@ export async function executeStaffCommand(
where: { id: actor.kitaId },
select: { name: true },
});
const job = await tx.staffEmail.create({
data: {
profileId,
userId: user.id,
subject: `Einladung zu ${kita.name} im Kita-Planer`,
message: "Account aktivieren",
path: `/invite/${token}`,
inviteToken: token,
},
});
return {
emailIds: [job.id],
invite: {
email,
name: `${profile.educator.firstName} ${profile.educator.lastName}`,
@@ -845,6 +906,10 @@ export async function executeStaffCommand(
return notice(
`Arbeitswoche ab ${start} wurde zur Prüfung eingereicht.`,
true,
{
message: `Arbeitswoche ${start}–${end} wurde zur Prüfung eingereicht.`,
query: `tab=zeit&week=${start}`,
},
);
}
requireAdmin(actor);
@@ -901,6 +966,10 @@ export async function executeStaffCommand(
return notice(
`Arbeitswoche ab ${start}: ${input.approve ? "genehmigt" : "zur Korrektur zurückgegeben"}. ${input.reason}`,
false,
{
message: `Arbeitswoche ${start}–${end}: ${input.approve ? "genehmigt" : "zur Korrektur zurückgegeben"}.`,
query: `tab=zeit&week=${start}`,
},
);
}
if (
@@ -937,6 +1006,12 @@ export async function executeStaffCommand(
? "Ein offener Abwesenheitsantrag wurde zurückgezogen."
: "Die Stornierung einer genehmigten Abwesenheit wurde beantragt.",
true,
existing.kind === "SICKNESS"
? undefined
: {
message: `Abwesenheitsantrag ${dateKey(existing.startDate)}–${dateKey(existing.endDate)}: ${status === "CANCELLED" ? "zurückgezogen" : "Stornierung beantragt"}.`,
query: `tab=urlaub&year=${existing.startDate.getUTCFullYear()}`,
},
);
}
if (command === "leaveReview") {
@@ -1032,6 +1107,12 @@ export async function executeStaffCommand(
return notice(
`${existing.kind === "VACATION" ? "Urlaub" : existing.kind === "TIME_OFF" ? "Freizeitausgleich" : "Krankmeldung"}: ${status === "CANCELLED" ? "storniert" : status === "REJECTED" ? "abgelehnt" : "genehmigt"}. ${input.reason}`,
false,
existing.kind === "SICKNESS"
? undefined
: {
message: `${existing.kind === "VACATION" ? "Urlaub" : "Freizeitausgleich"} ${dateKey(existing.startDate)}–${dateKey(existing.endDate)}: ${status === "CANCELLED" ? "storniert" : status === "REJECTED" ? "abgelehnt" : existing.status === "CANCEL_REQUESTED" ? "Stornierung abgelehnt; Genehmigung bleibt bestehen" : "genehmigt"}.`,
query: `tab=urlaub&year=${existing.startDate.getUTCFullYear()}`,
},
);
}
if (!profile.educator.active)
@@ -1140,6 +1221,12 @@ export async function executeStaffCommand(
return notice(
`${input.kind === "SICKNESS" ? "Krankheit wurde hinterlegt" : "Ein Abwesenheitsantrag wurde eingereicht"}: ${input.startDate}–${input.endDate}.`,
input.kind !== "SICKNESS",
input.kind === "SICKNESS"
? undefined
: {
message: `${input.kind === "VACATION" ? "Urlaubsantrag" : "Antrag auf Freizeitausgleich"} ${input.startDate}–${input.endDate} wurde ${existing ? "geändert" : "eingereicht"}.`,
query: `tab=urlaub&year=${input.startDate.slice(0, 4)}`,
},
);
}
throw new Error("Unbekannte Aktion.");