diff --git a/docs/swagger/swagger-spec.json b/docs/swagger/swagger-spec.json index 8aa7528..4e6ef5a 100644 --- a/docs/swagger/swagger-spec.json +++ b/docs/swagger/swagger-spec.json @@ -162,7 +162,31 @@ ] } }, - "/pay-periods/{year}/{periodNumber}/{email}": { + "/pay-periods/crew/pay-period-approval": { + "patch": { + "operationId": "PayPeriodsController_bulkApproval", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkCrewApprovalDto" + } + } + } + }, + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "PayPeriods" + ] + } + }, + "/pay-periods/crew/{year}/{periodNumber}": { "get": { "operationId": "PayPeriodsController_getCrewOverview", "parameters": [ @@ -224,6 +248,37 @@ ] } }, + "/timesheets": { + "get": { + "operationId": "TimesheetController_getTimesheetByIds", + "parameters": [ + { + "name": "year", + "required": true, + "in": "query", + "schema": { + "type": "number" + } + }, + { + "name": "period_number", + "required": true, + "in": "query", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "Timesheet" + ] + } + }, "/preferences": { "patch": { "operationId": "PreferencesController_updatePreferences", @@ -257,37 +312,6 @@ ] } }, - "/timesheets": { - "get": { - "operationId": "TimesheetController_getTimesheetByIds", - "parameters": [ - { - "name": "year", - "required": true, - "in": "query", - "schema": { - "type": "number" - } - }, - { - "name": "period_number", - "required": true, - "in": "query", - "schema": { - "type": "number" - } - } - ], - "responses": { - "200": { - "description": "" - } - }, - "tags": [ - "Timesheet" - ] - } - }, "/shift/create": { "post": { "operationId": "ShiftController_createBatch", @@ -609,6 +633,10 @@ } }, "schemas": { + "BulkCrewApprovalDto": { + "type": "object", + "properties": {} + }, "PreferencesDto": { "type": "object", "properties": {} diff --git a/src/time-and-attendance/expenses/services/expense-upsert.service.ts b/src/time-and-attendance/expenses/services/expense-upsert.service.ts index d48ea50..7f16064 100644 --- a/src/time-and-attendance/expenses/services/expense-upsert.service.ts +++ b/src/time-and-attendance/expenses/services/expense-upsert.service.ts @@ -1,7 +1,6 @@ - import { CreateExpenseResult, UpdateExpensePayload, UpdateExpenseResult, DeleteExpenseResult, NormalizedExpense } from "src/time-and-attendance/utils/type.utils"; import { toDateFromString, toStringFromDate, weekStartSunday } from "src/time-and-attendance/utils/date-time.utils"; -import { Injectable, NotFoundException, Req } from "@nestjs/common"; +import { Injectable, NotFoundException } from "@nestjs/common"; import { expense_select } from "src/time-and-attendance/utils/selects.utils"; import { PrismaService } from "src/prisma/prisma.service"; import { ExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-create.dto"; diff --git a/src/time-and-attendance/pay-period/controllers/pay-periods.controller.ts b/src/time-and-attendance/pay-period/controllers/pay-periods.controller.ts index 65ed977..1d43791 100644 --- a/src/time-and-attendance/pay-period/controllers/pay-periods.controller.ts +++ b/src/time-and-attendance/pay-period/controllers/pay-periods.controller.ts @@ -1,10 +1,9 @@ -import { Controller, Get, Param, ParseBoolPipe, ParseIntPipe, Query, Req } from "@nestjs/common"; -import { PayPeriodDto } from "../dtos/pay-period.dto"; +import { Body, Controller, Get, Param, ParseBoolPipe, ParseIntPipe, Patch, Query, Req, UnauthorizedException } from "@nestjs/common"; import { PayPeriodOverviewDto } from "../dtos/overview-pay-period.dto"; import { PayPeriodsQueryService } from "../services/pay-periods-query.service"; import { RolesAllowed } from "src/common/decorators/roles.decorators"; import { Roles as RoleEnum } from '.prisma/client'; -// import { PayPeriodsCommandService } from "../services/pay-periods-command.service"; +import { PayPeriodsCommandService } from "../services/pay-periods-command.service"; import { PayPeriodBundleDto } from "../dtos/bundle-pay-period.dto"; import { BulkCrewApprovalDto } from "../dtos/bulk-crew-approval.dto"; @@ -14,8 +13,8 @@ export class PayPeriodsController { constructor( private readonly queryService: PayPeriodsQueryService, - // private readonly commandService: PayPeriodsCommandService, - ) {} + private readonly commandService: PayPeriodsCommandService, + ) { } @Get('current-and-all') async getCurrentAndAll(@Query('date') date?: string): Promise { @@ -39,42 +38,32 @@ export class PayPeriodsController { return this.queryService.findOneByYearPeriod(year, period_no); } - // @Patch("crew/bulk-approval") - // //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR) - // @ApiOperation({ summary: "Approve all selected timesheets in the period" }) - // @ApiResponse({ status: 200, description: "Pay period approved" }) - // async bulkApproval(@Body() dto: BulkCrewApprovalDto) { - // return this.commandService.bulkApproveCrew(dto); - // } + @Patch("crew/pay-period-approval") + @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR) + async bulkApproval(@Req() req, @Body() dto: BulkCrewApprovalDto) { + const email = req.user?.email; + if(!email) throw new UnauthorizedException(`Session infos not found`); + return this.commandService.bulkApproveCrew(email, dto); + } - @Get(':year/:periodNumber/:email') - //@RolesAllowed(RoleEnum.SUPERVISOR) - async getCrewOverview( @Req() req, + @Get('crew/:year/:periodNumber') + @RolesAllowed(RoleEnum.SUPERVISOR) + async getCrewOverview(@Req() req, @Param('year', ParseIntPipe) year: number, @Param('periodNumber', ParseIntPipe) period_no: number, @Query('includeSubtree', new ParseBoolPipe({ optional: true })) include_subtree = false, ): Promise { const email = req.user?.email; + if(!email) throw new UnauthorizedException(`Session infos not found`); return this.queryService.getCrewOverview(year, period_no, email, include_subtree); } @Get('overview/:year/:periodNumber') - async getOverviewByYear( - @Param('year', ParseIntPipe) year: number, + @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR) + async getOverviewByYear( + @Param('year', ParseIntPipe) year: number, @Param('periodNumber', ParseIntPipe) period_no: number, ): Promise { return this.queryService.getOverviewByYearPeriod(year, period_no); } - - - //_____________________________________________________________________________________________ - // Deprecated or unused methods - //_____________________________________________________________________________________________ - - // @Get() - // @ApiOperation({ summary: 'Find all pay period' }) - // @ApiResponse({status: 200,description: 'List of pay period found', type: PayPeriodDto, isArray: true }) - // async findAll(): Promise { - // return this.queryService.findAll(); - // } } diff --git a/src/time-and-attendance/pay-period/dtos/bulk-crew-approval.dto.ts b/src/time-and-attendance/pay-period/dtos/bulk-crew-approval.dto.ts index 650927d..4ba5527 100644 --- a/src/time-and-attendance/pay-period/dtos/bulk-crew-approval.dto.ts +++ b/src/time-and-attendance/pay-period/dtos/bulk-crew-approval.dto.ts @@ -16,9 +16,6 @@ export class BulkCrewApprovalItemDto { } export class BulkCrewApprovalDto { - @IsEmail() - supervisor_email: string; - @IsBoolean() include_subtree: boolean = false; diff --git a/src/time-and-attendance/pay-period/pay-periods.module.ts b/src/time-and-attendance/pay-period/pay-periods.module.ts index 3670c72..f971084 100644 --- a/src/time-and-attendance/pay-period/pay-periods.module.ts +++ b/src/time-and-attendance/pay-period/pay-periods.module.ts @@ -1,10 +1,20 @@ import { PayPeriodsQueryService } from "./services/pay-periods-query.service"; import { PayPeriodsController } from "./controllers/pay-periods.controller"; import { Module } from "@nestjs/common"; +import { PayPeriodsCommandService } from "src/time-and-attendance/pay-period/services/pay-periods-command.service"; +import { TimesheetsModule } from "src/time-and-attendance/time-tracker/timesheets/timesheets.module"; +import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; +import { TimesheetApprovalService } from "src/time-and-attendance/time-tracker/timesheets/services/timesheet-approval.service"; @Module({ + imports:[TimesheetsModule], controllers: [PayPeriodsController], - providers: [PayPeriodsQueryService], + providers: [ + PayPeriodsQueryService, + PayPeriodsCommandService, + EmailToIdResolver, + TimesheetApprovalService, + ], }) export class PayperiodsModule {} \ No newline at end of file diff --git a/src/time-and-attendance/pay-period/services/pay-periods-command.service.ts b/src/time-and-attendance/pay-period/services/pay-periods-command.service.ts index 960e1ef..6c48fe2 100644 --- a/src/time-and-attendance/pay-period/services/pay-periods-command.service.ts +++ b/src/time-and-attendance/pay-period/services/pay-periods-command.service.ts @@ -1,71 +1,72 @@ -// import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common"; -// import { PrismaService } from "src/prisma/prisma.service"; -// import { BulkCrewApprovalDto } from "../dtos/bulk-crew-approval.dto"; -// import { PayPeriodsQueryService } from "./pay-periods-query.service"; -// import { TimesheetApprovalService } from "src/modules/timesheets/services/timesheet-approval.service"; +import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common"; +import { PrismaService } from "src/prisma/prisma.service"; +import { BulkCrewApprovalDto } from "../dtos/bulk-crew-approval.dto"; +import { PayPeriodsQueryService } from "./pay-periods-query.service"; +import { TimesheetApprovalService } from "src/time-and-attendance/time-tracker/timesheets/services/timesheet-approval.service"; -// @Injectable() -// export class PayPeriodsCommandService { -// constructor( -// private readonly prisma: PrismaService, -// private readonly timesheets_approval: TimesheetApprovalService, -// private readonly query: PayPeriodsQueryService, -// ) {} +@Injectable() +export class PayPeriodsCommandService { + constructor( + private readonly prisma: PrismaService, + private readonly timesheets_approval: TimesheetApprovalService, + private readonly query: PayPeriodsQueryService, + ) {} -// //function to approve pay-periods according to selected crew members -// async bulkApproveCrew(dto:BulkCrewApprovalDto): Promise<{updated: number}> { -// const { supervisor_email, include_subtree, items } = dto; -// if(!items?.length) throw new BadRequestException('no items to process'); + //function to approve pay-periods according to selected crew members + async bulkApproveCrew(email: string, dto:BulkCrewApprovalDto): Promise<{updated: number}> { + const { include_subtree, items } = dto; + if(!items?.length) throw new BadRequestException('no items to process'); -// //fetch and validate supervisor status -// const supervisor = await this.query.getSupervisor(supervisor_email); -// if(!supervisor) throw new NotFoundException('No employee record linked to current user'); -// if(!supervisor.is_supervisor) throw new ForbiddenException('Employee is not a supervisor'); + //fetch and validate supervisor status + const supervisor = await this.query.getSupervisor(email); + if(!supervisor) throw new NotFoundException('No employee record linked to current user'); + if(!supervisor.is_supervisor) throw new ForbiddenException('Employee is not a supervisor'); -// //fetches emails of crew members linked to supervisor -// const crew_emails = await this.query.resolveCrewEmails(supervisor.id, include_subtree); + //fetches emails of crew members linked to supervisor + const crew_emails = await this.query.resolveCrewEmails(supervisor.id, include_subtree); -// for(const item of items) { -// if(!crew_emails.has(item.employee_email)) { -// throw new ForbiddenException(`Employee ${item.employee_email} not in supervisor crew`); -// } -// } + for(const item of items) { + if(!crew_emails.has(item.employee_email)) { + throw new ForbiddenException(`Employee ${item.employee_email} not in supervisor crew`); + } + } -// const period_cache = new Map(); -// const getPeriod = async (y:number, no: number) => { -// const key = `${y}-${no}`; -// if(!period_cache.has(key)) return period_cache.get(key)!; -// const period = await this.query.getPeriodWindow(y,no); -// if(!period) throw new NotFoundException(`Pay period ${y}-${no} not found`); -// period_cache.set(key, period); -// return period; -// }; + const period_cache = new Map(); + const getPeriod = async (year:number, period_no: number) => { + const key = `${year}-${period_no}`; + if(period_cache.has(key)) return period_cache.get(key)!; -// let updated = 0; + const period = await this.query.getPeriodWindow(year,period_no); + if(!period) throw new NotFoundException(`Pay period ${year}-${period_no} not found`); + period_cache.set(key, period); + return period; + }; -// await this.prisma.$transaction(async (transaction) => { -// for(const item of items) { -// const { period_start, period_end } = await getPeriod(item.pay_year, item.period_no); + let updated = 0; -// const t_sheets = await transaction.timesheets.findMany({ -// where: { -// employee: { user: { email: item.employee_email } }, -// OR: [ -// {shift : { some: { date: { gte: period_start, lte: period_end } } } }, -// {expense: { some: { date: { gte: period_start, lte: period_end } } } }, -// ], -// }, -// select: { id: true }, -// }); + await this.prisma.$transaction(async (transaction) => { + for(const item of items) { + const { period_start, period_end } = await getPeriod(item.pay_year, item.period_no); -// for(const { id } of t_sheets) { -// await this.timesheets_approval.updateApprovalWithTransaction(transaction, id, item.approve); -// updated++; -// } + const t_sheets = await transaction.timesheets.findMany({ + where: { + employee: { user: { email: item.employee_email } }, + OR: [ + {shift : { some: { date: { gte: period_start, lte: period_end } } } }, + {expense: { some: { date: { gte: period_start, lte: period_end } } } }, + ], + }, + select: { id: true }, + }); + + for(const { id } of t_sheets) { + await this.timesheets_approval.cascadeApprovalWithtx(transaction, id, item.approve); + updated++; + } -// } -// }); -// return {updated}; -// } -// } \ No newline at end of file + } + }); + return {updated}; + } +} \ No newline at end of file diff --git a/src/time-and-attendance/time-tracker/schedule-presets/controller/schedule-presets.controller.ts b/src/time-and-attendance/time-tracker/schedule-presets/controller/schedule-presets.controller.ts index 56fc28a..042b8df 100644 --- a/src/time-and-attendance/time-tracker/schedule-presets/controller/schedule-presets.controller.ts +++ b/src/time-and-attendance/time-tracker/schedule-presets/controller/schedule-presets.controller.ts @@ -1,10 +1,11 @@ import { Controller, Param, Query, Body, Get, Post, BadRequestException, ParseIntPipe, Delete, Patch, Req } from "@nestjs/common"; +import { RolesAllowed } from "src/common/decorators/roles.decorators"; import { SchedulePresetsDto } from "src/time-and-attendance/time-tracker/schedule-presets/dtos/create-schedule-presets.dto"; import { SchedulePresetsUpdateDto } from "src/time-and-attendance/time-tracker/schedule-presets/dtos/update-schedule-presets.dto"; import { SchedulePresetsApplyService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-apply.service"; import { SchedulePresetsGetService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-get.service"; import { SchedulePresetsUpsertService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-upsert.service"; - +import { Roles as RoleEnum } from '.prisma/client'; @Controller('schedule-presets') export class SchedulePresetsController { @@ -16,45 +17,39 @@ export class SchedulePresetsController { //used to create a schedule preset @Post('create') - async createPreset( @Req() req, - @Body() dto: SchedulePresetsDto, - ) { + @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ADMIN) + async createPreset( @Req() req, @Body() dto: SchedulePresetsDto ) { const email = req.user?.email; return await this.upsertService.createPreset(email, dto); } //used to update an already existing schedule preset @Patch('update/:preset_id') - async updatePreset( - @Param('preset_id', ParseIntPipe) preset_id: number, - @Body() dto: SchedulePresetsUpdateDto, - ) { + @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ADMIN) + async updatePreset( @Param('preset_id', ParseIntPipe) preset_id: number,@Body() dto: SchedulePresetsUpdateDto ) { return await this.upsertService.updatePreset(preset_id, dto); } //used to delete a schedule preset @Delete('delete/:preset_id') - async deletePreset( - @Param('preset_id') preset_id: number, - ) { + @RolesAllowed(RoleEnum.ADMIN) + async deletePreset( @Param('preset_id') preset_id: number ) { return await this.upsertService.deletePreset(preset_id); } //used to show the list of available schedule presets @Get('find-list') + @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ADMIN) async findListById( @Req() req) { const email = req.user?.email; return this.getService.getSchedulePresets(email); } - //used to apply a preset to a timesheet @Post('apply-presets') - async applyPresets( @Req() req, - @Query('preset') preset_name: string, - @Query('start') start_date: string, - ) { + @RolesAllowed(RoleEnum.EMPLOYEE, RoleEnum.ACCOUNTING, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ADMIN) + async applyPresets( @Req() req, @Query('preset') preset_name: string, @Query('start') start_date: string ) { const email = req.user?.email; if(!preset_name?.trim()) throw new BadRequestException('Query "preset" is required'); if(!start_date?.trim()) throw new BadRequestException('Query "start" is required YYYY-MM-DD'); diff --git a/src/time-and-attendance/time-tracker/shifts/controllers/shift.controller.ts b/src/time-and-attendance/time-tracker/shifts/controllers/shift.controller.ts index 07395f9..767191b 100644 --- a/src/time-and-attendance/time-tracker/shifts/controllers/shift.controller.ts +++ b/src/time-and-attendance/time-tracker/shifts/controllers/shift.controller.ts @@ -3,16 +3,16 @@ import { ShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift import { UpdateShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-update.dto"; import { ShiftsUpsertService } from "src/time-and-attendance/time-tracker/shifts/services/shifts-upsert.service"; import { CreateShiftResult, UpdateShiftResult } from "src/time-and-attendance/utils/type.utils"; - +import { Roles as RoleEnum } from '.prisma/client'; +import { RolesAllowed } from "src/common/decorators/roles.decorators"; @Controller('shift') export class ShiftController { constructor( private readonly upsert_service: ShiftsUpsertService ){} @Post('create') - createBatch( - @Req() req, - @Body()dtos: ShiftDto[]): Promise { + @RolesAllowed(RoleEnum.EMPLOYEE, RoleEnum.ACCOUNTING, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ADMIN) + createBatch( @Req() req, @Body()dtos: ShiftDto[]): Promise { const email = req.user?.email; const list = Array.isArray(dtos) ? dtos : []; if(list.length === 0) throw new BadRequestException('Body is missing or invalid (create shifts)'); @@ -22,14 +22,15 @@ export class ShiftController { //change Body to receive dtos @Patch('update') - updateBatch( - @Body() dtos: UpdateShiftDto[]): Promise{ + @RolesAllowed(RoleEnum.EMPLOYEE, RoleEnum.ACCOUNTING, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ADMIN) + updateBatch( @Body() dtos: UpdateShiftDto[]): Promise{ const list = Array.isArray(dtos) ? dtos: []; if(list.length === 0) throw new BadRequestException('Body is missing or invalid (update shifts)'); return this.upsert_service.updateShifts(dtos); } @Delete(':shift_id') + @RolesAllowed(RoleEnum.EMPLOYEE, RoleEnum.ACCOUNTING, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ADMIN) remove(@Param('shift_id') shift_id: number ) { return this.upsert_service.deleteShift(shift_id); } diff --git a/src/time-and-attendance/time-tracker/timesheets/controllers/timesheet.controller.ts b/src/time-and-attendance/time-tracker/timesheets/controllers/timesheet.controller.ts index 220d068..ffde38b 100644 --- a/src/time-and-attendance/time-tracker/timesheets/controllers/timesheet.controller.ts +++ b/src/time-and-attendance/time-tracker/timesheets/controllers/timesheet.controller.ts @@ -11,12 +11,9 @@ export class TimesheetController { @Get() @RolesAllowed(RoleEnum.SUPERVISOR, RoleEnum.HR, RoleEnum.ACCOUNTING, RoleEnum.ADMIN) async getTimesheetByIds( - @Req() req, - @Query('year', ParseIntPipe) year: number, - @Query('period_number', ParseIntPipe) period_number: number, - ) { + @Req() req, @Query('year', ParseIntPipe) year:number, @Query('period_number', ParseIntPipe) period_number: number) { const email = req.user?.email; - if(!email) throw new UnauthorizedException('Unauthorized User'); + if(!email) throw new UnauthorizedException('Unauthorized User');  return this.timesheetOverview.getTimesheetsForEmployeeByPeriod(email, year, period_number); } } diff --git a/src/time-and-attendance/time-tracker/timesheets/services/timesheet-approval.service.ts b/src/time-and-attendance/time-tracker/timesheets/services/timesheet-approval.service.ts index 088b5b7..84756c2 100644 --- a/src/time-and-attendance/time-tracker/timesheets/services/timesheet-approval.service.ts +++ b/src/time-and-attendance/time-tracker/timesheets/services/timesheet-approval.service.ts @@ -27,11 +27,11 @@ import { Injectable } from "@nestjs/common"; const timesheet = await this.updateApprovalWithTransaction(transaction, timesheetId, isApproved); await transaction.shifts.updateMany({ where: { timesheet_id: timesheetId }, - data: { is_approved: isApproved }, + data: { is_approved: isApproved }, }); await transaction.expenses.updateManyAndReturn({ where: { timesheet_id: timesheetId }, - data: { is_approved: isApproved }, + data: { is_approved: isApproved }, }); return timesheet; } diff --git a/src/time-and-attendance/time-tracker/timesheets/timesheets.module.ts b/src/time-and-attendance/time-tracker/timesheets/timesheets.module.ts index a12322a..761a78e 100644 --- a/src/time-and-attendance/time-tracker/timesheets/timesheets.module.ts +++ b/src/time-and-attendance/time-tracker/timesheets/timesheets.module.ts @@ -1,14 +1,19 @@ import { Module } from '@nestjs/common'; import { TimesheetController } from 'src/time-and-attendance/time-tracker/timesheets/controllers/timesheet.controller'; +import { TimesheetApprovalService } from 'src/time-and-attendance/time-tracker/timesheets/services/timesheet-approval.service'; import { TimesheetArchiveService } from 'src/time-and-attendance/time-tracker/timesheets/services/timesheet-archive.service'; import { GetTimesheetsOverviewService } from 'src/time-and-attendance/time-tracker/timesheets/services/timesheet-get-overview.service'; +import { EmailToIdResolver } from 'src/time-and-attendance/utils/resolve-email-id.utils'; @Module({ + controllers: [TimesheetController], providers: [ TimesheetArchiveService, GetTimesheetsOverviewService, + TimesheetApprovalService, + EmailToIdResolver, ], exports: [], })