diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts index 6ec175dde0..c879cc2e86 100644 --- a/packages/contracts/src/index.ts +++ b/packages/contracts/src/index.ts @@ -40,6 +40,7 @@ export * from './lib/email-reset.model'; export * from './lib/email-template.model'; export * from './lib/email.model'; export * from './lib/employee-appointment.model'; +export * from './lib/employee-availability.model'; export * from './lib/employee-award.model'; export * from './lib/employee-job.model'; export * from './lib/employee-phone.model'; diff --git a/packages/contracts/src/lib/employee-availability.model.ts b/packages/contracts/src/lib/employee-availability.model.ts new file mode 100644 index 0000000000..9520e78dca --- /dev/null +++ b/packages/contracts/src/lib/employee-availability.model.ts @@ -0,0 +1,64 @@ +import { IBasePerTenantAndOrganizationEntityModel, ID } from './base-entity.model'; +import { IEmployee } from './employee.model'; + +/** + * Enum representing different availability statuses. + */ +export enum AvailabilityStatusEnum { + Available = 'Available', + Partial = 'Partial', + Unavailable = 'Unavailable' +} + +/** + * Enum mapping availability statuses to numerical values. + */ +export enum AvailabilityStatusValue { + Available = 0, + Partial = 1, + Unavailable = 2 +} + +/** + * A mapping object to relate status labels to their respective numerical values. + */ +export const AvailabilityStatusMap: Record = { + [AvailabilityStatusEnum.Available]: AvailabilityStatusValue.Available, + [AvailabilityStatusEnum.Partial]: AvailabilityStatusValue.Partial, + [AvailabilityStatusEnum.Unavailable]: AvailabilityStatusValue.Unavailable +}; + +/** + * Base interface for Employee Availability data. + * Includes common properties shared across different input types. + */ +interface IBaseEmployeeAvailability extends IBasePerTenantAndOrganizationEntityModel { + employeeId: ID; + startDate: Date; + endDate: Date; + dayOfWeek: number; // 0 = Sunday, 6 = Saturday + availabilityStatus: AvailabilityStatusEnum; + availabilityNotes?: string; +} + +/** + * Represents an Employee Availability record. + */ +export interface IEmployeeAvailability extends IBaseEmployeeAvailability { + employee: IEmployee; +} + +/** + * Input interface for finding Employee Availability records. + */ +export interface IEmployeeAvailabilityFindInput extends Partial {} + +/** + * Input interface for creating new Employee Availability records. + */ +export interface IEmployeeAvailabilityCreateInput extends IBaseEmployeeAvailability {} + +/** + * Input interface for updating Employee Availability records. + */ +export interface IEmployeeAvailabilityUpdateInput extends Partial {} diff --git a/packages/contracts/src/lib/role-permission.model.ts b/packages/contracts/src/lib/role-permission.model.ts index 727f5b7516..764d121582 100644 --- a/packages/contracts/src/lib/role-permission.model.ts +++ b/packages/contracts/src/lib/role-permission.model.ts @@ -224,11 +224,15 @@ export enum PermissionsEnum { DASHBOARD_READ = 'DASHBOARD_READ', DASHBOARD_UPDATE = 'DASHBOARD_UPDATE', DASHBOARD_DELETE = 'DASHBOARD_DELETE', - /** Tenant API Key */ TENANT_API_KEY_CREATE = 'TENANT_API_KEY_CREATE', TENANT_API_KEY_VIEW = 'TENANT_API_KEY_VIEW', - TENANT_API_KEY_DELETE = 'TENANT_API_KEY_DELETE' + TENANT_API_KEY_DELETE = 'TENANT_API_KEY_DELETE', + /** Employee Availability CRUD Permissions Start */ + EMPLOYEE_AVAILABILITY_CREATE = 'EMPLOYEE_AVAILABILITY_CREATE', + EMPLOYEE_AVAILABILITY_READ = 'EMPLOYEE_AVAILABILITY_READ', + EMPLOYEE_AVAILABILITY_UPDATE = 'EMPLOYEE_AVAILABILITY_UPDATE', + EMPLOYEE_AVAILABILITY_DELETE = 'EMPLOYEE_AVAILABILITY_DELETE' } export const PermissionGroups = { @@ -401,7 +405,12 @@ export const PermissionGroups = { PermissionsEnum.ALLOW_MANUAL_TIME, PermissionsEnum.DELETE_SCREENSHOTS, PermissionsEnum.ACCESS_DELETE_ACCOUNT, - PermissionsEnum.ORG_MEMBER_LAST_LOG_VIEW + PermissionsEnum.ORG_MEMBER_LAST_LOG_VIEW, + /** Employee Availability CRUD Permissions Start */ + PermissionsEnum.EMPLOYEE_AVAILABILITY_CREATE, + PermissionsEnum.EMPLOYEE_AVAILABILITY_READ, + PermissionsEnum.EMPLOYEE_AVAILABILITY_UPDATE, + PermissionsEnum.EMPLOYEE_AVAILABILITY_DELETE ], //Readonly permissions, are only enabled for Super Admin/Admin role diff --git a/packages/core/src/lib/core/crud/crud.service.ts b/packages/core/src/lib/core/crud/crud.service.ts index a9d6395ac1..bd5207d6ed 100644 --- a/packages/core/src/lib/core/crud/crud.service.ts +++ b/packages/core/src/lib/core/crud/crud.service.ts @@ -16,7 +16,7 @@ import { import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity'; import { Collection, CreateOptions, FilterQuery as MikroFilterQuery, RequiredEntityData, wrap } from '@mikro-orm/core'; import { AssignOptions } from '@mikro-orm/knex'; -import { IPagination } from '@gauzy/contracts'; +import { ID, IPagination } from '@gauzy/contracts'; import { BaseEntity, SoftDeletableBaseEntity } from '../entities/internal'; import { multiORMCreateQueryBuilder } from '../../core/orm/query-builder/query-builder.factory'; import { IQueryBuilder } from '../../core/orm/query-builder/iquery-builder'; @@ -45,7 +45,6 @@ import { ITryRequest } from './try-request'; const ormType: MultiORM = getORMType(); export abstract class CrudService implements ICrudService { - constructor( protected readonly typeOrmRepository: Repository, protected readonly mikroOrmRepository: MikroOrmBaseEntityRepository @@ -347,7 +346,7 @@ export abstract class CrudService implements ICrudService< * @param options * @returns */ - public async findOneByIdString(id: T['id'], options?: IFindOneOptions): Promise { + public async findOneByIdString(id: ID, options?: IFindOneOptions): Promise { let record: T; switch (this.ormType) { @@ -645,7 +644,7 @@ export abstract class CrudService implements ICrudService< * @param saveOptions - Additional save options for the ORM operation (specific to TypeORM). * @returns A promise that resolves to the softly removed entity. */ - public async softRemove(id: T['id'], options?: IFindOneOptions, saveOptions?: SaveOptions): Promise { + public async softRemove(id: ID, options?: IFindOneOptions, saveOptions?: SaveOptions): Promise { try { switch (this.ormType) { case MultiORMEnum.MikroORM: { @@ -681,7 +680,7 @@ export abstract class CrudService implements ICrudService< * @param options - Optional settings for database save operations. * @returns A promise that resolves with the recovered entity. */ - public async softRecover(id: T['id'], options?: IFindOneOptions, saveOptions?: SaveOptions): Promise { + public async softRecover(id: ID, options?: IFindOneOptions, saveOptions?: SaveOptions): Promise { try { switch (this.ormType) { case MultiORMEnum.MikroORM: { diff --git a/packages/core/src/lib/core/crud/tenant-aware-crud.service.ts b/packages/core/src/lib/core/crud/tenant-aware-crud.service.ts index 2154948e7b..d125ed3ef8 100644 --- a/packages/core/src/lib/core/crud/tenant-aware-crud.service.ts +++ b/packages/core/src/lib/core/crud/tenant-aware-crud.service.ts @@ -1,7 +1,7 @@ import { NotFoundException } from '@nestjs/common'; import { DeleteResult, FindOptionsWhere, FindManyOptions, FindOneOptions, Repository, UpdateResult } from 'typeorm'; import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity'; -import { IPagination, IUser, PermissionsEnum } from '@gauzy/contracts'; +import { ID, IPagination, IUser, PermissionsEnum } from '@gauzy/contracts'; import { isNotEmpty } from '@gauzy/utils'; import { MikroOrmBaseEntityRepository } from '../../core/repository/mikro-orm-base-entity.repository'; import { RequestContext } from '../context'; @@ -239,7 +239,7 @@ export abstract class TenantAwareCrudService * @param options * @returns */ - public async findOneOrFailByIdString(id: T['id'], options?: FindOneOptions): Promise> { + public async findOneOrFailByIdString(id: ID, options?: FindOneOptions): Promise> { return await super.findOneOrFailByIdString(id, this.findOneWithTenant(options)); } @@ -282,7 +282,7 @@ export abstract class TenantAwareCrudService * @param options * @returns */ - public async findOneByIdString(id: T['id'], options?: FindOneOptions): Promise { + public async findOneByIdString(id: ID, options?: FindOneOptions): Promise { return await super.findOneByIdString(id, this.findOneWithTenant(options)); } diff --git a/packages/core/src/lib/core/entities/index.ts b/packages/core/src/lib/core/entities/index.ts index 6d796401cb..8c6bda7261 100644 --- a/packages/core/src/lib/core/entities/index.ts +++ b/packages/core/src/lib/core/entities/index.ts @@ -32,6 +32,7 @@ import { EmailTemplate, Employee, EmployeeAppointment, + EmployeeAvailability, EmployeeAward, EmployeeLevel, EmployeePhone, @@ -190,6 +191,7 @@ export const coreEntities = [ EmailTemplate, Employee, EmployeeAppointment, + EmployeeAvailability, EmployeeAward, EmployeeLevel, EmployeePhone, diff --git a/packages/core/src/lib/core/entities/internal.ts b/packages/core/src/lib/core/entities/internal.ts index 99acc3ce69..42bf5c166b 100644 --- a/packages/core/src/lib/core/entities/internal.ts +++ b/packages/core/src/lib/core/entities/internal.ts @@ -36,6 +36,7 @@ export * from '../../email-history/email-history.entity'; export * from '../../email-reset/email-reset.entity'; export * from '../../email-template/email-template.entity'; export * from '../../employee-appointment/employee-appointment.entity'; +export * from '../../employee-availability/employee-availability.entity'; export * from '../../employee-award/employee-award.entity'; export * from '../../employee-level/employee-level.entity'; export * from '../../employee-phone/employee-phone.entity'; diff --git a/packages/core/src/lib/database/migrations/1737958313399-CreateEmployeeAvailabilityTable.ts b/packages/core/src/lib/database/migrations/1737958313399-CreateEmployeeAvailabilityTable.ts new file mode 100644 index 0000000000..912ccd1445 --- /dev/null +++ b/packages/core/src/lib/database/migrations/1737958313399-CreateEmployeeAvailabilityTable.ts @@ -0,0 +1,227 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; +import * as chalk from 'chalk'; +import { DatabaseTypeEnum } from '@gauzy/config'; + +export class CreateEmployeeAvailabilityTable1737958313399 implements MigrationInterface { + name = 'CreateEmployeeAvailabilityTable1737958313399'; + + /** + * Up Migration + * + * @param queryRunner + */ + public async up(queryRunner: QueryRunner): Promise { + console.log(chalk.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( + `CREATE TABLE "employee_availability" ("deletedAt" TIMESTAMP, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "id" uuid NOT NULL DEFAULT gen_random_uuid(), "isActive" boolean DEFAULT true, "isArchived" boolean DEFAULT false, "archivedAt" TIMESTAMP, "tenantId" uuid, "organizationId" uuid, "startDate" TIMESTAMP NOT NULL, "endDate" TIMESTAMP NOT NULL, "dayOfWeek" integer NOT NULL, "availabilityStatus" integer NOT NULL, "availabilityNotes" text, "employeeId" uuid NOT NULL, CONSTRAINT "PK_8c252ae622cac6f708bc79aa3e7" PRIMARY KEY ("id"))` + ); + await queryRunner.query( + `CREATE INDEX "IDX_4df1dc5482972ff5344b670f78" ON "employee_availability" ("isActive") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_1c5a1979ce3d47b895f11f7395" ON "employee_availability" ("isArchived") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_981ccd9a51cc8706cbf8cdbdfb" ON "employee_availability" ("tenantId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_9324ccb5291bd98d5fba6349f7" ON "employee_availability" ("organizationId") ` + ); + await queryRunner.query( + `ALTER TABLE "employee_availability" ADD CONSTRAINT "FK_981ccd9a51cc8706cbf8cdbdfb6" FOREIGN KEY ("tenantId") REFERENCES "tenant"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE "employee_availability" ADD CONSTRAINT "FK_9324ccb5291bd98d5fba6349f75" FOREIGN KEY ("organizationId") REFERENCES "organization"("id") ON DELETE CASCADE ON UPDATE CASCADE` + ); + await queryRunner.query( + `ALTER TABLE "employee_availability" ADD CONSTRAINT "FK_63a5d274ac6ca68482e50f8f99a" FOREIGN KEY ("employeeId") REFERENCES "employee"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ); + } + + /** + * PostgresDB Down Migration + * + * @param queryRunner + */ + public async postgresDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "employee_availability" DROP CONSTRAINT "FK_63a5d274ac6ca68482e50f8f99a"`); + await queryRunner.query(`ALTER TABLE "employee_availability" DROP CONSTRAINT "FK_9324ccb5291bd98d5fba6349f75"`); + await queryRunner.query(`ALTER TABLE "employee_availability" DROP CONSTRAINT "FK_981ccd9a51cc8706cbf8cdbdfb6"`); + await queryRunner.query(`DROP INDEX "public"."IDX_9324ccb5291bd98d5fba6349f7"`); + await queryRunner.query(`DROP INDEX "public"."IDX_981ccd9a51cc8706cbf8cdbdfb"`); + await queryRunner.query(`DROP INDEX "public"."IDX_1c5a1979ce3d47b895f11f7395"`); + await queryRunner.query(`DROP INDEX "public"."IDX_4df1dc5482972ff5344b670f78"`); + await queryRunner.query(`DROP TABLE "employee_availability"`); + } + + /** + * SqliteDB and BetterSQlite3DB Up Migration + * + * @param queryRunner + */ + public async sqliteUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "employee_availability" ("deletedAt" datetime, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "id" varchar PRIMARY KEY NOT NULL, "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "archivedAt" datetime, "tenantId" varchar, "organizationId" varchar, "startDate" datetime NOT NULL, "endDate" datetime NOT NULL, "dayOfWeek" integer NOT NULL, "availabilityStatus" integer NOT NULL, "availabilityNotes" text, "employeeId" varchar NOT NULL)` + ); + await queryRunner.query( + `CREATE INDEX "IDX_4df1dc5482972ff5344b670f78" ON "employee_availability" ("isActive") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_1c5a1979ce3d47b895f11f7395" ON "employee_availability" ("isArchived") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_981ccd9a51cc8706cbf8cdbdfb" ON "employee_availability" ("tenantId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_9324ccb5291bd98d5fba6349f7" ON "employee_availability" ("organizationId") ` + ); + await queryRunner.query(`DROP INDEX "IDX_4df1dc5482972ff5344b670f78"`); + await queryRunner.query(`DROP INDEX "IDX_1c5a1979ce3d47b895f11f7395"`); + await queryRunner.query(`DROP INDEX "IDX_981ccd9a51cc8706cbf8cdbdfb"`); + await queryRunner.query(`DROP INDEX "IDX_9324ccb5291bd98d5fba6349f7"`); + await queryRunner.query( + `CREATE TABLE "temporary_employee_availability" ("deletedAt" datetime, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "id" varchar PRIMARY KEY NOT NULL, "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "archivedAt" datetime, "tenantId" varchar, "organizationId" varchar, "startDate" datetime NOT NULL, "endDate" datetime NOT NULL, "dayOfWeek" integer NOT NULL, "availabilityStatus" integer NOT NULL, "availabilityNotes" text, "employeeId" varchar NOT NULL, CONSTRAINT "FK_981ccd9a51cc8706cbf8cdbdfb6" FOREIGN KEY ("tenantId") REFERENCES "tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_9324ccb5291bd98d5fba6349f75" FOREIGN KEY ("organizationId") REFERENCES "organization" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_63a5d274ac6ca68482e50f8f99a" FOREIGN KEY ("employeeId") REFERENCES "employee" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "temporary_employee_availability"("deletedAt", "createdAt", "updatedAt", "id", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "startDate", "endDate", "dayOfWeek", "availabilityStatus", "availabilityNotes", "employeeId") SELECT "deletedAt", "createdAt", "updatedAt", "id", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "startDate", "endDate", "dayOfWeek", "availabilityStatus", "availabilityNotes", "employeeId" FROM "employee_availability"` + ); + await queryRunner.query(`DROP TABLE "employee_availability"`); + await queryRunner.query(`ALTER TABLE "temporary_employee_availability" RENAME TO "employee_availability"`); + await queryRunner.query( + `CREATE INDEX "IDX_4df1dc5482972ff5344b670f78" ON "employee_availability" ("isActive") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_1c5a1979ce3d47b895f11f7395" ON "employee_availability" ("isArchived") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_981ccd9a51cc8706cbf8cdbdfb" ON "employee_availability" ("tenantId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_9324ccb5291bd98d5fba6349f7" ON "employee_availability" ("organizationId") ` + ); + } + + /** + * SqliteDB and BetterSQlite3DB Down Migration + * + * @param queryRunner + */ + public async sqliteDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_9324ccb5291bd98d5fba6349f7"`); + await queryRunner.query(`DROP INDEX "IDX_981ccd9a51cc8706cbf8cdbdfb"`); + await queryRunner.query(`DROP INDEX "IDX_1c5a1979ce3d47b895f11f7395"`); + await queryRunner.query(`DROP INDEX "IDX_4df1dc5482972ff5344b670f78"`); + await queryRunner.query(`ALTER TABLE "employee_availability" RENAME TO "temporary_employee_availability"`); + await queryRunner.query( + `CREATE TABLE "employee_availability" ("deletedAt" datetime, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "id" varchar PRIMARY KEY NOT NULL, "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "archivedAt" datetime, "tenantId" varchar, "organizationId" varchar, "startDate" datetime NOT NULL, "endDate" datetime NOT NULL, "dayOfWeek" integer NOT NULL, "availabilityStatus" integer NOT NULL, "availabilityNotes" text, "employeeId" varchar NOT NULL)` + ); + await queryRunner.query( + `INSERT INTO "employee_availability"("deletedAt", "createdAt", "updatedAt", "id", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "startDate", "endDate", "dayOfWeek", "availabilityStatus", "availabilityNotes", "employeeId") SELECT "deletedAt", "createdAt", "updatedAt", "id", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "startDate", "endDate", "dayOfWeek", "availabilityStatus", "availabilityNotes", "employeeId" FROM "temporary_employee_availability"` + ); + await queryRunner.query(`DROP TABLE "temporary_employee_availability"`); + await queryRunner.query( + `CREATE INDEX "IDX_9324ccb5291bd98d5fba6349f7" ON "employee_availability" ("organizationId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_981ccd9a51cc8706cbf8cdbdfb" ON "employee_availability" ("tenantId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_1c5a1979ce3d47b895f11f7395" ON "employee_availability" ("isArchived") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_4df1dc5482972ff5344b670f78" ON "employee_availability" ("isActive") ` + ); + await queryRunner.query(`DROP INDEX "IDX_9324ccb5291bd98d5fba6349f7"`); + await queryRunner.query(`DROP INDEX "IDX_981ccd9a51cc8706cbf8cdbdfb"`); + await queryRunner.query(`DROP INDEX "IDX_1c5a1979ce3d47b895f11f7395"`); + await queryRunner.query(`DROP INDEX "IDX_4df1dc5482972ff5344b670f78"`); + await queryRunner.query(`DROP TABLE "employee_availability"`); + } + + /** + * MySQL Up Migration + * + * @param queryRunner + */ + public async mysqlUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE \`employee_availability\` (\`deletedAt\` datetime(6) NULL, \`createdAt\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updatedAt\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), \`id\` varchar(36) NOT NULL, \`isActive\` tinyint NULL DEFAULT 1, \`isArchived\` tinyint NULL DEFAULT 0, \`archivedAt\` datetime NULL, \`tenantId\` varchar(255) NULL, \`organizationId\` varchar(255) NULL, \`startDate\` datetime NOT NULL, \`endDate\` datetime NOT NULL, \`dayOfWeek\` int NOT NULL, \`availabilityStatus\` int NOT NULL, \`availabilityNotes\` text NULL, \`employeeId\` varchar(255) NOT NULL, INDEX \`IDX_4df1dc5482972ff5344b670f78\` (\`isActive\`), INDEX \`IDX_1c5a1979ce3d47b895f11f7395\` (\`isArchived\`), INDEX \`IDX_981ccd9a51cc8706cbf8cdbdfb\` (\`tenantId\`), INDEX \`IDX_9324ccb5291bd98d5fba6349f7\` (\`organizationId\`), PRIMARY KEY (\`id\`)) ENGINE=InnoDB` + ); + await queryRunner.query( + `ALTER TABLE \`employee_availability\` ADD CONSTRAINT \`FK_981ccd9a51cc8706cbf8cdbdfb6\` FOREIGN KEY (\`tenantId\`) REFERENCES \`tenant\`(\`id\`) ON DELETE CASCADE ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE \`employee_availability\` ADD CONSTRAINT \`FK_9324ccb5291bd98d5fba6349f75\` FOREIGN KEY (\`organizationId\`) REFERENCES \`organization\`(\`id\`) ON DELETE CASCADE ON UPDATE CASCADE` + ); + await queryRunner.query( + `ALTER TABLE \`employee_availability\` ADD CONSTRAINT \`FK_63a5d274ac6ca68482e50f8f99a\` FOREIGN KEY (\`employeeId\`) REFERENCES \`employee\`(\`id\`) ON DELETE CASCADE ON UPDATE NO ACTION` + ); + } + + /** + * MySQL Down Migration + * + * @param queryRunner + */ + public async mysqlDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE \`employee_availability\` DROP FOREIGN KEY \`FK_63a5d274ac6ca68482e50f8f99a\`` + ); + await queryRunner.query( + `ALTER TABLE \`employee_availability\` DROP FOREIGN KEY \`FK_9324ccb5291bd98d5fba6349f75\`` + ); + await queryRunner.query( + `ALTER TABLE \`employee_availability\` DROP FOREIGN KEY \`FK_981ccd9a51cc8706cbf8cdbdfb6\`` + ); + await queryRunner.query(`DROP INDEX \`IDX_9324ccb5291bd98d5fba6349f7\` ON \`employee_availability\``); + await queryRunner.query(`DROP INDEX \`IDX_981ccd9a51cc8706cbf8cdbdfb\` ON \`employee_availability\``); + await queryRunner.query(`DROP INDEX \`IDX_1c5a1979ce3d47b895f11f7395\` ON \`employee_availability\``); + await queryRunner.query(`DROP INDEX \`IDX_4df1dc5482972ff5344b670f78\` ON \`employee_availability\``); + await queryRunner.query(`DROP TABLE \`employee_availability\``); + } +} diff --git a/packages/core/src/lib/employee-availability/commands/employee-availability.bulk.create.command.ts b/packages/core/src/lib/employee-availability/commands/employee-availability.bulk.create.command.ts new file mode 100644 index 0000000000..fbe674883d --- /dev/null +++ b/packages/core/src/lib/employee-availability/commands/employee-availability.bulk.create.command.ts @@ -0,0 +1,8 @@ +import { ICommand } from '@nestjs/cqrs'; +import { IEmployeeAvailabilityCreateInput } from '@gauzy/contracts'; + +export class EmployeeAvailabilityBulkCreateCommand implements ICommand { + static readonly type = '[Employee Availability] Bulk Create'; + + constructor(public readonly input: IEmployeeAvailabilityCreateInput[]) {} +} diff --git a/packages/core/src/lib/employee-availability/commands/employee-availability.create.command.ts b/packages/core/src/lib/employee-availability/commands/employee-availability.create.command.ts new file mode 100644 index 0000000000..86ad58300f --- /dev/null +++ b/packages/core/src/lib/employee-availability/commands/employee-availability.create.command.ts @@ -0,0 +1,8 @@ +import { ICommand } from '@nestjs/cqrs'; +import { IEmployeeAvailabilityCreateInput } from '@gauzy/contracts'; + +export class EmployeeAvailabilityCreateCommand implements ICommand { + static readonly type = '[EmployeeAvailability] Create'; + + constructor(public readonly input: IEmployeeAvailabilityCreateInput) {} +} diff --git a/packages/core/src/lib/employee-availability/commands/handlers/employee-availability.bulk.create.handler.ts b/packages/core/src/lib/employee-availability/commands/handlers/employee-availability.bulk.create.handler.ts new file mode 100644 index 0000000000..5e627b5bcf --- /dev/null +++ b/packages/core/src/lib/employee-availability/commands/handlers/employee-availability.bulk.create.handler.ts @@ -0,0 +1,36 @@ +import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; +import { IEmployeeAvailability } from '@gauzy/contracts'; +import { RequestContext } from '../../../core/context'; +import { EmployeeAvailabilityService } from '../../employee-availability.service'; +import { EmployeeAvailabilityBulkCreateCommand } from '../employee-availability.bulk.create.command'; +import { EmployeeAvailability } from '../../employee-availability.entity'; + +/** + * Handles the bulk creation of employee availability records. + */ +@CommandHandler(EmployeeAvailabilityBulkCreateCommand) +export class EmployeeAvailabilityBulkCreateHandler implements ICommandHandler { + constructor(private readonly _availabilityService: EmployeeAvailabilityService) {} + + /** + * Executes the bulk creation command for employee availability. + * + * @param command The command containing the list of availability records to create. + * @returns A promise resolving to the list of created employee availability records. + */ + public async execute(command: EmployeeAvailabilityBulkCreateCommand): Promise { + const { input } = command; + const tenantId = RequestContext.currentTenantId(); + + // Prepare employee availability records with tenantId + const employeeAvailabilities = input.map(item => + new EmployeeAvailability({ + ...item, + tenantId + }) + ); + + // Perform bulk insert using the availability service + return await this._availabilityService.bulkCreate(employeeAvailabilities); + } +} diff --git a/packages/core/src/lib/employee-availability/commands/handlers/employee-availability.create.handler.ts b/packages/core/src/lib/employee-availability/commands/handlers/employee-availability.create.handler.ts new file mode 100644 index 0000000000..e0b4412b90 --- /dev/null +++ b/packages/core/src/lib/employee-availability/commands/handlers/employee-availability.create.handler.ts @@ -0,0 +1,28 @@ +import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; +import { IEmployeeAvailability } from '@gauzy/contracts'; +import { RequestContext } from '../../../core/context'; +import { EmployeeAvailabilityService } from '../../employee-availability.service'; +import { EmployeeAvailability } from '../../employee-availability.entity'; +import { EmployeeAvailabilityCreateCommand } from '../employee-availability.create.command'; + +@CommandHandler(EmployeeAvailabilityCreateCommand) +export class EmployeeAvailabilityCreateHandler implements ICommandHandler { + constructor(private readonly _availabilityService: EmployeeAvailabilityService) {} + + /** + * Handles the creation of an employee availability record. + * + * @param {EmployeeAvailabilityCreateCommand} command - The command containing employee availability details. + * @returns {Promise} - The newly created employee availability record. + * @throws {BadRequestException} - If any validation fails (e.g., missing fields, invalid dates). + */ + public async execute(command: EmployeeAvailabilityCreateCommand): Promise { + const { input } = command; + const tenantId = RequestContext.currentTenantId() ?? input.tenantId; + + return await this._availabilityService.create(new EmployeeAvailability({ + ...input, + tenantId + })); + } +} diff --git a/packages/core/src/lib/employee-availability/commands/handlers/index.ts b/packages/core/src/lib/employee-availability/commands/handlers/index.ts new file mode 100644 index 0000000000..af7f9627ba --- /dev/null +++ b/packages/core/src/lib/employee-availability/commands/handlers/index.ts @@ -0,0 +1,7 @@ +import { EmployeeAvailabilityBulkCreateHandler } from './employee-availability.bulk.create.handler'; +import { EmployeeAvailabilityCreateHandler } from './employee-availability.create.handler'; + +/** + * Exports all command handlers for EmployeeAvailability.` + */ +export const CommandHandlers = [EmployeeAvailabilityBulkCreateHandler, EmployeeAvailabilityCreateHandler]; diff --git a/packages/core/src/lib/employee-availability/commands/index.ts b/packages/core/src/lib/employee-availability/commands/index.ts new file mode 100644 index 0000000000..33eae03e06 --- /dev/null +++ b/packages/core/src/lib/employee-availability/commands/index.ts @@ -0,0 +1,2 @@ +export * from './employee-availability.bulk.create.command'; +export * from './employee-availability.create.command'; diff --git a/packages/core/src/lib/employee-availability/dto/create-employee-availability.dto.ts b/packages/core/src/lib/employee-availability/dto/create-employee-availability.dto.ts new file mode 100644 index 0000000000..5f92c995ee --- /dev/null +++ b/packages/core/src/lib/employee-availability/dto/create-employee-availability.dto.ts @@ -0,0 +1,18 @@ +import { IntersectionType, PickType } from '@nestjs/swagger'; +import { IEmployeeAvailabilityCreateInput } from '@gauzy/contracts'; +import { TenantOrganizationBaseDTO } from '../../core/dto'; +import { EmployeeAvailability } from '../employee-availability.entity'; + +export class CreateEmployeeAvailabilityDTO + extends IntersectionType( + TenantOrganizationBaseDTO, + PickType(EmployeeAvailability, [ + 'dayOfWeek', + 'startDate', + 'endDate', + 'availabilityNotes', + 'availabilityStatus', + 'employeeId' + ]) + ) + implements IEmployeeAvailabilityCreateInput {} diff --git a/packages/core/src/lib/employee-availability/dto/update-employee-availability.dto.ts b/packages/core/src/lib/employee-availability/dto/update-employee-availability.dto.ts new file mode 100644 index 0000000000..8c50bd31e3 --- /dev/null +++ b/packages/core/src/lib/employee-availability/dto/update-employee-availability.dto.ts @@ -0,0 +1,8 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { IEmployeeAvailabilityUpdateInput } from '@gauzy/contracts'; +import { CreateEmployeeAvailabilityDTO } from './create-employee-availability.dto'; +import { IntersectionType } from '@nestjs/swagger'; + +export class UpdateEmployeeAvailabilityDTO + extends IntersectionType(PartialType(CreateEmployeeAvailabilityDTO)) + implements IEmployeeAvailabilityUpdateInput {} diff --git a/packages/core/src/lib/employee-availability/employee-availability.controller.ts b/packages/core/src/lib/employee-availability/employee-availability.controller.ts new file mode 100644 index 0000000000..7691b0b7ee --- /dev/null +++ b/packages/core/src/lib/employee-availability/employee-availability.controller.ts @@ -0,0 +1,124 @@ +import { UpdateResult } from 'typeorm'; +import { Body, Controller, Get, HttpCode, HttpStatus, Param, Post, Put, Query, UseGuards } from '@nestjs/common'; +import { CommandBus } from '@nestjs/cqrs'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { ID, IEmployeeAvailability, IPagination, PermissionsEnum } from '@gauzy/contracts'; +import { CrudController, PaginationParams } from '../core/crud'; +import { Permissions } from '../shared/decorators'; +import { PermissionGuard, TenantPermissionGuard } from '../shared/guards'; +import { UseValidationPipe, UUIDValidationPipe } from '../shared/pipes'; +import { EmployeeAvailabilityService } from './employee-availability.service'; +import { EmployeeAvailability } from './employee-availability.entity'; +import { EmployeeAvailabilityBulkCreateCommand, EmployeeAvailabilityCreateCommand } from './commands'; +import { CreateEmployeeAvailabilityDTO } from './dto/create-employee-availability.dto'; +import { UpdateEmployeeAvailabilityDTO } from './dto/update-employee-availability.dto'; + +@ApiTags('EmployeeAvailability') +@UseGuards(TenantPermissionGuard, PermissionGuard) +@Permissions(PermissionsEnum.EMPLOYEE_AVAILABILITY_UPDATE, PermissionsEnum.EMPLOYEE_AVAILABILITY_DELETE) +@Controller('/employee-availability') +export class EmployeeAvailabilityController extends CrudController { + constructor( + private readonly availabilityService: EmployeeAvailabilityService, + private readonly commandBus: CommandBus + ) { + super(availabilityService); + } + + /** + * Create multiple employee availability records in bulk. + * + * @param entities List of availability records to create + * @returns The created availability records + */ + @ApiOperation({ summary: 'Create multiple availability records' }) + @ApiResponse({ + status: HttpStatus.CREATED, + description: 'The records have been successfully created.' + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: 'Invalid input. The response body may contain clues as to what went wrong.' + }) + @HttpCode(HttpStatus.CREATED) + @Permissions(PermissionsEnum.EMPLOYEE_AVAILABILITY_CREATE) + @Post('/bulk') + @UseValidationPipe() + async createBulk(@Body() entities: CreateEmployeeAvailabilityDTO[]): Promise { + return await this.commandBus.execute(new EmployeeAvailabilityBulkCreateCommand(entities)); + } + + /** + * Retrieve all employee availability records. + * + * @param data Query parameters, including relations and filters + * @returns A paginated list of availability records + */ + @ApiOperation({ summary: 'Retrieve all availability records' }) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Successfully retrieved availability records.' + }) + @ApiResponse({ + status: HttpStatus.NOT_FOUND, + description: 'No availability records found.' + }) + @Permissions(PermissionsEnum.EMPLOYEE_AVAILABILITY_READ) + @Get('/') + @UseValidationPipe() + async findAll( + @Query() filter: PaginationParams + ): Promise> { + return this.availabilityService.findAll(filter); + } + + /** + * Create a new employee availability record. + * + * @param entity The data for the new availability record + * @returns The created availability record + */ + @ApiOperation({ summary: 'Create a new availability record' }) + @ApiResponse({ + status: HttpStatus.CREATED, + description: 'The record has been successfully created.' + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: 'Invalid input. The response body may contain clues as to what went wrong.' + }) + @HttpCode(HttpStatus.CREATED) + @Permissions(PermissionsEnum.EMPLOYEE_AVAILABILITY_CREATE) + @Post('/') + @UseValidationPipe() + async create(@Body() entity: CreateEmployeeAvailabilityDTO): Promise { + return this.commandBus.execute(new EmployeeAvailabilityCreateCommand(entity)); + } + + /** + * Update an existing employee availability record by its ID. + * + * @param id The ID of the availability record + * @param entity The updated data for the record + * @returns The updated availability record + */ + @ApiOperation({ summary: 'Update an existing availability record' }) + @ApiResponse({ + status: HttpStatus.ACCEPTED, + description: 'The record has been successfully updated.' + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: 'Invalid input. The response body may contain clues as to what went wrong.' + }) + @HttpCode(HttpStatus.ACCEPTED) + @Permissions(PermissionsEnum.EMPLOYEE_AVAILABILITY_UPDATE) + @Put('/:id') + @UseValidationPipe() + async update( + @Param('id', UUIDValidationPipe) id: ID, + @Body() entity: UpdateEmployeeAvailabilityDTO + ): Promise { + return this.availabilityService.update(id, { ...entity }); + } +} diff --git a/packages/core/src/lib/employee-availability/employee-availability.entity.ts b/packages/core/src/lib/employee-availability/employee-availability.entity.ts new file mode 100644 index 0000000000..37ed2ffb31 --- /dev/null +++ b/packages/core/src/lib/employee-availability/employee-availability.entity.ts @@ -0,0 +1,96 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsNotEmpty, IsString, IsDate, IsInt, IsOptional, IsEnum, IsUUID, Min, Max } from 'class-validator'; +import { JoinColumn, RelationId } from 'typeorm'; +import { AvailabilityStatusEnum, ID, IEmployee, IEmployeeAvailability } from '@gauzy/contracts'; +import { Employee, TenantOrganizationBaseEntity } from '../core/entities/internal'; +import { MultiORMColumn, MultiORMEntity, MultiORMManyToOne } from './../core/decorators/entity'; +import { AvailabilityStatusTransformer } from './pipes/employee-availability-status.pipe'; +import { IsBeforeDate } from '../shared/validators'; + +@MultiORMEntity('employee_availability') +export class EmployeeAvailability extends TenantOrganizationBaseEntity implements IEmployeeAvailability { + /** + * Represents the start date of an employee's availability period. + * This marks when the availability status takes effect. + */ + @ApiProperty({ type: () => Date }) + @IsDate() + @IsNotEmpty() + @IsBeforeDate(EmployeeAvailability, (it) => it.endDate, { + message: 'Start date must be before to the end date' + }) + @MultiORMColumn() + startDate: Date; + + /** + * Represents the end date of an employee's availability period. + * This marks when the availability status expires. + */ + @ApiProperty({ type: () => Date }) + @IsDate() + @IsNotEmpty() + @MultiORMColumn() + endDate: Date; + + /** + * The day of the week corresponding to the availability. + * Values range from `0` (Sunday) to `6` (Saturday). + */ + @ApiProperty({ type: () => Number, description: 'Day of the week (0 = Sunday, 6 = Saturday)' }) + @IsNotEmpty() + @IsInt() + @Min(0, { message: 'Day of week must be between 0 and 6' }) + @Max(6, { message: 'Day of week must be between 0 and 6' }) + @MultiORMColumn() + dayOfWeek: number; + + /** + * The availability status of the employee. + * Uses `AvailabilityStatusEnum` to define the available states. + */ + @ApiProperty({ enum: AvailabilityStatusEnum }) + @IsEnum(AvailabilityStatusEnum) + @MultiORMColumn({ type: 'int', transformer: new AvailabilityStatusTransformer() }) + availabilityStatus: AvailabilityStatusEnum; + + /** + * Optional notes providing additional details about the availability. + * Example: "Available until 2 PM" or "Remote work only." + */ + @ApiPropertyOptional({ + type: () => String, + description: 'Optional notes (e.g., "Available until 2 PM")' + }) + @IsString() + @IsOptional() + @MultiORMColumn({ type: 'text', nullable: true }) + availabilityNotes?: string; + + /* + |-------------------------------------------------------------------------- + | @ManyToOne + |-------------------------------------------------------------------------- + */ + + /** + * Reference to the Employee associated with this availability record. + * Establishes a many-to-one relationship, meaning multiple availability records + * can be linked to a single employee. + */ + @MultiORMManyToOne(() => Employee, (it) => it.availabilities, { + /** Database cascade action on delete. */ + onDelete: 'CASCADE' + }) + @JoinColumn() + employee: IEmployee; + + /** + * The UUID representing the linked `Employee` record. + * This serves as the foreign key that connects the availability record to a specific employee. + */ + @ApiProperty({ type: () => String }) + @IsUUID() + @RelationId((it: EmployeeAvailability) => it.employee) + @MultiORMColumn({ relationId: true }) + employeeId: ID; +} diff --git a/packages/core/src/lib/employee-availability/employee-availability.module.ts b/packages/core/src/lib/employee-availability/employee-availability.module.ts new file mode 100644 index 0000000000..ef1ec3c59c --- /dev/null +++ b/packages/core/src/lib/employee-availability/employee-availability.module.ts @@ -0,0 +1,26 @@ +import { Module } from '@nestjs/common'; +import { CqrsModule } from '@nestjs/cqrs'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { MikroOrmModule } from '@mikro-orm/nestjs'; +import { CommandHandlers } from './commands/handlers'; +import { EmployeeAvailabilityService } from './employee-availability.service'; +import { EmployeeAvailabilityController } from './employee-availability.controller'; +import { EmployeeAvailability } from './employee-availability.entity'; +import { TypeOrmEmployeeAvailabilityRepository } from './repository/type-orm-employee-availability.repository'; +import { MikroOrmEmployeeAvailabilityRepository } from './repository/micro-orm-employee-availability.repository'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([EmployeeAvailability]), + MikroOrmModule.forFeature([EmployeeAvailability]), + CqrsModule + ], + providers: [ + EmployeeAvailabilityService, + TypeOrmEmployeeAvailabilityRepository, + MikroOrmEmployeeAvailabilityRepository, + ...CommandHandlers + ], + controllers: [EmployeeAvailabilityController] +}) +export class EmployeeAvailabilityModule {} diff --git a/packages/core/src/lib/employee-availability/employee-availability.service.ts b/packages/core/src/lib/employee-availability/employee-availability.service.ts new file mode 100644 index 0000000000..a2b0807124 --- /dev/null +++ b/packages/core/src/lib/employee-availability/employee-availability.service.ts @@ -0,0 +1,80 @@ +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { DeepPartial } from 'typeorm'; +import { IEmployeeAvailability } from '@gauzy/contracts'; +import { RequestContext } from '../core/context/request-context'; +import { TenantAwareCrudService } from './../core/crud/tenant-aware-crud.service'; +import { MultiORMEnum } from '../core/utils'; +import { TypeOrmEmployeeAvailabilityRepository } from './repository/type-orm-employee-availability.repository'; +import { MikroOrmEmployeeAvailabilityRepository } from './repository/micro-orm-employee-availability.repository'; +import { EmployeeAvailability } from './employee-availability.entity'; + +@Injectable() +export class EmployeeAvailabilityService extends TenantAwareCrudService { + constructor( + readonly typeOrmEmployeeAvailabilityRepository: TypeOrmEmployeeAvailabilityRepository, + readonly mikroOrmEmployeeAvailabilityRepository: MikroOrmEmployeeAvailabilityRepository + ) { + super(typeOrmEmployeeAvailabilityRepository, mikroOrmEmployeeAvailabilityRepository); + } + + /** + * Bulk creates new employee availability records while ensuring each entity has `tenantId`. + * Supports both TypeORM & MikroORM. + * + * @param entities List of employee availability objects to create. + * @returns Promise List of created employee availability records. + */ + public async bulkCreate(entities: Partial[]): Promise { + const tenantId = RequestContext.currentTenantId(); + + // Prepare entities ensuring `tenantId` is assigned + const items = entities.map(entity => + new EmployeeAvailability({ + ...entity, + tenantId + }) + ); + + try { + switch (this.ormType) { + case MultiORMEnum.MikroORM: + try { + // Convert input entities to MikroORM format + const mikroEntities = items.map((item: EmployeeAvailability) => + this.mikroOrmRepository.create(item, { + managed: true + }) + ); + + // Bulk insert using MikroORM + await this.mikroOrmRepository.persistAndFlush(mikroEntities); + return mikroEntities; + } catch (error) { + throw new HttpException( + `Error during MikroORM bulk create transaction : ${error.message}`, + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + + case MultiORMEnum.TypeORM: + try { + // Bulk insert using TypeORM's `save` method for optimized inserts + return await this.typeOrmRepository.save(items as DeepPartial[]); + } catch (error) { + throw new HttpException( + `Error during TypeORM bulk create transaction : ${error.message}`, + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + + default: + throw new Error(`Not implemented for ${this.ormType}`); + } + } catch (error) { + throw new HttpException( + `Error in bulkCreate method of employee availability: ${error.message}`, + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } +} diff --git a/packages/core/src/lib/employee-availability/index.ts b/packages/core/src/lib/employee-availability/index.ts new file mode 100644 index 0000000000..c4cf093110 --- /dev/null +++ b/packages/core/src/lib/employee-availability/index.ts @@ -0,0 +1,3 @@ +export { EmployeeAvailability } from './employee-availability.entity'; +export { EmployeeAvailabilityModule } from './employee-availability.module'; +export { EmployeeAvailabilityService } from './employee-availability.service'; diff --git a/packages/core/src/lib/employee-availability/pipes/employee-availability-status.pipe.ts b/packages/core/src/lib/employee-availability/pipes/employee-availability-status.pipe.ts new file mode 100644 index 0000000000..9e0c16ee41 --- /dev/null +++ b/packages/core/src/lib/employee-availability/pipes/employee-availability-status.pipe.ts @@ -0,0 +1,48 @@ +import { ValueTransformer } from 'typeorm'; +import { AvailabilityStatusEnum, AvailabilityStatusValue } from '@gauzy/contracts'; + +/** + * Transformer to handle the conversion between the enum values + * (used in the application) and integer values (stored in the database). + */ +export class AvailabilityStatusTransformer implements ValueTransformer { + /** + * Converts the enum value to its corresponding integer representation + * when saving to the database. + * + * @param value - The `AvailabilityStatusEnum` value. + * @returns The corresponding integer value for storage in the database. + */ + to(value: AvailabilityStatusEnum): number { + switch (value) { + case AvailabilityStatusEnum.Available: + return AvailabilityStatusValue.Available; + case AvailabilityStatusEnum.Partial: + return AvailabilityStatusValue.Partial; + case AvailabilityStatusEnum.Unavailable: + return AvailabilityStatusValue.Unavailable; + default: + throw new Error(`Invalid availability status: ${value}`); + } + } + + /** + * Converts the integer value from the database back to the corresponding + * enum value when reading. + * + * @param value - The integer value stored in the database. + * @returns The corresponding `AvailabilityStatusEnum` value. + */ + from(value: number): AvailabilityStatusEnum { + switch (value) { + case AvailabilityStatusValue.Available: + return AvailabilityStatusEnum.Available; + case AvailabilityStatusValue.Partial: + return AvailabilityStatusEnum.Partial; + case AvailabilityStatusValue.Unavailable: + return AvailabilityStatusEnum.Unavailable; + default: + throw new Error(`Invalid status value: ${value}`); + } + } +} diff --git a/packages/core/src/lib/employee-availability/repository/micro-orm-employee-availability.repository.ts b/packages/core/src/lib/employee-availability/repository/micro-orm-employee-availability.repository.ts new file mode 100644 index 0000000000..aa4de6314e --- /dev/null +++ b/packages/core/src/lib/employee-availability/repository/micro-orm-employee-availability.repository.ts @@ -0,0 +1,4 @@ +import { MikroOrmBaseEntityRepository } from '../../core/repository/mikro-orm-base-entity.repository'; +import { EmployeeAvailability } from '../employee-availability.entity'; + +export class MikroOrmEmployeeAvailabilityRepository extends MikroOrmBaseEntityRepository {} diff --git a/packages/core/src/lib/employee-availability/repository/type-orm-employee-availability.repository.ts b/packages/core/src/lib/employee-availability/repository/type-orm-employee-availability.repository.ts new file mode 100644 index 0000000000..1b3bec31f4 --- /dev/null +++ b/packages/core/src/lib/employee-availability/repository/type-orm-employee-availability.repository.ts @@ -0,0 +1,11 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { EmployeeAvailability } from '../employee-availability.entity'; + +@Injectable() +export class TypeOrmEmployeeAvailabilityRepository extends Repository { + constructor(@InjectRepository(EmployeeAvailability) readonly repository: Repository) { + super(repository.target, repository.manager, repository.queryRunner); + } +} diff --git a/packages/core/src/lib/employee/employee.entity.ts b/packages/core/src/lib/employee/employee.entity.ts index 4a0dd0b31f..414c8a39f9 100644 --- a/packages/core/src/lib/employee/employee.entity.ts +++ b/packages/core/src/lib/employee/employee.entity.ts @@ -37,7 +37,8 @@ import { ID, IFavorite, IComment, - IOrganizationSprint + IOrganizationSprint, + IEmployeeAvailability } from '@gauzy/contracts'; import { ColumnIndex, @@ -55,6 +56,7 @@ import { Comment, Contact, DailyPlan, + EmployeeAvailability, EmployeeAward, EmployeePhone, EmployeeSetting, @@ -541,6 +543,15 @@ export class Employee extends TenantOrganizationBaseEntity implements IEmployee, }) modules?: IOrganizationProjectModule[]; + /** + * One-to-many relationship with EmployeeAvailability. + * An employee can have multiple availability records. + */ + @MultiORMOneToMany(() => EmployeeAvailability, (availability) => availability.employee, { + cascade: true + }) + availabilities?: IEmployeeAvailability[]; + /** * Estimations */ diff --git a/packages/core/src/lib/role-permission/default-role-permissions.ts b/packages/core/src/lib/role-permission/default-role-permissions.ts index 664b29faca..3d905e71b1 100644 --- a/packages/core/src/lib/role-permission/default-role-permissions.ts +++ b/packages/core/src/lib/role-permission/default-role-permissions.ts @@ -196,7 +196,12 @@ export const DEFAULT_ROLE_PERMISSIONS = [ /** Tenant API Key */ PermissionsEnum.TENANT_API_KEY_CREATE, PermissionsEnum.TENANT_API_KEY_VIEW, - PermissionsEnum.TENANT_API_KEY_DELETE + PermissionsEnum.TENANT_API_KEY_DELETE, + /** Employee Availability */ + PermissionsEnum.EMPLOYEE_AVAILABILITY_CREATE, + PermissionsEnum.EMPLOYEE_AVAILABILITY_READ, + PermissionsEnum.EMPLOYEE_AVAILABILITY_UPDATE, + PermissionsEnum.EMPLOYEE_AVAILABILITY_DELETE ] }, { @@ -399,7 +404,12 @@ export const DEFAULT_ROLE_PERMISSIONS = [ /** Tenant API Key */ PermissionsEnum.TENANT_API_KEY_CREATE, PermissionsEnum.TENANT_API_KEY_VIEW, - PermissionsEnum.TENANT_API_KEY_DELETE + PermissionsEnum.TENANT_API_KEY_DELETE, + /** Employee Availability */ + PermissionsEnum.EMPLOYEE_AVAILABILITY_CREATE, + PermissionsEnum.EMPLOYEE_AVAILABILITY_READ, + PermissionsEnum.EMPLOYEE_AVAILABILITY_UPDATE, + PermissionsEnum.EMPLOYEE_AVAILABILITY_DELETE ] }, { @@ -534,7 +544,12 @@ export const DEFAULT_ROLE_PERMISSIONS = [ PermissionsEnum.ALLOW_MODIFY_TIME, PermissionsEnum.ALLOW_MANUAL_TIME, PermissionsEnum.DELETE_SCREENSHOTS, - PermissionsEnum.ORG_MEMBER_LAST_LOG_VIEW + PermissionsEnum.ORG_MEMBER_LAST_LOG_VIEW, + /** Employee Availability */ + PermissionsEnum.EMPLOYEE_AVAILABILITY_CREATE, + PermissionsEnum.EMPLOYEE_AVAILABILITY_READ, + PermissionsEnum.EMPLOYEE_AVAILABILITY_UPDATE, + PermissionsEnum.EMPLOYEE_AVAILABILITY_DELETE ] }, { diff --git a/packages/ui-core/i18n/assets/i18n/bg.json b/packages/ui-core/i18n/assets/i18n/bg.json index 65b43e551c..780375b268 100644 --- a/packages/ui-core/i18n/assets/i18n/bg.json +++ b/packages/ui-core/i18n/assets/i18n/bg.json @@ -2200,7 +2200,11 @@ "ORG_MEMBER_LAST_LOG_VIEW": "Преглед на последния запис", "TENANT_API_KEY_CREATE": "Create API Key", "TENANT_API_KEY_VIEW": "View API Key", - "TENANT_API_KEY_DELETE": "Delete API Key" + "TENANT_API_KEY_DELETE": "Delete API Key", + "EMPLOYEE_AVAILABILITY_CREATE": "Създаване на наличност на служител", + "EMPLOYEE_AVAILABILITY_READ": "Преглед на наличността на служител", + "EMPLOYEE_AVAILABILITY_UPDATE": "Актуализиране на наличността на служител", + "EMPLOYEE_AVAILABILITY_DELETE": "Изтриване на наличността на служител" }, "BILLING": "Billing", "BUDGET": "Budget", diff --git a/packages/ui-core/i18n/assets/i18n/de.json b/packages/ui-core/i18n/assets/i18n/de.json index 21258805ae..aba43f4d05 100644 --- a/packages/ui-core/i18n/assets/i18n/de.json +++ b/packages/ui-core/i18n/assets/i18n/de.json @@ -2137,7 +2137,11 @@ "ORG_MEMBER_LAST_LOG_VIEW": "Letztes Protokoll anzeigen", "TENANT_API_KEY_CREATE": "API-Schlüssel erstellen", "TENANT_API_KEY_VIEW": "API-Schlüssel anzeigen", - "TENANT_API_KEY_DELETE": "API-Schlüssel löschen" + "TENANT_API_KEY_DELETE": "API-Schlüssel löschen", + "EMPLOYEE_AVAILABILITY_CREATE": "Mitarbeiterverfügbarkeit erstellen", + "EMPLOYEE_AVAILABILITY_READ": "Mitarbeiterverfügbarkeit anzeigen", + "EMPLOYEE_AVAILABILITY_UPDATE": "Mitarbeiterverfügbarkeit aktualisieren", + "EMPLOYEE_AVAILABILITY_DELETE": "Mitarbeiterverfügbarkeit löschen" }, "BILLING": "Rechnungsstellung", "BUDGET": "Budget", diff --git a/packages/ui-core/i18n/assets/i18n/en.json b/packages/ui-core/i18n/assets/i18n/en.json index 34cd68da6d..aef190eca5 100644 --- a/packages/ui-core/i18n/assets/i18n/en.json +++ b/packages/ui-core/i18n/assets/i18n/en.json @@ -2263,7 +2263,11 @@ "ORG_MEMBER_LAST_LOG_VIEW": "View Last Log", "TENANT_API_KEY_CREATE": "Create API Key", "TENANT_API_KEY_VIEW": "View API Key", - "TENANT_API_KEY_DELETE": "Delete API Key" + "TENANT_API_KEY_DELETE": "Delete API Key", + "EMPLOYEE_AVAILABILITY_CREATE": "Create Employee Availability", + "EMPLOYEE_AVAILABILITY_READ": "View Employee Availability", + "EMPLOYEE_AVAILABILITY_UPDATE": "Update Employee Availability", + "EMPLOYEE_AVAILABILITY_DELETE": "Delete Employee Availability" }, "BILLING": "Billing", "BUDGET": "Budget", diff --git a/packages/ui-core/i18n/assets/i18n/es.json b/packages/ui-core/i18n/assets/i18n/es.json index 033297203d..c9d21aaa7b 100644 --- a/packages/ui-core/i18n/assets/i18n/es.json +++ b/packages/ui-core/i18n/assets/i18n/es.json @@ -2143,7 +2143,11 @@ "ORG_MEMBER_LAST_LOG_VIEW": "Ver último registro", "TENANT_API_KEY_CREATE": "Crear clave API", "TENANT_API_KEY_VIEW": "Ver clave API", - "TENANT_API_KEY_DELETE": "Eliminar clave API" + "TENANT_API_KEY_DELETE": "Eliminar clave API", + "EMPLOYEE_AVAILABILITY_CREATE": "Crear disponibilidad del empleado", + "EMPLOYEE_AVAILABILITY_READ": "Ver disponibilidad del empleado", + "EMPLOYEE_AVAILABILITY_UPDATE": "Actualizar disponibilidad del empleado", + "EMPLOYEE_AVAILABILITY_DELETE": "Eliminar disponibilidad del empleado" }, "BILLING": "Facturación", "BUDGET": "Presupuesto", diff --git a/packages/ui-core/i18n/assets/i18n/fr.json b/packages/ui-core/i18n/assets/i18n/fr.json index 0589adff3b..91e42ed7a5 100644 --- a/packages/ui-core/i18n/assets/i18n/fr.json +++ b/packages/ui-core/i18n/assets/i18n/fr.json @@ -2142,7 +2142,11 @@ "ORG_MEMBER_LAST_LOG_VIEW": "Voir le dernier journal", "TENANT_API_KEY_CREATE": "Créer une clé API", "TENANT_API_KEY_VIEW": "Afficher la clé API", - "TENANT_API_KEY_DELETE": "Supprimer la clé API" + "TENANT_API_KEY_DELETE": "Supprimer la clé API", + "EMPLOYEE_AVAILABILITY_CREATE": "Créer une disponibilité d'employé", + "EMPLOYEE_AVAILABILITY_READ": "Voir la disponibilité de l'employé", + "EMPLOYEE_AVAILABILITY_UPDATE": "Mettre à jour la disponibilité de l'employé", + "EMPLOYEE_AVAILABILITY_DELETE": "Supprimer la disponibilité de l'employé" }, "BILLING": "Facturation", "BUDGET": "Budget", diff --git a/packages/ui-core/i18n/assets/i18n/he.json b/packages/ui-core/i18n/assets/i18n/he.json index 24d63d4b49..7c919b2721 100644 --- a/packages/ui-core/i18n/assets/i18n/he.json +++ b/packages/ui-core/i18n/assets/i18n/he.json @@ -2166,7 +2166,11 @@ "ORG_MEMBER_LAST_LOG_VIEW": "צפה ביומן האחרון", "TENANT_API_KEY_CREATE": "יצירת מפתח API", "TENANT_API_KEY_VIEW": "הצגת מפתח API", - "TENANT_API_KEY_DELETE": "מחיקת מפתח API" + "TENANT_API_KEY_DELETE": "מחיקת מפתח API", + "EMPLOYEE_AVAILABILITY_CREATE": "יצירת זמינות לעובד", + "EMPLOYEE_AVAILABILITY_READ": "צפה בזמינות העובד", + "EMPLOYEE_AVAILABILITY_UPDATE": "עדכן זמינות לעובד", + "EMPLOYEE_AVAILABILITY_DELETE": "מחק זמינות לעובד" }, "BILLING": "Billing", "BUDGET": "Budget", diff --git a/packages/ui-core/i18n/assets/i18n/it.json b/packages/ui-core/i18n/assets/i18n/it.json index 7f5e49bf23..fb0085b43b 100644 --- a/packages/ui-core/i18n/assets/i18n/it.json +++ b/packages/ui-core/i18n/assets/i18n/it.json @@ -2141,7 +2141,11 @@ "ORG_MEMBER_LAST_LOG_VIEW": "Visualizza l'ultimo registro", "TENANT_API_KEY_CREATE": "Crea chiave API", "TENANT_API_KEY_VIEW": "Visualizza chiave API", - "TENANT_API_KEY_DELETE": "Elimina chiave API" + "TENANT_API_KEY_DELETE": "Elimina chiave API", + "EMPLOYEE_AVAILABILITY_CREATE": "Creare disponibilità del dipendente", + "EMPLOYEE_AVAILABILITY_READ": "Visualizza disponibilità del dipendente", + "EMPLOYEE_AVAILABILITY_UPDATE": "Aggiorna disponibilità del dipendente", + "EMPLOYEE_AVAILABILITY_DELETE": "Elimina disponibilità del dipendente" }, "BILLING": "Fatturazione", "BUDGET": "Bilancio", diff --git a/packages/ui-core/i18n/assets/i18n/nl.json b/packages/ui-core/i18n/assets/i18n/nl.json index a0425495f0..9b66a0c3f7 100644 --- a/packages/ui-core/i18n/assets/i18n/nl.json +++ b/packages/ui-core/i18n/assets/i18n/nl.json @@ -2141,7 +2141,11 @@ "ORG_MEMBER_LAST_LOG_VIEW": "Bekijk laatste logboek", "TENANT_API_KEY_CREATE": "API-sleutel maken", "TENANT_API_KEY_VIEW": "API-sleutel bekijken", - "TENANT_API_KEY_DELETE": "API-sleutel verwijderen" + "TENANT_API_KEY_DELETE": "API-sleutel verwijderen", + "EMPLOYEE_AVAILABILITY_CREATE": "Werknemersbeschikbaarheid aanmaken", + "EMPLOYEE_AVAILABILITY_READ": "Bekijk werknemersbeschikbaarheid", + "EMPLOYEE_AVAILABILITY_UPDATE": "Werknemersbeschikbaarheid bijwerken", + "EMPLOYEE_AVAILABILITY_DELETE": "Werknemersbeschikbaarheid verwijderen" }, "BILLING": "Facturatie", "BUDGET": "Budget", diff --git a/packages/ui-core/i18n/assets/i18n/pl.json b/packages/ui-core/i18n/assets/i18n/pl.json index 8478594b7d..c70af46bbe 100644 --- a/packages/ui-core/i18n/assets/i18n/pl.json +++ b/packages/ui-core/i18n/assets/i18n/pl.json @@ -2141,7 +2141,11 @@ "ORG_MEMBER_LAST_LOG_VIEW": "Zobacz ostatni dziennik", "TENANT_API_KEY_CREATE": "Utwórz klucz API", "TENANT_API_KEY_VIEW": "Zobacz klucz API", - "TENANT_API_KEY_DELETE": "Usuń klucz API" + "TENANT_API_KEY_DELETE": "Usuń klucz API", + "EMPLOYEE_AVAILABILITY_CREATE": "Utwórz dostępność pracownika", + "EMPLOYEE_AVAILABILITY_READ": "Zobacz dostępność pracownika", + "EMPLOYEE_AVAILABILITY_UPDATE": "Zaktualizuj dostępność pracownika", + "EMPLOYEE_AVAILABILITY_DELETE": "Usuń dostępność pracownika" }, "BILLING": "Rozliczenie / Faktury", "BUDGET": "Budżet", diff --git a/packages/ui-core/i18n/assets/i18n/pt.json b/packages/ui-core/i18n/assets/i18n/pt.json index 083a78a12b..e6e9c9c4a3 100644 --- a/packages/ui-core/i18n/assets/i18n/pt.json +++ b/packages/ui-core/i18n/assets/i18n/pt.json @@ -2141,7 +2141,11 @@ "ORG_MEMBER_LAST_LOG_VIEW": "Ver último registro", "TENANT_API_KEY_CREATE": "Criar chave API", "TENANT_API_KEY_VIEW": "Visualizar chave API", - "TENANT_API_KEY_DELETE": "Excluir chave API" + "TENANT_API_KEY_DELETE": "Excluir chave API", + "EMPLOYEE_AVAILABILITY_CREATE": "Criar disponibilidade do funcionário", + "EMPLOYEE_AVAILABILITY_READ": "Visualizar disponibilidade do funcionário", + "EMPLOYEE_AVAILABILITY_UPDATE": "Atualizar disponibilidade do funcionário", + "EMPLOYEE_AVAILABILITY_DELETE": "Excluir disponibilidade do funcionário" }, "BILLING": "Faturamento", "BUDGET": "Orçamento", diff --git a/packages/ui-core/i18n/assets/i18n/ru.json b/packages/ui-core/i18n/assets/i18n/ru.json index 6f3de6be22..b0c44e0ed9 100644 --- a/packages/ui-core/i18n/assets/i18n/ru.json +++ b/packages/ui-core/i18n/assets/i18n/ru.json @@ -2172,7 +2172,11 @@ "ORG_MEMBER_LAST_LOG_VIEW": "Посмотреть последний журнал", "TENANT_API_KEY_CREATE": "Создать API-ключ", "TENANT_API_KEY_VIEW": "Просмотр API-ключа", - "TENANT_API_KEY_DELETE": "Удалить API-ключ" + "TENANT_API_KEY_DELETE": "Удалить API-ключ", + "EMPLOYEE_AVAILABILITY_CREATE": "Создать доступность сотрудника", + "EMPLOYEE_AVAILABILITY_READ": "Просмотреть доступность сотрудника", + "EMPLOYEE_AVAILABILITY_UPDATE": "Обновить доступность сотрудника", + "EMPLOYEE_AVAILABILITY_DELETE": "Удалить доступность сотрудника" }, "BILLING": "Биллинг", "BUDGET": "Бюджет", diff --git a/packages/ui-core/i18n/assets/i18n/zh.json b/packages/ui-core/i18n/assets/i18n/zh.json index 090824e900..899cfd6f47 100644 --- a/packages/ui-core/i18n/assets/i18n/zh.json +++ b/packages/ui-core/i18n/assets/i18n/zh.json @@ -2141,7 +2141,11 @@ "ORG_MEMBER_LAST_LOG_VIEW": "查看最后日志", "TENANT_API_KEY_CREATE": "创建 API 密钥", "TENANT_API_KEY_VIEW": "查看 API 密钥", - "TENANT_API_KEY_DELETE": "删除 API 密钥" + "TENANT_API_KEY_DELETE": "删除 API 密钥", + "EMPLOYEE_AVAILABILITY_CREATE": "创建员工可用性", + "EMPLOYEE_AVAILABILITY_READ": "查看员工可用性", + "EMPLOYEE_AVAILABILITY_UPDATE": "更新员工可用性", + "EMPLOYEE_AVAILABILITY_DELETE": "删除员工可用性" }, "BILLING": "计费", "BUDGET": "预算",