Branches: - 5 services: branch, schedule, inventory-settings, payment-terminal, user-assignment - Hierarchical management, schedules, terminals Products: - 6 services: category, product, price, supplier, attribute, variant - Hierarchical categories, multi-pricing, variants Projects: - 6 services: project, task, timesheet, milestone, member, stage - Kanban support, timesheet approval workflow Sales: - 2 services: quotation, sales-order - Full sales workflow with quotation-to-order conversion Invoices: - 2 services: invoice, payment - Complete invoicing with payment allocation Notifications: - 5 services: notification, preference, template, channel, in-app - Multi-channel support (email, sms, push, whatsapp, in-app) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
264 lines
6.0 KiB
TypeScript
264 lines
6.0 KiB
TypeScript
/**
|
|
* 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<ProjectMemberEntity>;
|
|
|
|
constructor(dataSource: DataSource) {
|
|
this.repository = dataSource.getRepository(ProjectMemberEntity);
|
|
}
|
|
|
|
/**
|
|
* Find member by ID
|
|
*/
|
|
async findById(ctx: ServiceContext, id: string): Promise<ProjectMemberEntity | null> {
|
|
return this.repository.findOne({
|
|
where: {
|
|
id,
|
|
tenantId: ctx.tenantId,
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Find member by project and user
|
|
*/
|
|
async findByProjectAndUser(
|
|
ctx: ServiceContext,
|
|
projectId: string,
|
|
userId: string
|
|
): Promise<ProjectMemberEntity | null> {
|
|
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<PaginatedResult<ProjectMemberEntity>> {
|
|
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<ProjectMemberEntity> {
|
|
// 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<ProjectMemberEntity | null> {
|
|
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<boolean> {
|
|
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<boolean> {
|
|
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<PaginatedResult<ProjectMemberEntity>> {
|
|
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<PaginatedResult<ProjectMemberEntity>> {
|
|
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<boolean> {
|
|
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<string | null> {
|
|
const member = await this.findByProjectAndUser(ctx, projectId, userId);
|
|
return member?.role || null;
|
|
}
|
|
|
|
/**
|
|
* Count members in a project
|
|
*/
|
|
async countMembers(ctx: ServiceContext, projectId: string): Promise<number> {
|
|
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<ProjectMemberEntity[]> {
|
|
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<number> {
|
|
const result = await this.repository.delete({
|
|
projectId,
|
|
tenantId: ctx.tenantId,
|
|
});
|
|
|
|
return result.affected || 0;
|
|
}
|
|
}
|