diff --git a/packages/core/src/lib/organization-project-module/organization-project-module.entity.ts b/packages/core/src/lib/organization-project-module/organization-project-module.entity.ts index 35aae835e9..cca11c45ac 100644 --- a/packages/core/src/lib/organization-project-module/organization-project-module.entity.ts +++ b/packages/core/src/lib/organization-project-module/organization-project-module.entity.ts @@ -181,6 +181,9 @@ export class OrganizationProjectModule extends TenantOrganizationBaseEntity impl /** * Task */ + @ApiPropertyOptional({ type: () => Array, isArray: true, description: 'List of task IDs' }) + @IsOptional() + @IsArray() @MultiORMManyToMany(() => Task, (it) => it.modules, { /** Defines the database action to perform on update. */ onUpdate: 'CASCADE', diff --git a/packages/core/src/lib/organization-project-module/organization-project-module.module.ts b/packages/core/src/lib/organization-project-module/organization-project-module.module.ts index 57cc834872..f310e7d71a 100644 --- a/packages/core/src/lib/organization-project-module/organization-project-module.module.ts +++ b/packages/core/src/lib/organization-project-module/organization-project-module.module.ts @@ -6,11 +6,12 @@ import { CommandHandlers } from './commands/handlers'; import { OrganizationProjectModuleService } from './organization-project-module.service'; import { OrganizationProjectModuleController } from './organization-project-module.controller'; import { OrganizationProjectModule } from './organization-project-module.entity'; -import { TypeOrmOrganizationProjectModuleRepository } from './repository/type-orm-organization-project-module.repository'; import { RolePermissionModule } from '../role-permission/role-permission.module'; import { RoleModule } from '../role/role.module'; import { EmployeeModule } from '../employee/employee.module'; +import { TaskModule } from '../tasks/task.module'; import { OrganizationProjectModuleEmployee } from './organization-project-module-employee.entity'; +import { TypeOrmOrganizationProjectModuleRepository } from './repository/type-orm-organization-project-module.repository'; import { TypeOrmOrganizationProjectModuleEmployeeRepository } from './repository/type-orm-organization-project-module-employee.repository'; @Module({ @@ -21,6 +22,7 @@ import { TypeOrmOrganizationProjectModuleEmployeeRepository } from './repository RolePermissionModule, RoleModule, EmployeeModule, + TaskModule, CqrsModule ], controllers: [OrganizationProjectModuleController], diff --git a/packages/core/src/lib/organization-project-module/organization-project-module.service.ts b/packages/core/src/lib/organization-project-module/organization-project-module.service.ts index 00e02b68dc..cc536eda77 100644 --- a/packages/core/src/lib/organization-project-module/organization-project-module.service.ts +++ b/packages/core/src/lib/organization-project-module/organization-project-module.service.ts @@ -1,5 +1,14 @@ import { BadRequestException, HttpException, HttpStatus, Injectable } from '@nestjs/common'; -import { Brackets, FindManyOptions, SelectQueryBuilder, UpdateResult, WhereExpressionBuilder } from 'typeorm'; +import { InjectDataSource } from '@nestjs/typeorm'; +import { + Brackets, + DataSource, + FindManyOptions, + In, + SelectQueryBuilder, + UpdateResult, + WhereExpressionBuilder +} from 'typeorm'; import { BaseEntityEnum, ActorTypeEnum, @@ -13,12 +22,13 @@ import { ProjectModuleStatusEnum, ActionTypeEnum, RolesEnum, - IEmployee + IEmployee, + ITask } from '@gauzy/contracts'; import { isEmpty, isNotEmpty } from '@gauzy/utils'; import { isPostgres } from '@gauzy/config'; import { PaginationParams, TenantAwareCrudService } from './../core/crud'; -import { RequestContext } from '../core/context'; +import { RequestContext } from '../core/context/request-context'; import { OrganizationProjectModule } from './organization-project-module.entity'; import { prepareSQLQuery as p } from './../database/database.helper'; import { ActivityLogService } from '../activity-log/activity-log.service'; @@ -29,17 +39,20 @@ import { EmployeeService } from '../employee/employee.service'; import { OrganizationProjectModuleEmployee } from './organization-project-module-employee.entity'; import { TypeOrmOrganizationProjectModuleEmployeeRepository } from './repository/type-orm-organization-project-module-employee.repository'; import { MikroOrmOrganizationProjectModuleEmployeeRepository } from './repository/mikro-orm-organization-project-module-employee.repository'; +import { TaskService } from '../tasks/task.service'; @Injectable() export class OrganizationProjectModuleService extends TenantAwareCrudService { constructor( + @InjectDataSource() private readonly dataSource: DataSource, readonly typeOrmProjectModuleRepository: TypeOrmOrganizationProjectModuleRepository, readonly mikroOrmProjectModuleRepository: MikroOrmOrganizationProjectModuleRepository, readonly typeOrmOrganizationProjectModuleEmployeeRepository: TypeOrmOrganizationProjectModuleEmployeeRepository, readonly mikroOrmOrganizationProjectModuleEmployeeRepository: MikroOrmOrganizationProjectModuleEmployeeRepository, - private readonly activityLogService: ActivityLogService, + private readonly _activityLogService: ActivityLogService, private readonly _roleService: RoleService, - private readonly _employeeService: EmployeeService + private readonly _employeeService: EmployeeService, + private readonly _taskService: TaskService ) { super(typeOrmProjectModuleRepository, mikroOrmProjectModuleRepository); } @@ -56,53 +69,29 @@ export class OrganizationProjectModuleService extends TenantAwareCrudService { - // If the employee is a manager, assign the existing manager with the latest assignedAt date - const isManager = managerIdsSet.has(employeeId); - const assignedAt = new Date(); - - return new OrganizationProjectModuleEmployee({ - employeeId, - organizationId, - tenantId, - isManager, - assignedAt, - role: isManager ? managerRole : null - }); - }); + const existingTasks = await this.getExistingTasks(tasks); + const members = await this.buildModuleMembers(employees, managerIds, organizationId, tenantId); const projectModule = await super.create({ ...input, @@ -110,25 +99,20 @@ export class OrganizationProjectModuleService extends TenantAwareCrudService( - BaseEntityEnum.OrganizationProjectModule, - ActionTypeEnum.Created, - ActorTypeEnum.User, - projectModule.id, - projectModule.name, - projectModule, - organizationId, - tenantId - ); + await this.assignTasksToModule(existingTasks, projectModule); + await queryRunner.commitTransaction(); + + this.logModuleActivity(ActionTypeEnum.Created, projectModule, undefined, entity); return projectModule; } catch (error) { - // Handle errors and return an appropriate error response + await queryRunner.rollbackTransaction(); throw new HttpException( `Failed to create organization project module: ${error.message}`, HttpStatus.BAD_REQUEST ); + } finally { + await queryRunner.release(); } } @@ -146,23 +130,22 @@ export class OrganizationProjectModuleService extends TenantAwareCrudService task.id)); + const newTasks = tasks.filter((task) => !existingTaskIds.has(task.id)); + + // Determine tasks to remove + const tasksToRemove = existingProjectModule.tasks.filter( + (task) => !tasks.some((updatedTask) => updatedTask.id === task.id) + ); + + // Add new tasks + for (const task of newTasks) { + task.modules = [...(task.modules || []), existingProjectModule]; + await this._taskService.update(task.id, task); + } + + // Remove tasks + for (const task of tasksToRemove) { + task.modules = task.modules?.filter((module) => module.id !== existingProjectModule.id) || []; + await this._taskService.update(task.id, task); + } + } + + // Update the project module with new values const updatedProjectModule = await super.create({ ...entity, id }); // Generate the activity log + this.logModuleActivity(ActionTypeEnum.Updated, updatedProjectModule, existingProjectModule, entity); - this.activityLogService.logActivity( - BaseEntityEnum.OrganizationProjectModule, - ActionTypeEnum.Updated, - ActorTypeEnum.User, - updatedProjectModule.id, - updatedProjectModule.name, - updatedProjectModule, - organizationId, - tenantId, - existingProjectModule, - entity - ); - - // return updated Module + // Return updated module return updatedProjectModule; } catch (error) { throw new BadRequestException(error); @@ -523,20 +520,20 @@ export class OrganizationProjectModuleService extends TenantAwareCrudService { + try { + const currentRole = await this._roleService.findOneByIdString(currentRoleId, { + where: { name: RolesEnum.EMPLOYEE } + }); + if (currentRole && !managerIds.includes(employeeId)) { + managerIds.push(employeeId); + } + } catch { + // Role is not "EMPLOYEE" or no action needed. + } + } + + /** + * Fetch existing tasks related to the project module. + * @param tasks List of tasks to check. + * @returns A list of existing tasks found in the database. + */ + private async getExistingTasks(tasks: ITask[]): Promise { + const taskIds = tasks.map((task) => task.id); + return this._taskService.find({ + where: { id: In(taskIds) }, + relations: { modules: true } + }); + } + + /** + * Build module members from employees and assign manager roles. + * @param employees List of employees to assign as members. + * @param managerIds List of manager IDs. + * @param organizationId The ID of the organization. + * @param tenantId The ID of the tenant. + * @returns A list of organization project module members. + */ + private async buildModuleMembers( + employees: IEmployee[], + managerIds: string[], + organizationId: string, + tenantId: string + ): Promise { + const managerRole = await this._roleService.findOneByWhereOptions({ name: RolesEnum.MANAGER }); + const managerIdsSet = new Set(managerIds); + + return employees.map(({ id: employeeId }) => { + const isManager = managerIdsSet.has(employeeId); + return new OrganizationProjectModuleEmployee({ + employeeId, + organizationId, + tenantId, + isManager, + assignedAt: new Date(), + role: isManager ? managerRole : null + }); + }); + } + + /** + * Assign tasks to the project module. + * @param tasks List of tasks to associate with the module. + * @param projectModule The project module to assign tasks to. + */ + private async assignTasksToModule(tasks: ITask[], projectModule: IOrganizationProjectModule): Promise { + const taskUpdates = tasks.map((task) => { + if (!task.modules) { + task.modules = []; + } + task.modules.push(projectModule); + return this._taskService.update(task.id, { ...task }); + }); + await Promise.all(taskUpdates); + } + + /** + * Log activity for a project module. + * @param projectModule The project module to log. + * @param organizationId The ID of the organization. + * @param tenantId The ID of the tenant. + */ + private logModuleActivity( + action: ActionTypeEnum, + updatedModule: IOrganizationProjectModule, + existingModule?: IOrganizationProjectModule, + changes?: Partial + ): void { + const tenantId = RequestContext.currentTenantId() ?? updatedModule.tenantId; + const organizationId = updatedModule.organizationId; + + this._activityLogService.logActivity( + BaseEntityEnum.OrganizationProjectModule, + action, + ActorTypeEnum.User, + updatedModule.id, + updatedModule.name, + updatedModule, + organizationId, + tenantId, + existingModule, + changes + ); + } } diff --git a/packages/ui-core/shared/src/lib/project-module/project-module-table/project-module-table.module.ts b/packages/ui-core/shared/src/lib/project-module/project-module-table/project-module-table.module.ts index 99980c410e..3a131a9235 100644 --- a/packages/ui-core/shared/src/lib/project-module/project-module-table/project-module-table.module.ts +++ b/packages/ui-core/shared/src/lib/project-module/project-module-table/project-module-table.module.ts @@ -1,13 +1,22 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { NbSpinnerModule } from '@nebular/theme'; +import { NbButtonModule, NbIconModule, NbSpinnerModule } from '@nebular/theme'; import { SmartDataViewLayoutModule } from '../../smart-data-layout'; import { ProjectModuleTableComponent } from './project-module-table.component'; import { ProjectModuleMutationModule } from '../project-module-mutation/project-module-mutation.module'; +import { TranslateModule } from '@ngx-translate/core'; @NgModule({ declarations: [ProjectModuleTableComponent], exports: [ProjectModuleTableComponent], - imports: [CommonModule, NbSpinnerModule, ProjectModuleMutationModule, SmartDataViewLayoutModule] + imports: [ + CommonModule, + NbSpinnerModule, + NbButtonModule, + NbIconModule, + TranslateModule.forChild(), + ProjectModuleMutationModule, + SmartDataViewLayoutModule + ] }) export class ProjectModuleTableModule {}