/** * ProjectMemberService - Project Team Member Management Service * * Provides team member management for projects. * Supports multi-tenant architecture with ServiceContext pattern. * * @module Projects */ import { DataSource, Repository } from 'typeorm'; import { ProjectMemberEntity, ProjectMemberRole } from '../entities/project-member.entity'; import { ServiceContext, PaginatedResult } from './project.service'; export interface AddMemberDto { projectId: string; userId: string; role?: string; } export interface UpdateMemberRoleDto { role: string; } export interface ProjectMemberFilters { projectId?: string; userId?: string; role?: string; } export class ProjectMemberService { private repository: Repository; constructor(dataSource: DataSource) { this.repository = dataSource.getRepository(ProjectMemberEntity); } /** * Find member by ID */ async findById(ctx: ServiceContext, id: string): Promise { return this.repository.findOne({ where: { id, tenantId: ctx.tenantId, }, }); } /** * Find member by project and user */ async findByProjectAndUser( ctx: ServiceContext, projectId: string, userId: string ): Promise { return this.repository.findOne({ where: { projectId, userId, tenantId: ctx.tenantId, }, }); } /** * Find all members with pagination and filters */ async findAll( ctx: ServiceContext, filters: ProjectMemberFilters = {}, page = 1, limit = 20 ): Promise> { const skip = (page - 1) * limit; const qb = this.repository .createQueryBuilder('pm') .where('pm.tenant_id = :tenantId', { tenantId: ctx.tenantId }); if (filters.projectId) { qb.andWhere('pm.project_id = :projectId', { projectId: filters.projectId }); } if (filters.userId) { qb.andWhere('pm.user_id = :userId', { userId: filters.userId }); } if (filters.role) { qb.andWhere('pm.role = :role', { role: filters.role }); } qb.orderBy('pm.created_at', 'DESC').skip(skip).take(limit); const [data, total] = await qb.getManyAndCount(); return { data, total, page, limit, totalPages: Math.ceil(total / limit), }; } /** * Add a member to a project */ async addMember(ctx: ServiceContext, dto: AddMemberDto): Promise { // Check if already a member const existing = await this.findByProjectAndUser(ctx, dto.projectId, dto.userId); if (existing) { throw new Error('User is already a member of this project'); } const entity = this.repository.create({ tenantId: ctx.tenantId, projectId: dto.projectId, userId: dto.userId, role: dto.role || ProjectMemberRole.MEMBER, }); return this.repository.save(entity); } /** * Update member role */ async updateRole( ctx: ServiceContext, id: string, dto: UpdateMemberRoleDto ): Promise { const entity = await this.findById(ctx, id); if (!entity) { return null; } entity.role = dto.role; return this.repository.save(entity); } /** * Remove a member from a project */ async removeMember(ctx: ServiceContext, id: string): Promise { const entity = await this.findById(ctx, id); if (!entity) { return false; } await this.repository.delete(id); return true; } /** * Remove a member by project and user */ async removeMemberByProjectAndUser( ctx: ServiceContext, projectId: string, userId: string ): Promise { const entity = await this.findByProjectAndUser(ctx, projectId, userId); if (!entity) { return false; } await this.repository.delete(entity.id); return true; } /** * Find members by project */ async findByProject( ctx: ServiceContext, projectId: string, page = 1, limit = 20 ): Promise> { return this.findAll(ctx, { projectId }, page, limit); } /** * Find projects where user is a member */ async findProjectsByUser( ctx: ServiceContext, userId: string, page = 1, limit = 20 ): Promise> { return this.findAll(ctx, { userId }, page, limit); } /** * Check if user is a member of a project */ async isMember(ctx: ServiceContext, projectId: string, userId: string): Promise { const member = await this.findByProjectAndUser(ctx, projectId, userId); return member !== null; } /** * Get user role in a project */ async getUserRole( ctx: ServiceContext, projectId: string, userId: string ): Promise { const member = await this.findByProjectAndUser(ctx, projectId, userId); return member?.role || null; } /** * Count members in a project */ async countMembers(ctx: ServiceContext, projectId: string): Promise { return this.repository.count({ where: { projectId, tenantId: ctx.tenantId }, }); } /** * Add multiple members to a project */ async addMembers( ctx: ServiceContext, projectId: string, userIds: string[], role?: string ): Promise { const added: ProjectMemberEntity[] = []; for (const userId of userIds) { try { const member = await this.addMember(ctx, { projectId, userId, role: role || ProjectMemberRole.MEMBER, }); added.push(member); } catch { // Skip if already a member } } return added; } /** * Remove all members from a project */ async removeAllMembers(ctx: ServiceContext, projectId: string): Promise { const result = await this.repository.delete({ projectId, tenantId: ctx.tenantId, }); return result.affected || 0; } }