From f4bfda74850cdc3449bf15321d994d2993af2858 Mon Sep 17 00:00:00 2001 From: "Rahul R." Date: Tue, 24 Sep 2024 23:43:19 +0530 Subject: [PATCH] [Fix] Calculated stopped date for Start/Stop timer (#8246) * fix: updated time log create command/handler * fix: build issue * fix: calculated stopped date for stop timer * fix: missing last time logs slots * fix: close previous running timer entries - When stop (/api/timesheet/timer/stop) endpoint hit, we will close last time log entry along with all pending previous timer entries * fix: moment utc * fix: updated schedule time log entries command * fix: added example for each case * fix(deepscan): without null check prior --- .../schedule-time-log-entries.handler.ts | 98 +++++-- .../handlers/time-log-create.handler.ts | 267 +++++++++++------- .../time-tracking/time-log/time-log.module.ts | 2 +- .../time-slot/time-slot.entity.ts | 12 +- .../time-slot/time-slot.service.ts | 43 +-- .../src/time-tracking/timer/timer.service.ts | 109 ++++++- .../timesheet-first-or-create.command.ts | 6 +- 7 files changed, 381 insertions(+), 156 deletions(-) diff --git a/packages/core/src/time-tracking/time-log/commands/handlers/schedule-time-log-entries.handler.ts b/packages/core/src/time-tracking/time-log/commands/handlers/schedule-time-log-entries.handler.ts index dafe2070356..60b990e7b21 100644 --- a/packages/core/src/time-tracking/time-log/commands/handlers/schedule-time-log-entries.handler.ts +++ b/packages/core/src/time-tracking/time-log/commands/handlers/schedule-time-log-entries.handler.ts @@ -2,7 +2,7 @@ import { ICommandHandler, CommandHandler } from '@nestjs/cqrs'; import { Brackets, SelectQueryBuilder, WhereExpressionBuilder } from 'typeorm'; import * as moment from 'moment'; import { isEmpty } from '@gauzy/common'; -import { ID, ITimeLog } from '@gauzy/contracts'; +import { ID, ITimeLog, ITimeSlot } from '@gauzy/contracts'; import { prepareSQLQuery as p } from './../../../../database/database.helper'; import { TimeLog } from './../../time-log.entity'; import { ScheduleTimeLogEntriesCommand } from '../schedule-time-log-entries.command'; @@ -104,40 +104,52 @@ export class ScheduleTimeLogEntriesHandler implements ICommandHandler { const { timeSlots } = timeLog; - // Calculate the minutes difference since the time log started - const minutes = moment().diff(moment.utc(timeLog.startedAt), 'minutes'); - // Handle cases where there are no time slots if (isEmpty(timeSlots)) { + // Retrieve the last log's startedAt date + const lastLogStartedAt = moment.utc(timeLog.startedAt); + + // Example: + // If timeLog.startedAt = "2024-09-24 20:00:00" + // then lastLogStartedAt will be "2024-09-24 20:00:00" + // If the minutes difference is greater than 10, update the stoppedAt date - if (minutes > 10) { + // Example: + // If the current time is "2024-09-24 20:15:00", the difference is 15 minutes, which is greater than 10 + if (moment.utc().diff(lastLogStartedAt, 'minutes') > 10) { await this.updateStoppedAtUsingStartedAt(timeLog); } } else { // Handle cases where there are time slots await this.updateStoppedAtUsingTimeSlots(timeLog, timeSlots); + // Example: If timeSlots = [{ startedAt: "2024-09-24 20:05:00", duration: 300 }] } // Stop the pending time log entry await this.stopTimeLog(timeLog); } - /** - * Update the stoppedAt field using the startedAt value for a time log. + * Updates the stoppedAt field using the startedAt value for a time log. * - * @param timeLog The time log entry to update + * @param timeLog - The time log entry to update */ private async updateStoppedAtUsingStartedAt(timeLog: ITimeLog): Promise { - // Calculate the stoppedAt date + // Calculate the stoppedAt date by adding 10 seconds to the startedAt value const stoppedAt = moment.utc(timeLog.startedAt).add(10, 'seconds').toDate(); + // Example: + // If timeLog.startedAt = "2024-09-24 21:00:00", + // then stoppedAt will be calculated as "2024-09-24 21:00:10" (10 seconds later). + // Update the stoppedAt field in the database await this.typeOrmTimeLogRepository.save({ id: timeLog.id, stoppedAt }); + console.log('Schedule Time Log Entry Updated StoppedAt Using StartedAt', timeLog.startedAt); + // Example log output: "Schedule Time Log Entry Updated StoppedAt Using StartedAt 2024-09-24 21:00:00" } /** @@ -146,32 +158,76 @@ export class ScheduleTimeLogEntriesHandler implements ICommandHandler { - // Calculate the duration - const duration = timeSlots.reduce((sum, { duration }) => sum + duration, 0); - - // Calculate the stoppedAt date - const stoppedAt = moment.utc(timeLog.startedAt).add(duration, 'seconds').toDate(); - - // Calculate the minutes difference - const minutes = moment.utc().diff(moment.utc(stoppedAt), 'minutes'); + private async updateStoppedAtUsingTimeSlots(timeLog: ITimeLog, timeSlots: ITimeSlot[]): Promise { + // Calculate the total duration in seconds from all time slots + const totalDurationInSeconds = timeSlots.reduce((sum, { duration }) => sum + duration, 0); + + // Example: + // If timeSlots = [{ duration: 300 }, { duration: 600 }] + // Then totalDurationInSeconds = 300 + 600 = 900 seconds (i.e., 15 minutes) + + // Calculate the stoppedAt date by adding the total duration to the startedAt date of the time log + let stoppedAt = moment.utc(timeLog.startedAt).add(totalDurationInSeconds, 'seconds').toDate(); + + // Example: + // If timeLog.startedAt = "2024-09-24 10:00:00" and totalDurationInSeconds = 900, + // then stoppedAt = "2024-09-24 10:15:00" + + // Retrieve the most recent time slot from the last log + const lastTimeSlot: ITimeSlot | undefined = timeSlots.sort((a: ITimeSlot, b: ITimeSlot) => + moment(a.startedAt).isBefore(b.startedAt) ? 1 : -1 + )[0]; + + // Example: + // If timeSlots = [{ startedAt: "2024-09-24 10:05:00" }, { startedAt: "2024-09-24 10:10:00" }] + // The sorted result will be [{ startedAt: "2024-09-24 10:10:00" }, { startedAt: "2024-09-24 10:05:00" }] + // Thus, lastTimeSlot = { startedAt: "2024-09-24 10:10:00" } + + // Check if the last time slot was created more than 10 minutes ago + if (lastTimeSlot) { + // Retrieve the last time slot's startedAt date + const lastTimeSlotStartedAt = moment.utc(lastTimeSlot.startedAt); + // Retrieve the last time slot's duration + const duration = lastTimeSlot.duration; + + // Example: + // If lastTimeSlot.startedAt = "2024-09-24 10:00:00" and duration = 300 (i.e., 5 minutes) + // then lastTimeSlotStartedAt would be "2024-09-24 10:00:00" + // and the stoppedAt time will be calculated as "2024-09-24 10:05:00". + + // Check if the last time slot was created more than 10 minutes ago + if (moment.utc().diff(lastTimeSlotStartedAt, 'minutes') > 10) { + // Calculate the potential stoppedAt time using the total duration + // Example: If the last time slot started at "2024-09-24 10:00:00" and ran for 300 seconds (5 minutes), + // then the calculated stoppedAt time would be "2024-09-24 10:05:00". + stoppedAt = moment.utc(lastTimeSlot.startedAt).add(duration, 'seconds').toDate(); + } + } // Update the stoppedAt field in the database - if (minutes > 10) { + if (moment.utc().diff(stoppedAt, 'minutes') > 10) { + // Example: + // If the current time is "2024-09-24 21:30:00" and stoppedAt is "2024-09-24 21:15:00", + // the difference would be 15 minutes, which is greater than 10. + // In this case, the stoppedAt field will be updated in the database. + + // Calculate the potential stoppedAt time using the total duration await this.typeOrmTimeLogRepository.save({ id: timeLog.id, stoppedAt }); console.log('Schedule Time Log Entry Updated StoppedAt Using StoppedAt', stoppedAt); + // Example log output: "Schedule Time Log Entry Updated StoppedAt Using StoppedAt 2024-09-24 21:15:00" } } /** - * Mark the time log as not running (stopped) in the database. + * Marks the time log as not running (stopped) in the database. * - * @param timeLog The time log entry to stop + * @param timeLog - The time log entry to stop */ private async stopTimeLog(timeLog: ITimeLog): Promise { + // Update the isRunning field to false in the database for the given time log await this.typeOrmTimeLogRepository.save({ id: timeLog.id, isRunning: false diff --git a/packages/core/src/time-tracking/time-log/commands/handlers/time-log-create.handler.ts b/packages/core/src/time-tracking/time-log/commands/handlers/time-log-create.handler.ts index 8f89700b63a..e68f9459333 100644 --- a/packages/core/src/time-tracking/time-log/commands/handlers/time-log-create.handler.ts +++ b/packages/core/src/time-tracking/time-log/commands/handlers/time-log-create.handler.ts @@ -1,139 +1,206 @@ import { ICommandHandler, CommandBus, CommandHandler } from '@nestjs/cqrs'; -import { TimeLogType, TimeLogSourceEnum, ITimeSlot } from '@gauzy/contracts'; -import { InjectRepository } from '@nestjs/typeorm'; import * as moment from 'moment'; -import { TimeLog } from './../../time-log.entity'; -import { TimeLogCreateCommand } from '../time-log-create.command'; +import { TimeLogType, TimeLogSourceEnum, ID, ITimeSlot, ITimesheet } from '@gauzy/contracts'; import { TimeSlotService } from '../../../time-slot/time-slot.service'; import { TimesheetFirstOrCreateCommand, TimesheetRecalculateCommand -} from './../../../timesheet/commands'; +} from '../../../timesheet/commands'; import { UpdateEmployeeTotalWorkedHoursCommand } from '../../../../employee/commands'; import { RequestContext } from '../../../../core/context'; -import { TypeOrmTimeLogRepository } from '../../repository/type-orm-time-log.repository'; -import { MikroOrmTimeLogRepository } from '../../repository/mikro-orm-time-log.repository'; +import { TimeLogService } from '../../time-log.service'; +import { TimeLog } from '../../time-log.entity'; +import { TimeLogCreateCommand } from '../time-log-create.command'; +import { MikroOrmTimeLogRepository, TypeOrmTimeLogRepository } from '../../repository'; @CommandHandler(TimeLogCreateCommand) export class TimeLogCreateHandler implements ICommandHandler { constructor( - @InjectRepository(TimeLog) readonly typeOrmTimeLogRepository: TypeOrmTimeLogRepository, - readonly mikroOrmTimeLogRepository: MikroOrmTimeLogRepository, - - private readonly commandBus: CommandBus, - private readonly timeSlotService: TimeSlotService - ) { } - + private readonly _commandBus: CommandBus, + private readonly _timeSlotService: TimeSlotService, + private readonly _timeLogService: TimeLogService, + ) {} + + /** + * Handles the execution of the TimeLogCreateCommand + * + * @param command TimeLogCreateCommand + * @returns Promise + */ public async execute(command: TimeLogCreateCommand): Promise { const { input } = command; - const { startedAt, employeeId, organizationId } = input; + const { startedAt, employeeId, organizationId, stoppedAt, timeSlots: inputTimeSlots } = input; + // Retrieve the tenant ID from the current context or the provided one in the input const tenantId = RequestContext.currentTenantId() || input.tenantId; - const timesheet = await this.commandBus.execute( - new TimesheetFirstOrCreateCommand( - startedAt, - employeeId, - organizationId - ) + // Create timesheet if it doesn't exist + const timesheet = await this._commandBus.execute( + new TimesheetFirstOrCreateCommand(startedAt, employeeId, organizationId) + ); + + // Create time log entity + const timeLog = this.createTimeLogEntity( + input, + tenantId, + timesheet + ); + + // Generate blank time slots if stoppedAt is provided + let generatedTimeSlots: ITimeSlot[] = stoppedAt ? this.generateBlankTimeSlots(input, tenantId) : []; + + // Merge input time slots with generated blank slots + const mergeTimeSlots = this.mergeTimeSlots( + generatedTimeSlots, + inputTimeSlots, + employeeId, + organizationId, + tenantId + ) + + // Bulk create time slots + timeLog.timeSlots = await this._timeSlotService.bulkCreate( + mergeTimeSlots, + employeeId, + organizationId ); - const timeLog = new TimeLog({ + await this.typeOrmTimeLogRepository.save(timeLog); + + // Recalculate timesheet activity + await this.recalculateTimesheet(timesheet); + + // Update total worked hours for the employee + await this.updateEmployeeTotalWorkedHours(employeeId); + + // Return the newly created time log + return await this._timeLogService.findOneByIdString(timeLog.id); + } + + /** + * Creates a new TimeLog entity based on the provided input + * + * @param input Partial + * @param tenantId ID + * @param timesheet ITimesheet + * @returns TimeLog + */ + private createTimeLogEntity(input: Partial, tenantId: ID, timesheet: ITimesheet): TimeLog { + const { + startedAt, + stoppedAt, + employeeId, + organizationId, + projectId, + taskId, + organizationContactId, + organizationTeamId, + logType = TimeLogType.MANUAL, + description = null, + reason = null, + isBillable = false, + source = TimeLogSourceEnum.WEB_TIMER, + version = null, + isRunning = source === TimeLogSourceEnum.DESKTOP + } = input; + + console.log('create new time log with', { input, tenantId, timesheet }); + + return new TimeLog({ startedAt: moment.utc(startedAt).toDate(), - ...(input.stoppedAt - ? { stoppedAt: moment.utc(input.stoppedAt).toDate() } - : {}), + stoppedAt: stoppedAt ? moment.utc(stoppedAt).toDate() : undefined, timesheet, organizationId, tenantId, employeeId, - projectId: input.projectId || null, - taskId: input.taskId || null, - organizationContactId: input.organizationContactId || null, - organizationTeamId: input.organizationTeamId || null, - logType: input.logType || TimeLogType.MANUAL, - description: input.description || null, - reason: input.reason || null, - isBillable: input.isBillable || false, - source: input.source || TimeLogSourceEnum.WEB_TIMER, - version: input.version || null, - isRunning: input.isRunning || (input.source === TimeLogSourceEnum.DESKTOP) + projectId, + taskId, + organizationContactId, + organizationTeamId, + logType, + description, + reason, + isBillable, + source, + version, + isRunning }); + } - let timeSlots: ITimeSlot[] = []; - if (input.stoppedAt) { - timeSlots = this.timeSlotService.generateTimeSlots( - input.startedAt, - input.stoppedAt - ).map((slot: ITimeSlot) => ({ - ...slot, - employeeId, - organizationId, - tenantId, - keyboard: 0, - mouse: 0, - overall: 0 - })); - } - - if (input.timeSlots) { - /* - * Merge blank timeSlot if missing in request. - * I.e - * Time Logs is : 04:00:00 to 05:00:00 and pass time slots for 04:00:00, 04:20:00, 04:30:00, 04:40:00 - * then it will add 04:10:00, 04:50:00 as blank time slots in array to insert - */ - input.timeSlots = input.timeSlots.map((timeSlot: ITimeSlot) => ({ - ...timeSlot, - employeeId, - organizationId, - tenantId - })); - - timeSlots = timeSlots.map((blankTimeSlot) => { - let timeSlot = input.timeSlots.find((requestTimeSlot) => { - return moment(requestTimeSlot.startedAt).isSame(blankTimeSlot.startedAt); // true - }); - - timeSlot = timeSlot ? timeSlot : blankTimeSlot; - timeSlot.employeeId = input.employeeId; - return timeSlot; - }); - } + /** + * Generates blank time slots between startedAt and stoppedAt + * @param input Partial + * @param tenantId string + * @returns ITimeSlot[] + */ + private generateBlankTimeSlots(input: Partial, tenantId: ID): ITimeSlot[] { + const { startedAt, stoppedAt, employeeId, organizationId } = input; + + // Generate time slots between startedAt and stoppedAt + return this._timeSlotService.generateTimeSlots(startedAt, stoppedAt).map((slot) => ({ + ...slot, + employeeId, + organizationId, + tenantId, + keyboard: 0, + mouse: 0, + overall: 0 + })); + } - timeLog.timeSlots = await this.timeSlotService.bulkCreate( - timeSlots, + /** + * Merges input time slots with generated blank slots + * @param generatedSlots ITimeSlot[] + * @param inputSlots ITimeSlot[] + * @param employeeId ID + * @param organizationId ID + * @param tenantId ID + * @returns ITimeSlot[] + */ + private mergeTimeSlots( + generatedSlots: ITimeSlot[], + inputSlots: ITimeSlot[] = [], + employeeId: ID, + organizationId: ID, + tenantId: ID + ): ITimeSlot[] { + const standardizedInputSlots = inputSlots.map(slot => ({ + ...slot, employeeId, - organizationId - ); + organizationId, + tenantId + })); - await this.typeOrmTimeLogRepository.save(timeLog); + return generatedSlots.map(blankSlot => { + const matchingSlot = standardizedInputSlots.find(slot => + moment(slot.startedAt).isSame(blankSlot.startedAt) + ); + return matchingSlot ? { ...matchingSlot } : blankSlot; + }); + } - /** - * RECALCULATE timesheet activity - */ - if (timesheet) { - const { id: timesheetId } = timesheet; - await this.commandBus.execute( - new TimesheetRecalculateCommand(timesheetId) + /** + * Recalculates the timesheet activity + * @param timesheet ITimesheet + */ + private async recalculateTimesheet(timesheet: ITimesheet): Promise { + if (timesheet?.id) { + await this._commandBus.execute( + new TimesheetRecalculateCommand(timesheet.id) ); } + } - /** - * UPDATE employee total worked hours - */ - await this.commandBus.execute( + /** + * Updates total worked hours for the employee + * + * @param employeeId ID + */ + private async updateEmployeeTotalWorkedHours(employeeId: ID): Promise { + await this._commandBus.execute( new UpdateEmployeeTotalWorkedHoursCommand(employeeId) ); - - console.log('Newly created time log & request', { - timeLog, - input - }); - return await this.typeOrmTimeLogRepository.findOneBy({ - id: timeLog.id - }); } } diff --git a/packages/core/src/time-tracking/time-log/time-log.module.ts b/packages/core/src/time-tracking/time-log/time-log.module.ts index 432171d6c60..bde95297ce8 100644 --- a/packages/core/src/time-tracking/time-log/time-log.module.ts +++ b/packages/core/src/time-tracking/time-log/time-log.module.ts @@ -11,7 +11,7 @@ import { TimeLog } from './time-log.entity'; import { TimeLogController } from './time-log.controller'; import { TimeLogService } from './time-log.service'; import { TimeSlotModule } from './../time-slot/time-slot.module'; -import { TypeOrmTimeLogRepository } from './repository'; +import { TypeOrmTimeLogRepository } from './repository/type-orm-time-log.repository'; @Module({ controllers: [ diff --git a/packages/core/src/time-tracking/time-slot/time-slot.entity.ts b/packages/core/src/time-tracking/time-slot/time-slot.entity.ts index 1061e16cabd..a229b3e85dc 100644 --- a/packages/core/src/time-tracking/time-slot/time-slot.entity.ts +++ b/packages/core/src/time-tracking/time-slot/time-slot.entity.ts @@ -10,7 +10,8 @@ import { IActivity, IScreenshot, IEmployee, - ITimeLog + ITimeLog, + ID } from '@gauzy/contracts'; import { Activity, @@ -60,7 +61,9 @@ export class TimeSlot extends TenantOrganizationBaseEntity @MultiORMColumn() startedAt: Date; - /** Additional virtual columns */ + /** + * Additional virtual columns + */ @VirtualMultiOrmColumn() stoppedAt?: Date; @@ -72,16 +75,17 @@ export class TimeSlot extends TenantOrganizationBaseEntity @VirtualMultiOrmColumn() mousePercentage?: number; + /* |-------------------------------------------------------------------------- | @ManyToOne |-------------------------------------------------------------------------- */ - /** * Employee */ @MultiORMManyToOne(() => Employee, (it) => it.timeSlots, { + /** Database cascade action on delete. */ onDelete: 'CASCADE' }) employee?: IEmployee; @@ -92,7 +96,7 @@ export class TimeSlot extends TenantOrganizationBaseEntity @RelationId((it: TimeSlot) => it.employee) @ColumnIndex() @MultiORMColumn({ relationId: true }) - employeeId: IEmployee['id']; + employeeId: ID; /* |-------------------------------------------------------------------------- diff --git a/packages/core/src/time-tracking/time-slot/time-slot.service.ts b/packages/core/src/time-tracking/time-slot/time-slot.service.ts index a064d3d4902..f045ce3edf7 100644 --- a/packages/core/src/time-tracking/time-slot/time-slot.service.ts +++ b/packages/core/src/time-tracking/time-slot/time-slot.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { CommandBus } from '@nestjs/cqrs'; import { Brackets, SelectQueryBuilder, WhereExpressionBuilder } from 'typeorm'; -import { PermissionsEnum, IGetTimeSlotInput, ITimeSlot } from '@gauzy/contracts'; +import { PermissionsEnum, IGetTimeSlotInput,ID, ITimeSlot } from '@gauzy/contracts'; import { isEmpty, isNotEmpty } from '@gauzy/common'; import { TenantAwareCrudService } from './../../core/crud'; import { moment } from '../../core/moment-extend'; @@ -187,32 +187,37 @@ export class TimeSlotService extends TenantAwareCrudService { */ async bulkCreateOrUpdate( slots: ITimeSlot[], - employeeId: ITimeSlot['employeeId'], - organizationId: ITimeSlot['organizationId'] + employeeId: ID, + organizationId: ID ) { - return await this.commandBus.execute(new TimeSlotBulkCreateOrUpdateCommand(slots, employeeId, organizationId)); + return await this.commandBus.execute( + new TimeSlotBulkCreateOrUpdateCommand(slots, employeeId, organizationId) + ); } /** + * Bulk create time slots for a given employee and organization * - * @param slots - * @param employeeId - * @param organizationId - * @returns + * @param slots The array of time slots to be created + * @param employeeId The ID of the employee + * @param organizationId The ID of the organization + * @returns The result of the bulk creation command */ async bulkCreate( slots: ITimeSlot[], - employeeId: ITimeSlot['employeeId'], - organizationId: ITimeSlot['organizationId'] + employeeId: ID, + organizationId: ID ) { - return await this.commandBus.execute(new TimeSlotBulkCreateCommand(slots, employeeId, organizationId)); + return await this.commandBus.execute( + new TimeSlotBulkCreateCommand(slots, employeeId, organizationId) + ); } /** - * - * @param start - * @param end - * @returns + * Generates time slots between the start and end times at a given interval. + * @param start The start time of the range + * @param end The end time of the range + * @returns An array of generated time slots */ generateTimeSlots(start: Date, end: Date) { return generateTimeSlots(start, end); @@ -223,13 +228,17 @@ export class TimeSlotService extends TenantAwareCrudService { */ async createTimeSlotMinute(request: TimeSlotMinute) { // const { keyboard, mouse, datetime, timeSlot } = request; - return await this.commandBus.execute(new CreateTimeSlotMinutesCommand(request)); + return await this.commandBus.execute( + new CreateTimeSlotMinutesCommand(request) + ); } /* * Update TimeSlot minute activity for specific TimeSlot */ async updateTimeSlotMinute(id: string, request: TimeSlotMinute) { - return await this.commandBus.execute(new UpdateTimeSlotMinutesCommand(id, request)); + return await this.commandBus.execute( + new UpdateTimeSlotMinutesCommand(id, request) + ); } } diff --git a/packages/core/src/time-tracking/timer/timer.service.ts b/packages/core/src/time-tracking/timer/timer.service.ts index 93d7183e30d..d908857705c 100644 --- a/packages/core/src/time-tracking/timer/timer.service.ts +++ b/packages/core/src/time-tracking/timer/timer.service.ts @@ -1,4 +1,4 @@ -import { Injectable, NotFoundException, ForbiddenException, BadRequestException } from '@nestjs/common'; +import { Injectable, NotFoundException, ForbiddenException, NotAcceptableException } from '@nestjs/common'; import { CommandBus } from '@nestjs/cqrs'; import { IsNull, Between, Not, In } from 'typeorm'; import * as moment from 'moment'; @@ -229,6 +229,7 @@ export class TimerService { try { // Retrieve any existing running logs for the employee const logs = await this.getLastRunningLogs(); + console.log('Last Running Logs Count:', logs.length); // If there are existing running logs, stop them before starting a new one if (logs.length > 0) { @@ -300,13 +301,22 @@ export class TimerService { let lastLog = await this.getLastRunningLog(); if (!lastLog) { console.log('No running log found. Starting a new timer before stopping it.'); - lastLog = await this.startTimer(request); + throw new NotAcceptableException('No running log found. Starting a new timer before stopping it.'); } - const organizationId = employee.organizationId ?? lastLog.organizationId; - const stoppedAt = moment.utc(request.stoppedAt ?? moment.utc()).toDate(); + // Retrieve the employee ID and organization ID + const { id: employeeId, organizationId } = employee; + + // Get the lastLog + lastLog = await this.typeOrmTimeLogRepository.findOne({ + where: { id: lastLog.id, tenantId, organizationId, employeeId }, + relations: { timeSlots: true } + }); + + // Retrieve stoppedAt date or use current date if not provided + let stoppedAt = await this.calculateStoppedAt(request, lastLog); - // Validate the date range + // Validate the date range and check if the timer is running validateDateRange(lastLog.startedAt, stoppedAt); // Update the time log entry to mark it as stopped @@ -320,10 +330,22 @@ export class TimerService { request.manualTimeSlot ) ); - console.log('Stop Timer Time Log', { lastLog }); + + try { + // Retrieve any existing running logs for the employee + const logs = await this.getLastRunningLogs(); + console.log('Last Running Logs Count:', logs.length); + + // If there are existing running logs, stop them before starting a new one + if (logs.length > 0) { + await this.stopPreviousRunningTimers(employeeId, organizationId, tenantId); + } + } catch (error) { + console.error('Error while getting last running logs', error); + } // Update the employee's tracking status - await this._employeeService.update(employee.id, { + await this._employeeService.update(employeeId, { isOnline: false, // Employee status (Online/Offline) isTrackingTime: false // Employee time tracking status }); @@ -389,14 +411,81 @@ export class TimerService { } } + /** + * Calculates the stoppedAt time based on the last log and request parameters. + * It handles the case for DESKTOP source, considering time slots' durations. + * + * @param request - The input request containing timer toggle information + * @param lastLog - The last running time log for the employee + * @returns The calculated stoppedAt date + */ + async calculateStoppedAt(request: ITimerToggleInput, lastLog: ITimeLog): Promise { + // Retrieve stoppedAt date or default to the current date if not provided + let stoppedAt = moment.utc(request.stoppedAt ?? moment.utc()).toDate(); + + // Handle the DESKTOP source case + if (request.source === TimeLogSourceEnum.DESKTOP) { + // Retrieve the most recent time slot from the last log + const lastTimeSlot: ITimeSlot | undefined = lastLog.timeSlots?.sort((a: ITimeSlot, b: ITimeSlot) => + moment(a.startedAt).isBefore(b.startedAt) ? 1 : -1 + )[0]; + + // Example: + // If lastLog.timeSlots = [{ startedAt: "2024-09-24 19:50:00", duration: 600 }, { startedAt: "2024-09-24 19:40:00", duration: 600 }] + // The sorted result will be [{ startedAt: "2024-09-24 19:50:00", duration: 600 }, { startedAt: "2024-09-24 19:40:00", duration: 600 }] + // Hence, lastTimeSlot will be the one with startedAt = "2024-09-24 19:50:00". + + // Check if the last time slot was created more than 10 minutes ago + if (lastTimeSlot) { + // Retrieve the last time slot's startedAt date + const lastTimeSlotStartedAt = moment.utc(lastTimeSlot.startedAt); + + // Retrieve the last time slot's duration + const duration = lastTimeSlot.duration; + + // Example: + // If lastTimeSlotStartedAt = "2024-09-24 19:50:00" and duration = 600 (10 minutes) + // and the current time is "2024-09-24 20:10:00", the difference is 20 minutes, which is more than 10 minutes. + + // Check if the last time slot was created more than 10 minutes ago + if (moment.utc().diff(lastTimeSlotStartedAt, 'minutes') > 10) { + // Calculate the potential stoppedAt time using the total duration + stoppedAt = moment.utc(lastTimeSlot.startedAt).add(duration, 'seconds').toDate(); + // Example: stoppedAt = "2024-09-24 20:00:00" + } + } else { + // Retrieve the last log's startedAt date + const lastLogStartedAt = moment.utc(lastLog.startedAt); + + // Example: + // If lastLog.startedAt = "2024-09-24 19:30:00" and there are no time slots, + // and the current time is "2024-09-24 20:00:00", the difference is 30 minutes. + + // If no time slots exist and the difference is more than 10 minutes, adjust the stoppedAt + if (moment.utc().diff(lastLogStartedAt, 'minutes') > 10) { + stoppedAt = moment.utc(lastLog.startedAt).add(10, 'seconds').toDate(); + // Example: stoppedAt will be "2024-09-24 19:30:10" + } + } + } + + console.log('Last calculated stoppedAt: %s', stoppedAt); + // Example log output: "Last calculated stoppedAt: 2024-09-24 20:00:00" + return stoppedAt; + } + + /** * Toggle time tracking start/stop * - * @param request - * @returns + * @param request The timer toggle request input + * @returns The started or stopped TimeLog */ async toggleTimeLog(request: ITimerToggleInput): Promise { + // Retrieve the last running time log const lastLog = await this.getLastRunningLog(); + + // Start a new timer if no running log exists, otherwise stop the current timer if (!lastLog) { return this.startTimer(request); } else { @@ -486,7 +575,7 @@ export class TimerService { */ private async getLastRunningLog(): Promise { // Retrieve the last running log by using the `getRunningLogs` method with `fetchAll` set to false - const lastRunningLog = await this.getRunningLogs(); + const lastRunningLog = await this.getRunningLogs(false); // Ensure that the returned log is of type ITimeLog return lastRunningLog as ITimeLog; diff --git a/packages/core/src/time-tracking/timesheet/commands/timesheet-first-or-create.command.ts b/packages/core/src/time-tracking/timesheet/commands/timesheet-first-or-create.command.ts index 054ff414fa9..5381566c4d0 100644 --- a/packages/core/src/time-tracking/timesheet/commands/timesheet-first-or-create.command.ts +++ b/packages/core/src/time-tracking/timesheet/commands/timesheet-first-or-create.command.ts @@ -1,12 +1,12 @@ -import { ITimesheet } from '@gauzy/contracts'; import { ICommand } from '@nestjs/cqrs'; +import { ID } from '@gauzy/contracts'; export class TimesheetFirstOrCreateCommand implements ICommand { static readonly type = '[Timesheet] First Or Create'; constructor( public readonly date: Date, - public readonly employeeId: ITimesheet['employeeId'], - public readonly organizationId?: ITimesheet['organizationId'] + public readonly employeeId: ID, + public readonly organizationId?: ID ) { } }