feat(leave-request): added holiday shift's creation and CRUD for holiday leave-requests.
This commit is contained in:
parent
77f065f37f
commit
d36d2f922b
78
package-lock.json
generated
78
package-lock.json
generated
|
|
@ -51,7 +51,7 @@
|
|||
"globals": "^16.0.0",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^3.4.2",
|
||||
"prisma": "^6.14.0",
|
||||
"prisma": "^6.16.3",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^7.0.0",
|
||||
"ts-jest": "^29.2.5",
|
||||
|
|
@ -3148,9 +3148,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@prisma/config": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.14.0.tgz",
|
||||
"integrity": "sha512-IwC7o5KNNGhmblLs23swnfBjADkacBb7wvyDXUWLwuvUQciKJZqyecU0jw0d7JRkswrj+XTL8fdr0y2/VerKQQ==",
|
||||
"version": "6.16.3",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.16.3.tgz",
|
||||
"integrity": "sha512-VlsLnG4oOuKGGMToEeVaRhoTBZu5H3q51jTQXb/diRags3WV0+BQK5MolJTtP6G7COlzoXmWeS11rNBtvg+qFQ==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"c12": "3.1.0",
|
||||
|
|
@ -3160,48 +3160,48 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@prisma/debug": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.14.0.tgz",
|
||||
"integrity": "sha512-j4Lf+y+5QIJgQD4sJWSbkOD7geKx9CakaLp/TyTy/UDu9Wo0awvWCBH/BAxTHUaCpIl9USA5VS/KJhDqKJSwug==",
|
||||
"version": "6.16.3",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.16.3.tgz",
|
||||
"integrity": "sha512-89DdqWtdKd7qoc9/qJCKLTazj3W3zPEiz0hc7HfZdpjzm21c7orOUB5oHWJsG+4KbV4cWU5pefq3CuDVYF9vgA==",
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/@prisma/engines": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.14.0.tgz",
|
||||
"integrity": "sha512-LhJjqsALFEcoAtF07nSaOkVguaxw/ZsgfROIYZ8bAZDobe7y8Wy+PkYQaPOK1iLSsFgV2MhCO/eNrI1gdSOj6w==",
|
||||
"version": "6.16.3",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.16.3.tgz",
|
||||
"integrity": "sha512-b+Rl4nzQDcoqe6RIpSHv8f5lLnwdDGvXhHjGDiokObguAAv/O1KaX1Oc69mBW/GFWKQpCkOraobLjU6s1h8HGg==",
|
||||
"devOptional": true,
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@prisma/debug": "6.14.0",
|
||||
"@prisma/engines-version": "6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49",
|
||||
"@prisma/fetch-engine": "6.14.0",
|
||||
"@prisma/get-platform": "6.14.0"
|
||||
"@prisma/debug": "6.16.3",
|
||||
"@prisma/engines-version": "6.16.1-1.bb420e667c1820a8c05a38023385f6cc7ef8e83a",
|
||||
"@prisma/fetch-engine": "6.16.3",
|
||||
"@prisma/get-platform": "6.16.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/engines-version": {
|
||||
"version": "6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49.tgz",
|
||||
"integrity": "sha512-EgN9ODJpiX45yvwcngoStp3uQPJ3l+AEVoQ6dMMO2QvmwIlnxfApzKmJQExzdo7/hqQANrz5txHJdGYHzOnGHA==",
|
||||
"version": "6.16.1-1.bb420e667c1820a8c05a38023385f6cc7ef8e83a",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.16.1-1.bb420e667c1820a8c05a38023385f6cc7ef8e83a.tgz",
|
||||
"integrity": "sha512-fftRmosBex48Ph1v2ll1FrPpirwtPZpNkE5CDCY1Lw2SD2ctyrLlVlHiuxDAAlALwWBOkPbAll4+EaqdGuMhJw==",
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/@prisma/fetch-engine": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.14.0.tgz",
|
||||
"integrity": "sha512-MPzYPOKMENYOaY3AcAbaKrfvXVlvTc6iHmTXsp9RiwCX+bPyfDMqMFVUSVXPYrXnrvEzhGHfyiFy0PRLHPysNg==",
|
||||
"version": "6.16.3",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.16.3.tgz",
|
||||
"integrity": "sha512-bUoRIkVaI+CCaVGrSfcKev0/Mk4ateubqWqGZvQ9uCqFv2ENwWIR3OeNuGin96nZn5+SkebcD7RGgKr/+mJelw==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"@prisma/debug": "6.14.0",
|
||||
"@prisma/engines-version": "6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49",
|
||||
"@prisma/get-platform": "6.14.0"
|
||||
"@prisma/debug": "6.16.3",
|
||||
"@prisma/engines-version": "6.16.1-1.bb420e667c1820a8c05a38023385f6cc7ef8e83a",
|
||||
"@prisma/get-platform": "6.16.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/get-platform": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.14.0.tgz",
|
||||
"integrity": "sha512-7VjuxKNwjnBhKfqPpMeWiHEa2sVjYzmHdl1slW6STuUCe9QnOY0OY1ljGSvz6wpG4U8DfbDqkG1yofd/1GINww==",
|
||||
"version": "6.16.3",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.16.3.tgz",
|
||||
"integrity": "sha512-X1LxiFXinJ4iQehrodGp0f66Dv6cDL0GbRlcCoLtSu6f4Wi+hgo7eND/afIs5029GQLgNWKZ46vn8hjyXTsHLA==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"@prisma/debug": "6.14.0"
|
||||
"@prisma/debug": "6.16.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@scarf/scarf": {
|
||||
|
|
@ -9450,15 +9450,15 @@
|
|||
}
|
||||
},
|
||||
"node_modules/nypm": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.1.tgz",
|
||||
"integrity": "sha512-hlacBiRiv1k9hZFiphPUkfSQ/ZfQzZDzC+8z0wL3lvDAOUu/2NnChkKuMoMjNur/9OpKuz2QsIeiPVN0xM5Q0w==",
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz",
|
||||
"integrity": "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"citty": "^0.1.6",
|
||||
"consola": "^3.4.2",
|
||||
"pathe": "^2.0.3",
|
||||
"pkg-types": "^2.2.0",
|
||||
"pkg-types": "^2.3.0",
|
||||
"tinyexec": "^1.0.1"
|
||||
},
|
||||
"bin": {
|
||||
|
|
@ -9967,9 +9967,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/pkg-types": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.2.0.tgz",
|
||||
"integrity": "sha512-2SM/GZGAEkPp3KWORxQZns4M+WSeXbC2HEvmOIJe3Cmiv6ieAJvdVhDldtHqM5J1Y7MrR1XhkBT/rMlhh9FdqQ==",
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
|
||||
"integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"confbox": "^0.2.2",
|
||||
|
|
@ -10049,14 +10049,14 @@
|
|||
}
|
||||
},
|
||||
"node_modules/prisma": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.14.0.tgz",
|
||||
"integrity": "sha512-QEuCwxu+Uq9BffFw7in8In+WfbSUN0ewnaSUKloLkbJd42w6EyFckux4M0f7VwwHlM3A8ssaz4OyniCXlsn0WA==",
|
||||
"version": "6.16.3",
|
||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.16.3.tgz",
|
||||
"integrity": "sha512-4tJq3KB9WRshH5+QmzOLV54YMkNlKOtLKaSdvraI5kC/axF47HuOw6zDM8xrxJ6s9o2WodY654On4XKkrobQdQ==",
|
||||
"devOptional": true,
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@prisma/config": "6.14.0",
|
||||
"@prisma/engines": "6.14.0"
|
||||
"@prisma/config": "6.16.3",
|
||||
"@prisma/engines": "6.16.3"
|
||||
},
|
||||
"bin": {
|
||||
"prisma": "build/index.js"
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@
|
|||
"globals": "^16.0.0",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^3.4.2",
|
||||
"prisma": "^6.14.0",
|
||||
"prisma": "^6.16.3",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^7.0.0",
|
||||
"ts-jest": "^29.2.5",
|
||||
|
|
|
|||
|
|
@ -105,16 +105,19 @@ model LeaveRequests {
|
|||
id Int @id @default(autoincrement())
|
||||
employee Employees @relation("LeaveRequestEmployee", fields: [employee_id], references: [id])
|
||||
employee_id Int
|
||||
bank_code BankCodes? @relation("LeaveRequestBankCodes", fields: [bank_code_id], references: [id])
|
||||
bank_code BankCodes @relation("LeaveRequestBankCodes", fields: [bank_code_id], references: [id])
|
||||
bank_code_id Int
|
||||
leave_type LeaveTypes
|
||||
start_date_time DateTime @db.Date
|
||||
end_date_time DateTime? @db.Date
|
||||
date DateTime @db.Date
|
||||
payable_hours Decimal? @db.Decimal(5,2)
|
||||
requested_hours Decimal? @db.Decimal(5,2)
|
||||
comment String
|
||||
approval_status LeaveApprovalStatus @default(PENDING)
|
||||
|
||||
archive LeaveRequestsArchive[] @relation("LeaveRequestToArchive")
|
||||
|
||||
@@unique([employee_id, leave_type, date], name: "leave_per_employee_date")
|
||||
@@index([employee_id, date])
|
||||
@@map("leave_requests")
|
||||
}
|
||||
|
||||
|
|
@ -125,11 +128,14 @@ model LeaveRequestsArchive {
|
|||
archived_at DateTime @default(now())
|
||||
employee_id Int
|
||||
leave_type LeaveTypes
|
||||
start_date_time DateTime @db.Date
|
||||
end_date_time DateTime? @db.Date
|
||||
date DateTime @db.Date
|
||||
payable_hours Decimal? @db.Decimal(5,2)
|
||||
requested_hours Decimal? @db.Decimal(5,2)
|
||||
comment String
|
||||
approval_status LeaveApprovalStatus
|
||||
|
||||
@@unique([leave_request_id])
|
||||
@@index([employee_id, date])
|
||||
@@map("leave_requests_archive")
|
||||
}
|
||||
|
||||
|
|
@ -340,6 +346,7 @@ enum LeaveTypes {
|
|||
PARENTAL // maternite/paternite/adoption
|
||||
LEGAL // obligations legales comme devoir de juree
|
||||
WEDDING // mariage
|
||||
HOLIDAY // férier
|
||||
|
||||
@@map("leave_types")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,14 @@ import { Injectable, Logger, NotFoundException } from "@nestjs/common";
|
|||
import { PrismaService } from "../../../prisma/prisma.service";
|
||||
import { computeHours, getWeekStart } from "src/common/utils/date-utils";
|
||||
|
||||
const WEEK_IN_MS = 7 * 24 * 60 * 60 * 1000;
|
||||
|
||||
/*
|
||||
le calcul est 1/20 des 4 dernières semaines, précédent la semaine incluant le férier.
|
||||
Un maximum de 08h00 est allouable pour le férier
|
||||
Un maximum de 40hrs par semaine est retenue pour faire le calcul.
|
||||
*/
|
||||
|
||||
@Injectable()
|
||||
export class HolidayService {
|
||||
private readonly logger = new Logger(HolidayService.name);
|
||||
|
|
@ -22,36 +30,49 @@ export class HolidayService {
|
|||
|
||||
private async computeHoursPrevious4WeeksByEmail(email: string, holiday_date: Date): Promise<number> {
|
||||
const employee_id = await this.resolveEmployeeByEmail(email);
|
||||
return this.computeHoursPrevious4Weeks(employee_id, holiday_date)
|
||||
return this.computeHoursPrevious4Weeks(employee_id, holiday_date);
|
||||
}
|
||||
|
||||
private async computeHoursPrevious4Weeks(employee_id: number, holiday_date: Date): Promise<number> {
|
||||
//sets the end of the window to 1ms before the week with the holiday
|
||||
const holiday_week_start = getWeekStart(holiday_date);
|
||||
const window_start = new Date(holiday_week_start.getTime() - 4 * WEEK_IN_MS);
|
||||
const window_end = new Date(holiday_week_start.getTime() - 1);
|
||||
//sets the start of the window to 28 days ( 4 completed weeks ) before the week with the holiday
|
||||
const window_start = new Date(window_end.getTime() - 28 * 24 * 60 * 60000 + 1 )
|
||||
|
||||
const valid_codes = ['G1', 'G43', 'G56', 'G104', 'G105', 'G700'];
|
||||
//fetches all shift of the employee in said window ( 4 previous completed weeks )
|
||||
const shifts = await this.prisma.shifts.findMany({
|
||||
where: { timesheet: { employee_id: employee_id } ,
|
||||
where: {
|
||||
timesheet: { employee_id: employee_id },
|
||||
date: { gte: window_start, lte: window_end },
|
||||
bank_code: { bank_code: { in: valid_codes } },
|
||||
},
|
||||
select: { date: true, start_time: true, end_time: true },
|
||||
});
|
||||
|
||||
const total_hours = shifts.map(s => computeHours(s.start_time, s.end_time)).reduce((sum, h)=> sum + h, 0);
|
||||
const daily_hours = total_hours / 20;
|
||||
const hours_by_week = new Map<number, number>();
|
||||
for(const shift of shifts) {
|
||||
const hours = computeHours(shift.start_time, shift.end_time);
|
||||
if(hours <= 0) continue;
|
||||
const shift_week_start = getWeekStart(shift.date);
|
||||
const key = shift_week_start.getTime();
|
||||
hours_by_week.set(key, (hours_by_week.get(key) ?? 0) + hours);
|
||||
}
|
||||
|
||||
return daily_hours;
|
||||
let capped_total = 0;
|
||||
for(let offset = 1; offset <= 4; offset++) {
|
||||
const week_start = new Date(holiday_week_start.getTime() - offset * WEEK_IN_MS);
|
||||
const key = week_start.getTime();
|
||||
const weekly_hours = hours_by_week.get(key) ?? 0;
|
||||
capped_total += Math.min(weekly_hours, 40);
|
||||
}
|
||||
|
||||
const average_daily_hours = capped_total / 20;
|
||||
return average_daily_hours;
|
||||
}
|
||||
|
||||
async calculateHolidayPay( email: string, holiday_date: Date, modifier: number): Promise<number> {
|
||||
const hours = await this.computeHoursPrevious4WeeksByEmail(email, holiday_date);
|
||||
const daily_rate = Math.min(hours, 8);
|
||||
this.logger.debug(`Holiday pay calculation: hours= ${hours.toFixed(2)}`);
|
||||
const average_daily_hours = await this.computeHoursPrevious4WeeksByEmail(email, holiday_date);
|
||||
const daily_rate = Math.min(average_daily_hours, 8);
|
||||
this.logger.debug(`Holiday pay calculation: cappedHoursPerDay= ${average_daily_hours.toFixed(2)}, appliedDailyRate= ${daily_rate.toFixed(2)}`);
|
||||
return daily_rate * modifier;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,76 +1,13 @@
|
|||
import { Body, Controller, Delete, Get, Param, ParseBoolPipe, ParseIntPipe, Patch, Post, Query, UseGuards, UsePipes, ValidationPipe } from "@nestjs/common";
|
||||
import { Controller } from "@nestjs/common";
|
||||
import { LeaveRequestsService } from "../services/leave-requests.service";
|
||||
import { CreateLeaveRequestsDto } from "../dtos/create-leave-request.dto";
|
||||
import { LeaveRequests } from "@prisma/client";
|
||||
import { UpdateLeaveRequestsDto } from "../dtos/update-leave-request.dto";
|
||||
import { RolesAllowed } from "src/common/decorators/roles.decorators";
|
||||
import { LeaveApprovalStatus, Roles as RoleEnum } from '.prisma/client';
|
||||
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
|
||||
import { SearchLeaveRequestsDto } from "../dtos/search-leave-request.dto";
|
||||
import { LeaveRequestViewDto } from "../dtos/leave-request.view.dto";
|
||||
|
||||
import { ApiBearerAuth, ApiTags } from "@nestjs/swagger";
|
||||
|
||||
@ApiTags('Leave Requests')
|
||||
@ApiBearerAuth('access-token')
|
||||
// @UseGuards()
|
||||
@Controller('leave-requests')
|
||||
export class LeaveRequestController {
|
||||
constructor(private readonly leaveRequetsService: LeaveRequestsService){}
|
||||
|
||||
@Post()
|
||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
@ApiOperation({summary: 'Create leave request' })
|
||||
@ApiResponse({ status: 201, description: 'Leave request created',type: CreateLeaveRequestsDto })
|
||||
@ApiResponse({ status: 400, description: 'Incomplete task or invalid data' })
|
||||
create(@Body() dto: CreateLeaveRequestsDto): Promise<LeaveRequestViewDto> {
|
||||
return this. leaveRequetsService.create(dto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
//@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: LeaveRequestViewDto, isArray: true })
|
||||
@ApiResponse({ status: 400, description: 'List of leave request not found' })
|
||||
@UsePipes(new ValidationPipe({transform: true, whitelist: true}))
|
||||
findAll(@Query() filters: SearchLeaveRequestsDto): Promise<LeaveRequestViewDto[]> {
|
||||
return this.leaveRequetsService.findAll(filters);
|
||||
}
|
||||
//remove emp_id and use email
|
||||
@Get(':id')
|
||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
@ApiOperation({summary: 'Find leave request' })
|
||||
@ApiResponse({ status: 201, description: 'Leave request found',type: LeaveRequestViewDto })
|
||||
@ApiResponse({ status: 400, description: 'Leave request not found' })
|
||||
findOne(@Param('id', ParseIntPipe) id: number): Promise<LeaveRequestViewDto> {
|
||||
return this.leaveRequetsService.findOne(id);
|
||||
}
|
||||
//remove emp_id and use email
|
||||
@Patch(':id')
|
||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
@ApiOperation({summary: 'Update leave request' })
|
||||
@ApiResponse({ status: 201, description: 'Leave request updated',type: LeaveRequestViewDto })
|
||||
@ApiResponse({ status: 400, description: 'Leave request not found' })
|
||||
update(@Param('id', ParseIntPipe) id: number,@Body() dto: UpdateLeaveRequestsDto): Promise<LeaveRequestViewDto> {
|
||||
return this.leaveRequetsService.update(id, dto);
|
||||
}
|
||||
|
||||
//remove emp_id and use email
|
||||
@Delete(':id')
|
||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
@ApiOperation({summary: 'Delete leave request' })
|
||||
@ApiResponse({ status: 201, description: 'Leave request deleted',type: CreateLeaveRequestsDto })
|
||||
@ApiResponse({ status: 400, description: 'Leave request not found' })
|
||||
remove(@Param('id', ParseIntPipe) id: number): Promise<LeaveRequestViewDto> {
|
||||
return this.leaveRequetsService.remove(id);
|
||||
}
|
||||
|
||||
//remove emp_id and use email
|
||||
@Patch('approval/:id')
|
||||
updateApproval( @Param('id', ParseIntPipe) id: number,
|
||||
@Body('is_approved', ParseBoolPipe) is_approved: boolean): Promise<LeaveRequestViewDto> {
|
||||
const approvalStatus = is_approved ?
|
||||
LeaveApprovalStatus.APPROVED : LeaveApprovalStatus.DENIED;
|
||||
return this.leaveRequetsService.update(id, { approval_status: approvalStatus });
|
||||
}
|
||||
|
||||
}
|
||||
constructor(private readonly leave_service: LeaveRequestsService){}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,56 +0,0 @@
|
|||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { LeaveApprovalStatus, LeaveTypes } from "@prisma/client";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsDateString, IsEmail, IsEnum, IsInt, IsISO8601, IsNotEmpty, IsOptional, IsString } from "class-validator";
|
||||
|
||||
export class CreateLeaveRequestsDto {
|
||||
|
||||
@IsEmail()
|
||||
email: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 7,
|
||||
description: 'ID number of a leave-request code (link with bank-codes)',
|
||||
})
|
||||
@Type(()=> Number)
|
||||
@IsInt()
|
||||
bank_code_id: number;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'Sick or Vacation or Unpaid or Bereavement or Parental or Legal',
|
||||
description: 'type of leave request for an accounting perception',
|
||||
})
|
||||
@IsEnum(LeaveTypes)
|
||||
leave_type: LeaveTypes;
|
||||
|
||||
@ApiProperty({
|
||||
example: '22/06/2463',
|
||||
description: 'Leave request`s start date',
|
||||
})
|
||||
@IsISO8601()
|
||||
start_date_time:string;
|
||||
|
||||
@ApiProperty({
|
||||
example: '25/03/3019',
|
||||
description: 'Leave request`s end date',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsISO8601()
|
||||
end_date_time?: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'My precious',
|
||||
description: 'Leave request`s comment',
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
comment: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'True or False or Pending or Denied or Cancelled or Escalated',
|
||||
description: 'Leave request`s approval status',
|
||||
})
|
||||
@IsEnum(LeaveApprovalStatus)
|
||||
@IsOptional()
|
||||
approval_status?: LeaveApprovalStatus;
|
||||
}
|
||||
14
src/modules/leave-requests/dtos/leave-request-view.dto.ts
Normal file
14
src/modules/leave-requests/dtos/leave-request-view.dto.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { LeaveApprovalStatus, LeaveTypes } from "@prisma/client";
|
||||
|
||||
export class LeaveRequestViewDto {
|
||||
id: number;
|
||||
leave_type!: LeaveTypes;
|
||||
date!: string;
|
||||
comment!: string;
|
||||
approval_status: LeaveApprovalStatus;
|
||||
email!: string;
|
||||
employee_full_name!: string;
|
||||
payable_hours?: number;
|
||||
requested_hours?: number;
|
||||
action?: 'created' | 'updated' | 'deleted';
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import { LeaveApprovalStatus, LeaveTypes } from "@prisma/client";
|
||||
|
||||
export class LeaveRequestViewDto {
|
||||
id!: number;
|
||||
leave_type!: LeaveTypes;
|
||||
start_date_time!: string;
|
||||
end_date_time!: string | null;
|
||||
comment!: string | null;
|
||||
approval_status: LeaveApprovalStatus;
|
||||
email!: string;
|
||||
employee_full_name: string;
|
||||
days_requested?: number;
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
import { LeaveApprovalStatus, LeaveTypes } from "@prisma/client";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsOptional, IsInt, IsEnum, IsDateString, IsEmail } from "class-validator";
|
||||
|
||||
export class SearchLeaveRequestsDto {
|
||||
|
||||
@IsEmail()
|
||||
email: string;
|
||||
|
||||
@IsOptional()
|
||||
@Type(()=> Number)
|
||||
@IsInt()
|
||||
bank_code_id?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(LeaveApprovalStatus)
|
||||
approval_status?: LeaveApprovalStatus
|
||||
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
start_date?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
end_date?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(LeaveTypes)
|
||||
leave_type?: LeaveTypes;
|
||||
}
|
||||
52
src/modules/leave-requests/dtos/sick.dto.ts
Normal file
52
src/modules/leave-requests/dtos/sick.dto.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import {
|
||||
ArrayNotEmpty,
|
||||
ArrayUnique,
|
||||
IsArray,
|
||||
IsEmail,
|
||||
IsISO8601,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Max,
|
||||
Min,
|
||||
} from "class-validator";
|
||||
|
||||
export class UpsertSickDto {
|
||||
@ApiProperty({ example: "jane.doe@example.com" })
|
||||
@IsEmail()
|
||||
email!: string;
|
||||
|
||||
@ApiProperty({
|
||||
type: [String],
|
||||
example: ["2025-03-04"],
|
||||
description: "ISO dates that represent the sick leave request.",
|
||||
})
|
||||
@IsArray()
|
||||
@ArrayNotEmpty()
|
||||
@ArrayUnique()
|
||||
@IsISO8601({}, { each: true })
|
||||
dates!: string[];
|
||||
|
||||
@ApiProperty({
|
||||
required: false,
|
||||
example: "Medical note provided",
|
||||
description: "Optional comment applied to every date.",
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
comment?: string;
|
||||
|
||||
@ApiProperty({
|
||||
required: false,
|
||||
example: 8,
|
||||
description: "Hours requested per day. Lets you keep the user input even if the calculation differs.",
|
||||
})
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsNumber({ maxDecimalPlaces: 2 })
|
||||
@Min(0)
|
||||
@Max(24)
|
||||
requested_hours?: number;
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
import { PartialType } from "@nestjs/swagger";
|
||||
import { CreateLeaveRequestsDto } from "./create-leave-request.dto";
|
||||
|
||||
export class UpdateLeaveRequestsDto extends PartialType(CreateLeaveRequestsDto){}
|
||||
42
src/modules/leave-requests/dtos/upsert-holiday.dto.ts
Normal file
42
src/modules/leave-requests/dtos/upsert-holiday.dto.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { Type } from "class-transformer";
|
||||
import {
|
||||
ArrayNotEmpty,
|
||||
ArrayUnique,
|
||||
IsArray,
|
||||
IsEmail,
|
||||
IsIn,
|
||||
IsISO8601,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Max,
|
||||
Min,
|
||||
} from "class-validator";
|
||||
|
||||
export const HOLIDAY_UPSERT_ACTIONS = ['create', 'update', 'delete'] as const;
|
||||
export type HolidayUpsertAction = typeof HOLIDAY_UPSERT_ACTIONS[number];
|
||||
|
||||
export class UpsertHolidayDto {
|
||||
@IsEmail()
|
||||
email!: string;
|
||||
|
||||
@IsArray()
|
||||
@ArrayNotEmpty()
|
||||
@ArrayUnique()
|
||||
@IsISO8601({}, { each: true })
|
||||
dates!: string[];
|
||||
|
||||
@IsIn(HOLIDAY_UPSERT_ACTIONS)
|
||||
action!: HolidayUpsertAction;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
comment?: string;
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsNumber({ maxDecimalPlaces: 2 })
|
||||
@Min(0)
|
||||
@Max(24)
|
||||
requested_hours?: number;
|
||||
}
|
||||
52
src/modules/leave-requests/dtos/vacation.dto.ts
Normal file
52
src/modules/leave-requests/dtos/vacation.dto.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import {
|
||||
ArrayNotEmpty,
|
||||
ArrayUnique,
|
||||
IsArray,
|
||||
IsEmail,
|
||||
IsISO8601,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Max,
|
||||
Min,
|
||||
} from "class-validator";
|
||||
|
||||
export class UpsertVacationDto {
|
||||
@ApiProperty({ example: "jane.doe@example.com" })
|
||||
@IsEmail()
|
||||
email!: string;
|
||||
|
||||
@ApiProperty({
|
||||
type: [String],
|
||||
example: ["2025-07-14", "2025-07-15"],
|
||||
description: "ISO dates that represent the vacation request.",
|
||||
})
|
||||
@IsArray()
|
||||
@ArrayNotEmpty()
|
||||
@ArrayUnique()
|
||||
@IsISO8601({}, { each: true })
|
||||
dates!: string[];
|
||||
|
||||
@ApiProperty({
|
||||
required: false,
|
||||
example: "Summer break",
|
||||
description: "Optional comment applied to every date.",
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
comment?: string;
|
||||
|
||||
@ApiProperty({
|
||||
required: false,
|
||||
example: 8,
|
||||
description: "Hours requested per day. Used as default when creating shifts.",
|
||||
})
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsNumber({ maxDecimalPlaces: 2 })
|
||||
@Min(0)
|
||||
@Max(24)
|
||||
requested_hours?: number;
|
||||
}
|
||||
|
|
@ -1,14 +1,20 @@
|
|||
import { LeaveRequestViewDto } from "../dtos/leave-request.view.dto";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { LeaveRequestViewDto } from "../dtos/leave-request-view.dto";
|
||||
import { LeaveRequestArchiveRow } from "../utils/leave-requests-archive.select";
|
||||
|
||||
const toISO = (date: Date | null): string | null => (date ? date.toISOString().slice(0,10): null);
|
||||
const toNum = (value?: Prisma.Decimal | null) => value ? Number(value) : undefined;
|
||||
|
||||
export function mapArchiveRowToView(row: LeaveRequestArchiveRow, email: string, employee_full_name:string): LeaveRequestViewDto {
|
||||
const isoDate = row.date?.toISOString().slice(0, 10);
|
||||
if (!isoDate) {
|
||||
throw new Error(`Leave request #${row.id} has no date set.`);
|
||||
}
|
||||
return {
|
||||
id: row.id,
|
||||
leave_type: row.leave_type,
|
||||
start_date_time: toISO(row.start_date_time)!,
|
||||
end_date_time: toISO(row.end_date_time),
|
||||
date: isoDate,
|
||||
payable_hours: toNum(row.payable_hours),
|
||||
requested_hours: toNum(row.requested_hours),
|
||||
comment: row.comment,
|
||||
approval_status: row.approval_status,
|
||||
email,
|
||||
|
|
|
|||
|
|
@ -1,19 +1,23 @@
|
|||
import { LeaveRequestViewDto } from "../dtos/leave-request.view.dto";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { LeaveRequestViewDto } from "../dtos/leave-request-view.dto";
|
||||
import { LeaveRequestRow } from "../utils/leave-requests.select";
|
||||
|
||||
function toISODateString(date:Date | null): string | null {
|
||||
return date ? date.toISOString().slice(0,10) : null;
|
||||
}
|
||||
const toNum = (value?: Prisma.Decimal | null) =>
|
||||
value !== null && value !== undefined ? Number(value) : undefined;
|
||||
|
||||
export function mapRowToView(row: LeaveRequestRow): LeaveRequestViewDto {
|
||||
const isoDate = row.date?.toISOString().slice(0, 10);
|
||||
if (!isoDate) throw new Error(`Leave request #${row.id} has no date set.`);
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
leave_type: row.leave_type,
|
||||
start_date_time: toISODateString(row.start_date_time)!,
|
||||
end_date_time: toISODateString(row.end_date_time),
|
||||
date: isoDate,
|
||||
payable_hours: toNum(row.payable_hours),
|
||||
requested_hours: toNum(row.requested_hours),
|
||||
comment: row.comment,
|
||||
approval_status: row.approval_status,
|
||||
email: row.employee.user.email,
|
||||
employee_full_name: `${row.employee.user.first_name} ${row.employee.user.last_name}`
|
||||
}
|
||||
employee_full_name: `${row.employee.user.first_name} ${row.employee.user.last_name}`,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,175 +1,224 @@
|
|||
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { LeaveTypes, LeaveRequestsArchive } from "@prisma/client";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { CreateLeaveRequestsDto } from "../dtos/create-leave-request.dto";
|
||||
import { LeaveRequests, LeaveRequestsArchive } from "@prisma/client";
|
||||
import { UpdateLeaveRequestsDto } from "../dtos/update-leave-request.dto";
|
||||
import { HolidayService } from "src/modules/business-logics/services/holiday.service";
|
||||
import { SickLeaveService } from "src/modules/business-logics/services/sick-leave.service";
|
||||
import { VacationService } from "src/modules/business-logics/services/vacation.service";
|
||||
import { SearchLeaveRequestsDto } from "../dtos/search-leave-request.dto";
|
||||
import { buildPrismaWhere } from "src/common/shared/build-prisma-where";
|
||||
import { LeaveRequestViewDto } from "../dtos/leave-request.view.dto";
|
||||
import { LeaveRequestRow, leaveRequestsSelect } from "../utils/leave-requests.select";
|
||||
|
||||
import { UpsertHolidayDto, HolidayUpsertAction } from "../dtos/upsert-holiday.dto";
|
||||
import { LeaveRequestViewDto } from "../dtos/leave-request-view.dto";
|
||||
import { mapRowToView } from "../mappers/leave-requests.mapper";
|
||||
import { LeaveRequestsArchiveController } from "src/modules/archival/controllers/leave-requests-archive.controller";
|
||||
import { mapArchiveRowToViewWithDays } from "../utils/leave-request.transform";
|
||||
import { LeaveRequestArchiveRow, leaveRequestsArchiveSelect } from "../utils/leave-requests-archive.select";
|
||||
import { mapArchiveRowToView } from "../mappers/leave-requests-archive.mapper";
|
||||
import { mapArchiveRowToViewWithDays, mapRowToViewWithDays } from "../utils/leave-request.transform";
|
||||
import { leaveRequestsSelect } from "../utils/leave-requests.select";
|
||||
|
||||
@Injectable()
|
||||
export class LeaveRequestsService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly holidayService: HolidayService,
|
||||
private readonly vacationService: VacationService,
|
||||
private readonly sickLeaveService: SickLeaveService
|
||||
) {}
|
||||
|
||||
//function to avoid using employee_id as identifier in the frontend.
|
||||
//-------------------- helpers --------------------
|
||||
private async resolveEmployeeIdByEmail(email: string): Promise<number> {
|
||||
const employee = await this.prisma.employees.findFirst({
|
||||
where: { user: { email} },
|
||||
select: { id:true },
|
||||
where: { user: { email } },
|
||||
select: { id: true },
|
||||
});
|
||||
if(!employee) throw new NotFoundException(`Employee with email ${email} not found`);
|
||||
if (!employee) {
|
||||
throw new NotFoundException(`Employee with email ${email} not found`);
|
||||
}
|
||||
return employee.id;
|
||||
}
|
||||
|
||||
//create a leave-request without the use of employee_id
|
||||
async create(dto: CreateLeaveRequestsDto): Promise<LeaveRequestViewDto> {
|
||||
const employee_id = await this.resolveEmployeeIdByEmail(dto.email);
|
||||
const row: LeaveRequestRow = await this.prisma.leaveRequests.create({
|
||||
private async resolveHolidayBankCode() {
|
||||
const bankCode = await this.prisma.bankCodes.findFirst({
|
||||
where: { type: 'HOLIDAY' },
|
||||
select: { id: true, bank_code: true, modifier: true },
|
||||
});
|
||||
if (!bankCode) {
|
||||
throw new BadRequestException('Bank code type "HOLIDAY" not found');
|
||||
}
|
||||
return bankCode;
|
||||
}
|
||||
|
||||
async handleHoliday(dto: UpsertHolidayDto): Promise<{ action: HolidayUpsertAction; leave_requests: LeaveRequestViewDto[] }> {
|
||||
switch (dto.action) {
|
||||
case 'create':
|
||||
return this.createHoliday(dto);
|
||||
case 'update':
|
||||
return this.updateHoliday(dto);
|
||||
case 'delete':
|
||||
return this.deleteHoliday(dto);
|
||||
default:
|
||||
throw new BadRequestException(`Unknown action: ${dto.action}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async createHoliday(dto: UpsertHolidayDto): Promise<{ action: 'create'; leave_requests: LeaveRequestViewDto[] }> {
|
||||
const email = dto.email.trim();
|
||||
const employeeId = await this.resolveEmployeeIdByEmail(email);
|
||||
const bankCode = await this.resolveHolidayBankCode();
|
||||
const dates = normalizeDates(dto.dates);
|
||||
if (!dates.length) {
|
||||
throw new BadRequestException('Dates array must not be empty');
|
||||
}
|
||||
|
||||
const created: LeaveRequestViewDto[] = [];
|
||||
for (const isoDate of dates) {
|
||||
const date = toDateOnly(isoDate);
|
||||
const existing = await this.prisma.leaveRequests.findUnique({
|
||||
where: {
|
||||
leave_per_employee_date: {
|
||||
employee_id: employeeId,
|
||||
leave_type: LeaveTypes.HOLIDAY,
|
||||
date,
|
||||
},
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
if (existing) {
|
||||
throw new BadRequestException(`A holiday request already exists for ${isoDate}`);
|
||||
}
|
||||
|
||||
const payable = await this.holidayService.calculateHolidayPay(email, date, bankCode.modifier);
|
||||
const row = await this.prisma.leaveRequests.create({
|
||||
data: {
|
||||
employee_id,
|
||||
bank_code_id: dto.bank_code_id,
|
||||
leave_type: dto.leave_type,
|
||||
start_date_time: new Date(dto.start_date_time),
|
||||
end_date_time: dto.end_date_time ? new Date(dto.end_date_time) : null,
|
||||
comment: dto.comment,
|
||||
approval_status: dto.approval_status ?? undefined,
|
||||
employee_id: employeeId,
|
||||
bank_code_id: bankCode.id,
|
||||
leave_type: LeaveTypes.HOLIDAY,
|
||||
date,
|
||||
comment: dto.comment ?? '',
|
||||
approval_status: undefined,
|
||||
requested_hours: dto.requested_hours ?? 8,
|
||||
payable_hours: payable,
|
||||
},
|
||||
select: leaveRequestsSelect,
|
||||
});
|
||||
return mapRowToViewWithDays(row);
|
||||
created.push({ ...mapRowToView(row), action: 'create' });
|
||||
}
|
||||
|
||||
//fetches all leave-requests using email
|
||||
async findAll(filters: SearchLeaveRequestsDto): Promise<LeaveRequestViewDto[]> {
|
||||
const {start_date, end_date,email, leave_type, approval_status, bank_code_id } = filters;
|
||||
const where: any = {};
|
||||
|
||||
if (start_date) where.start_date_time = { ...(where.start_date_time ?? {}), gte: new Date(start_date) };
|
||||
if (end_date) where.end_date_time = { ...(where.end_date_time ?? {}), lte: new Date(end_date) };
|
||||
if (email) where.employee = { user: { email } };
|
||||
if (leave_type) where.leave_type = leave_type;
|
||||
if (approval_status) where.approval_status = approval_status;
|
||||
if (typeof bank_code_id === 'number') where.bank_code_id = bank_code_id;
|
||||
|
||||
const rows= await this.prisma.leaveRequests.findMany({
|
||||
where,
|
||||
select: leaveRequestsSelect,
|
||||
orderBy: { start_date_time: 'desc' },
|
||||
});
|
||||
|
||||
return rows.map(mapRowToViewWithDays);
|
||||
return { action: 'create', leave_requests: created };
|
||||
}
|
||||
|
||||
//fetch 1 leave-request using email
|
||||
async findOne(id:number): Promise<LeaveRequestViewDto> {
|
||||
const row: LeaveRequestRow | null = await this.prisma.leaveRequests.findUnique({
|
||||
where: { id },
|
||||
private async updateHoliday(dto: UpsertHolidayDto): Promise<{ action: 'update'; leave_requests: LeaveRequestViewDto[] }> {
|
||||
const email = dto.email.trim();
|
||||
const employeeId = await this.resolveEmployeeIdByEmail(email);
|
||||
const bankCode = await this.resolveHolidayBankCode();
|
||||
const dates = normalizeDates(dto.dates);
|
||||
if (!dates.length) {
|
||||
throw new BadRequestException('Dates array must not be empty');
|
||||
}
|
||||
|
||||
const updated: LeaveRequestViewDto[] = [];
|
||||
for (const isoDate of dates) {
|
||||
const date = toDateOnly(isoDate);
|
||||
const existing = await this.prisma.leaveRequests.findUnique({
|
||||
where: {
|
||||
leave_per_employee_date: {
|
||||
employee_id: employeeId,
|
||||
leave_type: LeaveTypes.HOLIDAY,
|
||||
date,
|
||||
},
|
||||
},
|
||||
select: leaveRequestsSelect,
|
||||
});
|
||||
if(!row) throw new NotFoundException(`Leave Request #${id} not found`);
|
||||
return mapRowToViewWithDays(row);
|
||||
if (!existing) {
|
||||
throw new NotFoundException(`No HOLIDAY request found for ${isoDate}`);
|
||||
}
|
||||
|
||||
//updates 1 leave-request using email
|
||||
async update(id: number, dto: UpdateLeaveRequestsDto): Promise<LeaveRequestViewDto> {
|
||||
await this.findOne(id);
|
||||
const data: Record<string, any> = {};
|
||||
|
||||
if(dto.email !== undefined) data.employee_id = await this.resolveEmployeeIdByEmail(dto.email);
|
||||
if(dto.leave_type !== undefined) data.bank_code_id = dto.bank_code_id;
|
||||
if(dto.start_date_time !== undefined) data.start_date_time = new Date(dto.start_date_time);
|
||||
if(dto.end_date_time !== undefined) data.end_date_time = new Date(dto.end_date_time);
|
||||
if(dto.comment !== undefined) data.comment = dto.comment;
|
||||
if(dto.approval_status !== undefined) data.approval_status = dto.approval_status;
|
||||
|
||||
const row: LeaveRequestRow = await this.prisma.leaveRequests.update({
|
||||
where: { id },
|
||||
data,
|
||||
const payable = await this.holidayService.calculateHolidayPay(email, date, bankCode.modifier);
|
||||
const row = await this.prisma.leaveRequests.update({
|
||||
where: { id: existing.id },
|
||||
data: {
|
||||
comment: dto.comment ?? existing.comment,
|
||||
requested_hours: dto.requested_hours ?? undefined,
|
||||
payable_hours: payable,
|
||||
bank_code_id: bankCode.id,
|
||||
},
|
||||
select: leaveRequestsSelect,
|
||||
});
|
||||
return mapRowToViewWithDays(row);
|
||||
updated.push({ ...mapRowToView(row), action: 'update' });
|
||||
}
|
||||
|
||||
//removes 1 leave-request using email
|
||||
async remove(id:number): Promise<LeaveRequestViewDto> {
|
||||
await this.findOne(id);
|
||||
const row: LeaveRequestRow = await this.prisma.leaveRequests.delete({
|
||||
where: { id },
|
||||
return { action: 'update', leave_requests: updated };
|
||||
}
|
||||
|
||||
private async deleteHoliday(dto: UpsertHolidayDto): Promise<{ action: 'delete'; leave_requests: LeaveRequestViewDto[] }> {
|
||||
const email = dto.email.trim();
|
||||
const employeeId = await this.resolveEmployeeIdByEmail(email);
|
||||
const dates = normalizeDates(dto.dates);
|
||||
if (!dates.length) {
|
||||
throw new BadRequestException('Dates array must not be empty');
|
||||
}
|
||||
|
||||
const rows = await this.prisma.leaveRequests.findMany({
|
||||
where: {
|
||||
employee_id: employeeId,
|
||||
leave_type: LeaveTypes.HOLIDAY,
|
||||
date: { in: dates.map((d) => toDateOnly(d)) },
|
||||
},
|
||||
select: leaveRequestsSelect,
|
||||
});
|
||||
return mapRowToViewWithDays(row);
|
||||
|
||||
if (rows.length !== dates.length) {
|
||||
const missing = dates.filter((isoDate) => !rows.some((row) => toISODateKey(row.date) === isoDate));
|
||||
throw new NotFoundException(`No HOLIDAY request found for: ${missing.join(', ')}`);
|
||||
}
|
||||
|
||||
//archivation functions ******************************************************
|
||||
await this.prisma.leaveRequests.deleteMany({
|
||||
where: { id: { in: rows.map((row) => row.id) } },
|
||||
});
|
||||
|
||||
const deleted = rows.map((row) => ({ ...mapRowToView(row), action: 'delete' as const }));
|
||||
return { action: 'delete', leave_requests: deleted };
|
||||
}
|
||||
|
||||
//-------------------- archival --------------------
|
||||
async archiveExpired(): Promise<void> {
|
||||
const now = new Date();
|
||||
|
||||
await this.prisma.$transaction(async transaction => {
|
||||
//fetches expired leave requests
|
||||
const expired = await transaction.leaveRequests.findMany({
|
||||
where: { end_date_time: { lt: now } },
|
||||
});
|
||||
if(expired.length === 0) {
|
||||
return;
|
||||
}
|
||||
//copy unto archive table
|
||||
await transaction.leaveRequestsArchive.createMany({
|
||||
data: expired.map(request => ({
|
||||
leave_request_id: request.id,
|
||||
employee_id: request.employee_id,
|
||||
leave_type: request.leave_type,
|
||||
start_date_time: request.start_date_time,
|
||||
end_date_time: request.end_date_time,
|
||||
comment: request.comment,
|
||||
approval_status: request.approval_status,
|
||||
})),
|
||||
});
|
||||
//delete from leave_requests table
|
||||
await transaction.leaveRequests.deleteMany({
|
||||
where: { id: { in: expired.map(request => request.id ) } },
|
||||
});
|
||||
});
|
||||
// TODO: adjust logic to the new LeaveRequests structure
|
||||
}
|
||||
|
||||
//fetches all archived leave-requests
|
||||
async findAllArchived(): Promise<LeaveRequestsArchive[]> {
|
||||
return this.prisma.leaveRequestsArchive.findMany();
|
||||
}
|
||||
|
||||
//remove emp_id and use email
|
||||
//fetches an archived employee
|
||||
async findOneArchived(id: number): Promise<LeaveRequestViewDto> {
|
||||
const row: LeaveRequestArchiveRow | null = await this.prisma.leaveRequestsArchive.findUnique({
|
||||
where: { id },
|
||||
select: leaveRequestsArchiveSelect,
|
||||
});
|
||||
if(!row) throw new NotFoundException(`Archived Leave Request #${id} not found`);
|
||||
if (!row) {
|
||||
throw new NotFoundException(`Archived Leave Request #${id} not found`);
|
||||
}
|
||||
|
||||
const emp = await this.prisma.employees.findUnique({
|
||||
where: { id: row.employee_id },
|
||||
select: { user: {select: { email:true,
|
||||
select: {
|
||||
user: {
|
||||
select: {
|
||||
email: true,
|
||||
first_name: true,
|
||||
last_name: true,
|
||||
}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const email = emp?.user.email ?? "";
|
||||
const full_name = emp ? `${emp.user.first_name} ${emp.user.last_name}` : "";
|
||||
const email = emp?.user.email ?? '';
|
||||
const fullName = emp ? `${emp.user.first_name} ${emp.user.last_name}` : '';
|
||||
|
||||
return mapArchiveRowToViewWithDays(row, email, full_name);
|
||||
return mapArchiveRowToViewWithDays(row, email, fullName);
|
||||
}
|
||||
}
|
||||
|
||||
const toDateOnly = (iso: string): Date => {
|
||||
const date = new Date(iso);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
throw new BadRequestException(`Invalid date: ${iso}`);
|
||||
}
|
||||
date.setHours(0, 0, 0, 0);
|
||||
return date;
|
||||
};
|
||||
|
||||
const toISODateKey = (date: Date): string => date.toISOString().slice(0, 10);
|
||||
|
||||
const normalizeDates = (dates: string[]): string[] =>
|
||||
Array.from(new Set(dates.map((iso) => toISODateKey(toDateOnly(iso)))));
|
||||
|
|
@ -6,11 +6,11 @@ export const leaveRequestsArchiveSelect = {
|
|||
archived_at: true,
|
||||
employee_id: true,
|
||||
leave_type: true,
|
||||
start_date_time: true,
|
||||
end_date_time: true,
|
||||
date: true,
|
||||
payable_hours: true,
|
||||
requested_hours: true,
|
||||
comment: true,
|
||||
approval_status: true,
|
||||
|
||||
} satisfies Prisma.LeaveRequestsArchiveSelect;
|
||||
|
||||
export type LeaveRequestArchiveRow = Prisma.LeaveRequestsArchiveGetPayload<{ select: typeof leaveRequestsArchiveSelect}>;
|
||||
|
|
@ -5,8 +5,9 @@ export const leaveRequestsSelect = {
|
|||
id: true,
|
||||
bank_code_id: true,
|
||||
leave_type: true,
|
||||
start_date_time: true,
|
||||
end_date_time: true,
|
||||
date: true,
|
||||
payable_hours: true,
|
||||
requested_hours: true,
|
||||
comment: true,
|
||||
approval_status: true,
|
||||
employee: { select: {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user