Skip to content

Commit

Permalink
Merge pull request #8332 from ever-co/feat/timesheet-entity-subscriber
Browse files Browse the repository at this point in the history
[Feat] Timesheet Entity Event Subscriber
  • Loading branch information
rahul-rocket authored Oct 4, 2024
2 parents 3242681 + 165fb49 commit 6f6caaf
Show file tree
Hide file tree
Showing 11 changed files with 145 additions and 124 deletions.
2 changes: 1 addition & 1 deletion packages/contracts/src/timesheet.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export interface ITimesheet extends IBasePerTenantAndOrganizationEntityModel {
lockedAt?: Date;
editedAt?: Date;
isBilled?: boolean;
status: string;
status: TimesheetStatus;
isEdited?: boolean;
}

Expand Down
1 change: 1 addition & 0 deletions packages/core/src/core/entities/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,5 +185,6 @@ export * from '../../tenant/tenant.subscriber';
export * from '../../time-off-request/time-off-request.subscriber';
export * from '../../time-tracking/activity/activity.subscriber';
export * from '../../time-tracking/screenshot/screenshot.subscriber';
export * from '../../time-tracking/timesheet/timesheet.subscriber';
export * from '../../time-tracking/time-slot/time-slot.subscriber';
export * from '../../user/user.subscriber';
2 changes: 2 additions & 0 deletions packages/core/src/core/entities/subscribers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
TaskVersionSubscriber,
TenantSubscriber,
TimeOffRequestSubscriber,
TimesheetSubscriber,
TimeSlotSubscriber,
UserSubscriber
} from '../internal';
Expand Down Expand Up @@ -90,6 +91,7 @@ export const coreSubscribers = [
TaskVersionSubscriber,
TenantSubscriber,
TimeOffRequestSubscriber,
TimesheetSubscriber,
TimeSlotSubscriber,
UserSubscriber
];
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { ICommand } from '@nestjs/cqrs';
import { ITimesheet } from '@gauzy/contracts';
import { ID } from '@gauzy/contracts';

