feat: show planning availability and manage Kita access roles
This commit is contained in:
+18
-3
@@ -39,14 +39,29 @@ export async function deliverAlarmEmails(
|
||||
include: { user: true, alarm: { include: { kita: true } } },
|
||||
});
|
||||
const { user, alarm } = job;
|
||||
let eligible = user.kitaId === alarm.kitaId && !user.disabledAt && !!user.emailVerifiedAt && !!user.privacyPolicyAcceptedAt && (!user.lockedUntil || user.lockedUntil <= now);
|
||||
let eligible =
|
||||
user.kitaId === alarm.kitaId &&
|
||||
!user.disabledAt &&
|
||||
!!user.emailVerifiedAt &&
|
||||
!!user.privacyPolicyAcceptedAt &&
|
||||
(!user.lockedUntil || user.lockedUntil <= now);
|
||||
if (job.kind === "MANAGER") eligible &&= managerRoles.includes(user.role);
|
||||
else
|
||||
eligible &&= user.role === "ELTERN" && user.familyId === alarm.familyId;
|
||||
eligible &&=
|
||||
["ELTERN", "ADMIN", "KOORDINATOR"].includes(user.role) &&
|
||||
user.familyId === alarm.familyId;
|
||||
if (job.kind === "REQUEST") {
|
||||
const token = job.path.split("/").pop();
|
||||
const recipient = await prisma.notdienstAlarmRecipient.findFirst({
|
||||
where: { alarmId: alarm.id, userId: user.id, sessionVersion: user.sessionVersion, OR: [{ token: hashToken(token ?? "") }, ...(token?.length === 36 ? [{ token }] : [])] },
|
||||
where: {
|
||||
alarmId: alarm.id,
|
||||
userId: user.id,
|
||||
sessionVersion: user.sessionVersion,
|
||||
OR: [
|
||||
{ token: hashToken(token ?? "") },
|
||||
...(token?.length === 36 ? [{ token }] : []),
|
||||
],
|
||||
},
|
||||
});
|
||||
eligible &&=
|
||||
!!recipient && alarm.status === "PENDING" && alarm.expiresAt > now;
|
||||
|
||||
+52
-16
@@ -114,22 +114,30 @@ async function queue(
|
||||
const users =
|
||||
audience === "managers"
|
||||
? await tx.user.findMany({
|
||||
where: { kitaId: alarm.kitaId, role: { in: managerRoles }, emailVerifiedAt: { not: null }, disabledAt: null, privacyPolicyAcceptedAt: { not: null } },
|
||||
where: {
|
||||
kitaId: alarm.kitaId,
|
||||
role: { in: managerRoles },
|
||||
emailVerifiedAt: { not: null },
|
||||
disabledAt: null,
|
||||
privacyPolicyAcceptedAt: { not: null },
|
||||
},
|
||||
})
|
||||
: await tx.user.findMany({
|
||||
where: {
|
||||
kitaId: alarm.kitaId,
|
||||
familyId: alarm.familyId,
|
||||
role: "ELTERN",
|
||||
emailVerifiedAt: { not: null }, disabledAt: null, privacyPolicyAcceptedAt: { not: null },
|
||||
role: { in: ["ELTERN", "ADMIN", "KOORDINATOR"] },
|
||||
emailVerifiedAt: { not: null },
|
||||
disabledAt: null,
|
||||
privacyPolicyAcceptedAt: { not: null },
|
||||
alarmRecipients: { some: { alarmId: alarm.id } },
|
||||
},
|
||||
});
|
||||
const jobs: string[] = [];
|
||||
for (const user of users) {
|
||||
const recipient =
|
||||
kind === "REQUEST" ? requestTokens?.get(user.id) : null;
|
||||
if (kind === "REQUEST" && !recipient) throw new Error("Request token missing");
|
||||
const recipient = kind === "REQUEST" ? requestTokens?.get(user.id) : null;
|
||||
if (kind === "REQUEST" && !recipient)
|
||||
throw new Error("Request token missing");
|
||||
await tx.notdienstAlarmNotification.create({
|
||||
data: { alarmId: alarm.id, userId: user.id, eventKey, message },
|
||||
});
|
||||
@@ -204,7 +212,15 @@ export async function executeAlarmCommand(
|
||||
include: {
|
||||
family: {
|
||||
include: {
|
||||
users: { where: { role: "ELTERN", kitaId: actor.kitaId, emailVerifiedAt: { not: null }, disabledAt: null, privacyPolicyAcceptedAt: { not: null } } },
|
||||
users: {
|
||||
where: {
|
||||
role: { in: ["ELTERN", "ADMIN", "KOORDINATOR"] },
|
||||
kitaId: actor.kitaId,
|
||||
emailVerifiedAt: { not: null },
|
||||
disabledAt: null,
|
||||
privacyPolicyAcceptedAt: { not: null },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -221,10 +237,14 @@ export async function executeAlarmCommand(
|
||||
if (!assignment.kita.notdienstModuleEnabled)
|
||||
throw new UserFacingError("Notdienst ist für diese Kita deaktiviert.");
|
||||
if (assignment.plan.status !== "PUBLISHED")
|
||||
throw new UserFacingError("Bitte den Notdienstplan zuerst veröffentlichen.");
|
||||
throw new UserFacingError(
|
||||
"Bitte den Notdienstplan zuerst veröffentlichen.",
|
||||
);
|
||||
const date = dateKey(assignment.date);
|
||||
if (date < todayKey(now))
|
||||
throw new UserFacingError("Vergangene Notdienste können nicht alarmiert werden.");
|
||||
throw new UserFacingError(
|
||||
"Vergangene Notdienste können nicht alarmiert werden.",
|
||||
);
|
||||
const expiresAt = endOfAlarm(date, input.wholeDay, input.endMinute);
|
||||
if (expiresAt <= now)
|
||||
throw new UserFacingError("Dieser Einsatz ist bereits beendet.");
|
||||
@@ -245,7 +265,9 @@ export async function executeAlarmCommand(
|
||||
"Für diese Einteilung besteht bereits ein aktiver Alarm.",
|
||||
);
|
||||
const parents = assignment.child.family.users;
|
||||
const requestTokens = new Map(parents.map(parent => [parent.id, newToken()]));
|
||||
const requestTokens = new Map(
|
||||
parents.map((parent) => [parent.id, newToken()]),
|
||||
);
|
||||
if (!parents.length)
|
||||
throw new UserFacingError(
|
||||
"Für diesen Haushalt sind keine Elternkonten hinterlegt.",
|
||||
@@ -353,7 +375,9 @@ export async function executeAlarmCommand(
|
||||
);
|
||||
const reason = note.parse(data.reason);
|
||||
if (command === "approveLate" && alarm.status !== "LATE_CONFIRMATION")
|
||||
throw new UserFacingError("Es liegt keine verspätete Zusage zur Freigabe vor.");
|
||||
throw new UserFacingError(
|
||||
"Es liegt keine verspätete Zusage zur Freigabe vor.",
|
||||
);
|
||||
const approve =
|
||||
command === "approveLate" || z.boolean().parse(data.approve);
|
||||
const updated = await tx.notdienstAlarm.update({
|
||||
@@ -401,7 +425,12 @@ export async function executeAlarmCommand(
|
||||
|
||||
async function recipientForToken(tx: Tx, token: string) {
|
||||
const recipient = await tx.notdienstAlarmRecipient.findFirst({
|
||||
where: { OR: [{ token: hashToken(token) }, ...(token.length === 36 ? [{ token }] : [])] },
|
||||
where: {
|
||||
OR: [
|
||||
{ token: hashToken(token) },
|
||||
...(token.length === 36 ? [{ token }] : []),
|
||||
],
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
alarm: { include: { kita: { select: { name: true } } } },
|
||||
@@ -409,9 +438,11 @@ async function recipientForToken(tx: Tx, token: string) {
|
||||
});
|
||||
if (
|
||||
!recipient ||
|
||||
recipient.user.role !== "ELTERN" ||
|
||||
!["ELTERN", "ADMIN", "KOORDINATOR"].includes(recipient.user.role) ||
|
||||
recipient.sessionVersion !== recipient.user.sessionVersion ||
|
||||
!!recipient.user.disabledAt || !recipient.user.emailVerifiedAt || !recipient.user.privacyPolicyAcceptedAt ||
|
||||
!!recipient.user.disabledAt ||
|
||||
!recipient.user.emailVerifiedAt ||
|
||||
!recipient.user.privacyPolicyAcceptedAt ||
|
||||
recipient.alarm.expiresAt <= new Date() ||
|
||||
(recipient.user.lockedUntil && recipient.user.lockedUntil > new Date()) ||
|
||||
recipient.user.kitaId !== recipient.alarm.kitaId ||
|
||||
@@ -423,7 +454,12 @@ async function recipientForToken(tx: Tx, token: string) {
|
||||
export async function getPublicAlarm(token: string) {
|
||||
if (!tokenSchema.safeParse(token).success) return null;
|
||||
const legacy = await prisma.notdienstAlert.findFirst({
|
||||
where: { OR: [{ confirmationToken: hashToken(token) }, ...(token.length === 36 ? [{ confirmationToken: token }] : [])] },
|
||||
where: {
|
||||
OR: [
|
||||
{ confirmationToken: hashToken(token) },
|
||||
...(token.length === 36 ? [{ confirmationToken: token }] : []),
|
||||
],
|
||||
},
|
||||
select: { alarmId: true, assignmentId: true },
|
||||
});
|
||||
if (legacy && !legacy.alarmId)
|
||||
@@ -591,7 +627,7 @@ export async function importLegacyAlarms(
|
||||
const old = assignment.alerts;
|
||||
const valid = old.filter(
|
||||
(a) =>
|
||||
a.parentUser.role === "ELTERN" &&
|
||||
["ELTERN", "ADMIN", "KOORDINATOR"].includes(a.parentUser.role) &&
|
||||
a.parentUser.familyId === assignment.child.familyId &&
|
||||
a.parentUser.kitaId === assignment.kitaId,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import { UserFacingError } from "@/lib/action-error";
|
||||
|
||||
// Serialize role changes, account deletion and support locks for the same Kita.
|
||||
export async function lockKitaAccounts(
|
||||
tx: Prisma.TransactionClient,
|
||||
kitaId: string,
|
||||
) {
|
||||
const rows = await tx.$queryRaw<
|
||||
{ id: string }[]
|
||||
>`SELECT id FROM kitas WHERE id = ${kitaId} FOR UPDATE`;
|
||||
if (!rows.length) throw new UserFacingError("Kita wurde nicht gefunden.");
|
||||
}
|
||||
|
||||
export async function assertKitaAdminRemains(
|
||||
tx: Prisma.TransactionClient,
|
||||
kitaId: string,
|
||||
excludedUserIds: string[],
|
||||
) {
|
||||
const removesAdmin = await tx.user.count({
|
||||
where: { kitaId, id: { in: excludedUserIds }, role: "ADMIN" },
|
||||
});
|
||||
if (!removesAdmin) return;
|
||||
const now = new Date();
|
||||
const remaining = await tx.user.count({
|
||||
where: {
|
||||
kitaId,
|
||||
id: { notIn: excludedUserIds },
|
||||
role: "ADMIN",
|
||||
disabledAt: null,
|
||||
emailVerifiedAt: { not: null },
|
||||
privacyPolicyAcceptedAt: { not: null },
|
||||
passwordHash: { not: "" },
|
||||
OR: [{ lockedUntil: null }, { lockedUntil: { lte: now } }],
|
||||
},
|
||||
});
|
||||
if (!remaining)
|
||||
throw new UserFacingError(
|
||||
"Mindestens ein aktiver Kita-Admin muss erhalten bleiben. Ernenne zuerst eine weitere Person mit aktiviertem Zugang und Datenschutzzustimmung.",
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import "server-only";
|
||||
import { AuditTargetType } from "@prisma/client";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { serializableTransaction } from "@/lib/transaction";
|
||||
import { UserFacingError } from "@/lib/action-error";
|
||||
import {
|
||||
assertKitaAdminRemains,
|
||||
lockKitaAccounts,
|
||||
} from "@/lib/kita-admin-guard";
|
||||
import { availableKitaRoles } from "@/lib/kita-roles";
|
||||
|
||||
const inputSchema = z.object({
|
||||
userId: z.string().min(1).max(64),
|
||||
role: z.enum(["ADMIN", "KOORDINATOR", "ELTERN", "ERZIEHER"]),
|
||||
expectedRole: z.enum(["ADMIN", "KOORDINATOR", "ELTERN", "ERZIEHER"]),
|
||||
});
|
||||
|
||||
type Actor = { id: string; kitaId: string; sessionVersion: number };
|
||||
export async function changeKitaRole(actor: Actor, raw: unknown) {
|
||||
const input = inputSchema.safeParse(raw);
|
||||
if (!input.success) throw new UserFacingError("Ungültige Rollenänderung.");
|
||||
return serializableTransaction(async (tx) => {
|
||||
await lockKitaAccounts(tx, actor.kitaId);
|
||||
const currentActor = await tx.user.findFirst({
|
||||
where: {
|
||||
id: actor.id,
|
||||
kitaId: actor.kitaId,
|
||||
role: "ADMIN",
|
||||
sessionVersion: actor.sessionVersion,
|
||||
disabledAt: null,
|
||||
emailVerifiedAt: { not: null },
|
||||
privacyPolicyAcceptedAt: { not: null },
|
||||
OR: [{ lockedUntil: null }, { lockedUntil: { lte: new Date() } }],
|
||||
},
|
||||
select: { id: true, email: true },
|
||||
});
|
||||
if (!currentActor)
|
||||
throw new UserFacingError(
|
||||
"Du darfst die Zugriffsrechte nicht mehr ändern. Bitte lade die Seite neu.",
|
||||
);
|
||||
const person = await tx.user.findFirst({
|
||||
where: { id: input.data.userId, kitaId: actor.kitaId },
|
||||
select: {
|
||||
id: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
email: true,
|
||||
role: true,
|
||||
familyId: true,
|
||||
staffProfile: { select: { id: true, kitaId: true } },
|
||||
},
|
||||
});
|
||||
if (!person || person.role === "SUPERADMIN")
|
||||
throw new UserFacingError(
|
||||
"Dieses Kita-Konto kann hier nicht geändert werden.",
|
||||
);
|
||||
if (person.role !== input.data.expectedRole)
|
||||
throw new UserFacingError(
|
||||
"Die Rolle wurde inzwischen geändert. Bitte lade die Seite neu.",
|
||||
);
|
||||
if (person.staffProfile && person.staffProfile.kitaId !== actor.kitaId)
|
||||
throw new UserFacingError(
|
||||
"Das Personalprofil gehört nicht zu dieser Kita.",
|
||||
);
|
||||
if (
|
||||
!availableKitaRoles({
|
||||
familyId: person.familyId,
|
||||
hasStaffProfile: !!person.staffProfile,
|
||||
}).includes(input.data.role)
|
||||
)
|
||||
throw new UserFacingError(
|
||||
person.staffProfile
|
||||
? "Ein Personalprofil benötigt die Rolle Fachkraft oder Kita-Admin."
|
||||
: "Eltern benötigen einen Haushalt; die Fachkraftrolle benötigt ein Personalprofil.",
|
||||
);
|
||||
if (person.role === input.data.role)
|
||||
return { changed: false, ownRoleChanged: false };
|
||||
if (person.role === "ADMIN")
|
||||
await assertKitaAdminRemains(tx, actor.kitaId, [person.id]);
|
||||
await tx.user.update({
|
||||
where: { id: person.id, kitaId: actor.kitaId },
|
||||
data: { role: input.data.role },
|
||||
});
|
||||
await tx.auditLog.create({
|
||||
data: {
|
||||
actorUserId: currentActor.id,
|
||||
actorEmail: currentActor.email,
|
||||
action: "USER_ROLE_CHANGE",
|
||||
targetType: AuditTargetType.USER,
|
||||
targetId: person.id,
|
||||
targetLabel: `${person.firstName} ${person.lastName}`,
|
||||
targetKitaId: actor.kitaId,
|
||||
metadata: { previousRole: person.role, newRole: input.data.role },
|
||||
},
|
||||
});
|
||||
// Auth reloads the role from the database on every request. Keeping sessionVersion
|
||||
// preserves already-issued household alarm links and existing login sessions.
|
||||
return { changed: true, ownRoleChanged: person.id === actor.id };
|
||||
});
|
||||
}
|
||||
|
||||
export async function getKitaRoleHistory(kitaId: string) {
|
||||
return prisma.auditLog.findMany({
|
||||
where: { targetKitaId: kitaId, action: "USER_ROLE_CHANGE" },
|
||||
orderBy: { createdAt: "desc" },
|
||||
take: 15,
|
||||
select: {
|
||||
id: true,
|
||||
targetLabel: true,
|
||||
actorEmail: true,
|
||||
metadata: true,
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { UserRole } from "@prisma/client";
|
||||
|
||||
export const kitaRoleLabels: Record<UserRole, string> = {
|
||||
ADMIN: "Kita-Admin",
|
||||
KOORDINATOR: "Notdienst-Koordination",
|
||||
ELTERN: "Eltern",
|
||||
ERZIEHER: "Fachkraft",
|
||||
SUPERADMIN: "Systemadmin",
|
||||
};
|
||||
|
||||
export function availableKitaRoles(person: {
|
||||
familyId: string | null;
|
||||
hasStaffProfile: boolean;
|
||||
}): UserRole[] {
|
||||
if (person.hasStaffProfile) return ["ERZIEHER", "ADMIN"];
|
||||
return person.familyId
|
||||
? ["ELTERN", "KOORDINATOR", "ADMIN"]
|
||||
: ["KOORDINATOR", "ADMIN"];
|
||||
}
|
||||
@@ -40,25 +40,29 @@ export async function deliverStaffEmails(ids: string[], send = sendAppEmail) {
|
||||
},
|
||||
});
|
||||
const { user, profile } = job;
|
||||
let eligible = user.kitaId === profile.kitaId && !user.disabledAt && (!user.lockedUntil || user.lockedUntil <= now);
|
||||
let eligible =
|
||||
user.kitaId === profile.kitaId &&
|
||||
!user.disabledAt &&
|
||||
(!user.lockedUntil || user.lockedUntil <= now);
|
||||
if (job.inviteToken) {
|
||||
const token = await prisma.verificationToken.findUnique({
|
||||
where: { token: hashToken(job.inviteToken) },
|
||||
});
|
||||
eligible &&=
|
||||
profile.userId === user.id &&
|
||||
user.role === "ERZIEHER" &&
|
||||
["ERZIEHER", "ADMIN"].includes(user.role) &&
|
||||
!user.emailVerifiedAt &&
|
||||
!!token &&
|
||||
token.identifier === user.id &&
|
||||
token.expires > now;
|
||||
} else {
|
||||
eligible &&=
|
||||
!!user.emailVerifiedAt && !!user.privacyPolicyAcceptedAt &&
|
||||
!!user.emailVerifiedAt &&
|
||||
!!user.privacyPolicyAcceptedAt &&
|
||||
(!user.lockedUntil || user.lockedUntil <= now) &&
|
||||
(job.toAdmins
|
||||
? user.role === "ADMIN"
|
||||
: user.role === "ERZIEHER" &&
|
||||
: ["ERZIEHER", "ADMIN"].includes(user.role) &&
|
||||
profile.userId === user.id &&
|
||||
user.staffDecisionEmails);
|
||||
}
|
||||
|
||||
+48
-17
@@ -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.",
|
||||
|
||||
Reference in New Issue
Block a user