Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] Employee weekly schedule planning #8741

Open
wants to merge 19 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
e00c602
Implementation of employee weekly schedule planning
samuelmbabhazi Jan 22, 2025
7c4e4f5
Feedback integration
samuelmbabhazi Jan 23, 2025
ff2bacb
Merge branch 'develop' into feat/#5293-weekly-schedule-planning
samuelmbabhazi Jan 23, 2025
c369a13
Feedback integration
samuelmbabhazi Jan 23, 2025
f7eb738
Feedback integration
samuelmbabhazi Jan 23, 2025
96052b1
Feedback integration
samuelmbabhazi Jan 23, 2025
6c3ba54
Fix deepscan
samuelmbabhazi Jan 23, 2025
49d09b7
Merge branch 'develop' into feat/#5293-weekly-schedule-planning
rahul-rocket Jan 24, 2025
2b926a6
refactor: updated employee availability feature
rahul-rocket Jan 24, 2025
55092e8
fix: entity metadata for Employee#availabilities was not found
rahul-rocket Jan 24, 2025
a6a6e4c
Integration of requested changes
samuelmbabhazi Jan 24, 2025
b1dd009
Merge branch 'develop' into feat/#5293-weekly-schedule-planning
rahul-rocket Jan 26, 2025
39e500f
fix: #5293 bulk insert method for employee availability
rahul-rocket Jan 26, 2025
9037f32
fix: #5293 bulk insert method for employee availability
rahul-rocket Jan 26, 2025
413bf4e
fix: #5293 apply validation and permissions guard
rahul-rocket Jan 26, 2025
043f77f
fix: property 'tenantId' does not exist on type 'IEmployeeAvailabilit…
rahul-rocket Jan 26, 2025
fdab334
feat: #5293 added permission for Employee Availability
rahul-rocket Jan 26, 2025
d51f3f7
feat: #5293 added permission for Employee Availability
rahul-rocket Jan 27, 2025
d8ece61
feat: [table migration] for employee availability for all DB types.
rahul-rocket Jan 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/contracts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
65 changes: 65 additions & 0 deletions packages/contracts/src/lib/employee-availability.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
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, AvailabilityStatusValue> = {
[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 {
employeeId: ID;
employee: IEmployee;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Remove duplicate employeeId property.

The employeeId property is already inherited from IBaseEmployeeAvailability and doesn't need to be redeclared.

export interface IEmployeeAvailability extends IBaseEmployeeAvailability {
-  employeeId: ID;
  employee: IEmployee;
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export interface IEmployeeAvailability extends IBaseEmployeeAvailability {
employeeId: ID;
employee: IEmployee;
}
export interface IEmployeeAvailability extends IBaseEmployeeAvailability {
employee: IEmployee;
}


/**
* Input interface for finding Employee Availability records.
*/
export interface IEmployeeAvailabilityFindInput extends Partial<IBaseEmployeeAvailability> {}

/**
* 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<IBaseEmployeeAvailability> {}
1 change: 1 addition & 0 deletions packages/core/src/lib/core/entities/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
@@ -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[]) {}
}
Original file line number Diff line number Diff line change
@@ -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) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { BadRequestException } from '@nestjs/common';
import { CommandBus, CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { IEmployeeAvailability } from '@gauzy/contracts';
import { RequestContext } from '../../../core/context';
import { EmployeeAvailabilityBulkCreateCommand } from '../employee-availability.bulk.create.command';
import { EmployeeAvailability } from '../../employee-availability.entity';
import { EmployeeAvailabilityCreateCommand } from '../employee-availability.create.command';

@CommandHandler(EmployeeAvailabilityBulkCreateCommand)
export class EmployeeAvailabilityBulkCreateHandler implements ICommandHandler<EmployeeAvailabilityBulkCreateCommand> {
constructor(private readonly commandBus: CommandBus) {}

public async execute(command: EmployeeAvailabilityBulkCreateCommand): Promise<IEmployeeAvailability[]> {
const { input } = command;
if (!Array.isArray(input) || input.length === 0) {
throw new BadRequestException('Input must be a non-empty array of availability records.');
}

const allAvailability: IEmployeeAvailability[] = [];
const tenantId = RequestContext.currentTenantId();

try {
// Process items in parallel with Promise.all
const results = await Promise.all(
input.map(async (item) => {
const availability = new EmployeeAvailability({
...item,
tenantId
});
return this.commandBus.execute(new EmployeeAvailabilityCreateCommand(availability));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@samuelmbabhazi Why we creating EmployeeAvailability in loop instead of bulk?

})
);
allAvailability.push(...results);
} catch (error) {
throw new BadRequestException('Failed to create some availability records: ' + error.message);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider adding transaction support and input size limits.

While parallel processing is implemented, consider these improvements:

  1. Wrap the bulk operation in a transaction to ensure atomicity
  2. Add a limit to the input array size to prevent performance issues
 try {
+  const MAX_BULK_SIZE = 100;
+  if (input.length > MAX_BULK_SIZE) {
+    throw new BadRequestException(`Cannot process more than ${MAX_BULK_SIZE} records at once`);
+  }
+  // Use TypeORM's transaction
+  const queryRunner = this.connection.createQueryRunner();
+  await queryRunner.connect();
+  await queryRunner.startTransaction();
   try {
     const results = await Promise.all(
       input.map(async (item) => {
         const availability = new EmployeeAvailability({
           ...item,
           tenantId
         });
         return this.commandBus.execute(new EmployeeAvailabilityCreateCommand(availability));
       })
     );
     allAvailability.push(...results);
+    await queryRunner.commitTransaction();
   } catch (error) {
+    await queryRunner.rollbackTransaction();
     throw new BadRequestException('Failed to create some availability records: ' + error.message);
+  } finally {
+    await queryRunner.release();
   }

Committable suggestion skipped: line range outside the PR's diff.

return allAvailability;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { BadRequestException } from '@nestjs/common';
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<EmployeeAvailabilityCreateCommand> {
constructor(private readonly availabilityService: EmployeeAvailabilityService) {}

/**
* Handles the creation of an employee availability record.
*
* @param {EmployeeAvailabilityCreateCommand} command - The command containing employee availability details.
* @returns {Promise<IEmployeeAvailability>} - The newly created employee availability record.
* @throws {BadRequestException} - If any validation fails (e.g., missing fields, invalid dates).
*/
public async execute(command: EmployeeAvailabilityCreateCommand): Promise<IEmployeeAvailability> {
const { input } = command;
const { startDate, endDate, employeeId, dayOfWeek, availabilityStatus } = input;

if (!employeeId) {
throw new BadRequestException('Employee ID is required.');
}
if (typeof dayOfWeek !== 'number' || dayOfWeek < 0 || dayOfWeek > 6) {
throw new BadRequestException('Day of week must be a number between 0 and 6.');
}
if (!availabilityStatus) {
throw new BadRequestException('Availability status is required.');
}

if (!startDate || !endDate) {
throw new BadRequestException('Start date and end date are required.');
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add date range validation.

Validate that the end date is after the start date to ensure a valid date range.

 if (!startDate || !endDate) {
   throw new BadRequestException('Start date and end date are required.');
 }
+if (new Date(endDate) <= new Date(startDate)) {
+  throw new BadRequestException('End date must be after start date.');
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!startDate || !endDate) {
throw new BadRequestException('Start date and end date are required.');
}
if (!startDate || !endDate) {
throw new BadRequestException('Start date and end date are required.');
}
if (new Date(endDate) <= new Date(startDate)) {
throw new BadRequestException('End date must be after start date.');
}


if (new Date(endDate) <= new Date(startDate)) {
throw new BadRequestException('End date must be after start date.');
}

const tenantId = RequestContext.currentTenantId();

const availability = new EmployeeAvailability({
...input,
tenantId
});

return await this.availabilityService.create(availability);
}
}
Original file line number Diff line number Diff line change
@@ -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];
2 changes: 2 additions & 0 deletions packages/core/src/lib/employee-availability/commands/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './employee-availability.bulk.create.command';
export * from './employee-availability.create.command';
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { IsUUID, IsDateString, IsEnum, IsOptional, IsString, IsInt } from 'class-validator';
import { AvailabilityStatusEnum, ID, IEmployeeAvailabilityCreateInput } from '@gauzy/contracts';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { TenantOrganizationBaseDTO } from '../../core/dto/tenant-organization-base.dto';

export class CreateEmployeeAvailabilityDTO
extends TenantOrganizationBaseDTO
implements IEmployeeAvailabilityCreateInput
{
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@samuelmbabhazi Why we not used EmployeeAvailability entity here?? Why we duplicated property which already existed into EmployeeAvailability

Note: Please try to reuse as much as code.

@ApiProperty({ type: () => String })
@IsUUID()
employeeId: ID;

@ApiProperty({ type: () => Date })
@IsDateString()
startDate: Date;

@ApiProperty({ type: () => Date })
@IsDateString()
endDate: Date;
Copy link
Contributor

@coderabbitai coderabbitai bot Jan 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add date range validation.

Add validation to ensure startDate is before endDate.

@IsDateString()
@ValidateIf((o) => o.endDate && new Date(o.startDate) >= new Date(o.endDate))
@ApiProperty({ type: () => Date })
startDate: Date;

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@samuelmbabhazi Yes. Please add date range validation here. We have some custom validator for it. Can you please check?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!


@ApiProperty({ type: () => Number })
@IsInt()
dayOfWeek: number;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add range validation for dayOfWeek.

The dayOfWeek property should be validated to ensure it's between 0 and 6 (Sunday to Saturday).

@ApiProperty({ type: () => Number, description: '0 = Sunday, 6 = Saturday' })
@IsInt()
@Min(0)
@Max(6)
dayOfWeek: number;


@ApiProperty({ enum: AvailabilityStatusEnum })
@IsEnum(AvailabilityStatusEnum)
availabilityStatus: AvailabilityStatusEnum;

@ApiPropertyOptional({
type: () => String
})
@IsOptional()
@IsString()
availabilityNotes?: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { PartialType } from '@nestjs/mapped-types';
import { IEmployeeAvailabilityUpdateInput } from '@gauzy/contracts';
import { CreateEmployeeAvailabilityDTO } from './create-employee-availability.dto';

export class UpdateEmployeeAvailabilityDTO
extends PartialType(CreateEmployeeAvailabilityDTO)
implements IEmployeeAvailabilityUpdateInput {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
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 } from '@gauzy/contracts';
import { EmployeeAvailabilityService } from './employee-availability.service';
import { EmployeeAvailability } from './employee-availability.entity';
import { CrudController, PaginationParams } from '../core';
import { PermissionGuard, TenantPermissionGuard, UUIDValidationPipe } from '../shared';
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)
@Controller('/employee-availability')
export class EmployeeAvailabilityController extends CrudController<EmployeeAvailability> {
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)
@Post('/bulk')
async createBulk(@Body() entities: CreateEmployeeAvailabilityDTO[]): Promise<IEmployeeAvailability[]> {
return await this.commandBus.execute(new EmployeeAvailabilityBulkCreateCommand(entities));
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider adding rate limiting for bulk operations.

The bulk creation endpoint could be vulnerable to abuse. Consider adding rate limiting.

 @HttpCode(HttpStatus.CREATED)
+@UseGuards(ThrottlerGuard)
+@Throttle(10, 60) // 10 requests per minute
 @Post('/bulk')
 async createBulk(@Body() entities: CreateEmployeeAvailabilityDTO[]): Promise<IEmployeeAvailability[]> {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Post('/bulk')
async createBulk(@Body() entities: CreateEmployeeAvailabilityDTO[]): Promise<IEmployeeAvailability[]> {
return await this.commandBus.execute(new EmployeeAvailabilityBulkCreateCommand(entities));
}
@HttpCode(HttpStatus.CREATED)
@UseGuards(ThrottlerGuard)
@Throttle(10, 60) // 10 requests per minute
@Post('/bulk')
async createBulk(@Body() entities: CreateEmployeeAvailabilityDTO[]): Promise<IEmployeeAvailability[]> {
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.'
})
@Get()
async findAll(
@Query() filter: PaginationParams<EmployeeAvailability>
): Promise<IPagination<IEmployeeAvailability>> {
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)
@Post()
async create(@Body() entity: CreateEmployeeAvailabilityDTO): Promise<IEmployeeAvailability> {
return await 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)
@Put(':id')
async update(
@Param('id', UUIDValidationPipe) id: ID,
@Body() entity: UpdateEmployeeAvailabilityDTO
): Promise<IEmployeeAvailability | UpdateResult> {
return this.availabilityService.update(id, { ...entity });
}
}
Loading
Loading