Skip to content

Commit

Permalink
Merge pull request #320 from framgia/hris-376
Browse files Browse the repository at this point in the history
HRIS-376 [BE] Navbar: Time Out
  • Loading branch information
Miguel21Monacillo authored Aug 9, 2024
2 parents dec7913 + c7ef843 commit 93cca04
Show file tree
Hide file tree
Showing 12 changed files with 441 additions and 125 deletions.
2 changes: 2 additions & 0 deletions api_v2/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { PositionsGuard } from './guards/position.guard';
import { TimeRecordModule } from './time-record/time-record.module';
import { UserModule } from './user/user.module';
import { DtrManagementModule } from './dtr-management/dtr-management.module';
import { TimeOutModule } from './time-out/time-out.module';

@Module({
imports: [
Expand Down Expand Up @@ -86,6 +87,7 @@ import { DtrManagementModule } from './dtr-management/dtr-management.module';
TimeRecordModule,
UserModule,
DtrManagementModule,
TimeOutModule,
],
controllers: [AppController],
providers: [
Expand Down
9 changes: 9 additions & 0 deletions api_v2/src/time-out/time-out.module.ts
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 {}
61 changes: 61 additions & 0 deletions api_v2/src/time-out/time-out.resolver.spec.ts
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);
});
});
});
19 changes: 19 additions & 0 deletions api_v2/src/time-out/time-out.resolver.ts
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);
}
}
}
19 changes: 19 additions & 0 deletions api_v2/src/time-out/time-out.service.spec.ts
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();
});
});
182 changes: 182 additions & 0 deletions api_v2/src/time-out/time-out.service.ts
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);
}
}
2 changes: 1 addition & 1 deletion api_v2/src/time-record/time-record.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ import { TimeRecordService } from './time-record.service';
import { PrismaService } from '@/prisma/prisma.service';

@Module({
providers: [TimeRecordResolver, TimeRecordService, PrismaService]
providers: [TimeRecordResolver, TimeRecordService, PrismaService],
})
export class TimeRecordModule {}
21 changes: 13 additions & 8 deletions api_v2/src/time-record/time-record.resolver.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,21 @@ import { TimeRecordService } from './time-record.service';
describe('TimeRecordResolver', () => {
let resolver: TimeRecordResolver;
const mockTimeRecordService = {
getUserById: jest.fn().mockResolvedValue({id: 1, name: 'Abdul Jalil Palala'}),
getTimeEntriesById: jest.fn().mockResolvedValue({startTime: "09:30:00"}),
getUserById: jest
.fn()
.mockResolvedValue({ id: 1, name: 'Abdul Jalil Palala' }),
getTimeEntriesById: jest.fn().mockResolvedValue({ startTime: '09:30:00' }),
};

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [TimeRecordResolver, {
provide: TimeRecordService,
useValue: mockTimeRecordService
}],
providers: [
TimeRecordResolver,
{
provide: TimeRecordService,
useValue: mockTimeRecordService,
},
],
}).compile();

resolver = module.get<TimeRecordResolver>(TimeRecordResolver);
Expand All @@ -28,15 +33,15 @@ describe('TimeRecordResolver', () => {
it('should call the service to get user', async () => {
const result = await resolver.userById(1);

expect(result).toEqual({id: 1, name: 'Abdul Jalil Palala'});
expect(result).toEqual({ id: 1, name: 'Abdul Jalil Palala' });
});
});

describe('timeEntriesByEmployeeId', () => {
it('should call the service to get time entries', async () => {
const result = await resolver.timeEntriesByEmployeeId(1);

expect(result).toEqual({startTime: "09:30:00"});
expect(result).toEqual({ startTime: '09:30:00' });
});
});
});
Loading

0 comments on commit 93cca04

Please sign in to comment.