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
+114 -91
View File
@@ -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.",
),
};
}
}