feat: show planning availability and manage Kita access roles
This commit is contained in:
@@ -11,6 +11,10 @@ import {
|
||||
parseDateInput,
|
||||
} from "@/lib/child-birthdate";
|
||||
import { serializableTransaction } from "@/lib/transaction";
|
||||
import {
|
||||
assertKitaAdminRemains,
|
||||
lockKitaAccounts,
|
||||
} from "@/lib/kita-admin-guard";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { assertChildrenRemovable } from "@/lib/notdienst-planning";
|
||||
import { UserFacingError, publicActionError } from "@/lib/action-error";
|
||||
@@ -59,7 +63,11 @@ const childSchema = z
|
||||
});
|
||||
|
||||
const familySchema = z.object({
|
||||
familyName: z.string().min(1, "Familienname ist erforderlich.").max(120).trim(),
|
||||
familyName: z
|
||||
.string()
|
||||
.min(1, "Familienname ist erforderlich.")
|
||||
.max(120)
|
||||
.trim(),
|
||||
});
|
||||
|
||||
const contactSchema = z.object({
|
||||
@@ -88,10 +96,7 @@ const contactSchema = z.object({
|
||||
|
||||
// Gibt die erste konkrete Field-Error-Message aus einem Zod-Fehler zurück,
|
||||
// damit der User nicht nur "Ungültige Eingabedaten" sieht.
|
||||
function firstZodMessage(
|
||||
error: z.ZodError,
|
||||
fallback: string,
|
||||
): string {
|
||||
function firstZodMessage(error: z.ZodError, fallback: string): string {
|
||||
const issue = error.issues[0];
|
||||
return issue?.message || fallback;
|
||||
}
|
||||
@@ -121,8 +126,13 @@ export async function createMyChild(rawPayload: unknown) {
|
||||
}
|
||||
|
||||
try {
|
||||
await serializableTransaction(async tx => {
|
||||
const count = await tx.child.count({ where: { kitaId: session.user.kitaId, familyId: session.user.familyId! } });
|
||||
await serializableTransaction(async (tx) => {
|
||||
const count = await tx.child.count({
|
||||
where: {
|
||||
kitaId: session.user.kitaId,
|
||||
familyId: session.user.familyId!,
|
||||
},
|
||||
});
|
||||
if (count >= 20) throw new Error("Household child limit reached");
|
||||
await tx.child.create({
|
||||
data: {
|
||||
@@ -166,10 +176,17 @@ export async function createMyFamily(rawPayload: unknown) {
|
||||
});
|
||||
|
||||
const attached = await tx.user.updateMany({
|
||||
where: { id: session.user.id, kitaId: session.user.kitaId, familyId: null, disabledAt: null, sessionVersion: session.user.sessionVersion },
|
||||
where: {
|
||||
id: session.user.id,
|
||||
kitaId: session.user.kitaId,
|
||||
familyId: null,
|
||||
disabledAt: null,
|
||||
sessionVersion: session.user.sessionVersion,
|
||||
},
|
||||
data: { familyId: family.id },
|
||||
});
|
||||
if (attached.count !== 1) throw new Error("Account already assigned or revoked");
|
||||
if (attached.count !== 1)
|
||||
throw new Error("Account already assigned or revoked");
|
||||
});
|
||||
|
||||
revalidatePath("/dashboard/profil");
|
||||
@@ -178,7 +195,12 @@ export async function createMyFamily(rawPayload: unknown) {
|
||||
revalidatePath("/dashboard");
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
if (!(error instanceof Error && error.message === "Account already assigned or revoked")) {
|
||||
if (
|
||||
!(
|
||||
error instanceof Error &&
|
||||
error.message === "Account already assigned or revoked"
|
||||
)
|
||||
) {
|
||||
console.error("Fehler beim Anlegen des eigenen Haushalts:", error);
|
||||
}
|
||||
return { error: "Der Haushalt konnte nicht angelegt werden." };
|
||||
@@ -249,7 +271,11 @@ export async function updateMyChild(childId: string, rawPayload: unknown) {
|
||||
}
|
||||
|
||||
await prisma.child.update({
|
||||
where: { id: childId, kitaId: session.user.kitaId, familyId: session.user.familyId! },
|
||||
where: {
|
||||
id: childId,
|
||||
kitaId: session.user.kitaId,
|
||||
familyId: session.user.familyId!,
|
||||
},
|
||||
data: {
|
||||
firstName: parsed.data.firstName,
|
||||
lastName: parsed.data.lastName,
|
||||
@@ -271,15 +297,34 @@ export async function deleteMyChild(childId: string) {
|
||||
|
||||
try {
|
||||
await serializableTransaction(async (tx) => {
|
||||
if (!session.user.familyId) throw new UserFacingError("Dieses Kind gehört nicht zu deinem Haushalt.");
|
||||
if (!session.user.familyId)
|
||||
throw new UserFacingError(
|
||||
"Dieses Kind gehört nicht zu deinem Haushalt.",
|
||||
);
|
||||
const child = await tx.child.findFirst({
|
||||
where: { id: childId, kitaId: session.user.kitaId, familyId: session.user.familyId },
|
||||
where: {
|
||||
id: childId,
|
||||
kitaId: session.user.kitaId,
|
||||
familyId: session.user.familyId,
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
if (!child) throw new UserFacingError("Dieses Kind gehört nicht zu deinem Haushalt.");
|
||||
await assertChildrenRemovable(tx, session.user.kitaId, session.user.familyId, [childId]);
|
||||
if (!child)
|
||||
throw new UserFacingError(
|
||||
"Dieses Kind gehört nicht zu deinem Haushalt.",
|
||||
);
|
||||
await assertChildrenRemovable(
|
||||
tx,
|
||||
session.user.kitaId,
|
||||
session.user.familyId,
|
||||
[childId],
|
||||
);
|
||||
await tx.child.delete({
|
||||
where: { id: childId, kitaId: session.user.kitaId, familyId: session.user.familyId },
|
||||
where: {
|
||||
id: childId,
|
||||
kitaId: session.user.kitaId,
|
||||
familyId: session.user.familyId,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -287,89 +332,67 @@ export async function deleteMyChild(childId: string) {
|
||||
revalidatePath("/dashboard/families");
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
if (!(error instanceof UserFacingError)) console.error("Fehler beim Entfernen des Kindes:", error);
|
||||
return { error: publicActionError(error, "Das Kind konnte nicht entfernt werden.") };
|
||||
if (!(error instanceof UserFacingError))
|
||||
console.error("Fehler beim Entfernen des Kindes:", error);
|
||||
return {
|
||||
error: publicActionError(error, "Das Kind konnte nicht entfernt werden."),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteMyAccount() {
|
||||
export async function deleteMyAccount(): Promise<{
|
||||
success?: boolean;
|
||||
error?: string;
|
||||
}> {
|
||||
const session = await requireKitaSession();
|
||||
|
||||
try {
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
id: session.user.id,
|
||||
kitaId: session.user.kitaId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
familyId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return { error: "Account wurde nicht gefunden." };
|
||||
}
|
||||
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
const activeNotdienstAssignments = user.familyId
|
||||
? await prisma.notdienstAssignment.count({
|
||||
where: {
|
||||
kitaId: session.user.kitaId,
|
||||
date: { gte: today },
|
||||
plan: { status: NotdienstPlanStatus.PUBLISHED },
|
||||
child: { familyId: user.familyId },
|
||||
},
|
||||
})
|
||||
: 0;
|
||||
|
||||
if (activeNotdienstAssignments > 0) {
|
||||
return {
|
||||
error:
|
||||
return await serializableTransaction(async (tx) => {
|
||||
await lockKitaAccounts(tx, session.user.kitaId);
|
||||
const user = await tx.user.findFirst({
|
||||
where: { id: session.user.id, kitaId: session.user.kitaId },
|
||||
select: { id: true, familyId: true },
|
||||
});
|
||||
if (!user) throw new UserFacingError("Account wurde nicht gefunden.");
|
||||
await assertKitaAdminRemains(tx, session.user.kitaId, [user.id]);
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const assignments = user.familyId
|
||||
? await tx.notdienstAssignment.count({
|
||||
where: {
|
||||
kitaId: session.user.kitaId,
|
||||
date: { gte: today },
|
||||
plan: { status: NotdienstPlanStatus.PUBLISHED },
|
||||
child: { familyId: user.familyId },
|
||||
},
|
||||
})
|
||||
: 0;
|
||||
if (assignments)
|
||||
throw new UserFacingError(
|
||||
"Dein Account kann nicht gelöscht werden, solange aktive Notdienst-Verpflichtungen bestehen.",
|
||||
};
|
||||
}
|
||||
|
||||
if (!user.familyId) {
|
||||
await prisma.user.delete({
|
||||
where: { id: user.id },
|
||||
});
|
||||
);
|
||||
if (user.familyId) {
|
||||
const family = await tx.family.findFirst({
|
||||
where: { id: user.familyId, kitaId: session.user.kitaId },
|
||||
select: { id: true, users: { select: { id: true } } },
|
||||
});
|
||||
if (!family)
|
||||
throw new UserFacingError("Haushalt wurde nicht gefunden.");
|
||||
if (family.users.length <= 1) {
|
||||
await tx.family.delete({ where: { id: family.id } });
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
await tx.user.delete({ where: { id: user.id } });
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
const family = await prisma.family.findFirst({
|
||||
where: {
|
||||
id: user.familyId,
|
||||
kitaId: session.user.kitaId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
users: {
|
||||
select: { id: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!family) {
|
||||
return { error: "Haushalt wurde nicht gefunden." };
|
||||
}
|
||||
|
||||
if (family.users.length <= 1) {
|
||||
await prisma.family.delete({
|
||||
where: { id: family.id },
|
||||
});
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
await prisma.user.delete({
|
||||
where: { id: user.id },
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("Fehler beim Löschen des Accounts:", error);
|
||||
return { error: "Ein Fehler ist beim Löschen aufgetreten." };
|
||||
if (!(error instanceof UserFacingError))
|
||||
console.error("Fehler beim Löschen des Accounts:", error);
|
||||
return {
|
||||
error: publicActionError(
|
||||
error,
|
||||
"Ein Fehler ist beim Löschen aufgetreten.",
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user