diff --git a/docs/swagger/swagger-spec.json b/docs/swagger/swagger-spec.json index 430ea23..d35159e 100644 --- a/docs/swagger/swagger-spec.json +++ b/docs/swagger/swagger-spec.json @@ -468,6 +468,42 @@ ] } }, + "/timesheets/{email}": { + "get": { + "operationId": "TimesheetsController_getByEmail", + "parameters": [ + { + "name": "email", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "offset", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "security": [ + { + "access-token": [] + } + ], + "tags": [ + "Timesheets" + ] + } + }, "/timesheets/{id}": { "get": { "operationId": "TimesheetsController_findOne", diff --git a/src/common/utils/date-utils.ts b/src/common/utils/date-utils.ts index e383f98..5d85548 100644 --- a/src/common/utils/date-utils.ts +++ b/src/common/utils/date-utils.ts @@ -49,6 +49,13 @@ export function getYearStart(date:Date): Date { return new Date(date.getFullYear(),0,1,0,0,0,0); } +export function getCurrentWeek(): { start_date_week: Date; end_date_week: Date } { + const now = new Date(); + const start_date_week = getWeekStart(now, 0); + const end_date_week = getWeekEnd(start_date_week); + return { start_date_week, end_date_week }; +} + //cloning methods (helps with notify for overtime in a single day) // export function toDateOnly(day: Date): Date { // const d = new Date(day); diff --git a/src/modules/timesheets/controllers/timesheets.controller.ts b/src/modules/timesheets/controllers/timesheets.controller.ts index d575d9b..865f43d 100644 --- a/src/modules/timesheets/controllers/timesheets.controller.ts +++ b/src/modules/timesheets/controllers/timesheets.controller.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Body, Controller, Delete, Get, Param, ParseBoolPipe, ParseIntPipe, Patch, Post, Query, UseGuards, UsePipes, ValidationPipe } from '@nestjs/common'; +import { BadRequestException, Body, Controller, Delete, Get, Param, ParseBoolPipe, ParseIntPipe, Patch, Query } from '@nestjs/common'; import { TimesheetsQueryService } from '../services/timesheets-query.service'; import { CreateTimesheetDto } from '../dtos/create-timesheet.dto'; import { Timesheets } from '@prisma/client'; @@ -7,8 +7,8 @@ import { RolesAllowed } from "src/common/decorators/roles.decorators"; import { Roles as RoleEnum } from '.prisma/client'; import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { TimesheetsCommandService } from '../services/timesheets-command.service'; -import { SearchTimesheetDto } from '../dtos/search-timesheet.dto'; import { TimesheetPeriodDto } from '../dtos/timesheet-period.dto'; +import { TimesheetDto } from '../dtos/overview-timesheet.dto'; @ApiTags('Timesheets') @ApiBearerAuth('access-token') @@ -39,6 +39,15 @@ export class TimesheetsController { if(!email || !(email = email.trim())) throw new BadRequestException('Query param "email" is mandatory for this route.'); return this.timesheetsQuery.findAll(year, period_no, email); } + + @Get('/:email') + async getByEmail( + @Param('email') email: string, + @Query('offset') offset?: string, + ): Promise { + const week_offset = Number.isFinite(Number(offset)) ? Number(offset) : 0; + return this.timesheetsQuery.getTimesheetByEmail(email, week_offset); + } @Get(':id') //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR) diff --git a/src/modules/timesheets/dtos/overview-timesheet.dto.ts b/src/modules/timesheets/dtos/overview-timesheet.dto.ts new file mode 100644 index 0000000..956a7a4 --- /dev/null +++ b/src/modules/timesheets/dtos/overview-timesheet.dto.ts @@ -0,0 +1,27 @@ +export class TimesheetDto { + is_approved: boolean; + start_day: string; + end_day: string; + label: string; + shifts: ShiftsDto[]; + expenses: ExpensesDto[] +} + +export class ShiftsDto { + bank_type: string; + date: string; + start_time: string; + end_time: string; + description: string; + is_approved: boolean; +} + +export class ExpensesDto { + bank_type: string; + date: string; + amount: number; + km: number; + description: string; + supervisor_comment: string; + is_approved: boolean; +} \ No newline at end of file diff --git a/src/modules/timesheets/services/timesheets-query.service.ts b/src/modules/timesheets/services/timesheets-query.service.ts index c28c8fd..5937dbc 100644 --- a/src/modules/timesheets/services/timesheets-query.service.ts +++ b/src/modules/timesheets/services/timesheets-query.service.ts @@ -1,13 +1,13 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { PrismaService } from 'src/prisma/prisma.service'; -import { CreateTimesheetDto } from '../dtos/create-timesheet.dto'; import { Timesheets, TimesheetsArchive } from '@prisma/client'; import { UpdateTimesheetDto } from '../dtos/update-timesheet.dto'; import { OvertimeService } from 'src/modules/business-logics/services/overtime.service'; -import { computeHours } from 'src/common/utils/date-utils'; +import { computeHours, formatDateISO, getCurrentWeek, getWeekEnd, getWeekStart } from 'src/common/utils/date-utils'; import { TimesheetPeriodDto } from '../dtos/timesheet-period.dto'; import { buildPeriod, endOfDayUTC, toUTCDateOnly } from '../utils/timesheet.helpers'; import type { ShiftRow, ExpenseRow } from '../utils/timesheet.helpers'; +import { TimesheetDto } from '../dtos/overview-timesheet.dto'; @Injectable() @@ -98,6 +98,102 @@ export class TimesheetsQueryService { return buildPeriod(period.period_start, period.period_end, shifts , expenses); } + async getTimesheetByEmail(email: string, week_offset = 0): Promise { + + //fetch user related to email + const user = await this.prisma.users.findUnique({ + where: { email }, + select: { id: true }, + }); + if(!user) throw new NotFoundException(`user with email ${email} not found`); + + //fetch employee_id matching the email + const employee = await this.prisma.employees.findFirst({ + where: { user_id: user.id }, + select: { id: true }, + }); + if(!employee) throw new NotFoundException(`Employee with email: ${email} not found`); + + //sets current week Sunday -> Saturday + const base = new Date(); + const offset = new Date(base); + offset.setDate(offset.getDate() + (week_offset * 7)); + + const start_date_week = getWeekStart(offset, 0); + const end_date_week = getWeekEnd(start_date_week); + const start_day = formatDateISO(start_date_week); + const end_day = formatDateISO(end_date_week); + + //build the label MM/DD/YYYY.MM/DD.YYYY + const mm_dd = (date: Date) => `${String(date.getFullYear())}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2,'0')}`; + const label = `${mm_dd(start_date_week)}.${mm_dd(end_date_week)}`; + + //fetch timesheet shifts and expenses + const timesheet = await this.prisma.timesheets.findUnique({ + where: { + employee_id_start_date: { + employee_id: employee.id, + start_date: start_date_week, + }, + }, + include: { + shift: { + include: { bank_code: true }, + orderBy: [{ date: 'asc'}, { start_time: 'asc'}], + }, + expense: { + include: { bank_code: true }, + orderBy: [{date: 'asc'}], + }, + }, + }); + + //returns an empty timesheet if not found + if(!timesheet) { + return { + is_approved: false, + start_day, + end_day, + label, + shifts:[], + expenses: [], + } as TimesheetDto; + } + + //small helper to format hours:minutes + const to_HH_mm = (date: Date) => date.toISOString().slice(11, 16); + + //maps all shifts of selected timesheet + const shifts = timesheet.shift.map((sft) => ({ + bank_type: sft.bank_code?.type ?? '', + date: formatDateISO(sft.date), + start_time: to_HH_mm(sft.start_time), + end_time: to_HH_mm(sft.end_time), + description: sft.description ?? '', + is_approved: sft.is_approved ?? false, + })); + + //maps all expenses of selected timsheet + const expenses = timesheet.expense.map((exp) => ({ + bank_type: exp.bank_code?.type ?? '', + date: formatDateISO(exp.date), + amount: Number(exp.amount) || 0, + km: 0, + description: exp.description ?? '', + supervisor_comment: exp.supervisor_comment ?? '', + is_approved: exp.is_approved ?? false, + })); + + return { + is_approved: timesheet.is_approved, + start_day, + end_day, + label, + shifts, + expenses, + } as TimesheetDto; + } + async findOne(id: number): Promise { const timesheet = await this.prisma.timesheets.findUnique({ where: { id },