import { TenantUserStatus } from "~/application/enums/tenants/TenantUserStatus"; import { TenantUserType } from "~/application/enums/tenants/TenantUserType"; import { db } from "~/utils/db.server"; // R-P2b — User-Lifecycle: Soft-Deaktivierung mit Last-Admin-Guard + optionalem Ownership-Reassign. // Ein Tenant muss IMMER mindestens einen aktiven Owner/Admin behalten, sonst waere er handlungsunfaehig. const PRIVILEGED_TYPES = [TenantUserType.OWNER, TenantUserType.ADMIN]; export type DeactivateTenantUserResult = | { ok: true } | { ok: false; reason: "not-found" | "already-inactive" | "last-admin" }; // Zaehlt aktive Owner/Admins eines Tenants (optional einen User ausschliessend). export async function countActivePrivilegedMembers(tenantId: string, excludeUserId?: string): Promise { return db.tenantUser.count({ where: { tenantId, status: TenantUserStatus.ACTIVE, type: { in: PRIVILEGED_TYPES }, ...(excludeUserId ? { userId: { not: excludeUserId } } : {}), }, }); } // Reassign des tenant-eigenen Besitzes eines deaktivierten Users an einen Nachfolger. Bei P2 ist das // die Gruppen-Ownership; Fach-Module (CRM/Projekte ab P6) haengen ihren Reassign hier an, wenn sie // existieren. Gibt die Anzahl umgehaengter Datensaetze zurueck. export async function reassignTenantUserOwnership({ tenantId, fromUserId, toUserId, }: { tenantId: string; fromUserId: string; toUserId: string; }): Promise { const groups = await db.group.updateMany({ where: { tenantId, createdByUserId: fromUserId }, data: { createdByUserId: toUserId }, }); return groups.count; } export async function deactivateTenantUser({ tenantId, targetUserId, reassignToUserId, }: { tenantId: string; targetUserId: string; reassignToUserId?: string; }): Promise { const member = await db.tenantUser.findFirst({ where: { tenantId, userId: targetUserId }, select: { id: true, type: true, status: true }, }); if (!member) { return { ok: false, reason: "not-found" }; } if (member.status === TenantUserStatus.INACTIVE) { return { ok: false, reason: "already-inactive" }; } if (PRIVILEGED_TYPES.includes(member.type)) { const otherActiveAdmins = await countActivePrivilegedMembers(tenantId, targetUserId); if (otherActiveAdmins === 0) { return { ok: false, reason: "last-admin" }; } } if (reassignToUserId && reassignToUserId !== targetUserId) { await reassignTenantUserOwnership({ tenantId, fromUserId: targetUserId, toUserId: reassignToUserId }); } await db.tenantUser.update({ where: { id: member.id }, data: { status: TenantUserStatus.INACTIVE } }); return { ok: true }; } export async function reactivateTenantUser({ tenantId, targetUserId, }: { tenantId: string; targetUserId: string; }): Promise<{ ok: true } | { ok: false; reason: "not-found" | "already-active" }> { const member = await db.tenantUser.findFirst({ where: { tenantId, userId: targetUserId }, select: { id: true, status: true }, }); if (!member) { return { ok: false, reason: "not-found" }; } if (member.status === TenantUserStatus.ACTIVE) { return { ok: false, reason: "already-active" }; } await db.tenantUser.update({ where: { id: member.id }, data: { status: TenantUserStatus.ACTIVE } }); return { ok: true }; }