fix(timesheets): added type to expense in return

This commit is contained in:
Matthieu Haineault 2025-11-12 13:30:51 -05:00
parent 73a2a755e4
commit e067e15bb1
8 changed files with 171 additions and 117 deletions

View File

@ -248,10 +248,27 @@
] ]
} }
}, },
"/timesheets": { "/timesheets/{year}/{period_number}": {
"get": { "get": {
"operationId": "TimesheetController_getTimesheetByPayPeriod", "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": { "responses": {
"200": { "200": {
"description": "" "description": ""

View File

@ -12,6 +12,7 @@ import {
} from 'class-validator'; } from 'class-validator';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { UserDto } from 'src/identity-and-account/users-management/dtos/user.dto';
export class CreateEmployeeDto { export class CreateEmployeeDto {
@ApiProperty({ @ApiProperty({
@ -115,4 +116,6 @@ export class CreateEmployeeDto {
@IsDateString() @IsDateString()
@IsOptional() @IsOptional()
last_work_day?: string; last_work_day?: string;
user?: UserDto;
} }

View File

@ -0,0 +1,7 @@
export class BankCodeEntity {
id: number;
type: string;
categorie: string;
modifier: number;
bank_code: string;
}

View File

@ -1,13 +1,14 @@
import { Prisma } from "@prisma/client";
export class ExpenseEntity { export class ExpenseEntity {
id: number; id: number;
timesheet_id: number; timesheet_id: number;
bank_code_id: number; bank_code_id: number;
attachment?:number; attachment?:number | null;
date: Date; date: Date;
amount?: number; amount?: number | Prisma.Decimal | null;
mileage?:number; mileage?:number | Prisma.Decimal | null;
comment: string; comment: string;
supervisor_comment?:string; supervisor_comment?:string | null;
is_approved: boolean; is_approved: boolean;
} }

View File

@ -1,3 +1,5 @@
import { BankCodeEntity } from "src/modules/bank-codes/dtos/bank-code-entity";
export class ShiftEntity { export class ShiftEntity {
id: number; id: number;
timesheet_id: number; timesheet_id: number;
@ -7,5 +9,6 @@ export class ShiftEntity {
end_time: Date; end_time: Date;
is_remote: boolean; is_remote: boolean;
is_approved: boolean; is_approved: boolean;
comment?: string; comment?: string | null ;
bank_code?: BankCodeEntity;
} }

View File

@ -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 { RolesAllowed } from "src/common/decorators/roles.decorators";
import { GetTimesheetsOverviewService } from "src/time-and-attendance/time-tracker/timesheets/services/timesheet-get-overview.service"; 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"; 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, private readonly approvalService: TimesheetApprovalService,
) { } ) { }
@Get() @Get(':year/:period_number')
getTimesheetByPayPeriod( getTimesheetByPayPeriod(
@Req() req, @Req() req,
@Body('year', ParseIntPipe) year: number, @Param('year', ParseIntPipe) year: number,
@Body('period_number', ParseIntPipe) period_number: number @Param('period_number', ParseIntPipe) period_number: number
) { ) {
const email = req.user?.email; const email = req.user?.email;
if (!email) throw new UnauthorizedException('Unauthorized User'); if (!email) throw new UnauthorizedException('Unauthorized User');

View File

@ -1,3 +1,10 @@
export class TimesheetEntity {
id: number;
employee_id: number;
start_date: Date;
is_approved: boolean;
}
export class Timesheets { export class Timesheets {
employee_fullname: string; employee_fullname: string;
timesheets: Timesheet[]; timesheets: Timesheet[];
@ -50,6 +57,7 @@ export class Shift {
export class Expense { export class Expense {
date: string; date: string;
is_approved: boolean; is_approved: boolean;
type: string;
comment: string; comment: string;
amount?: number; amount?: number;
mileage?: number; mileage?: number;

View File

@ -3,8 +3,14 @@ import { NUMBER_OF_TIMESHEETS_TO_RETURN } from "src/time-and-attendance/utils/co
import { Injectable } from "@nestjs/common"; import { Injectable } from "@nestjs/common";
import { PrismaService } from "src/prisma/prisma.service"; import { PrismaService } from "src/prisma/prisma.service";
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; 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 { 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 = { export type TotalHours = {
regular: number; regular: number;
@ -28,6 +34,7 @@ export class GetTimesheetsOverviewService {
constructor( constructor(
private readonly prisma: PrismaService, private readonly prisma: PrismaService,
private readonly emailResolver: EmailToIdResolver, private readonly emailResolver: EmailToIdResolver,
private readonly typeResolver: BankCodesResolver,
) { } ) { }
//----------------------------------------------------------------------------------- //-----------------------------------------------------------------------------------
@ -75,8 +82,10 @@ export class GetTimesheetsOverviewService {
const user = employee.user; const user = employee.user;
const employee_fullname = `${user.first_name} ${user.last_name}`.trim(); const employee_fullname = `${user.first_name} ${user.last_name}`.trim();
//maps all timesheet's infos //maps all timesheet's infos
const timesheets = await Promise.all(rows.map((timesheet) => this.mapOneTimesheet(timesheet))); 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 } }; return { success: true, data: { employee_fullname, timesheets } };
} catch (error) { } catch (error) {
@ -87,127 +96,125 @@ export class GetTimesheetsOverviewService {
//----------------------------------------------------------------------------------- //-----------------------------------------------------------------------------------
// MAPPERS & HELPERS // MAPPERS & HELPERS
//----------------------------------------------------------------------------------- //-----------------------------------------------------------------------------------
// const timesheet_range = { employee_id: employee_id.data, start_date: { gte: period.period_start, lte: period.period_end } };
//fetch timesheet's infos //fetch timesheet's infos
private async loadTimesheets(employee_id: number, period_start: Date, period_end: Date) { private async loadTimesheets(employee_id: number, period_start: Date, period_end: Date) {
return this.prisma.timesheets.findMany({ 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: { include: {
employee: { include: { user: true } }, employee: { include: { user: true } },
shift: { include: { bank_code: true } }, shift: { include: { bank_code: true } },
expense: { include: { bank_code: true, attachment_record: true } }, expense: { include: { bank_code: true } },
}, },
orderBy: { start_date: 'asc' }, orderBy: { start_date: 'asc' },
}); });
} }
private mapOneTimesheet(timesheet: any) { private async mapOneTimesheet(timesheet: TimesheetResult): Promise<Timesheet> {
//converts string to UTC date format //converts string to UTC date format
const start = toDateFromString(timesheet.start_date); const start = toDateFromString(timesheet.start_date);
const day_dates = sevenDaysFrom(start); const day_dates = sevenDaysFrom(start);
//map of shifts by days //map of shifts by days
const shifts_by_date = new Map<string, any[]>(); const shifts_by_date = new Map<string, any[]>();
for (const shift of timesheet.shift) { for (const shift of timesheet.shift) {
const date = toStringFromDate(shift.date); const date_string = toStringFromDate(shift.date);
const arr = shifts_by_date.get(date) ?? []; const arr = shifts_by_date.get(date_string) ?? [];
arr.push(shift); arr.push(shift);
shifts_by_date.set(date, arr); shifts_by_date.set(date_string, arr);
}
//map of expenses by days
const expenses_by_date = new Map<string, any[]>();
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 expenses by days
const expenses_by_date = new Map<string, any[]>();
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 //map of days
for (const expense of expenses_source) { const days = day_dates.map((date) => {
const subgroup = expenseSubgroupFromBankCode(expense.bank_code); const date_iso = toStringFromDate(date);
if (subgroup === 'mileage') { const shifts_source = shifts_by_date.get(date_iso) ?? [];
const mileage = num(expense.mileage); const expenses_source = expenses_by_date.get(date_iso) ?? [];
daily_expenses[0].mileage += mileage;
weekly_expenses[0].mileage += mileage; //inner map of shifts
} else if (subgroup === 'per_diem') { const shifts = shifts_source.map((shift) => ({
const amount = num(expense.amount); timesheet_id: shift.timesheet_id,
daily_expenses[0].per_diem += amount; date: toStringFromDate(shift.date),
weekly_expenses[0].per_diem += amount; start_time: toHHmmFromDate(shift.start_time),
} else if (subgroup === 'on_call') { end_time: toHHmmFromDate(shift.end_time),
const amount = num(expense.amount); type: shift.bank_code?.type ?? '',
daily_expenses[0].on_call += amount; is_remote: shift.is_remote ?? false,
weekly_expenses[0].on_call += amount; is_approved: shift.is_approved ?? false,
} else { id: shift.id ?? null,
const amount = num(expense.amount); comment: shift.comment ?? null,
daily_expenses[0].expenses += amount; }));
weekly_expenses[0].expenses += amount;
//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 { return {
date: date_iso, timesheet_id: timesheet.id,
shifts, is_approved: timesheet.is_approved ?? false,
expenses, days,
daily_hours, weekly_hours,
daily_expenses, 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) => { private ensureTimesheet = async (employee_id: number, start_date: Date | string) => {
@ -236,13 +243,21 @@ export class GetTimesheetsOverviewService {
include: { include: {
employee: { include: { user: true } }, employee: { include: { user: true } },
shift: { include: { bank_code: true } }, shift: { include: { bank_code: true } },
expense: { include: { bank_code: true, attachment_record: true } }, expense: { include: { bank_code: true, attachment_record: true, } },
}, },
}); });
return row!; return row!;
} }
} }
interface TimesheetResult extends TimesheetEntity {
employee: {
user: Users
},
shift: ShiftEntity[],
expense: ExpenseEntity[],
}
//filled array with default values //filled array with default values
const emptyHours = (): TotalHours => { return { regular: 0, evening: 0, emergency: 0, overtime: 0, vacation: 0, holiday: 0, sick: 0 } }; 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 } }; const emptyExpenses = (): TotalExpenses => { return { expenses: 0, per_diem: 0, on_call: 0, mileage: 0 } };