-
Notifications
You must be signed in to change notification settings - Fork 573
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
base: develop
Are you sure you want to change the base?
Changes from 6 commits
e00c602
7c4e4f5
ff2bacb
c369a13
f7eb738
96052b1
6c3ba54
49d09b7
2b926a6
55092e8
a6a6e4c
b1dd009
39e500f
9037f32
413bf4e
043f77f
fdab334
d51f3f7
d8ece61
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import { IBasePerTenantAndOrganizationEntityModel, ID } from './base-entity.model'; | ||
import { IEmployee } from './employee.model'; | ||
|
||
/** | ||
* Enum representing the availability status of an employee. | ||
*/ | ||
export enum AvailabilityStatusEnum { | ||
Available = 'Available', | ||
Partial = 'Partial', | ||
Unavailable = 'Unavailable' | ||
} | ||
|
||
export enum AvailabilityStatusValue { | ||
Available = 0, | ||
Partial = 1, | ||
Unavailable = 2 | ||
} | ||
|
||
export interface IEmployeeAvailability extends IBasePerTenantAndOrganizationEntityModel { | ||
employee: IEmployee; | ||
employeeId: ID; | ||
startDate: Date; | ||
endDate: Date; | ||
dayOfWeek: number; // 0 = Sunday, 6 = Saturday | ||
availabilityStatus: AvailabilityStatusEnum; | ||
availabilityNotes?: string; | ||
} | ||
|
||
/** | ||
* Input interface for finding Employee Availability records. | ||
*/ | ||
export interface IEmployeeAvailabilityFindInput { | ||
employeeId?: ID; | ||
availabilityStatus?: AvailabilityStatusEnum; | ||
startDate?: Date; | ||
endDate?: Date; | ||
} | ||
|
||
/** | ||
* Input interface for creating new Employee Availability records. | ||
*/ | ||
export interface IEmployeeAvailabilityCreateInput extends IBasePerTenantAndOrganizationEntityModel { | ||
employeeId: ID; | ||
startDate: Date; | ||
endDate: Date; | ||
dayOfWeek: number; // 0 = Sunday, 6 = Saturday | ||
availabilityStatus: AvailabilityStatusEnum; | ||
availabilityNotes?: string; | ||
} | ||
|
||
/** | ||
* Input interface for updating Employee Availability records. | ||
*/ | ||
export interface IEmployeeAvailabilityUpdateInput extends IBasePerTenantAndOrganizationEntityModel { | ||
employeeId?: ID; | ||
startDate?: Date; | ||
endDate?: Date; | ||
dayOfWeek?: number; // 0 = Sunday, 6 = Saturday | ||
availabilityStatus?: AvailabilityStatusEnum; | ||
availabilityNotes?: string; | ||
} |
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 Bulk Availability ] 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)); | ||
}) | ||
); | ||
allAvailability.push(...results); | ||
} catch (error) { | ||
throw new BadRequestException('Failed to create some availability records: ' + error.message); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
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();
}
|
||
return allAvailability; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,44 @@ | ||||||||||||||||||||
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) {} | ||||||||||||||||||||
|
||||||||||||||||||||
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.'); | ||||||||||||||||||||
} | ||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
|
||||||||||||||||||||
|
||||||||||||||||||||
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,2 @@ | ||
export * from './employee-availability.bulk.create.handler'; | ||
export * from './employee-availability.create.handler'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export * from './employee-availability.bulk.create.command'; | ||
export * from './employee-availability.create.command'; | ||
export * from './handlers/employee-availability.bulk.create.handler'; | ||
export * from './handlers/employee-availability.create.handler'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import { IsUUID, IsDateString, IsEnum, IsOptional, IsString, IsInt } from 'class-validator'; | ||
import { AvailabilityStatusEnum, ID, IEmployeeAvailabilityCreateInput } from '@gauzy/contracts'; | ||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; | ||
|
||
export class CreateEmployeeAvailabilityDTO implements IEmployeeAvailabilityCreateInput { | ||
@ApiProperty({ type: () => String }) | ||
@IsUUID() | ||
employeeId: ID; | ||
|
||
@ApiProperty({ type: () => Date }) | ||
@IsDateString() | ||
startDate: Date; | ||
|
||
@ApiProperty({ type: () => Date }) | ||
@IsDateString() | ||
endDate: Date; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
||
@ApiProperty({ type: () => Number }) | ||
@IsInt() | ||
dayOfWeek: number; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { CreateEmployeeAvailabilityDTO } from './create-employee-availability.dto'; | ||
import { IEmployeeAvailabilityUpdateInput } from '@gauzy/contracts'; | ||
|
||
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, IEmployeeAvailabilityCreateInput, IPagination } from '@gauzy/contracts'; | ||||||||||||||||||||||||
import { EmployeeAvailabilityService } from './employee-availability.service'; | ||||||||||||||||||||||||
import { EmployeeAvailability } from './employee-availability.entity'; | ||||||||||||||||||||||||
import { CrudController, PaginationParams } from '../core'; | ||||||||||||||||||||||||
import { 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) | ||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @samuelmbabhazi Permission Gurad is missing and also permissions missing for API routes. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Consider adding role-based access control. The controller only uses +@UseGuards(TenantPermissionGuard, RolesGuard)
+@Roles(RolesEnum.ADMIN, RolesEnum.MANAGER)
@UseGuards(TenantPermissionGuard)
|
||||||||||||||||||||||||
@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)); | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
|
||||||||||||||||||||||||
|
||||||||||||||||||||||||
/** | ||||||||||||||||||||||||
* 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 }); | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
} |
There was a problem hiding this comment.
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?