feat: show planning availability and manage Kita access roles

This commit is contained in:
t.indorf
2026-10-12 16:28:56 +02:00
parent 99d3a63eed
commit 061024d901
29 changed files with 2347 additions and 404 deletions
+48 -17
View File
@@ -81,7 +81,11 @@ async function inviteStaff(
if (
existingUser &&
(existingUser.kitaId !== actor.kitaId ||
existingUser.role !== UserRole.ERZIEHER ||
!(
existingUser.role === UserRole.ERZIEHER ||
(existingUser.role === UserRole.ADMIN &&
existingUser.id === profile.userId)
) ||
(await tx.staffProfile.count({
where: { userId: existingUser.id, id: { not: profile.id } },
})))
@@ -205,7 +209,9 @@ export function assertStaffAccess(
throw new UserFacingError("Kein Zugriff auf dieses Personalprofil.");
if (adminOnly) requireAdmin(actor);
else if (actor.role !== UserRole.ADMIN && profile.userId !== actor.id)
throw new UserFacingError("Du kannst nur dein eigenes Personalprofil verwalten.");
throw new UserFacingError(
"Du kannst nur dein eigenes Personalprofil verwalten.",
);
}
export async function getStaffProfile(actor: StaffActor, profileId?: string) {
@@ -320,8 +326,8 @@ export async function executeStaffCommand(
kitaId: actor.kitaId,
role: actor.role,
privacyPolicyAcceptedAt: { not: null },
emailVerifiedAt: { not: null },
disabledAt: null,
emailVerifiedAt: { not: null },
disabledAt: null,
},
});
if (
@@ -419,7 +425,9 @@ export async function executeStaffCommand(
if (
profile.user &&
(profile.user.kitaId !== actor.kitaId ||
profile.user.role !== UserRole.ERZIEHER)
!([UserRole.ERZIEHER, UserRole.ADMIN] as UserRole[]).includes(
profile.user.role,
))
)
throw new UserFacingError(
"Der verknüpfte Account gehört nicht zum Fachpersonal dieser Kita.",
@@ -440,7 +448,11 @@ export async function executeStaffCommand(
return { ...invitation, educatorId: educator.id };
}
if (command === "emailPreference") {
if (actor.role !== UserRole.ERZIEHER)
if (
!(await tx.staffProfile.count({
where: { userId: actor.id, kitaId: actor.kitaId },
}))
)
throw new UserFacingError(
"Diese Einstellung ist für Fachkräfte vorgesehen.",
);
@@ -534,7 +546,9 @@ export async function executeStaffCommand(
input.employmentEnd &&
input.employmentEnd < input.employmentStart
)
throw new UserFacingError("Das Beschäftigungsende liegt vor dem Beginn.");
throw new UserFacingError(
"Das Beschäftigungsende liegt vor dem Beginn.",
);
if (
input.trackingStart < input.employmentStart ||
(input.employmentEnd && input.trackingStart > input.employmentEnd)
@@ -545,7 +559,8 @@ export async function executeStaffCommand(
const educator = await tx.educator.findFirst({
where: { id: input.educatorId, kitaId: actor.kitaId },
});
if (!educator) throw new UserFacingError("Fachkraft nicht gefunden.");
if (!educator)
throw new UserFacingError("Fachkraft nicht gefunden.");
const existing = await tx.staffProfile.findFirst({
where: { educatorId: educator.id, kitaId: actor.kitaId },
include: profileInclude,
@@ -591,7 +606,8 @@ export async function executeStaffCommand(
where: { id: profileId, kitaId: actor.kitaId },
include: profileInclude,
});
if (!profile) throw new UserFacingError("Personalprofil nicht gefunden.");
if (!profile)
throw new UserFacingError("Personalprofil nicht gefunden.");
assertStaffAccess(actor, profile);
const holidays = new Set(
(
@@ -631,7 +647,10 @@ export async function executeStaffCommand(
for (const user of recipients) {
if (
!toAdmins &&
(user.role !== UserRole.ERZIEHER || !user.staffDecisionEmails)
(!(
[UserRole.ERZIEHER, UserRole.ADMIN] as UserRole[]
).includes(user.role) ||
!user.staffDecisionEmails)
)
continue;
const job = await tx.staffEmail.create({
@@ -719,7 +738,9 @@ export async function executeStaffCommand(
input.employmentEnd &&
input.employmentEnd < dateKey(profile.employmentStart)
)
throw new UserFacingError("Das Beschäftigungsende liegt vor dem Beginn.");
throw new UserFacingError(
"Das Beschäftigungsende liegt vor dem Beginn.",
);
if (
input.employmentEnd &&
(input.employmentEnd < dateKey(profile.trackingStart) ||
@@ -786,7 +807,9 @@ export async function executeStaffCommand(
(profile.employmentEnd &&
input.date > dateKey(profile.employmentEnd))
)
throw new UserFacingError("Der Tag liegt außerhalb der Beschäftigung.");
throw new UserFacingError(
"Der Tag liegt außerhalb der Beschäftigung.",
);
if (input.remove)
await tx.staffDayOverride.deleteMany({
where: { profileId, date: dateOnly(input.date) },
@@ -859,7 +882,9 @@ export async function executeStaffCommand(
})
.parse(data);
if (input.date < dateKey(profile.trackingStart))
throw new UserFacingError("Korrekturen müssen ab Erfassungsbeginn liegen.");
throw new UserFacingError(
"Korrekturen müssen ab Erfassungsbeginn liegen.",
);
await tx.staffAdjustment.create({
data: {
profileId,
@@ -887,7 +912,8 @@ export async function executeStaffCommand(
throw new UserFacingError("Zeiteintrag nicht gefunden.");
if (existing) ensureDayEditable(profile, dateKey(existing.date));
if (command === "deleteTime") {
if (!existing) throw new UserFacingError("Zeiteintrag nicht gefunden.");
if (!existing)
throw new UserFacingError("Zeiteintrag nicht gefunden.");
await tx.staffTimeEntry.delete({ where: { id: existing.id } });
await history(
"Arbeitszeit gelöscht",
@@ -1135,7 +1161,8 @@ export async function executeStaffCommand(
const existing = data.id
? profile.leaves.find((l) => l.id === idSchema.parse(data.id))
: undefined;
if (data.id && !existing) throw new UserFacingError("Antrag nicht gefunden.");
if (data.id && !existing)
throw new UserFacingError("Antrag nicht gefunden.");
if (command === "leaveCancel") {
if (
!existing ||
@@ -1182,7 +1209,9 @@ export async function executeStaffCommand(
!existing ||
!["PENDING", "CANCEL_REQUESTED"].includes(existing.status)
)
throw new UserFacingError("Dieser Antrag ist nicht mehr offen.");
throw new UserFacingError(
"Dieser Antrag ist nicht mehr offen.",
);
const cancelling = existing.status === "CANCEL_REQUESTED";
if (input.approve)
ensureRangeEditable(
@@ -1284,7 +1313,9 @@ export async function executeStaffCommand(
.parse(data);
if (input.kind === "SICKNESS") requireAdmin(actor);
if (existing && existing.status !== "PENDING")
throw new UserFacingError("Nur offene Anträge können bearbeitet werden.");
throw new UserFacingError(
"Nur offene Anträge können bearbeitet werden.",
);
if (input.startDate < dateKey(profile.trackingStart))
throw new UserFacingError(
"Abwesenheiten bitte ab Erfassungsbeginn erfassen.",