import "server-only"; import { AuditTargetType, InvitationStatus, UserRole } from "@prisma/client"; import { prisma } from "@/lib/prisma"; const roleOrder: UserRole[] = [ UserRole.ADMIN, UserRole.KOORDINATOR, UserRole.ERZIEHER, UserRole.ELTERN, ]; export type KitaOverviewRow = { id: string; name: string; slug: string; createdAt: Date; updatedAt: Date; modules: { notdienst: boolean; termine: boolean; adressbuch: boolean; }; counts: { users: number; families: number; activeChildren: number; termine: number; absences: number; announcements: number; openInvitations: number; lockedUsers: number; missingConsent: number; familiesWithoutUsers: number; }; lastLoginAt: Date | null; roleCounts: Record; }; export type GlobalAdminHealth = { orphanTenantUsers: number; lockedTenantUsers: number; openInvitations: number; missingConsent: number; kitas: number; }; export async function getGlobalAdminHealth(): Promise { const now = new Date(); const [ orphanTenantUsers, lockedTenantUsers, openInvitations, missingConsent, kitas, ] = await Promise.all([ prisma.user.count({ where: { kitaId: null, role: { not: UserRole.SUPERADMIN }, }, }), prisma.user.count({ where: { role: { not: UserRole.SUPERADMIN }, lockedUntil: { gt: now }, }, }), prisma.invitation.count({ where: { status: InvitationStatus.PENDING, expiresAt: { gt: now }, }, }), prisma.user.count({ where: { role: { not: UserRole.SUPERADMIN }, privacyPolicyAcceptedAt: null, }, }), prisma.kita.count(), ]); return { orphanTenantUsers, lockedTenantUsers, openInvitations, missingConsent, kitas, }; } export async function getKitaOverviewRows(): Promise { const now = new Date(); const kitas = await prisma.kita.findMany({ select: { id: true, name: true, slug: true, notdienstModuleEnabled: true, terminModuleEnabled: true, adressbuchModuleEnabled: true, createdAt: true, updatedAt: true, users: { select: { role: true, lastLoginAt: true, lockedUntil: true, privacyPolicyAcceptedAt: true, }, }, families: { select: { users: { select: { id: true }, }, }, }, children: { select: { active: true }, }, invitations: { select: { status: true, expiresAt: true, }, }, _count: { select: { termine: true, absences: true, announcements: true, }, }, }, orderBy: { name: "asc" }, }); return kitas.map((kita) => { const roleCounts = Object.fromEntries( Object.values(UserRole).map((role) => [role, 0]), ) as Record; let lastLoginAt: Date | null = null; let lockedUsers = 0; let missingConsent = 0; for (const user of kita.users) { roleCounts[user.role] += 1; if (user.lastLoginAt && (!lastLoginAt || user.lastLoginAt > lastLoginAt)) { lastLoginAt = user.lastLoginAt; } if (user.lockedUntil && user.lockedUntil > now) { lockedUsers += 1; } if (user.role !== UserRole.SUPERADMIN && !user.privacyPolicyAcceptedAt) { missingConsent += 1; } } return { id: kita.id, name: kita.name, slug: kita.slug, createdAt: kita.createdAt, updatedAt: kita.updatedAt, modules: { notdienst: kita.notdienstModuleEnabled, termine: kita.terminModuleEnabled, adressbuch: kita.adressbuchModuleEnabled, }, counts: { users: kita.users.length, families: kita.families.length, activeChildren: kita.children.filter((child) => child.active).length, termine: kita._count.termine, absences: kita._count.absences, announcements: kita._count.announcements, openInvitations: kita.invitations.filter( (invitation) => invitation.status === InvitationStatus.PENDING && invitation.expiresAt > now, ).length, lockedUsers, missingConsent, familiesWithoutUsers: kita.families.filter( (family) => family.users.length === 0, ).length, }, lastLoginAt, roleCounts, }; }); } export type KitaDiagnostics = { kita: { id: string; name: string; slug: string; createdAt: Date; updatedAt: Date; modules: KitaOverviewRow["modules"]; }; counts: KitaOverviewRow["counts"] & { inactiveChildren: number; expiredPendingInvitations: number; }; roleCounts: Record; users: { id: string; email: string; firstName: string; lastName: string; role: UserRole; familyName: string | null; emailVerifiedAt: Date | null; privacyPolicyAcceptedAt: Date | null; lastLoginAt: Date | null; failedLoginAttempts: number; lockedUntil: Date | null; createdAt: Date; }[]; diagnostics: { lockedUsers: number; missingConsent: number; unverifiedUsers: number; familiesWithoutUsers: number; usersWithEmptyPassword: number; }; }; export async function getKitaDiagnostics( kitaId: string, ): Promise { const now = new Date(); const kita = await prisma.kita.findUnique({ where: { id: kitaId }, select: { id: true, name: true, slug: true, notdienstModuleEnabled: true, terminModuleEnabled: true, adressbuchModuleEnabled: true, createdAt: true, updatedAt: true, users: { select: { id: true, email: true, firstName: true, lastName: true, role: true, passwordHash: true, emailVerifiedAt: true, privacyPolicyAcceptedAt: true, lastLoginAt: true, failedLoginAttempts: true, lockedUntil: true, createdAt: true, family: { select: { name: true }, }, }, orderBy: [ { role: "asc" }, { lastName: "asc" }, { firstName: "asc" }, ], }, families: { select: { users: { select: { id: true } }, }, }, children: { select: { active: true }, }, invitations: { select: { status: true, expiresAt: true, }, }, _count: { select: { termine: true, absences: true, announcements: true, }, }, }, }); if (!kita) { return null; } const roleCounts = Object.fromEntries( Object.values(UserRole).map((role) => [role, 0]), ) as Record; let lockedUsers = 0; let missingConsent = 0; let unverifiedUsers = 0; let usersWithEmptyPassword = 0; for (const user of kita.users) { roleCounts[user.role] += 1; if (user.lockedUntil && user.lockedUntil > now) { lockedUsers += 1; } if (user.role !== UserRole.SUPERADMIN && !user.privacyPolicyAcceptedAt) { missingConsent += 1; } if (!user.emailVerifiedAt) { unverifiedUsers += 1; } if (!user.passwordHash) { usersWithEmptyPassword += 1; } } const activeChildren = kita.children.filter((child) => child.active).length; const inactiveChildren = kita.children.length - activeChildren; const familiesWithoutUsers = kita.families.filter( (family) => family.users.length === 0, ).length; return { kita: { id: kita.id, name: kita.name, slug: kita.slug, createdAt: kita.createdAt, updatedAt: kita.updatedAt, modules: { notdienst: kita.notdienstModuleEnabled, termine: kita.terminModuleEnabled, adressbuch: kita.adressbuchModuleEnabled, }, }, counts: { users: kita.users.length, families: kita.families.length, activeChildren, inactiveChildren, termine: kita._count.termine, absences: kita._count.absences, announcements: kita._count.announcements, openInvitations: kita.invitations.filter( (invitation) => invitation.status === InvitationStatus.PENDING && invitation.expiresAt > now, ).length, expiredPendingInvitations: kita.invitations.filter( (invitation) => invitation.status === InvitationStatus.PENDING && invitation.expiresAt <= now, ).length, lockedUsers, missingConsent, familiesWithoutUsers, }, roleCounts, users: kita.users.map((user) => ({ id: user.id, email: user.email, firstName: user.firstName, lastName: user.lastName, role: user.role, familyName: user.family?.name ?? null, emailVerifiedAt: user.emailVerifiedAt, privacyPolicyAcceptedAt: user.privacyPolicyAcceptedAt, lastLoginAt: user.lastLoginAt, failedLoginAttempts: user.failedLoginAttempts, lockedUntil: user.lockedUntil, createdAt: user.createdAt, })), diagnostics: { lockedUsers, missingConsent, unverifiedUsers, familiesWithoutUsers, usersWithEmptyPassword, }, }; } export async function recordKitaDiagnosticView({ actorUserId, actorEmail, kitaId, kitaName, kitaSlug, }: { actorUserId: string; actorEmail: string; kitaId: string; kitaName: string; kitaSlug: string; }) { await prisma.auditLog.create({ data: { actorUserId, actorEmail, action: "KITA_DIAGNOSTIC_VIEW", targetType: AuditTargetType.KITA, targetId: kitaId, targetLabel: `${kitaName} (${kitaSlug})`, targetKitaId: kitaId, }, }); } export async function getAuditLogRows() { return prisma.auditLog.findMany({ select: { id: true, actorEmail: true, action: true, targetType: true, targetId: true, targetLabel: true, targetKitaId: true, metadata: true, createdAt: true, }, orderBy: { createdAt: "desc" }, take: 200, }); } export { roleOrder };