From 47c28225f373b449378c4cb679fcb9d325342f58 Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Sun, 22 Sep 2024 16:15:39 +0200 Subject: [PATCH 01/28] fix: project modules entity --- .../organization-project-module.entity.ts | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/core/src/organization-project-module/organization-project-module.entity.ts b/packages/core/src/organization-project-module/organization-project-module.entity.ts index ebfe8108ab3..ed75890bec7 100644 --- a/packages/core/src/organization-project-module/organization-project-module.entity.ts +++ b/packages/core/src/organization-project-module/organization-project-module.entity.ts @@ -4,7 +4,7 @@ import { EntityRepositoryType } from '@mikro-orm/core'; import { IsArray, IsBoolean, - IsDate, + IsDateString, IsEnum, IsNotEmpty, IsObject, @@ -12,7 +12,6 @@ import { IsString, IsUUID } from 'class-validator'; -import { Type } from 'class-transformer'; import { ID, IEmployee, @@ -69,16 +68,14 @@ export class OrganizationProjectModule extends TenantOrganizationBaseEntity impl status?: TaskStatusEnum; @ApiPropertyOptional({ type: () => Date }) - @Type(() => Date) @IsOptional() - @IsDate() + @IsDateString() @MultiORMColumn({ nullable: true }) startDate?: Date; @ApiPropertyOptional({ type: () => Date }) - @Type(() => Date) @IsOptional() - @IsDate() + @IsDateString() @MultiORMColumn({ nullable: true }) endDate?: Date; @@ -142,6 +139,9 @@ export class OrganizationProjectModule extends TenantOrganizationBaseEntity impl /** * Creator */ + @ApiPropertyOptional({ type: () => Object }) + @IsOptional() + @IsObject() @MultiORMManyToOne(() => User, { nullable: true, onDelete: 'CASCADE' @@ -149,6 +149,9 @@ export class OrganizationProjectModule extends TenantOrganizationBaseEntity impl @JoinColumn() creator?: IUser; + @ApiPropertyOptional({ type: () => String }) + @IsOptional() + @IsUUID() @RelationId((it: OrganizationProjectModule) => it.creator) @ColumnIndex() @MultiORMColumn({ nullable: true, relationId: true }) @@ -157,6 +160,9 @@ export class OrganizationProjectModule extends TenantOrganizationBaseEntity impl /** * Module manager */ + @ApiPropertyOptional({ type: () => Object }) + @IsOptional() + @IsObject() @MultiORMManyToOne(() => User, { nullable: true, onDelete: 'CASCADE' @@ -164,6 +170,9 @@ export class OrganizationProjectModule extends TenantOrganizationBaseEntity impl @JoinColumn() manager?: IUser; + @ApiPropertyOptional({ type: () => String }) + @IsOptional() + @IsUUID() @RelationId((it: OrganizationProjectModule) => it.manager) @ColumnIndex() @MultiORMColumn({ nullable: true, relationId: true }) From bfeb313b89d82807a3ee4f14131a6fd47a7f8919 Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Tue, 24 Sep 2024 06:12:59 +0200 Subject: [PATCH 02/28] feat: module tasks relation --- packages/contracts/src/task.model.ts | 8 ++-- .../organization-project-module.entity.ts | 18 +++++--- packages/core/src/tasks/task.entity.ts | 42 +++++++++---------- 3 files changed, 34 insertions(+), 34 deletions(-) diff --git a/packages/contracts/src/task.model.ts b/packages/contracts/src/task.model.ts index e8f261c4050..2d8a5c25d16 100644 --- a/packages/contracts/src/task.model.ts +++ b/packages/contracts/src/task.model.ts @@ -9,12 +9,9 @@ import { IUser } from './user.model'; import { ITaskStatus, TaskStatusEnum } from './task-status.model'; import { ITaskPriority, TaskPriorityEnum } from './task-priority.model'; import { ITaskSize, TaskSizeEnum } from './task-size.model'; -import { IRelationalOrganizationProjectModule } from './organization-project-module.model'; +import { IOrganizationProjectModule } from 'organization-project-module.model'; -export interface ITask - extends IBasePerTenantAndOrganizationEntityModel, - IRelationalOrganizationProject, - IRelationalOrganizationProjectModule { +export interface ITask extends IBasePerTenantAndOrganizationEntityModel, IRelationalOrganizationProject { title: string; number?: number; public?: boolean; @@ -29,6 +26,7 @@ export interface ITask members?: IEmployee[]; invoiceItems?: IInvoiceItem[]; teams?: IOrganizationTeam[]; + modules?: IOrganizationProjectModule[]; organizationSprint?: IOrganizationSprint; organizationSprintId?: ID; creator?: IUser; diff --git a/packages/core/src/organization-project-module/organization-project-module.entity.ts b/packages/core/src/organization-project-module/organization-project-module.entity.ts index ed75890bec7..37e1b3673de 100644 --- a/packages/core/src/organization-project-module/organization-project-module.entity.ts +++ b/packages/core/src/organization-project-module/organization-project-module.entity.ts @@ -190,18 +190,24 @@ export class OrganizationProjectModule extends TenantOrganizationBaseEntity impl @MultiORMOneToMany(() => OrganizationProjectModule, (module) => module.parent) children?: OrganizationProjectModule[]; - /** - * Organization Tasks Relationship - */ - @MultiORMOneToMany(() => Task, (it) => it.projectModule) - tasks?: ITask[]; - /* |-------------------------------------------------------------------------- | @ManyToMany |-------------------------------------------------------------------------- */ + /** + * Task + */ + @MultiORMManyToMany(() => Task, (it) => it.modules, { + /** Defines the database action to perform on update. */ + onUpdate: 'CASCADE', + /** Defines the database cascade action on delete. */ + onDelete: 'CASCADE' + }) + @JoinTable() + tasks?: ITask[]; + /** * Organization Sprint */ diff --git a/packages/core/src/tasks/task.entity.ts b/packages/core/src/tasks/task.entity.ts index 098ffadd7b9..f631b891d99 100644 --- a/packages/core/src/tasks/task.entity.ts +++ b/packages/core/src/tasks/task.entity.ts @@ -208,29 +208,6 @@ export class Task extends TenantOrganizationBaseEntity implements ITask { @MultiORMColumn({ nullable: true, relationId: true }) projectId?: ID; - /** - * Organization Project Module - */ - @ApiPropertyOptional({ type: () => Object }) - @IsOptional() - @IsObject() - @MultiORMManyToOne(() => OrganizationProjectModule, (it) => it.tasks, { - /** Indicates if the relation column value can be nullable or not. */ - nullable: true, - - /** Defines the database cascade action on delete. */ - onDelete: 'CASCADE' - }) - projectModule?: IOrganizationProjectModule; - - @ApiPropertyOptional({ type: () => String }) - @IsOptional() - @IsUUID() - @RelationId((it: Task) => it.projectModule) - @ColumnIndex() - @MultiORMColumn({ nullable: true, relationId: true }) - projectModuleId?: ID; - /** * Creator */ @@ -446,4 +423,23 @@ export class Task extends TenantOrganizationBaseEntity implements ITask { name: 'task_team' }) teams?: IOrganizationTeam[]; + + /** + * Project Module + */ + @ApiPropertyOptional({ type: () => Array, isArray: true }) + @IsOptional() + @IsArray() + @MultiORMManyToMany(() => OrganizationProjectModule, (module) => module.tasks, { + /** Defines the database action to perform on update. */ + onUpdate: 'CASCADE', + /** Defines the database cascade action on delete. */ + onDelete: 'CASCADE', + owner: true, + pivotTable: 'project_module_task', + joinColumn: 'taskId', + inverseJoinColumn: 'organizationProjectModuleId' + }) + @JoinTable({ name: 'project_module_task' }) + modules?: IOrganizationProjectModule[]; } From 7e445a3123ecffc49d41bf9f2a204333e7996843 Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Tue, 24 Sep 2024 07:26:04 +0200 Subject: [PATCH 03/28] feat: postgres migration for module tasks --- ...7152443794-CreateProjectModuleTaskTable.ts | 123 ++++++++++++++++++ .../organization-project-module.entity.ts | 2 +- 2 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 packages/core/src/database/migrations/1727152443794-CreateProjectModuleTaskTable.ts diff --git a/packages/core/src/database/migrations/1727152443794-CreateProjectModuleTaskTable.ts b/packages/core/src/database/migrations/1727152443794-CreateProjectModuleTaskTable.ts new file mode 100644 index 00000000000..d4913b2d2a7 --- /dev/null +++ b/packages/core/src/database/migrations/1727152443794-CreateProjectModuleTaskTable.ts @@ -0,0 +1,123 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; +import { yellow } from 'chalk'; +import { DatabaseTypeEnum } from '@gauzy/config'; + +export class CreateProjectModuleTaskTable1727152443794 implements MigrationInterface { + name = 'CreateProjectModuleTaskTable1727152443794'; + + /** + * Up Migration + * + * @param queryRunner + */ + public async up(queryRunner: QueryRunner): Promise { + console.log(yellow(this.name + ' start running!')); + + switch (queryRunner.connection.options.type) { + case DatabaseTypeEnum.sqlite: + case DatabaseTypeEnum.betterSqlite3: + await this.sqliteUpQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.postgres: + await this.postgresUpQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.mysql: + await this.mysqlUpQueryRunner(queryRunner); + break; + default: + throw Error(`Unsupported database: ${queryRunner.connection.options.type}`); + } + } + + /** + * Down Migration + * + * @param queryRunner + */ + public async down(queryRunner: QueryRunner): Promise { + switch (queryRunner.connection.options.type) { + case DatabaseTypeEnum.sqlite: + case DatabaseTypeEnum.betterSqlite3: + await this.sqliteDownQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.postgres: + await this.postgresDownQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.mysql: + await this.mysqlDownQueryRunner(queryRunner); + break; + default: + throw Error(`Unsupported database: ${queryRunner.connection.options.type}`); + } + } + + /** + * PostgresDB Up Migration + * + * @param queryRunner + */ + public async postgresUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "task" DROP CONSTRAINT "FK_579534d8e12f22d308d6bd5f428"`); + await queryRunner.query(`DROP INDEX "public"."IDX_579534d8e12f22d308d6bd5f42"`); + await queryRunner.query( + `CREATE TABLE "project_module_task" ("organizationProjectModuleId" uuid NOT NULL, "taskId" uuid NOT NULL, CONSTRAINT "PK_524eec559972ae7bd85df1ac492" PRIMARY KEY ("organizationProjectModuleId", "taskId"))` + ); + await queryRunner.query( + `CREATE INDEX "IDX_58941b9cf23de12b2ecea4a959" ON "project_module_task" ("organizationProjectModuleId") ` + ); + await queryRunner.query(`CREATE INDEX "IDX_d056d5ba005e15ff92d4d7a8ca" ON "project_module_task" ("taskId") `); + await queryRunner.query(`ALTER TABLE "task" DROP COLUMN "projectModuleId"`); + await queryRunner.query( + `ALTER TABLE "project_module_task" ADD CONSTRAINT "FK_58941b9cf23de12b2ecea4a959f" FOREIGN KEY ("organizationProjectModuleId") REFERENCES "organization_project_module"("id") ON DELETE CASCADE ON UPDATE CASCADE` + ); + await queryRunner.query( + `ALTER TABLE "project_module_task" ADD CONSTRAINT "FK_d056d5ba005e15ff92d4d7a8ca5" FOREIGN KEY ("taskId") REFERENCES "task"("id") ON DELETE CASCADE ON UPDATE CASCADE` + ); + } + + /** + * PostgresDB Down Migration + * + * @param queryRunner + */ + public async postgresDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "project_module_task" DROP CONSTRAINT "FK_d056d5ba005e15ff92d4d7a8ca5"`); + await queryRunner.query(`ALTER TABLE "project_module_task" DROP CONSTRAINT "FK_58941b9cf23de12b2ecea4a959f"`); + await queryRunner.query(`ALTER TABLE "task" ADD "projectModuleId" uuid`); + await queryRunner.query(`DROP INDEX "public"."IDX_d056d5ba005e15ff92d4d7a8ca"`); + await queryRunner.query(`DROP INDEX "public"."IDX_58941b9cf23de12b2ecea4a959"`); + await queryRunner.query(`DROP TABLE "project_module_task"`); + await queryRunner.query(`CREATE INDEX "IDX_579534d8e12f22d308d6bd5f42" ON "task" ("projectModuleId") `); + await queryRunner.query( + `ALTER TABLE "task" ADD CONSTRAINT "FK_579534d8e12f22d308d6bd5f428" FOREIGN KEY ("projectModuleId") REFERENCES "organization_project_module"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ); + } + + /** + * SqliteDB and BetterSQlite3DB Up Migration + * + * @param queryRunner + */ + public async sqliteUpQueryRunner(queryRunner: QueryRunner): Promise {} + + /** + * SqliteDB and BetterSQlite3DB Down Migration + * + * @param queryRunner + */ + public async sqliteDownQueryRunner(queryRunner: QueryRunner): Promise {} + + /** + * MySQL Up Migration + * + * @param queryRunner + */ + public async mysqlUpQueryRunner(queryRunner: QueryRunner): Promise {} + + /** + * MySQL Down Migration + * + * @param queryRunner + */ + public async mysqlDownQueryRunner(queryRunner: QueryRunner): Promise {} +} diff --git a/packages/core/src/organization-project-module/organization-project-module.entity.ts b/packages/core/src/organization-project-module/organization-project-module.entity.ts index 37e1b3673de..7401f0fe07d 100644 --- a/packages/core/src/organization-project-module/organization-project-module.entity.ts +++ b/packages/core/src/organization-project-module/organization-project-module.entity.ts @@ -205,7 +205,7 @@ export class OrganizationProjectModule extends TenantOrganizationBaseEntity impl /** Defines the database cascade action on delete. */ onDelete: 'CASCADE' }) - @JoinTable() + @JoinTable({ name: 'project_module_task' }) tasks?: ITask[]; /** From f6968fe3098a06699556eda75bc627a708c590ee Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Tue, 24 Sep 2024 09:15:19 +0200 Subject: [PATCH 04/28] feat: project module task mysql and sqlite migration --- ...7152443794-CreateProjectModuleTaskTable.ts | 234 +++++++++++++++++- 1 file changed, 230 insertions(+), 4 deletions(-) diff --git a/packages/core/src/database/migrations/1727152443794-CreateProjectModuleTaskTable.ts b/packages/core/src/database/migrations/1727152443794-CreateProjectModuleTaskTable.ts index d4913b2d2a7..e9a24d95d86 100644 --- a/packages/core/src/database/migrations/1727152443794-CreateProjectModuleTaskTable.ts +++ b/packages/core/src/database/migrations/1727152443794-CreateProjectModuleTaskTable.ts @@ -98,26 +98,252 @@ export class CreateProjectModuleTaskTable1727152443794 implements MigrationInter * * @param queryRunner */ - public async sqliteUpQueryRunner(queryRunner: QueryRunner): Promise {} + public async sqliteUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_579534d8e12f22d308d6bd5f42"`); + await queryRunner.query(`DROP INDEX "IDX_ca2f7edd5a5ce8f14b257c9d54"`); + await queryRunner.query(`DROP INDEX "IDX_3e16c81005c389a4db83c0e5e3"`); + await queryRunner.query(`DROP INDEX "taskNumber"`); + await queryRunner.query(`DROP INDEX "IDX_1e1f64696aa3a26d3e12c840e5"`); + await queryRunner.query(`DROP INDEX "IDX_94fe6b3a5aec5f85427df4f8cd"`); + await queryRunner.query(`DROP INDEX "IDX_3797a20ef5553ae87af126bc2f"`); + await queryRunner.query(`DROP INDEX "IDX_5b0272d923a31c972bed1a1ac4"`); + await queryRunner.query(`DROP INDEX "IDX_e91cbff3d206f150ccc14d0c3a"`); + await queryRunner.query(`DROP INDEX "IDX_2fe7a278e6f08d2be55740a939"`); + await queryRunner.query(`DROP INDEX "IDX_f092f3386f10f2e2ef5b0b6ad1"`); + await queryRunner.query(`DROP INDEX "IDX_7127880d6fae956ecc1c84ac31"`); + await queryRunner.query(`DROP INDEX "IDX_ed5441fb13e82854a994da5a78"`); + await queryRunner.query(`DROP INDEX "IDX_0cbe714983eb0aae5feeee8212"`); + await queryRunner.query(`DROP INDEX "IDX_2f4bdd2593fd6038aaa91fd107"`); + await queryRunner.query(`DROP INDEX "IDX_b8616deefe44d0622233e73fbf"`); + await queryRunner.query( + `CREATE TABLE "temporary_task" ("id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "tenantId" varchar, "organizationId" varchar, "title" varchar NOT NULL, "description" varchar, "status" varchar, "estimate" integer, "dueDate" datetime, "projectId" varchar, "creatorId" varchar, "organizationSprintId" varchar, "number" integer, "prefix" varchar, "priority" varchar, "size" varchar, "public" boolean DEFAULT (1), "startDate" datetime, "resolvedAt" datetime, "version" varchar, "issueType" varchar, "parentId" varchar, "taskStatusId" varchar, "taskSizeId" varchar, "taskPriorityId" varchar, "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "deletedAt" datetime, "projectModuleId" varchar, "isDraft" boolean DEFAULT (0), "archivedAt" datetime, CONSTRAINT "FK_8c9920b5fb32c3d8453f64b705c" FOREIGN KEY ("parentId") REFERENCES "task" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_1e1f64696aa3a26d3e12c840e55" FOREIGN KEY ("organizationSprintId") REFERENCES "organization_sprint" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_94fe6b3a5aec5f85427df4f8cd7" FOREIGN KEY ("creatorId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_3797a20ef5553ae87af126bc2fe" FOREIGN KEY ("projectId") REFERENCES "organization_project" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_5b0272d923a31c972bed1a1ac4d" FOREIGN KEY ("organizationId") REFERENCES "organization" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_e91cbff3d206f150ccc14d0c3a1" FOREIGN KEY ("tenantId") REFERENCES "tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_0cbe714983eb0aae5feeee8212b" FOREIGN KEY ("taskStatusId") REFERENCES "task_status" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_2f4bdd2593fd6038aaa91fd1076" FOREIGN KEY ("taskSizeId") REFERENCES "task_size" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_b8616deefe44d0622233e73fbf9" FOREIGN KEY ("taskPriorityId") REFERENCES "task_priority" ("id") ON DELETE SET NULL ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "temporary_task"("id", "createdAt", "updatedAt", "tenantId", "organizationId", "title", "description", "status", "estimate", "dueDate", "projectId", "creatorId", "organizationSprintId", "number", "prefix", "priority", "size", "public", "startDate", "resolvedAt", "version", "issueType", "parentId", "taskStatusId", "taskSizeId", "taskPriorityId", "isActive", "isArchived", "deletedAt", "projectModuleId", "isDraft", "archivedAt") SELECT "id", "createdAt", "updatedAt", "tenantId", "organizationId", "title", "description", "status", "estimate", "dueDate", "projectId", "creatorId", "organizationSprintId", "number", "prefix", "priority", "size", "public", "startDate", "resolvedAt", "version", "issueType", "parentId", "taskStatusId", "taskSizeId", "taskPriorityId", "isActive", "isArchived", "deletedAt", "projectModuleId", "isDraft", "archivedAt" FROM "task"` + ); + await queryRunner.query(`DROP TABLE "task"`); + await queryRunner.query(`ALTER TABLE "temporary_task" RENAME TO "task"`); + await queryRunner.query(`CREATE INDEX "IDX_579534d8e12f22d308d6bd5f42" ON "task" ("projectModuleId") `); + await queryRunner.query(`CREATE INDEX "IDX_ca2f7edd5a5ce8f14b257c9d54" ON "task" ("isArchived") `); + await queryRunner.query(`CREATE INDEX "IDX_3e16c81005c389a4db83c0e5e3" ON "task" ("isActive") `); + await queryRunner.query(`CREATE UNIQUE INDEX "taskNumber" ON "task" ("projectId", "number") `); + await queryRunner.query(`CREATE INDEX "IDX_1e1f64696aa3a26d3e12c840e5" ON "task" ("organizationSprintId") `); + await queryRunner.query(`CREATE INDEX "IDX_94fe6b3a5aec5f85427df4f8cd" ON "task" ("creatorId") `); + await queryRunner.query(`CREATE INDEX "IDX_3797a20ef5553ae87af126bc2f" ON "task" ("projectId") `); + await queryRunner.query(`CREATE INDEX "IDX_5b0272d923a31c972bed1a1ac4" ON "task" ("organizationId") `); + await queryRunner.query(`CREATE INDEX "IDX_e91cbff3d206f150ccc14d0c3a" ON "task" ("tenantId") `); + await queryRunner.query(`CREATE INDEX "IDX_2fe7a278e6f08d2be55740a939" ON "task" ("status") `); + await queryRunner.query(`CREATE INDEX "IDX_f092f3386f10f2e2ef5b0b6ad1" ON "task" ("priority") `); + await queryRunner.query(`CREATE INDEX "IDX_7127880d6fae956ecc1c84ac31" ON "task" ("size") `); + await queryRunner.query(`CREATE INDEX "IDX_ed5441fb13e82854a994da5a78" ON "task" ("issueType") `); + await queryRunner.query(`CREATE INDEX "IDX_0cbe714983eb0aae5feeee8212" ON "task" ("taskStatusId") `); + await queryRunner.query(`CREATE INDEX "IDX_2f4bdd2593fd6038aaa91fd107" ON "task" ("taskSizeId") `); + await queryRunner.query(`CREATE INDEX "IDX_b8616deefe44d0622233e73fbf" ON "task" ("taskPriorityId") `); + await queryRunner.query(`DROP INDEX "IDX_579534d8e12f22d308d6bd5f42"`); + await queryRunner.query( + `CREATE TABLE "project_module_task" ("organizationProjectModuleId" varchar NOT NULL, "taskId" varchar NOT NULL, PRIMARY KEY ("organizationProjectModuleId", "taskId"))` + ); + await queryRunner.query( + `CREATE INDEX "IDX_58941b9cf23de12b2ecea4a959" ON "project_module_task" ("organizationProjectModuleId") ` + ); + await queryRunner.query(`CREATE INDEX "IDX_d056d5ba005e15ff92d4d7a8ca" ON "project_module_task" ("taskId") `); + await queryRunner.query(`DROP INDEX "IDX_ca2f7edd5a5ce8f14b257c9d54"`); + await queryRunner.query(`DROP INDEX "IDX_3e16c81005c389a4db83c0e5e3"`); + await queryRunner.query(`DROP INDEX "taskNumber"`); + await queryRunner.query(`DROP INDEX "IDX_1e1f64696aa3a26d3e12c840e5"`); + await queryRunner.query(`DROP INDEX "IDX_94fe6b3a5aec5f85427df4f8cd"`); + await queryRunner.query(`DROP INDEX "IDX_3797a20ef5553ae87af126bc2f"`); + await queryRunner.query(`DROP INDEX "IDX_5b0272d923a31c972bed1a1ac4"`); + await queryRunner.query(`DROP INDEX "IDX_e91cbff3d206f150ccc14d0c3a"`); + await queryRunner.query(`DROP INDEX "IDX_2fe7a278e6f08d2be55740a939"`); + await queryRunner.query(`DROP INDEX "IDX_f092f3386f10f2e2ef5b0b6ad1"`); + await queryRunner.query(`DROP INDEX "IDX_7127880d6fae956ecc1c84ac31"`); + await queryRunner.query(`DROP INDEX "IDX_ed5441fb13e82854a994da5a78"`); + await queryRunner.query(`DROP INDEX "IDX_0cbe714983eb0aae5feeee8212"`); + await queryRunner.query(`DROP INDEX "IDX_2f4bdd2593fd6038aaa91fd107"`); + await queryRunner.query(`DROP INDEX "IDX_b8616deefe44d0622233e73fbf"`); + await queryRunner.query( + `CREATE TABLE "temporary_task" ("id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "tenantId" varchar, "organizationId" varchar, "title" varchar NOT NULL, "description" varchar, "status" varchar, "estimate" integer, "dueDate" datetime, "projectId" varchar, "creatorId" varchar, "organizationSprintId" varchar, "number" integer, "prefix" varchar, "priority" varchar, "size" varchar, "public" boolean DEFAULT (1), "startDate" datetime, "resolvedAt" datetime, "version" varchar, "issueType" varchar, "parentId" varchar, "taskStatusId" varchar, "taskSizeId" varchar, "taskPriorityId" varchar, "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "deletedAt" datetime, "isDraft" boolean DEFAULT (0), "archivedAt" datetime, CONSTRAINT "FK_8c9920b5fb32c3d8453f64b705c" FOREIGN KEY ("parentId") REFERENCES "task" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_1e1f64696aa3a26d3e12c840e55" FOREIGN KEY ("organizationSprintId") REFERENCES "organization_sprint" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_94fe6b3a5aec5f85427df4f8cd7" FOREIGN KEY ("creatorId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_3797a20ef5553ae87af126bc2fe" FOREIGN KEY ("projectId") REFERENCES "organization_project" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_5b0272d923a31c972bed1a1ac4d" FOREIGN KEY ("organizationId") REFERENCES "organization" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_e91cbff3d206f150ccc14d0c3a1" FOREIGN KEY ("tenantId") REFERENCES "tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_0cbe714983eb0aae5feeee8212b" FOREIGN KEY ("taskStatusId") REFERENCES "task_status" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_2f4bdd2593fd6038aaa91fd1076" FOREIGN KEY ("taskSizeId") REFERENCES "task_size" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_b8616deefe44d0622233e73fbf9" FOREIGN KEY ("taskPriorityId") REFERENCES "task_priority" ("id") ON DELETE SET NULL ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "temporary_task"("id", "createdAt", "updatedAt", "tenantId", "organizationId", "title", "description", "status", "estimate", "dueDate", "projectId", "creatorId", "organizationSprintId", "number", "prefix", "priority", "size", "public", "startDate", "resolvedAt", "version", "issueType", "parentId", "taskStatusId", "taskSizeId", "taskPriorityId", "isActive", "isArchived", "deletedAt", "isDraft", "archivedAt") SELECT "id", "createdAt", "updatedAt", "tenantId", "organizationId", "title", "description", "status", "estimate", "dueDate", "projectId", "creatorId", "organizationSprintId", "number", "prefix", "priority", "size", "public", "startDate", "resolvedAt", "version", "issueType", "parentId", "taskStatusId", "taskSizeId", "taskPriorityId", "isActive", "isArchived", "deletedAt", "isDraft", "archivedAt" FROM "task"` + ); + await queryRunner.query(`DROP TABLE "task"`); + await queryRunner.query(`ALTER TABLE "temporary_task" RENAME TO "task"`); + await queryRunner.query(`CREATE INDEX "IDX_ca2f7edd5a5ce8f14b257c9d54" ON "task" ("isArchived") `); + await queryRunner.query(`CREATE INDEX "IDX_3e16c81005c389a4db83c0e5e3" ON "task" ("isActive") `); + await queryRunner.query(`CREATE UNIQUE INDEX "taskNumber" ON "task" ("projectId", "number") `); + await queryRunner.query(`CREATE INDEX "IDX_1e1f64696aa3a26d3e12c840e5" ON "task" ("organizationSprintId") `); + await queryRunner.query(`CREATE INDEX "IDX_94fe6b3a5aec5f85427df4f8cd" ON "task" ("creatorId") `); + await queryRunner.query(`CREATE INDEX "IDX_3797a20ef5553ae87af126bc2f" ON "task" ("projectId") `); + await queryRunner.query(`CREATE INDEX "IDX_5b0272d923a31c972bed1a1ac4" ON "task" ("organizationId") `); + await queryRunner.query(`CREATE INDEX "IDX_e91cbff3d206f150ccc14d0c3a" ON "task" ("tenantId") `); + await queryRunner.query(`CREATE INDEX "IDX_2fe7a278e6f08d2be55740a939" ON "task" ("status") `); + await queryRunner.query(`CREATE INDEX "IDX_f092f3386f10f2e2ef5b0b6ad1" ON "task" ("priority") `); + await queryRunner.query(`CREATE INDEX "IDX_7127880d6fae956ecc1c84ac31" ON "task" ("size") `); + await queryRunner.query(`CREATE INDEX "IDX_ed5441fb13e82854a994da5a78" ON "task" ("issueType") `); + await queryRunner.query(`CREATE INDEX "IDX_0cbe714983eb0aae5feeee8212" ON "task" ("taskStatusId") `); + await queryRunner.query(`CREATE INDEX "IDX_2f4bdd2593fd6038aaa91fd107" ON "task" ("taskSizeId") `); + await queryRunner.query(`CREATE INDEX "IDX_b8616deefe44d0622233e73fbf" ON "task" ("taskPriorityId") `); + await queryRunner.query(`DROP INDEX "IDX_58941b9cf23de12b2ecea4a959"`); + await queryRunner.query(`DROP INDEX "IDX_d056d5ba005e15ff92d4d7a8ca"`); + await queryRunner.query( + `CREATE TABLE "temporary_project_module_task" ("organizationProjectModuleId" varchar NOT NULL, "taskId" varchar NOT NULL, CONSTRAINT "FK_58941b9cf23de12b2ecea4a959f" FOREIGN KEY ("organizationProjectModuleId") REFERENCES "organization_project_module" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_d056d5ba005e15ff92d4d7a8ca5" FOREIGN KEY ("taskId") REFERENCES "task" ("id") ON DELETE CASCADE ON UPDATE CASCADE, PRIMARY KEY ("organizationProjectModuleId", "taskId"))` + ); + await queryRunner.query( + `INSERT INTO "temporary_project_module_task"("organizationProjectModuleId", "taskId") SELECT "organizationProjectModuleId", "taskId" FROM "project_module_task"` + ); + await queryRunner.query(`DROP TABLE "project_module_task"`); + await queryRunner.query(`ALTER TABLE "temporary_project_module_task" RENAME TO "project_module_task"`); + await queryRunner.query( + `CREATE INDEX "IDX_58941b9cf23de12b2ecea4a959" ON "project_module_task" ("organizationProjectModuleId") ` + ); + await queryRunner.query(`CREATE INDEX "IDX_d056d5ba005e15ff92d4d7a8ca" ON "project_module_task" ("taskId") `); + } /** * SqliteDB and BetterSQlite3DB Down Migration * * @param queryRunner */ - public async sqliteDownQueryRunner(queryRunner: QueryRunner): Promise {} + public async sqliteDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_d056d5ba005e15ff92d4d7a8ca"`); + await queryRunner.query(`DROP INDEX "IDX_58941b9cf23de12b2ecea4a959"`); + await queryRunner.query(`ALTER TABLE "project_module_task" RENAME TO "temporary_project_module_task"`); + await queryRunner.query( + `CREATE TABLE "project_module_task" ("organizationProjectModuleId" varchar NOT NULL, "taskId" varchar NOT NULL, PRIMARY KEY ("organizationProjectModuleId", "taskId"))` + ); + await queryRunner.query( + `INSERT INTO "project_module_task"("organizationProjectModuleId", "taskId") SELECT "organizationProjectModuleId", "taskId" FROM "temporary_project_module_task"` + ); + await queryRunner.query(`DROP TABLE "temporary_project_module_task"`); + await queryRunner.query(`CREATE INDEX "IDX_d056d5ba005e15ff92d4d7a8ca" ON "project_module_task" ("taskId") `); + await queryRunner.query( + `CREATE INDEX "IDX_58941b9cf23de12b2ecea4a959" ON "project_module_task" ("organizationProjectModuleId") ` + ); + await queryRunner.query(`DROP INDEX "IDX_b8616deefe44d0622233e73fbf"`); + await queryRunner.query(`DROP INDEX "IDX_2f4bdd2593fd6038aaa91fd107"`); + await queryRunner.query(`DROP INDEX "IDX_0cbe714983eb0aae5feeee8212"`); + await queryRunner.query(`DROP INDEX "IDX_ed5441fb13e82854a994da5a78"`); + await queryRunner.query(`DROP INDEX "IDX_7127880d6fae956ecc1c84ac31"`); + await queryRunner.query(`DROP INDEX "IDX_f092f3386f10f2e2ef5b0b6ad1"`); + await queryRunner.query(`DROP INDEX "IDX_2fe7a278e6f08d2be55740a939"`); + await queryRunner.query(`DROP INDEX "IDX_e91cbff3d206f150ccc14d0c3a"`); + await queryRunner.query(`DROP INDEX "IDX_5b0272d923a31c972bed1a1ac4"`); + await queryRunner.query(`DROP INDEX "IDX_3797a20ef5553ae87af126bc2f"`); + await queryRunner.query(`DROP INDEX "IDX_94fe6b3a5aec5f85427df4f8cd"`); + await queryRunner.query(`DROP INDEX "IDX_1e1f64696aa3a26d3e12c840e5"`); + await queryRunner.query(`DROP INDEX "taskNumber"`); + await queryRunner.query(`DROP INDEX "IDX_3e16c81005c389a4db83c0e5e3"`); + await queryRunner.query(`DROP INDEX "IDX_ca2f7edd5a5ce8f14b257c9d54"`); + await queryRunner.query(`ALTER TABLE "task" RENAME TO "temporary_task"`); + await queryRunner.query( + `CREATE TABLE "task" ("id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "tenantId" varchar, "organizationId" varchar, "title" varchar NOT NULL, "description" varchar, "status" varchar, "estimate" integer, "dueDate" datetime, "projectId" varchar, "creatorId" varchar, "organizationSprintId" varchar, "number" integer, "prefix" varchar, "priority" varchar, "size" varchar, "public" boolean DEFAULT (1), "startDate" datetime, "resolvedAt" datetime, "version" varchar, "issueType" varchar, "parentId" varchar, "taskStatusId" varchar, "taskSizeId" varchar, "taskPriorityId" varchar, "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "deletedAt" datetime, "projectModuleId" varchar, "isDraft" boolean DEFAULT (0), "archivedAt" datetime, CONSTRAINT "FK_8c9920b5fb32c3d8453f64b705c" FOREIGN KEY ("parentId") REFERENCES "task" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_1e1f64696aa3a26d3e12c840e55" FOREIGN KEY ("organizationSprintId") REFERENCES "organization_sprint" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_94fe6b3a5aec5f85427df4f8cd7" FOREIGN KEY ("creatorId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_3797a20ef5553ae87af126bc2fe" FOREIGN KEY ("projectId") REFERENCES "organization_project" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_5b0272d923a31c972bed1a1ac4d" FOREIGN KEY ("organizationId") REFERENCES "organization" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_e91cbff3d206f150ccc14d0c3a1" FOREIGN KEY ("tenantId") REFERENCES "tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_0cbe714983eb0aae5feeee8212b" FOREIGN KEY ("taskStatusId") REFERENCES "task_status" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_2f4bdd2593fd6038aaa91fd1076" FOREIGN KEY ("taskSizeId") REFERENCES "task_size" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_b8616deefe44d0622233e73fbf9" FOREIGN KEY ("taskPriorityId") REFERENCES "task_priority" ("id") ON DELETE SET NULL ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "task"("id", "createdAt", "updatedAt", "tenantId", "organizationId", "title", "description", "status", "estimate", "dueDate", "projectId", "creatorId", "organizationSprintId", "number", "prefix", "priority", "size", "public", "startDate", "resolvedAt", "version", "issueType", "parentId", "taskStatusId", "taskSizeId", "taskPriorityId", "isActive", "isArchived", "deletedAt", "isDraft", "archivedAt") SELECT "id", "createdAt", "updatedAt", "tenantId", "organizationId", "title", "description", "status", "estimate", "dueDate", "projectId", "creatorId", "organizationSprintId", "number", "prefix", "priority", "size", "public", "startDate", "resolvedAt", "version", "issueType", "parentId", "taskStatusId", "taskSizeId", "taskPriorityId", "isActive", "isArchived", "deletedAt", "isDraft", "archivedAt" FROM "temporary_task"` + ); + await queryRunner.query(`DROP TABLE "temporary_task"`); + await queryRunner.query(`CREATE INDEX "IDX_b8616deefe44d0622233e73fbf" ON "task" ("taskPriorityId") `); + await queryRunner.query(`CREATE INDEX "IDX_2f4bdd2593fd6038aaa91fd107" ON "task" ("taskSizeId") `); + await queryRunner.query(`CREATE INDEX "IDX_0cbe714983eb0aae5feeee8212" ON "task" ("taskStatusId") `); + await queryRunner.query(`CREATE INDEX "IDX_ed5441fb13e82854a994da5a78" ON "task" ("issueType") `); + await queryRunner.query(`CREATE INDEX "IDX_7127880d6fae956ecc1c84ac31" ON "task" ("size") `); + await queryRunner.query(`CREATE INDEX "IDX_f092f3386f10f2e2ef5b0b6ad1" ON "task" ("priority") `); + await queryRunner.query(`CREATE INDEX "IDX_2fe7a278e6f08d2be55740a939" ON "task" ("status") `); + await queryRunner.query(`CREATE INDEX "IDX_e91cbff3d206f150ccc14d0c3a" ON "task" ("tenantId") `); + await queryRunner.query(`CREATE INDEX "IDX_5b0272d923a31c972bed1a1ac4" ON "task" ("organizationId") `); + await queryRunner.query(`CREATE INDEX "IDX_3797a20ef5553ae87af126bc2f" ON "task" ("projectId") `); + await queryRunner.query(`CREATE INDEX "IDX_94fe6b3a5aec5f85427df4f8cd" ON "task" ("creatorId") `); + await queryRunner.query(`CREATE INDEX "IDX_1e1f64696aa3a26d3e12c840e5" ON "task" ("organizationSprintId") `); + await queryRunner.query(`CREATE UNIQUE INDEX "taskNumber" ON "task" ("projectId", "number") `); + await queryRunner.query(`CREATE INDEX "IDX_3e16c81005c389a4db83c0e5e3" ON "task" ("isActive") `); + await queryRunner.query(`CREATE INDEX "IDX_ca2f7edd5a5ce8f14b257c9d54" ON "task" ("isArchived") `); + await queryRunner.query(`DROP INDEX "IDX_d056d5ba005e15ff92d4d7a8ca"`); + await queryRunner.query(`DROP INDEX "IDX_58941b9cf23de12b2ecea4a959"`); + await queryRunner.query(`DROP TABLE "project_module_task"`); + await queryRunner.query(`CREATE INDEX "IDX_579534d8e12f22d308d6bd5f42" ON "task" ("projectModuleId") `); + await queryRunner.query(`DROP INDEX "IDX_b8616deefe44d0622233e73fbf"`); + await queryRunner.query(`DROP INDEX "IDX_2f4bdd2593fd6038aaa91fd107"`); + await queryRunner.query(`DROP INDEX "IDX_0cbe714983eb0aae5feeee8212"`); + await queryRunner.query(`DROP INDEX "IDX_ed5441fb13e82854a994da5a78"`); + await queryRunner.query(`DROP INDEX "IDX_7127880d6fae956ecc1c84ac31"`); + await queryRunner.query(`DROP INDEX "IDX_f092f3386f10f2e2ef5b0b6ad1"`); + await queryRunner.query(`DROP INDEX "IDX_2fe7a278e6f08d2be55740a939"`); + await queryRunner.query(`DROP INDEX "IDX_e91cbff3d206f150ccc14d0c3a"`); + await queryRunner.query(`DROP INDEX "IDX_5b0272d923a31c972bed1a1ac4"`); + await queryRunner.query(`DROP INDEX "IDX_3797a20ef5553ae87af126bc2f"`); + await queryRunner.query(`DROP INDEX "IDX_94fe6b3a5aec5f85427df4f8cd"`); + await queryRunner.query(`DROP INDEX "IDX_1e1f64696aa3a26d3e12c840e5"`); + await queryRunner.query(`DROP INDEX "taskNumber"`); + await queryRunner.query(`DROP INDEX "IDX_3e16c81005c389a4db83c0e5e3"`); + await queryRunner.query(`DROP INDEX "IDX_ca2f7edd5a5ce8f14b257c9d54"`); + await queryRunner.query(`DROP INDEX "IDX_579534d8e12f22d308d6bd5f42"`); + await queryRunner.query(`ALTER TABLE "task" RENAME TO "temporary_task"`); + await queryRunner.query( + `CREATE TABLE "task" ("id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "tenantId" varchar, "organizationId" varchar, "title" varchar NOT NULL, "description" varchar, "status" varchar, "estimate" integer, "dueDate" datetime, "projectId" varchar, "creatorId" varchar, "organizationSprintId" varchar, "number" integer, "prefix" varchar, "priority" varchar, "size" varchar, "public" boolean DEFAULT (1), "startDate" datetime, "resolvedAt" datetime, "version" varchar, "issueType" varchar, "parentId" varchar, "taskStatusId" varchar, "taskSizeId" varchar, "taskPriorityId" varchar, "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "deletedAt" datetime, "projectModuleId" varchar, "isDraft" boolean DEFAULT (0), "archivedAt" datetime, CONSTRAINT "FK_579534d8e12f22d308d6bd5f428" FOREIGN KEY ("projectModuleId") REFERENCES "organization_project_module" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_8c9920b5fb32c3d8453f64b705c" FOREIGN KEY ("parentId") REFERENCES "task" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_1e1f64696aa3a26d3e12c840e55" FOREIGN KEY ("organizationSprintId") REFERENCES "organization_sprint" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_94fe6b3a5aec5f85427df4f8cd7" FOREIGN KEY ("creatorId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_3797a20ef5553ae87af126bc2fe" FOREIGN KEY ("projectId") REFERENCES "organization_project" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_5b0272d923a31c972bed1a1ac4d" FOREIGN KEY ("organizationId") REFERENCES "organization" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_e91cbff3d206f150ccc14d0c3a1" FOREIGN KEY ("tenantId") REFERENCES "tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_0cbe714983eb0aae5feeee8212b" FOREIGN KEY ("taskStatusId") REFERENCES "task_status" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_2f4bdd2593fd6038aaa91fd1076" FOREIGN KEY ("taskSizeId") REFERENCES "task_size" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_b8616deefe44d0622233e73fbf9" FOREIGN KEY ("taskPriorityId") REFERENCES "task_priority" ("id") ON DELETE SET NULL ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "task"("id", "createdAt", "updatedAt", "tenantId", "organizationId", "title", "description", "status", "estimate", "dueDate", "projectId", "creatorId", "organizationSprintId", "number", "prefix", "priority", "size", "public", "startDate", "resolvedAt", "version", "issueType", "parentId", "taskStatusId", "taskSizeId", "taskPriorityId", "isActive", "isArchived", "deletedAt", "projectModuleId", "isDraft", "archivedAt") SELECT "id", "createdAt", "updatedAt", "tenantId", "organizationId", "title", "description", "status", "estimate", "dueDate", "projectId", "creatorId", "organizationSprintId", "number", "prefix", "priority", "size", "public", "startDate", "resolvedAt", "version", "issueType", "parentId", "taskStatusId", "taskSizeId", "taskPriorityId", "isActive", "isArchived", "deletedAt", "projectModuleId", "isDraft", "archivedAt" FROM "temporary_task"` + ); + await queryRunner.query(`DROP TABLE "temporary_task"`); + await queryRunner.query(`CREATE INDEX "IDX_b8616deefe44d0622233e73fbf" ON "task" ("taskPriorityId") `); + await queryRunner.query(`CREATE INDEX "IDX_2f4bdd2593fd6038aaa91fd107" ON "task" ("taskSizeId") `); + await queryRunner.query(`CREATE INDEX "IDX_0cbe714983eb0aae5feeee8212" ON "task" ("taskStatusId") `); + await queryRunner.query(`CREATE INDEX "IDX_ed5441fb13e82854a994da5a78" ON "task" ("issueType") `); + await queryRunner.query(`CREATE INDEX "IDX_7127880d6fae956ecc1c84ac31" ON "task" ("size") `); + await queryRunner.query(`CREATE INDEX "IDX_f092f3386f10f2e2ef5b0b6ad1" ON "task" ("priority") `); + await queryRunner.query(`CREATE INDEX "IDX_2fe7a278e6f08d2be55740a939" ON "task" ("status") `); + await queryRunner.query(`CREATE INDEX "IDX_e91cbff3d206f150ccc14d0c3a" ON "task" ("tenantId") `); + await queryRunner.query(`CREATE INDEX "IDX_5b0272d923a31c972bed1a1ac4" ON "task" ("organizationId") `); + await queryRunner.query(`CREATE INDEX "IDX_3797a20ef5553ae87af126bc2f" ON "task" ("projectId") `); + await queryRunner.query(`CREATE INDEX "IDX_94fe6b3a5aec5f85427df4f8cd" ON "task" ("creatorId") `); + await queryRunner.query(`CREATE INDEX "IDX_1e1f64696aa3a26d3e12c840e5" ON "task" ("organizationSprintId") `); + await queryRunner.query(`CREATE UNIQUE INDEX "taskNumber" ON "task" ("projectId", "number") `); + await queryRunner.query(`CREATE INDEX "IDX_3e16c81005c389a4db83c0e5e3" ON "task" ("isActive") `); + await queryRunner.query(`CREATE INDEX "IDX_ca2f7edd5a5ce8f14b257c9d54" ON "task" ("isArchived") `); + await queryRunner.query(`CREATE INDEX "IDX_579534d8e12f22d308d6bd5f42" ON "task" ("projectModuleId") `); + } /** * MySQL Up Migration * * @param queryRunner */ - public async mysqlUpQueryRunner(queryRunner: QueryRunner): Promise {} + public async mysqlUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`task\` DROP FOREIGN KEY \`FK_579534d8e12f22d308d6bd5f428\``); + await queryRunner.query(`DROP INDEX \`IDX_579534d8e12f22d308d6bd5f42\` ON \`task\``); + await queryRunner.query( + `CREATE TABLE \`project_module_task\` (\`organizationProjectModuleId\` varchar(36) NOT NULL, \`taskId\` varchar(36) NOT NULL, INDEX \`IDX_58941b9cf23de12b2ecea4a959\` (\`organizationProjectModuleId\`), INDEX \`IDX_d056d5ba005e15ff92d4d7a8ca\` (\`taskId\`), PRIMARY KEY (\`organizationProjectModuleId\`, \`taskId\`)) ENGINE=InnoDB` + ); + await queryRunner.query(`ALTER TABLE \`task\` DROP COLUMN \`projectModuleId\``); + await queryRunner.query( + `ALTER TABLE \`project_module_task\` ADD CONSTRAINT \`FK_58941b9cf23de12b2ecea4a959f\` FOREIGN KEY (\`organizationProjectModuleId\`) REFERENCES \`organization_project_module\`(\`id\`) ON DELETE CASCADE ON UPDATE CASCADE` + ); + await queryRunner.query( + `ALTER TABLE \`project_module_task\` ADD CONSTRAINT \`FK_d056d5ba005e15ff92d4d7a8ca5\` FOREIGN KEY (\`taskId\`) REFERENCES \`task\`(\`id\`) ON DELETE CASCADE ON UPDATE CASCADE` + ); + } /** * MySQL Down Migration * * @param queryRunner */ - public async mysqlDownQueryRunner(queryRunner: QueryRunner): Promise {} + public async mysqlDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE \`project_module_task\` DROP FOREIGN KEY \`FK_d056d5ba005e15ff92d4d7a8ca5\`` + ); + await queryRunner.query( + `ALTER TABLE \`project_module_task\` DROP FOREIGN KEY \`FK_58941b9cf23de12b2ecea4a959f\`` + ); + await queryRunner.query(`ALTER TABLE \`task\` ADD \`projectModuleId\` varchar(255) NULL`); + await queryRunner.query(`DROP INDEX \`IDX_d056d5ba005e15ff92d4d7a8ca\` ON \`project_module_task\``); + await queryRunner.query(`DROP INDEX \`IDX_58941b9cf23de12b2ecea4a959\` ON \`project_module_task\``); + await queryRunner.query(`DROP TABLE \`project_module_task\``); + await queryRunner.query(`CREATE INDEX \`IDX_579534d8e12f22d308d6bd5f42\` ON \`task\` (\`projectModuleId\`)`); + await queryRunner.query( + `ALTER TABLE \`task\` ADD CONSTRAINT \`FK_579534d8e12f22d308d6bd5f428\` FOREIGN KEY (\`projectModuleId\`) REFERENCES \`organization_project_module\`(\`id\`) ON DELETE CASCADE ON UPDATE NO ACTION` + ); + } } From ca44748298e1627cb6fd3f9e10b1f28a04773827 Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Tue, 24 Sep 2024 09:28:49 +0200 Subject: [PATCH 05/28] fix: project module status --- .../src/organization-project-module.model.ts | 12 ++++++++++-- .../organization-project-module.entity.ts | 6 +++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/contracts/src/organization-project-module.model.ts b/packages/contracts/src/organization-project-module.model.ts index 9a4fe67be0f..71888a437df 100644 --- a/packages/contracts/src/organization-project-module.model.ts +++ b/packages/contracts/src/organization-project-module.model.ts @@ -3,7 +3,6 @@ import { IEmployee } from './employee.model'; import { IRelationalOrganizationProject } from './organization-projects.model'; import { IOrganizationSprint } from './organization-sprint.model'; import { IOrganizationTeam } from './organization-team.model'; -import { TaskStatusEnum } from './task-status.model'; import { ITask } from './task.model'; import { IUser } from './user.model'; @@ -17,7 +16,7 @@ export interface IOrganizationProjectModule IRelationalOrganizationProject { name: string; description?: string; - status?: TaskStatusEnum; + status?: ProjectModuleStatusEnum; startDate?: Date; endDate?: Date; isFavorite?: boolean; @@ -42,6 +41,15 @@ export interface IOrganizationProjectModuleFindInput organizationSprintId?: ID; } +export enum ProjectModuleStatusEnum { + BACKLOG = 'backlog', + PLANNED = 'planned', + IN_PROGRESS = 'in-progress', + PAUSED = 'paused', + COMPLETED = 'completed', + CANCELLED = 'cancelled' +} + export interface IOrganizationProjectModuleCreateInput extends Omit {} export interface IOrganizationProjectModuleUpdateInput extends Partial {} diff --git a/packages/core/src/organization-project-module/organization-project-module.entity.ts b/packages/core/src/organization-project-module/organization-project-module.entity.ts index 7401f0fe07d..9cbccb63802 100644 --- a/packages/core/src/organization-project-module/organization-project-module.entity.ts +++ b/packages/core/src/organization-project-module/organization-project-module.entity.ts @@ -21,7 +21,7 @@ import { IOrganizationTeam, ITask, IUser, - TaskStatusEnum + ProjectModuleStatusEnum } from '@gauzy/contracts'; import { Employee, @@ -62,10 +62,10 @@ export class OrganizationProjectModule extends TenantOrganizationBaseEntity impl @ApiPropertyOptional({ type: () => String }) @IsOptional() - @IsEnum(TaskStatusEnum) + @IsEnum(ProjectModuleStatusEnum) @ColumnIndex() @MultiORMColumn({ nullable: true }) - status?: TaskStatusEnum; + status?: ProjectModuleStatusEnum; @ApiPropertyOptional({ type: () => Date }) @IsOptional() From 56524a92b093e876c0d95bd94ddac26be150055c Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Tue, 24 Sep 2024 10:47:35 +0200 Subject: [PATCH 06/28] feat: find module tasks API --- .../organization-project-module.service.ts | 8 +- packages/core/src/tasks/task.controller.ts | 23 ++++ packages/core/src/tasks/task.service.ts | 108 ++++++++++++++++++ 3 files changed, 135 insertions(+), 4 deletions(-) diff --git a/packages/core/src/organization-project-module/organization-project-module.service.ts b/packages/core/src/organization-project-module/organization-project-module.service.ts index edeb0064a83..21382948c38 100644 --- a/packages/core/src/organization-project-module/organization-project-module.service.ts +++ b/packages/core/src/organization-project-module/organization-project-module.service.ts @@ -6,7 +6,7 @@ import { IOrganizationProjectModuleFindInput, IPagination, PermissionsEnum, - TaskStatusEnum + ProjectModuleStatusEnum } from '@gauzy/contracts'; import { isEmpty, isNotEmpty } from '@gauzy/common'; import { isPostgres } from '@gauzy/config'; @@ -82,7 +82,7 @@ export class OrganizationProjectModuleService extends TenantAwareCrudService { // Apply optional filters const filters: IOrganizationProjectModuleFindInput = { - status: status as TaskStatusEnum, + status: status as ProjectModuleStatusEnum, projectId: projectId as ID, name: name as string }; @@ -176,7 +176,7 @@ export class OrganizationProjectModuleService extends TenantAwareCrudService(query); diff --git a/packages/core/src/tasks/task.controller.ts b/packages/core/src/tasks/task.controller.ts index 03414b0bd62..01f8d54c953 100644 --- a/packages/core/src/tasks/task.controller.ts +++ b/packages/core/src/tasks/task.controller.ts @@ -152,6 +152,29 @@ export class TaskController extends CrudController { return await this.taskService.findTeamTasks(params); } + /** + * GET module tasks + * + * @param params + * @returns + */ + @ApiOperation({ summary: 'Find module tasks.' }) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Found tasks', + type: Task + }) + @ApiResponse({ + status: HttpStatus.NOT_FOUND, + description: 'Records not found' + }) + @Permissions(PermissionsEnum.ALL_ORG_VIEW, PermissionsEnum.ORG_TASK_VIEW) + @Get('module') + @UseValidationPipe({ transform: true }) + async findModuleTasks(@Query() params: PaginationParams): Promise> { + return await this.taskService.findModuleTasks(params); + } + @ApiOperation({ summary: 'Find by id' }) @ApiResponse({ status: HttpStatus.OK, diff --git a/packages/core/src/tasks/task.service.ts b/packages/core/src/tasks/task.service.ts index 0d6a3ac492d..3d5093e2c12 100644 --- a/packages/core/src/tasks/task.service.ts +++ b/packages/core/src/tasks/task.service.ts @@ -502,4 +502,112 @@ export class TaskService extends TenantAwareCrudService { throw new BadRequestException(error); } } + + /** + * GET module tasks + * + * @param options + * @returns A promise that resolves with pagination task items and total + */ + async findModuleTasks(options: PaginationParams): Promise> { + try { + const { where } = options; + + const { status, modules = [], title, prefix, isDraft, organizationSprintId = null } = where; + const { organizationId, projectId, members } = where; + const likeOperator = isPostgres() ? 'ILIKE' : 'LIKE'; + + const query = this.typeOrmRepository.createQueryBuilder(this.tableName); + query.leftJoin(`${query.alias}.modules`, 'modules'); + + /** + * Find options + */ + if (isNotEmpty(options)) { + query.setFindOptions({ + ...(options.select ? { select: options.select } : {}), + ...(options.relations ? { relations: options.relations } : {}), + ...(options.order ? { order: options.order } : {}) + }); + } + + // Filter options + query.andWhere((qb: SelectQueryBuilder) => { + const subQuery = qb.subQuery(); + subQuery + .select(p('"project_module_task"."taskId"')) + .from(p('project_module_task'), p('project_module_task')); + subQuery.leftJoin( + 'project_module_employee', + 'project_module_employee', + p( + '"project_module_employee"."organizationProjectModuleId" = "project_module_task"."organizationProjectModuleId"' + ) + ); + // If user have permission to change employee + if (RequestContext.hasPermission(PermissionsEnum.CHANGE_SELECTED_EMPLOYEE)) { + if (isNotEmpty(members) && isNotEmpty(members['id'])) { + const employeeId = members['id']; + subQuery.andWhere(p('"project_module_employee"."employeeId" = :employeeId'), { employeeId }); + } + } else { + // If employee has login and don't have permission to change employee + const employeeId = RequestContext.currentEmployeeId(); + if (isNotEmpty(employeeId)) { + subQuery.andWhere(p('"project_module_employee"."employeeId" = :employeeId'), { employeeId }); + } + } + if (isNotEmpty(modules)) { + subQuery.andWhere(p(`"${subQuery.alias}"."organizationProjectModuleId" IN (:...modules)`), { + modules + }); + } + return p(`"project_module_tasks"."taskId" IN `) + subQuery.distinct(true).getQuery(); + }); + query.andWhere( + new Brackets((qb: WhereExpressionBuilder) => { + const tenantId = RequestContext.currentTenantId(); + qb.andWhere(p(`"${query.alias}"."organizationId" = :organizationId`), { organizationId }); + qb.andWhere(p(`"${query.alias}"."tenantId" = :tenantId`), { tenantId }); + }) + ); + if (isNotEmpty(projectId) && isNotEmpty(modules)) { + query.orWhere(p(`"${query.alias}"."projectId" = :projectId`), { projectId }); + } + query.andWhere( + new Brackets((qb: WhereExpressionBuilder) => { + if (isNotEmpty(projectId) && isEmpty(modules)) { + qb.andWhere(p(`"${query.alias}"."projectId" = :projectId`), { projectId }); + } + if (isNotEmpty(status)) { + qb.andWhere(p(`"${query.alias}"."status" = :status`), { + status + }); + } + if (isNotEmpty(isDraft)) { + qb.andWhere(p(`"${query.alias}"."isDraft" = :isDraft`), { + isDraft + }); + } + if (isNotEmpty(title)) { + qb.andWhere(p(`"${query.alias}"."title" ${likeOperator} :title`), { + title: `%${title}%` + }); + } + if (isNotEmpty(title)) { + qb.andWhere(p(`"${query.alias}"."prefix" ${likeOperator} :prefix`), { + prefix: `%${prefix}%` + }); + } + if (isNotEmpty(organizationSprintId) && !isUUID(organizationSprintId)) { + qb.andWhere(p(`"${query.alias}"."organizationSprintId" IS NULL`)); + } + }) + ); + const [items, total] = await query.getManyAndCount(); + return { items, total }; + } catch (error) { + throw new BadRequestException(error); + } + } } From 6ac3f039f180ac5b6dbec7f1fa4823d29dad391d Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Wed, 25 Sep 2024 08:12:29 +0200 Subject: [PATCH 07/28] fix: build import issue --- packages/contracts/src/task.model.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts/src/task.model.ts b/packages/contracts/src/task.model.ts index 2d8a5c25d16..b8bb62e2b48 100644 --- a/packages/contracts/src/task.model.ts +++ b/packages/contracts/src/task.model.ts @@ -9,7 +9,7 @@ import { IUser } from './user.model'; import { ITaskStatus, TaskStatusEnum } from './task-status.model'; import { ITaskPriority, TaskPriorityEnum } from './task-priority.model'; import { ITaskSize, TaskSizeEnum } from './task-size.model'; -import { IOrganizationProjectModule } from 'organization-project-module.model'; +import { IOrganizationProjectModule } from './organization-project-module.model'; export interface ITask extends IBasePerTenantAndOrganizationEntityModel, IRelationalOrganizationProject { title: string; From 3caee38cc8ea6ae4f695c6c44257ec4afe9ae1fe Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Thu, 26 Sep 2024 07:49:38 +0200 Subject: [PATCH 08/28] fix: project members IDs on creation --- packages/contracts/src/organization-team.model.ts | 4 ++-- .../src/organization-project/organization-project.service.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/contracts/src/organization-team.model.ts b/packages/contracts/src/organization-team.model.ts index 2c60250a184..1d49272cc4d 100644 --- a/packages/contracts/src/organization-team.model.ts +++ b/packages/contracts/src/organization-team.model.ts @@ -49,8 +49,8 @@ export interface IOrganizationTeamCreateInput extends IBasePerTenantAndOrganizat requirePlanToTrack?: boolean; public?: boolean; profile_link?: string; - memberIds?: string[]; - managerIds?: string[]; + memberIds?: ID[]; + managerIds?: ID[]; tags?: ITag[]; projects?: IOrganizationProject[]; } diff --git a/packages/core/src/organization-project/organization-project.service.ts b/packages/core/src/organization-project/organization-project.service.ts index c9005e4d565..ce9758b9cc0 100644 --- a/packages/core/src/organization-project/organization-project.service.ts +++ b/packages/core/src/organization-project/organization-project.service.ts @@ -95,8 +95,8 @@ export class OrganizationProjectService extends TenantAwareCrudService manager.id); - const memberIds = members.map((member) => member.id); + const managerIds = managers.map((manager) => manager.employeeId); + const memberIds = members.map((member) => member.employeeId); // If the employee creates the project, default add as a manager try { From 5f61b9b47d349a28057a43cdc6d6b4be69dba0ae Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Thu, 26 Sep 2024 07:53:01 +0200 Subject: [PATCH 09/28] fix: project members IDs on updating --- .../src/organization-project/organization-project.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/organization-project/organization-project.service.ts b/packages/core/src/organization-project/organization-project.service.ts index ce9758b9cc0..28202bdb315 100644 --- a/packages/core/src/organization-project/organization-project.service.ts +++ b/packages/core/src/organization-project/organization-project.service.ts @@ -229,8 +229,8 @@ export class OrganizationProjectService extends TenantAwareCrudService manager.id); - const memberIds = members.map((member) => member.id); + const managerIds = managers.map((manager) => manager.employeeId); + const memberIds = members.map((member) => member.employeeId); if (isNotEmpty(memberIds) || isNotEmpty(managerIds)) { // Find the manager role const role = await this._roleService.findOneByWhereOptions({ From d6930df88a0389ce874727acad8a937aecc98a14 Mon Sep 17 00:00:00 2001 From: Kifungo A <45813955+adkif@users.noreply.github.com> Date: Thu, 26 Sep 2024 09:43:35 +0200 Subject: [PATCH 10/28] feat: add name filtering to project --- .../organization-project/organization-project.service.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/core/src/organization-project/organization-project.service.ts b/packages/core/src/organization-project/organization-project.service.ts index 28202bdb315..8ef69cfc508 100644 --- a/packages/core/src/organization-project/organization-project.service.ts +++ b/packages/core/src/organization-project/organization-project.service.ts @@ -1,6 +1,6 @@ import { BadRequestException, ForbiddenException, Injectable } from '@nestjs/common'; import { EventBus } from '@nestjs/cqrs'; -import { In, IsNull, SelectQueryBuilder } from 'typeorm'; +import { ILike, In, IsNull, SelectQueryBuilder } from 'typeorm'; import { ActionTypeEnum, ActivityLogEntityEnum, @@ -423,6 +423,10 @@ export class OrganizationProjectService extends TenantAwareCrudService Date: Thu, 26 Sep 2024 10:37:37 +0200 Subject: [PATCH 11/28] fix: creator class validator --- .../organization-project-module.entity.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/core/src/organization-project-module/organization-project-module.entity.ts b/packages/core/src/organization-project-module/organization-project-module.entity.ts index 9cbccb63802..3a282cbcc97 100644 --- a/packages/core/src/organization-project-module/organization-project-module.entity.ts +++ b/packages/core/src/organization-project-module/organization-project-module.entity.ts @@ -139,9 +139,6 @@ export class OrganizationProjectModule extends TenantOrganizationBaseEntity impl /** * Creator */ - @ApiPropertyOptional({ type: () => Object }) - @IsOptional() - @IsObject() @MultiORMManyToOne(() => User, { nullable: true, onDelete: 'CASCADE' @@ -149,9 +146,6 @@ export class OrganizationProjectModule extends TenantOrganizationBaseEntity impl @JoinColumn() creator?: IUser; - @ApiPropertyOptional({ type: () => String }) - @IsOptional() - @IsUUID() @RelationId((it: OrganizationProjectModule) => it.creator) @ColumnIndex() @MultiORMColumn({ nullable: true, relationId: true }) From eb0f487cdbd3d0f2af8e467696f113b1c67a2ff3 Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Thu, 26 Sep 2024 10:47:09 +0200 Subject: [PATCH 12/28] feat: override project module creation --- .../organization-project-module.service.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/core/src/organization-project-module/organization-project-module.service.ts b/packages/core/src/organization-project-module/organization-project-module.service.ts index 21382948c38..9f0fe19e327 100644 --- a/packages/core/src/organization-project-module/organization-project-module.service.ts +++ b/packages/core/src/organization-project-module/organization-project-module.service.ts @@ -1,8 +1,9 @@ -import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { BadRequestException, HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { Brackets, FindManyOptions, SelectQueryBuilder, WhereExpressionBuilder } from 'typeorm'; import { ID, IOrganizationProjectModule, + IOrganizationProjectModuleCreateInput, IOrganizationProjectModuleFindInput, IPagination, PermissionsEnum, @@ -26,6 +27,18 @@ export class OrganizationProjectModuleService extends TenantAwareCrudService { + try { + const creatorId = RequestContext.currentUserId(); + return super.create({ + ...entity, + creatorId + }); + } catch (error) { + throw new BadRequestException(error); + } + } + /** * @description Find employee project modules * @param options - Options finders and relations From 98432419ba5cb5580008a6c9d616762236b24490 Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Thu, 26 Sep 2024 10:49:49 +0200 Subject: [PATCH 13/28] fix: duplicate condition filters --- packages/core/src/tasks/task.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/tasks/task.service.ts b/packages/core/src/tasks/task.service.ts index 3d5093e2c12..fda90473746 100644 --- a/packages/core/src/tasks/task.service.ts +++ b/packages/core/src/tasks/task.service.ts @@ -594,7 +594,7 @@ export class TaskService extends TenantAwareCrudService { title: `%${title}%` }); } - if (isNotEmpty(title)) { + if (isNotEmpty(prefix)) { qb.andWhere(p(`"${query.alias}"."prefix" ${likeOperator} :prefix`), { prefix: `%${prefix}%` }); From 263d31e2372e1b6b92a429881d3cb322e72dacf8 Mon Sep 17 00:00:00 2001 From: "Rahul R." Date: Thu, 26 Sep 2024 14:31:30 +0530 Subject: [PATCH 14/28] fix: update condition and optimize code --- packages/core/src/tasks/task.service.ts | 114 +++++++++++++----------- 1 file changed, 60 insertions(+), 54 deletions(-) diff --git a/packages/core/src/tasks/task.service.ts b/packages/core/src/tasks/task.service.ts index fda90473746..d76709286ba 100644 --- a/packages/core/src/tasks/task.service.ts +++ b/packages/core/src/tasks/task.service.ts @@ -504,58 +504,62 @@ export class TaskService extends TenantAwareCrudService { } /** - * GET module tasks + * Retrieves module tasks based on the provided options. * - * @param options - * @returns A promise that resolves with pagination task items and total + * @param {PaginationParams} options - The pagination options and filters for querying tasks. + * @returns {Promise>} A promise that resolves with pagination task items and total count. */ async findModuleTasks(options: PaginationParams): Promise> { try { const { where } = options; - - const { status, modules = [], title, prefix, isDraft, organizationSprintId = null } = where; - const { organizationId, projectId, members } = where; + const { + status, + modules = [], + title, + prefix, + isDraft, + organizationSprintId = null, + organizationId, + projectId, + members + } = where; + const tenantId = RequestContext.currentTenantId() || where?.tenantId; const likeOperator = isPostgres() ? 'ILIKE' : 'LIKE'; + // Initialize the query const query = this.typeOrmRepository.createQueryBuilder(this.tableName); query.leftJoin(`${query.alias}.modules`, 'modules'); - /** - * Find options - */ + // Apply find options if provided if (isNotEmpty(options)) { query.setFindOptions({ - ...(options.select ? { select: options.select } : {}), - ...(options.relations ? { relations: options.relations } : {}), - ...(options.order ? { order: options.order } : {}) + ...(options.select && { select: options.select }), + ...(options.relations && { relations: options.relations }), + ...(options.order && { order: options.order }) }); } - // Filter options + // Filter by project_module_task with a subquery query.andWhere((qb: SelectQueryBuilder) => { - const subQuery = qb.subQuery(); - subQuery + const subQuery = qb + .subQuery() .select(p('"project_module_task"."taskId"')) - .from(p('project_module_task'), p('project_module_task')); - subQuery.leftJoin( - 'project_module_employee', - 'project_module_employee', - p( - '"project_module_employee"."organizationProjectModuleId" = "project_module_task"."organizationProjectModuleId"' - ) - ); - // If user have permission to change employee - if (RequestContext.hasPermission(PermissionsEnum.CHANGE_SELECTED_EMPLOYEE)) { - if (isNotEmpty(members) && isNotEmpty(members['id'])) { - const employeeId = members['id']; - subQuery.andWhere(p('"project_module_employee"."employeeId" = :employeeId'), { employeeId }); - } - } else { - // If employee has login and don't have permission to change employee - const employeeId = RequestContext.currentEmployeeId(); - if (isNotEmpty(employeeId)) { - subQuery.andWhere(p('"project_module_employee"."employeeId" = :employeeId'), { employeeId }); - } + .from(p('project_module_task'), p('project_module_task')) + .leftJoin( + 'project_module_employee', + 'project_module_employee', + p( + '"project_module_employee"."organizationProjectModuleId" = "project_module_task"."organizationProjectModuleId"' + ) + ); + + // Retrieve the employee ID based on the permission + const employeeId = RequestContext.hasPermission(PermissionsEnum.CHANGE_SELECTED_EMPLOYEE) + ? members?.id + : RequestContext.currentEmployeeId(); + + if (isNotEmpty(employeeId)) { + subQuery.andWhere(p('"project_module_employee"."employeeId" = :employeeId'), { employeeId }); } if (isNotEmpty(modules)) { subQuery.andWhere(p(`"${subQuery.alias}"."organizationProjectModuleId" IN (:...modules)`), { @@ -564,49 +568,51 @@ export class TaskService extends TenantAwareCrudService { } return p(`"project_module_tasks"."taskId" IN `) + subQuery.distinct(true).getQuery(); }); + + // Add organization and tenant filters query.andWhere( new Brackets((qb: WhereExpressionBuilder) => { - const tenantId = RequestContext.currentTenantId(); qb.andWhere(p(`"${query.alias}"."organizationId" = :organizationId`), { organizationId }); qb.andWhere(p(`"${query.alias}"."tenantId" = :tenantId`), { tenantId }); }) ); - if (isNotEmpty(projectId) && isNotEmpty(modules)) { - query.orWhere(p(`"${query.alias}"."projectId" = :projectId`), { projectId }); + + // Filter by projectId and modules + if (isNotEmpty(projectId)) { + query.andWhere( + new Brackets((qb: WhereExpressionBuilder) => { + if (isEmpty(modules)) { + qb.andWhere(p(`"${query.alias}"."projectId" = :projectId`), { projectId }); + } + }) + ); } + + // Add additional filters (status, draft, title, etc.) query.andWhere( new Brackets((qb: WhereExpressionBuilder) => { - if (isNotEmpty(projectId) && isEmpty(modules)) { - qb.andWhere(p(`"${query.alias}"."projectId" = :projectId`), { projectId }); - } if (isNotEmpty(status)) { - qb.andWhere(p(`"${query.alias}"."status" = :status`), { - status - }); + qb.andWhere(p(`"${query.alias}"."status" = :status`), { status }); } if (isNotEmpty(isDraft)) { - qb.andWhere(p(`"${query.alias}"."isDraft" = :isDraft`), { - isDraft - }); + qb.andWhere(p(`"${query.alias}"."isDraft" = :isDraft`), { isDraft }); } if (isNotEmpty(title)) { - qb.andWhere(p(`"${query.alias}"."title" ${likeOperator} :title`), { - title: `%${title}%` - }); + qb.andWhere(p(`"${query.alias}"."title" ${likeOperator} :title`), { title: `%${title}%` }); } if (isNotEmpty(prefix)) { - qb.andWhere(p(`"${query.alias}"."prefix" ${likeOperator} :prefix`), { - prefix: `%${prefix}%` - }); + qb.andWhere(p(`"${query.alias}"."prefix" ${likeOperator} :prefix`), { prefix: `%${prefix}%` }); } - if (isNotEmpty(organizationSprintId) && !isUUID(organizationSprintId)) { + if (!isUUID(organizationSprintId)) { qb.andWhere(p(`"${query.alias}"."organizationSprintId" IS NULL`)); } }) ); + const [items, total] = await query.getManyAndCount(); return { items, total }; } catch (error) { + console.log('Error while retrieving module tasks', error); throw new BadRequestException(error); } } From bb46ef49625c196da10b0cffe2adc72e08a0b225 Mon Sep 17 00:00:00 2001 From: "Rahul R." Date: Thu, 26 Sep 2024 14:54:48 +0530 Subject: [PATCH 15/28] fix: packages build --- packages/core/src/tasks/task.service.ts | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/packages/core/src/tasks/task.service.ts b/packages/core/src/tasks/task.service.ts index d76709286ba..8fedfe19feb 100644 --- a/packages/core/src/tasks/task.service.ts +++ b/packages/core/src/tasks/task.service.ts @@ -543,29 +543,26 @@ export class TaskService extends TenantAwareCrudService { query.andWhere((qb: SelectQueryBuilder) => { const subQuery = qb .subQuery() - .select(p('"project_module_task"."taskId"')) - .from(p('project_module_task'), p('project_module_task')) + .select(p('"pmt"."taskId"')) // Use the alias 'pmt' here + .from(p('project_module_task'), 'pmt') // Assign alias 'pmt' to project_module_task .leftJoin( 'project_module_employee', - 'project_module_employee', - p( - '"project_module_employee"."organizationProjectModuleId" = "project_module_task"."organizationProjectModuleId"' - ) + 'pme', + p('"pme"."organizationProjectModuleId" = "pmt"."organizationProjectModuleId"') ); // Retrieve the employee ID based on the permission const employeeId = RequestContext.hasPermission(PermissionsEnum.CHANGE_SELECTED_EMPLOYEE) - ? members?.id + ? members?.['id'] : RequestContext.currentEmployeeId(); if (isNotEmpty(employeeId)) { - subQuery.andWhere(p('"project_module_employee"."employeeId" = :employeeId'), { employeeId }); + subQuery.andWhere(p('"pme"."employeeId" = :employeeId'), { employeeId }); } if (isNotEmpty(modules)) { - subQuery.andWhere(p(`"${subQuery.alias}"."organizationProjectModuleId" IN (:...modules)`), { - modules - }); + subQuery.andWhere(p(`"pmt"."organizationProjectModuleId" IN (:...modules)`), { modules }); } + return p(`"project_module_tasks"."taskId" IN `) + subQuery.distinct(true).getQuery(); }); From b3a8e9a3181fe4d3ea1a7bc32a5c1163d4195628 Mon Sep 17 00:00:00 2001 From: "Rahul R." Date: Thu, 26 Sep 2024 19:07:48 +0530 Subject: [PATCH 16/28] fix: adjust timer last stopped based on last time slot --- packages/core/src/core/utils.ts | 13 +-- .../core/src/employee/employee.service.ts | 22 ++-- .../screenshot/screenshot.controller.ts | 39 +++---- .../screenshot/screenshot.helper.ts | 41 +++++++ .../schedule-time-log-entries.handler.ts | 102 +++++++----------- .../time-tracking/timer/timer.controller.ts | 4 +- .../src/time-tracking/timer/timer.module.ts | 13 +-- .../src/time-tracking/timer/timer.service.ts | 90 ++++++++-------- 8 files changed, 161 insertions(+), 163 deletions(-) create mode 100644 packages/core/src/time-tracking/screenshot/screenshot.helper.ts diff --git a/packages/core/src/core/utils.ts b/packages/core/src/core/utils.ts index fa98c9c057a..f47ebc26490 100644 --- a/packages/core/src/core/utils.ts +++ b/packages/core/src/core/utils.ts @@ -310,24 +310,25 @@ export function freshTimestamp(): Date { * * @param startedAt The start date of the range. * @param stoppedAt The end date of the range. - * @throws BadRequestException if the dates are invalid or if the stoppedAt date is before the startedAt date. + * @throws BadRequestException if the stoppedAt date is before the startedAt date. */ export function validateDateRange(startedAt: Date, stoppedAt: Date): void { const start = moment(startedAt); const end = moment(stoppedAt); - console.log('------ Stopped Timer ------', start.toDate(), end.toDate()); + console.log('------ Timer Date Range ------', start.toDate(), end.toDate()); + // Validate that both dates are valid if (!start.isValid() || !end.isValid()) { throw new BadRequestException('Started and Stopped date must be valid dates.'); } + // Only throw error if stoppedAt is smaller than startedAt if (end.isBefore(start)) { - throw new BadRequestException('Stopped date must be greater than the started date.'); + throw new BadRequestException('Stopped date must be greater than or equal to the started date.'); } } - /** * Function that returns intersection of 2 arrays * @param arr1 Array 1 @@ -469,8 +470,8 @@ export const flatten = (input: any): any => { const newKey = Array.isArray(value) ? key : nestedKeys.length > 0 - ? `${key}.${nestedKeys.join('.')}` - : key; + ? `${key}.${nestedKeys.join('.')}` + : key; return acc.concat(newKey); } }, []) || [] diff --git a/packages/core/src/employee/employee.service.ts b/packages/core/src/employee/employee.service.ts index 65d2b1f5826..e5b4d9c51f1 100644 --- a/packages/core/src/employee/employee.service.ts +++ b/packages/core/src/employee/employee.service.ts @@ -174,15 +174,11 @@ export class EmployeeService extends TenantAwareCrudService { */ async findOneByUserId(userId: ID, options?: FindOneOptions): Promise { try { - // Retrieve the tenant ID from the current context - const tenantId = RequestContext.currentTenantId(); - // Define the base where clause const whereClause = { userId, - ...(tenantId && { tenantId }), // Include tenantId if available isActive: true, - isArchived: false, + isArchived: false }; // Merge the existing where conditions in options, if any @@ -196,11 +192,13 @@ export class EmployeeService extends TenantAwareCrudService { switch (this.ormType) { case MultiORMEnum.MikroORM: - const { where, mikroOptions } = parseTypeORMFindToMikroOrm(queryOptions as FindManyOptions); + const { where, mikroOptions } = parseTypeORMFindToMikroOrm( + queryOptions as FindManyOptions + ); const item = await this.mikroOrmRepository.findOne(where, mikroOptions); return this.serialize(item as Employee); case MultiORMEnum.TypeORM: - return this.typeOrmRepository.findOne(queryOptions); + return await this.typeOrmRepository.findOne(queryOptions); default: throw new Error(`Not implemented for ${this.ormType}`); } @@ -506,10 +504,7 @@ export class EmployeeService extends TenantAwareCrudService { * @param params - Contains organizationId and possibly other per-tenant information. * @returns - UpdateResult or DeleteResult depending on the ORM type. */ - async softRemovedById( - employeeId: ID, - params: IBasePerTenantAndOrganizationEntityModel - ): Promise { + async softRemovedById(employeeId: ID, params: IBasePerTenantAndOrganizationEntityModel): Promise { try { // Obtain the organization ID from the provided parameters const organizationId = params.organizationId; @@ -539,10 +534,7 @@ export class EmployeeService extends TenantAwareCrudService { * @returns The restored Employee entity. * @throws BadRequestException if the employee cannot be restored or if an error occurs. */ - async softRecoverById( - employeeId: ID, - params: IBasePerTenantAndOrganizationEntityModel - ): Promise { + async softRecoverById(employeeId: ID, params: IBasePerTenantAndOrganizationEntityModel): Promise { try { // Obtain the organization ID from the provided parameters const organizationId = params.organizationId; diff --git a/packages/core/src/time-tracking/screenshot/screenshot.controller.ts b/packages/core/src/time-tracking/screenshot/screenshot.controller.ts index 449bb497a5f..a1233f25bb6 100644 --- a/packages/core/src/time-tracking/screenshot/screenshot.controller.ts +++ b/packages/core/src/time-tracking/screenshot/screenshot.controller.ts @@ -2,9 +2,7 @@ import { Controller, UseGuards, HttpStatus, Post, Body, UseInterceptors, Delete, import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; import { isUUID } from 'class-validator'; import * as path from 'path'; -import * as moment from 'moment'; import * as fs from 'fs'; -import { v4 as uuid } from 'uuid'; import * as Jimp from 'jimp'; import { IScreenshot, PermissionsEnum, UploadedFile } from '@gauzy/contracts'; import { EventBus } from '../../event-bus/event-bus'; @@ -20,6 +18,7 @@ import { UUIDValidationPipe, UseValidationPipe } from './../../shared/pipes'; import { DeleteQueryDTO } from './../../shared/dto'; import { Screenshot } from './screenshot.entity'; import { ScreenshotService } from './screenshot.service'; +import { createFileStorage } from './screenshot.helper'; @ApiTags('Screenshot') @UseGuards(TenantPermissionGuard, PermissionGuard) @@ -29,40 +28,32 @@ export class ScreenshotController { constructor(private readonly _screenshotService: ScreenshotService, private readonly _eventBus: EventBus) {} /** - * Create start/stop screenshot. + * Capture a start/stop screenshot + * * @param input The screenshot input data. * @param file The uploaded file data. * @returns The created screenshot entity. */ - @ApiOperation({ summary: 'Create start/stop screenshot.' }) + @ApiOperation({ + summary: 'Capture a start/stop screenshot', + description: + 'Captures a screenshot when the timer is started or stopped. This API allows uploading the screenshot file along with related metadata.' + }) @ApiResponse({ status: HttpStatus.OK, - description: 'The screenshot has been successfully captured.' + description: 'Screenshot captured successfully.', + type: Screenshot }) @ApiResponse({ status: HttpStatus.BAD_REQUEST, - description: 'Invalid input, The response body may contain clues as to what went wrong' + description: 'Invalid input provided. Check the response body for error details.' }) @Post() @UseInterceptors( // Use LazyFileInterceptor for handling file uploads with custom storage settings LazyFileInterceptor('file', { // Define storage settings for uploaded files - storage: () => { - // Define the base directory for storing screenshots - const baseDirectory = path.join('screenshots', moment().format('YYYY/MM/DD')); - - // Generate unique sub directories based on the current tenant and employee IDs - const subDirectory = path.join( - RequestContext.currentTenantId() || uuid(), - RequestContext.currentEmployeeId() || uuid() - ); - - return new FileStorage().storage({ - dest: () => path.join(baseDirectory, subDirectory), - prefix: 'screenshots' - }); - } + storage: () => createFileStorage() }) ) async create(@Body() input: Screenshot, @UploadedFileStorage() file: UploadedFile) { @@ -71,7 +62,7 @@ export class ScreenshotController { return; } - console.log('Screenshot Http Request Input: ', { input }); + // console.log('Screenshot Http Request Input: ', { input }); // Extract user information from the request context const user = RequestContext.currentUser(); @@ -137,11 +128,9 @@ export class ScreenshotController { recordedAt: input.recordedAt ? input.recordedAt : new Date() }); - console.log(`Screenshot entity for employee (${user.name})`, { entity }); - // Create the screenshot entity in the database const screenshot = await this._screenshotService.create(entity); - console.log(`Screenshot created for employee (${user.name})`, screenshot); + // console.log(`Screenshot created for employee (${user.name})`, screenshot); // Publish the screenshot created event const ctx = RequestContext.currentRequestContext(); // Get current request context; diff --git a/packages/core/src/time-tracking/screenshot/screenshot.helper.ts b/packages/core/src/time-tracking/screenshot/screenshot.helper.ts new file mode 100644 index 00000000000..609eae545c2 --- /dev/null +++ b/packages/core/src/time-tracking/screenshot/screenshot.helper.ts @@ -0,0 +1,41 @@ +import * as path from 'path'; +import * as moment from 'moment'; +import { v4 as uuid } from 'uuid'; +import { RequestContext } from '../../core/context'; +import { FileStorage } from '../../core/file-storage'; + +/** + * Creates file storage configuration for handling screenshot uploads. + * + * @returns Configured FileStorage instance + */ +export function createFileStorage() { + // Define the base directory for storing screenshots + const baseDirectory = getBaseDirectory(); + // Generate unique sub directories based on the current tenant and employee IDs + const subDirectory = getSubDirectory(); + + return new FileStorage().storage({ + dest: () => path.join(baseDirectory, subDirectory), + prefix: 'screenshots' + }); +} + +/** + * Gets the base directory for storing screenshots based on the current date. + * @returns The base directory path + */ +function getBaseDirectory(): string { + return path.join('screenshots', moment().format('YYYY/MM/DD')); +} + +/** + * Generates a unique sub-directory based on the current tenant and employee IDs. + * @returns The sub-directory path + */ +function getSubDirectory(): string { + // Retrieve the tenant ID from the current context or a random UUID + const tenantId = RequestContext.currentTenantId() || uuid(); + const employeeId = RequestContext.currentEmployeeId() || uuid(); + return path.join(tenantId, employeeId); +} diff --git a/packages/core/src/time-tracking/time-log/commands/handlers/schedule-time-log-entries.handler.ts b/packages/core/src/time-tracking/time-log/commands/handlers/schedule-time-log-entries.handler.ts index 1aafd99f3f7..1c08bb17eb8 100644 --- a/packages/core/src/time-tracking/time-log/commands/handlers/schedule-time-log-entries.handler.ts +++ b/packages/core/src/time-tracking/time-log/commands/handlers/schedule-time-log-entries.handler.ts @@ -10,7 +10,7 @@ import { TypeOrmTimeLogRepository } from '../../repository/type-orm-time-log.rep @CommandHandler(ScheduleTimeLogEntriesCommand) export class ScheduleTimeLogEntriesHandler implements ICommandHandler { - constructor(readonly typeOrmTimeLogRepository: TypeOrmTimeLogRepository) { } + constructor(readonly typeOrmTimeLogRepository: TypeOrmTimeLogRepository) {} /** * Executes the scheduling of TimeLog entries based on the given command parameters. @@ -30,15 +30,11 @@ export class ScheduleTimeLogEntriesHandler implements ICommandHandler { + private async getPendingTimeLogs(tenantId: ID, organizationId: ID, employeeId?: ID): Promise { // Construct the query with find options const query = this.typeOrmTimeLogRepository.createQueryBuilder('time_log').setFindOptions({ relations: { timeSlots: true } @@ -82,10 +75,12 @@ export class ScheduleTimeLogEntriesHandler implements ICommandHandler { - web.andWhere(andWhere); - web.orWhere(orWhere); - })); + qb.andWhere( + new Brackets((web: WhereExpressionBuilder) => { + web.andWhere(andWhere); + web.orWhere(orWhere); + }) + ); }); console.log( @@ -107,16 +102,20 @@ export class ScheduleTimeLogEntriesHandler implements ICommandHandler 10) { + + const difference = moment.utc().diff(startedAt, 'minutes'); + console.log(`This log was created more than ${difference} minutes ago at ${startedAt}`); + + if (difference > 10) { await this.updateStoppedAtUsingStartedAt(timeLog); } } else { @@ -159,19 +158,8 @@ export class ScheduleTimeLogEntriesHandler implements ICommandHandler { - // Calculate the total duration in seconds from all time slots - const totalDurationInSeconds = timeSlots.reduce((sum, { duration }) => sum + duration, 0); - - // Example: - // If timeSlots = [{ duration: 300 }, { duration: 600 }] - // Then totalDurationInSeconds = 300 + 600 = 900 seconds (i.e., 15 minutes) - - // Calculate the stoppedAt date by adding the total duration to the startedAt date of the time log - let stoppedAt = moment.utc(timeLog.startedAt).add(totalDurationInSeconds, 'seconds').toDate(); - - // Example: - // If timeLog.startedAt = "2024-09-24 10:00:00" and totalDurationInSeconds = 900, - // then stoppedAt = "2024-09-24 10:15:00" + // Get the stoppedAt date from the time log + let stoppedAt = moment.utc(timeLog.stoppedAt).toDate(); // Retrieve the most recent time slot from the last log const lastTimeSlot: ITimeSlot | undefined = timeSlots.sort((a: ITimeSlot, b: ITimeSlot) => @@ -185,43 +173,33 @@ export class ScheduleTimeLogEntriesHandler implements ICommandHandler 10) { - // Calculate the potential stoppedAt time using the total duration + if (difference > 10) { // Example: If the last time slot started at "2024-09-24 10:00:00" and ran for 300 seconds (5 minutes), // then the calculated stoppedAt time would be "2024-09-24 10:05:00". - stoppedAt = lastTimeSlotStartedAt.add(duration, 'seconds').toDate(); + stoppedAt = startedAt.add(duration, 'seconds').toDate(); + + // Update the stoppedAt field in the database + await this.typeOrmTimeLogRepository.save({ + id: timeLog.id, + stoppedAt + }); } } - // Update the stoppedAt field in the database - if (moment.utc().diff(stoppedAt, 'minutes') > 10) { - // Example: - // If the current time is "2024-09-24 21:30:00" and stoppedAt is "2024-09-24 21:15:00", - // the difference would be 15 minutes, which is greater than 10. - // In this case, the stoppedAt field will be updated in the database. - - // Calculate the potential stoppedAt time using the total duration - await this.typeOrmTimeLogRepository.save({ - id: timeLog.id, - stoppedAt - }); - console.log('Schedule Time Log Entry Updated StoppedAt Using StoppedAt', stoppedAt); - // Example log output: "Schedule Time Log Entry Updated StoppedAt Using StoppedAt 2024-09-24 21:15:00" - } + console.log('Time log entry stoppedAt updated to', stoppedAt); } /** @@ -229,10 +207,10 @@ export class ScheduleTimeLogEntriesHandler implements ICommandHandler { + private async stopTimeLog(log: ITimeLog): Promise { // Update the isRunning field to false in the database for the given time log - await this.typeOrmTimeLogRepository.save({ - id: timeLog.id, + return await this.typeOrmTimeLogRepository.save({ + id: log.id, isRunning: false }); } diff --git a/packages/core/src/time-tracking/timer/timer.controller.ts b/packages/core/src/time-tracking/timer/timer.controller.ts index 47a6ad90b91..e852ba7599a 100644 --- a/packages/core/src/time-tracking/timer/timer.controller.ts +++ b/packages/core/src/time-tracking/timer/timer.controller.ts @@ -1,10 +1,10 @@ import { Controller, UseGuards, HttpStatus, Post, Body, Get, Query } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; import { ITimeLog, ITimerStatus, PermissionsEnum } from '@gauzy/contracts'; -import { TimerService } from './timer.service'; import { PermissionGuard, TenantPermissionGuard } from './../../shared/guards'; import { Permissions } from './../../shared/decorators'; import { UseValidationPipe } from '../../shared/pipes'; +import { TimerService } from './timer.service'; import { StartTimerDTO, StopTimerDTO, TimerStatusQueryDTO } from './dto'; @ApiTags('Timer Tracker') @@ -77,7 +77,6 @@ export class TimerController { @Post('/start') @UseValidationPipe() async startTimer(@Body() entity: StartTimerDTO): Promise { - console.log('----------------------------------Start Timer----------------------------------', entity); return await this.timerService.startTimer(entity); } @@ -98,7 +97,6 @@ export class TimerController { @Post('/stop') @UseValidationPipe() async stopTimer(@Body() entity: StopTimerDTO): Promise { - console.log('----------------------------------Stop Timer----------------------------------', entity); return await this.timerService.stopTimer(entity); } } diff --git a/packages/core/src/time-tracking/timer/timer.module.ts b/packages/core/src/time-tracking/timer/timer.module.ts index 4c9f981277f..dce76376474 100644 --- a/packages/core/src/time-tracking/timer/timer.module.ts +++ b/packages/core/src/time-tracking/timer/timer.module.ts @@ -7,14 +7,9 @@ import { TimerController } from './timer.controller'; import { TimerService } from './timer.service'; @Module({ + imports: [RolePermissionModule, TimeLogModule, EmployeeModule, CqrsModule], controllers: [TimerController], - imports: [ - RolePermissionModule, - TimeLogModule, - EmployeeModule, - CqrsModule - ], - providers: [TimerService], - exports: [TimerService] + exports: [TimerService], + providers: [TimerService] }) -export class TimerModule { } +export class TimerModule {} diff --git a/packages/core/src/time-tracking/timer/timer.service.ts b/packages/core/src/time-tracking/timer/timer.service.ts index 24ce618e5c3..d9311bcce0f 100644 --- a/packages/core/src/time-tracking/timer/timer.service.ts +++ b/packages/core/src/time-tracking/timer/timer.service.ts @@ -195,8 +195,8 @@ export class TimerService { */ async startTimer(request: ITimerToggleInput): Promise { console.log( - '----------------------------------Started Timer Date----------------------------------', - moment.utc(request.startedAt).toDate() + `-------------Start Timer Request (${moment.utc(request.startedAt).toDate()})-------------`, + JSON.stringify(request) ); // Retrieve the tenant ID from the current context or the provided one in the request @@ -215,6 +215,10 @@ export class TimerService { version } = request; + // Determine the start date and time in UTC + const startedAt = moment.utc(request.startedAt ?? moment.utc()).toDate(); + console.log('timer start date', startedAt); + // Retrieve the employee information const employee = await this.findEmployee(); @@ -239,9 +243,6 @@ export class TimerService { console.error('Error while getting last running logs', error); } - // Determine the start date and time in UTC - const startedAt = request.startedAt ? moment.utc(request.startedAt).toDate() : moment.utc().toDate(); - // Create a new time log entry using the command bus const timeLog = await this._commandBus.execute( new TimeLogCreateCommand({ @@ -282,43 +283,41 @@ export class TimerService { */ async stopTimer(request: ITimerToggleInput): Promise { console.log( - '----------------------------------Stopped Timer Date----------------------------------', - moment.utc(request.stoppedAt).toDate() + `-------------Stop Timer Request (${moment.utc(request.startedAt).toDate()})-------------`, + JSON.stringify(request) ); - // Retrieve tenant ID - const tenantId = RequestContext.currentTenantId() || request.tenantId; + // Validate the date range and check if the timer is running + validateDateRange(request.startedAt, request.stoppedAt); // Fetch the employee details const employee = await this.findEmployee(); + // Retrieve the employee ID and organization ID + const { id: employeeId, organizationId } = employee; + // Retrieve tenant ID + const tenantId = RequestContext.currentTenantId() || employee.tenantId || request.tenantId; + // Check if time tracking is enabled for the employee if (!employee.isTrackingEnabled) { throw new ForbiddenException('The time tracking functionality has been disabled for you.'); } - // Retrieve the last running log or start a new timer if none exist - let lastLog = await this.getLastRunningLog(); + // Determine whether to include time slots in the result + const includeTimeSlots = true; + + // Retrieve the last running log + let lastLog = await this.getLastRunningLog(includeTimeSlots); + + // If no running log is found throw an NotAcceptableException with a message if (!lastLog) { console.log(`No running log found. Can't stop timer because it was already stopped.`); throw new NotAcceptableException(`No running log found. Can't stop timer because it was already stopped.`); } - // Retrieve the employee ID and organization ID - const { id: employeeId, organizationId } = employee; - - // Get the lastLog - lastLog = await this.typeOrmTimeLogRepository.findOne({ - where: { id: lastLog.id, tenantId, organizationId, employeeId }, - relations: { timeSlots: true } - }); - // Retrieve stoppedAt date or use current date if not provided let stoppedAt = await this.calculateStoppedAt(request, lastLog); - // Validate the date range and check if the timer is running - validateDateRange(lastLog.startedAt, stoppedAt); - // Update the time log entry to mark it as stopped lastLog = await this._commandBus.execute( new TimeLogUpdateCommand( @@ -418,6 +417,7 @@ export class TimerService { async calculateStoppedAt(request: ITimerToggleInput, lastLog: ITimeLog): Promise { // Retrieve stoppedAt date or default to the current date if not provided let stoppedAt = moment.utc(request.stoppedAt ?? moment.utc()).toDate(); + console.log('last stop request was at', stoppedAt); // Handle the DESKTOP source case if (request.source === TimeLogSourceEnum.DESKTOP) { @@ -433,42 +433,40 @@ export class TimerService { // Check if the last time slot was created more than 10 minutes ago if (lastTimeSlot) { - // Retrieve the last time slot's startedAt date - const lastTimeSlotStartedAt = moment.utc(lastTimeSlot.startedAt); - - // Retrieve the request stopped moment - const requestStoppedAt = moment.utc(stoppedAt); - - // Retrieve the last time slot's duration - const duration = lastTimeSlot.duration; + const duration = lastTimeSlot.duration; // Retrieve the last time slot's duration + const startedAt = moment.utc(lastTimeSlot.startedAt); // Retrieve the last time slot's startedAt date // Example: // If lastTimeSlotStartedAt = "2024-09-24 19:50:00" and duration = 600 (10 minutes) // and the current time is "2024-09-24 20:10:00", the difference is 20 minutes, which is more than 10 minutes. + const difference = moment.utc(stoppedAt).diff(startedAt, 'minutes'); + console.log(`Last time slot (${duration}) created ${difference} mins ago at ${startedAt}`); + // Check if the last time slot was created more than 10 minutes ago - if (requestStoppedAt.diff(lastTimeSlotStartedAt, 'minutes') > 10) { - // Calculate the potential stoppedAt time using the total duration - stoppedAt = lastTimeSlotStartedAt.add(duration, 'seconds').toDate(); + if (difference > 10) { + stoppedAt = startedAt.add(duration, 'seconds').toDate(); // Calculate the potential stoppedAt time using the total duration // Example: stoppedAt = "2024-09-24 20:00:00" } } else { // Retrieve the last log's startedAt date - const lastLogStartedAt = moment.utc(lastLog.startedAt); - + const startedAt = moment.utc(lastLog.startedAt); // Example: // If lastLog.startedAt = "2024-09-24 19:30:00" and there are no time slots, // and the current time is "2024-09-24 20:00:00", the difference is 30 minutes. + const difference = moment.utc(stoppedAt).diff(startedAt, 'minutes'); + console.log(`last log was created more than ${difference} minutes ago at ${startedAt}`); + // If no time slots exist and the difference is more than 10 minutes, adjust the stoppedAt - if (moment.utc().diff(lastLogStartedAt, 'minutes') > 10) { - stoppedAt = moment.utc(lastLog.startedAt).add(10, 'seconds').toDate(); + if (difference > 10) { + stoppedAt = startedAt.add(10, 'seconds').toDate(); // Example: stoppedAt will be "2024-09-24 19:30:10" } } } - console.log('Last calculated stoppedAt: %s', stoppedAt); + console.log('Final last calculated stoppedAt: %s', stoppedAt); // Example log output: "Last calculated stoppedAt: 2024-09-24 20:00:00" return stoppedAt; } @@ -532,9 +530,13 @@ export class TimerService { * Get the last running log or all pending running logs for the current employee * * @param fetchAll - Set to `true` to fetch all pending logs, otherwise fetch the last running log + * @param includeTimeSlots - Set to `true` to include the associated time slots in the result * @returns A single time log if `fetchAll` is `false`, or an array of time logs if `fetchAll` is `true` */ - private async getRunningLogs(fetchAll: boolean = false): Promise { + private async getRunningLogs( + fetchAll: boolean = false, + includeTimeSlots: boolean = false + ): Promise { const tenantId = RequestContext.currentTenantId(); // Retrieve the tenant ID from the current context // Extract employeeId and organizationId @@ -557,7 +559,9 @@ export class TimerService { }) : await this.typeOrmTimeLogRepository.findOne({ where: whereClause, - order: { startedAt: 'DESC', createdAt: 'DESC' } + order: { startedAt: 'DESC', createdAt: 'DESC' }, + // Determine relations if includeTimeSlots is true + ...(includeTimeSlots && { relations: { timeSlots: true } }) }); } @@ -566,9 +570,9 @@ export class TimerService { * * @returns The last running ITimeLog entry for the current employee */ - private async getLastRunningLog(): Promise { + private async getLastRunningLog(includeTimeSlots: boolean = false): Promise { // Retrieve the last running log by using the `getRunningLogs` method with `fetchAll` set to false - const lastRunningLog = await this.getRunningLogs(false); + const lastRunningLog = await this.getRunningLogs(false, includeTimeSlots); // Ensure that the returned log is of type ITimeLog return lastRunningLog as ITimeLog; From b90723eeb99adac0bae59bd3ef18cce149e5ad07 Mon Sep 17 00:00:00 2001 From: "Rahul R." Date: Thu, 26 Sep 2024 19:12:36 +0530 Subject: [PATCH 17/28] fix: updated screenshot create method --- .../screenshot/screenshot.controller.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/core/src/time-tracking/screenshot/screenshot.controller.ts b/packages/core/src/time-tracking/screenshot/screenshot.controller.ts index a1233f25bb6..23341b79e44 100644 --- a/packages/core/src/time-tracking/screenshot/screenshot.controller.ts +++ b/packages/core/src/time-tracking/screenshot/screenshot.controller.ts @@ -62,16 +62,16 @@ export class ScreenshotController { return; } - // console.log('Screenshot Http Request Input: ', { input }); + console.log('Screenshot request input:', input); // Extract user information from the request context const user = RequestContext.currentUser(); + // Extract necessary properties from the request body + const tenantId = RequestContext.currentTenantId() || input.tenantId; + const organizationId = input.organizationId; + const userId = RequestContext.currentUserId(); try { - // Extract necessary properties from the request body - const { organizationId } = input; - const tenantId = RequestContext.currentTenantId() || input.tenantId; - // Initialize file storage provider and process thumbnail const provider = new FileStorage().getProvider(); @@ -118,9 +118,9 @@ export class ScreenshotController { // Populate entity properties for the screenshot const entity = new Screenshot({ - organizationId: organizationId, - tenantId: tenantId, - userId: RequestContext.currentUserId(), + organizationId, + tenantId, + userId, file: file.key, thumb: thumb.key, storageProvider: provider.name.toUpperCase(), @@ -130,7 +130,7 @@ export class ScreenshotController { // Create the screenshot entity in the database const screenshot = await this._screenshotService.create(entity); - // console.log(`Screenshot created for employee (${user.name})`, screenshot); + console.log(`Screenshot created for employee (${user.name})`, screenshot); // Publish the screenshot created event const ctx = RequestContext.currentRequestContext(); // Get current request context; From cf288bfcacf1543c5f167c2a735384ca8ed27f78 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Sep 2024 20:03:07 +0000 Subject: [PATCH 18/28] chore(deps): bump rollup from 3.29.4 to 3.29.5 Bumps [rollup](https://github.com/rollup/rollup) from 3.29.4 to 3.29.5. - [Release notes](https://github.com/rollup/rollup/releases) - [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md) - [Commits](https://github.com/rollup/rollup/compare/v3.29.4...v3.29.5) --- updated-dependencies: - dependency-name: rollup dependency-type: indirect ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 4ce6a4cc37d..d7cae5c844f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -33127,9 +33127,9 @@ robust-predicates@^3.0.2: integrity sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg== rollup@^3.0.0, rollup@^3.27.1: - version "3.29.4" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.29.4.tgz#4d70c0f9834146df8705bfb69a9a19c9e1109981" - integrity sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw== + version "3.29.5" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.29.5.tgz#8a2e477a758b520fb78daf04bca4c522c1da8a54" + integrity sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w== optionalDependencies: fsevents "~2.3.2" From b7e321667d4dbb195840f3c062020a5c1ececd5c Mon Sep 17 00:00:00 2001 From: samuel mbabhazi <111171386+samuelmbabhazi@users.noreply.github.com> Date: Thu, 26 Sep 2024 22:26:41 +0200 Subject: [PATCH 19/28] [Enhancement] Align fields with header - /pages/employees/timesheets (#8268) * [Enhancement] Align fields with header - /pages/employees/timesheets/daily * truncate title --- .../daily/daily/daily.component.html | 20 ++++-- .../daily/daily/daily.component.scss | 62 ++++++++++--------- 2 files changed, 47 insertions(+), 35 deletions(-) diff --git a/apps/gauzy/src/app/pages/employees/timesheet/daily/daily/daily.component.html b/apps/gauzy/src/app/pages/employees/timesheet/daily/daily/daily.component.html index 108c066735e..656322b19cb 100644 --- a/apps/gauzy/src/app/pages/employees/timesheet/daily/daily/daily.component.html +++ b/apps/gauzy/src/app/pages/employees/timesheet/daily/daily/daily.component.html @@ -84,7 +84,7 @@ -
+
@@ -94,7 +94,7 @@
{{ 'TIMESHEET.TODO' | translate }} : - {{ log?.task?.title }} + {{ log?.task?.title | truncate : 50 }} {{ 'TIMESHEET.NO_TODO' | translate }} @@ -119,10 +119,18 @@ {{ log.logType | titlecase }}
-
- - {{ log.source | replace : '_' : ' ' | titlecase }} - +
+ + +
{{ log.duration | durationFormat }} diff --git a/apps/gauzy/src/app/pages/employees/timesheet/daily/daily/daily.component.scss b/apps/gauzy/src/app/pages/employees/timesheet/daily/daily/daily.component.scss index 7eaeae3b3d5..d0f47a77407 100644 --- a/apps/gauzy/src/app/pages/employees/timesheet/daily/daily/daily.component.scss +++ b/apps/gauzy/src/app/pages/employees/timesheet/daily/daily/daily.component.scss @@ -1,33 +1,37 @@ @import '../../weekly/weekly/weekly.component.scss'; :host { - nb-card { - border-radius: 0 nb-theme(border-radius) nb-theme(border-radius) nb-theme(border-radius); - } - nb-card-body { - height: 100%; - } - .gauzy-button-action { - display: flex; - align-content: center; - justify-content: flex-end; - } - .log-container { - height: calc(100% - 50px); - } - .log { - width: fit-content; - font-size: 12px; - font-weight: 600; - line-height: 15px; - letter-spacing: 0em; - text-align: left; - padding: 3px 8px; - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - border-radius: nb-theme(border-radius); - background: var(--gauzy-sidebar-background-3); - } + nb-card { + border-radius: 0 nb-theme(border-radius) nb-theme(border-radius) nb-theme(border-radius); + } + nb-card-body { + height: 100%; + } + .gauzy-button-action { + display: flex; + align-content: center; + justify-content: flex-end; + } + .log-container { + height: calc(100% - 50px); + } + .log { + width: fit-content; + font-size: 12px; + font-weight: 600; + line-height: 15px; + letter-spacing: 0em; + text-align: left; + padding: 3px 8px; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + border-radius: nb-theme(border-radius); + background: var(--gauzy-sidebar-background-3); + } + .source-version { + display: flex; + gap: 5%; + } } From d045ecc044ed328f896bb094d1569887d592f426 Mon Sep 17 00:00:00 2001 From: "Rahul R." Date: Fri, 27 Sep 2024 15:04:57 +0530 Subject: [PATCH 20/28] fix: no need to clear data table columns registry (job employees) --- .../lib/components/job-employee/job-employee.component.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/plugins/job-employee-ui/src/lib/components/job-employee/job-employee.component.ts b/packages/plugins/job-employee-ui/src/lib/components/job-employee/job-employee.component.ts index 08566c8b70e..5e56f7f3231 100644 --- a/packages/plugins/job-employee-ui/src/lib/components/job-employee/job-employee.component.ts +++ b/packages/plugins/job-employee-ui/src/lib/components/job-employee/job-employee.component.ts @@ -84,11 +84,11 @@ export class JobEmployeeComponent extends PaginationFilterBaseComponent implemen } ngOnInit(): void { - this._applyTranslationOnSmartTable(); // - this._loadSmartTableSettings(); // Load smart table settings this.initializeUiPermissions(); // Initialize UI permissions this.initializeUiLanguagesAndLocale(); // Initialize UI languages and Update Locale this._initializePageElements(); // Register page elements + this._applyTranslationOnSmartTable(); // + this._loadSmartTableSettings(); // Load smart table settings } ngAfterViewInit(): void { @@ -629,8 +629,6 @@ export class JobEmployeeComponent extends PaginationFilterBaseComponent implemen }; ngOnDestroy(): void { - // Delete the dashboard tabset from the registry - this._pageTabRegistryService.deleteTabset(this.tabsetId); - this._pageDataTableRegistryService.deleteDataTable(this.dataTableId); + this._pageTabRegistryService.deleteTabset(this.tabsetId); // Delete the dashboard tabset from the registry } } From 31341905876c898ddd7d7f3917a25f6dcf36d781 Mon Sep 17 00:00:00 2001 From: "Rahul R." Date: Fri, 27 Sep 2024 17:55:32 +0530 Subject: [PATCH 21/28] chore(deps): bump rollup from 5.1.0 to 5.1.1 --- apps/gauzy/package.json | 2 +- package.json | 2 +- packages/auth/package.json | 2 +- packages/core/package.json | 2 +- yarn.lock | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/gauzy/package.json b/apps/gauzy/package.json index 584846b9852..faabd287c23 100644 --- a/apps/gauzy/package.json +++ b/apps/gauzy/package.json @@ -90,7 +90,7 @@ "@swimlane/ngx-charts": "^20.1.0", "angular2-smart-table": "^3.2.0", "angular2-toaster": "^11.0.1", - "bcrypt": "^5.1.0", + "bcrypt": "^5.1.1", "bootstrap": "^4.3.1", "brace": "^0.11.1", "camelcase": "^6.3.0", diff --git a/package.json b/package.json index 3a43560d102..d6a80346404 100644 --- a/package.json +++ b/package.json @@ -331,7 +331,7 @@ "@ng-select/ng-select": "^11.2.0", "angular2-smart-table": "^3.2.0", "autoprefixer": "10.4.14", - "bcrypt": "^5.1.0", + "bcrypt": "^5.1.1", "camelcase": "^6.3.0", "dotenv": "^16.0.3", "ffi-napi": "^4.0.3", diff --git a/packages/auth/package.json b/packages/auth/package.json index 11edb56b088..3f1dac772e1 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -44,7 +44,7 @@ "@nestjs/config": "^3.2.0", "@nestjs/passport": "^10.0.3", "axios": "^1.7.4", - "bcrypt": "^5.1.0", + "bcrypt": "^5.1.1", "express": "^4.18.2", "passport": "^0.6.0", "passport-auth0": "^1.3.3", diff --git a/packages/core/package.json b/packages/core/package.json index 474a6b5bbc7..b91193fa9a5 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -118,7 +118,7 @@ "archiver": "^5.3.0", "atlassian-connect-express": "^8.5.0", "axios": "^1.7.4", - "bcrypt": "^5.1.0", + "bcrypt": "^5.1.1", "better-sqlite3": "^9.4.3", "cache-manager": "^5.3.2", "cache-manager-redis-yet": "^4.1.2", diff --git a/yarn.lock b/yarn.lock index d7cae5c844f..56241f68fa3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14122,7 +14122,7 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" -bcrypt@^5.1.0: +bcrypt@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/bcrypt/-/bcrypt-5.1.1.tgz#0f732c6dcb4e12e5b70a25e326a72965879ba6e2" integrity sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww== From 5eaf9fc060dd953e4f9b48b43c7e65a6d6b23e31 Mon Sep 17 00:00:00 2001 From: "Rahul R." Date: Fri, 27 Sep 2024 18:26:33 +0530 Subject: [PATCH 22/28] Revert "chore(deps): bump rollup from 5.1.0 to 5.1.1" This reverts commit 31341905876c898ddd7d7f3917a25f6dcf36d781. --- apps/gauzy/package.json | 2 +- package.json | 2 +- packages/auth/package.json | 2 +- packages/core/package.json | 2 +- yarn.lock | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/gauzy/package.json b/apps/gauzy/package.json index faabd287c23..584846b9852 100644 --- a/apps/gauzy/package.json +++ b/apps/gauzy/package.json @@ -90,7 +90,7 @@ "@swimlane/ngx-charts": "^20.1.0", "angular2-smart-table": "^3.2.0", "angular2-toaster": "^11.0.1", - "bcrypt": "^5.1.1", + "bcrypt": "^5.1.0", "bootstrap": "^4.3.1", "brace": "^0.11.1", "camelcase": "^6.3.0", diff --git a/package.json b/package.json index d6a80346404..3a43560d102 100644 --- a/package.json +++ b/package.json @@ -331,7 +331,7 @@ "@ng-select/ng-select": "^11.2.0", "angular2-smart-table": "^3.2.0", "autoprefixer": "10.4.14", - "bcrypt": "^5.1.1", + "bcrypt": "^5.1.0", "camelcase": "^6.3.0", "dotenv": "^16.0.3", "ffi-napi": "^4.0.3", diff --git a/packages/auth/package.json b/packages/auth/package.json index 3f1dac772e1..11edb56b088 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -44,7 +44,7 @@ "@nestjs/config": "^3.2.0", "@nestjs/passport": "^10.0.3", "axios": "^1.7.4", - "bcrypt": "^5.1.1", + "bcrypt": "^5.1.0", "express": "^4.18.2", "passport": "^0.6.0", "passport-auth0": "^1.3.3", diff --git a/packages/core/package.json b/packages/core/package.json index b91193fa9a5..474a6b5bbc7 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -118,7 +118,7 @@ "archiver": "^5.3.0", "atlassian-connect-express": "^8.5.0", "axios": "^1.7.4", - "bcrypt": "^5.1.1", + "bcrypt": "^5.1.0", "better-sqlite3": "^9.4.3", "cache-manager": "^5.3.2", "cache-manager-redis-yet": "^4.1.2", diff --git a/yarn.lock b/yarn.lock index 56241f68fa3..d7cae5c844f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14122,7 +14122,7 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" -bcrypt@^5.1.1: +bcrypt@^5.1.0: version "5.1.1" resolved "https://registry.yarnpkg.com/bcrypt/-/bcrypt-5.1.1.tgz#0f732c6dcb4e12e5b70a25e326a72965879ba6e2" integrity sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww== From 9cd807fdf56f06b3aa3e728d0c8442dd8f89a926 Mon Sep 17 00:00:00 2001 From: "Rahul R." Date: Fri, 27 Sep 2024 19:44:59 +0530 Subject: [PATCH 23/28] fix: project mutation for members --- .../src/organization-projects.model.ts | 3 +- .../core/src/employee/employee.service.ts | 36 +++- .../integration-map.sync-project.handler.ts | 41 ++-- .../organization-project-create.handler.ts | 78 ++++--- .../organization-project-update.handler.ts | 39 ++-- .../organization-project-create.command.ts | 2 +- .../organization-project-update.command.ts | 6 +- .../dto/organization-project.dto.ts | 19 +- .../dto/update-organization-project.dto.ts | 2 +- .../organization-project.controller.ts | 4 +- .../organization-project.service.ts | 175 ++++++--------- .../project-mutation.component.html | 14 +- .../project-mutation.component.ts | 199 +++++++++++------- .../tags-color-input.component.html | 2 +- 14 files changed, 343 insertions(+), 277 deletions(-) diff --git a/packages/contracts/src/organization-projects.model.ts b/packages/contracts/src/organization-projects.model.ts index 244a4283d65..a96211c39b7 100644 --- a/packages/contracts/src/organization-projects.model.ts +++ b/packages/contracts/src/organization-projects.model.ts @@ -86,7 +86,8 @@ export interface IOrganizationProjectsFindInput } export interface IOrganizationProjectCreateInput extends IOrganizationProjectBase { - managers?: IOrganizationProjectEmployee[]; + memberIds?: ID[]; // Manager of the organization project + managerIds?: ID[]; // Manager of the organization project } export interface IOrganizationProjectUpdateInput extends IOrganizationProjectCreateInput {} diff --git a/packages/core/src/employee/employee.service.ts b/packages/core/src/employee/employee.service.ts index e5b4d9c51f1..18fdacf87d4 100644 --- a/packages/core/src/employee/employee.service.ts +++ b/packages/core/src/employee/employee.service.ts @@ -92,12 +92,46 @@ export class EmployeeService extends TenantAwareCrudService { return { items, total }; } + /** + * Retrieves a list of active, non-archived employees based on provided employee IDs, + * organization ID, and tenant ID. + * + * @param {ID[]} employeeIds - Array of employee IDs to search for. Defaults to an empty array if not provided. + * @param {ID} organizationId - The ID of the organization to filter employees. + * @param {ID} tenantId - The ID of the tenant to filter employees. + * @returns {Promise} - Promise resolving with an array of matching `IEmployee` objects. + * + * @throws {Error} - Throws an error if the retrieval process fails. + */ + async findActiveEmployeesByEmployeeIds( + employeeIds: ID[] = [], + organizationId: ID, + tenantId: ID + ): Promise { + try { + // Filter out any invalid values from the employee IDs array + const filteredIds = employeeIds.filter(Boolean); + + // Fetch employees using filtered IDs, organizationId, and tenantId + return await this.typeOrmEmployeeRepository.findBy({ + id: In(filteredIds), + organizationId, + tenantId, + isActive: true, + isArchived: false + }); + } catch (error) { + console.error('Error while retrieving employees', error); + throw new Error(`Failed to retrieve employees: ${error}`); + } + } + /** * Finds employees based on an array of user IDs. * @param userIds An array of user IDs. * @returns A promise resolving to an array of employees. */ - async findEmployeesByUserIds(userIds: ID[]): Promise { + async findEmployeesByUserIds(userIds: ID[]): Promise { try { // Get the tenant ID from the current request context const tenantId = RequestContext.currentTenantId(); diff --git a/packages/core/src/integration-map/commands/handlers/integration-map.sync-project.handler.ts b/packages/core/src/integration-map/commands/handlers/integration-map.sync-project.handler.ts index 2b0dbe5a8ce..3a707dc014c 100644 --- a/packages/core/src/integration-map/commands/handlers/integration-map.sync-project.handler.ts +++ b/packages/core/src/integration-map/commands/handlers/integration-map.sync-project.handler.ts @@ -1,34 +1,34 @@ import { CommandHandler, ICommandHandler, CommandBus } from '@nestjs/cqrs'; -import { IntegrationEntity } from '@gauzy/contracts'; +import { IIntegrationMap, IntegrationEntity } from '@gauzy/contracts'; import { RequestContext } from './../../../core/context'; import { IntegrationMapSyncProjectCommand } from './../integration-map.sync-project.command'; import { IntegrationMapSyncEntityCommand } from './../integration-map.sync-entity.command'; import { IntegrationMapService } from '../../integration-map.service'; -import { OrganizationProjectCreateCommand, OrganizationProjectUpdateCommand } from '../../../organization-project/commands'; +import { + OrganizationProjectCreateCommand, + OrganizationProjectUpdateCommand +} from '../../../organization-project/commands'; @CommandHandler(IntegrationMapSyncProjectCommand) -export class IntegrationMapSyncProjectHandler - implements ICommandHandler { - +export class IntegrationMapSyncProjectHandler implements ICommandHandler { constructor( private readonly _commandBus: CommandBus, private readonly _integrationMapService: IntegrationMapService - ) { } + ) {} /** - * Third party organization project integrated and mapped + * Third party organization project integration and mapping. * - * @param command - * @returns + * @param {IntegrationMapSyncProjectCommand} command - The command containing input data for integrating and mapping the project. + * @returns {Promise} - Returns a promise that resolves with the mapped project integration data. */ - public async execute( - command: IntegrationMapSyncProjectCommand - ) { + public async execute(command: IntegrationMapSyncProjectCommand): Promise { const { input } = command; const { integrationId, sourceId, organizationId, entity } = input; const tenantId = RequestContext.currentTenantId(); try { + // Attempt to find an existing project map const projectMap = await this._integrationMapService.findOneByWhereOptions({ entity: IntegrationEntity.PROJECT, sourceId, @@ -36,18 +36,15 @@ export class IntegrationMapSyncProjectHandler organizationId, tenantId }); - await this._commandBus.execute( - new OrganizationProjectUpdateCommand({ - ...entity, - id: projectMap.gauzyId - }) - ); + + // Update the project if it exists + await this._commandBus.execute(new OrganizationProjectUpdateCommand(projectMap.gauzyId, entity)); return projectMap; } catch (error) { - const project = await this._commandBus.execute( - new OrganizationProjectCreateCommand(entity) - ); - return await this._commandBus.execute( + // If project map is not found, create a new project and map it + const project = await this._commandBus.execute(new OrganizationProjectCreateCommand(entity)); + + return this._commandBus.execute( new IntegrationMapSyncEntityCommand({ gauzyId: project.id, integrationId, diff --git a/packages/core/src/organization-project/commands/handlers/organization-project-create.handler.ts b/packages/core/src/organization-project/commands/handlers/organization-project-create.handler.ts index 0b1cc68e4ee..e13b2a813a9 100644 --- a/packages/core/src/organization-project/commands/handlers/organization-project-create.handler.ts +++ b/packages/core/src/organization-project/commands/handlers/organization-project-create.handler.ts @@ -1,4 +1,4 @@ -import { BadRequestException } from '@nestjs/common'; +import { HttpException, HttpStatus } from '@nestjs/common'; import { CommandBus, CommandHandler, ICommandHandler } from '@nestjs/cqrs'; import { IOrganizationProject } from '@gauzy/contracts'; import { OrganizationProjectCreateCommand } from '../organization-project-create.command'; @@ -9,45 +9,73 @@ import { OrganizationProjectTaskSizeBulkCreateCommand } from '../../../tasks/siz import { OrganizationProjectIssueTypeBulkCreateCommand } from '../../../tasks/issue-type/commands'; @CommandHandler(OrganizationProjectCreateCommand) -export class OrganizationProjectCreateHandler - implements ICommandHandler -{ +export class OrganizationProjectCreateHandler implements ICommandHandler { constructor( private readonly _commandBus: CommandBus, private readonly _organizationProjectService: OrganizationProjectService ) {} - public async execute( - command: OrganizationProjectCreateCommand - ): Promise { + /** + * Executes the creation of an organization project along with its associated task statuses, + * task priorities, task sizes, and issue types. + * + * @param {OrganizationProjectCreateCommand} command - The command containing the input data for creating the organization project. + * @returns {Promise} - Returns a promise that resolves with the created organization project. + * + * @throws {BadRequestException} - Throws a BadRequestException if an error occurs during the process. + */ + public async execute(command: OrganizationProjectCreateCommand): Promise { try { + // Destructure the input data from the command const { input } = command; + // Create the organization project using the input data const project = await this._organizationProjectService.create(input); - // 1. Create task statuses for relative organization project. - this._commandBus.execute( - new OrganizationProjectStatusBulkCreateCommand(project) - ); + // Initialize associated entities for the created project + this.createAssociatedEntitiesForProject(project); - // 2. Create task priorities for relative organization project. - this._commandBus.execute( - new OrganizationProjectTaskPriorityBulkCreateCommand(project) - ); + // Return the created organization project + return project; + } catch (error) { + // Handle errors and return an appropriate error response + console.error('Error during organization project creation:', error); + throw new HttpException(`Failed to create organization project: ${error.message}`, HttpStatus.BAD_REQUEST); + } + } - // 3. Create task sizes for relative organization project. - this._commandBus.execute( - new OrganizationProjectTaskSizeBulkCreateCommand(project) - ); + /** + * Creates associated entities (task statuses, priorities, sizes, and issue types) for the organization project. + * + * @param {IOrganizationProject} project - The organization project for which associated entities will be created. + * @returns {Promise} - Returns a promise indicating the completion of the associated entities creation. + * + * @throws {HttpException} - Throws an HttpException if an error occurs during the process. + */ + private async createAssociatedEntitiesForProject(project: IOrganizationProject): Promise { + try { + console.log('Start: Creating associated entities for project with ID:', project.id); - // 4. Create issue types for relative organization project. - this._commandBus.execute( - new OrganizationProjectIssueTypeBulkCreateCommand(project) - ); + // Create task statuses for the newly created organization project + await this._commandBus.execute(new OrganizationProjectStatusBulkCreateCommand(project)); + console.log('Task statuses created successfully'); - return project; + // Create task priorities for the newly created organization project + await this._commandBus.execute(new OrganizationProjectTaskPriorityBulkCreateCommand(project)); + console.log('Task priorities created successfully'); + + // Create task sizes for the newly created organization project + await this._commandBus.execute(new OrganizationProjectTaskSizeBulkCreateCommand(project)); + console.log('Task sizes created successfully'); + + // Create issue types for the newly created organization project + await this._commandBus.execute(new OrganizationProjectIssueTypeBulkCreateCommand(project)); + console.log('Issue types created successfully'); + + console.log('End: Associated entities creation completed for project with ID:', project.id); } catch (error) { - throw new BadRequestException(error); + // Handle errors specific to the associated entities creation process + console.error('Error while creating associated entities for project:', error); } } } diff --git a/packages/core/src/organization-project/commands/handlers/organization-project-update.handler.ts b/packages/core/src/organization-project/commands/handlers/organization-project-update.handler.ts index 5b8fa23e2dc..59117b3603c 100644 --- a/packages/core/src/organization-project/commands/handlers/organization-project-update.handler.ts +++ b/packages/core/src/organization-project/commands/handlers/organization-project-update.handler.ts @@ -1,32 +1,27 @@ -import { BadRequestException } from '@nestjs/common'; import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; import { IOrganizationProject } from '@gauzy/contracts'; import { OrganizationProjectUpdateCommand } from '../organization-project-update.command'; import { OrganizationProjectService } from '../../organization-project.service'; @CommandHandler(OrganizationProjectUpdateCommand) -export class OrganizationProjectUpdateHandler - implements ICommandHandler { +export class OrganizationProjectUpdateHandler implements ICommandHandler { + constructor(private readonly _organizationProjectService: OrganizationProjectService) {} - constructor( - private readonly _organizationProjectService: OrganizationProjectService - ) { } + /** + * Executes the update of an organization project using the provided command data. + * + * @param {OrganizationProjectUpdateCommand} command - The command containing the input data for updating the organization project. + * @returns {Promise} - Returns a promise that resolves with the updated organization project. + * + * @throws {BadRequestException} - Throws a BadRequestException if an error occurs during the update process. + */ + public async execute(command: OrganizationProjectUpdateCommand): Promise { + const { id, input } = command; - public async execute( - command: OrganizationProjectUpdateCommand - ): Promise { - try { - const { input } = command; - const { id } = input; - //We are using create here because create calls the method save() - //We need save() to save ManyToMany relations - await this._organizationProjectService.create({ - ...input, - id - }); - return await this._organizationProjectService.findOneByIdString(id); - } catch (error) { - throw new BadRequestException(error); - } + // Update the organization project using the provided input + await this._organizationProjectService.update(id, input); + + // Find the updated organization project by ID + return await this._organizationProjectService.findOneByIdString(id); } } diff --git a/packages/core/src/organization-project/commands/organization-project-create.command.ts b/packages/core/src/organization-project/commands/organization-project-create.command.ts index 5090d7776dc..c75db392128 100644 --- a/packages/core/src/organization-project/commands/organization-project-create.command.ts +++ b/packages/core/src/organization-project/commands/organization-project-create.command.ts @@ -4,5 +4,5 @@ import { IOrganizationProjectCreateInput } from '@gauzy/contracts'; export class OrganizationProjectCreateCommand implements ICommand { static readonly type = '[Organization Project] Create'; - constructor(public readonly input: IOrganizationProjectCreateInput) { } + constructor(readonly input: IOrganizationProjectCreateInput) {} } diff --git a/packages/core/src/organization-project/commands/organization-project-update.command.ts b/packages/core/src/organization-project/commands/organization-project-update.command.ts index 46f39a23ca5..35fb4ffc7c8 100644 --- a/packages/core/src/organization-project/commands/organization-project-update.command.ts +++ b/packages/core/src/organization-project/commands/organization-project-update.command.ts @@ -1,10 +1,8 @@ import { ICommand } from '@nestjs/cqrs'; -import { IOrganizationProjectUpdateInput } from '@gauzy/contracts'; +import { ID, IOrganizationProjectUpdateInput } from '@gauzy/contracts'; export class OrganizationProjectUpdateCommand implements ICommand { static readonly type = '[Organization Project] Update'; - constructor( - public readonly input: IOrganizationProjectUpdateInput - ) { } + constructor(readonly id: ID, readonly input: IOrganizationProjectUpdateInput) {} } diff --git a/packages/core/src/organization-project/dto/organization-project.dto.ts b/packages/core/src/organization-project/dto/organization-project.dto.ts index d2baf43a7ab..463883c80b4 100644 --- a/packages/core/src/organization-project/dto/organization-project.dto.ts +++ b/packages/core/src/organization-project/dto/organization-project.dto.ts @@ -1,8 +1,23 @@ -import { IntersectionType, PartialType, PickType } from '@nestjs/swagger'; +import { ApiPropertyOptional, IntersectionType, PartialType, PickType } from '@nestjs/swagger'; +import { IsArray, IsOptional } from 'class-validator'; +import { ID } from '@gauzy/contracts'; import { OrganizationProject } from './../organization-project.entity'; import { UpdateTaskModeDTO } from './update-task-mode.dto'; +/** + * Organization Project DTO request validation + */ export class OrganizationProjectDTO extends IntersectionType( PickType(OrganizationProject, ['imageId', 'name', 'billing', 'budgetType'] as const), PartialType(UpdateTaskModeDTO) -) {} +) { + @ApiPropertyOptional({ type: () => String }) + @IsOptional() + @IsArray() + memberIds?: ID[] = []; + + @ApiPropertyOptional({ type: () => String }) + @IsOptional() + @IsArray() + managerIds?: ID[] = []; +} diff --git a/packages/core/src/organization-project/dto/update-organization-project.dto.ts b/packages/core/src/organization-project/dto/update-organization-project.dto.ts index 6a0795d2ee1..d45b91529a3 100644 --- a/packages/core/src/organization-project/dto/update-organization-project.dto.ts +++ b/packages/core/src/organization-project/dto/update-organization-project.dto.ts @@ -7,5 +7,5 @@ import { OrganizationProjectDTO } from './organization-project.dto'; * Update Organization Project DTO request validation */ export class UpdateOrganizationProjectDTO - extends IntersectionType(PartialType(OrganizationProjectDTO), TenantOrganizationBaseDTO) + extends IntersectionType(TenantOrganizationBaseDTO, PartialType(OrganizationProjectDTO)) implements IOrganizationProjectUpdateInput {} diff --git a/packages/core/src/organization-project/organization-project.controller.ts b/packages/core/src/organization-project/organization-project.controller.ts index ee3689fc362..6dc59912d87 100644 --- a/packages/core/src/organization-project/organization-project.controller.ts +++ b/packages/core/src/organization-project/organization-project.controller.ts @@ -138,7 +138,7 @@ export class OrganizationProjectController extends CrudController { - return await this.commandBus.execute(new OrganizationProjectUpdateCommand({ ...entity, id })); + return await this.commandBus.execute(new OrganizationProjectUpdateCommand(id, entity)); } /** @@ -291,7 +291,7 @@ export class OrganizationProjectController extends CrudController { - return await this.commandBus.execute(new OrganizationProjectUpdateCommand({ ...entity, id })); + return await this.commandBus.execute(new OrganizationProjectUpdateCommand(id, entity)); } /** diff --git a/packages/core/src/organization-project/organization-project.service.ts b/packages/core/src/organization-project/organization-project.service.ts index 8ef69cfc508..7d33718d46b 100644 --- a/packages/core/src/organization-project/organization-project.service.ts +++ b/packages/core/src/organization-project/organization-project.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, ForbiddenException, Injectable } from '@nestjs/common'; +import { BadRequestException, HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { EventBus } from '@nestjs/cqrs'; import { ILike, In, IsNull, SelectQueryBuilder } from 'typeorm'; import { @@ -15,20 +15,20 @@ import { IOrganizationProjectsFindInput, IOrganizationProjectUpdateInput, IPagination, - PermissionsEnum, RolesEnum } from '@gauzy/contracts'; import { getConfig } from '@gauzy/config'; import { CustomEmbeddedFieldConfig, isNotEmpty } from '@gauzy/common'; import { PaginationParams, TenantAwareCrudService } from '../core/crud'; import { RequestContext } from '../core/context'; -import { Employee, OrganizationProjectEmployee, Role } from '../core/entities/internal'; +import { OrganizationProjectEmployee, Role } from '../core/entities/internal'; import { FavoriteService } from '../core/decorators'; import { ActivityLogEvent } from '../activity-log/events'; import { generateActivityLogDescription } from '../activity-log/activity-log.helper'; import { RoleService } from '../role/role.service'; import { OrganizationProject } from './organization-project.entity'; import { prepareSQLQuery as p } from './../database/database.helper'; +import { EmployeeService } from '../employee/employee.service'; import { TypeOrmEmployeeRepository } from '../employee/repository'; import { MikroOrmOrganizationProjectEmployeeRepository, @@ -47,38 +47,12 @@ export class OrganizationProjectService extends TenantAwareCrudService { - try { - // Filter out falsy values (e.g., null or undefined) from the union of memberIds and managerIds - const filteredIds = [...memberIds, ...managerIds].filter(Boolean); - - // Retrieve employees based on specified criteria - const employees = await this.typeOrmEmployeeRepository.findBy({ - id: In(filteredIds), // Filtering by employee IDs (union of memberIds and managerIds) - organizationId, // Filtering by organizationId - tenantId // Filtering by tenantId - }); - - return employees; - } catch (error) { - // Handle any potential errors during the retrieval process - throw new Error(`Failed to retrieve employees: ${error}`); - } - } - /** * Creates an organization project based on the provided input. * @param input - Input data for creating the organization project. @@ -90,14 +64,10 @@ export class OrganizationProjectService extends TenantAwareCrudService manager.employeeId); - const memberIds = members.map((member) => member.employeeId); - // If the employee creates the project, default add as a manager try { // Check if the current role is EMPLOYEE @@ -112,11 +82,20 @@ export class OrganizationProjectService extends TenantAwareCrudService { // Check if the employee is a manager const isManager = managerIdsSet.has(employeeId); + // If the employee is a manager, assign the existing manager with the latest assignedAt date + const assignedAt = + isManager && !existingManagersMap.has(employeeId) + ? new Date() + : existingManagersMap.get(employeeId); // If the employee is a manager, assign the existing manager with the latest assignedAt date return new OrganizationProjectEmployee({ @@ -143,10 +127,7 @@ export class OrganizationProjectService extends TenantAwareCrudService { const tenantId = RequestContext.currentTenantId() || input.tenantId; - const { managers, members, organizationId } = input; + const { memberIds = [], managerIds = [], organizationId } = input; let organizationProject = await super.findOneByIdString(id, { where: { organizationId, tenantId } }); - // Check permission for CHANGE_SELECTED_EMPLOYEE - if (!RequestContext.hasPermission(PermissionsEnum.CHANGE_SELECTED_EMPLOYEE)) { - try { - const employeeId = RequestContext.currentEmployeeId(); - // If employee ID is present, restrict update to manager role - if (employeeId) { - organizationProject = await super.findOneByIdString(id, { - where: { - organizationId, - tenantId, - members: { - employeeId, - tenantId, - organizationId, - role: { name: RolesEnum.MANAGER } - } - } - }); - } - } catch (error) { - throw new ForbiddenException(); - } - } - try { // Retrieve members and managers IDs - const managerIds = managers.map((manager) => manager.employeeId); - const memberIds = members.map((member) => member.employeeId); if (isNotEmpty(memberIds) || isNotEmpty(managerIds)) { - // Find the manager role - const role = await this._roleService.findOneByWhereOptions({ - name: RolesEnum.MANAGER - }); + // Combine memberIds and managerIds into a single array + const employeeIds = [...memberIds, ...managerIds].filter(Boolean); // Retrieves a collection of employees based on specified criteria. - const projectMembers = await this.retrieveEmployees(memberIds, managerIds, organizationId, tenantId); + const projectMembers = await this._employeeService.findActiveEmployeesByEmployeeIds( + employeeIds, + organizationId, + tenantId + ); - // Update nested entity - await this.updateOrganizationProject(id, organizationId, projectMembers, role, managerIds, memberIds); + // Update nested entity (Organization Project Members) + await this.updateOrganizationProjectMembers(id, organizationId, projectMembers, managerIds, memberIds); } const { id: organizationProjectId } = organizationProject; @@ -254,7 +212,8 @@ export class OrganizationProjectService extends TenantAwareCrudService */ - async updateOrganizationProject( + async updateOrganizationProjectMembers( organizationProjectId: ID, organizationId: ID, employees: IEmployee[], - role: Role, - managerIds: string[], - memberIds: string[] + managerIds: ID[], + memberIds: ID[] ): Promise { const tenantId = RequestContext.currentTenantId(); - const membersToUpdate = [...managerIds, ...memberIds]; + const membersToUpdate = new Set([...managerIds, ...memberIds].filter(Boolean)); + + // Find the manager role + const managerRole = await this._roleService.findOneByWhereOptions({ + name: RolesEnum.MANAGER + }); // Fetch existing project members with their roles const projectMembers = await this.typeOrmOrganizationProjectEmployeeRepository.find({ - where: { tenantId, organizationId, organizationProjectId }, - relations: { role: true } + where: { tenantId, organizationId, organizationProjectId } }); - // Create a map for fast lookup of current project members + // Create a map of existing members for quick lookup const existingMemberMap = new Map(projectMembers.map((member) => [member.employeeId, member])); - // Separate members to remove and to update - const removedMembers = projectMembers.filter((member) => !membersToUpdate.includes(member.employeeId)); - const updatedMembers = projectMembers.filter((member) => membersToUpdate.includes(member.employeeId)); + // Separate members into removed, updated, and new members + const removedMembers = projectMembers.filter((member) => !membersToUpdate.has(member.employeeId)); + const updatedMembers = projectMembers.filter((member) => membersToUpdate.has(member.employeeId)); + const newMembers = employees.filter((employee) => !existingMemberMap.has(employee.id)); - // 1. Remove members who are no longer assigned to project - if (removedMembers.length > 0) { - const removedMemberIds = removedMembers.map((member) => member.id); - await this.deleteMemberByIds(removedMemberIds); + // 1. Remove members who are no longer assigned to the project + if (removedMembers.length) { + await this.deleteMemberByIds(removedMembers.map((member) => member.id)); } - // 2. Update role for existing members - for (const member of updatedMembers) { - const isManager = managerIds.includes(member.employeeId); - const newRole = isManager ? role : member.role?.id === role.id ? member.role : null; + // 2. Update roles for existing members where necessary + await Promise.all( + updatedMembers.map(async (member) => { + const isManager = managerIds.includes(member.employeeId); + const newRole = isManager ? managerRole : null; - // Only update if the role is different - if (newRole && newRole.id !== member.roleId) { - await this.typeOrmOrganizationProjectEmployeeRepository.update(member.id, { role: newRole }); - } - } + // Only update if the role has changed + if (newRole && newRole.id !== member.roleId) { + await this.typeOrmOrganizationProjectEmployeeRepository.update(member.id, { role: newRole }); + } + }) + ); // 3. Add new members to the project - const newMembers = employees.filter((employee) => !existingMemberMap.has(employee.id)); - - if (newMembers.length > 0) { + if (newMembers.length) { const newProjectMembers = newMembers.map( (employee) => new OrganizationProjectEmployee({ @@ -338,7 +299,7 @@ export class OrganizationProjectService extends TenantAwareCrudService - + >
@@ -321,8 +316,7 @@ [selectedTags]="form.get('tags').value" (selectedTagsEvent)="selectedTagsEvent($event)" [isOrgLevel]="true" - > - + >
diff --git a/packages/ui-core/shared/src/lib/project/project-mutation/project-mutation.component.ts b/packages/ui-core/shared/src/lib/project/project-mutation/project-mutation.component.ts index b882b805cbf..5db4d43f295 100644 --- a/packages/ui-core/shared/src/lib/project/project-mutation/project-mutation.component.ts +++ b/packages/ui-core/shared/src/lib/project/project-mutation/project-mutation.component.ts @@ -9,7 +9,6 @@ import { Router } from '@angular/router'; import { uniq } from 'underscore'; import { environment } from '@gauzy/ui-config'; import { - IEmployee, IOrganization, IOrganizationContact, ProjectBillingEnum, @@ -29,7 +28,8 @@ import { IIntegrationMapSyncRepository, IOrganizationGithubRepository, SYNC_TAG_GAUZY, - IOrganizationProjectEmployee + IOrganizationProjectEmployee, + ID } from '@gauzy/contracts'; import { GithubService, @@ -57,16 +57,15 @@ export class ProjectMutationComponent extends TranslationBaseComponent implement public FormHelpers: typeof FormHelpers = FormHelpers; public OrganizationProjectBudgetTypeEnum = OrganizationProjectBudgetTypeEnum; public TaskListTypeEnum = TaskListTypeEnum; - public members: string[] = []; - public selectedEmployeeIds: string[] = []; - public selectedTeamIds: string[] = []; + public memberIds: ID[] = []; + public selectedEmployeeIds: ID[] = []; + public selectedTeamIds: ID[] = []; public billings: string[] = Object.values(ProjectBillingEnum); public owners: ProjectOwnerEnum[] = Object.values(ProjectOwnerEnum); public taskViewModeTypes: TaskListTypeEnum[] = Object.values(TaskListTypeEnum); public showSprintManage = false; public ckConfig: CKEditor4.Config = ckEditorConfig; public organization: IOrganization; - public employees: IEmployee[] = []; public hoverState: boolean; public loading: boolean; @@ -159,6 +158,18 @@ export class ProjectMutationComponent extends TranslationBaseComponent implement */ @ViewChild('actionButtons', { static: true }) actionButtons: TemplateRef; + public get projectName(): AbstractControl { + return this.form.get('name'); + } + + public get projectUrl(): AbstractControl { + return this.form.get('projectUrl'); + } + + public get openSourceProjectUrl(): AbstractControl { + return this.form.get('openSourceProjectUrl'); + } + constructor( private readonly _router: Router, private readonly _fb: UntypedFormBuilder, @@ -292,7 +303,7 @@ export class ProjectMutationComponent extends TranslationBaseComponent implement // Selected Members Ids this.selectedEmployeeIds = project.members.map((member: IOrganizationProjectEmployee) => member.employeeId); - this.members = this.selectedEmployeeIds; + this.memberIds = this.selectedEmployeeIds; this.form.patchValue({ imageUrl: project.imageUrl || null, @@ -356,118 +367,171 @@ export class ProjectMutationComponent extends TranslationBaseComponent implement this.form.get('openSource').updateValueAndValidity(); } - onMembersSelected(members: string[]) { - this.members = members; + /** + * Handles the selection of members and updates the `memberIds` property. + * + * @param {ID[]} memberIds - An array of selected member IDs. + * The function is called when members are selected, and it sets the `memberIds` property + * with the array of selected IDs. + */ + onMembersSelected(memberIds: ID[]): void { + this.memberIds = memberIds; } - onTeamsSelected(teams: IOrganizationTeam[]) { + /** + * Updates the form's teams field with the selected organization teams. + * + * @param {IOrganizationTeam[]} teams - An array of selected organization teams. + */ + onTeamsSelected(teams: IOrganizationTeam[]): void { this.form.get('teams').setValue(teams); this.form.get('teams').updateValueAndValidity(); } /** + * Navigates to the organization projects page, canceling the current project workflow. * + * This method is typically called when the user decides to cancel the project creation/edit process. */ - navigateToCancelProject() { - this._router.navigate([`/pages/organization/projects`]); + navigateToCancelProject(): void { + this._router.navigate(['/pages/organization/projects']); } /** - * On submit project mutation form + * Handles the submission of the project mutation form. * - * @returns + * @returns void */ - onSubmit() { + onSubmit(): void { if (this.form.invalid) { return; } - const { name, code, projectUrl, owner, organizationContact, startDate, endDate } = this.form.value; - const { description, tags } = this.form.value; - const { billing, currency } = this.form.value; - const { budget, budgetType } = this.form.value; - const { openSource, openSourceProjectUrl } = this.form.value; - const { color, taskListType, public: isPublic, billable } = this.form.value; - const { imageId } = this.form.value; + // Emit the form values + this.onSubmitted.emit(this.getFormValues()); + } - this.onSubmitted.emit({ + /** + * Extracts and processes form values for submission. + * + * @returns {object} - The processed form values. + */ + private getFormValues(): object { + // Destructure the form values in one step + const { + name, + code, + projectUrl, + owner, + organizationContact, + startDate, + endDate, + description, + tags, + billing, + currency, + budget, + budgetType, + openSource, + openSourceProjectUrl, + color, + taskListType, + public: isPublic, + billable, + imageId, + teams + } = this.form.value; + + return { // Main Step - name: name, - code: code, - projectUrl: projectUrl, - owner: owner, - organizationContactId: organizationContact ? organizationContact.id : null, - startDate: startDate, - endDate: endDate, - members: this.members.map((id) => this.employees.find((e) => e.id === id)).filter((e) => !!e), - teams: this.form - .get('teams') - .value.map((id) => this.teams.find((e) => e.id === id)) - .filter((e) => !!e), + name, + code, + projectUrl, + owner, + organizationContactId: organizationContact?.id || null, + startDate, + endDate, + memberIds: this.memberIds, + teams: teams.map((id) => this.teams.find((team) => team.id === id)).filter(Boolean), + // Description Step - description: description, + description, tags: tags || [], // Billing Step - billing: billing, - billingFlat: billing === ProjectBillingEnum.RATE || billing === ProjectBillingEnum.FLAT_FEE ? true : false, - currency: currency, + billing, + billingFlat: [ProjectBillingEnum.RATE, ProjectBillingEnum.FLAT_FEE].includes(billing), + currency, // Budget Step - budget: budget, - budgetType: budgetType, + budget, + budgetType, // Open Source Step - openSource: openSource, - openSourceProjectUrl: openSourceProjectUrl, + openSource, + openSourceProjectUrl, // Setting Step - color: color, - taskListType: taskListType, + color, + taskListType, public: isPublic, - billable: billable, + billable, + // Image Step imageId - }); + }; } - selectedTagsEvent(selectedTags: ITag[]) { + /** + * Updates the form's tags field with the selected tags. + * + * @param {ITag[]} selectedTags - An array of selected tags. + */ + selectedTagsEvent(selectedTags: ITag[]): void { this.form.get('tags').setValue(selectedTags); this.form.get('tags').updateValueAndValidity(); } /** - * Add organization contact + * Adds a new organization contact with the provided name. * - * @param name - * @returns + * @param {string} name - The name of the new organization contact. + * @returns {Promise} - Returns a promise that resolves to the created organization contact. + * + * @throws {Error} - Handles errors using the error handler service if the contact creation fails. */ addNewOrganizationContact = async (name: string): Promise => { try { const { id: organizationId, tenantId } = this.organization; + // Create a new organization contact const contact: IOrganizationContact = await this._organizationContactService.create({ name, organizationId, tenantId, contactType: ContactType.CLIENT }); + + // Display a success message if the contact is created if (contact) { const { name } = contact; - this._toastrService.success('NOTES.ORGANIZATIONS.EDIT_ORGANIZATIONS_CONTACTS.ADD_CONTACT', { - name - }); + this._toastrService.success('NOTES.ORGANIZATIONS.EDIT_ORGANIZATIONS_CONTACTS.ADD_CONTACT', { name }); } return contact; } catch (error) { + // Handle any errors that occur during the contact creation process this._errorHandler.handleError(error); } }; + /** + * Navigates to the tasks settings page for the selected project. + */ openTasksSettings(): void { - this._router.navigate(['/pages/tasks/settings', this.project.id], { - state: this.project - }); + // Get the selected project + const project = this.project; + // Navigate to the tasks settings page with the selected project + this._router.navigate(['/pages/tasks/settings', project.id], { state: project }); } /* @@ -475,27 +539,6 @@ export class ProjectMutationComponent extends TranslationBaseComponent implement */ currencyChanged($event: ICurrency) {} - /** - * Load employees from multiple selected employees - * - * @param employees - */ - public onLoadEmployees(employees: IEmployee[]) { - this.employees = employees; - } - - public get projectName(): AbstractControl { - return this.form.get('name'); - } - - public get projectUrl(): AbstractControl { - return this.form.get('projectUrl'); - } - - public get openSourceProjectUrl(): AbstractControl { - return this.form.get('openSourceProjectUrl'); - } - /** * Upload project logo * diff --git a/packages/ui-core/shared/src/lib/tags/tags-color-input/tags-color-input.component.html b/packages/ui-core/shared/src/lib/tags/tags-color-input/tags-color-input.component.html index 5cc78d72514..e966bbae66d 100644 --- a/packages/ui-core/shared/src/lib/tags/tags-color-input/tags-color-input.component.html +++ b/packages/ui-core/shared/src/lib/tags/tags-color-input/tags-color-input.component.html @@ -42,7 +42,7 @@ ...
- +
Date: Fri, 27 Sep 2024 19:48:14 +0530 Subject: [PATCH 24/28] fix: removed unnecessary console --- .../src/organization-project/organization-project.service.ts | 2 +- packages/core/src/tasks/priorities/priority.subscriber.ts | 1 - packages/core/src/tasks/sizes/size.subscriber.ts | 1 - packages/core/src/tasks/statuses/status.subscriber.ts | 1 - packages/core/src/tasks/versions/version.subscriber.ts | 1 - 5 files changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/core/src/organization-project/organization-project.service.ts b/packages/core/src/organization-project/organization-project.service.ts index 7d33718d46b..06ca85814cd 100644 --- a/packages/core/src/organization-project/organization-project.service.ts +++ b/packages/core/src/organization-project/organization-project.service.ts @@ -21,7 +21,7 @@ import { getConfig } from '@gauzy/config'; import { CustomEmbeddedFieldConfig, isNotEmpty } from '@gauzy/common'; import { PaginationParams, TenantAwareCrudService } from '../core/crud'; import { RequestContext } from '../core/context'; -import { OrganizationProjectEmployee, Role } from '../core/entities/internal'; +import { OrganizationProjectEmployee } from '../core/entities/internal'; import { FavoriteService } from '../core/decorators'; import { ActivityLogEvent } from '../activity-log/events'; import { generateActivityLogDescription } from '../activity-log/activity-log.helper'; diff --git a/packages/core/src/tasks/priorities/priority.subscriber.ts b/packages/core/src/tasks/priorities/priority.subscriber.ts index 722a75f6d64..ab01d124636 100644 --- a/packages/core/src/tasks/priorities/priority.subscriber.ts +++ b/packages/core/src/tasks/priorities/priority.subscriber.ts @@ -26,7 +26,6 @@ export class TaskPrioritySubscriber extends BaseEntityEventSubscriber { try { // Update the fullIconUrl if an icon property is present if (Object.prototype.hasOwnProperty.call(entity, 'icon')) { - console.log('TaskSize: Setting fullIconUrl for task size ID ' + entity.id); await this.setFullIconUrl(entity); } } catch (error) { diff --git a/packages/core/src/tasks/statuses/status.subscriber.ts b/packages/core/src/tasks/statuses/status.subscriber.ts index 457b0402123..faa866a708d 100644 --- a/packages/core/src/tasks/statuses/status.subscriber.ts +++ b/packages/core/src/tasks/statuses/status.subscriber.ts @@ -26,7 +26,6 @@ export class TaskStatusSubscriber extends BaseEntityEventSubscriber try { // Update the fullIconUrl if an icon is present if (Object.prototype.hasOwnProperty.call(entity, 'icon')) { - console.log('TaskStatus: Setting fullIconUrl for task status ID ' + entity.id); await this.setFullIconUrl(entity); } } catch (error) { diff --git a/packages/core/src/tasks/versions/version.subscriber.ts b/packages/core/src/tasks/versions/version.subscriber.ts index 66f8643b121..f53de99a3de 100644 --- a/packages/core/src/tasks/versions/version.subscriber.ts +++ b/packages/core/src/tasks/versions/version.subscriber.ts @@ -26,7 +26,6 @@ export class TaskVersionSubscriber extends BaseEntityEventSubscriber Date: Fri, 27 Sep 2024 21:49:39 +0530 Subject: [PATCH 25/28] fix: packages build --- .../src/lib/upwork.service.ts | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/packages/plugins/integration-upwork/src/lib/upwork.service.ts b/packages/plugins/integration-upwork/src/lib/upwork.service.ts index eb70509c0e6..b63b220cceb 100644 --- a/packages/plugins/integration-upwork/src/lib/upwork.service.ts +++ b/packages/plugins/integration-upwork/src/lib/upwork.service.ts @@ -348,7 +348,7 @@ export class UpworkService { engagement_end_date, active_milestone }) => { - const payload = { + const input = { name, organizationId, public: true, @@ -356,18 +356,18 @@ export class UpworkService { }; if (isObject(active_milestone)) { - payload['billing'] = ProjectBillingEnum.MILESTONES; + input['billing'] = ProjectBillingEnum.MILESTONES; } else { - payload['billing'] = ProjectBillingEnum.RATE; + input['billing'] = ProjectBillingEnum.RATE; } // contract start date if (typeof engagement_start_date === 'string' && engagement_start_date.length > 0) { - payload['startDate'] = new Date(unixTimestampToDate(engagement_start_date)); + input['startDate'] = new Date(unixTimestampToDate(engagement_start_date)); } // contract end date if (typeof engagement_end_date === 'string' && engagement_end_date.length > 0) { - payload['endDate'] = new Date(unixTimestampToDate(engagement_end_date)); + input['endDate'] = new Date(unixTimestampToDate(engagement_end_date)); } const tenantId = RequestContext.currentTenantId(); @@ -382,16 +382,12 @@ export class UpworkService { //if project already integrated then only update model/entity if (integrationMap) { await this._commandBus.execute( - new OrganizationProjectUpdateCommand( - Object.assign(payload, { - id: integrationMap.gauzyId - }) - ) + new OrganizationProjectUpdateCommand(integrationMap.gauzyId, input) ); return integrationMap; } const project = await this._commandBus.execute( - new OrganizationProjectCreateCommand(Object.assign({}, payload)) + new OrganizationProjectCreateCommand(Object.assign({}, input)) ); return await this._commandBus.execute( new IntegrationMapSyncEntityCommand({ From dccb73d66c488e6ef37e3815a4f942e14bd87e82 Mon Sep 17 00:00:00 2001 From: Kifungo A <45813955+adkif@users.noreply.github.com> Date: Fri, 27 Sep 2024 19:34:23 +0200 Subject: [PATCH 26/28] fix: styles for switch theme component, including toggle button layout, icon display, and animation. --- .../switch-theme/switch-theme.component.scss | 144 ++++++++++-------- 1 file changed, 83 insertions(+), 61 deletions(-) diff --git a/packages/desktop-ui-lib/src/lib/theme-selector/switch-theme/switch-theme.component.scss b/packages/desktop-ui-lib/src/lib/theme-selector/switch-theme/switch-theme.component.scss index 8792a97ac2a..0023d6cffc7 100644 --- a/packages/desktop-ui-lib/src/lib/theme-selector/switch-theme/switch-theme.component.scss +++ b/packages/desktop-ui-lib/src/lib/theme-selector/switch-theme/switch-theme.component.scss @@ -1,74 +1,96 @@ @import 'var'; :host .switch-container { - display: flex; - justify-content: space-between; - align-items: center; - flex-wrap: wrap; + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; - nb-toggle.switch ::ng-deep .toggle { - height: 1.5rem; - width: 3.375rem; - font-family: 'Font Awesome 6 Free' !important; - font-size: .625rem; - display: flex; - align-items: center; - background-color: rgba(126, 126, 143, 0.5); - border-width: 0; - .toggle-switcher{ - height: 1rem; - width: 1rem; - } - span { - display: flex; - justify-content: center; - align-items: center; - justify-self: flex-end; - } + nb-toggle.switch ::ng-deep .toggle { + height: 1.5rem; + width: 3.375rem; + font-family: 'Font Awesome 6 Free' !important; + font-size: .625rem; + display: flex; + align-items: center; + background-color: rgba(126, 126, 143, 0.5); + border-width: 0; - nb-icon{ - display: none; - } - } - nb-toggle.light ::ng-deep .toggle { - justify-content: end; - &:before { - content: '\f186'; - @include nb-rtl(margin-left, .275rem); - @include nb-ltr(margin-right, .275rem); - color: rgba(255, 255, 255, 0.25); - } - span { - &:before { - content: '\f185'; - } - } - .toggle-switcher{ - @include nb-ltr(margin-left, .25rem); - @include nb-rtl(margin-right, .25rem); - } - } + .toggle-switcher { + height: 1rem; + width: 1rem; + @include nb-rtl(left, 3px !important); + @include nb-ltr(right, 3px !important); + } - nb-toggle.dark ::ng-deep .toggle { - justify-content: flex-start; - &:before { - content: '\f185'; - @include nb-ltr(margin-left, .325rem); - @include nb-rtl(margin-right, .325rem); - color: rgba(255, 255, 255, 0.25); - } - span { - &:before { - content: '\f186'; - } - } - } + span { + display: flex; + justify-content: center; + align-items: center; + justify-self: flex-end; + } + + nb-icon { + display: none; + } + } + + nb-toggle.light ::ng-deep .toggle { + justify-content: end; + + &:before { + content: '\f186'; + position: absolute; + @include nb-rtl(right, 6px); + @include nb-ltr(left, 6px); + color: rgba(255, 255, 255, 0.5); + animation: slide 0.4s ease-out forwards; + } + + span { + &:before { + content: '\f185'; + } + } + + .toggle-switcher { + @include nb-ltr(margin-left, .25rem); + @include nb-rtl(margin-right, .25rem); + } + } + + nb-toggle.dark ::ng-deep .toggle { + justify-content: flex-start; + + &:before { + content: '\f185'; + @include nb-ltr(margin-left, .325rem); + @include nb-rtl(margin-right, .325rem); + color: rgba(255, 255, 255, 0.5); + animation: slide 0.4s ease-out forwards; + } + + span { + &:before { + content: '\f186'; + } + } + } } ::ng-deep [dir=ltr] nb-toggle.switch .toggle.checked .toggle-switcher { - left: calc(100% - 1.375rem); + left: calc(100% - 1.375rem); } ::ng-deep [dir=rtl] nb-toggle.switch .toggle.checked .toggle-switcher { - right: calc(100% - 1.375rem); + right: calc(100% - 1.375rem); } + +@keyframes slide { + from { + transform: translateX(300%); + } + to { + transform: translateX(0%); + } + } From 417bb095070d5b798e855cbe6ab1fadf76a0d868 Mon Sep 17 00:00:00 2001 From: Kifungo A <45813955+adkif@users.noreply.github.com> Date: Sat, 28 Sep 2024 00:09:21 +0500 Subject: [PATCH 27/28] [Refactor] Timer Stop flow and updated activity building (#8269) * refactor: add remote timer check, updated activity building, and split screenshot taking and uploading into separate steps * refactor: modify createTimeSlot method to return timeSlotId and error, and adjust calling code accordingly * fix: deepscan * fix: comment in TimeTrackerComponent to change 'Check if is a remote timer.' to 'Make sure it's not a remote timer' * refactor: time tracker component to reorganize stop timer logic and add network processing * fix: typo in comment: "networtk" -> "network" --- .../time-tracker/time-tracker.component.ts | 134 ++++++++++++------ 1 file changed, 90 insertions(+), 44 deletions(-) diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.ts b/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.ts index 3cc1efdc863..7b1a3754c27 100644 --- a/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.ts +++ b/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.ts @@ -1516,33 +1516,65 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit { const config = { quitApp: this.quitApp, isEmergency }; + let timer; + this.electronService.ipcRenderer.send('stop-capture-screen'); if (this._startMode === TimerStartMode.MANUAL) { console.log('Taking screen capture'); - + // Collect activities const activities = await this.electronService.ipcRenderer.invoke('COLLECT_ACTIVITIES', config); - - asyncScheduler.schedule(async () => { + // Stop timer locally and return current state of timer + console.log('Stopping timer'); + timer = await this.electronService.ipcRenderer.invoke('STOP_TIMER', config); + // Update tray + console.log('Updating Tray stop'); + this.electronService.ipcRenderer.send('update_tray_stop'); + // Update view + this.start$.next(false); + // Stop loading + this.loading = false; + /* + Start network processing... + */ + // Make sure it's not a remote timer + if (!this.isRemoteTimer) { this._loggerService.info('Capturing Screen and Sending Activities Start...', activities); - await this.takeCaptureAndSendActivities(activities); - this._loggerService.info('Capturing Screen and Sending Activities Done ✔️'); - }, 1000); + // Create time slot and return time slot ID and error + const { timeSlotId, error } = await this.createTimeSlot(activities); + // Upload screenshots if available + asyncScheduler.schedule(async () => { + // Take Screenshots + const screenshots = await this.takeScreenCapture(activities); + //Check there's an error + if (error) { + // handle create time slot error + this.handleCreateTimeSlotError(error, activities, screenshots); + } + // Upload screenshots if timeslot and screenshots is available + if (timeSlotId && screenshots?.length > 0) { + await this.uploadScreenshots(activities, timeSlotId, screenshots); + this._loggerService.info('Capturing Screen and Sending Activities Done ✔️'); + } + }, 1000); + } + } else { + // Stop timer locally and return current state + timer = await this.electronService.ipcRenderer.invoke('STOP_TIMER', config); + // Update tray + console.log('Updating Tray stop'); + this.electronService.ipcRenderer.send('update_tray_stop'); + // Update view + this.start$.next(false); + // Stop loading + this.loading = false; } - - console.log('Stopping timer'); - const timer = await this.electronService.ipcRenderer.invoke('STOP_TIMER', config); - + // Stop timer on server console.log('Toggling timer'); await this._toggle(timer, onClick); - - this.start$.next(false); - - this.loading = false; - - console.log('Updating Tray stop'); - - this.electronService.ipcRenderer.send('update_tray_stop'); + /** + * End network processing + */ this._startMode = TimerStartMode.STOP; @@ -1967,28 +1999,38 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit { } } - private buildParamActivity(arg) { + private buildParamActivity(arg: any) { + const { user, organizationId, tenantId } = this._store; + const { + employeeId, + projectId, + duration, + keyboard, + mouse, + system: overall, + startedAt, + activities, + timeLogId, + organizationContactId + } = arg ?? {}; + return { - employeeId: arg.employeeId, - projectId: arg.projectId, - duration: arg.duration, - keyboard: arg.keyboard, - mouse: arg.mouse, - overall: arg.system, - startedAt: arg.startedAt, - activities: arg.activities, - timeLogId: arg.timeLogId, - organizationId: arg.organizationId, - tenantId: arg.tenantId, - organizationContactId: arg.organizationContactId, - apiHost: arg.apiHost, - token: arg.token, - isAw: arg.isAw, - isAwConnected: arg.isAwConnected + employeeId: employeeId ?? user?.employee?.id, + projectId: projectId ?? null, + duration: duration ?? 0, + keyboard: keyboard ?? 0, + mouse: mouse ?? 0, + overall: overall ?? null, + startedAt: startedAt ?? null, + activities: activities ?? [], + timeLogId: timeLogId ?? null, + organizationId: arg?.organizationId ?? organizationId, + tenantId: arg?.tenantId ?? tenantId, + organizationContactId: organizationContactId ?? null }; } - public async createTimeSlot(arg, screenshots: any[]) { + public async createTimeSlot(arg): Promise<{ timeSlotId: string; error: any }> { try { this._loggerService.info('Build Param Activity'); const paramActivity = this.buildParamActivity(arg); @@ -2023,23 +2065,27 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit { b64Imgs: [] }); - return timeSlotId; + return { timeSlotId, error: null }; } catch (error) { - this.handleCreateTimeSlotError(error, arg, screenshots); - return null; + return { timeSlotId: null, error }; } } public async takeCaptureAndSendActivities(activities) { // Check validations - if (this.checkSendActivitiesValidationFail(activities)) return; + if (this.isRemoteTimer) return; + // Create time slot and return time slot ID + const { timeSlotId, error } = await this.createTimeSlot(activities); // Take Screenshots const screenshots = await this.takeScreenCapture(activities); - // Create time slot and return time slot ID - const timeslotId = await this.createTimeSlot(activities, screenshots); + // Check if error + if (error) { + // handle create time slot error + this.handleCreateTimeSlotError(error, activities, screenshots); + } // Upload screenshots if available - if (timeslotId && screenshots.length > 0) { - await this.uploadScreenshots(activities, timeslotId, screenshots); + if (timeSlotId && screenshots?.length > 0) { + await this.uploadScreenshots(activities, timeSlotId, screenshots); } } From ccd993ac5707d9095e6ef7fdb2013e701c117273 Mon Sep 17 00:00:00 2001 From: Kifungo A <45813955+adkif@users.noreply.github.com> Date: Sat, 28 Sep 2024 00:23:39 +0500 Subject: [PATCH 28/28] [Perf] Introduce server side pagination for selectors (#8274) * feat: create routing for tabs * feat: improve lanaguage service * feat: create cached server data server * feat: caching task for 24 hours * feat: add new observable to `selectorService` * feat: change modidier private to public * feat: improve create task dialog * feat: create task table features * feat: replace table with router tabset * fix: deepscan * feat: add pagination type to task cache * feat: improve selector with pagination params * feat: add new input to generic ng selector * feat: update ISelector interface * feat: introduce server side pagination for task selectors * feat: infinite scroll on client selector * feat: removed 'tags' from relations in client selector request and updated method call to getClientWithPagination * refactor: code in various components and services to improve pagination and caching functionality * feat: add reset page functionality to client, project, and team selectors; updated task table component to use selected project and team IDs; refactored time tracker service to use toParams function * fix: remove "prefix" property from search term object in task table component. * feat: updated TimeTrackerService to use TaskStatisticsCacheService * fix: 'tags' from relations and modified join and where clauses in TimeTrackerService * feat: add search component with debounce and loading indicator, updated task table component to display loading spinner, and refactored cached server data source to handle loading state * fix: removed spinner from search component, updated search component to use distinctUntilChange and filter, and removed loading state handling from task table component * feat: update SelectComponent to use Subject for typeahead instead of any" * fix: add search functionality to TaskSelectorService and TaskSelectorComponent, updated TimeTrackerService to include search term in API request * fix: add search functionality to client selector, project selector, and team selector components and services * feat: add typeToSearchText input property to SelectComponent and updated template to use it * refactor: moved nbSpinner to separate div, added absolute positioning and dimensions to .smart-table class * fix: relations and join from TimeTrackerService query parameters, update employeeId condition * refactored: tasks component, replaced ng-select with gauzy-select, updated imports and modules, and made minor template changes. * fix: Task Table component HTML and SCSS: wrapped spinner in ng-container, changed smart-table styles to use absolute positioning and added padding. * fix: remove resetPage calls from client-selector, project-selector, and team-selector components. * refactor: selector components to extend AbstractSelectorComponent, update search logic, and add NG_VALUE_ACCESSOR provider * feat: use selectors as `formControl` for reactive forms * refactor: selectors and make it reusable for reactive form and template binding form * refactor: selector store and components, update time tracker component and dialog * fix: max-width property * fix: cspell * Update project-cache.service.ts * Update client-cache.service.ts * Update teams-cache.service.ts --------- Co-authored-by: Ruslan Konviser --- .cspell.json | 3 +- .../src/lib/services/client-cache.service.ts | 7 +- .../desktop-ui-lib/src/lib/services/index.ts | 36 +-- .../src/lib/services/project-cache.service.ts | 11 +- .../src/lib/services/task-cache.service.ts | 6 +- .../services/task-statistics-cache.service.ts | 16 + .../src/lib/services/teams-cache.service.ts | 11 +- .../src/lib/shared/+state/selector.query.ts | 20 ++ .../src/lib/shared/+state/selector.service.ts | 18 +- .../src/lib/shared/+state/selector.store.ts | 16 +- .../components/abstract/selector.abstract.ts | 72 +++++ .../ui/select/select.component.html | 3 + .../components/ui/select/select.component.ts | 63 +++- .../+state/client-selector.service.ts | 31 +- .../+state/client-selector.store.ts | 5 +- .../client-selector.component.html | 2 + .../client-selector.component.ts | 56 ++-- .../note/+state/note-selector.store.ts | 4 +- .../features/note/+state/note.service.ts | 6 +- .../shared/features/note/note.component.ts | 41 ++- .../+state/project-selector.service.ts | 22 +- .../+state/project-selector.store.ts | 5 +- .../project-selector.component.html | 2 + .../project-selector.component.ts | 51 ++-- .../+state/task-selector.service.ts | 22 +- .../+state/task-selector.store.ts | 5 +- .../task-selector.component.html | 2 + .../task-selector/task-selector.component.ts | 54 ++-- .../+state/team-selector.service.ts | 13 +- .../+state/team-selector.store.ts | 5 +- .../team-selector.component.html | 2 + .../team-selector/team-selector.component.ts | 53 ++-- .../shared/interfaces/selector.interface.ts | 5 +- .../src/lib/tasks/tasks.component.html | 280 +++--------------- .../src/lib/tasks/tasks.component.ts | 116 ++------ .../src/lib/tasks/tasks.module.ts | 44 +-- .../task-table/search/search.component.html | 1 - .../task-table/search/search.component.ts | 32 +- .../table/task-table.component.html | 3 + .../table/task-table.component.scss | 10 + .../task-table/table/task-table.component.ts | 27 +- .../task-table/task-table.module.ts | 4 +- .../time-tracker/time-tracker.component.ts | 17 +- .../lib/time-tracker/time-tracker.module.ts | 12 +- .../lib/time-tracker/time-tracker.service.ts | 175 ++++++++++- ...timer-tracker-change-dialog.component.html | 51 ++-- ...timer-tracker-change-dialog.component.scss | 15 + .../timer-tracker-change-dialog.component.ts | 61 +++- .../smart-table/cached-server.data-source.ts | 15 +- 49 files changed, 970 insertions(+), 561 deletions(-) create mode 100644 packages/desktop-ui-lib/src/lib/services/task-statistics-cache.service.ts create mode 100644 packages/desktop-ui-lib/src/lib/shared/components/abstract/selector.abstract.ts diff --git a/.cspell.json b/.cspell.json index 0e7abbb0716..19e7be763f2 100644 --- a/.cspell.json +++ b/.cspell.json @@ -517,7 +517,8 @@ "nbbutton", "xaxis", "wdth", - "concate" + "concate", + "typeahead" ], "useGitignore": true, "ignorePaths": [ diff --git a/packages/desktop-ui-lib/src/lib/services/client-cache.service.ts b/packages/desktop-ui-lib/src/lib/services/client-cache.service.ts index ecdb1e8c46f..546e1235f9b 100644 --- a/packages/desktop-ui-lib/src/lib/services/client-cache.service.ts +++ b/packages/desktop-ui-lib/src/lib/services/client-cache.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; import { AbstractCacheService } from './abstract-cache.service'; -import { IOrganizationContact } from '@gauzy/contracts'; +import { IOrganizationContact, IPagination } from '@gauzy/contracts'; import { StorageService } from './storage.service'; import { Store } from '../services'; @@ -8,13 +8,14 @@ import { Store } from '../services'; providedIn: 'root', }) export class ClientCacheService extends AbstractCacheService< - IOrganizationContact[] + IOrganizationContact[] | IPagination > { constructor( - protected _storageService: StorageService, + protected _storageService: StorageService>, protected _store: Store ) { super(_storageService, _store); this.prefix = ClientCacheService.name.toString(); + this.duration = 1 * 3600 * 1000; // 1 hour } } diff --git a/packages/desktop-ui-lib/src/lib/services/index.ts b/packages/desktop-ui-lib/src/lib/services/index.ts index 64d94285696..342dec0657c 100644 --- a/packages/desktop-ui-lib/src/lib/services/index.ts +++ b/packages/desktop-ui-lib/src/lib/services/index.ts @@ -1,29 +1,31 @@ export * from './abstract-cache.service'; export * from './client-cache.service'; export * from './employee-cache.service'; +export * from './error-client.service'; +export * from './error-handler.service'; +export * from './error-server.service'; +export * from './image-cache.service'; +export * from './language-cache.service'; +export * from './native-notification.service'; +export * from './notification.service'; export * from './organizations-cache.service'; export * from './project-cache.service'; +export * from './server-connection.service'; +export * from './status-icon-service/status-icon-cache.service'; +export * from './status-icon-service/status-icon.service'; export * from './storage.service'; +export * from './store.service'; export * from './tag-cache.service'; +export * from './tag.service'; export * from './task-cache.service'; +export * from './task-priority-cache.service'; +export * from './task-size-cache.service'; +export * from './task-statistics-cache.service'; +export * from './task-status-cache.service'; +export * from './teams-cache.service'; export * from './time-log-cache.service'; export * from './time-slot-cache.service'; -export * from './user-organization-cache.service'; -export * from './error-client.service'; -export * from './error-handler.service'; -export * from './error-server.service'; -export * from './native-notification.service'; -export * from './notification.service'; -export * from './toastr-notification.service'; -export * from './store.service'; -export * from './server-connection.service'; -export * from './image-cache.service'; -export * from './language-cache.service'; export * from './time-tracker-date.manager'; export * from './time-zone-manager'; -export * from './task-status-cache.service'; -export * from './teams-cache.service'; -export * from './status-icon-service'; -export * from './task-priority-cache.service'; -export * from './task-size-cache.service'; -export * from './tag.service'; +export * from './toastr-notification.service'; +export * from './user-organization-cache.service'; diff --git a/packages/desktop-ui-lib/src/lib/services/project-cache.service.ts b/packages/desktop-ui-lib/src/lib/services/project-cache.service.ts index 797c538c0a1..ee4e06d0805 100644 --- a/packages/desktop-ui-lib/src/lib/services/project-cache.service.ts +++ b/packages/desktop-ui-lib/src/lib/services/project-cache.service.ts @@ -1,20 +1,21 @@ import { Injectable } from '@angular/core'; +import { IOrganizationProject, IPagination } from '@gauzy/contracts'; +import { Store } from '../services'; import { AbstractCacheService } from './abstract-cache.service'; -import { IOrganizationProject } from '@gauzy/contracts'; import { StorageService } from './storage.service'; -import { Store } from '../services'; @Injectable({ - providedIn: 'root', + providedIn: 'root' }) export class ProjectCacheService extends AbstractCacheService< - IOrganizationProject[] + IOrganizationProject[] | IPagination > { constructor( - protected _storageService: StorageService, + protected _storageService: StorageService>, protected _store: Store ) { super(_storageService, _store); this.prefix = ProjectCacheService.name.toString(); + this.duration = 1 * 3600 * 1000; // 1 hour } } diff --git a/packages/desktop-ui-lib/src/lib/services/task-cache.service.ts b/packages/desktop-ui-lib/src/lib/services/task-cache.service.ts index 557c5a405e9..9b9a6232dbb 100644 --- a/packages/desktop-ui-lib/src/lib/services/task-cache.service.ts +++ b/packages/desktop-ui-lib/src/lib/services/task-cache.service.ts @@ -1,15 +1,15 @@ import { Injectable } from '@angular/core'; import { AbstractCacheService } from './abstract-cache.service'; -import { ITask } from '@gauzy/contracts'; +import { IPagination, ITask } from '@gauzy/contracts'; import { StorageService } from './storage.service'; import { Store } from '../services'; @Injectable({ providedIn: 'root', }) -export class TaskCacheService extends AbstractCacheService { +export class TaskCacheService extends AbstractCacheService> { constructor( - protected _storageService: StorageService, + protected _storageService: StorageService>, protected _store: Store ) { super(_storageService, _store); diff --git a/packages/desktop-ui-lib/src/lib/services/task-statistics-cache.service.ts b/packages/desktop-ui-lib/src/lib/services/task-statistics-cache.service.ts new file mode 100644 index 00000000000..be1c20a332f --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/services/task-statistics-cache.service.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@angular/core'; +import { ITasksStatistics } from '@gauzy/contracts'; +import { Store } from '.'; +import { AbstractCacheService } from './abstract-cache.service'; +import { StorageService } from './storage.service'; + +@Injectable({ + providedIn: 'root' +}) +export class TaskStatisticsCacheService extends AbstractCacheService { + constructor(protected _storageService: StorageService, protected _store: Store) { + super(_storageService, _store); + this.prefix = TaskStatisticsCacheService.name.toString(); + this.duration = 600 * 1000; // 1O minutes + } +} diff --git a/packages/desktop-ui-lib/src/lib/services/teams-cache.service.ts b/packages/desktop-ui-lib/src/lib/services/teams-cache.service.ts index b5af417bdab..0fdad0dd3b3 100644 --- a/packages/desktop-ui-lib/src/lib/services/teams-cache.service.ts +++ b/packages/desktop-ui-lib/src/lib/services/teams-cache.service.ts @@ -1,19 +1,18 @@ import { Injectable } from '@angular/core'; +import { IOrganizationTeam, IPagination } from '@gauzy/contracts'; import { StorageService, Store } from '../services'; -import { IOrganizationTeam } from '@gauzy/contracts'; import { AbstractCacheService } from './abstract-cache.service'; @Injectable({ - providedIn: 'root', + providedIn: 'root' }) -export class TeamsCacheService extends AbstractCacheService< - IOrganizationTeam[] -> { +export class TeamsCacheService extends AbstractCacheService> { constructor( - protected _storageService: StorageService, + protected _storageService: StorageService>, protected _store: Store ) { super(_storageService, _store); this.prefix = TeamsCacheService.name.toString(); + this.duration = 1 * 3600 * 1000; // 1 hour } } diff --git a/packages/desktop-ui-lib/src/lib/shared/+state/selector.query.ts b/packages/desktop-ui-lib/src/lib/shared/+state/selector.query.ts index 3a8a668f258..5fe84ad5f72 100644 --- a/packages/desktop-ui-lib/src/lib/shared/+state/selector.query.ts +++ b/packages/desktop-ui-lib/src/lib/shared/+state/selector.query.ts @@ -28,6 +28,26 @@ export abstract class SelectorQuery extends Query> { return this.getValue().selected; } + public get page(): number { + return this.getValue().page; + } + + public get page$(): Observable { + return this.select((state) => state.page); + } + + public get limit(): number { + return this.getValue().limit; + } + + public get total(): number { + return this.getValue().total; + } + + public get hasNext(): boolean { + return this.page * this.limit < this.total; + } + public get hasPermission$(): Observable { return this.select((state) => state.hasPermission); } diff --git a/packages/desktop-ui-lib/src/lib/shared/+state/selector.service.ts b/packages/desktop-ui-lib/src/lib/shared/+state/selector.service.ts index 622c9e0b5d7..d8a1daf8b28 100644 --- a/packages/desktop-ui-lib/src/lib/shared/+state/selector.service.ts +++ b/packages/desktop-ui-lib/src/lib/shared/+state/selector.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { Observable } from 'rxjs'; +import { concatMap, Observable } from 'rxjs'; import { SelectorQuery } from './selector.query'; import { SelectorStore } from './selector.store'; @@ -12,7 +12,7 @@ export abstract class SelectorService { public readonly selectorQuery: SelectorQuery ) {} - public abstract load(): Promise; + public abstract load(options?: { searchTerm?: string }): Promise; public getAll$(): Observable { return this.selectorQuery.data$; @@ -45,4 +45,18 @@ export abstract class SelectorService { public get selected$(): Observable { return this.selectorQuery.selected$; } + + public onScrollToEnd(): void { + if (this.selectorQuery.hasNext) { + this.selectorStore.next(); + } + } + + public get onScroll$(): Observable { + return this.selectorQuery.page$.pipe(concatMap(() => this.load())); + } + + public resetPage() { + this.selectorStore.update({ page: 1, data: [] }); + } } diff --git a/packages/desktop-ui-lib/src/lib/shared/+state/selector.store.ts b/packages/desktop-ui-lib/src/lib/shared/+state/selector.store.ts index 3cae1c8edd3..0b804c91a36 100644 --- a/packages/desktop-ui-lib/src/lib/shared/+state/selector.store.ts +++ b/packages/desktop-ui-lib/src/lib/shared/+state/selector.store.ts @@ -10,6 +10,15 @@ export abstract class SelectorStore extends Store> { this.update({ data }); } + public updateInfiniteList(list: { data: T[]; total: number }): void { + const { data, total } = list; + const items = this.getValue().data; + this.update({ + data: [...new Map([...items, ...data].map((item) => [item['id'], item])).values()], + total + }); + } + public updateSelected(selected: T | string): void { if (!selected) { this.update({ selected: null }); @@ -28,11 +37,16 @@ export abstract class SelectorStore extends Store> { return; } const data = this.getValue().data; + this.updateData([...new Map([...data, selected].map((item) => [item['id'], item])).values()]); this.updateSelected(selected); - this.updateData(data.concat([selected])); } public resetToInitialState(): void { this.update(this.initialState); } + + public next(): void { + const current = this.getValue().page; + this.update({ page: current + 1 }); + } } diff --git a/packages/desktop-ui-lib/src/lib/shared/components/abstract/selector.abstract.ts b/packages/desktop-ui-lib/src/lib/shared/components/abstract/selector.abstract.ts new file mode 100644 index 00000000000..ef1be0edd60 --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/shared/components/abstract/selector.abstract.ts @@ -0,0 +1,72 @@ +import { Component } from '@angular/core'; +import { ControlValueAccessor } from '@angular/forms'; +import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; +import { BehaviorSubject, Observable, Subject } from 'rxjs'; +import { debounceTime, distinctUntilChanged, switchMap, tap } from 'rxjs/operators'; +import { SelectorService } from '../../+state/selector.service'; + +@UntilDestroy({ checkProperties: true }) +@Component({ + template: '' +}) +export abstract class AbstractSelectorComponent implements ControlValueAccessor { + public search$ = new Subject(); + private onChange: (value: any) => void; + private onTouched: () => void; + protected isDisabled$ = new BehaviorSubject(false); + + // Flag to control whether to update the store + protected useStore: boolean = true; + + // Abstract members to be implemented in derived classes + public abstract data$: Observable; + public abstract selected$: Observable; + public abstract isLoading$: Observable; + public abstract disabled$: Observable; + public abstract hasPermission$: Observable; + + constructor() {} + + // Handle value change + public change(value: string): void { + this.onChange?.(value); // Notify the form control + this.onTouched?.(); + this.updateSelected(value); // Update the store only if useStore is true + } + + // Implement ControlValueAccessor methods + public writeValue(value: string): void { + this.useStore = false; // Disable store updates when used in a form + if (value) { + this.updateSelected(value); + } + } + + public registerOnChange(fn: any): void { + this.onChange = fn; + } + + public registerOnTouched(fn: any): void { + this.onTouched = fn; + } + + public setDisabledState(isDisabled: boolean): void { + this.isDisabled$.next(isDisabled); + } + + // Abstract method to update selected item + protected abstract updateSelected(value: string): void; + + // Common search handling logic + protected handleSearch(service: SelectorService) { + this.search$ + .pipe( + debounceTime(300), + distinctUntilChanged(), + tap(() => service.resetPage()), + switchMap((searchTerm) => service.load({ searchTerm })), + untilDestroyed(this) + ) + .subscribe(); + } +} diff --git a/packages/desktop-ui-lib/src/lib/shared/components/ui/select/select.component.html b/packages/desktop-ui-lib/src/lib/shared/components/ui/select/select.component.html index 61b53c178a5..79db78dccb4 100644 --- a/packages/desktop-ui-lib/src/lib/shared/components/ui/select/select.component.html +++ b/packages/desktop-ui-lib/src/lib/shared/components/ui/select/select.component.html @@ -17,6 +17,9 @@ [bindLabel]="bindLabel" [bindValue]="bindValue" nbTooltipStatus="warning" + [typeahead]="typeahead" + [typeToSearchText]="typeToSearchText | translate" + (scrollToEnd)="onScrollToEnd()" > diff --git a/packages/desktop-ui-lib/src/lib/shared/components/ui/select/select.component.ts b/packages/desktop-ui-lib/src/lib/shared/components/ui/select/select.component.ts index b454e4ae732..22491ea7264 100644 --- a/packages/desktop-ui-lib/src/lib/shared/components/ui/select/select.component.ts +++ b/packages/desktop-ui-lib/src/lib/shared/components/ui/select/select.component.ts @@ -1,12 +1,21 @@ -import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; +import { ChangeDetectionStrategy, Component, EventEmitter, forwardRef, Input, Output } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Subject } from 'rxjs'; @Component({ selector: 'gauzy-select', templateUrl: './select.component.html', styleUrls: ['./select.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => SelectComponent), + multi: true + } + ] }) -export class SelectComponent { +export class SelectComponent implements ControlValueAccessor { private _selectedItem: string = null; private _items: any[] = []; private _bindLabel: string = 'id'; @@ -20,10 +29,25 @@ export class SelectComponent { private _isLoading: boolean = false; private _addTagText: string = null; private _clearable: boolean = true; + private _typeToSearchText: string = 'FORM.PLACEHOLDERS.TYPE_SEARCH_REQUEST'; + private _typeahead!: Subject; private _addTag!: Function; @Output() clear = new EventEmitter(); @Output() modelChange = new EventEmitter(); + @Output() scrollToEnd = new EventEmitter(); + + onChange: (value: any) => void = () => {}; + onTouched: () => void = () => {}; + + // Getter and Setter for searchTextPlaceholder + @Input() + public get typeToSearchText(): string { + return this._typeToSearchText; + } + public set typeToSearchText(value: string) { + this._typeToSearchText = value; + } // Getter and Setter for selectedItem @Input() @@ -32,6 +56,7 @@ export class SelectComponent { } public set selectedItem(value: any) { this._selectedItem = value; + this.onTouched(); } // Getter and Setter for items @@ -151,13 +176,45 @@ export class SelectComponent { this._addTag = value; } + // Getter and Setter for selectedItem + @Input() + public get typeahead(): Subject { + return this._typeahead; + } + public set typeahead(value: Subject) { + this._typeahead = value; + } // Handle clear action public onClear() { this.clear.emit(); + this.onChange(null); } // Emit model change event public onModelChange(event: any) { this.modelChange.emit(event); + this.onChange(event); + } + + public onScrollToEnd() { + this.scrollToEnd.emit(); + } + + // ControlValueAccessor methods + writeValue(value: any): void { + this.selectedItem = value; + this.onChange(value); + } + + registerOnChange(fn: any): void { + this.onChange = fn; + } + + registerOnTouched(fn: any): void { + this.onTouched = fn; + } + + setDisabledState(isDisabled: boolean): void { + this._disabled = isDisabled; } } diff --git a/packages/desktop-ui-lib/src/lib/shared/features/client-selector/+state/client-selector.service.ts b/packages/desktop-ui-lib/src/lib/shared/features/client-selector/+state/client-selector.service.ts index 4360629f583..c75455cb178 100644 --- a/packages/desktop-ui-lib/src/lib/shared/features/client-selector/+state/client-selector.service.ts +++ b/packages/desktop-ui-lib/src/lib/shared/features/client-selector/+state/client-selector.service.ts @@ -33,6 +33,7 @@ export class ClientSelectorService extends SelectorService /* Creating a new contact for the organization. */ public async addContact(name: IOrganizationContact['name']): Promise { try { + this.selectorStore.setLoading(true); const { tenantId, organizationId, user } = this.store; const member: any = { ...user.employee }; const payload = { @@ -45,14 +46,19 @@ export class ClientSelectorService extends SelectorService const contact = await this.timeTrackerService.createNewContact(payload, user); this.selectorStore.appendData(contact); this.toastrNotifier.success(this.translateService.instant('TIMER_TRACKER.TOASTR.CLIENT_ADDED')); + this.selectorStore.setError(null); } catch (error) { console.error('ERROR', error); + this.selectorStore.setError(error); + } finally { + this.selectorStore.setLoading(false); } } - public async load(): Promise { + public async load(options?: { searchTerm?: string }): Promise { try { this.selectorStore.setLoading(true); + const { searchTerm: name } = options || {}; const { organizationId, tenantId, @@ -61,12 +67,25 @@ export class ClientSelectorService extends SelectorService } } = this.store; const request = { - organizationId, - employeeId, - tenantId + relations: ['projects.members', 'members.user', 'contact'], + join: { + alias: 'organization_contact', + leftJoin: { + members: 'organization_contact.members' + } + }, + where: { + organizationId, + tenantId, + contactType: ContactType.CLIENT, + members: [employeeId], + ...(name && { name }) + }, + take: this.selectorQuery.limit, + skip: this.selectorQuery.page }; - const data = await this.timeTrackerService.getClient(request); - this.selectorStore.updateData(data); + const { items: data, total } = await this.timeTrackerService.getPaginatedClients(request); + this.selectorStore.updateInfiniteList({ data, total }); this.selectorStore.setError(null); } catch (error) { this.toastrNotifier.error(error.message || 'An error occurred while fetching clients.'); diff --git a/packages/desktop-ui-lib/src/lib/shared/features/client-selector/+state/client-selector.store.ts b/packages/desktop-ui-lib/src/lib/shared/features/client-selector/+state/client-selector.store.ts index 9933ee2ca16..5f5b045e7f6 100644 --- a/packages/desktop-ui-lib/src/lib/shared/features/client-selector/+state/client-selector.store.ts +++ b/packages/desktop-ui-lib/src/lib/shared/features/client-selector/+state/client-selector.store.ts @@ -10,7 +10,10 @@ export function createInitialState(): IClientSelectorState { return { hasPermission: false, selected: null, - data: [] + data: [], + total: 0, + page: 1, + limit: 10 }; } diff --git a/packages/desktop-ui-lib/src/lib/shared/features/client-selector/client-selector.component.html b/packages/desktop-ui-lib/src/lib/shared/features/client-selector/client-selector.component.html index 97d0660f668..4ef926d1913 100644 --- a/packages/desktop-ui-lib/src/lib/shared/features/client-selector/client-selector.component.html +++ b/packages/desktop-ui-lib/src/lib/shared/features/client-selector/client-selector.component.html @@ -15,4 +15,6 @@ [isLoading]="isLoading$ | async" [disabled]="disabled$ | async" [hasError]="error$ | async" + [typeahead]="search$" + (scrollToEnd)="onShowMore()" > diff --git a/packages/desktop-ui-lib/src/lib/shared/features/client-selector/client-selector.component.ts b/packages/desktop-ui-lib/src/lib/shared/features/client-selector/client-selector.component.ts index 0f6c4b05b01..f3325773d06 100644 --- a/packages/desktop-ui-lib/src/lib/shared/features/client-selector/client-selector.component.ts +++ b/packages/desktop-ui-lib/src/lib/shared/features/client-selector/client-selector.component.ts @@ -1,12 +1,12 @@ -import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, forwardRef, OnInit } from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; +import { IOrganizationContact } from '@gauzy/contracts'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; -import { IOrganizationContact } from 'packages/contracts/dist'; -import { concatMap, filter, Observable, tap } from 'rxjs'; +import { combineLatest, concatMap, filter, map, Observable, tap } from 'rxjs'; import { ElectronService } from '../../../electron/services'; import { TimeTrackerQuery } from '../../../time-tracker/+state/time-tracker.query'; +import { AbstractSelectorComponent } from '../../components/abstract/selector.abstract'; import { ProjectSelectorService } from '../project-selector/+state/project-selector.service'; -import { TaskSelectorService } from '../task-selector/+state/task-selector.service'; -import { TeamSelectorService } from '../team-selector/+state/team-selector.service'; import { ClientSelectorQuery } from './+state/client-selector.query'; import { ClientSelectorService } from './+state/client-selector.service'; import { ClientSelectorStore } from './+state/client-selector.store'; @@ -16,38 +16,39 @@ import { ClientSelectorStore } from './+state/client-selector.store'; selector: 'gauzy-client-selector', templateUrl: './client-selector.component.html', styleUrls: ['./client-selector.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ClientSelectorComponent), + multi: true + } + ] }) -export class ClientSelectorComponent implements OnInit { +export class ClientSelectorComponent extends AbstractSelectorComponent implements OnInit { constructor( private readonly electronService: ElectronService, public readonly clientSelectorStore: ClientSelectorStore, public readonly clientSelectorQuery: ClientSelectorQuery, private readonly clientSelectorService: ClientSelectorService, private readonly projectSelectorService: ProjectSelectorService, - private readonly taskSelectorService: TaskSelectorService, - private readonly teamSelectorService: TeamSelectorService, private readonly timeTrackerQuery: TimeTrackerQuery - ) {} + ) { + super(); + } public ngOnInit(): void { - this.clientSelectorService - .getAll$() - .pipe( - filter((data) => !data.some((value) => value.id === this.clientSelectorService.selectedId)), - tap(() => (this.clientSelectorService.selected = null)), - untilDestroyed(this) - ) - .subscribe(); + this.clientSelectorService.onScroll$.pipe(untilDestroyed(this)).subscribe(); this.clientSelectorQuery.selected$ .pipe( filter(Boolean), + tap(() => this.projectSelectorService.resetPage()), concatMap(() => this.projectSelectorService.load()), - concatMap(() => this.taskSelectorService.load()), - concatMap(() => this.teamSelectorService.load()), untilDestroyed(this) ) .subscribe(); + // Handle search logic + this.handleSearch(this.clientSelectorService); } public refresh(): void { @@ -70,8 +71,11 @@ export class ClientSelectorComponent implements OnInit { return this.clientSelectorQuery.data$; } - public change(clientId: IOrganizationContact['id']) { - this.clientSelectorStore.updateSelected(clientId); + protected updateSelected(value: IOrganizationContact['id']): void { + // Update store only if useStore is true + if (this.useStore) { + this.clientSelectorStore.updateSelected(value); + } } public get isLoading$(): Observable { @@ -79,10 +83,16 @@ export class ClientSelectorComponent implements OnInit { } public get disabled$(): Observable { - return this.timeTrackerQuery.disabled$; + return combineLatest([this.timeTrackerQuery.disabled$, this.isDisabled$.asObservable()]).pipe( + map(([disabled, selectorDisabled]) => disabled || selectorDisabled) + ); } public get hasPermission$(): Observable { return this.clientSelectorService.hasPermission$; } + + public onShowMore(): void { + this.clientSelectorService.onScrollToEnd(); + } } diff --git a/packages/desktop-ui-lib/src/lib/shared/features/note/+state/note-selector.store.ts b/packages/desktop-ui-lib/src/lib/shared/features/note/+state/note-selector.store.ts index c72dd91b00d..a81defa0991 100644 --- a/packages/desktop-ui-lib/src/lib/shared/features/note/+state/note-selector.store.ts +++ b/packages/desktop-ui-lib/src/lib/shared/features/note/+state/note-selector.store.ts @@ -3,11 +3,13 @@ import { Store, StoreConfig } from '@datorama/akita'; export interface INoteSelectorState { note: string; + disabled: boolean; } export function createInitialState(): INoteSelectorState { return { - note: '' + note: '', + disabled: false }; } diff --git a/packages/desktop-ui-lib/src/lib/shared/features/note/+state/note.service.ts b/packages/desktop-ui-lib/src/lib/shared/features/note/+state/note.service.ts index 2925528aca1..b72a1b678cc 100644 --- a/packages/desktop-ui-lib/src/lib/shared/features/note/+state/note.service.ts +++ b/packages/desktop-ui-lib/src/lib/shared/features/note/+state/note.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { Observable } from 'rxjs'; +import { Observable, combineLatest, map } from 'rxjs'; import { TimeTrackerQuery } from '../../../../time-tracker/+state/time-tracker.query'; import { NoteSelectorQuery } from './note-selector.query'; import { NoteSelectorStore } from './note-selector.store'; @@ -23,6 +23,8 @@ export class NoteService { } public get disabled$(): Observable { - return this.timeTrackerQuery.disabled$; + return combineLatest([this.timeTrackerQuery.disabled$, this.query.select((s) => s.disabled)]).pipe( + map(([disabled, noteDisabled]) => disabled || noteDisabled) + ); } } diff --git a/packages/desktop-ui-lib/src/lib/shared/features/note/note.component.ts b/packages/desktop-ui-lib/src/lib/shared/features/note/note.component.ts index 67521fa8108..015f4befb9f 100644 --- a/packages/desktop-ui-lib/src/lib/shared/features/note/note.component.ts +++ b/packages/desktop-ui-lib/src/lib/shared/features/note/note.component.ts @@ -1,4 +1,5 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component, forwardRef } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { Observable } from 'rxjs'; import { ElectronService } from '../../../electron/services'; import { NoteSelectorQuery } from './+state/note-selector.query'; @@ -9,15 +10,43 @@ import { NoteService } from './+state/note.service'; selector: 'gauzy-note', templateUrl: './note.component.html', styleUrls: ['./note.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => NoteComponent), + multi: true + } + ] }) -export class NoteComponent { +export class NoteComponent implements ControlValueAccessor { + private onChange: (value: string) => void; + private onTouched: () => void; + // Flag to control whether to update the store + protected useStore: boolean = true; constructor( private readonly electronService: ElectronService, public readonly noteSelectorStore: NoteSelectorStore, public readonly noteSelectorQuery: NoteSelectorQuery, public readonly noteSelectorService: NoteService ) {} + writeValue(note: string): void { + this.useStore = false; + if (this.useStore) { + this.noteSelectorStore.update({ note }); + } + } + + registerOnChange(fn: (note: string) => void): void { + this.onChange = fn; + } + + registerOnTouched(fn: () => void): void { + this.onTouched = fn; + } + setDisabledState(disabled: boolean): void { + this.noteSelectorStore.update({ disabled }); + } public refresh(): void { this.electronService.ipcRenderer.send('refresh-timer'); @@ -28,7 +57,11 @@ export class NoteComponent { } public change(note: string) { - this.noteSelectorStore.update({ note }); + if (this.useStore) { + this.noteSelectorStore.update({ note }); + } + this.onChange(note); + this.onTouched(); } public get disabled$(): Observable { diff --git a/packages/desktop-ui-lib/src/lib/shared/features/project-selector/+state/project-selector.service.ts b/packages/desktop-ui-lib/src/lib/shared/features/project-selector/+state/project-selector.service.ts index 1c3ee38a0a8..b738c0d6650 100644 --- a/packages/desktop-ui-lib/src/lib/shared/features/project-selector/+state/project-selector.service.ts +++ b/packages/desktop-ui-lib/src/lib/shared/features/project-selector/+state/project-selector.service.ts @@ -32,6 +32,7 @@ export class ProjectSelectorService extends SelectorService { try { + this.projectSelectorStore.setLoading(true); const { tenantId, user } = this.store; const organizationId = this.store.organizationId; const request = { @@ -49,14 +50,23 @@ export class ProjectSelectorService extends SelectorService { + public async load(options?: { + searchTerm?: string; + organizationContactId?: string; + organizationTeamId?: string; + }): Promise { try { this.projectSelectorStore.setLoading(true); + const { searchTerm: name } = options || {}; const { organizationId, tenantId, @@ -68,11 +78,15 @@ export class ProjectSelectorService extends SelectorService diff --git a/packages/desktop-ui-lib/src/lib/shared/features/project-selector/project-selector.component.ts b/packages/desktop-ui-lib/src/lib/shared/features/project-selector/project-selector.component.ts index cf149d8ae3f..2abb73e04cd 100644 --- a/packages/desktop-ui-lib/src/lib/shared/features/project-selector/project-selector.component.ts +++ b/packages/desktop-ui-lib/src/lib/shared/features/project-selector/project-selector.component.ts @@ -1,9 +1,11 @@ -import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, forwardRef, OnInit } from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; +import { IOrganizationProject } from '@gauzy/contracts'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; -import { IOrganizationProject } from 'packages/contracts/dist'; -import { concatMap, filter, Observable, tap } from 'rxjs'; +import { combineLatest, concatMap, filter, map, Observable, tap } from 'rxjs'; import { ElectronService } from '../../../electron/services'; import { TimeTrackerQuery } from '../../../time-tracker/+state/time-tracker.query'; +import { AbstractSelectorComponent } from '../../components/abstract/selector.abstract'; import { TaskSelectorService } from '../task-selector/+state/task-selector.service'; import { TeamSelectorService } from '../team-selector/+state/team-selector.service'; import { ProjectSelectorQuery } from './+state/project-selector.query'; @@ -15,9 +17,16 @@ import { ProjectSelectorStore } from './+state/project-selector.store'; selector: 'gauzy-project-selector', templateUrl: './project-selector.component.html', styleUrls: ['./project-selector.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ProjectSelectorComponent), + multi: true + } + ] }) -export class ProjectSelectorComponent implements OnInit { +export class ProjectSelectorComponent extends AbstractSelectorComponent implements OnInit { constructor( private readonly electronService: ElectronService, private readonly projectSelectorStore: ProjectSelectorStore, @@ -26,24 +35,23 @@ export class ProjectSelectorComponent implements OnInit { private readonly taskSelectorService: TaskSelectorService, private readonly teamSelectorService: TeamSelectorService, private readonly timeTrackerQuery: TimeTrackerQuery - ) {} + ) { + super(); + } public ngOnInit(): void { + this.projectSelectorService.onScroll$.pipe(untilDestroyed(this)).subscribe(); this.projectSelectorQuery.selected$ .pipe( filter(Boolean), + tap(() => this.teamSelectorService.resetPage()), + tap(() => this.taskSelectorService.resetPage()), concatMap(() => Promise.allSettled([this.teamSelectorService.load(), this.taskSelectorService.load()])), untilDestroyed(this) ) .subscribe(); - this.projectSelectorService - .getAll$() - .pipe( - filter((data) => !data.some((value) => value.id === this.projectSelectorService.selectedId)), - tap(() => (this.projectSelectorService.selected = null)), - untilDestroyed(this) - ) - .subscribe(); + // Handle search logic + this.handleSearch(this.projectSelectorService); } public refresh(): void { @@ -66,8 +74,11 @@ export class ProjectSelectorComponent implements OnInit { return this.projectSelectorQuery.data$; } - public change(projectId: IOrganizationProject['id']) { - this.projectSelectorStore.updateSelected(projectId); + protected updateSelected(value: IOrganizationProject['id']): void { + // Update store only if useStore is true + if (this.useStore) { + this.projectSelectorStore.updateSelected(value); + } } public get isLoading$(): Observable { @@ -75,10 +86,16 @@ export class ProjectSelectorComponent implements OnInit { } public get disabled$(): Observable { - return this.timeTrackerQuery.disabled$; + return combineLatest([this.timeTrackerQuery.disabled$, this.isDisabled$.asObservable()]).pipe( + map(([disabled, selectorDisabled]) => disabled || selectorDisabled) + ); } public get hasPermission$(): Observable { return this.projectSelectorService.hasPermission$; } + + public onShowMore(): void { + this.projectSelectorService.onScrollToEnd(); + } } diff --git a/packages/desktop-ui-lib/src/lib/shared/features/task-selector/+state/task-selector.service.ts b/packages/desktop-ui-lib/src/lib/shared/features/task-selector/+state/task-selector.service.ts index 35a53ab31eb..95904822e3a 100644 --- a/packages/desktop-ui-lib/src/lib/shared/features/task-selector/+state/task-selector.service.ts +++ b/packages/desktop-ui-lib/src/lib/shared/features/task-selector/+state/task-selector.service.ts @@ -35,6 +35,7 @@ export class TaskSelectorService extends SelectorService { if (!title) { return; } + this.taskSelectorStore.setLoading(true); const { tenantId, organizationId, user, statuses } = this.store; const taskStatus = statuses.find((status) => status.isInProgress); const data = { @@ -59,14 +60,19 @@ export class TaskSelectorService extends SelectorService { }); this.taskSelectorStore.appendData(task); this.toastrNotifier.success(this.translateService.instant('TIMER_TRACKER.TOASTR.TASK_ADDED')); + this.taskSelectorStore.setError(null); } catch (error) { console.error('ERROR', error); + this.taskSelectorStore.setError(error); + } finally { + this.taskSelectorStore.setLoading(false); } } - public async load(): Promise { + public async load(options?: { searchTerm?: string; projectId?: string }): Promise { try { this.taskSelectorStore.setLoading(true); + const { searchTerm } = options || {}; const { organizationId, tenantId, @@ -77,20 +83,24 @@ export class TaskSelectorService extends SelectorService { const request = { organizationId, tenantId, + searchTerm, projectId: this.projectSelectorQuery.selectedId, organizationTeamId: this.teamSelectorQuery.selectedId, - employeeId + take: this.taskSelectorQuery.limit, + skip: this.taskSelectorQuery.page, + employeeId, + ...options }; - const tasks = await this.timeTrackerService.getTasks(request); + const { total, items: tasks } = await this.timeTrackerService.getPaginatedTasks(request); if (tasks.length) { const statistics = await this.timeTrackerService.getTasksStatistics({ ...request, taskIds: tasks.map((task) => task.id) }); - const merged = this.merge(tasks, statistics); - this.taskSelectorStore.updateData(merged); + const data = this.merge(tasks, statistics); + this.taskSelectorStore.updateInfiniteList({ data, total }); } else { - this.taskSelectorStore.updateData([]); + this.taskSelectorStore.update({ data: [], total: 0 }); } this.taskSelectorStore.setError(null); } catch (error) { diff --git a/packages/desktop-ui-lib/src/lib/shared/features/task-selector/+state/task-selector.store.ts b/packages/desktop-ui-lib/src/lib/shared/features/task-selector/+state/task-selector.store.ts index de16de21ae9..8548308e962 100644 --- a/packages/desktop-ui-lib/src/lib/shared/features/task-selector/+state/task-selector.store.ts +++ b/packages/desktop-ui-lib/src/lib/shared/features/task-selector/+state/task-selector.store.ts @@ -10,7 +10,10 @@ export function createInitialState(): ITaskSelectorState { return { hasPermission: false, selected: null, - data: [] + data: [], + total: 0, + page: 1, + limit: 10 }; } diff --git a/packages/desktop-ui-lib/src/lib/shared/features/task-selector/task-selector.component.html b/packages/desktop-ui-lib/src/lib/shared/features/task-selector/task-selector.component.html index f0847cacbd1..5bdaacd41a6 100644 --- a/packages/desktop-ui-lib/src/lib/shared/features/task-selector/task-selector.component.html +++ b/packages/desktop-ui-lib/src/lib/shared/features/task-selector/task-selector.component.html @@ -15,4 +15,6 @@ [isLoading]="isLoading$ | async" [disabled]="disabled$ | async" [hasError]="error$ | async" + [typeahead]="search$" + (scrollToEnd)="onScrollToEnd()" > diff --git a/packages/desktop-ui-lib/src/lib/shared/features/task-selector/task-selector.component.ts b/packages/desktop-ui-lib/src/lib/shared/features/task-selector/task-selector.component.ts index 086a8ccbf5b..d21e1dcba57 100644 --- a/packages/desktop-ui-lib/src/lib/shared/features/task-selector/task-selector.component.ts +++ b/packages/desktop-ui-lib/src/lib/shared/features/task-selector/task-selector.component.ts @@ -1,10 +1,11 @@ -import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, forwardRef, OnInit } from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; +import { ITask } from '@gauzy/contracts'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; -import { ITask } from 'packages/contracts/dist'; -import { filter, Observable, tap } from 'rxjs'; +import { combineLatest, map, Observable } from 'rxjs'; import { ElectronService } from '../../../electron/services'; import { TimeTrackerQuery } from '../../../time-tracker/+state/time-tracker.query'; -import { NoteService } from '../note/+state/note.service'; +import { AbstractSelectorComponent } from '../../components/abstract/selector.abstract'; import { TaskSelectorQuery } from './+state/task-selector.query'; import { TaskSelectorService } from './+state/task-selector.service'; import { TaskSelectorStore } from './+state/task-selector.store'; @@ -14,27 +15,31 @@ import { TaskSelectorStore } from './+state/task-selector.store'; selector: 'gauzy-task-selector', templateUrl: './task-selector.component.html', styleUrls: ['./task-selector.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TaskSelectorComponent), + multi: true + } + ] }) -export class TaskSelectorComponent implements OnInit { +export class TaskSelectorComponent extends AbstractSelectorComponent implements OnInit { constructor( private readonly electronService: ElectronService, public readonly taskSelectorStore: TaskSelectorStore, public readonly taskSelectorQuery: TaskSelectorQuery, private readonly timeTrackerQuery: TimeTrackerQuery, - private readonly taskSelectorService: TaskSelectorService, - private readonly noteService: NoteService - ) {} + private readonly taskSelectorService: TaskSelectorService + ) { + super(); + } public ngOnInit() { - this.taskSelectorService - .getAll$() - .pipe( - filter((data) => !data.some((value) => value.id === this.taskSelectorService.selectedId)), - tap(() => (this.taskSelectorService.selected = null)), - untilDestroyed(this) - ) - .subscribe(); + // Subscribe to onScroll$ + this.taskSelectorService.onScroll$.pipe(untilDestroyed(this)).subscribe(); + // Handle search logic + this.handleSearch(this.taskSelectorService); } public refresh(): void { @@ -57,8 +62,11 @@ export class TaskSelectorComponent implements OnInit { return this.taskSelectorQuery.data$; } - public change(taskId: ITask['id']) { - this.taskSelectorStore.updateSelected(taskId); + protected updateSelected(value: ITask['id']): void { + // Update store only if useStore is true + if (this.useStore) { + this.taskSelectorStore.updateSelected(value); + } } public get isLoading$(): Observable { @@ -66,10 +74,16 @@ export class TaskSelectorComponent implements OnInit { } public get disabled$(): Observable { - return this.timeTrackerQuery.disabled$; + return combineLatest([this.timeTrackerQuery.disabled$, this.isDisabled$.asObservable()]).pipe( + map(([disabled, selectorDisabled]) => disabled || selectorDisabled) + ); } public get hasPermission$(): Observable { return this.taskSelectorService.hasPermission$; } + + public onScrollToEnd(): void { + this.taskSelectorService.onScrollToEnd(); + } } diff --git a/packages/desktop-ui-lib/src/lib/shared/features/team-selector/+state/team-selector.service.ts b/packages/desktop-ui-lib/src/lib/shared/features/team-selector/+state/team-selector.service.ts index cfcee977c11..847cb922c50 100644 --- a/packages/desktop-ui-lib/src/lib/shared/features/team-selector/+state/team-selector.service.ts +++ b/packages/desktop-ui-lib/src/lib/shared/features/team-selector/+state/team-selector.service.ts @@ -26,9 +26,10 @@ export class TeamSelectorService extends SelectorService { return this.teamSelectorQuery.selectedId; } - public async load(): Promise { + public async load(options?: { searchTerm?: string; projectId?: string }): Promise { try { this.teamSelectorStore.setLoading(true); + const { searchTerm: name } = options || {}; const { organizationId, tenantId, @@ -40,10 +41,14 @@ export class TeamSelectorService extends SelectorService { organizationId, tenantId, employeeId, - projectId: this.projectSelectorQuery.selectedId + name, + projectId: this.projectSelectorQuery.selectedId, + take: this.teamSelectorQuery.limit, + skip: this.teamSelectorQuery.page, + ...options }; - const data = await this.timeTrackerService.getTeams(request); - this.teamSelectorStore.updateData(data); + const { items: data, total } = await this.timeTrackerService.getPaginatedTeams(request); + this.teamSelectorStore.updateInfiniteList({ data, total }); this.teamSelectorStore.setError(null); } catch (error) { this.toastrNotifier.error(error.message || 'An error occurred while fetching teams.'); diff --git a/packages/desktop-ui-lib/src/lib/shared/features/team-selector/+state/team-selector.store.ts b/packages/desktop-ui-lib/src/lib/shared/features/team-selector/+state/team-selector.store.ts index dd1010e4102..bf55c35f633 100644 --- a/packages/desktop-ui-lib/src/lib/shared/features/team-selector/+state/team-selector.store.ts +++ b/packages/desktop-ui-lib/src/lib/shared/features/team-selector/+state/team-selector.store.ts @@ -10,7 +10,10 @@ export function createInitialState(): ITeamSelectorState { return { hasPermission: false, selected: null, - data: [] + data: [], + total: 0, + page: 1, + limit: 10 }; } diff --git a/packages/desktop-ui-lib/src/lib/shared/features/team-selector/team-selector.component.html b/packages/desktop-ui-lib/src/lib/shared/features/team-selector/team-selector.component.html index a7ddf01fb89..c8150f84687 100644 --- a/packages/desktop-ui-lib/src/lib/shared/features/team-selector/team-selector.component.html +++ b/packages/desktop-ui-lib/src/lib/shared/features/team-selector/team-selector.component.html @@ -12,4 +12,6 @@ [isLoading]="isLoading$ | async" [disabled]="disabled$ | async" [hasError]="error$ | async" + [typeahead]="search$" + (scrollToEnd)="onShowMore()" > diff --git a/packages/desktop-ui-lib/src/lib/shared/features/team-selector/team-selector.component.ts b/packages/desktop-ui-lib/src/lib/shared/features/team-selector/team-selector.component.ts index 02535aae13b..e93a4c91b20 100644 --- a/packages/desktop-ui-lib/src/lib/shared/features/team-selector/team-selector.component.ts +++ b/packages/desktop-ui-lib/src/lib/shared/features/team-selector/team-selector.component.ts @@ -1,9 +1,11 @@ -import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, forwardRef, OnInit } from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { IOrganizationTeam } from 'packages/contracts/dist'; -import { concatMap, filter, Observable, tap } from 'rxjs'; +import { combineLatest, concatMap, filter, map, Observable, of, tap } from 'rxjs'; import { ElectronService } from '../../../electron/services'; import { TimeTrackerQuery } from '../../../time-tracker/+state/time-tracker.query'; +import { AbstractSelectorComponent } from '../../components/abstract/selector.abstract'; import { ProjectSelectorService } from '../project-selector/+state/project-selector.service'; import { TaskSelectorService } from '../task-selector/+state/task-selector.service'; import { TeamSelectorQuery } from './+state/team-selector.query'; @@ -15,9 +17,16 @@ import { TeamSelectorStore } from './+state/team-selector.store'; selector: 'gauzy-team-selector', templateUrl: './team-selector.component.html', styleUrls: ['./team-selector.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TeamSelectorComponent), + multi: true + } + ] }) -export class TeamSelectorComponent implements OnInit { +export class TeamSelectorComponent extends AbstractSelectorComponent implements OnInit { constructor( private readonly electronService: ElectronService, private readonly teamSelectorStore: TeamSelectorStore, @@ -26,24 +35,21 @@ export class TeamSelectorComponent implements OnInit { private readonly taskSelectorService: TaskSelectorService, private readonly timeTrackerQuery: TimeTrackerQuery, private readonly teamSelectorService: TeamSelectorService - ) {} + ) { + super(); + } public ngOnInit(): void { - this.teamSelectorService - .getAll$() - .pipe( - filter((data) => !data.some((value) => value.id === this.teamSelectorService.selectedId)), - tap(() => (this.teamSelectorService.selected = null)), - untilDestroyed(this) - ) - .subscribe(); + this.taskSelectorService.onScroll$.pipe(untilDestroyed(this)).subscribe(); this.teamSelectorQuery.selected$ .pipe( filter(Boolean), + tap(() => this.projectSelectorService.resetPage()), concatMap(() => this.projectSelectorService.load()), - concatMap(() => this.taskSelectorService.load()), untilDestroyed(this) ) .subscribe(); + // Handle search logic + this.handleSearch(this.projectSelectorService); } public refresh(): void { @@ -62,8 +68,11 @@ export class TeamSelectorComponent implements OnInit { return this.teamSelectorQuery.data$; } - public change(teamId: IOrganizationTeam['id']) { - this.teamSelectorStore.updateSelected(teamId); + protected updateSelected(value: IOrganizationTeam['id']): void { + // Update store only if useStore is true + if (this.useStore) { + this.teamSelectorStore.updateSelected(value); + } } public get isLoading$(): Observable { @@ -71,6 +80,16 @@ export class TeamSelectorComponent implements OnInit { } public get disabled$(): Observable { - return this.timeTrackerQuery.disabled$; + return combineLatest([this.timeTrackerQuery.disabled$, this.isDisabled$.asObservable()]).pipe( + map(([disabled, selectorDisabled]) => disabled || selectorDisabled) + ); + } + + public get hasPermission$(): Observable { + return of(false); + } + + public onShowMore(): void { + this.teamSelectorService.onScrollToEnd(); } } diff --git a/packages/desktop-ui-lib/src/lib/shared/interfaces/selector.interface.ts b/packages/desktop-ui-lib/src/lib/shared/interfaces/selector.interface.ts index fa25870d141..034ad9cd1f7 100644 --- a/packages/desktop-ui-lib/src/lib/shared/interfaces/selector.interface.ts +++ b/packages/desktop-ui-lib/src/lib/shared/interfaces/selector.interface.ts @@ -1,5 +1,8 @@ -export interface ISelector { +import { IPaginationInput } from '@gauzy/contracts'; + +export interface ISelector extends IPaginationInput { hasPermission: boolean; + total: number; selected: T; data: T[]; } diff --git a/packages/desktop-ui-lib/src/lib/tasks/tasks.component.html b/packages/desktop-ui-lib/src/lib/tasks/tasks.component.html index 01467686c54..9d2df1434e8 100644 --- a/packages/desktop-ui-lib/src/lib/tasks/tasks.component.html +++ b/packages/desktop-ui-lib/src/lib/tasks/tasks.component.html @@ -1,10 +1,8 @@ - +
- {{ "TIMER_TRACKER.ADD_TASK" | translate }} + {{ 'TIMER_TRACKER.ADD_TASK' | translate }}
@@ -13,182 +11,37 @@
- - - - - - {{ item?.name }} - - - -
- - - {{ item?.name }} - -
-
-
+
- - - - - - {{ item?.name }} - - - -
- - - {{ item?.name }} - -
-
-
+
- - - - - - {{ item?.name }} - - - -
- - - {{ item?.name }} - -
-
-
+
- + - - + + - +
@@ -197,9 +50,7 @@
- +
bindLabel="name" formControlName="tags" > - +
- + {{ tag.name }}
- + @@ -270,41 +106,27 @@
- + - - + + - +
- + bindLabel="name" formControlName="taskSize" > - - + + - +
@@ -334,9 +148,7 @@
- +
- +
- + - diff --git a/packages/desktop-ui-lib/src/lib/tasks/tasks.component.ts b/packages/desktop-ui-lib/src/lib/tasks/tasks.component.ts index 8b55930d6ff..11743fca7cf 100644 --- a/packages/desktop-ui-lib/src/lib/tasks/tasks.component.ts +++ b/packages/desktop-ui-lib/src/lib/tasks/tasks.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Inject, Input, OnInit, Output } from '@angular/core'; +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { FormControl, UntypedFormGroup, Validators } from '@angular/forms'; import { IEmployee, @@ -14,12 +14,11 @@ import { PermissionsEnum, TaskStatusEnum } from '@gauzy/contracts'; -import { NbDialogRef, NbToastrService } from '@nebular/theme'; +import { NbDialogRef } from '@nebular/theme'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { TranslateService } from '@ngx-translate/core'; import * as moment from 'moment'; -import { concatMap, from, map, Observable, tap } from 'rxjs'; -import { GAUZY_ENV } from '../constants'; +import { combineLatest, concatMap, from, map, Observable, startWith, tap } from 'rxjs'; import { Store, TagService } from '../services'; import { ClientSelectorService } from '../shared/features/client-selector/+state/client-selector.service'; import { ProjectSelectorService } from '../shared/features/project-selector/+state/project-selector.service'; @@ -36,16 +35,6 @@ import { CkEditorConfig, ColorAdapter } from '../utils'; export class TasksComponent implements OnInit { @Input() userData: IUserOrganization = this.store.user as any; @Input() employee: IEmployee = this.store.user.employee; - @Input() hasProjectPermission: boolean = this.projectSelectorService.hasPermission; - @Input() selected: { - projectId: IOrganizationProject['id']; - teamId: IOrganizationTeam['id']; - contactId: IOrganizationContact['id']; - } = { - projectId: this.projectSelectorService.selectedId, - teamId: this.teamSelectorService.selectedId, - contactId: this.clientSelectorService.selectedId - }; @Output() isAddTask: EventEmitter = new EventEmitter(); @Output() newTaskCallback: EventEmitter<{ isSuccess: boolean; @@ -94,10 +83,7 @@ export class TasksComponent implements OnInit { constructor( private timeTrackerService: TimeTrackerService, - private toastrService: NbToastrService, private translate: TranslateService, - @Inject(GAUZY_ENV) - private readonly _environment: any, private store: Store, private _dialogRef: NbDialogRef, private _tagService: TagService, @@ -108,38 +94,6 @@ export class TasksComponent implements OnInit { this.isSaving = false; } - private async _projects(value?: { - organizationContactId?: string; - organizationTeamId?: string; - projectId?: string; - }): Promise { - try { - const { organizationId, user, tenantId } = this.store; - const employeeId = user?.employee?.id; - - if (!employeeId) { - throw new Error('Employee ID is missing.'); - } - - const filterParams = { - organizationId, - tenantId, - employeeId, - ...(value?.organizationContactId && { organizationContactId: value.organizationContactId }), - ...(value?.organizationTeamId && { organizationTeamId: value.organizationTeamId }) - }; - - this.projects = await this.timeTrackerService.getProjects(filterParams); - - // Clear the form's projectId if the selected project does not exist in the fetched list - if (value?.projectId && !this.projects.some(({ id }) => id === value.projectId)) { - this.form.patchValue({ projectId: null }); - } - } catch (error) { - console.error('[Projects Fetch Error]', `Unable to fetch employee projects: ${error.message}`); - } - } - private async _tags(): Promise { try { this.tags = await this._tagService.getTags(); @@ -159,24 +113,6 @@ export class TasksComponent implements OnInit { } } - private async _clients(): Promise { - try { - const { organizationId, user, tenantId } = this.store; - const employeeId = user.employee.id; - this.contacts = await this.timeTrackerService.getClient({ organizationId, employeeId, tenantId }); - } catch (error) { - console.error('[error]', 'while get contacts::' + error.message); - } - } - - private async _teams(): Promise { - try { - this.teams = await this.timeTrackerService.getTeams(); - } catch (error) { - console.error('[error]', 'while get teams::' + error.message); - } - } - private async _sizes(): Promise { try { this.taskSizes = await this.timeTrackerService.taskSizes(); @@ -204,17 +140,7 @@ export class TasksComponent implements OnInit { const { organizationId, tenantId } = this.store; this.editorConfig.editorplaceholder = this.translate.instant('FORM.PLACEHOLDERS.DESCRIPTION'); this.taskStatuses = this.store.statuses; - from( - Promise.allSettled([ - this._projects(), - this._tags(), - this._employees(), - this._clients(), - this._teams(), - this._sizes(), - this._priorities() - ]) - ) + from(Promise.allSettled([this._tags(), this._employees(), this._sizes(), this._priorities()])) .pipe( tap(() => this.form.patchValue({ taskStatus: this.taskStatuses[0] })), untilDestroyed(this) @@ -231,7 +157,7 @@ export class TasksComponent implements OnInit { members: new FormControl([]), organizationId: new FormControl(organizationId), project: new FormControl(null), - projectId: new FormControl(this.selected.projectId), + projectId: new FormControl(this.projectSelectorService.selectedId), status: new FormControl(TaskStatusEnum.OPEN), priority: new FormControl(null), size: new FormControl(null), @@ -242,18 +168,32 @@ export class TasksComponent implements OnInit { taskStatus: new FormControl(null), taskPriority: new FormControl(null), taskSize: new FormControl(null), - organizationContactId: new FormControl(this.selected.contactId), - organizationTeamId: new FormControl(this.selected.teamId) + organizationContactId: new FormControl(this.clientSelectorService.selectedId), + organizationTeamId: new FormControl(this.teamSelectorService.selectedId) }); - this.form.valueChanges + this.hasAddTagPermission$ = this.store.userRolePermissions$.pipe( + map(() => this.store.hasPermissions(PermissionsEnum.ALL_ORG_EDIT, PermissionsEnum.ORG_TAGS_ADD)) + ); + combineLatest([ + this.form.get('organizationContactId').valueChanges.pipe(startWith(this.clientSelectorService.selectedId)), + this.form.get('organizationTeamId').valueChanges.pipe(startWith(this.teamSelectorService.selectedId)) + ]) .pipe( - concatMap((values) => this._projects(values)), + tap(() => this.projectSelectorService.resetPage()), + concatMap(([organizationContactId, organizationTeamId]) => + this.projectSelectorService.load({ organizationContactId, organizationTeamId }) + ), + untilDestroyed(this) + ) + .subscribe(); + this.form + .get('projectId') + .valueChanges.pipe( + tap(() => this.teamSelectorService.resetPage()), + concatMap((projectId) => this.teamSelectorService.load({ projectId })), untilDestroyed(this) ) .subscribe(); - this.hasAddTagPermission$ = this.store.userRolePermissions$.pipe( - map(() => this.store.hasPermissions(PermissionsEnum.ALL_ORG_EDIT, PermissionsEnum.ORG_TAGS_ADD)) - ); } public close(res?: any): void { @@ -310,10 +250,6 @@ export class TasksComponent implements OnInit { this.isSaving = false; } - public addProject = async (name: string) => { - await this.projectSelectorService.addProject(name); - }; - public background(bgColor: string) { return ColorAdapter.background(bgColor); } diff --git a/packages/desktop-ui-lib/src/lib/tasks/tasks.module.ts b/packages/desktop-ui-lib/src/lib/tasks/tasks.module.ts index 8a0f72ae662..d3622c710ad 100644 --- a/packages/desktop-ui-lib/src/lib/tasks/tasks.module.ts +++ b/packages/desktop-ui-lib/src/lib/tasks/tasks.module.ts @@ -1,33 +1,37 @@ -import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { TasksComponent } from './tasks.component'; +import { NgModule } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { - NbLayoutModule, - NbSidebarModule, - NbMenuModule, + NbAccordionModule, + NbAlertModule, + NbBadgeModule, + NbButtonModule, NbCardModule, + NbDatepickerModule, NbIconModule, - NbListModule, - NbSelectModule, - NbToggleModule, NbInputModule, - NbButtonModule, - NbAlertModule, + NbLayoutModule, + NbListModule, + NbMenuModule, NbProgressBarModule, + NbSelectModule, + NbSidebarModule, NbTabsetModule, NbToastrService, - NbAccordionModule, - NbDatepickerModule, - NbBadgeModule + NbToggleModule } from '@nebular/theme'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { NgSelectModule } from '@ng-select/ng-select'; -import { TimeTrackerService } from '../time-tracker/time-tracker.service'; -import { DesktopDirectiveModule } from '../directives/desktop-directive.module'; import { TranslateModule } from '@ngx-translate/core'; import { CKEditorModule } from 'ckeditor4-angular'; -import { TaskRenderModule } from '../time-tracker/task-render'; +import { DesktopDirectiveModule } from '../directives/desktop-directive.module'; import { TagService } from '../services'; +import { ClientSelectorModule } from '../shared/features/client-selector/client-selector.module'; +import { ProjectSelectorModule } from '../shared/features/project-selector/project-selector.module'; +import { TaskSelectorModule } from '../shared/features/task-selector/task-selector.module'; +import { TeamSelectorModule } from '../shared/features/team-selector/team-selector.module'; +import { TaskRenderModule } from '../time-tracker/task-render'; +import { TimeTrackerService } from '../time-tracker/time-tracker.service'; +import { TasksComponent } from './tasks.component'; @NgModule({ declarations: [TasksComponent], @@ -55,7 +59,11 @@ import { TagService } from '../services'; DesktopDirectiveModule, TranslateModule, CKEditorModule, - TaskRenderModule + TaskRenderModule, + ClientSelectorModule, + TaskSelectorModule, + TeamSelectorModule, + ProjectSelectorModule ], providers: [NbToastrService, TimeTrackerService, TagService], exports: [TasksComponent] diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/task-table/search/search.component.html b/packages/desktop-ui-lib/src/lib/time-tracker/task-table/search/search.component.html index 3e34fccc3b4..24a018e978e 100644 --- a/packages/desktop-ui-lib/src/lib/time-tracker/task-table/search/search.component.html +++ b/packages/desktop-ui-lib/src/lib/time-tracker/task-table/search/search.component.html @@ -3,7 +3,6 @@ { + public get disabled$(): Observable { return this.timeTrackerQuery.disabled$; } + + public get loading$(): Observable { + return this.searchTermQuery.selectLoading(); + } + + ngAfterViewInit() { + fromEvent(this.search.nativeElement, 'input') + .pipe( + map((event: any) => event.target.value), + distinctUntilChange(), + debounceTime(300), + filter((term) => term !== this.searchTermQuery.value), + untilDestroyed(this) + ) + .subscribe((searchTerm: string) => { + this.onSearch(searchTerm); + }); + } } diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/task-table/table/task-table.component.html b/packages/desktop-ui-lib/src/lib/time-tracker/task-table/table/task-table.component.html index 053de80b41b..63fa50fe8a7 100644 --- a/packages/desktop-ui-lib/src/lib/time-tracker/task-table/table/task-table.component.html +++ b/packages/desktop-ui-lib/src/lib/time-tracker/task-table/table/task-table.component.html @@ -11,6 +11,9 @@
+ +
+
; @ViewChild('smartTable') public set smartTable(content: Angular2SmartTableComponent) { @@ -55,6 +57,7 @@ export class TaskTableComponent implements OnInit, AfterViewInit { private readonly projectSelectorService: ProjectSelectorService, private readonly actionButtonStore: ActionButtonStore, private readonly searchTermQuery: SearchTermQuery, + private readonly searchTermStore: SearchTermStore, private readonly taskCacheService: TaskCacheService, private readonly store: Store ) {} @@ -78,6 +81,11 @@ export class TaskTableComponent implements OnInit, AfterViewInit { this.loadSmartTableSettings(); }); this.onChangedSource(); + this.monitorLoadingState(); + } + + private monitorLoadingState(): void { + this.loading$ = this.smartTableSource.loading$; } public refreshTimer(): void { @@ -213,16 +221,19 @@ export class TaskTableComponent implements OnInit, AfterViewInit { } // Prepare request parameters for filtering + const { selectedId: projectId } = this.projectSelectorService; + const { selectedId: teamId } = this.teamSelectorService; + const { value: searchTerm } = this.searchTermQuery; + const requestFilters = { tenantId, organizationId, - ...(this.projectSelectorService.selectedId && { projectId: this.projectSelectorService.selectedId }), - ...(this.teamSelectorService.selectedId && { teams: [this.teamSelectorService.selectedId] }), - members: { id: employeeId }, - ...(this.searchTermQuery.value && { - title: this.searchTermQuery.value, - prefix: this.searchTermQuery.value - }) + ...(projectId && { projectId }), + ...(teamId && { teams: [teamId] }), + ...(searchTerm && { + title: searchTerm + }), + members: { id: employeeId } }; // Initialize the smart table data source diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/task-table/task-table.module.ts b/packages/desktop-ui-lib/src/lib/time-tracker/task-table/task-table.module.ts index 0a7bbc0e7fa..5ef6c6ec30f 100644 --- a/packages/desktop-ui-lib/src/lib/time-tracker/task-table/task-table.module.ts +++ b/packages/desktop-ui-lib/src/lib/time-tracker/task-table/task-table.module.ts @@ -6,6 +6,7 @@ import { NbFormFieldModule, NbIconModule, NbInputModule, + NbSpinnerModule, NbTooltipModule } from '@nebular/theme'; import { Angular2SmartTableModule } from 'angular2-smart-table'; @@ -43,7 +44,8 @@ import { TaskTableComponent } from './table/task-table.component'; ProjectSelectorModule, TeamSelectorModule, LanguageModule.forChild(), - Angular2SmartTableModule + Angular2SmartTableModule, + NbSpinnerModule ], providers: [ ActionButtonStore, diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.ts b/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.ts index 7b1a3754c27..be14d9958ec 100644 --- a/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.ts +++ b/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.ts @@ -71,6 +71,7 @@ import { NoteService } from '../shared/features/note/+state/note.service'; import { ProjectSelectorService } from '../shared/features/project-selector/+state/project-selector.service'; import { TaskSelectorService } from '../shared/features/task-selector/+state/task-selector.service'; import { TeamSelectorService } from '../shared/features/team-selector/+state/team-selector.service'; +import { TimeTrackerFormService } from '../shared/features/time-tracker-form/time-tracker-form.service'; import { hasAllPermissions } from '../shared/utils/permission.util'; import { TimeTrackerQuery } from './+state/time-tracker.query'; import { IgnitionState, TimeTrackerStore } from './+state/time-tracker.store'; @@ -189,7 +190,8 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit { private readonly taskSelectorService: TaskSelectorService, private readonly noteService: NoteService, private readonly timeTrackerQuery: TimeTrackerQuery, - private readonly timeTrackerStore: TimeTrackerStore + private readonly timeTrackerStore: TimeTrackerStore, + private readonly timeTrackerFormService: TimeTrackerFormService ) { this.iconLibraries.registerFontPack('font-awesome', { packClass: 'fas', @@ -733,12 +735,15 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit { this.timeTrackerQuery.isEditing$ .pipe( filter((editing) => editing && !this._isOffline), - tap(() => - this.dialogService.open(TimerTrackerChangeDialogComponent, { - backdropClass: 'backdrop-blur', - closeOnBackdropClick: false - }) + concatMap( + () => + this.dialogService.open(TimerTrackerChangeDialogComponent, { + backdropClass: 'backdrop-blur', + closeOnBackdropClick: false + }).onClose ), + filter(Boolean), + tap((value) => this.timeTrackerFormService.setState(value)), untilDestroyed(this) ) .subscribe(); diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.module.ts b/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.module.ts index bcd270d7ae4..9ef76eef803 100644 --- a/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.module.ts +++ b/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.module.ts @@ -34,8 +34,13 @@ import { LanguageModule } from '../language/language.module'; import { TimeSlotQueueService } from '../offline-sync'; import { ErrorHandlerService, NativeNotificationService, Store, ToastrNotificationService } from '../services'; import { SelectModule } from '../shared/components/ui/select/select.module'; +import { ClientSelectorModule } from '../shared/features/client-selector/client-selector.module'; +import { NoteModule } from '../shared/features/note/note.module'; +import { TaskSelectorModule } from '../shared/features/task-selector/task-selector.module'; +import { TeamSelectorModule } from '../shared/features/team-selector/team-selector.module'; import { TimeTrackerFormModule } from '../shared/features/time-tracker-form/time-tracker-form.module'; import { TasksModule } from '../tasks/tasks.module'; +import { ProjectSelectorModule } from './../shared/features/project-selector/project-selector.module'; import { CustomRenderComponent } from './custom-render-cell.component'; import { NoDataMessageModule } from './no-data-message/no-data-message.module'; import { OrganizationSelectorComponent } from './organization-selector/organization-selector.component'; @@ -91,7 +96,12 @@ import { TimerTrackerChangeDialogComponent } from './timer-tracker-change-dialog TimeTrackerFormModule, SelectModule, DesktopDirectiveModule, - NbRouteTabsetModule + NbRouteTabsetModule, + ClientSelectorModule, + TaskSelectorModule, + TeamSelectorModule, + ProjectSelectorModule, + NoteModule ], providers: [ NbSidebarService, diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.service.ts b/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.service.ts index cd0275953c9..f62d41610e5 100644 --- a/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.service.ts +++ b/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.service.ts @@ -9,6 +9,7 @@ import { IOrganizationTeam, IOrganizationTeamEmployee, IPagination, + ITask, ITaskPriority, ITaskSize, ITaskSizeFindInput, @@ -34,6 +35,7 @@ import { TaskCacheService, TaskPriorityCacheService, TaskSizeCacheService, + TaskStatisticsCacheService, TaskStatusCacheService, TeamsCacheService, TimeLogCacheService, @@ -54,6 +56,7 @@ export class TimeTrackerService { private readonly http: HttpClient, private readonly _clientCacheService: ClientCacheService, private readonly _taskCacheService: TaskCacheService, + private readonly _taskStatisticsCacheService: TaskStatisticsCacheService, private readonly _projectCacheService: ProjectCacheService, private readonly _timeSlotCacheService: TimeSlotCacheService, private readonly _employeeCacheService: EmployeeCacheService, @@ -120,7 +123,82 @@ export class TimeTrackerService { ); this._taskCacheService.setValue(tasks$, request); } - return firstValueFrom(tasks$); + return firstValueFrom(tasks$) as Promise; + } + + async getPaginatedTasks(values: { + organizationId: string; + tenantId: string; + projectId?: string; + organizationTeamId?: string; + employeeId: string; + take: number; + skip: number; + searchTerm: string; + }): Promise> { + const { + organizationId, + tenantId, + projectId, + organizationTeamId, + employeeId, + take, + skip, + searchTerm: title + } = values; + + const request = { + where: { + organizationId, + tenantId, + ...(projectId && { projectId }), + ...(organizationTeamId && { teams: [organizationTeamId] }), + ...(title && { title }), + members: { id: employeeId } + }, + relations: [ + 'members', + 'members.user', + 'project', + 'tags', + 'teams', + 'teams.members', + 'teams.members.employee', + 'teams.members.employee.user', + 'creator', + 'organizationSprint', + 'taskStatus', + 'taskSize', + 'taskPriority' + ], + join: { + alias: 'task', + leftJoin: { + members: 'task.members', + user: 'members.user' + } + }, + order: { updatedAt: 'DESC' }, + take, + skip + }; + + let tasks$ = this._taskCacheService.getValue(request); + + if (!tasks$) { + tasks$ = this.http + .get>(`${API_PREFIX}/tasks/pagination`, { + params: toParams(request) + }) + .pipe( + map((response: any) => response), + shareReplay(1) + ); + + this._taskCacheService.setValue(tasks$, request); + } + + return firstValueFrom(tasks$) as Promise>; } /** @@ -145,7 +223,7 @@ export class TimeTrackerService { taskIds: values.taskIds, projectId: values.projectId }; - let tasksStatistics$ = this._taskCacheService.getValue(cacheReference); + let tasksStatistics$ = this._taskStatisticsCacheService.getValue(cacheReference); if (!tasksStatistics$) { // Fetch tasks statistics @@ -157,11 +235,11 @@ export class TimeTrackerService { ); // Set the tasks statistics in the cache - this._taskCacheService.setValue(tasksStatistics$, cacheReference); + this._taskStatisticsCacheService.setValue(tasksStatistics$, cacheReference); } // Return the tasks statistics - return await firstValueFrom(tasksStatistics$); + return firstValueFrom(tasksStatistics$) as Promise; } async getEmployees(values) { @@ -216,7 +294,42 @@ export class TimeTrackerService { ); this._projectCacheService.setValue(projects$, params); } - return firstValueFrom(projects$); + return firstValueFrom(projects$) as Promise; + } + + async getPaginatedProjects(values) { + const { organizationId, tenantId, employeeId, organizationTeamId, organizationContactId, skip, take, name } = + values; + + // Prepare the parameters + const params = { + where: { + organizationId, + tenantId, + ...(employeeId && { members: { employeeId } }), + ...(organizationContactId && { organizationContactId }), + ...(organizationTeamId && { teams: { id: organizationTeamId } }), + ...(name && { name }) + }, + skip, + take + }; + + // Check for cached projects + let projects$ = this._projectCacheService.getValue(params); + if (!projects$) { + // If not cached, make HTTP request and cache result + projects$ = this.http + .get>(`${API_PREFIX}/organization-projects/pagination`, { + params: toParams(params) + }) + .pipe(shareReplay(1)); + + this._projectCacheService.setValue(projects$, params); + } + + // Return the first emitted value from the observable + return firstValueFrom(projects$) as Promise>; } async getClient(values) { @@ -235,7 +348,21 @@ export class TimeTrackerService { ); this._clientCacheService.setValue(clients$, params); } - return firstValueFrom(clients$); + return firstValueFrom(clients$) as Promise; + } + + async getPaginatedClients(values) { + const params = toParams(values); + let clients$ = this._clientCacheService.getValue(params); + if (!clients$) { + clients$ = this.http + .get>(`${API_PREFIX}/organization-contact/pagination`, { + params + }) + .pipe(shareReplay(1)); + this._clientCacheService.setValue(clients$, params); + } + return firstValueFrom(clients$) as Promise>; } getUserDetail() { @@ -661,7 +788,41 @@ export class TimeTrackerService { ); this._teamsCacheService.setValue(teams$, params); } - return firstValueFrom(teams$); + return firstValueFrom(teams$) as Promise; + } + + public async getPaginatedTeams(values?: any): Promise> { + const { employeeId, projectId, skip, take, name } = values ?? {}; + + // Prepare the query parameters + const params = { + where: { + organizationId: this._store.organizationId, + tenantId: this._store.tenantId, + ...(employeeId && { members: { employeeId } }), + ...(projectId && { projects: { id: projectId } }), + ...(name && { name }) + }, + relations: ['projects', 'members.role', 'members.employee.user'], + skip, + take + }; + + // Retrieve cached teams if available + let teams$ = this._teamsCacheService.getValue(params); + if (!teams$) { + // If not cached, make HTTP request and cache the result + teams$ = this.http + .get>(`${API_PREFIX}/organization-team/pagination`, { + params: toParams(params) + }) + .pipe(shareReplay(1)); + + this._teamsCacheService.setValue(teams$, params); + } + + // Return the first emitted value from the observable + return firstValueFrom(teams$) as Promise>; } public async taskSizes(): Promise { diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/timer-tracker-change-dialog/timer-tracker-change-dialog.component.html b/packages/desktop-ui-lib/src/lib/time-tracker/timer-tracker-change-dialog/timer-tracker-change-dialog.component.html index 8fd7a1ca3df..93e458b48ba 100644 --- a/packages/desktop-ui-lib/src/lib/time-tracker/timer-tracker-change-dialog/timer-tracker-change-dialog.component.html +++ b/packages/desktop-ui-lib/src/lib/time-tracker/timer-tracker-change-dialog/timer-tracker-change-dialog.component.html @@ -1,18 +1,33 @@ - - - -
{{ 'Update' | translate }}
-
- - - - - - - -
+
+ + + +
{{ 'Update' | translate }}
+
+ + + + + + + + + + + + + + + + + + + + +
+
diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/timer-tracker-change-dialog/timer-tracker-change-dialog.component.scss b/packages/desktop-ui-lib/src/lib/time-tracker/timer-tracker-change-dialog/timer-tracker-change-dialog.component.scss index 89aeea80c72..15afea12c57 100644 --- a/packages/desktop-ui-lib/src/lib/time-tracker/timer-tracker-change-dialog/timer-tracker-change-dialog.component.scss +++ b/packages/desktop-ui-lib/src/lib/time-tracker/timer-tracker-change-dialog/timer-tracker-change-dialog.component.scss @@ -18,6 +18,21 @@ nb-card { nb-card-body { padding: 1rem; + + &.selector-container { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + gap: .5rem; + flex-direction: column; + position: relative; + + .selector { + width: 100%; + } + } + } nb-card-footer { diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/timer-tracker-change-dialog/timer-tracker-change-dialog.component.ts b/packages/desktop-ui-lib/src/lib/time-tracker/timer-tracker-change-dialog/timer-tracker-change-dialog.component.ts index e3743a56963..90281ecd468 100644 --- a/packages/desktop-ui-lib/src/lib/time-tracker/timer-tracker-change-dialog/timer-tracker-change-dialog.component.ts +++ b/packages/desktop-ui-lib/src/lib/time-tracker/timer-tracker-change-dialog/timer-tracker-change-dialog.component.ts @@ -1,9 +1,15 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { FormControl, FormGroup } from '@angular/forms'; +import { distinctUntilChange } from '@gauzy/ui-core/common'; import { NbDialogRef } from '@nebular/theme'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; -import { filter, map, Observable, startWith, tap } from 'rxjs'; +import { combineLatest, concatMap, filter, map, Observable, startWith, tap } from 'rxjs'; import { TimeTrackerQuery } from '../+state/time-tracker.query'; import { IgnitionState, TimeTrackerStore } from '../+state/time-tracker.store'; +import { ClientSelectorService } from '../../shared/features/client-selector/+state/client-selector.service'; +import { ProjectSelectorService } from '../../shared/features/project-selector/+state/project-selector.service'; +import { TaskSelectorService } from '../../shared/features/task-selector/+state/task-selector.service'; +import { TeamSelectorService } from '../../shared/features/team-selector/+state/team-selector.service'; import { TimeTrackerFormService } from '../../shared/features/time-tracker-form/time-tracker-form.service'; @UntilDestroy({ checkProperties: true }) @@ -14,13 +20,24 @@ import { TimeTrackerFormService } from '../../shared/features/time-tracker-form/ changeDetection: ChangeDetectionStrategy.OnPush }) export class TimerTrackerChangeDialogComponent implements OnInit { - private currentState!: any; + public form: FormGroup = new FormGroup({ + clientId: new FormControl(null), + projectId: new FormControl(null), + teamId: new FormControl(null), + taskId: new FormControl(null), + note: new FormControl(null) + }); constructor( private dialogRef: NbDialogRef, private readonly timeTrackerStore: TimeTrackerStore, private readonly timeTrackerQuery: TimeTrackerQuery, - private readonly timeTrackerFormService: TimeTrackerFormService + private readonly timeTrackerFormService: TimeTrackerFormService, + private readonly projectSelectorService: ProjectSelectorService, + private readonly teamSelectorService: TeamSelectorService, + private readonly taskSelectorService: TaskSelectorService, + private readonly clientSelectorService: ClientSelectorService ) {} + public ngOnInit(): void { this.timeTrackerQuery.ignition$ .pipe( @@ -33,16 +50,43 @@ export class TimerTrackerChangeDialogComponent implements OnInit { .pipe( filter(({ state }) => state === IgnitionState.RESTARTED), tap(() => this.timeTrackerStore.update({ ignition: { state: IgnitionState.STARTED } })), - tap(() => this.setCurrentState()), - tap(() => this.dismiss()), + tap(() => this.dismiss(this.form.value)), untilDestroyed(this) ) .subscribe(); this.setCurrentState(); + combineLatest([ + this.form.get('clientId').valueChanges.pipe(startWith(this.clientSelectorService.selectedId)), + this.form.get('teamId').valueChanges.pipe(startWith(this.teamSelectorService.selectedId)) + ]) + .pipe( + distinctUntilChange(), + tap(() => this.projectSelectorService.resetPage()), + concatMap(([organizationContactId, organizationTeamId]) => + this.projectSelectorService.load({ organizationContactId, organizationTeamId }) + ), + untilDestroyed(this) + ) + .subscribe(); + this.form + .get('projectId') + .valueChanges.pipe( + distinctUntilChange(), + tap(() => this.teamSelectorService.resetPage()), + tap(() => this.taskSelectorService.resetPage()), + concatMap((projectId) => + Promise.allSettled([ + this.teamSelectorService.load({ projectId }), + this.taskSelectorService.load({ projectId }) + ]) + ), + untilDestroyed(this) + ) + .subscribe(); } private setCurrentState() { - this.currentState = this.timeTrackerFormService.getState(); + this.form.patchValue(this.timeTrackerFormService.getState()); } public applyChanges() { @@ -60,9 +104,8 @@ export class TimerTrackerChangeDialogComponent implements OnInit { return this.timeTrackerQuery.isExpanded$; } - public dismiss() { + public dismiss(data?) { this.timeTrackerStore.update({ isEditing: false }); - this.timeTrackerFormService.setState(this.currentState); - this.dialogRef.close(); + this.dialogRef.close(data); } } diff --git a/packages/desktop-ui-lib/src/lib/utils/smart-table/cached-server.data-source.ts b/packages/desktop-ui-lib/src/lib/utils/smart-table/cached-server.data-source.ts index 64895f85f54..4fed4eae3d0 100644 --- a/packages/desktop-ui-lib/src/lib/utils/smart-table/cached-server.data-source.ts +++ b/packages/desktop-ui-lib/src/lib/utils/smart-table/cached-server.data-source.ts @@ -1,7 +1,7 @@ import { HttpClient, HttpParams } from '@angular/common/http'; import { isNotEmpty, toParams } from '@gauzy/ui-core/common'; import { IFilterConfig, LocalDataSource } from 'angular2-smart-table'; -import { firstValueFrom, Observable, shareReplay } from 'rxjs'; +import { BehaviorSubject, firstValueFrom, Observable, shareReplay } from 'rxjs'; import { catchError, map, tap } from 'rxjs/operators'; import { AbstractCacheService } from '../../services/abstract-cache.service'; import { ServerSourceConf } from './server-source.conf'; @@ -10,6 +10,7 @@ export class CachedServerDataSource extends LocalDataSource { protected conf: ServerSourceConf; protected lastRequestCount: number = 0; protected operatorFunctions: any[] = []; + protected _loading$ = new BehaviorSubject(false); constructor( protected http: HttpClient, @@ -46,8 +47,10 @@ export class CachedServerDataSource extends LocalDataSource { ) .pipe( tap(() => this.conf.finalize?.()), + tap(() => this._loading$.next(false)), catchError((error) => { this.conf.finalize?.(); + this._loading$.next(false); throw new Error(error); }) ) @@ -91,6 +94,7 @@ export class CachedServerDataSource extends LocalDataSource { } protected requestElements(): Observable { + this._loading$.next(true); const httpParams = this.createRequestParams(); return this.cacheService ? this.cachedRequestElements(httpParams) @@ -102,10 +106,7 @@ export class CachedServerDataSource extends LocalDataSource { if (!elements$) { // Fetch elements - elements$ = this.http.get(this.conf.endPoint, { params, observe: 'response' }).pipe( - map((httpResponse) => httpResponse), - shareReplay(1) - ); + elements$ = this.http.get(this.conf.endPoint, { params, observe: 'response' }).pipe(shareReplay(1)); // Set elements in the cache this.cacheService.setValue(elements$, params); } @@ -192,4 +193,8 @@ export class CachedServerDataSource extends LocalDataSource { public registerOperatorFunction(operatorFunction: any) { this.operatorFunctions.push(operatorFunction); } + + public get loading$(): Observable { + return this._loading$.asObservable(); + } }