diff --git a/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee-profile/edit-employee-settings/edit-employee-other-settings.component.html b/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee-profile/edit-employee-settings/edit-employee-other-settings.component.html index e0083798014..3c10456bc9b 100644 --- a/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee-profile/edit-employee-settings/edit-employee-other-settings.component.html +++ b/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee-profile/edit-employee-settings/edit-employee-other-settings.component.html @@ -9,6 +9,11 @@

{{ 'EMPLOYEES_PAGE.EDIT_EMPLOYEE.GENERAL_SETTINGS' | translate }} + +
  • + {{ 'ORGANIZATIONS_PAGE.EDIT.SETTINGS.TIMER_SETTINGS' | translate }} +
  • +
  • {{ 'EMPLOYEES_PAGE.EDIT_EMPLOYEE.INTEGRATIONS' | translate }} @@ -49,6 +54,112 @@

    + + + {{ 'ORGANIZATIONS_PAGE.EDIT.SETTINGS.TIMER_SETTINGS' | translate }} + + +
    +
    +
    +
    + + {{ 'ORGANIZATIONS_PAGE.EDIT.SETTINGS.ALLOW_MANUAL_TIME' | translate }} + + +
    +
    +
    +
    + + {{ 'ORGANIZATIONS_PAGE.EDIT.SETTINGS.ALLOW_MODIFY_TIME' | translate }} + + +
    +
    +
    +
    + + {{ 'ORGANIZATIONS_PAGE.EDIT.SETTINGS.ALLOW_DELETE_TIME' | translate }} + + +
    +
    + +
    + + {{ 'ORGANIZATIONS_PAGE.EDIT.SETTINGS.ALLOW_SCREEN_CAPTURE' | translate }} + + +
    +
    +
    +
    +
    {{ 'EMPLOYEES_PAGE.EDIT_EMPLOYEE.INTEGRATIONS' | translate }} diff --git a/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee-profile/edit-employee-settings/edit-employee-other-settings.component.scss b/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee-profile/edit-employee-settings/edit-employee-other-settings.component.scss index f39bdbd2305..ce5a3031253 100644 --- a/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee-profile/edit-employee-settings/edit-employee-other-settings.component.scss +++ b/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee-profile/edit-employee-settings/edit-employee-other-settings.component.scss @@ -303,3 +303,22 @@ nb-accordion { width: 100%; } } +:host ::ng-deep nb-toggle { + padding: 10px; + border: 1px solid nb-theme(gauzy-border-default-color); + border-radius: nb-theme(border-radius); + & > label { + margin-bottom: 0; + } +} +:host ::ng-deep .toggle { + border: 1px solid #7e7e8f !important; + background-color: #7e7e8f !important; + &.checked { + background-color: nb-theme(text-primary-color) !important; + border: 1px solid nb-theme(text-primary-color) !important; + & + span { + color: nb-theme(text-primary-color); + } + } +} diff --git a/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee-profile/edit-employee-settings/edit-employee-other-settings.component.ts b/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee-profile/edit-employee-settings/edit-employee-other-settings.component.ts index ab62a34c899..fe79b27c4a6 100644 --- a/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee-profile/edit-employee-settings/edit-employee-other-settings.component.ts +++ b/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee-profile/edit-employee-settings/edit-employee-other-settings.component.ts @@ -1,6 +1,6 @@ import { Component, OnInit, OnDestroy, ChangeDetectorRef, ViewChild } from '@angular/core'; -import { UntypedFormBuilder, UntypedFormGroup, NgForm } from '@angular/forms'; -import { filter, tap } from 'rxjs/operators'; +import { FormBuilder, FormGroup, NgForm } from '@angular/forms'; +import { filter, tap } from 'rxjs'; import { NbAccordionComponent, NbAccordionItemComponent } from '@nebular/theme'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import * as moment from 'moment'; @@ -32,23 +32,28 @@ export class EditEmployeeOtherSettingsComponent implements OnInit, OnDestroy { */ @ViewChild('general') general: NbAccordionItemComponent; @ViewChild('integrations') integrations: NbAccordionItemComponent; + @ViewChild('timer') timer: NbAccordionItemComponent; /** * Employee other settings settings */ - public form: UntypedFormGroup = EditEmployeeOtherSettingsComponent.buildForm(this.fb); - static buildForm(fb: UntypedFormBuilder): UntypedFormGroup { + public form: FormGroup = EditEmployeeOtherSettingsComponent.buildForm(this.fb); + static buildForm(fb: FormBuilder): FormGroup { return fb.group({ timeZone: [], timeFormat: [], upworkId: [], - linkedInId: [] + linkedInId: [], + allowManualTime: [false], + allowModifyTime: [false], + allowDeleteTime: [false], + allowScreenshotCapture: [true] }); } constructor( private readonly cdr: ChangeDetectorRef, - private readonly fb: UntypedFormBuilder, + private readonly fb: FormBuilder, private readonly employeeStore: EmployeeStore ) {} @@ -82,7 +87,11 @@ export class EditEmployeeOtherSettingsComponent implements OnInit, OnDestroy { timeZone: user.timeZone || moment.tz.guess(), // set current timezone, if employee don't have any timezone timeFormat: user.timeFormat, upworkId: employee.upworkId, - linkedInId: employee.linkedInId + linkedInId: employee.linkedInId, + allowManualTime: employee.allowManualTime, + allowDeleteTime: employee.allowDeleteTime, + allowModifyTime: employee.allowModifyTime, + allowScreenshotCapture: employee.allowScreenshotCapture }); this.form.updateValueAndValidity(); } @@ -97,7 +106,16 @@ export class EditEmployeeOtherSettingsComponent implements OnInit, OnDestroy { return; } const { organizationId, tenantId } = this.selectedEmployee; - const { timeZone, timeFormat, upworkId, linkedInId } = this.form.value; + const { + timeZone, + timeFormat, + upworkId, + linkedInId, + allowScreenshotCapture, + allowManualTime, + allowModifyTime, + allowDeleteTime + } = this.form.value; /** Update user fields */ this.employeeStore.userForm = { @@ -110,7 +128,11 @@ export class EditEmployeeOtherSettingsComponent implements OnInit, OnDestroy { upworkId, linkedInId, organizationId, - tenantId + tenantId, + allowManualTime, + allowModifyTime, + allowDeleteTime, + allowScreenshotCapture }; } diff --git a/packages/contracts/src/employee.model.ts b/packages/contracts/src/employee.model.ts index bf4bab5e5fe..3bbd3957f24 100644 --- a/packages/contracts/src/employee.model.ts +++ b/packages/contracts/src/employee.model.ts @@ -113,6 +113,10 @@ export interface IEmployee extends IBasePerTenantAndOrganizationEntityModel, ITa isTrackingEnabled: boolean; isDeleted?: boolean; allowScreenshotCapture?: boolean; + allowManualTime?: boolean; + allowModifyTime?: boolean; + allowDeleteTime?: boolean; + /** Upwork ID For Gauzy AI*/ upworkId?: string; /** LinkedIn ID For Gauzy AI*/ @@ -176,6 +180,9 @@ export interface IEmployeeUpdateInput extends IBasePerTenantAndOrganizationEntit upworkUrl?: string; profile_link?: string; allowScreenshotCapture?: boolean; + allowManualTime?: boolean; + allowModifyTime?: boolean; + allowDeleteTime?: boolean; /** Upwork ID For Gauzy AI*/ upworkId?: string; /** LinkedIn ID For Gauzy AI*/ diff --git a/packages/core/src/database/migrations/1729867054227-AddTimeTrackingPermissionsColumnsToEmployeeTable.ts b/packages/core/src/database/migrations/1729867054227-AddTimeTrackingPermissionsColumnsToEmployeeTable.ts new file mode 100644 index 00000000000..d24fe4f5013 --- /dev/null +++ b/packages/core/src/database/migrations/1729867054227-AddTimeTrackingPermissionsColumnsToEmployeeTable.ts @@ -0,0 +1,166 @@ +import { Logger } from '@nestjs/common'; +import { MigrationInterface, QueryRunner } from 'typeorm'; +import { yellow } from 'chalk'; +import { DatabaseTypeEnum } from '@gauzy/config'; + +export class AddTimeTrackingPermissionsColumnsToEmployeeTable1729867054227 implements MigrationInterface { + name = 'AddTimeTrackingPermissionsColumnsToEmployeeTable1729867054227'; + + /** + * Up Migration + * + * @param queryRunner + */ + public async up(queryRunner: QueryRunner): Promise { + Logger.debug(yellow(this.name + ' start running!'), 'Migration'); + + 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 "employee" ADD "allowManualTime" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "employee" ADD "allowModifyTime" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "employee" ADD "allowDeleteTime" boolean NOT NULL DEFAULT false`); + } + + /** + * PostgresDB Down Migration + * + * @param queryRunner + */ + public async postgresDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "employee" DROP COLUMN "allowDeleteTime"`); + await queryRunner.query(`ALTER TABLE "employee" DROP COLUMN "allowModifyTime"`); + await queryRunner.query(`ALTER TABLE "employee" DROP COLUMN "allowManualTime"`); + } + + /** + * SqliteDB and BetterSQlite3DB Up Migration + * + * @param queryRunner + */ + public async sqliteUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_5e719204dcafa8d6b2ecdeda13"`); + await queryRunner.query(`DROP INDEX "IDX_1c0c1370ecd98040259625e17e"`); + await queryRunner.query(`DROP INDEX "IDX_f4b0d329c4a3cf79ffe9d56504"`); + await queryRunner.query(`DROP INDEX "IDX_96dfbcaa2990df01fe5bb39ccc"`); + await queryRunner.query(`DROP INDEX "IDX_c6a48286f3aa8ae903bee0d1e7"`); + await queryRunner.query(`DROP INDEX "IDX_4b3303a6b7eb92d237a4379734"`); + await queryRunner.query(`DROP INDEX "IDX_510cb87f5da169e57e694d1a5c"`); + await queryRunner.query(`DROP INDEX "IDX_175b7be641928a31521224daa8"`); + await queryRunner.query( + `CREATE TABLE "temporary_employee" ("id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "tenantId" varchar, "organizationId" varchar, "valueDate" datetime, "isActive" boolean DEFAULT (1), "short_description" varchar(200), "description" varchar, "startedWorkOn" datetime, "endWork" datetime, "payPeriod" varchar, "billRateValue" integer, "billRateCurrency" varchar, "reWeeklyLimit" integer, "offerDate" datetime, "acceptDate" datetime, "rejectDate" datetime, "employeeLevel" varchar(500), "anonymousBonus" boolean, "averageIncome" numeric, "averageBonus" numeric, "totalWorkHours" numeric DEFAULT (0), "averageExpenses" numeric, "show_anonymous_bonus" boolean, "show_average_bonus" boolean, "show_average_expenses" boolean, "show_average_income" boolean, "show_billrate" boolean, "show_payperiod" boolean, "show_start_work_on" boolean, "isJobSearchActive" boolean, "linkedInUrl" varchar, "facebookUrl" varchar, "instagramUrl" varchar, "twitterUrl" varchar, "githubUrl" varchar, "gitlabUrl" varchar, "upworkUrl" varchar, "stackoverflowUrl" varchar, "isVerified" boolean, "isVetted" boolean, "totalJobs" numeric, "jobSuccess" numeric, "profile_link" varchar, "userId" varchar NOT NULL, "contactId" varchar, "organizationPositionId" varchar, "isTrackingEnabled" boolean DEFAULT (0), "deletedAt" datetime, "allowScreenshotCapture" boolean NOT NULL DEFAULT (1), "upworkId" varchar, "linkedInId" varchar, "isOnline" boolean DEFAULT (0), "isTrackingTime" boolean DEFAULT (0), "minimumBillingRate" integer, "isAway" boolean DEFAULT (0), "isArchived" boolean DEFAULT (0), "fix_relational_custom_fields" boolean, "archivedAt" datetime, "allowManualTime" boolean NOT NULL DEFAULT (0), "allowModifyTime" boolean NOT NULL DEFAULT (0), "allowDeleteTime" boolean NOT NULL DEFAULT (0), CONSTRAINT "REL_1c0c1370ecd98040259625e17e" UNIQUE ("contactId"), CONSTRAINT "REL_f4b0d329c4a3cf79ffe9d56504" UNIQUE ("userId"), CONSTRAINT "FK_5e719204dcafa8d6b2ecdeda130" FOREIGN KEY ("organizationPositionId") REFERENCES "organization_position" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_1c0c1370ecd98040259625e17e2" FOREIGN KEY ("contactId") REFERENCES "contact" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_f4b0d329c4a3cf79ffe9d565047" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_c6a48286f3aa8ae903bee0d1e72" FOREIGN KEY ("organizationId") REFERENCES "organization" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_4b3303a6b7eb92d237a4379734e" FOREIGN KEY ("tenantId") REFERENCES "tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "temporary_employee"("id", "createdAt", "updatedAt", "tenantId", "organizationId", "valueDate", "isActive", "short_description", "description", "startedWorkOn", "endWork", "payPeriod", "billRateValue", "billRateCurrency", "reWeeklyLimit", "offerDate", "acceptDate", "rejectDate", "employeeLevel", "anonymousBonus", "averageIncome", "averageBonus", "totalWorkHours", "averageExpenses", "show_anonymous_bonus", "show_average_bonus", "show_average_expenses", "show_average_income", "show_billrate", "show_payperiod", "show_start_work_on", "isJobSearchActive", "linkedInUrl", "facebookUrl", "instagramUrl", "twitterUrl", "githubUrl", "gitlabUrl", "upworkUrl", "stackoverflowUrl", "isVerified", "isVetted", "totalJobs", "jobSuccess", "profile_link", "userId", "contactId", "organizationPositionId", "isTrackingEnabled", "deletedAt", "allowScreenshotCapture", "upworkId", "linkedInId", "isOnline", "isTrackingTime", "minimumBillingRate", "isAway", "isArchived", "fix_relational_custom_fields", "archivedAt") SELECT "id", "createdAt", "updatedAt", "tenantId", "organizationId", "valueDate", "isActive", "short_description", "description", "startedWorkOn", "endWork", "payPeriod", "billRateValue", "billRateCurrency", "reWeeklyLimit", "offerDate", "acceptDate", "rejectDate", "employeeLevel", "anonymousBonus", "averageIncome", "averageBonus", "totalWorkHours", "averageExpenses", "show_anonymous_bonus", "show_average_bonus", "show_average_expenses", "show_average_income", "show_billrate", "show_payperiod", "show_start_work_on", "isJobSearchActive", "linkedInUrl", "facebookUrl", "instagramUrl", "twitterUrl", "githubUrl", "gitlabUrl", "upworkUrl", "stackoverflowUrl", "isVerified", "isVetted", "totalJobs", "jobSuccess", "profile_link", "userId", "contactId", "organizationPositionId", "isTrackingEnabled", "deletedAt", "allowScreenshotCapture", "upworkId", "linkedInId", "isOnline", "isTrackingTime", "minimumBillingRate", "isAway", "isArchived", "fix_relational_custom_fields", "archivedAt" FROM "employee"` + ); + await queryRunner.query(`DROP TABLE "employee"`); + await queryRunner.query(`ALTER TABLE "temporary_employee" RENAME TO "employee"`); + await queryRunner.query( + `CREATE INDEX "IDX_5e719204dcafa8d6b2ecdeda13" ON "employee" ("organizationPositionId") ` + ); + await queryRunner.query(`CREATE INDEX "IDX_1c0c1370ecd98040259625e17e" ON "employee" ("contactId") `); + await queryRunner.query(`CREATE INDEX "IDX_f4b0d329c4a3cf79ffe9d56504" ON "employee" ("userId") `); + await queryRunner.query(`CREATE INDEX "IDX_96dfbcaa2990df01fe5bb39ccc" ON "employee" ("profile_link") `); + await queryRunner.query(`CREATE INDEX "IDX_c6a48286f3aa8ae903bee0d1e7" ON "employee" ("organizationId") `); + await queryRunner.query(`CREATE INDEX "IDX_4b3303a6b7eb92d237a4379734" ON "employee" ("tenantId") `); + await queryRunner.query(`CREATE INDEX "IDX_510cb87f5da169e57e694d1a5c" ON "employee" ("isActive") `); + await queryRunner.query(`CREATE INDEX "IDX_175b7be641928a31521224daa8" ON "employee" ("isArchived") `); + } + + /** + * SqliteDB and BetterSQlite3DB Down Migration + * + * @param queryRunner + */ + public async sqliteDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_175b7be641928a31521224daa8"`); + await queryRunner.query(`DROP INDEX "IDX_510cb87f5da169e57e694d1a5c"`); + await queryRunner.query(`DROP INDEX "IDX_4b3303a6b7eb92d237a4379734"`); + await queryRunner.query(`DROP INDEX "IDX_c6a48286f3aa8ae903bee0d1e7"`); + await queryRunner.query(`DROP INDEX "IDX_96dfbcaa2990df01fe5bb39ccc"`); + await queryRunner.query(`DROP INDEX "IDX_f4b0d329c4a3cf79ffe9d56504"`); + await queryRunner.query(`DROP INDEX "IDX_1c0c1370ecd98040259625e17e"`); + await queryRunner.query(`DROP INDEX "IDX_5e719204dcafa8d6b2ecdeda13"`); + await queryRunner.query(`ALTER TABLE "employee" RENAME TO "temporary_employee"`); + await queryRunner.query( + `CREATE TABLE "employee" ("id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "tenantId" varchar, "organizationId" varchar, "valueDate" datetime, "isActive" boolean DEFAULT (1), "short_description" varchar(200), "description" varchar, "startedWorkOn" datetime, "endWork" datetime, "payPeriod" varchar, "billRateValue" integer, "billRateCurrency" varchar, "reWeeklyLimit" integer, "offerDate" datetime, "acceptDate" datetime, "rejectDate" datetime, "employeeLevel" varchar(500), "anonymousBonus" boolean, "averageIncome" numeric, "averageBonus" numeric, "totalWorkHours" numeric DEFAULT (0), "averageExpenses" numeric, "show_anonymous_bonus" boolean, "show_average_bonus" boolean, "show_average_expenses" boolean, "show_average_income" boolean, "show_billrate" boolean, "show_payperiod" boolean, "show_start_work_on" boolean, "isJobSearchActive" boolean, "linkedInUrl" varchar, "facebookUrl" varchar, "instagramUrl" varchar, "twitterUrl" varchar, "githubUrl" varchar, "gitlabUrl" varchar, "upworkUrl" varchar, "stackoverflowUrl" varchar, "isVerified" boolean, "isVetted" boolean, "totalJobs" numeric, "jobSuccess" numeric, "profile_link" varchar, "userId" varchar NOT NULL, "contactId" varchar, "organizationPositionId" varchar, "isTrackingEnabled" boolean DEFAULT (0), "deletedAt" datetime, "allowScreenshotCapture" boolean NOT NULL DEFAULT (1), "upworkId" varchar, "linkedInId" varchar, "isOnline" boolean DEFAULT (0), "isTrackingTime" boolean DEFAULT (0), "minimumBillingRate" integer, "isAway" boolean DEFAULT (0), "isArchived" boolean DEFAULT (0), "fix_relational_custom_fields" boolean, "archivedAt" datetime, CONSTRAINT "REL_1c0c1370ecd98040259625e17e" UNIQUE ("contactId"), CONSTRAINT "REL_f4b0d329c4a3cf79ffe9d56504" UNIQUE ("userId"), CONSTRAINT "FK_5e719204dcafa8d6b2ecdeda130" FOREIGN KEY ("organizationPositionId") REFERENCES "organization_position" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_1c0c1370ecd98040259625e17e2" FOREIGN KEY ("contactId") REFERENCES "contact" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_f4b0d329c4a3cf79ffe9d565047" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_c6a48286f3aa8ae903bee0d1e72" FOREIGN KEY ("organizationId") REFERENCES "organization" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_4b3303a6b7eb92d237a4379734e" FOREIGN KEY ("tenantId") REFERENCES "tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "employee"("id", "createdAt", "updatedAt", "tenantId", "organizationId", "valueDate", "isActive", "short_description", "description", "startedWorkOn", "endWork", "payPeriod", "billRateValue", "billRateCurrency", "reWeeklyLimit", "offerDate", "acceptDate", "rejectDate", "employeeLevel", "anonymousBonus", "averageIncome", "averageBonus", "totalWorkHours", "averageExpenses", "show_anonymous_bonus", "show_average_bonus", "show_average_expenses", "show_average_income", "show_billrate", "show_payperiod", "show_start_work_on", "isJobSearchActive", "linkedInUrl", "facebookUrl", "instagramUrl", "twitterUrl", "githubUrl", "gitlabUrl", "upworkUrl", "stackoverflowUrl", "isVerified", "isVetted", "totalJobs", "jobSuccess", "profile_link", "userId", "contactId", "organizationPositionId", "isTrackingEnabled", "deletedAt", "allowScreenshotCapture", "upworkId", "linkedInId", "isOnline", "isTrackingTime", "minimumBillingRate", "isAway", "isArchived", "fix_relational_custom_fields", "archivedAt") SELECT "id", "createdAt", "updatedAt", "tenantId", "organizationId", "valueDate", "isActive", "short_description", "description", "startedWorkOn", "endWork", "payPeriod", "billRateValue", "billRateCurrency", "reWeeklyLimit", "offerDate", "acceptDate", "rejectDate", "employeeLevel", "anonymousBonus", "averageIncome", "averageBonus", "totalWorkHours", "averageExpenses", "show_anonymous_bonus", "show_average_bonus", "show_average_expenses", "show_average_income", "show_billrate", "show_payperiod", "show_start_work_on", "isJobSearchActive", "linkedInUrl", "facebookUrl", "instagramUrl", "twitterUrl", "githubUrl", "gitlabUrl", "upworkUrl", "stackoverflowUrl", "isVerified", "isVetted", "totalJobs", "jobSuccess", "profile_link", "userId", "contactId", "organizationPositionId", "isTrackingEnabled", "deletedAt", "allowScreenshotCapture", "upworkId", "linkedInId", "isOnline", "isTrackingTime", "minimumBillingRate", "isAway", "isArchived", "fix_relational_custom_fields", "archivedAt" FROM "temporary_employee"` + ); + await queryRunner.query(`DROP TABLE "temporary_employee"`); + await queryRunner.query(`CREATE INDEX "IDX_175b7be641928a31521224daa8" ON "employee" ("isArchived") `); + await queryRunner.query(`CREATE INDEX "IDX_510cb87f5da169e57e694d1a5c" ON "employee" ("isActive") `); + await queryRunner.query(`CREATE INDEX "IDX_4b3303a6b7eb92d237a4379734" ON "employee" ("tenantId") `); + await queryRunner.query(`CREATE INDEX "IDX_c6a48286f3aa8ae903bee0d1e7" ON "employee" ("organizationId") `); + await queryRunner.query(`CREATE INDEX "IDX_96dfbcaa2990df01fe5bb39ccc" ON "employee" ("profile_link") `); + await queryRunner.query(`CREATE INDEX "IDX_f4b0d329c4a3cf79ffe9d56504" ON "employee" ("userId") `); + await queryRunner.query(`CREATE INDEX "IDX_1c0c1370ecd98040259625e17e" ON "employee" ("contactId") `); + await queryRunner.query( + `CREATE INDEX "IDX_5e719204dcafa8d6b2ecdeda13" ON "employee" ("organizationPositionId") ` + ); + } + + /** + * MySQL Up Migration + * + * @param queryRunner + */ + public async mysqlUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`employee\` ADD \`allowManualTime\` tinyint NOT NULL DEFAULT 0`); + await queryRunner.query(`ALTER TABLE \`employee\` ADD \`allowModifyTime\` tinyint NOT NULL DEFAULT 0`); + await queryRunner.query(`ALTER TABLE \`employee\` ADD \`allowDeleteTime\` tinyint NOT NULL DEFAULT 0`); + } + + /** + * MySQL Down Migration + * + * @param queryRunner + */ + public async mysqlDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`employee\` DROP COLUMN \`allowDeleteTime\``); + await queryRunner.query(`ALTER TABLE \`employee\` DROP COLUMN \`allowModifyTime\``); + await queryRunner.query(`ALTER TABLE \`employee\` DROP COLUMN \`allowManualTime\``); + } +} diff --git a/packages/core/src/employee/commands/handlers/employee.update.handler.ts b/packages/core/src/employee/commands/handlers/employee.update.handler.ts index 95cfa25641c..e43379d0624 100644 --- a/packages/core/src/employee/commands/handlers/employee.update.handler.ts +++ b/packages/core/src/employee/commands/handlers/employee.update.handler.ts @@ -7,10 +7,7 @@ import { RequestContext } from './../../../core/context'; @CommandHandler(EmployeeUpdateCommand) export class EmployeeUpdateHandler implements ICommandHandler { - - constructor( - private readonly _employeeService: EmployeeService, - ) { } + constructor(private readonly _employeeService: EmployeeService) {} public async execute(command: EmployeeUpdateCommand): Promise { const { id, input } = command; diff --git a/packages/core/src/employee/dto/update-employee.dto.ts b/packages/core/src/employee/dto/update-employee.dto.ts index 5c9c2e3567a..8313cff2363 100644 --- a/packages/core/src/employee/dto/update-employee.dto.ts +++ b/packages/core/src/employee/dto/update-employee.dto.ts @@ -31,7 +31,10 @@ export class UpdateEmployeeDTO 'isTrackingEnabled', 'isTrackingTime', 'isJobSearchActive', - 'allowScreenshotCapture' + 'allowScreenshotCapture', + 'allowManualTime', + 'allowModifyTime', + 'allowDeleteTime' ] as const) ) implements IEmployeeUpdateInput {} diff --git a/packages/core/src/employee/employee.entity.ts b/packages/core/src/employee/employee.entity.ts index a95c7117774..043079d7729 100644 --- a/packages/core/src/employee/employee.entity.ts +++ b/packages/core/src/employee/employee.entity.ts @@ -376,6 +376,36 @@ export class Employee extends TenantOrganizationBaseEntity implements IEmployee, @MultiORMColumn({ default: true }) allowScreenshotCapture?: boolean; + /** + * Indicates whether manual time entry is allowed for time tracking + * for a specific employee. + */ + @ApiPropertyOptional({ type: () => Boolean }) + @IsOptional() + @IsBoolean() + @MultiORMColumn({ default: false }) + allowManualTime?: boolean; + + /** + * Indicates whether modification of time entries is allowed for time tracking + * for a specific employee. + */ + @ApiPropertyOptional({ type: () => Boolean }) + @IsOptional() + @IsBoolean() + @MultiORMColumn({ default: false }) + allowModifyTime?: boolean; + + /** + * Indicates whether deletion of time entries is allowed for time tracking + * for a specific employee. + */ + @ApiPropertyOptional({ type: () => Boolean }) + @IsOptional() + @IsBoolean() + @MultiORMColumn({ default: false }) + allowDeleteTime?: boolean; + /** Upwork ID */ @ApiPropertyOptional({ type: () => String }) @IsOptional() diff --git a/packages/core/src/employee/employee.service.ts b/packages/core/src/employee/employee.service.ts index 5fc7b8a1d3e..c716fa91f87 100644 --- a/packages/core/src/employee/employee.service.ts +++ b/packages/core/src/employee/employee.service.ts @@ -449,6 +449,9 @@ export class EmployeeService extends TenantAwareCrudService { isTrackingEnabled: true, deletedAt: true, allowScreenshotCapture: true, + allowManualTime: true, + allowModifyTime: true, + allowDeleteTime: true, isActive: true, isArchived: true, isAway: true, diff --git a/packages/ui-core/shared/src/lib/directives/time-tracking-authorized-directive.ts b/packages/ui-core/shared/src/lib/directives/time-tracking-authorized-directive.ts index 954b1d0f4e7..fbb6c2f4045 100644 --- a/packages/ui-core/shared/src/lib/directives/time-tracking-authorized-directive.ts +++ b/packages/ui-core/shared/src/lib/directives/time-tracking-authorized-directive.ts @@ -1,8 +1,8 @@ import { ChangeDetectorRef, Directive, Input, OnInit, TemplateRef, ViewContainerRef } from '@angular/core'; -import { filter, tap } from 'rxjs/operators'; +import { filter, tap, map, switchMap, distinctUntilChanged } from 'rxjs/operators'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import * as camelcase from 'camelcase'; -import { IOrganization } from '@gauzy/contracts'; +import { IOrganization, IUser } from '@gauzy/contracts'; import { distinctUntilChange } from '@gauzy/ui-core/common'; import { Store } from '@gauzy/ui-core/core'; @@ -11,19 +11,23 @@ import { Store } from '@gauzy/ui-core/core'; selector: '[ngxTimeTrackingAuthorized]' }) export class TimeTrackingAuthorizedDirective implements OnInit { - /* - * Getter & Setter for dynamic permission + private _permission: string | string[] = []; // Default initialization + /** + * Setter for dynamic permission. + * @param permission - The permission(s) to be set. */ - _permission: string | string[]; - get permission(): string | string[] { - return this._permission; - } @Input() set permission(permission: string | string[]) { if (!permission) { - throw false; + throw new Error('Permission must be provided'); } this._permission = permission; } + /** + * Getter for dynamic permission. + */ + get permission(): string | string[] { + return this._permission; + } @Input() permissionElse: TemplateRef; @@ -39,10 +43,27 @@ export class TimeTrackingAuthorizedDirective implements OnInit { .pipe( distinctUntilChange(), filter((organization: IOrganization) => !!organization), - filter((organization: IOrganization) => camelcase(this.permission) in organization), - tap(() => this._viewContainer.clear()), - tap((organization: IOrganization) => { - if (organization[camelcase(this.permission)]) { + switchMap((organization: IOrganization) => + this._store.user$.pipe( + filter((user: IUser) => !!user), + map((user: IUser) => { + // Determine permission based on employee existence + const hasPermission = user.employee + ? camelcase(this.permission) in organization && + organization[camelcase(this.permission)] && + camelcase(this.permission) in user.employee && + user.employee[camelcase(this.permission)] + : camelcase(this.permission) in organization && + organization[camelcase(this.permission)]; + + return hasPermission; + }), + distinctUntilChanged() // Only emit when permission status changes + ) + ), + tap((hasPermission: boolean) => { + if (hasPermission) { + this._viewContainer.clear(); // Clear the container once per status change this._viewContainer.createEmbeddedView(this._templateRef); } else { this.showTemplateBlockInView(this.permissionElse); @@ -60,10 +81,11 @@ export class TimeTrackingAuthorizedDirective implements OnInit { * @returns */ showTemplateBlockInView(template: TemplateRef) { - this._viewContainer.clear(); + this._viewContainer.clear(); // Clear the container once per status change if (!template) { return; } + this._viewContainer.createEmbeddedView(template); this._cdr.markForCheck(); }