feat(timesheet): added getTimesheetByEmail

This commit is contained in:
Matthieu Haineault 2025-09-02 14:29:00 -04:00
parent c170481f3b
commit 93cf2d571b
5 changed files with 179 additions and 4 deletions

View File

@ -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",

View File

@ -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);

View File

@ -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<TimesheetDto> {
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)

View File

@ -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;
}

View File

@ -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<TimesheetDto> {
//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<any> {
const timesheet = await this.prisma.timesheets.findUnique({
where: { id },