export class TimesheetRecalculateCommand implements ICommand {
static readonly type = '[Timesheet] Recalculate';

constructor(
public readonly id: ITimesheet['id']
) { }
constructor(public readonly id: ID) {}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import { ApiProperty } from "@nestjs/swagger";
import { ArrayNotEmpty, IsNotEmpty } from "class-validator";
import { ISubmitTimesheetInput } from "@gauzy/contracts";
import { TenantOrganizationBaseDTO } from "../../../../core/dto";
import { ApiProperty } from '@nestjs/swagger';
import { ArrayNotEmpty, IsEnum, IsNotEmpty } from 'class-validator';
import { ID, ISubmitTimesheetInput } from '@gauzy/contracts';
import { TenantOrganizationBaseDTO } from '../../../../core/dto';

/**
* Submit timesheets status request DTO validation
*/
export class SubmitTimesheetStatusDTO extends TenantOrganizationBaseDTO
implements ISubmitTimesheetInput {
export class SubmitTimesheetStatusDTO extends TenantOrganizationBaseDTO implements ISubmitTimesheetInput {
@ApiProperty({ description: 'Array of timesheet IDs to submit/unsubmit' })
@ArrayNotEmpty({ message: 'At least one timesheet ID must be provided' })
readonly ids: ID[];

@ApiProperty({ type: () => Array, readOnly: true })
@ArrayNotEmpty()
readonly ids: string[] = [];

@ApiProperty({ type: () => String, readOnly: true })
@IsNotEmpty()
@ApiProperty({
enum: ['submit', 'unsubmit'],
description: 'Status to set on the timesheet (either "submit" or "unsubmit")'
})
@IsEnum(['submit', 'unsubmit'], { message: 'Status must be either "submit" or "unsubmit"' })
@IsNotEmpty({ message: 'Status must not be empty' })
readonly status: 'submit' | 'unsubmit' = 'submit';
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
import { ApiProperty } from "@nestjs/swagger";
import { ArrayNotEmpty, IsEnum } from "class-validator";
import { IUpdateTimesheetStatusInput, TimesheetStatus } from "@gauzy/contracts";
import { TenantOrganizationBaseDTO } from "./../../../../core/dto";
import { ApiProperty, IntersectionType, PickType } from '@nestjs/swagger';
import { ArrayNotEmpty } from 'class-validator';
import { ID, IUpdateTimesheetStatusInput } from '@gauzy/contracts';
import { TenantOrganizationBaseDTO } from './../../../../core/dto';
import { Timesheet } from '../../timesheet.entity';

/**
* Update timesheets status request DTO validation
*/
export class UpdateTimesheetStatusDTO extends TenantOrganizationBaseDTO
implements IUpdateTimesheetStatusInput {

@ApiProperty({ type: () => Array })
export class UpdateTimesheetStatusDTO
extends IntersectionType(TenantOrganizationBaseDTO, PickType(Timesheet, ['status'] as const))
implements IUpdateTimesheetStatusInput
{
@ApiProperty({ type: () => Array })
@ArrayNotEmpty()
readonly ids: string[] = [];

@ApiProperty({ enum: TimesheetStatus })
@IsEnum(TimesheetStatus)
readonly status: TimesheetStatus;
}
ids: ID[] = [];
}
71 changes: 40 additions & 31 deletions packages/core/src/time-tracking/timesheet/timesheet.controller.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Controller, UseGuards, Put, HttpStatus, Body, Get, Query, Param, BadRequestException } from '@nestjs/common';
import { Controller, UseGuards, Put, HttpStatus, Body, Get, Query, Param, HttpException } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
import { CommandBus } from '@nestjs/cqrs';
import { ITimesheet, PermissionsEnum } from '@gauzy/contracts';
import { ID, ITimesheet, PermissionsEnum } from '@gauzy/contracts';
import { TimeSheetService } from './timesheet.service';
import { PermissionGuard, TenantPermissionGuard } from './../../shared/guards';
import { UUIDValidationPipe, UseValidationPipe } from './../../shared/pipes';
Expand All @@ -14,38 +14,44 @@ import { TimesheetSubmitCommand, TimesheetUpdateStatusCommand } from './commands
@Permissions(PermissionsEnum.CAN_APPROVE_TIMESHEET)
@Controller()
export class TimeSheetController {
constructor(private readonly commandBus: CommandBus, private readonly timeSheetService: TimeSheetService) {}
constructor(private readonly _commandBus: CommandBus, private readonly _timeSheetService: TimeSheetService) {}

/**
* GET timesheet counts in the same tenant
* GET timesheet counts for the same tenant
* This method retrieves the count of timesheets for a tenant, filtered by the provided query options.
*
* @param options
* @returns
* @param options - The query parameters for filtering timesheets, such as tenant ID, date range, employee ID, etc.
* @returns Promise<number> - The count of timesheets matching the provided filters.
* @throws HttpException - If an error occurs during query execution, it returns an HTTP 400 error with an error message.
*/
@ApiOperation({ summary: 'Get timesheet Count' })
@ApiOperation({ summary: 'Get timesheet count' })
@ApiResponse({
status: HttpStatus.OK,
description: 'Get timesheet Count'
description: 'Timesheet count successfully retrieved'
})
@ApiResponse({
status: HttpStatus.BAD_REQUEST,
description: 'Invalid input, The response body may contain clues as to what went wrong'
description: 'Invalid input, check the response body for more details'
})
@Get('/count')
@UseValidationPipe({ whitelist: true })
async getTimesheetCount(@Query() options: TimesheetQueryDTO): Promise<number> {
try {
return await this.timeSheetService.getTimeSheetCount(options);
// Return the timesheet count directly
return await this._timeSheetService.getTimeSheetCount(options);
} catch (error) {
throw new BadRequestException(error);
// Handle errors and throw an appropriate error response
throw new HttpException(`Error retrieving timesheet count: ${error.message}`, HttpStatus.BAD_REQUEST);
}
}

/**
* UPDATE timesheet status
* This method updates the status of a timesheet based on the data provided in the DTO.
*
* @param entity
* @returns
* @param entity - The DTO containing the updated status for the timesheet.
* @returns Promise<ITimesheet[]> - The updated list of timesheets after applying the status changes.
* @throws HttpException - If an error occurs during status update, it throws an HTTP 400 error.
*/
@ApiOperation({ summary: 'Update timesheet' })
@ApiResponse({
Expand All @@ -59,14 +65,16 @@ export class TimeSheetController {
@Put('/status')
@UseValidationPipe({ whitelist: true })
async updateTimesheetStatus(@Body() entity: UpdateTimesheetStatusDTO): Promise<ITimesheet[]> {
return await this.commandBus.execute(new TimesheetUpdateStatusCommand(entity));
return await this._commandBus.execute(new TimesheetUpdateStatusCommand(entity));
}

/**
* UPDATE timesheet submit status
* This method submits a timesheet by updating its submission status.
*
* @param entity
* @returns
* @param entity - The DTO containing the submission details for the timesheet.
* @returns Promise<ITimesheet[]> - The updated list of timesheets after the submission.
* @throws HttpException - If an error occurs during submission, it throws an HTTP 400 error.
*/
@ApiOperation({ summary: 'Submit timesheet' })
@ApiResponse({
Expand All @@ -80,14 +88,16 @@ export class TimeSheetController {
@Put('/submit')
@UseValidationPipe({ whitelist: true })
async submitTimeSheet(@Body() entity: SubmitTimesheetStatusDTO): Promise<ITimesheet[]> {
return await this.commandBus.execute(new TimesheetSubmitCommand(entity));
return await this._commandBus.execute(new TimesheetSubmitCommand(entity));
}

/**
* GET all timesheet in same tenant
* GET all timesheets in the same tenant
* This method retrieves all timesheets for the same tenant based on the provided query options.
*
* @param options
* @returns
* @param options - The query parameters for filtering timesheets, such as tenant ID, date range, employee ID, etc.
* @returns Promise<ITimesheet[]> - A list of timesheets matching the provided filters.
* @throws HttpException - If an error occurs during query execution, it throws an HTTP 400 error with an error message.
*/
@ApiOperation({ summary: 'Get timesheet' })
@ApiResponse({
Expand All @@ -102,17 +112,20 @@ export class TimeSheetController {
@UseValidationPipe({ whitelist: true })
async get(@Query() options: TimesheetQueryDTO): Promise<ITimesheet[]> {
try {
return await this.timeSheetService.getTimeSheets(options);
return await this._timeSheetService.getTimeSheets(options);
} catch (error) {
throw new BadRequestException(error);
// Handle errors and throw an appropriate error response
throw new HttpException(`Error retrieving timesheets: ${error.message}`, HttpStatus.BAD_REQUEST);
}
}

/**
* Find timesheet by id
* Find timesheet by ID
* This method retrieves a specific timesheet by its unique identifier.
*
* @param id
* @returns
* @param id - The UUID of the timesheet to retrieve.
* @returns Promise<ITimesheet> - The timesheet with the specified ID.
* @throws HttpException - If the timesheet with the specified ID is not found, it throws an HTTP 400 error.
*/
@ApiOperation({ summary: 'Find timesheet by id' })
@ApiResponse({
Expand All @@ -124,11 +137,7 @@ export class TimeSheetController {
description: 'Invalid input, The response body may contain clues as to what went wrong'
})
@Get('/:id')
async findById(@Param('id', UUIDValidationPipe) id: ITimesheet['id']): Promise<ITimesheet> {
try {
return await this.timeSheetService.findOneByIdString(id);
} catch (error) {
throw new BadRequestException(error);
}
async findById(@Param('id', UUIDValidationPipe) id: ID): Promise<ITimesheet> {
return await this._timeSheetService.findOneByIdString(id);
}
}
46 changes: 14 additions & 32 deletions packages/core/src/time-tracking/timesheet/timesheet.entity.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
import {
RelationId,
JoinColumn,
AfterLoad
} from 'typeorm';
import { RelationId, JoinColumn } from 'typeorm';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsBoolean, IsDateString, IsEnum, IsNumber, IsOptional, IsUUID } from 'class-validator';
import { IEmployee, ITimesheet, IUser, TimesheetStatus } from '@gauzy/contracts';
import { ID, IEmployee, ITimesheet, IUser, TimesheetStatus } from '@gauzy/contracts';
import { Employee, TenantOrganizationBaseEntity, User } from './../../core/entities/internal';
import {
Employee,
TenantOrganizationBaseEntity,
User
} from './../../core/entities/internal';
import { ColumnIndex, MultiORMColumn, MultiORMEntity, MultiORMManyToOne, VirtualMultiOrmColumn } from './../../core/decorators/entity';
ColumnIndex,
MultiORMColumn,
MultiORMEntity,
MultiORMManyToOne,
VirtualMultiOrmColumn
} from './../../core/decorators/entity';
import { MikroOrmTimesheetRepository } from './repository/mikro-orm-timesheet.repository';

@MultiORMEntity('timesheet', { mikroOrmRepository: () => MikroOrmTimesheetRepository })
export class Timesheet extends TenantOrganizationBaseEntity implements ITimesheet {

@ApiPropertyOptional({ type: () => Number, default: 0 })
@IsOptional()
@IsNumber()
Expand Down Expand Up @@ -90,12 +87,11 @@ export class Timesheet extends TenantOrganizationBaseEntity implements ITimeshee
@MultiORMColumn({ default: false })
isBilled?: boolean;

@ApiPropertyOptional({ type: () => String, enum: TimesheetStatus, default: TimesheetStatus.PENDING })
@IsOptional()
@ApiProperty({ type: () => String, enum: TimesheetStatus, default: TimesheetStatus.PENDING })
@IsEnum(TimesheetStatus)
@ColumnIndex()
@MultiORMColumn({ default: TimesheetStatus.PENDING })
status: string;
status: TimesheetStatus;

/** Additional virtual columns */

Expand Down Expand Up @@ -128,14 +124,14 @@ export class Timesheet extends TenantOrganizationBaseEntity implements ITimeshee
@RelationId((it: Timesheet) => it.employee)
@ColumnIndex()
@MultiORMColumn({ relationId: true })
employeeId?: IEmployee['id'];
employeeId?: ID;

/**
* Approve By User
*/
@MultiORMManyToOne(() => User, {
/** Indicates if the relation column value can be nullable or not. */
nullable: true,
nullable: true
})
@JoinColumn()
approvedBy?: IUser;
Expand All @@ -146,19 +142,5 @@ export class Timesheet extends TenantOrganizationBaseEntity implements ITimeshee
@RelationId((it: Timesheet) => it.approvedBy)
@ColumnIndex()
@MultiORMColumn({ nullable: true, relationId: true })
approvedById?: IUser['id'];

/**
* Called after entity is loaded.
*/
@AfterLoad()
afterLoadEntity?() {
/**
* Sets the 'isEdited' property based on the presence of 'editedAt'.
* If 'editedAt' is defined, 'isEdited' is set to true; otherwise, it is set to false.
*/
if ('editedAt' in this) {
this.isEdited = !!this.editedAt;
}
}
approvedById?: ID;
}
5 changes: 3 additions & 2 deletions packages/core/src/time-tracking/timesheet/timesheet.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { CommandHandlers } from './commands/handlers';
import { TimeSheetController } from './timesheet.controller';
import { TimeSheetService } from './timesheet.service';
import { Timesheet } from './timesheet.entity';
import { TypeOrmTimesheetRepository } from './repository/type-orm-timesheet.repository';

@Module({
controllers: [TimeSheetController],
Expand All @@ -22,7 +23,7 @@ import { Timesheet } from './timesheet.entity';
TimeSlotModule,
EmployeeModule
],
providers: [TimeSheetService, ...CommandHandlers],
exports: [TimeSheetService, TypeOrmModule, MikroOrmModule]
providers: [TimeSheetService, TypeOrmTimesheetRepository, ...CommandHandlers],
exports: [TypeOrmModule, MikroOrmModule, TimeSheetService, TypeOrmTimesheetRepository]
})
export class TimesheetModule {}
Loading

0 comments on commit 6f6caaf

Please sign in to comment.