import { BadRequestException, ConflictException, ForbiddenException, Injectable, NotFoundException, } from '@nestjs/common'; import type { OrgRoleDto, RolePermissionKey, UserRole } from '@mento/shared'; import { Prisma } from '@prisma/client'; import { PrismaService } from '../prisma/prisma.service'; import { ALL_ROLE_PERMISSIONS, defaultColorForSystemKey, sanitizePermissions, SYSTEM_ROLE_SEEDS, systemKeyToUserRole, } from './role-defaults'; type RoleRow = { id: string; name: string; description: string; color: string; permissions: string[]; systemKey: string | null; memberships: Array<{ user: { id: string; fullName: string; avatarStorageKey: string | null; }; }>; }; const ROLE_MEMBERS_INCLUDE = { memberships: { include: { user: { select: { id: true, fullName: true, avatarStorageKey: true } }, }, orderBy: { createdAt: 'asc' as const }, }, }; @Injectable() export class OrgRolesService { constructor(private readonly prisma: PrismaService) {} async list(organizationId: string): Promise { await this.ensureDefaultRoles(organizationId); const rows = await this.prisma.orgRole.findMany({ where: { organizationId }, include: ROLE_MEMBERS_INCLUDE, orderBy: [{ systemKey: 'asc' }, { createdAt: 'asc' }], }); return rows.map((row) => this.toDto(row)); } async create( organizationId: string, actorRole: UserRole, dto: { name: string; description?: string; color?: string; permissions?: RolePermissionKey[]; }, ): Promise { this.assertManager(actorRole); const name = dto.name.trim(); if (name.length < 2) { throw new BadRequestException('نام نقش باید حداقل دو حرف باشد'); } try { const row = await this.prisma.orgRole.create({ data: { organizationId, name, description: (dto.description ?? '').trim(), color: dto.color ?? 'teal', permissions: dto.permissions?.length ? dto.permissions : [...ALL_ROLE_PERMISSIONS], }, include: ROLE_MEMBERS_INCLUDE, }); return this.toDto(row); } catch (e) { if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') { throw new ConflictException('نقشی با این نام از قبل وجود دارد'); } throw e; } } async update( organizationId: string, actorRole: UserRole, id: string, dto: { name?: string; description?: string; color?: string; permissions?: RolePermissionKey[]; }, ): Promise { this.assertManager(actorRole); const existing = await this.prisma.orgRole.findFirst({ where: { id, organizationId }, }); if (!existing) throw new NotFoundException('نقش پیدا نشد'); const data: Prisma.OrgRoleUpdateInput = {}; if (dto.name !== undefined) { const name = dto.name.trim(); if (name.length < 2) { throw new BadRequestException('نام نقش باید حداقل دو حرف باشد'); } data.name = name; } if (dto.description !== undefined) data.description = dto.description.trim(); if (dto.color !== undefined) data.color = dto.color; if (dto.permissions !== undefined) data.permissions = sanitizePermissions(dto.permissions); try { const row = await this.prisma.orgRole.update({ where: { id: existing.id }, data, include: ROLE_MEMBERS_INCLUDE, }); return this.toDto(row); } catch (e) { if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') { throw new ConflictException('نقشی با این نام از قبل وجود دارد'); } throw e; } } async bulkUpdatePermissions( organizationId: string, actorRole: UserRole, items: Array<{ id: string; permissions: RolePermissionKey[] }>, ): Promise { this.assertManager(actorRole); await this.ensureDefaultRoles(organizationId); await this.prisma.$transaction( items.map((item) => this.prisma.orgRole.updateMany({ where: { id: item.id, organizationId }, data: { permissions: sanitizePermissions(item.permissions) }, }), ), ); return this.list(organizationId); } async remove(organizationId: string, actorRole: UserRole, id: string): Promise<{ ok: true }> { this.assertManager(actorRole); const existing = await this.prisma.orgRole.findFirst({ where: { id, organizationId }, }); if (!existing) throw new NotFoundException('نقش پیدا نشد'); if (existing.systemKey) { throw new BadRequestException('نقش‌های سیستمی قابل حذف نیستند'); } await this.prisma.orgRole.delete({ where: { id: existing.id } }); return { ok: true }; } async ensureDefaultRoles(organizationId: string) { const existing = await this.prisma.orgRole.findMany({ where: { organizationId }, }); const byKey = new Map(existing.filter((r) => r.systemKey).map((r) => [r.systemKey!, r])); for (const seed of SYSTEM_ROLE_SEEDS) { if (byKey.has(seed.systemKey)) continue; const created = await this.prisma.orgRole.create({ data: { organizationId, name: seed.name, description: seed.description, color: seed.color, permissions: seed.permissions, systemKey: seed.systemKey, }, }); byKey.set(seed.systemKey, created); } const unassigned = await this.prisma.membership.findMany({ where: { organizationId, orgRoleId: null }, select: { id: true, role: true }, }); for (const membership of unassigned) { const role = byKey.get(membership.role); if (!role) continue; await this.prisma.membership.update({ where: { id: membership.id }, data: { orgRoleId: role.id }, }); } } async resolveInviteRole( organizationId: string, dto: { orgRoleId?: string; role?: Exclude }, ): Promise<{ orgRoleId: string; role: UserRole }> { await this.ensureDefaultRoles(organizationId); if (dto.orgRoleId) { const orgRole = await this.prisma.orgRole.findFirst({ where: { id: dto.orgRoleId, organizationId }, }); if (!orgRole) throw new BadRequestException('نقش انتخاب‌شده معتبر نیست'); return { orgRoleId: orgRole.id, role: systemKeyToUserRole(orgRole.systemKey), }; } const role = dto.role ?? 'sales'; const orgRole = await this.prisma.orgRole.findFirst({ where: { organizationId, systemKey: role }, }); if (!orgRole) { throw new BadRequestException('نقش پیش‌فرض پیدا نشد'); } return { orgRoleId: orgRole.id, role }; } defaultColorFor(role: UserRole) { return defaultColorForSystemKey(role); } private assertManager(actorRole: UserRole) { if (actorRole !== 'owner' && actorRole !== 'admin') { throw new ForbiddenException('فقط مالک یا مدیر می‌تواند نقش‌ها را مدیریت کند'); } } private toDto(row: RoleRow): OrgRoleDto { const systemKey = row.systemKey === 'owner' || row.systemKey === 'admin' || row.systemKey === 'sales' || row.systemKey === 'support' ? row.systemKey : null; return { id: row.id, name: row.name, description: row.description, color: row.color, permissions: sanitizePermissions(row.permissions), systemKey, userCount: row.memberships.length, members: row.memberships.map((membership) => ({ id: membership.user.id, fullName: membership.user.fullName, hasAvatar: Boolean(membership.user.avatarStorageKey), })), }; } }