refactor(module): rename and refactor services
This commit is contained in:
parent
6139d335c3
commit
4c880e47bf
|
|
@ -3,7 +3,7 @@
|
||||||
"paths": {
|
"paths": {
|
||||||
"/": {
|
"/": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "ShiftsOverviewController_getSummary",
|
"operationId": "AppController_getHello",
|
||||||
"parameters": [],
|
"parameters": [],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tags": [
|
"tags": [
|
||||||
"ShiftsOverview"
|
"App"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -835,24 +835,11 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "ShiftsController_findAll",
|
"operationId": "ShiftsController_getSummary",
|
||||||
"parameters": [],
|
"parameters": [],
|
||||||
"responses": {
|
"responses": {
|
||||||
"201": {
|
"200": {
|
||||||
"description": "List of shifts found",
|
"description": ""
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/components/schemas/CreateShiftDto"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "List of shifts not found"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"security": [
|
"security": [
|
||||||
|
|
@ -860,7 +847,6 @@
|
||||||
"access-token": []
|
"access-token": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"summary": "Find all shifts",
|
|
||||||
"tags": [
|
"tags": [
|
||||||
"Shifts"
|
"Shifts"
|
||||||
]
|
]
|
||||||
|
|
@ -1017,17 +1003,22 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/export.csv": {
|
"/shifts/export.csv": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "ShiftsOverviewController_exportCsv",
|
"operationId": "ShiftsController_exportCsv",
|
||||||
"parameters": [],
|
"parameters": [],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": ""
|
"description": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"access-token": []
|
||||||
|
}
|
||||||
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"ShiftsOverview"
|
"Shifts"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import { ScheduleModule } from '@nestjs/schedule';
|
||||||
import { ShiftsModule } from './modules/shifts/shifts.module';
|
import { ShiftsModule } from './modules/shifts/shifts.module';
|
||||||
import { TimesheetsModule } from './modules/timesheets/timesheets.module';
|
import { TimesheetsModule } from './modules/timesheets/timesheets.module';
|
||||||
import { UsersModule } from './modules/users-management/users.module';
|
import { UsersModule } from './modules/users-management/users.module';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
|
@ -28,6 +29,7 @@ import { UsersModule } from './modules/users-management/users.module';
|
||||||
AuthenticationModule,
|
AuthenticationModule,
|
||||||
BankCodesModule,
|
BankCodesModule,
|
||||||
BusinessLogicsModule,
|
BusinessLogicsModule,
|
||||||
|
ConfigModule.forRoot({isGlobal: true}),
|
||||||
CsvExportModule,
|
CsvExportModule,
|
||||||
CustomersModule,
|
CustomersModule,
|
||||||
EmployeesModule,
|
EmployeesModule,
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,13 @@ import { UseGuards, Controller, Get, Param, ParseIntPipe, NotFoundException } fr
|
||||||
import { ApiTags, ApiOperation, ApiResponse } from "@nestjs/swagger";
|
import { ApiTags, ApiOperation, ApiResponse } from "@nestjs/swagger";
|
||||||
import { ExpensesArchive,Roles as RoleEnum } from "@prisma/client";
|
import { ExpensesArchive,Roles as RoleEnum } from "@prisma/client";
|
||||||
import { RolesAllowed } from "src/common/decorators/roles.decorators";
|
import { RolesAllowed } from "src/common/decorators/roles.decorators";
|
||||||
import { ExpensesService } from "src/modules/expenses/services/expenses.service";
|
import { ExpensesQueryService } from "src/modules/expenses/services/expenses-query.service";
|
||||||
|
|
||||||
@ApiTags('Expense Archives')
|
@ApiTags('Expense Archives')
|
||||||
// @UseGuards()
|
// @UseGuards()
|
||||||
@Controller('archives/expenses')
|
@Controller('archives/expenses')
|
||||||
export class ExpensesArchiveController {
|
export class ExpensesArchiveController {
|
||||||
constructor(private readonly expensesService: ExpensesService) {}
|
constructor(private readonly expensesService: ExpensesQueryService) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,13 @@ import { Get, Param, ParseIntPipe, NotFoundException, Controller, UseGuards } fr
|
||||||
import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
|
import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
|
||||||
import { ShiftsArchive, Roles as RoleEnum } from "@prisma/client";
|
import { ShiftsArchive, Roles as RoleEnum } from "@prisma/client";
|
||||||
import { RolesAllowed } from "src/common/decorators/roles.decorators";
|
import { RolesAllowed } from "src/common/decorators/roles.decorators";
|
||||||
import { ShiftsService } from "src/modules/shifts/services/shifts.service";
|
import { ShiftsQueryService } from "src/modules/shifts/services/shifts-query.service";
|
||||||
|
|
||||||
@ApiTags('Shift Archives')
|
@ApiTags('Shift Archives')
|
||||||
// @UseGuards()
|
// @UseGuards()
|
||||||
@Controller('archives/shifts')
|
@Controller('archives/shifts')
|
||||||
export class ShiftsArchiveController {
|
export class ShiftsArchiveController {
|
||||||
constructor(private readonly shiftsService:ShiftsService) {}
|
constructor(private readonly shiftsService:ShiftsQueryService) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,13 @@ import { Controller, Get, NotFoundException, Param, ParseIntPipe, UseGuards } fr
|
||||||
import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
|
import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
|
||||||
import { RolesAllowed } from "src/common/decorators/roles.decorators";
|
import { RolesAllowed } from "src/common/decorators/roles.decorators";
|
||||||
import { TimesheetsArchive, Roles as RoleEnum } from '@prisma/client';
|
import { TimesheetsArchive, Roles as RoleEnum } from '@prisma/client';
|
||||||
import { TimesheetsService } from "src/modules/timesheets/services/timesheets.service";
|
import { TimesheetsQueryService } from "src/modules/timesheets/services/timesheets-query.service";
|
||||||
|
|
||||||
@ApiTags('Timesheet Archives')
|
@ApiTags('Timesheet Archives')
|
||||||
// @UseGuards()
|
// @UseGuards()
|
||||||
@Controller('archives/timesheets')
|
@Controller('archives/timesheets')
|
||||||
export class TimesheetsArchiveController {
|
export class TimesheetsArchiveController {
|
||||||
constructor(private readonly timesheetsService: TimesheetsService) {}
|
constructor(private readonly timesheetsService: TimesheetsQueryService) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
import { Injectable, Logger } from "@nestjs/common";
|
import { Injectable, Logger } from "@nestjs/common";
|
||||||
import { Cron } from "@nestjs/schedule";
|
import { Cron } from "@nestjs/schedule";
|
||||||
import { ExpensesService } from "src/modules/expenses/services/expenses.service";
|
import { ExpensesQueryService } from "src/modules/expenses/services/expenses-query.service";
|
||||||
import { LeaveRequestsService } from "src/modules/leave-requests/services/leave-requests.service";
|
import { LeaveRequestsService } from "src/modules/leave-requests/services/leave-requests.service";
|
||||||
import { ShiftsService } from "src/modules/shifts/services/shifts.service";
|
import { ShiftsQueryService } from "src/modules/shifts/services/shifts-query.service";
|
||||||
import { TimesheetsService } from "src/modules/timesheets/services/timesheets.service";
|
import { TimesheetsQueryService } from "src/modules/timesheets/services/timesheets-query.service";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ArchivalService {
|
export class ArchivalService {
|
||||||
private readonly logger = new Logger(ArchivalService.name);
|
private readonly logger = new Logger(ArchivalService.name);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly timesheetsService: TimesheetsService,
|
private readonly timesheetsService: TimesheetsQueryService,
|
||||||
private readonly expensesService: ExpensesService,
|
private readonly expensesService: ExpensesQueryService,
|
||||||
private readonly shiftsService: ShiftsService,
|
private readonly shiftsService: ShiftsQueryService,
|
||||||
private readonly leaveRequestsService: LeaveRequestsService,
|
private readonly leaveRequestsService: LeaveRequestsService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { AuthentikAuthService } from './services/authentik-auth.service';
|
||||||
import { UsersModule } from '../users-management/users.module';
|
import { UsersModule } from '../users-management/users.module';
|
||||||
import { AuthController } from './controllers/auth.controller';
|
import { AuthController } from './controllers/auth.controller';
|
||||||
import { AuthentikStrategy } from './strategies/authentik.strategy';
|
import { AuthentikStrategy } from './strategies/authentik.strategy';
|
||||||
import { ExpressSessionSerializer } from './services/express-session.serializer';
|
import { ExpressSessionSerializer } from './serializers/express-session.serializer';
|
||||||
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
import { BankCodesControllers } from "./controllers/bank-codes.controller";
|
import { BankCodesControllers } from "./controllers/bank-codes.controller";
|
||||||
import { BankCodesService } from "./services/bank-codes.services";
|
import { BankCodesService } from "./services/bank-codes.service";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [BankCodesControllers],
|
controllers: [BankCodesControllers],
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post } from "@nestjs/common";
|
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post } from "@nestjs/common";
|
||||||
import { BankCodesService } from "../services/bank-codes.services";
|
import { BankCodesService } from "../services/bank-codes.service";
|
||||||
import { CreateBankCodeDto } from "../dtos/create-bank-codes";
|
import { CreateBankCodeDto } from "../dtos/create-bank-code.dto";
|
||||||
import { UpdateBankCodeDto } from "../dtos/update-bank-codes";
|
import { UpdateBankCodeDto } from "../dtos/update-bank-code.dto";
|
||||||
import { ApiBadRequestResponse, ApiNotFoundResponse, ApiOperation, ApiResponse } from "@nestjs/swagger";
|
import { ApiBadRequestResponse, ApiNotFoundResponse, ApiOperation, ApiResponse } from "@nestjs/swagger";
|
||||||
|
|
||||||
@Controller('bank-codes')
|
@Controller('bank-codes')
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { PartialType } from "@nestjs/swagger";
|
import { PartialType } from "@nestjs/swagger";
|
||||||
import { CreateBankCodeDto } from "./create-bank-codes";
|
import { CreateBankCodeDto } from "./create-bank-code.dto";
|
||||||
|
|
||||||
export class UpdateBankCodeDto extends PartialType(CreateBankCodeDto) {}
|
export class UpdateBankCodeDto extends PartialType(CreateBankCodeDto) {}
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
import { CreateBankCodeDto } from "../dtos/create-bank-codes";
|
import { CreateBankCodeDto } from "../dtos/create-bank-code.dto";
|
||||||
import { BankCodes } from "@prisma/client";
|
import { BankCodes } from "@prisma/client";
|
||||||
import { UpdateBankCodeDto } from "../dtos/update-bank-codes";
|
import { UpdateBankCodeDto } from "../dtos/update-bank-code.dto";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BankCodesService {
|
export class BankCodesService {
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import { Body, Controller, Delete, Get, Param, ParseBoolPipe, ParseIntPipe, Patch, Post, Query, UseGuards, UsePipes, ValidationPipe } from "@nestjs/common";
|
import { Body, Controller, Delete, Get, Param, ParseBoolPipe, ParseIntPipe, Patch, Post, Query, UseGuards, UsePipes, ValidationPipe } from "@nestjs/common";
|
||||||
import { ExpensesService } from "../services/expenses.service";
|
import { ExpensesQueryService } from "../services/expenses-query.service";
|
||||||
import { CreateExpenseDto } from "../dtos/create-expense.dto";
|
import { CreateExpenseDto } from "../dtos/create-expense.dto";
|
||||||
import { Expenses } from "@prisma/client";
|
import { Expenses } from "@prisma/client";
|
||||||
import { Roles as RoleEnum } from '.prisma/client';
|
import { Roles as RoleEnum } from '.prisma/client';
|
||||||
import { UpdateExpenseDto } from "../dtos/update-expense.dto";
|
import { UpdateExpenseDto } from "../dtos/update-expense.dto";
|
||||||
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
|
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
|
||||||
import { RolesAllowed } from "src/common/decorators/roles.decorators";
|
import { RolesAllowed } from "src/common/decorators/roles.decorators";
|
||||||
import { ExpensesApprovalService } from "../services/expenses-command.service";
|
import { ExpensesCommandService } from "../services/expenses-command.service";
|
||||||
import { SearchExpensesDto } from "../dtos/search-expense.dto";
|
import { SearchExpensesDto } from "../dtos/search-expense.dto";
|
||||||
|
|
||||||
@ApiTags('Expenses')
|
@ApiTags('Expenses')
|
||||||
|
|
@ -15,8 +15,8 @@ import { SearchExpensesDto } from "../dtos/search-expense.dto";
|
||||||
@Controller('Expenses')
|
@Controller('Expenses')
|
||||||
export class ExpensesController {
|
export class ExpensesController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly expensesService: ExpensesService,
|
private readonly expensesService: ExpensesQueryService,
|
||||||
private readonly expensesApprovalService: ExpensesApprovalService,
|
private readonly expensesApprovalService: ExpensesCommandService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
import { ExpensesController } from "./controllers/expenses.controller";
|
import { ExpensesController } from "./controllers/expenses.controller";
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
import { ExpensesService } from "./services/expenses.service";
|
import { ExpensesQueryService } from "./services/expenses-query.service";
|
||||||
import { BusinessLogicsModule } from "src/modules/business-logics/business-logics.module";
|
import { BusinessLogicsModule } from "src/modules/business-logics/business-logics.module";
|
||||||
import { ExpensesApprovalService } from "./services/expenses-command.service";
|
import { ExpensesCommandService } from "./services/expenses-command.service";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [BusinessLogicsModule],
|
imports: [BusinessLogicsModule],
|
||||||
controllers: [ExpensesController],
|
controllers: [ExpensesController],
|
||||||
providers: [ExpensesService, ExpensesApprovalService],
|
providers: [ExpensesQueryService, ExpensesCommandService],
|
||||||
exports: [ ExpensesService ],
|
exports: [ ExpensesQueryService ],
|
||||||
})
|
})
|
||||||
|
|
||||||
export class ExpensesModule {}
|
export class ExpensesModule {}
|
||||||
|
|
@ -4,7 +4,7 @@ import { BaseApprovalService } from "src/common/shared/base-approval.service";
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ExpensesApprovalService extends BaseApprovalService<Expenses> {
|
export class ExpensesCommandService extends BaseApprovalService<Expenses> {
|
||||||
constructor(prisma: PrismaService) { super(prisma); }
|
constructor(prisma: PrismaService) { super(prisma); }
|
||||||
|
|
||||||
protected get delegate() {
|
protected get delegate() {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { SearchExpensesDto } from "../dtos/search-expense.dto";
|
||||||
import { buildPrismaWhere } from "src/common/shared/build-prisma-where";
|
import { buildPrismaWhere } from "src/common/shared/build-prisma-where";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ExpensesService {
|
export class ExpensesQueryService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
private readonly mileageService: MileageService,
|
private readonly mileageService: MileageService,
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import { BadRequestException, Body, Controller, Delete, Get, Param, ParseBoolPipe, ParseEnumPipe, ParseIntPipe, Patch, Post, Query, UseGuards, UsePipes, ValidationPipe } from "@nestjs/common";
|
import { BadRequestException, Body, Controller, Delete, Get, Param, ParseBoolPipe, ParseEnumPipe, ParseIntPipe, Patch, Post, Query, UseGuards, UsePipes, ValidationPipe } from "@nestjs/common";
|
||||||
import { LeaveRequestsService } from "../services/leave-requests.service";
|
import { LeaveRequestsService } from "../services/leave-requests.service";
|
||||||
import { CreateLeaveRequestsDto } from "../dtos/create-leave-requests.dto";
|
import { CreateLeaveRequestsDto } from "../dtos/create-leave-request.dto";
|
||||||
import { LeaveRequests } from "@prisma/client";
|
import { LeaveRequests } from "@prisma/client";
|
||||||
import { UpdateLeaveRequestsDto } from "../dtos/update-leave-requests.dto";
|
import { UpdateLeaveRequestsDto } from "../dtos/update-leave-request.dto";
|
||||||
import { RolesAllowed } from "src/common/decorators/roles.decorators";
|
import { RolesAllowed } from "src/common/decorators/roles.decorators";
|
||||||
import { LeaveApprovalStatus, Roles as RoleEnum } from '.prisma/client';
|
import { LeaveApprovalStatus, Roles as RoleEnum } from '.prisma/client';
|
||||||
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
|
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
|
||||||
import { SearchLeaveRequestsDto } from "../dtos/search-leave-requests.dto";
|
import { SearchLeaveRequestsDto } from "../dtos/search-leave-request.dto";
|
||||||
|
|
||||||
@ApiTags('Leave Requests')
|
@ApiTags('Leave Requests')
|
||||||
@ApiBearerAuth('access-token')
|
@ApiBearerAuth('access-token')
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { PartialType } from "@nestjs/swagger";
|
import { PartialType } from "@nestjs/swagger";
|
||||||
import { CreateLeaveRequestsDto } from "./create-leave-requests.dto";
|
import { CreateLeaveRequestsDto } from "./create-leave-request.dto";
|
||||||
|
|
||||||
export class UpdateLeaveRequestsDto extends PartialType(CreateLeaveRequestsDto){}
|
export class UpdateLeaveRequestsDto extends PartialType(CreateLeaveRequestsDto){}
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
|
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
import { CreateLeaveRequestsDto } from "../dtos/create-leave-requests.dto";
|
import { CreateLeaveRequestsDto } from "../dtos/create-leave-request.dto";
|
||||||
import { LeaveRequests, LeaveRequestsArchive } from "@prisma/client";
|
import { LeaveRequests, LeaveRequestsArchive } from "@prisma/client";
|
||||||
import { UpdateLeaveRequestsDto } from "../dtos/update-leave-requests.dto";
|
import { UpdateLeaveRequestsDto } from "../dtos/update-leave-request.dto";
|
||||||
import { HolidayService } from "src/modules/business-logics/services/holiday.service";
|
import { HolidayService } from "src/modules/business-logics/services/holiday.service";
|
||||||
import { SickLeaveService } from "src/modules/business-logics/services/sick-leave.service";
|
import { SickLeaveService } from "src/modules/business-logics/services/sick-leave.service";
|
||||||
import { VacationService } from "src/modules/business-logics/services/vacation.service";
|
import { VacationService } from "src/modules/business-logics/services/vacation.service";
|
||||||
import { SearchLeaveRequestsDto } from "../dtos/search-leave-requests.dto";
|
import { SearchLeaveRequestsDto } from "../dtos/search-leave-request.dto";
|
||||||
import { buildPrismaWhere } from "src/common/shared/build-prisma-where";
|
import { buildPrismaWhere } from "src/common/shared/build-prisma-where";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Injectable, Logger } from "@nestjs/common";
|
import { Injectable, Logger } from "@nestjs/common";
|
||||||
import { Subject } from "rxjs";
|
import { Subject } from "rxjs";
|
||||||
import { NotificationCard } from "../dtos/notifications.types";
|
import { NotificationCard } from "../dtos/notification.types";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NotificationsService {
|
export class NotificationsService {
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@ import { OAuthSessions } from '@prisma/client';
|
||||||
import { RolesAllowed } from "src/common/decorators/roles.decorators";
|
import { RolesAllowed } from "src/common/decorators/roles.decorators";
|
||||||
import { Roles as RoleEnum } from '.prisma/client';
|
import { Roles as RoleEnum } from '.prisma/client';
|
||||||
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||||
import { CreateOauthSessionDto } from '../dtos/create-oauth-sessions.dto';
|
import { CreateOauthSessionDto } from '../dtos/create-oauth-session.dto';
|
||||||
import { OauthSessionsService } from '../services/oauth-sessions.service';
|
import { OauthSessionsService } from '../services/oauth-sessions.service';
|
||||||
import { UpdateOauthSessionDto } from '../dtos/update-oauth-sessions.dto';
|
import { UpdateOauthSessionDto } from '../dtos/update-oauth-session.dto';
|
||||||
|
|
||||||
@ApiTags('OAuth Sessions')
|
@ApiTags('OAuth Sessions')
|
||||||
@ApiBearerAuth('sessions')
|
@ApiBearerAuth('sessions')
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { PartialType } from "@nestjs/swagger";
|
import { PartialType } from "@nestjs/swagger";
|
||||||
import { CreateOauthSessionDto } from "./create-oauth-sessions.dto";
|
import { CreateOauthSessionDto } from "./create-oauth-session.dto";
|
||||||
|
|
||||||
export class UpdateOauthSessionDto extends PartialType(CreateOauthSessionDto) {}
|
export class UpdateOauthSessionDto extends PartialType(CreateOauthSessionDto) {}
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
import { CreateOauthSessionDto } from '../dtos/create-oauth-sessions.dto';
|
import { CreateOauthSessionDto } from '../dtos/create-oauth-session.dto';
|
||||||
import { OAuthSessions } from '@prisma/client';
|
import { OAuthSessions } from '@prisma/client';
|
||||||
import { UpdateOauthSessionDto } from '../dtos/update-oauth-sessions.dto';
|
import { UpdateOauthSessionDto } from '../dtos/update-oauth-session.dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class OauthSessionsService {
|
export class OauthSessionsService {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { Controller, ForbiddenException, Get, Param, ParseBoolPipe, ParseIntPipe, Patch, Query } from "@nestjs/common";
|
import { Controller, ForbiddenException, Get, Param, ParseBoolPipe, ParseIntPipe, Patch, Query } from "@nestjs/common";
|
||||||
import { ApiNotFoundResponse, ApiOperation, ApiParam, ApiQuery, ApiResponse, ApiTags } from "@nestjs/swagger";
|
import { ApiNotFoundResponse, ApiOperation, ApiParam, ApiQuery, ApiResponse, ApiTags } from "@nestjs/swagger";
|
||||||
import { PayPeriodsService } from "../services/pay-periods.service";
|
|
||||||
import { PayPeriodDto } from "../dtos/pay-period.dto";
|
import { PayPeriodDto } from "../dtos/pay-period.dto";
|
||||||
import { PayPeriodOverviewDto } from "../dtos/overview-pay-period.dto";
|
import { PayPeriodOverviewDto } from "../dtos/overview-pay-period.dto";
|
||||||
import { PayPeriodsQueryService } from "../services/pay-periods-query.service";
|
import { PayPeriodsQueryService } from "../services/pay-periods-query.service";
|
||||||
|
|
@ -10,23 +9,22 @@ import { Req } from '@nestjs/common';
|
||||||
import { Request } from 'express';
|
import { Request } from 'express';
|
||||||
import { PayPeriodsCommandService } from "../services/pay-periods-command.service";
|
import { PayPeriodsCommandService } from "../services/pay-periods-command.service";
|
||||||
import { REPL_MODE_STRICT } from "repl";
|
import { REPL_MODE_STRICT } from "repl";
|
||||||
import { PayPeriodBundleDto } from "../dtos/bundle-pay-periods.dto";
|
import { PayPeriodBundleDto } from "../dtos/bundle-pay-period.dto";
|
||||||
|
|
||||||
@ApiTags('pay-periods')
|
@ApiTags('pay-periods')
|
||||||
@Controller('pay-periods')
|
@Controller('pay-periods')
|
||||||
export class PayPeriodsController {
|
export class PayPeriodsController {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly payPeriodsService: PayPeriodsService,
|
private readonly queryService: PayPeriodsQueryService,
|
||||||
private readonly queryService: PayPeriodsQueryService,
|
private readonly commandService: PayPeriodsCommandService,
|
||||||
private readonly commandService: PayPeriodsCommandService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@ApiOperation({ summary: 'Find all pay period' })
|
@ApiOperation({ summary: 'Find all pay period' })
|
||||||
@ApiResponse({status: 200,description: 'List of pay period found', type: PayPeriodDto, isArray: true })
|
@ApiResponse({status: 200,description: 'List of pay period found', type: PayPeriodDto, isArray: true })
|
||||||
async findAll(): Promise<PayPeriodDto[]> {
|
async findAll(): Promise<PayPeriodDto[]> {
|
||||||
return this.payPeriodsService.findAll();
|
return this.queryService.findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('bundle/current-and-all')
|
@Get('bundle/current-and-all')
|
||||||
|
|
@ -35,8 +33,8 @@ export class PayPeriodsController {
|
||||||
@ApiResponse({status: 200, description:'Find current and all pay periods', type: PayPeriodBundleDto})
|
@ApiResponse({status: 200, description:'Find current and all pay periods', type: PayPeriodBundleDto})
|
||||||
async getCurrentAndAll(@Query('date') date?: string): Promise<PayPeriodBundleDto> {
|
async getCurrentAndAll(@Query('date') date?: string): Promise<PayPeriodBundleDto> {
|
||||||
const [current, periods] = await Promise.all([
|
const [current, periods] = await Promise.all([
|
||||||
this.payPeriodsService.findCurrent(date),
|
this.queryService.findCurrent(date),
|
||||||
this.payPeriodsService.findAll(),
|
this.queryService.findAll(),
|
||||||
]);
|
]);
|
||||||
return { current, periods };
|
return { current, periods };
|
||||||
}
|
}
|
||||||
|
|
@ -46,7 +44,7 @@ export class PayPeriodsController {
|
||||||
@ApiResponse({ status: 200, description: "Pay period found for the selected date", type: PayPeriodDto })
|
@ApiResponse({ status: 200, description: "Pay period found for the selected date", type: PayPeriodDto })
|
||||||
@ApiNotFoundResponse({ description: "Pay period not found for the selected date" })
|
@ApiNotFoundResponse({ description: "Pay period not found for the selected date" })
|
||||||
async findByDate(@Param("date") date: string) {
|
async findByDate(@Param("date") date: string) {
|
||||||
return this.payPeriodsService.findByDate(date);
|
return this.queryService.findByDate(date);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(":year/:periodNumber")
|
@Get(":year/:periodNumber")
|
||||||
|
|
@ -59,7 +57,7 @@ export class PayPeriodsController {
|
||||||
@Param("year", ParseIntPipe) year: number,
|
@Param("year", ParseIntPipe) year: number,
|
||||||
@Param("periodNumber", ParseIntPipe) periodNumber: number,
|
@Param("periodNumber", ParseIntPipe) periodNumber: number,
|
||||||
) {
|
) {
|
||||||
return this.payPeriodsService.findOneByYearPeriod(year, periodNumber);
|
return this.queryService.findOneByYearPeriod(year, periodNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch(":year/:periodNumber/approval")
|
@Patch(":year/:periodNumber/approval")
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,28 @@
|
||||||
import { PrismaModule } from "src/prisma/prisma.module";
|
import { PrismaModule } from "src/prisma/prisma.module";
|
||||||
import { PayPeriodsService } from "./services/pay-periods.service";
|
|
||||||
import { PayPeriodsController } from "./controllers/pay-periods.controller";
|
import { PayPeriodsController } from "./controllers/pay-periods.controller";
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
import { PayPeriodsCommandService } from "./services/pay-periods-command.service";
|
import { PayPeriodsCommandService } from "./services/pay-periods-command.service";
|
||||||
import { PayPeriodsQueryService } from "./services/pay-periods-query.service";
|
import { PayPeriodsQueryService } from "./services/pay-periods-query.service";
|
||||||
import { TimesheetsModule } from "../timesheets/timesheets.module";
|
import { TimesheetsModule } from "../timesheets/timesheets.module";
|
||||||
import { TimesheetsCommandService } from "../timesheets/services/timesheets-command.service";
|
import { TimesheetsCommandService } from "../timesheets/services/timesheets-command.service";
|
||||||
import { ExpensesApprovalService } from "../expenses/services/expenses-command.service";
|
import { ExpensesCommandService } from "../expenses/services/expenses-command.service";
|
||||||
import { ShiftsApprovalService } from "../shifts/services/shifts-command.service";
|
import { ShiftsCommandService } from "../shifts/services/shifts-command.service";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule, TimesheetsModule],
|
imports: [PrismaModule, TimesheetsModule],
|
||||||
providers: [
|
providers: [
|
||||||
PayPeriodsService,
|
PayPeriodsQueryService,
|
||||||
PayPeriodsQueryService,
|
PayPeriodsQueryService,
|
||||||
PayPeriodsCommandService,
|
PayPeriodsCommandService,
|
||||||
TimesheetsCommandService,
|
TimesheetsCommandService,
|
||||||
ExpensesApprovalService,
|
ExpensesCommandService,
|
||||||
ShiftsApprovalService,
|
ShiftsCommandService,
|
||||||
],
|
],
|
||||||
controllers: [PayPeriodsController],
|
controllers: [PayPeriodsController],
|
||||||
exports: [
|
exports: [
|
||||||
PayPeriodsQueryService,
|
PayPeriodsQueryService,
|
||||||
PayPeriodsCommandService,
|
PayPeriodsCommandService,
|
||||||
PayPeriodsService,
|
PayPeriodsQueryService,
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,15 @@ import { PrismaService } from "src/prisma/prisma.service";
|
||||||
import { computeHours } from "src/common/utils/date-utils";
|
import { computeHours } from "src/common/utils/date-utils";
|
||||||
import { PayPeriodOverviewDto } from "../dtos/overview-pay-period.dto";
|
import { PayPeriodOverviewDto } from "../dtos/overview-pay-period.dto";
|
||||||
import { EmployeePeriodOverviewDto } from "../dtos/overview-employee-period.dto";
|
import { EmployeePeriodOverviewDto } from "../dtos/overview-employee-period.dto";
|
||||||
import { computePeriod } from "../utils/pay-year.util";
|
import { computePeriod, listPayYear, payYearOfDate } from "../utils/pay-year.util";
|
||||||
|
import { PayPeriodDto } from "../dtos/pay-period.dto";
|
||||||
|
import { mapPayPeriodToDto } from "../mappers/pay-periods.mapper";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PayPeriodsQueryService {
|
export class PayPeriodsQueryService {
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
constructor(
|
||||||
|
private readonly prisma: PrismaService,
|
||||||
|
) {}
|
||||||
|
|
||||||
async getOverview(periodNumber: number): Promise<PayPeriodOverviewDto> {
|
async getOverview(periodNumber: number): Promise<PayPeriodOverviewDto> {
|
||||||
const period = await this.prisma.payPeriods.findFirst({
|
const period = await this.prisma.payPeriods.findFirst({
|
||||||
|
|
@ -177,52 +181,107 @@ export class PayPeriodsQueryService {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async getCrewOverview(year: number, periodNumber: number, userId: string, includeSubtree: boolean): Promise<PayPeriodOverviewDto> {
|
async getCrewOverview(year: number, periodNumber: number, userId: string, includeSubtree: boolean): Promise<PayPeriodOverviewDto> {
|
||||||
// 1) Trouver la période
|
// 1) Trouver la période
|
||||||
const period = await this.prisma.payPeriods.findFirst({ where: { year, period_number: periodNumber } });
|
const period = await this.prisma.payPeriods.findFirst({ where: { year, period_number: periodNumber } });
|
||||||
if (!period) throw new NotFoundException(`Pay period ${year}-${periodNumber} not found`);
|
if (!period) throw new NotFoundException(`Pay period ${year}-${periodNumber} not found`);
|
||||||
|
|
||||||
// 2) Résoudre l'employé superviseur depuis l'utilisateur courant (Users.id -> Employees)
|
// 2) Résoudre l'employé superviseur depuis l'utilisateur courant (Users.id -> Employees)
|
||||||
const supervisor = await this.prisma.employees.findUnique({
|
const supervisor = await this.prisma.employees.findUnique({
|
||||||
where: { user_id: userId },
|
where: { user_id: userId },
|
||||||
select: { id: true },
|
select: { id: true },
|
||||||
});
|
});
|
||||||
if (!supervisor) throw new ForbiddenException('No employee record linked to current user');
|
if (!supervisor) throw new ForbiddenException('No employee record linked to current user');
|
||||||
|
|
||||||
// 3) Récupérer la liste des employés du crew (directs ou sous-arbo complète)
|
// 3) Récupérer la liste des employés du crew (directs ou sous-arbo complète)
|
||||||
const crew = await this.resolveCrew(supervisor.id, includeSubtree); // [{ id, first_name, last_name }]
|
const crew = await this.resolveCrew(supervisor.id, includeSubtree); // [{ id, first_name, last_name }]
|
||||||
const crewIds = crew.map(c => c.id);
|
const crewIds = crew.map(c => c.id);
|
||||||
// seed names map for employés sans données
|
// seed names map for employés sans données
|
||||||
const seedNames = new Map<number, string>(crew.map(c => [c.id, `${c.first_name} ${c.last_name}`.trim()]));
|
const seedNames = new Map<number, string>(crew.map(c => [c.id, `${c.first_name} ${c.last_name}`.trim()]));
|
||||||
|
|
||||||
// 4) Construire l’overview filtré par ce crew
|
// 4) Construire l’overview filtré par ce crew
|
||||||
return this.buildOverview(period, { restrictEmployeeIds: crewIds, seedNames });
|
return this.buildOverview(period, { restrictEmployeeIds: crewIds, seedNames });
|
||||||
}
|
}
|
||||||
|
|
||||||
private async resolveCrew(supervisorId: number, includeSubtree: boolean): Promise<Array<{ id: number; first_name: string; last_name: string }>> {
|
private async resolveCrew(supervisorId: number, includeSubtree: boolean): Promise<Array<{ id: number; first_name: string; last_name: string }>> {
|
||||||
const result: Array<{ id: number; first_name: string; last_name: string }> = [];
|
const result: Array<{ id: number; first_name: string; last_name: string }> = [];
|
||||||
|
|
||||||
// niveau 1 (directs)
|
// niveau 1 (directs)
|
||||||
let frontier = await this.prisma.employees.findMany({
|
let frontier = await this.prisma.employees.findMany({
|
||||||
where: { supervisor_id: supervisorId },
|
where: { supervisor_id: supervisorId },
|
||||||
|
select: { id: true, user: { select: { first_name: true, last_name: true } } },
|
||||||
|
});
|
||||||
|
result.push(...frontier.map(e => ({ id: e.id, first_name: e.user.first_name, last_name: e.user.last_name })));
|
||||||
|
|
||||||
|
if (!includeSubtree) return result;
|
||||||
|
|
||||||
|
// BFS pour les niveaux suivants
|
||||||
|
while (frontier.length) {
|
||||||
|
const parentIds = frontier.map(e => e.id);
|
||||||
|
const next = await this.prisma.employees.findMany({
|
||||||
|
where: { supervisor_id: { in: parentIds } },
|
||||||
select: { id: true, user: { select: { first_name: true, last_name: true } } },
|
select: { id: true, user: { select: { first_name: true, last_name: true } } },
|
||||||
});
|
});
|
||||||
result.push(...frontier.map(e => ({ id: e.id, first_name: e.user.first_name, last_name: e.user.last_name })));
|
if (next.length === 0) break;
|
||||||
|
result.push(...next.map(e => ({ id: e.id, first_name: e.user.first_name, last_name: e.user.last_name })));
|
||||||
|
frontier = next;
|
||||||
|
}
|
||||||
|
|
||||||
if (!includeSubtree) return result;
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
// BFS pour les niveaux suivants
|
|
||||||
while (frontier.length) {
|
async findAll(): Promise<PayPeriodDto[]> {
|
||||||
const parentIds = frontier.map(e => e.id);
|
const currentPayYear = payYearOfDate(new Date());
|
||||||
const next = await this.prisma.employees.findMany({
|
return listPayYear(currentPayYear).map(period =>({
|
||||||
where: { supervisor_id: { in: parentIds } },
|
period_number: period.period_number,
|
||||||
select: { id: true, user: { select: { first_name: true, last_name: true } } },
|
year: period.year,
|
||||||
|
start_date: period.start_date,
|
||||||
|
end_date: period.end_date,
|
||||||
|
label: period.label,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne(periodNumber: number): Promise<PayPeriodDto> {
|
||||||
|
const row = await this.prisma.payPeriods.findFirst({
|
||||||
|
where: { period_number: periodNumber },
|
||||||
|
orderBy: { year: "desc" },
|
||||||
});
|
});
|
||||||
if (next.length === 0) break;
|
if (!row) throw new NotFoundException(`Pay period #${periodNumber} not found`);
|
||||||
result.push(...next.map(e => ({ id: e.id, first_name: e.user.first_name, last_name: e.user.last_name })));
|
return mapPayPeriodToDto(row);
|
||||||
frontier = next;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
async findOneByYearPeriod(year: number, periodNumber: number): Promise<PayPeriodDto> {
|
||||||
|
const row = await this.prisma.payPeriods.findFirst({
|
||||||
|
where: { year, period_number: periodNumber },
|
||||||
|
});
|
||||||
|
if(row) return mapPayPeriodToDto(row);
|
||||||
|
|
||||||
|
// fallback for outside of view periods
|
||||||
|
const p = computePeriod(year, periodNumber);
|
||||||
|
return {period_number: p.period_number, year: p.year, start_date: p.start_date, end_date: p.end_date, label: p.label}
|
||||||
|
}
|
||||||
|
|
||||||
|
//function to cherry pick a Date to find a period
|
||||||
|
async findByDate(date: string): Promise<PayPeriodDto> {
|
||||||
|
const dt = new Date(date);
|
||||||
|
const row = await this.prisma.payPeriods.findFirst({
|
||||||
|
where: { start_date: { lte: dt }, end_date: { gte: dt } },
|
||||||
|
});
|
||||||
|
if(row) return mapPayPeriodToDto(row);
|
||||||
|
|
||||||
|
//fallback for outwside view periods
|
||||||
|
const payYear = payYearOfDate(date);
|
||||||
|
const periods = listPayYear(payYear);
|
||||||
|
const hit = periods.find(p => date >= p.start_date && date <= p.end_date);
|
||||||
|
if(!hit) throw new NotFoundException(`No period found for ${date}`);
|
||||||
|
|
||||||
|
return { period_number: hit.period_number, year: hit.year, start_date: hit.start_date, end_date:hit.end_date, label: hit.label}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async findCurrent(date?: string): Promise<PayPeriodDto> {
|
||||||
|
const isoDay = date ?? new Date().toISOString().slice(0,10);
|
||||||
|
return this.findByDate(isoDay);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
|
||||||
import { PayPeriodsCommandService } from "./pay-periods-command.service";
|
|
||||||
import { mapMany, mapPayPeriodToDto } from "../mappers/pay-periods.mapper";
|
|
||||||
import { PayPeriodDto } from "../dtos/pay-period.dto";
|
|
||||||
import { computePeriod, listPayYear, payYearOfDate } from "../utils/pay-year.util";
|
|
||||||
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class PayPeriodsService {
|
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
|
||||||
|
|
||||||
async findAll(): Promise<PayPeriodDto[]> {
|
|
||||||
const currentPayYear = payYearOfDate(new Date());
|
|
||||||
return listPayYear(currentPayYear).map(period =>({
|
|
||||||
period_number: period.period_number,
|
|
||||||
year: period.year,
|
|
||||||
start_date: period.start_date,
|
|
||||||
end_date: period.end_date,
|
|
||||||
label: period.label,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
async findOne(periodNumber: number): Promise<PayPeriodDto> {
|
|
||||||
const row = await this.prisma.payPeriods.findFirst({
|
|
||||||
where: { period_number: periodNumber },
|
|
||||||
orderBy: { year: "desc" },
|
|
||||||
});
|
|
||||||
if (!row) throw new NotFoundException(`Pay period #${periodNumber} not found`);
|
|
||||||
return mapPayPeriodToDto(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
async findOneByYearPeriod(year: number, periodNumber: number): Promise<PayPeriodDto> {
|
|
||||||
const row = await this.prisma.payPeriods.findFirst({
|
|
||||||
where: { year, period_number: periodNumber },
|
|
||||||
});
|
|
||||||
if(row) return mapPayPeriodToDto(row);
|
|
||||||
|
|
||||||
// fallback for outside of view periods
|
|
||||||
const p = computePeriod(year, periodNumber);
|
|
||||||
return {period_number: p.period_number, year: p.year, start_date: p.start_date, end_date: p.end_date, label: p.label}
|
|
||||||
}
|
|
||||||
|
|
||||||
//function to cherry pick a Date to find a period
|
|
||||||
async findByDate(date: string): Promise<PayPeriodDto> {
|
|
||||||
const dt = new Date(date);
|
|
||||||
const row = await this.prisma.payPeriods.findFirst({
|
|
||||||
where: { start_date: { lte: dt }, end_date: { gte: dt } },
|
|
||||||
});
|
|
||||||
if(row) return mapPayPeriodToDto(row);
|
|
||||||
|
|
||||||
//fallback for outwside view periods
|
|
||||||
const payYear = payYearOfDate(date);
|
|
||||||
const periods = listPayYear(payYear);
|
|
||||||
const hit = periods.find(p => date >= p.start_date && date <= p.end_date);
|
|
||||||
if(!hit) throw new NotFoundException(`No period found for ${date}`);
|
|
||||||
|
|
||||||
return { period_number: hit.period_number, year: hit.year, start_date: hit.start_date, end_date:hit.end_date, label: hit.label}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
async findCurrent(date?: string): Promise<PayPeriodDto> {
|
|
||||||
const isoDay = date ?? new Date().toISOString().slice(0,10);
|
|
||||||
return this.findByDate(isoDay);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
export const ANCHOR_ISO = '2023-12-17'; // ancre date
|
export const ANCHOR_ISO = '2023-12-17'; // ancre date
|
||||||
const PERIOD_DAYS = 14;
|
const PERIOD_DAYS = 14;
|
||||||
const PERIODS_PER_YEAR = 26;
|
const PERIODS_PER_YEAR = 26;
|
||||||
|
const MS_PER_DAY = 86_400_000;
|
||||||
|
|
||||||
const toUTCDate = (iso: string | Date) => {
|
const toUTCDate = (iso: string | Date) => {
|
||||||
const d = typeof iso === 'string' ? new Date(iso + 'T00:00:00.000Z') : iso;
|
const d = typeof iso === 'string' ? new Date(iso + 'T00:00:00.000Z') : iso;
|
||||||
|
|
@ -10,18 +11,20 @@ export const toDateString = (d: Date) => d.toISOString().slice(0, 10);
|
||||||
|
|
||||||
const ANCHOR = toUTCDate(ANCHOR_ISO);
|
const ANCHOR = toUTCDate(ANCHOR_ISO);
|
||||||
|
|
||||||
export function payYearOfDate(date: string | Date): number {
|
export function payYearOfDate(date: string | Date, anchorISO = ANCHOR_ISO): number {
|
||||||
|
const ANCHOR = toUTCDate(anchorISO);
|
||||||
const d = toUTCDate(date);
|
const d = toUTCDate(date);
|
||||||
const days = Math.floor((+d - +ANCHOR) / 86400000);
|
const days = Math.floor((+d - +ANCHOR) / MS_PER_DAY);
|
||||||
const cycles = Math.floor(days / (PERIODS_PER_YEAR * PERIOD_DAYS));
|
const cycles = Math.floor(days / (PERIODS_PER_YEAR * PERIOD_DAYS));
|
||||||
return ANCHOR.getUTCFullYear() + 1 + cycles;
|
return ANCHOR.getUTCFullYear() + 1 + cycles;
|
||||||
}
|
}
|
||||||
//compute labels for periods
|
//compute labels for periods
|
||||||
export function computePeriod(payYear: number, periodNumber: number) {
|
export function computePeriod(payYear: number, periodNumber: number, anchorISO = ANCHOR_ISO) {
|
||||||
|
const ANCHOR = toUTCDate(anchorISO);
|
||||||
const cycles = payYear - (ANCHOR.getUTCFullYear() + 1);
|
const cycles = payYear - (ANCHOR.getUTCFullYear() + 1);
|
||||||
const offsetPeriods = cycles * PERIODS_PER_YEAR + (periodNumber - 1);
|
const offsetPeriods = cycles * PERIODS_PER_YEAR + (periodNumber - 1);
|
||||||
const start = new Date(+ANCHOR + offsetPeriods * PERIOD_DAYS * 86400000);
|
const start = new Date(+ANCHOR + offsetPeriods * PERIOD_DAYS * MS_PER_DAY);
|
||||||
const end = new Date(+start + (PERIOD_DAYS - 1) * 86400000);
|
const end = new Date(+start + (PERIOD_DAYS - 1) * MS_PER_DAY);
|
||||||
return {
|
return {
|
||||||
period_number: periodNumber,
|
period_number: periodNumber,
|
||||||
year: payYear,
|
year: payYear,
|
||||||
|
|
@ -33,6 +36,6 @@ export function computePeriod(payYear: number, periodNumber: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
//list of all 26 periods for a full year
|
//list of all 26 periods for a full year
|
||||||
export function listPayYear(payYear: number) {
|
export function listPayYear(payYear: number, anchorISO = ANCHOR_ISO) {
|
||||||
return Array.from({ length: PERIODS_PER_YEAR }, (_, i) => computePeriod(payYear, i + 1));
|
return Array.from({ length: PERIODS_PER_YEAR }, (_, i) => computePeriod(payYear, i + 1, anchorISO));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
import { Controller, Get, Header, Query } from "@nestjs/common";
|
|
||||||
import { OverviewRow, ShiftsOverviewService } from "../services/shifts-query.service";
|
|
||||||
import { GetShiftsOverviewDto } from "../dtos/get-shifts-overview.dto";
|
|
||||||
|
|
||||||
@Controller()
|
|
||||||
export class ShiftsOverviewController {
|
|
||||||
constructor(private readonly shiftsValidationService: ShiftsOverviewService) {}
|
|
||||||
|
|
||||||
@Get()
|
|
||||||
async getSummary( @Query() query: GetShiftsOverviewDto): Promise<OverviewRow[]> {
|
|
||||||
return this.shiftsValidationService.getSummary(query.period_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('export.csv')
|
|
||||||
@Header('Content-Type', 'text/csv; charset=utf-8')
|
|
||||||
@Header('Content-Disposition', 'attachment; filename="shifts-validation.csv"')
|
|
||||||
async exportCsv(@Query() query: GetShiftsOverviewDto): Promise<Buffer>{
|
|
||||||
const rows = await this.shiftsValidationService.getSummary(query.period_id);
|
|
||||||
|
|
||||||
//CSV Headers
|
|
||||||
const header = [
|
|
||||||
'fullName',
|
|
||||||
'supervisor',
|
|
||||||
'totalRegularHrs',
|
|
||||||
'totalEveningHrs',
|
|
||||||
'totalOvertimeHrs',
|
|
||||||
'totalExpenses',
|
|
||||||
'totalMileage',
|
|
||||||
'isValidated'
|
|
||||||
].join(',') + '\n';
|
|
||||||
|
|
||||||
//CSV rows
|
|
||||||
const body = rows.map(r => {
|
|
||||||
const esc = (str: string) => `"${str.replace(/"/g, '""')}"`;
|
|
||||||
|
|
||||||
return [
|
|
||||||
esc(r.fullName),
|
|
||||||
esc(r.supervisor),
|
|
||||||
r.totalRegularHrs.toFixed(2),
|
|
||||||
r.totalEveningHrs.toFixed(2),
|
|
||||||
r.totalOvertimeHrs.toFixed(2),
|
|
||||||
r.totalExpenses.toFixed(2),
|
|
||||||
r.totalMileage.toFixed(2),
|
|
||||||
r.isValidated,
|
|
||||||
].join(',');
|
|
||||||
}).join('\n');
|
|
||||||
|
|
||||||
return Buffer.from('\uFEFF' + header + body, 'utf8');
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
import { Body, Controller, Delete, Get, Param, ParseBoolPipe, ParseIntPipe, Patch, Post, Query, UseGuards, UsePipes, ValidationPipe } from "@nestjs/common";
|
import { Body, Controller, Delete, Get, Header, Param, ParseBoolPipe, ParseIntPipe, Patch, Post, Query, UseGuards, UsePipes, ValidationPipe } from "@nestjs/common";
|
||||||
import { ShiftsService } from "../services/shifts.service";
|
|
||||||
import { Shifts } from "@prisma/client";
|
import { Shifts } from "@prisma/client";
|
||||||
import { CreateShiftDto } from "../dtos/create-shift.dto";
|
import { CreateShiftDto } from "../dtos/create-shift.dto";
|
||||||
import { UpdateShiftsDto } from "../dtos/update-shift.dto";
|
import { UpdateShiftsDto } from "../dtos/update-shift.dto";
|
||||||
import { RolesAllowed } from "src/common/decorators/roles.decorators";
|
import { RolesAllowed } from "src/common/decorators/roles.decorators";
|
||||||
import { Roles as RoleEnum } from '.prisma/client';
|
import { Roles as RoleEnum } from '.prisma/client';
|
||||||
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
|
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
|
||||||
import { ShiftsApprovalService } from "../services/shifts-command.service";
|
import { ShiftsCommandService } from "../services/shifts-command.service";
|
||||||
import { SearchShiftsDto } from "../dtos/search-shifts.dto";
|
import { SearchShiftsDto } from "../dtos/search-shift.dto";
|
||||||
|
import { OverviewRow, ShiftsQueryService } from "../services/shifts-query.service";
|
||||||
|
import { GetShiftsOverviewDto } from "../dtos/get-shift-overview.dto";
|
||||||
|
|
||||||
@ApiTags('Shifts')
|
@ApiTags('Shifts')
|
||||||
@ApiBearerAuth('access-token')
|
@ApiBearerAuth('access-token')
|
||||||
|
|
@ -15,8 +16,9 @@ import { SearchShiftsDto } from "../dtos/search-shifts.dto";
|
||||||
@Controller('shifts')
|
@Controller('shifts')
|
||||||
export class ShiftsController {
|
export class ShiftsController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly shiftsService: ShiftsService,
|
private readonly shiftsService: ShiftsQueryService,
|
||||||
private readonly shiftsApprovalService: ShiftsApprovalService,
|
private readonly shiftsApprovalService: ShiftsCommandService,
|
||||||
|
private readonly shiftsValidationService: ShiftsQueryService,
|
||||||
){}
|
){}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
|
|
@ -71,4 +73,46 @@ export class ShiftsController {
|
||||||
return this.shiftsApprovalService.updateApproval(id, isApproved);
|
return this.shiftsApprovalService.updateApproval(id, isApproved);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
async getSummary( @Query() query: GetShiftsOverviewDto): Promise<OverviewRow[]> {
|
||||||
|
return this.shiftsValidationService.getSummary(query.period_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('export.csv')
|
||||||
|
@Header('Content-Type', 'text/csv; charset=utf-8')
|
||||||
|
@Header('Content-Disposition', 'attachment; filename="shifts-validation.csv"')
|
||||||
|
async exportCsv(@Query() query: GetShiftsOverviewDto): Promise<Buffer>{
|
||||||
|
const rows = await this.shiftsValidationService.getSummary(query.period_id);
|
||||||
|
|
||||||
|
//CSV Headers
|
||||||
|
const header = [
|
||||||
|
'fullName',
|
||||||
|
'supervisor',
|
||||||
|
'totalRegularHrs',
|
||||||
|
'totalEveningHrs',
|
||||||
|
'totalOvertimeHrs',
|
||||||
|
'totalExpenses',
|
||||||
|
'totalMileage',
|
||||||
|
'isValidated'
|
||||||
|
].join(',') + '\n';
|
||||||
|
|
||||||
|
//CSV rows
|
||||||
|
const body = rows.map(r => {
|
||||||
|
const esc = (str: string) => `"${str.replace(/"/g, '""')}"`;
|
||||||
|
|
||||||
|
return [
|
||||||
|
esc(r.fullName),
|
||||||
|
esc(r.supervisor),
|
||||||
|
r.totalRegularHrs.toFixed(2),
|
||||||
|
r.totalEveningHrs.toFixed(2),
|
||||||
|
r.totalOvertimeHrs.toFixed(2),
|
||||||
|
r.totalExpenses.toFixed(2),
|
||||||
|
r.totalMileage.toFixed(2),
|
||||||
|
r.isValidated,
|
||||||
|
].join(',');
|
||||||
|
}).join('\n');
|
||||||
|
|
||||||
|
return Buffer.from('\uFEFF' + header + body, 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -4,7 +4,7 @@ import { BaseApprovalService } from "src/common/shared/base-approval.service";
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ShiftsApprovalService extends BaseApprovalService<Shifts> {
|
export class ShiftsCommandService extends BaseApprovalService<Shifts> {
|
||||||
constructor(prisma: PrismaService) { super(prisma); }
|
constructor(prisma: PrismaService) { super(prisma); }
|
||||||
|
|
||||||
protected get delegate() {
|
protected get delegate() {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,14 @@
|
||||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||||
import { computeHours } from "src/common/utils/date-utils";
|
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
import { CreateShiftDto } from "../dtos/create-shift.dto";
|
||||||
|
import { Shifts, ShiftsArchive } from "@prisma/client";
|
||||||
|
import { UpdateShiftsDto } from "../dtos/update-shift.dto";
|
||||||
|
import { buildPrismaWhere } from "src/common/shared/build-prisma-where";
|
||||||
|
import { SearchShiftsDto } from "../dtos/search-shift.dto";
|
||||||
|
import { NotificationsService } from "src/modules/notifications/services/notifications.service";
|
||||||
|
import { computeHours, hoursBetweenSameDay } from "src/common/utils/date-utils";
|
||||||
|
|
||||||
|
const DAILY_LIMIT_HOURS = Number(process.env.DAILY_LIMIT_HOURS ?? 8);
|
||||||
|
|
||||||
export interface OverviewRow {
|
export interface OverviewRow {
|
||||||
fullName: string;
|
fullName: string;
|
||||||
|
|
@ -14,10 +22,98 @@ export interface OverviewRow {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ShiftsOverviewService {
|
export class ShiftsQueryService {
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
constructor(
|
||||||
|
private readonly prisma: PrismaService,
|
||||||
|
private readonly notifs: NotificationsService,
|
||||||
|
) {}
|
||||||
|
|
||||||
async getSummary(period_id: number): Promise<OverviewRow[]> {
|
async create(dto: CreateShiftDto): Promise<Shifts> {
|
||||||
|
const { timesheet_id, bank_code_id, date, start_time, end_time } = dto;
|
||||||
|
|
||||||
|
//shift creation
|
||||||
|
const shift = await this.prisma.shifts.create({
|
||||||
|
data: { timesheet_id, bank_code_id, date, start_time, end_time },
|
||||||
|
include: { timesheet: { include: { employee: { include: { user: true } } } },
|
||||||
|
bank_code: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
//fetches all shifts of the same day to check for daily overtime
|
||||||
|
const sameDayShifts = await this.prisma.shifts.findMany({
|
||||||
|
where: { timesheet_id, date },
|
||||||
|
select: { id: true, date: true, start_time: true, end_time: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
//sums hours of the day
|
||||||
|
const totalHours = sameDayShifts.reduce((sum, s) => {
|
||||||
|
return sum + hoursBetweenSameDay(s.date, s.start_time, s.end_time);
|
||||||
|
}, 0 );
|
||||||
|
|
||||||
|
//Notify if total hours > 8 for a single day
|
||||||
|
if(totalHours > DAILY_LIMIT_HOURS ) {
|
||||||
|
const userId = String(shift.timesheet.employee.user.id);
|
||||||
|
const dateLabel = new Date(date).toLocaleDateString('fr-CA');
|
||||||
|
this.notifs.notify(userId, {
|
||||||
|
type: 'shift.overtime.daily',
|
||||||
|
severity: 'warn',
|
||||||
|
message: `Tu viens de dépasser ${DAILY_LIMIT_HOURS.toFixed(2)}h pour la journée du ${dateLabel} (total: ${totalHours.toFixed(2)}h).`,
|
||||||
|
ts: new Date().toISOString(),
|
||||||
|
meta: {
|
||||||
|
timesheet_id,
|
||||||
|
date: new Date(date).toISOString(),
|
||||||
|
totalHours,
|
||||||
|
threshold: DAILY_LIMIT_HOURS,
|
||||||
|
lastShiftId: shift.id
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return shift;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAll(filters: SearchShiftsDto): Promise <Shifts[]> {
|
||||||
|
const where = buildPrismaWhere(filters);
|
||||||
|
const shifts = await this.prisma.shifts.findMany({ where })
|
||||||
|
return shifts;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne(id: number): Promise<Shifts> {
|
||||||
|
const shift = await this.prisma.shifts.findUnique({
|
||||||
|
where: { id },
|
||||||
|
include: { timesheet: { include: { employee: { include: { user: true } } } },
|
||||||
|
bank_code: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if(!shift) {
|
||||||
|
throw new NotFoundException(`Shift #${id} not found`);
|
||||||
|
}
|
||||||
|
return shift;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: number, dto: UpdateShiftsDto): Promise<Shifts> {
|
||||||
|
await this.findOne(id);
|
||||||
|
const { timesheet_id, bank_code_id, date,start_time,end_time} = dto;
|
||||||
|
return this.prisma.shifts.update({
|
||||||
|
where: { id },
|
||||||
|
data: {
|
||||||
|
...(timesheet_id !== undefined && { timesheet_id }),
|
||||||
|
...(bank_code_id !== undefined && { bank_code_id }),
|
||||||
|
...(date !== undefined && { date }),
|
||||||
|
...(start_time !== undefined && { start_time }),
|
||||||
|
...(end_time !== undefined && { end_time }),
|
||||||
|
},
|
||||||
|
include: { timesheet: { include: { employee: { include: { user: true } } } },
|
||||||
|
bank_code: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(id: number): Promise<Shifts> {
|
||||||
|
await this.findOne(id);
|
||||||
|
return this.prisma.shifts.delete({ where: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSummary(period_id: number): Promise<OverviewRow[]> {
|
||||||
//fetch pay-period to display
|
//fetch pay-period to display
|
||||||
const period = await this.prisma.payPeriods.findUnique({
|
const period = await this.prisma.payPeriods.findUnique({
|
||||||
where: { period_number: period_id },
|
where: { period_number: period_id },
|
||||||
|
|
@ -116,4 +212,58 @@ export class ShiftsOverviewService {
|
||||||
return Array.from(mapRow.values()).sort((a,b) => a.fullName.localeCompare(b.fullName));
|
return Array.from(mapRow.values()).sort((a,b) => a.fullName.localeCompare(b.fullName));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//archivation functions ******************************************************
|
||||||
|
|
||||||
|
async archiveOld(): Promise<void> {
|
||||||
|
//fetches archived timesheet's Ids
|
||||||
|
const archivedTimesheets = await this.prisma.timesheetsArchive.findMany({
|
||||||
|
select: { timesheet_id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const timesheetIds = archivedTimesheets.map(sheet => sheet.timesheet_id);
|
||||||
|
if(timesheetIds.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy/delete transaction
|
||||||
|
await this.prisma.$transaction(async transaction => {
|
||||||
|
//fetches shifts to move to archive
|
||||||
|
const shiftsToArchive = await transaction.shifts.findMany({
|
||||||
|
where: { timesheet_id: { in: timesheetIds } },
|
||||||
|
});
|
||||||
|
if(shiftsToArchive.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//copies sent to archive table
|
||||||
|
await transaction.shiftsArchive.createMany({
|
||||||
|
data: shiftsToArchive.map(shift => ({
|
||||||
|
shift_id: shift.id,
|
||||||
|
timesheet_id: shift.timesheet_id,
|
||||||
|
bank_code_id: shift.bank_code_id,
|
||||||
|
description: shift.description ?? undefined,
|
||||||
|
date: shift.date,
|
||||||
|
start_time: shift.start_time,
|
||||||
|
end_time: shift.end_time,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
//delete from shifts table
|
||||||
|
await transaction.shifts.deleteMany({
|
||||||
|
where: { id: { in: shiftsToArchive.map(shift => shift.id) } },
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
//fetches all archived timesheets
|
||||||
|
async findAllArchived(): Promise<ShiftsArchive[]> {
|
||||||
|
return this.prisma.shiftsArchive.findMany();
|
||||||
|
}
|
||||||
|
|
||||||
|
//fetches an archived timesheet
|
||||||
|
async findOneArchived(id: number): Promise<ShiftsArchive> {
|
||||||
|
return this.prisma.shiftsArchive.findUniqueOrThrow({ where: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,159 +0,0 @@
|
||||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
|
||||||
import { CreateShiftDto } from "../dtos/create-shift.dto";
|
|
||||||
import { Shifts, ShiftsArchive } from "@prisma/client";
|
|
||||||
import { UpdateShiftsDto } from "../dtos/update-shift.dto";
|
|
||||||
import { buildPrismaWhere } from "src/common/shared/build-prisma-where";
|
|
||||||
import { SearchShiftsDto } from "../dtos/search-shifts.dto";
|
|
||||||
import { NotificationsService } from "src/modules/notifications/services/notifications.service";
|
|
||||||
import { hoursBetweenSameDay } from "src/common/utils/date-utils";
|
|
||||||
|
|
||||||
const DAILY_LIMIT_HOURS = Number(process.env.DAILY_LIMIT_HOURS ?? 8);
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class ShiftsService {
|
|
||||||
constructor(
|
|
||||||
private readonly prisma: PrismaService,
|
|
||||||
private readonly notifs: NotificationsService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async create(dto: CreateShiftDto): Promise<Shifts> {
|
|
||||||
const { timesheet_id, bank_code_id, date, start_time, end_time } = dto;
|
|
||||||
|
|
||||||
//shift creation
|
|
||||||
const shift = await this.prisma.shifts.create({
|
|
||||||
data: { timesheet_id, bank_code_id, date, start_time, end_time },
|
|
||||||
include: { timesheet: { include: { employee: { include: { user: true } } } },
|
|
||||||
bank_code: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
//fetches all shifts of the same day to check for daily overtime
|
|
||||||
const sameDayShifts = await this.prisma.shifts.findMany({
|
|
||||||
where: { timesheet_id, date },
|
|
||||||
select: { id: true, date: true, start_time: true, end_time: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
//sums hours of the day
|
|
||||||
const totalHours = sameDayShifts.reduce((sum, s) => {
|
|
||||||
return sum + hoursBetweenSameDay(s.date, s.start_time, s.end_time);
|
|
||||||
}, 0 );
|
|
||||||
|
|
||||||
//Notify if total hours > 8 for a single day
|
|
||||||
if(totalHours > DAILY_LIMIT_HOURS ) {
|
|
||||||
const userId = String(shift.timesheet.employee.user.id);
|
|
||||||
const dateLabel = new Date(date).toLocaleDateString('fr-CA');
|
|
||||||
this.notifs.notify(userId, {
|
|
||||||
type: 'shift.overtime.daily',
|
|
||||||
severity: 'warn',
|
|
||||||
message: `Tu viens de dépasser ${DAILY_LIMIT_HOURS.toFixed(2)}h pour la journée du ${dateLabel} (total: ${totalHours.toFixed(2)}h).`,
|
|
||||||
ts: new Date().toISOString(),
|
|
||||||
meta: {
|
|
||||||
timesheet_id,
|
|
||||||
date: new Date(date).toISOString(),
|
|
||||||
totalHours,
|
|
||||||
threshold: DAILY_LIMIT_HOURS,
|
|
||||||
lastShiftId: shift.id
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return shift;
|
|
||||||
}
|
|
||||||
|
|
||||||
async findAll(filters: SearchShiftsDto): Promise <Shifts[]> {
|
|
||||||
const where = buildPrismaWhere(filters);
|
|
||||||
const shifts = await this.prisma.shifts.findMany({ where })
|
|
||||||
return shifts;
|
|
||||||
}
|
|
||||||
|
|
||||||
async findOne(id: number): Promise<Shifts> {
|
|
||||||
const shift = await this.prisma.shifts.findUnique({
|
|
||||||
where: { id },
|
|
||||||
include: { timesheet: { include: { employee: { include: { user: true } } } },
|
|
||||||
bank_code: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if(!shift) {
|
|
||||||
throw new NotFoundException(`Shift #${id} not found`);
|
|
||||||
}
|
|
||||||
return shift;
|
|
||||||
}
|
|
||||||
|
|
||||||
async update(id: number, dto: UpdateShiftsDto): Promise<Shifts> {
|
|
||||||
await this.findOne(id);
|
|
||||||
const { timesheet_id, bank_code_id, date,start_time,end_time} = dto;
|
|
||||||
return this.prisma.shifts.update({
|
|
||||||
where: { id },
|
|
||||||
data: {
|
|
||||||
...(timesheet_id !== undefined && { timesheet_id }),
|
|
||||||
...(bank_code_id !== undefined && { bank_code_id }),
|
|
||||||
...(date !== undefined && { date }),
|
|
||||||
...(start_time !== undefined && { start_time }),
|
|
||||||
...(end_time !== undefined && { end_time }),
|
|
||||||
},
|
|
||||||
include: { timesheet: { include: { employee: { include: { user: true } } } },
|
|
||||||
bank_code: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async remove(id: number): Promise<Shifts> {
|
|
||||||
await this.findOne(id);
|
|
||||||
return this.prisma.shifts.delete({ where: { id } });
|
|
||||||
}
|
|
||||||
|
|
||||||
//archivation functions ******************************************************
|
|
||||||
|
|
||||||
async archiveOld(): Promise<void> {
|
|
||||||
//fetches archived timesheet's Ids
|
|
||||||
const archivedTimesheets = await this.prisma.timesheetsArchive.findMany({
|
|
||||||
select: { timesheet_id: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
const timesheetIds = archivedTimesheets.map(sheet => sheet.timesheet_id);
|
|
||||||
if(timesheetIds.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// copy/delete transaction
|
|
||||||
await this.prisma.$transaction(async transaction => {
|
|
||||||
//fetches shifts to move to archive
|
|
||||||
const shiftsToArchive = await transaction.shifts.findMany({
|
|
||||||
where: { timesheet_id: { in: timesheetIds } },
|
|
||||||
});
|
|
||||||
if(shiftsToArchive.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//copies sent to archive table
|
|
||||||
await transaction.shiftsArchive.createMany({
|
|
||||||
data: shiftsToArchive.map(shift => ({
|
|
||||||
shift_id: shift.id,
|
|
||||||
timesheet_id: shift.timesheet_id,
|
|
||||||
bank_code_id: shift.bank_code_id,
|
|
||||||
description: shift.description ?? undefined,
|
|
||||||
date: shift.date,
|
|
||||||
start_time: shift.start_time,
|
|
||||||
end_time: shift.end_time,
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
|
|
||||||
//delete from shifts table
|
|
||||||
await transaction.shifts.deleteMany({
|
|
||||||
where: { id: { in: shiftsToArchive.map(shift => shift.id) } },
|
|
||||||
})
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
//fetches all archived timesheets
|
|
||||||
async findAllArchived(): Promise<ShiftsArchive[]> {
|
|
||||||
return this.prisma.shiftsArchive.findMany();
|
|
||||||
}
|
|
||||||
|
|
||||||
//fetches an archived timesheet
|
|
||||||
async findOneArchived(id: number): Promise<ShiftsArchive> {
|
|
||||||
return this.prisma.shiftsArchive.findUniqueOrThrow({ where: { id } });
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +1,14 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { ShiftsController } from './controllers/shifts.controller';
|
import { ShiftsController } from './controllers/shifts.controller';
|
||||||
import { ShiftsService } from './services/shifts.service';
|
|
||||||
import { BusinessLogicsModule } from 'src/modules/business-logics/business-logics.module';
|
import { BusinessLogicsModule } from 'src/modules/business-logics/business-logics.module';
|
||||||
import { ShiftsOverviewController } from './controllers/shifts-overview.controller';
|
import { ShiftsCommandService } from './services/shifts-command.service';
|
||||||
import { ShiftsOverviewService } from './services/shifts-query.service';
|
|
||||||
import { ShiftsApprovalService } from './services/shifts-command.service';
|
|
||||||
import { NotificationsModule } from '../notifications/notifications.module';
|
import { NotificationsModule } from '../notifications/notifications.module';
|
||||||
|
import { ShiftsQueryService } from './services/shifts-query.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [BusinessLogicsModule, NotificationsModule],
|
imports: [BusinessLogicsModule, NotificationsModule],
|
||||||
controllers: [ShiftsController, ShiftsOverviewController],
|
controllers: [ShiftsController],
|
||||||
providers: [ShiftsService, ShiftsOverviewService, ShiftsApprovalService],
|
providers: [ShiftsQueryService, ShiftsCommandService],
|
||||||
exports: [ShiftsService, ShiftsOverviewService, ShiftsApprovalService],
|
exports: [ShiftsQueryService, ShiftsCommandService],
|
||||||
})
|
})
|
||||||
export class ShiftsModule {}
|
export class ShiftsModule {}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Body, Controller, Delete, Get, Param, ParseBoolPipe, ParseIntPipe, Patch, Post, Query, UseGuards, UsePipes, ValidationPipe } from '@nestjs/common';
|
import { Body, Controller, Delete, Get, Param, ParseBoolPipe, ParseIntPipe, Patch, Post, Query, UseGuards, UsePipes, ValidationPipe } from '@nestjs/common';
|
||||||
import { TimesheetsService } from '../services/timesheets.service';
|
import { TimesheetsQueryService } from '../services/timesheets-query.service';
|
||||||
import { CreateTimesheetDto } from '../dtos/create-timesheet.dto';
|
import { CreateTimesheetDto } from '../dtos/create-timesheet.dto';
|
||||||
import { Timesheets } from '@prisma/client';
|
import { Timesheets } from '@prisma/client';
|
||||||
import { UpdateTimesheetDto } from '../dtos/update-timesheet.dto';
|
import { UpdateTimesheetDto } from '../dtos/update-timesheet.dto';
|
||||||
|
|
@ -7,7 +7,7 @@ import { RolesAllowed } from "src/common/decorators/roles.decorators";
|
||||||
import { Roles as RoleEnum } from '.prisma/client';
|
import { Roles as RoleEnum } from '.prisma/client';
|
||||||
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||||
import { TimesheetsCommandService } from '../services/timesheets-command.service';
|
import { TimesheetsCommandService } from '../services/timesheets-command.service';
|
||||||
import { SearchTimesheetDto } from '../dtos/search-timesheets.dto';
|
import { SearchTimesheetDto } from '../dtos/search-timesheet.dto';
|
||||||
|
|
||||||
@ApiTags('Timesheets')
|
@ApiTags('Timesheets')
|
||||||
@ApiBearerAuth('access-token')
|
@ApiBearerAuth('access-token')
|
||||||
|
|
@ -15,7 +15,7 @@ import { SearchTimesheetDto } from '../dtos/search-timesheets.dto';
|
||||||
@Controller('timesheets')
|
@Controller('timesheets')
|
||||||
export class TimesheetsController {
|
export class TimesheetsController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly timesheetsService: TimesheetsService,
|
private readonly timesheetsService: TimesheetsQueryService,
|
||||||
private readonly timesheetsCommandService: TimesheetsCommandService,
|
private readonly timesheetsCommandService: TimesheetsCommandService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,10 @@ import { UpdateTimesheetDto } from '../dtos/update-timesheet.dto';
|
||||||
import { OvertimeService } from 'src/modules/business-logics/services/overtime.service';
|
import { OvertimeService } from 'src/modules/business-logics/services/overtime.service';
|
||||||
import { computeHours } from 'src/common/utils/date-utils';
|
import { computeHours } from 'src/common/utils/date-utils';
|
||||||
import { buildPrismaWhere } from 'src/common/shared/build-prisma-where';
|
import { buildPrismaWhere } from 'src/common/shared/build-prisma-where';
|
||||||
import { SearchTimesheetDto } from '../dtos/search-timesheets.dto';
|
import { SearchTimesheetDto } from '../dtos/search-timesheet.dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TimesheetsService {
|
export class TimesheetsQueryService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
private readonly overtime: OvertimeService,
|
private readonly overtime: OvertimeService,
|
||||||
|
|
@ -1,20 +1,20 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TimesheetsController } from './controllers/timesheets.controller';
|
import { TimesheetsController } from './controllers/timesheets.controller';
|
||||||
import { TimesheetsService } from './services/timesheets.service';
|
import { TimesheetsQueryService } from './services/timesheets-query.service';
|
||||||
import { BusinessLogicsModule } from 'src/modules/business-logics/business-logics.module';
|
import { BusinessLogicsModule } from 'src/modules/business-logics/business-logics.module';
|
||||||
import { TimesheetsCommandService } from './services/timesheets-command.service';
|
import { TimesheetsCommandService } from './services/timesheets-command.service';
|
||||||
import { ShiftsApprovalService } from '../shifts/services/shifts-command.service';
|
import { ShiftsCommandService } from '../shifts/services/shifts-command.service';
|
||||||
import { ExpensesApprovalService } from '../expenses/services/expenses-command.service';
|
import { ExpensesCommandService } from '../expenses/services/expenses-command.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [BusinessLogicsModule],
|
imports: [BusinessLogicsModule],
|
||||||
controllers: [TimesheetsController],
|
controllers: [TimesheetsController],
|
||||||
providers: [
|
providers: [
|
||||||
TimesheetsService,
|
TimesheetsQueryService,
|
||||||
TimesheetsCommandService,
|
TimesheetsCommandService,
|
||||||
ShiftsApprovalService,
|
ShiftsCommandService,
|
||||||
ExpensesApprovalService
|
ExpensesCommandService
|
||||||
],
|
],
|
||||||
exports: [TimesheetsService],
|
exports: [TimesheetsQueryService],
|
||||||
})
|
})
|
||||||
export class TimesheetsModule {}
|
export class TimesheetsModule {}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user