feat(modules): added pay-periods view module with functions to navigate and search by date

This commit is contained in:
Matthieu Haineault 2025-07-25 15:33:39 -04:00
parent 49f99a6b9c
commit 4d538fc78a
10 changed files with 284 additions and 1 deletions

View File

@ -66,6 +66,17 @@ model LeaveRequests {
@@map("leave_requests") @@map("leave_requests")
} }
//pay-period vue
model PayPeriods {
period_number Int @id
start_date DateTime
end_date DateTime
year Int
label String
@@map("pay_period")
}
model Timesheets { model Timesheets {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
employee Employees @relation("TimesheetEmployee", fields: [employee_id], references: [id]) employee Employees @relation("TimesheetEmployee", fields: [employee_id], references: [id])

View File

@ -15,6 +15,7 @@ import { TimesheetsModule } from './modules/timesheets/timesheets.module';
import { AuthenticationModule } from './modules/authentication/auth.module'; import { AuthenticationModule } from './modules/authentication/auth.module';
import { ExpensesModule } from './modules/expenses/expenses.module'; import { ExpensesModule } from './modules/expenses/expenses.module';
import { ExpenseCodesModule } from './modules/expense-codes/expense-codes.module'; import { ExpenseCodesModule } from './modules/expense-codes/expense-codes.module';
import { PayperiodModule } from './modules/pay-periods/pay-periods.module';
@Module({ @Module({
imports: [ imports: [
@ -31,6 +32,7 @@ import { ExpenseCodesModule } from './modules/expense-codes/expense-codes.module
ShiftsModule, ShiftsModule,
TimesheetsModule, TimesheetsModule,
AuthenticationModule, AuthenticationModule,
PayperiodModule,
], ],
controllers: [AppController, HealthController], controllers: [AppController, HealthController],
providers: [AppService], providers: [AppService],

View File

@ -29,7 +29,7 @@ export class LeaveRequestController {
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR) @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
@ApiOperation({summary: 'Find all leave request' }) @ApiOperation({summary: 'Find all leave request' })
@ApiResponse({ status: 201, description: 'List of Leave requests found',type: LeaveRequestEntity, isArray: true }) @ApiResponse({ status: 201, description: 'List of Leave requests found',type: LeaveRequestEntity, isArray: true })
@ApiResponse({ status: 400, description: 'Leave request not found' }) @ApiResponse({ status: 400, description: 'List of leave request not found' })
findAll(): Promise<LeaveRequests[]> { findAll(): Promise<LeaveRequests[]> {
return this.leaveRequetsService.findAll(); return this.leaveRequetsService.findAll();
} }

View File

@ -0,0 +1,53 @@
import { Controller, Get, Param, ParseIntPipe } from "@nestjs/common";
import { PayPeriods } from "@prisma/client";
import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
import { PayPeriodsService } from "../services/pay-periods.service";
import { PayPeriodsOverviewService } from "../services/pay-periods-overview.service";
import { PayPeriodEntity } from "../dtos/swagger-entities/pay-period.dto";
import { PayPeriodOverviewDto } from "../dtos/pay-period-overview.dto";
@ApiTags('pay-periods')
@Controller('pay-periods')
export class PayPeriodsController {
constructor(
private readonly payPeriodsService: PayPeriodsService,
private readonly overviewService: PayPeriodsOverviewService
) {}
@Get()
@ApiOperation({ summary: 'Find all pay period' })
@ApiResponse({status: 200,description: 'List of pay period found', type: PayPeriodEntity, isArray: true })
@ApiResponse({status: 400, description: 'List of pay period not found' })
async findAll(): Promise<PayPeriods[]> {
return this.payPeriodsService.findAll();
}
@Get(':periodNumber')
@ApiOperation({ summary: 'Find pay period' })
@ApiResponse({status: 200,description: 'Pay period found', type: PayPeriodEntity })
@ApiResponse({status: 400, description: 'Pay period not found' })
findOne(@Param('periodNumber', ParseIntPipe) periodNumber: number): Promise<PayPeriods | null> {
return this.payPeriodsService.findOne(periodNumber);
}
@Get(':periodNumber/overview')
@ApiOperation({ summary: 'detailed view of a pay period'})
@ApiResponse({ status: 200,description: 'Pay period overview found', type: PayPeriodOverviewDto })
@ApiResponse({status: 400, description: 'Pay period not found' })
async getOverview(@Param('periodNumber', ParseIntPipe) periodNumber: number):
Promise<PayPeriodOverviewDto> {
return this.overviewService.getOverview(periodNumber);
}
@Get('date/:date')
@ApiOperation({ summary: 'cherry picking a date to find a period'})
@ApiResponse({status:200, description: 'Pay period found for the selected date', type: PayPeriodEntity })
@ApiResponse({status:400, description: 'Pay period not found for the selected date date' })
async findByDate(@Param('date') date: string ):
Promise<PayPeriodEntity> {
return this.payPeriodsService.findByDate(date);
}
}

View File

@ -0,0 +1,27 @@
import { ApiProperty } from '@nestjs/swagger';
export class EmployeePeriodOverviewDto {
@ApiProperty({
example: 'a1b2c3d4',
description: "Employee`s ID",
})
employee_id: string;
@ApiProperty({
example: 'Alex Dupont',
description: 'Employee`s full name',
})
employee_name: string;
@ApiProperty({
example: 34,
description: 'period`s total worked hours',
})
total_hours: number;
@ApiProperty({
example: true,
description: 'All timesheets are approved for this employee',
})
is_approved: boolean;
}

View File

@ -0,0 +1,38 @@
import { ApiProperty } from '@nestjs/swagger';
import { EmployeePeriodOverviewDto } from './employee-period-overview.dto';
export class PayPeriodOverviewDto {
@ApiProperty({
example: 1,
description: 'period`s number ( 1-26 )',
})
period_number: number;
@ApiProperty({
example: '2023-12-17',
type: String,
format: 'date',
description: 'Period`s starting date',
})
start_date: Date;
@ApiProperty({
example: '2023-12-30',
type: String,
format: 'date',
description: 'Period`s ending date',
})
end_date: Date;
@ApiProperty({
example: '2023-12-17 → 2023-12-30',
description: 'period`s label for showing',
})
label: string;
@ApiProperty({
type: [EmployeePeriodOverviewDto],
description: 'Detailed view by employee for a chosen period',
})
employees_overview: EmployeePeriodOverviewDto[];
}

View File

@ -0,0 +1,33 @@
import { ApiProperty } from "@nestjs/swagger";
export class PayPeriodEntity {
@ApiProperty({
example: 1,
description: 'numéro cyclique de la période entre 1 et 26'
})
period_number: number;
@ApiProperty({
example: '2023-12-17',
type: String,
format: 'date'
})
start_date: Date;
@ApiProperty({
example: '2023-12-30',
type: String,
format: 'date'
})
end_date: Date;
@ApiProperty({
example: 2023
})
year: number;
@ApiProperty({
example: '2023-12-17 → 2023-12-30'
})
label: string;
}

View File

@ -0,0 +1,12 @@
import { PrismaModule } from "src/prisma/prisma.module";
import { PayPeriodsService } from "./services/pay-periods.service";
import { PayPeriodsController } from "./controllers/pay-periods.controller";
import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule],
providers: [PayPeriodsService],
controllers: [PayPeriodsController],
})
export class PayperiodModule {}

View File

@ -0,0 +1,70 @@
import { Injectable, NotFoundException } from "@nestjs/common";
import { EmployeePeriodOverviewDto } from "../dtos/employee-period-overview.dto";
import { PayPeriodOverviewDto } from "../dtos/pay-period-overview.dto";
import { PrismaService } from "src/prisma/prisma.service";
@Injectable()
export class PayPeriodsOverviewService {
constructor(private readonly prisma: PrismaService) {}
//function to get a full overview of a selected period filtered by employee ID
async getOverview(periodNumber: number): Promise<PayPeriodOverviewDto> {
//fetch the period
const period = await this.prisma.payPeriods.findUnique({
where: { period_number: periodNumber },
});
if(!period) {
throw new NotFoundException(`Period #${periodNumber} not found`);
}
//fetch all included shifts for that period
const shifts = await this.prisma.shifts.findMany({
where: {
date: {
gte: period.start_date,
lte: period.end_date,
},
},
include: {
timesheet: {
include: {
employee: { include: { user: true }},
},
},
},
});
//regroup by employee
const map = new Map<string, EmployeePeriodOverviewDto>();
for (const shift of shifts) {
const employee_record = shift.timesheet.employee;
const user = employee_record.user;
const employee_id = employee_record.user_id;
const employee_name = `${user.first_name} ${user.last_name}`;
const hours = (shift.end_time.getTime() - shift.start_time.getTime() / 3600000);
//check if employee had prior shifts and adds hours of found shift to the total hours
if (map.has(employee_id)) {
const summary = map.get(employee_id)!;
summary.total_hours += hours;
//keeps is_approved false as long as a single shift is left un-validated
summary.is_approved = summary.is_approved && shift.timesheet.is_approved;
} else {
//if first shift of an employee is found, it adds a new entry
map.set(employee_id, {
employee_id: employee_id,
employee_name: employee_name,
total_hours: hours,
is_approved: shift.timesheet.is_approved,
});
}
}
return {
period_number: period.period_number,
start_date: period.start_date,
end_date: period.end_date,
label: period.label,
employees_overview: Array.from(map.values()),
};
}
}

View File

@ -0,0 +1,37 @@
import { Injectable, NotFoundException } from "@nestjs/common";
import { PayPeriods } from "@prisma/client";
import { PrismaService } from "src/prisma/prisma.service";
import { PayPeriodOverviewDto } from "../dtos/pay-period-overview.dto";
import { EmployeePeriodOverviewDto } from "../dtos/employee-period-overview.dto";
@Injectable()
export class PayPeriodsService {
constructor(private readonly prisma: PrismaService) {}
async findAll(): Promise<PayPeriods[]> {
return this.prisma.payPeriods.findMany({
orderBy: { period_number: 'asc'},
});
}
async findOne(periodNumber: number): Promise<PayPeriods | null> {
return this.prisma.payPeriods.findUnique({
where: { period_number: periodNumber},
});
}
//function to cherry pick a Date to find a period
async findByDate(date:string): Promise<PayPeriods> {
const dt = new Date(date);
const period = await this.prisma.payPeriods.findFirst({
where: {
start_date: { lte: dt },
end_date: { gte: dt },
},
});
if(!period) {
throw new NotFoundException(`No period found for this date: ${date}`);
}
return period;
}
}