feat(modules): added pay-periods view module with functions to navigate and search by date
This commit is contained in:
parent
49f99a6b9c
commit
4d538fc78a
|
|
@ -66,6 +66,17 @@ model LeaveRequests {
|
|||
@@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 {
|
||||
id Int @id @default(autoincrement())
|
||||
employee Employees @relation("TimesheetEmployee", fields: [employee_id], references: [id])
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { TimesheetsModule } from './modules/timesheets/timesheets.module';
|
|||
import { AuthenticationModule } from './modules/authentication/auth.module';
|
||||
import { ExpensesModule } from './modules/expenses/expenses.module';
|
||||
import { ExpenseCodesModule } from './modules/expense-codes/expense-codes.module';
|
||||
import { PayperiodModule } from './modules/pay-periods/pay-periods.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
|
@ -31,6 +32,7 @@ import { ExpenseCodesModule } from './modules/expense-codes/expense-codes.module
|
|||
ShiftsModule,
|
||||
TimesheetsModule,
|
||||
AuthenticationModule,
|
||||
PayperiodModule,
|
||||
],
|
||||
controllers: [AppController, HealthController],
|
||||
providers: [AppService],
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export class LeaveRequestController {
|
|||
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
@ApiOperation({summary: 'Find all leave request' })
|
||||
@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[]> {
|
||||
return this.leaveRequetsService.findAll();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
27
src/modules/pay-periods/dtos/employee-period-overview.dto.ts
Normal file
27
src/modules/pay-periods/dtos/employee-period-overview.dto.ts
Normal 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;
|
||||
}
|
||||
38
src/modules/pay-periods/dtos/pay-period-overview.dto.ts
Normal file
38
src/modules/pay-periods/dtos/pay-period-overview.dto.ts
Normal 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[];
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
12
src/modules/pay-periods/pay-periods.module.ts
Normal file
12
src/modules/pay-periods/pay-periods.module.ts
Normal 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 {}
|
||||
|
|
@ -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()),
|
||||
};
|
||||
}
|
||||
}
|
||||
37
src/modules/pay-periods/services/pay-periods.service.ts
Normal file
37
src/modules/pay-periods/services/pay-periods.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user