-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #320 from framgia/hris-376
HRIS-376 [BE] Navbar: Time Out
- Loading branch information
Showing
12 changed files
with
441 additions
and
125 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import { Module } from '@nestjs/common'; | ||
import { TimeOutService } from './time-out.service'; | ||
import { TimeOutMutation } from './time-out.resolver'; | ||
import { PrismaService } from '@/prisma/prisma.service'; | ||
|
||
@Module({ | ||
providers: [TimeOutMutation, TimeOutService, PrismaService], | ||
}) | ||
export class TimeOutModule {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import { Test, TestingModule } from '@nestjs/testing'; | ||
import { TimeOutMutation } from './time-out.resolver'; | ||
import { TimeOutService } from './time-out.service'; | ||
import { TimeOutRequestInput } from '@/graphql/graphql'; | ||
|
||
describe('TimeOutMutation', () => { | ||
let timeOutMutation: TimeOutMutation; | ||
let timeOutService: TimeOutService; | ||
|
||
const mockTimeOutService = { | ||
update: jest.fn(), | ||
}; | ||
|
||
beforeEach(async () => { | ||
const module: TestingModule = await Test.createTestingModule({ | ||
providers: [ | ||
TimeOutMutation, | ||
{ provide: TimeOutService, useValue: mockTimeOutService }, | ||
], | ||
}).compile(); | ||
|
||
timeOutMutation = module.get<TimeOutMutation>(TimeOutMutation); | ||
timeOutService = module.get<TimeOutService>(TimeOutService); | ||
}); | ||
|
||
it('should be defined', () => { | ||
expect(timeOutMutation).toBeDefined(); | ||
}); | ||
|
||
describe('updateTimeOut', () => { | ||
it('should return a success message when update is successful', async () => { | ||
const timeOutInput: TimeOutRequestInput = { | ||
userId: 0, | ||
timeHour: undefined, | ||
}; | ||
|
||
mockTimeOutService.update.mockResolvedValue( | ||
'TimeOut updated successfully.', | ||
); | ||
|
||
const result = await timeOutMutation.updateTimeOut(timeOutInput); | ||
|
||
expect(result).toEqual('TimeOut updated successfully.'); | ||
expect(timeOutService.update).toHaveBeenCalledWith(timeOutInput); | ||
}); | ||
|
||
it('should throw an error when update fails', async () => { | ||
const timeOutInput: TimeOutRequestInput = { | ||
userId: 0, | ||
timeHour: undefined, | ||
}; | ||
|
||
mockTimeOutService.update.mockRejectedValue(new Error('Update failed')); | ||
|
||
await expect(timeOutMutation.updateTimeOut(timeOutInput)).rejects.toThrow( | ||
'Update failed', | ||
); | ||
expect(timeOutService.update).toHaveBeenCalledWith(timeOutInput); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import { Resolver, Mutation, Args } from '@nestjs/graphql'; | ||
import { TimeOutService } from './time-out.service'; | ||
import { TimeOutRequestInput } from '@/graphql/graphql'; | ||
|
||
@Resolver() | ||
export class TimeOutMutation { | ||
constructor(private readonly timeOutService: TimeOutService) {} | ||
|
||
@Mutation(() => String) | ||
async updateTimeOut( | ||
@Args('timeOut') timeOut: TimeOutRequestInput, | ||
): Promise<string> { | ||
try { | ||
return await this.timeOutService.update(timeOut); | ||
} catch (e) { | ||
throw new Error(e.message); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import { Test, TestingModule } from '@nestjs/testing'; | ||
import { TimeOutService } from './time-out.service'; | ||
import { PrismaService } from '@/prisma/prisma.service'; | ||
|
||
describe('TimeOutService', () => { | ||
let service: TimeOutService; | ||
|
||
beforeEach(async () => { | ||
const module: TestingModule = await Test.createTestingModule({ | ||
providers: [TimeOutService, PrismaService], | ||
}).compile(); | ||
|
||
service = module.get<TimeOutService>(TimeOutService); | ||
}); | ||
|
||
it('should be defined', () => { | ||
expect(service).toBeDefined(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,182 @@ | ||
import { PrismaService } from '@/prisma/prisma.service'; | ||
import { Injectable, Logger } from '@nestjs/common'; | ||
import { TimeOutRequestInput } from '@/graphql/graphql'; | ||
import { TimeEntryDTO } from '@/graphql/graphql'; | ||
import { TimeEntry } from '@/graphql/graphql'; | ||
import { Time } from '@/graphql/graphql'; | ||
import { DateTime } from 'luxon'; | ||
|
||
@Injectable() | ||
export class TimeOutService { | ||
private readonly logger = new Logger(TimeOutService.name); | ||
constructor(private readonly prisma: PrismaService) {} | ||
/** | ||
* Updates the time out for a given time entry. | ||
* | ||
* @param {TimeOutRequestInput} timeout - The time out request input containing the necessary information. | ||
* @return {Promise<string>} A promise that resolves to a success message if the update is successful. | ||
* @throws {Error} If the time entry is not found, the worked hours format is invalid, or the tracked hours format is invalid. | ||
*/ | ||
async update(timeout: TimeOutRequestInput): Promise<string> { | ||
try { | ||
const currentTime = new Date(); | ||
const timeZoneOffset = currentTime.getTimezoneOffset() * 60000; | ||
const localTime = new Date(currentTime.getTime() - timeZoneOffset); | ||
|
||
const time = await this.prisma.time.create({ | ||
data: { | ||
timeHour: localTime, | ||
remarks: timeout.remarks, | ||
createdAt: localTime, | ||
updatedAt: localTime, | ||
}, | ||
}); | ||
|
||
const timeEntry = (await this.prisma.timeEntry.findUnique({ | ||
where: { id: timeout.timeEntryId! }, | ||
include: { | ||
timeIn: true, | ||
timeOut: true, | ||
overtime: true, | ||
user: { | ||
select: { | ||
profileImageId: true, | ||
}, | ||
}, | ||
workInterruptions: true, | ||
}, | ||
})) as TimeEntry; | ||
|
||
if (!timeEntry) { | ||
throw new Error('Time entry not found'); | ||
} | ||
|
||
const workedHours = this.getWorkedHours(timeEntry, time); | ||
const trackedHours = this.getTrackedHours(timeEntry); | ||
|
||
this.logger.debug(`workedHours: ${workedHours}`); | ||
this.logger.debug(`trackedHours: ${trackedHours}`); | ||
|
||
// Directly use the formatted workedHours | ||
if (!this.isValidWorkedHoursFormat(workedHours)) { | ||
throw new Error('Invalid workedHours value'); | ||
} | ||
|
||
// trackedHours is expected to be DateTime in schema | ||
if (!DateTime.isDateTime(trackedHours)) { | ||
throw new Error('Invalid trackedHours value'); | ||
} | ||
|
||
await this.prisma.timeEntry.update({ | ||
where: { id: timeout.timeEntryId! }, | ||
data: { | ||
timeOut: { | ||
connect: { id: time.id! }, | ||
}, | ||
workedHours: workedHours, // Use formatted workedHours | ||
trackedHours: trackedHours.toJSDate(), // Use DateTime directly | ||
}, | ||
}); | ||
|
||
return 'Successful Time Out!'; | ||
} catch (error) { | ||
this.logger.error(`Failed to update time out: ${error.message}`); | ||
throw new Error('Something went wrong...'); | ||
} | ||
} | ||
|
||
/** | ||
* Calculates the tracked hours for a given time entry. | ||
* | ||
* @param {TimeEntry} timeEntry - The time entry to calculate tracked hours for. | ||
* @return {DateTime} The calculated tracked hours as a DateTime object. | ||
*/ | ||
private getTrackedHours(timeEntry: TimeEntry): DateTime { | ||
// Fetch or initialize `undertime` and `late` values appropriately | ||
const timeEntryDto = new TimeEntryDTO(); | ||
const undertime = timeEntryDto.undertime || 0; | ||
const late = timeEntryDto.late || 0; | ||
|
||
// Convert values to time spans in milliseconds | ||
const undertimeTimeSpan = this.getTimeSpanFromMinutes(undertime); | ||
const lateTimeSpan = this.getTimeSpanFromMinutes(late); | ||
|
||
// Ensure endTime and startTime are Date objects | ||
const scheduledHours = | ||
timeEntry.endTime.getTime() - timeEntry.startTime.getTime(); | ||
|
||
const trackedHours = scheduledHours - undertimeTimeSpan - lateTimeSpan; | ||
|
||
// Handle negative trackedHours values | ||
if (trackedHours < 0) { | ||
this.logger.error( | ||
'Tracked hours calculation resulted in a negative value', | ||
); | ||
return DateTime.fromMillis(0); // Return a DateTime representing 0 hours | ||
} | ||
|
||
// Convert trackedHours to a DateTime | ||
|
||
const trackedHoursDateTime = DateTime.fromMillis(trackedHours); | ||
|
||
return trackedHoursDateTime; | ||
} | ||
/** | ||
* Calculates the worked hours between the time entry's time in and time out. | ||
* | ||
* @param {TimeEntry} timeEntry - The time entry object containing the time in and time out information. | ||
* @param {Time} timeOut - The time out object containing the time out information. | ||
* @return {string} The worked hours in the format "HH:MM:SS". If either the time in or time out is missing, returns "00:00:00". | ||
*/ | ||
private getWorkedHours(timeEntry: TimeEntry, timeOut: Time): string { | ||
const timeInCreatedAt = timeEntry.timeIn?.createdAt; | ||
const timeOutCreatedAt = timeOut?.createdAt; | ||
|
||
if (!timeInCreatedAt || !timeOutCreatedAt == null) { | ||
this.logger.error( | ||
`Missing timeInCreatedAt or timeOutCreatedAt: timeInCreatedAt=${timeInCreatedAt}, timeOutCreatedAt=${timeOutCreatedAt}`, | ||
); | ||
return '00:00:00'; | ||
} | ||
|
||
const start = DateTime.fromJSDate(timeInCreatedAt); | ||
const end = DateTime.fromJSDate(timeOutCreatedAt); | ||
|
||
const totalTimeSpan = end.diff(start, 'seconds'); // Get total duration in seconds | ||
|
||
const totalSeconds = totalTimeSpan.as('seconds'); | ||
const hours = Math.floor(totalSeconds / 3600); // Total hours | ||
const minutes = Math.floor((totalSeconds % 3600) / 60); // Remaining minutes | ||
const seconds = Math.floor(totalSeconds % 60); // Remaining seconds | ||
|
||
// Format hours, minutes, and seconds as strings | ||
const formattedHours = hours.toString().padStart(2, '0'); | ||
const formattedMinutes = minutes.toString().padStart(2, '0'); | ||
const formattedSeconds = seconds.toString().padStart(2, '0'); | ||
|
||
const workedHours = `${formattedHours}:${formattedMinutes}:${formattedSeconds}`; | ||
|
||
return workedHours; | ||
} | ||
|
||
/** | ||
* Converts minutes to milliseconds. | ||
* | ||
* @param {number} minutes - The number of minutes to convert. | ||
* @return {number} The equivalent time span in milliseconds. | ||
*/ | ||
private getTimeSpanFromMinutes(minutes: number): number { | ||
return minutes * 60 * 1000; // Convert minutes to milliseconds | ||
} | ||
/** | ||
* Checks if the given string is in the valid "HH:MM:SS" format. | ||
* | ||
* @param {string} hours - The string to be checked. | ||
* @return {boolean} Returns true if the string is in the valid format, false otherwise. | ||
*/ | ||
public isValidWorkedHoursFormat(hours: string): boolean { | ||
// Expect "HH:MM:SS" format | ||
const workedHoursPattern = /^([0-9]{2}):([0-9]{2}):([0-9]{2})$/; | ||
return workedHoursPattern.test(hours); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.