diff --git a/docs/swagger/swagger-spec.json b/docs/swagger/swagger-spec.json index 0ecfa52..7e75002 100644 --- a/docs/swagger/swagger-spec.json +++ b/docs/swagger/swagger-spec.json @@ -248,10 +248,27 @@ ] } }, - "/timesheets": { + "/timesheets/{year}/{period_number}": { "get": { "operationId": "TimesheetController_getTimesheetByPayPeriod", - "parameters": [], + "parameters": [ + { + "name": "year", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "period_number", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], "responses": { "200": { "description": "" diff --git a/src/identity-and-account/employees/dtos/create-employee.dto.ts b/src/identity-and-account/employees/dtos/create-employee.dto.ts index 89279ef..6b717ee 100644 --- a/src/identity-and-account/employees/dtos/create-employee.dto.ts +++ b/src/identity-and-account/employees/dtos/create-employee.dto.ts @@ -12,6 +12,7 @@ import { } from 'class-validator'; import { Type } from 'class-transformer'; import { ApiProperty } from '@nestjs/swagger'; +import { UserDto } from 'src/identity-and-account/users-management/dtos/user.dto'; export class CreateEmployeeDto { @ApiProperty({ @@ -115,4 +116,6 @@ export class CreateEmployeeDto { @IsDateString() @IsOptional() last_work_day?: string; + + user?: UserDto; } diff --git a/src/modules/bank-codes/dtos/bank-code-entity.ts b/src/modules/bank-codes/dtos/bank-code-entity.ts new file mode 100644 index 0000000..95e7756 --- /dev/null +++ b/src/modules/bank-codes/dtos/bank-code-entity.ts @@ -0,0 +1,7 @@ +export class BankCodeEntity { + id: number; + type: string; + categorie: string; + modifier: number; + bank_code: string; +} \ No newline at end of file diff --git a/src/time-and-attendance/expenses/dtos/expense-entity.dto.ts b/src/time-and-attendance/expenses/dtos/expense-entity.dto.ts index f04c989..3347a3c 100644 --- a/src/time-and-attendance/expenses/dtos/expense-entity.dto.ts +++ b/src/time-and-attendance/expenses/dtos/expense-entity.dto.ts @@ -1,13 +1,14 @@ +import { Prisma } from "@prisma/client"; export class ExpenseEntity { id: number; timesheet_id: number; bank_code_id: number; - attachment?:number; + attachment?:number | null; date: Date; - amount?: number; - mileage?:number; + amount?: number | Prisma.Decimal | null; + mileage?:number | Prisma.Decimal | null; comment: string; - supervisor_comment?:string; + supervisor_comment?:string | null; is_approved: boolean; } \ No newline at end of file diff --git a/src/time-and-attendance/time-tracker/shifts/dtos/shift-entity.dto.ts b/src/time-and-attendance/time-tracker/shifts/dtos/shift-entity.dto.ts index f2b833f..f5993c5 100644 --- a/src/time-and-attendance/time-tracker/shifts/dtos/shift-entity.dto.ts +++ b/src/time-and-attendance/time-tracker/shifts/dtos/shift-entity.dto.ts @@ -1,3 +1,5 @@ +import { BankCodeEntity } from "src/modules/bank-codes/dtos/bank-code-entity"; + export class ShiftEntity { id: number; timesheet_id: number; @@ -7,5 +9,6 @@ export class ShiftEntity { end_time: Date; is_remote: boolean; is_approved: boolean; - comment?: string; + comment?: string | null ; + bank_code?: BankCodeEntity; } diff --git a/src/time-and-attendance/time-tracker/timesheets/controllers/timesheet.controller.ts b/src/time-and-attendance/time-tracker/timesheets/controllers/timesheet.controller.ts index 886bb4a..ea9b181 100644 --- a/src/time-and-attendance/time-tracker/timesheets/controllers/timesheet.controller.ts +++ b/src/time-and-attendance/time-tracker/timesheets/controllers/timesheet.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Get, ParseBoolPipe, ParseIntPipe, Patch, Req, UnauthorizedException } from "@nestjs/common"; +import { Body, Controller, Get, Param, ParseBoolPipe, ParseIntPipe, Patch, Req, UnauthorizedException } from "@nestjs/common"; import { RolesAllowed } from "src/common/decorators/roles.decorators"; import { GetTimesheetsOverviewService } from "src/time-and-attendance/time-tracker/timesheets/services/timesheet-get-overview.service"; import { TimesheetApprovalService } from "src/time-and-attendance/time-tracker/timesheets/services/timesheet-approval.service"; @@ -13,11 +13,11 @@ export class TimesheetController { private readonly approvalService: TimesheetApprovalService, ) { } - @Get() + @Get(':year/:period_number') getTimesheetByPayPeriod( @Req() req, - @Body('year', ParseIntPipe) year: number, - @Body('period_number', ParseIntPipe) period_number: number + @Param('year', ParseIntPipe) year: number, + @Param('period_number', ParseIntPipe) period_number: number ) { const email = req.user?.email; if (!email) throw new UnauthorizedException('Unauthorized User'); diff --git a/src/time-and-attendance/time-tracker/timesheets/dtos/timesheet.dto.ts b/src/time-and-attendance/time-tracker/timesheets/dtos/timesheet.dto.ts index 7e72aac..d6b480f 100644 --- a/src/time-and-attendance/time-tracker/timesheets/dtos/timesheet.dto.ts +++ b/src/time-and-attendance/time-tracker/timesheets/dtos/timesheet.dto.ts @@ -1,3 +1,10 @@ +export class TimesheetEntity { + id: number; + employee_id: number; + start_date: Date; + is_approved: boolean; +} + export class Timesheets { employee_fullname: string; timesheets: Timesheet[]; @@ -50,6 +57,7 @@ export class Shift { export class Expense { date: string; is_approved: boolean; + type: string; comment: string; amount?: number; mileage?: number; diff --git a/src/time-and-attendance/time-tracker/timesheets/services/timesheet-get-overview.service.ts b/src/time-and-attendance/time-tracker/timesheets/services/timesheet-get-overview.service.ts index abe99c5..6a603c5 100644 --- a/src/time-and-attendance/time-tracker/timesheets/services/timesheet-get-overview.service.ts +++ b/src/time-and-attendance/time-tracker/timesheets/services/timesheet-get-overview.service.ts @@ -3,8 +3,14 @@ import { NUMBER_OF_TIMESHEETS_TO_RETURN } from "src/time-and-attendance/utils/co import { Injectable } from "@nestjs/common"; import { PrismaService } from "src/prisma/prisma.service"; import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; -import { Timesheets } from "src/time-and-attendance/time-tracker/timesheets/dtos/timesheet.dto"; +import { Timesheet, TimesheetEntity, Timesheets } from "src/time-and-attendance/time-tracker/timesheets/dtos/timesheet.dto"; import { Result } from "src/common/errors/result-error.factory"; +import { Users } from "@prisma/client"; +import { ShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-create.dto"; +import { ExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-create.dto"; +import { ShiftEntity } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-entity.dto"; +import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils"; +import { ExpenseEntity } from "src/time-and-attendance/expenses/dtos/expense-entity.dto"; export type TotalHours = { regular: number; @@ -28,6 +34,7 @@ export class GetTimesheetsOverviewService { constructor( private readonly prisma: PrismaService, private readonly emailResolver: EmailToIdResolver, + private readonly typeResolver: BankCodesResolver, ) { } //----------------------------------------------------------------------------------- @@ -74,9 +81,11 @@ export class GetTimesheetsOverviewService { //builds employee full name const user = employee.user; const employee_fullname = `${user.first_name} ${user.last_name}`.trim(); + //maps all timesheet's infos const timesheets = await Promise.all(rows.map((timesheet) => this.mapOneTimesheet(timesheet))); + if(!timesheets) return { success: false, error: 'an error occured during the mapping of a timesheet'} return { success: true, data: { employee_fullname, timesheets } }; } catch (error) { @@ -87,127 +96,125 @@ export class GetTimesheetsOverviewService { //----------------------------------------------------------------------------------- // MAPPERS & HELPERS //----------------------------------------------------------------------------------- -// const timesheet_range = { employee_id: employee_id.data, start_date: { gte: period.period_start, lte: period.period_end } }; //fetch timesheet's infos private async loadTimesheets(employee_id: number, period_start: Date, period_end: Date) { return this.prisma.timesheets.findMany({ - where: { employee_id , start_date: { gte: period_start, lte: period_end } }, + where: { employee_id, start_date: { gte: period_start, lte: period_end } }, include: { employee: { include: { user: true } }, shift: { include: { bank_code: true } }, - expense: { include: { bank_code: true, attachment_record: true } }, + expense: { include: { bank_code: true } }, }, orderBy: { start_date: 'asc' }, }); } - private mapOneTimesheet(timesheet: any) { - //converts string to UTC date format - const start = toDateFromString(timesheet.start_date); - const day_dates = sevenDaysFrom(start); + private async mapOneTimesheet(timesheet: TimesheetResult): Promise { + //converts string to UTC date format + const start = toDateFromString(timesheet.start_date); + const day_dates = sevenDaysFrom(start); - //map of shifts by days - const shifts_by_date = new Map(); - for (const shift of timesheet.shift) { - const date = toStringFromDate(shift.date); - const arr = shifts_by_date.get(date) ?? []; - arr.push(shift); - shifts_by_date.set(date, arr); - } - //map of expenses by days - const expenses_by_date = new Map(); - for (const expense of timesheet.expense) { - const date = toStringFromDate(expense.date); - const arr = expenses_by_date.get(date) ?? []; - arr.push(expense); - expenses_by_date.set(date, arr); - } - //weekly totals - const weekly_hours: TotalHours[] = [emptyHours()]; - const weekly_expenses: TotalExpenses[] = [emptyExpenses()]; - - - - //map of days - const days = day_dates.map((date) => { - const date_iso = toStringFromDate(date); - const shifts_source = shifts_by_date.get(date_iso) ?? []; - const expenses_source = expenses_by_date.get(date_iso) ?? []; - - //inner map of shifts - const shifts = shifts_source.map((shift) => ({ - timesheet_id: shift.timesheet_id, - date: toStringFromDate(shift.date), - start_time: toHHmmFromDate(shift.start_time), - end_time: toHHmmFromDate(shift.end_time), - type: shift.bank_code?.type ?? '', - is_remote: shift.is_remote ?? false, - is_approved: shift.is_approved ?? false, - id: shift.id ?? null, - comment: shift.comment ?? null, - })); - - //inner map of expenses - const expenses = expenses_source.map((expense) => ({ - date: toStringFromDate(expense.date), - amount: expense.amount ? Number(expense.amount) : undefined, - mileage: expense.mileage ? Number(expense.mileage) : undefined, - expense_id: expense.id ?? null, - attachment: expense.attachment_record ? String(expense.attachment_record.id) : undefined, - is_approved: expense.is_approved ?? false, - comment: expense.comment ?? '', - supervisor_comment: expense.supervisor_comment, - type: expense.type, - })); - - //daily totals - const daily_hours = [emptyHours()]; - const daily_expenses = [emptyExpenses()]; - - //totals by shift types - for (const shift of shifts_source) { - const hours = diffOfHours(shift.start_time, shift.end_time); - const subgroup = hoursSubGroupFromBankCode(shift.bank_code); - daily_hours[0][subgroup] += hours; - weekly_hours[0][subgroup] += hours; + //map of shifts by days + const shifts_by_date = new Map(); + for (const shift of timesheet.shift) { + const date_string = toStringFromDate(shift.date); + const arr = shifts_by_date.get(date_string) ?? []; + arr.push(shift); + shifts_by_date.set(date_string, arr); } + //map of expenses by days + const expenses_by_date = new Map(); + for (const expense of timesheet.expense) { + const date_string = toStringFromDate(expense.date); + const arr = expenses_by_date.get(date_string) ?? []; + arr.push(expense); + expenses_by_date.set(date_string, arr); + } + //weekly totals + const weekly_hours: TotalHours[] = [emptyHours()]; + const weekly_expenses: TotalExpenses[] = [emptyExpenses()]; - //totals by expense types - for (const expense of expenses_source) { - const subgroup = expenseSubgroupFromBankCode(expense.bank_code); - if (subgroup === 'mileage') { - const mileage = num(expense.mileage); - daily_expenses[0].mileage += mileage; - weekly_expenses[0].mileage += mileage; - } else if (subgroup === 'per_diem') { - const amount = num(expense.amount); - daily_expenses[0].per_diem += amount; - weekly_expenses[0].per_diem += amount; - } else if (subgroup === 'on_call') { - const amount = num(expense.amount); - daily_expenses[0].on_call += amount; - weekly_expenses[0].on_call += amount; - } else { - const amount = num(expense.amount); - daily_expenses[0].expenses += amount; - weekly_expenses[0].expenses += amount; + //map of days + const days = day_dates.map((date) => { + const date_iso = toStringFromDate(date); + const shifts_source = shifts_by_date.get(date_iso) ?? []; + const expenses_source = expenses_by_date.get(date_iso) ?? []; + + //inner map of shifts + const shifts = shifts_source.map((shift) => ({ + timesheet_id: shift.timesheet_id, + date: toStringFromDate(shift.date), + start_time: toHHmmFromDate(shift.start_time), + end_time: toHHmmFromDate(shift.end_time), + type: shift.bank_code?.type ?? '', + is_remote: shift.is_remote ?? false, + is_approved: shift.is_approved ?? false, + id: shift.id ?? null, + comment: shift.comment ?? null, + })); + + //inner map of expenses + const expenses = expenses_source.map((expense) => ({ + date: toStringFromDate(expense.date), + amount: expense.amount != null ? Number(expense.amount) : undefined, + mileage: expense.mileage != null ? Number(expense.mileage) : undefined, + expense_id: expense.id ?? null, + attachment: expense.attachment_record ? String(expense.attachment_record.id) : undefined, + is_approved: expense.is_approved ?? false, + comment: expense.comment ?? '', + supervisor_comment: expense.supervisor_comment, + type: expense.type, + })); + + //daily totals + const daily_hours = [emptyHours()]; + const daily_expenses = [emptyExpenses()]; + + //totals by shift types + for (const shift of shifts_source) { + const hours = diffOfHours(shift.start_time, shift.end_time); + const subgroup = hoursSubGroupFromBankCode(shift.bank_code); + daily_hours[0][subgroup] += hours; + weekly_hours[0][subgroup] += hours; } - } + + //totals by expense types + for (const expense of expenses_source) { + const subgroup = expenseSubgroupFromBankCode(expense.bank_code); + if (subgroup === 'mileage') { + const mileage = num(expense.mileage); + daily_expenses[0].mileage += mileage; + weekly_expenses[0].mileage += mileage; + } else if (subgroup === 'per_diem') { + const amount = num(expense.amount); + daily_expenses[0].per_diem += amount; + weekly_expenses[0].per_diem += amount; + } else if (subgroup === 'on_call') { + const amount = num(expense.amount); + daily_expenses[0].on_call += amount; + weekly_expenses[0].on_call += amount; + } else { + const amount = num(expense.amount); + daily_expenses[0].expenses += amount; + weekly_expenses[0].expenses += amount; + } + } + return { + date: date_iso, + shifts, + expenses, + daily_hours, + daily_expenses, + }; + }); + return { - date: date_iso, - shifts, - expenses, - daily_hours, - daily_expenses, + timesheet_id: timesheet.id, + is_approved: timesheet.is_approved ?? false, + days, + weekly_hours, + weekly_expenses, }; - }); - return { - timesheet_id: timesheet.id, - is_approved: timesheet.is_approved ?? false, - days, - weekly_hours, - weekly_expenses, - }; } private ensureTimesheet = async (employee_id: number, start_date: Date | string) => { @@ -236,13 +243,21 @@ export class GetTimesheetsOverviewService { include: { employee: { include: { user: true } }, shift: { include: { bank_code: true } }, - expense: { include: { bank_code: true, attachment_record: true } }, + expense: { include: { bank_code: true, attachment_record: true, } }, }, }); return row!; } } +interface TimesheetResult extends TimesheetEntity { + employee: { + user: Users + }, + shift: ShiftEntity[], + expense: ExpenseEntity[], +} + //filled array with default values const emptyHours = (): TotalHours => { return { regular: 0, evening: 0, emergency: 0, overtime: 0, vacation: 0, holiday: 0, sick: 0 } }; const emptyExpenses = (): TotalExpenses => { return { expenses: 0, per_diem: 0, on_call: 0, mileage: 0 } };