refactor(expenses): commented module expenses et leave-request. needs refactor to match new timesheets and shifts module.

This commit is contained in:
Matthieu Haineault 2025-10-21 16:27:56 -04:00
parent 11f6cf2049
commit 6aeaf16993
28 changed files with 2036 additions and 2818 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,21 +1,21 @@
import { BadRequestException, Module, ValidationPipe } from '@nestjs/common'; import { BadRequestException, Module, ValidationPipe } from '@nestjs/common';
import { AppController } from './app.controller'; import { AppController } from './app.controller';
import { AppService } from './app.service'; import { AppService } from './app.service';
import { ArchivalModule } from './modules/archival/archival.module'; // import { ArchivalModule } from './modules/archival/archival.module';
import { AuthenticationModule } from './modules/authentication/auth.module'; import { AuthenticationModule } from './modules/authentication/auth.module';
import { BankCodesModule } from './modules/bank-codes/bank-codes.module'; import { BankCodesModule } from './modules/bank-codes/bank-codes.module';
import { BusinessLogicsModule } from './modules/business-logics/business-logics.module'; import { BusinessLogicsModule } from './modules/business-logics/business-logics.module';
// import { CsvExportModule } from './modules/exports/csv-exports.module'; // import { CsvExportModule } from './modules/exports/csv-exports.module';
import { CustomersModule } from './modules/customers/customers.module'; import { CustomersModule } from './modules/customers/customers.module';
import { EmployeesModule } from './modules/employees/employees.module'; import { EmployeesModule } from './modules/employees/employees.module';
import { ExpensesModule } from './modules/expenses/expenses.module'; // import { ExpensesModule } from './modules/expenses/expenses.module';
import { HealthModule } from './health/health.module'; import { HealthModule } from './health/health.module';
import { HealthController } from './health/health.controller'; import { HealthController } from './health/health.controller';
import { LeaveRequestsModule } from './modules/leave-requests/leave-requests.module'; // import { LeaveRequestsModule } from './modules/leave-requests/leave-requests.module';
import { NotificationsModule } from './modules/notifications/notifications.module'; import { NotificationsModule } from './modules/notifications/notifications.module';
import { OauthSessionsModule } from './modules/oauth-sessions/oauth-sessions.module'; import { OauthSessionsModule } from './modules/oauth-sessions/oauth-sessions.module';
import { OvertimeService } from './modules/business-logics/services/overtime.service'; import { OvertimeService } from './modules/business-logics/services/overtime.service';
import { PayperiodsModule } from './modules/pay-periods/pay-periods.module'; // import { PayperiodsModule } from './modules/pay-periods/pay-periods.module';
import { PreferencesModule } from './modules/preferences/preferences.module'; import { PreferencesModule } from './modules/preferences/preferences.module';
import { PrismaModule } from './prisma/prisma.module'; import { PrismaModule } from './prisma/prisma.module';
import { ScheduleModule } from '@nestjs/schedule'; import { ScheduleModule } from '@nestjs/schedule';
@ -30,7 +30,7 @@ import { SchedulePresetsModule } from './modules/schedule-presets/schedule-prese
@Module({ @Module({
imports: [ imports: [
ArchivalModule, // ArchivalModule,
AuthenticationModule, AuthenticationModule,
BankCodesModule, BankCodesModule,
BusinessLogicsModule, BusinessLogicsModule,
@ -38,12 +38,12 @@ import { SchedulePresetsModule } from './modules/schedule-presets/schedule-prese
// CsvExportModule, // CsvExportModule,
CustomersModule, CustomersModule,
EmployeesModule, EmployeesModule,
ExpensesModule, // ExpensesModule,
HealthModule, HealthModule,
LeaveRequestsModule, // LeaveRequestsModule,
NotificationsModule, NotificationsModule,
OauthSessionsModule, OauthSessionsModule,
PayperiodsModule, // PayperiodsModule,
PreferencesModule, PreferencesModule,
PrismaModule, PrismaModule,
ScheduleModule.forRoot(), //cronjobs ScheduleModule.forRoot(), //cronjobs

View File

@ -1,34 +1,34 @@
import { Module } from "@nestjs/common"; // import { Module } from "@nestjs/common";
import { ScheduleModule } from "@nestjs/schedule"; // import { ScheduleModule } from "@nestjs/schedule";
import { TimesheetsModule } from "../timesheets/timesheets.module"; // import { TimesheetsModule } from "../timesheets/timesheets.module";
import { ExpensesModule } from "../expenses/expenses.module"; // import { ExpensesModule } from "../expenses/expenses.module";
import { ShiftsModule } from "../shifts/shifts.module"; // import { ShiftsModule } from "../shifts/shifts.module";
import { LeaveRequestsModule } from "../leave-requests/leave-requests.module"; // import { LeaveRequestsModule } from "../leave-requests/leave-requests.module";
import { ArchivalService } from "./services/archival.service"; // import { ArchivalService } from "./services/archival.service";
import { EmployeesArchiveController } from "./controllers/employees-archive.controller"; // import { EmployeesArchiveController } from "./controllers/employees-archive.controller";
import { ExpensesArchiveController } from "./controllers/expenses-archive.controller"; // import { ExpensesArchiveController } from "./controllers/expenses-archive.controller";
import { LeaveRequestsArchiveController } from "./controllers/leave-requests-archive.controller"; // import { LeaveRequestsArchiveController } from "./controllers/leave-requests-archive.controller";
import { ShiftsArchiveController } from "./controllers/shifts-archive.controller"; // import { ShiftsArchiveController } from "./controllers/shifts-archive.controller";
import { TimesheetsArchiveController } from "./controllers/timesheets-archive.controller"; // import { TimesheetsArchiveController } from "./controllers/timesheets-archive.controller";
import { EmployeesModule } from "../employees/employees.module"; // import { EmployeesModule } from "../employees/employees.module";
@Module({ // @Module({
imports: [ // imports: [
EmployeesModule, // EmployeesModule,
ScheduleModule, // ScheduleModule,
TimesheetsModule, // TimesheetsModule,
ExpensesModule, // ExpensesModule,
ShiftsModule, // ShiftsModule,
LeaveRequestsModule, // LeaveRequestsModule,
], // ],
providers: [ArchivalService], // providers: [ArchivalService],
controllers: [ // controllers: [
EmployeesArchiveController, // EmployeesArchiveController,
ExpensesArchiveController, // ExpensesArchiveController,
LeaveRequestsArchiveController, // LeaveRequestsArchiveController,
ShiftsArchiveController, // ShiftsArchiveController,
TimesheetsArchiveController, // TimesheetsArchiveController,
], // ],
}) // })
export class ArchivalModule {} // export class ArchivalModule {}

View File

@ -1,95 +1,95 @@
import { Body, Controller, Get, Param, Put, } from "@nestjs/common"; // import { Body, Controller, Get, Param, Put, } from "@nestjs/common";
import { Roles as RoleEnum } from '.prisma/client'; // import { Roles as RoleEnum } from '.prisma/client';
import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; // import { ApiBearerAuth, ApiTags } from "@nestjs/swagger";
import { RolesAllowed } from "src/common/decorators/roles.decorators"; // import { RolesAllowed } from "src/common/decorators/roles.decorators";
import { ExpensesCommandService } from "../services/expenses-command.service"; // import { ExpensesCommandService } from "../services/expenses-command.service";
import { UpsertExpenseDto } from "../dtos/upsert-expense.dto"; // import { UpsertExpenseDto } from "../dtos/upsert-expense.dto";
import { UpsertExpenseResult } from "../types and interfaces/expenses.types.interfaces"; // import { UpsertExpenseResult } from "../types and interfaces/expenses.types.interfaces";
import { DayExpensesDto } from "src/modules/timesheets/~misc_deprecated-files/timesheet-period.dto"; // import { DayExpensesDto } from "src/modules/timesheets/~misc_deprecated-files/timesheet-period.dto";
import { ExpensesQueryService } from "../services/expenses-query.service"; // import { ExpensesQueryService } from "../services/expenses-query.service";
@ApiTags('Expenses') // @ApiTags('Expenses')
@ApiBearerAuth('access-token') // @ApiBearerAuth('access-token')
// @UseGuards() // // @UseGuards()
@Controller('Expenses') // @Controller('Expenses')
export class ExpensesController { // export class ExpensesController {
constructor( // constructor(
private readonly query: ExpensesQueryService, // private readonly query: ExpensesQueryService,
private readonly command: ExpensesCommandService, // private readonly command: ExpensesCommandService,
) {} // ) {}
@Put('upsert/:email/:date') // @Put('upsert/:email/:date')
async upsert_by_date( // async upsert_by_date(
@Param('email') email: string, // @Param('email') email: string,
@Param('date') date: string, // @Param('date') date: string,
@Body() dto: UpsertExpenseDto, // @Body() dto: UpsertExpenseDto,
): Promise<UpsertExpenseResult> { // ): Promise<UpsertExpenseResult> {
return this.command.upsertExpensesByDate(email, date, dto); // return this.command.upsertExpensesByDate(email, date, dto);
} // }
@Get('list/:email/:year/:period_no') // @Get('list/:email/:year/:period_no')
async findExpenseListByPayPeriodAndEmail( // async findExpenseListByPayPeriodAndEmail(
@Param('email') email:string, // @Param('email') email:string,
@Param('year') year: number, // @Param('year') year: number,
@Param('period_no') period_no: number, // @Param('period_no') period_no: number,
): Promise<DayExpensesDto> { // ): Promise<DayExpensesDto> {
return this.query.findExpenseListByPayPeriodAndEmail(email, year, period_no); // return this.query.findExpenseListByPayPeriodAndEmail(email, year, period_no);
} // }
//_____________________________________________________________________________________________ // //_____________________________________________________________________________________________
// Deprecated or unused methods // // Deprecated or unused methods
//_____________________________________________________________________________________________ // //_____________________________________________________________________________________________
// @Post() // // @Post()
// //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR) // // //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
// @ApiOperation({ summary: 'Create expense' }) // // @ApiOperation({ summary: 'Create expense' })
// @ApiResponse({ status: 201, description: 'Expense created',type: CreateExpenseDto }) // // @ApiResponse({ status: 201, description: 'Expense created',type: CreateExpenseDto })
// @ApiResponse({ status: 400, description: 'Incomplete task or invalid data' }) // // @ApiResponse({ status: 400, description: 'Incomplete task or invalid data' })
// create(@Body() dto: CreateExpenseDto): Promise<Expenses> { // // create(@Body() dto: CreateExpenseDto): Promise<Expenses> {
// return this.query.create(dto); // // return this.query.create(dto);
// } // // }
// @Get() // // @Get()
// //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR) // // //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
// @ApiOperation({ summary: 'Find all expenses' }) // // @ApiOperation({ summary: 'Find all expenses' })
// @ApiResponse({ status: 201, description: 'List of expenses found',type: CreateExpenseDto, isArray: true }) // // @ApiResponse({ status: 201, description: 'List of expenses found',type: CreateExpenseDto, isArray: true })
// @ApiResponse({ status: 400, description: 'List of expenses not found' }) // // @ApiResponse({ status: 400, description: 'List of expenses not found' })
// @UsePipes(new ValidationPipe({ transform: true, whitelist: true })) // // @UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
// findAll(@Query() filters: SearchExpensesDto): Promise<Expenses[]> { // // findAll(@Query() filters: SearchExpensesDto): Promise<Expenses[]> {
// return this.query.findAll(filters); // // return this.query.findAll(filters);
// } // // }
// @Get(':id') // // @Get(':id')
// //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR) // // //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
// @ApiOperation({ summary: 'Find expense' }) // // @ApiOperation({ summary: 'Find expense' })
// @ApiResponse({ status: 201, description: 'Expense found',type: CreateExpenseDto }) // // @ApiResponse({ status: 201, description: 'Expense found',type: CreateExpenseDto })
// @ApiResponse({ status: 400, description: 'Expense not found' }) // // @ApiResponse({ status: 400, description: 'Expense not found' })
// findOne(@Param('id', ParseIntPipe) id: number): Promise <Expenses> { // // findOne(@Param('id', ParseIntPipe) id: number): Promise <Expenses> {
// return this.query.findOne(id); // // return this.query.findOne(id);
// } // // }
// @Patch(':id') // // @Patch(':id')
// //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR) // // //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
// @ApiOperation({ summary: 'Expense shift' }) // // @ApiOperation({ summary: 'Expense shift' })
// @ApiResponse({ status: 201, description: 'Expense updated',type: CreateExpenseDto }) // // @ApiResponse({ status: 201, description: 'Expense updated',type: CreateExpenseDto })
// @ApiResponse({ status: 400, description: 'Expense not found' }) // // @ApiResponse({ status: 400, description: 'Expense not found' })
// update(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdateExpenseDto) { // // update(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdateExpenseDto) {
// return this.query.update(id,dto); // // return this.query.update(id,dto);
// } // // }
// @Delete(':id') // // @Delete(':id')
// //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR) // // //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
// @ApiOperation({ summary: 'Delete expense' }) // // @ApiOperation({ summary: 'Delete expense' })
// @ApiResponse({ status: 201, description: 'Expense deleted',type: CreateExpenseDto }) // // @ApiResponse({ status: 201, description: 'Expense deleted',type: CreateExpenseDto })
// @ApiResponse({ status: 400, description: 'Expense not found' }) // // @ApiResponse({ status: 400, description: 'Expense not found' })
// remove(@Param('id', ParseIntPipe) id: number): Promise<Expenses> { // // remove(@Param('id', ParseIntPipe) id: number): Promise<Expenses> {
// return this.query.remove(id); // // return this.query.remove(id);
// } // // }
// @Patch('approval/:id') // // @Patch('approval/:id')
// //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR) // // //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
// async approve(@Param('id', ParseIntPipe) id: number, @Body('is_approved', ParseBoolPipe) isApproved: boolean) { // // async approve(@Param('id', ParseIntPipe) id: number, @Body('is_approved', ParseBoolPipe) isApproved: boolean) {
// return this.command.updateApproval(id, isApproved); // // return this.command.updateApproval(id, isApproved);
// } // // }
} // }

View File

@ -1,23 +1,23 @@
import { ExpensesController } from "./controllers/expenses.controller"; // import { ExpensesController } from "./controllers/expenses.controller";
import { Module } from "@nestjs/common"; // import { Module } from "@nestjs/common";
import { ExpensesQueryService } from "./services/expenses-query.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 { ExpensesCommandService } from "./services/expenses-command.service"; // import { ExpensesCommandService } from "./services/expenses-command.service";
import { ExpensesArchivalService } from "./services/expenses-archival.service"; // import { ExpensesArchivalService } from "./services/expenses-archival.service";
import { SharedModule } from "../shared/shared.module"; // import { SharedModule } from "../shared/shared.module";
@Module({ // @Module({
imports: [BusinessLogicsModule, SharedModule], // imports: [BusinessLogicsModule, SharedModule],
controllers: [ExpensesController], // controllers: [ExpensesController],
providers: [ // providers: [
ExpensesQueryService, // ExpensesQueryService,
ExpensesArchivalService, // ExpensesArchivalService,
ExpensesCommandService, // ExpensesCommandService,
], // ],
exports: [ // exports: [
ExpensesQueryService, // ExpensesQueryService,
ExpensesArchivalService, // ExpensesArchivalService,
], // ],
}) // })
export class ExpensesModule {} // export class ExpensesModule {}

View File

@ -1,250 +1,249 @@
import { BaseApprovalService } from "src/common/shared/base-approval.service"; // import { BaseApprovalService } from "src/common/shared/base-approval.service";
import { Expenses, Prisma } from "@prisma/client"; // import { Expenses, Prisma } from "@prisma/client";
import { PrismaService } from "src/prisma/prisma.service"; // import { PrismaService } from "src/prisma/prisma.service";
import { UpsertExpenseDto } from "../dtos/upsert-expense.dto"; // import { UpsertExpenseDto } from "../dtos/upsert-expense.dto";
import { BankCodesResolver } from "src/modules/shared/utils/resolve-bank-type-id.utils"; // import { BankCodesResolver } from "src/modules/shared/utils/resolve-bank-type-id.utils";
import { ExpenseResponse, UpsertAction } from "../types and interfaces/expenses.types.interfaces"; // import { ExpenseResponse, UpsertAction } from "../types and interfaces/expenses.types.interfaces";
import { EmailToIdResolver } from "src/modules/shared/utils/resolve-email-id.utils"; // import { EmailToIdResolver } from "src/modules/shared/utils/resolve-email-id.utils";
import { EmployeeTimesheetResolver } from "src/modules/shared/utils/resolve-timesheet.utils"; // import { EmployeeTimesheetResolver } from "src/modules/shared/utils/resolve-timesheet.utils";
import { // import {
BadRequestException, // BadRequestException,
Injectable, // Injectable,
NotFoundException // NotFoundException
} from "@nestjs/common"; // } from "@nestjs/common";
import { // import {
assertAndTrimComment, // assertAndTrimComment,
computeAmountDecimal, // computeAmountDecimal,
computeMileageAmount, // computeMileageAmount,
mapDbExpenseToDayResponse, // mapDbExpenseToDayResponse,
normalizeType, // normalizeType,
parseAttachmentId // parseAttachmentId
} from "../utils/expenses.utils"; // } from "../utils/expenses.utils";
import { toDateOnly } from "src/modules/shifts/helpers/shifts-date-time-helpers";
@Injectable() // @Injectable()
export class ExpensesCommandService extends BaseApprovalService<Expenses> { // export class ExpensesCommandService extends BaseApprovalService<Expenses> {
constructor( // constructor(
prisma: PrismaService, // prisma: PrismaService,
private readonly bankCodesResolver: BankCodesResolver, // private readonly bankCodesResolver: BankCodesResolver,
private readonly timesheetsResolver: EmployeeTimesheetResolver, // private readonly timesheetsResolver: EmployeeTimesheetResolver,
private readonly emailResolver: EmailToIdResolver, // private readonly emailResolver: EmailToIdResolver,
) { super(prisma); } // ) { super(prisma); }
//_____________________________________________________________________________________________ // //_____________________________________________________________________________________________
// APPROVAL TX-DELEGATE METHODS // // APPROVAL TX-DELEGATE METHODS
//_____________________________________________________________________________________________ // //_____________________________________________________________________________________________
protected get delegate() { // protected get delegate() {
return this.prisma.expenses; // return this.prisma.expenses;
} // }
protected delegateFor(transaction: Prisma.TransactionClient){ // protected delegateFor(transaction: Prisma.TransactionClient){
return transaction.expenses; // return transaction.expenses;
} // }
async updateApproval(id: number, isApproved: boolean): Promise<Expenses> { // async updateApproval(id: number, isApproved: boolean): Promise<Expenses> {
return this.prisma.$transaction((transaction) => // return this.prisma.$transaction((transaction) =>
this.updateApprovalWithTransaction(transaction, id, isApproved), // this.updateApprovalWithTransaction(transaction, id, isApproved),
); // );
} // }
//_____________________________________________________________________________________________ // //_____________________________________________________________________________________________
// MASTER CRUD FUNCTION // // MASTER CRUD FUNCTION
//_____________________________________________________________________________________________ // //_____________________________________________________________________________________________
readonly upsertExpensesByDate = async (email: string, date: string, dto: UpsertExpenseDto, // readonly upsertExpensesByDate = async (email: string, date: string, dto: UpsertExpenseDto,
): Promise<{ action:UpsertAction; day: ExpenseResponse[] }> => { // ): Promise<{ action:UpsertAction; day: ExpenseResponse[] }> => {
//validates if there is an existing expense, at least 1 old or new // //validates if there is an existing expense, at least 1 old or new
const { old_expense, new_expense } = dto ?? {}; // const { old_expense, new_expense } = dto ?? {};
if(!old_expense && !new_expense) throw new BadRequestException('At least one expense must be provided'); // if(!old_expense && !new_expense) throw new BadRequestException('At least one expense must be provided');
//validate date format // //validate date format
const date_only = toDateOnly(date); // const date_only = toDateOnly(date);
if(Number.isNaN(date_only.getTime())) throw new BadRequestException('Invalid date format (expected: YYYY-MM-DD)'); // if(Number.isNaN(date_only.getTime())) throw new BadRequestException('Invalid date format (expected: YYYY-MM-DD)');
//resolve employee_id by email // //resolve employee_id by email
const employee_id = await this.emailResolver.findIdByEmail(email); // const employee_id = await this.emailResolver.findIdByEmail(email);
//make sure a timesheet existes // //make sure a timesheet existes
const timesheet_id = await this.timesheetsResolver.findTimesheetIdByEmail(email, date_only); // const timesheet_id = await this.timesheetsResolver.findTimesheetIdByEmail(email, date_only);
if(!timesheet_id) throw new NotFoundException(`no timesheet found for employee #${employee_id}`) // if(!timesheet_id) throw new NotFoundException(`no timesheet found for employee #${employee_id}`)
const {id} = timesheet_id; // const {id} = timesheet_id;
return this.prisma.$transaction(async (tx) => { // return this.prisma.$transaction(async (tx) => {
const loadDay = async (): Promise<ExpenseResponse[]> => { // const loadDay = async (): Promise<ExpenseResponse[]> => {
const rows = await tx.expenses.findMany({ // const rows = await tx.expenses.findMany({
where: { // where: {
timesheet_id: id, // timesheet_id: id,
date: date_only, // date: date_only,
}, // },
include: { // include: {
bank_code: { // bank_code: {
select: { // select: {
type: true, // type: true,
}, // },
}, // },
}, // },
orderBy: [{ date: 'asc' }, { id: 'asc' }], // orderBy: [{ date: 'asc' }, { id: 'asc' }],
}); // });
return rows.map((r) => // return rows.map((r) =>
mapDbExpenseToDayResponse({ // mapDbExpenseToDayResponse({
date: r.date, // date: r.date,
amount: r.amount ?? 0, // amount: r.amount ?? 0,
mileage: r.mileage ?? 0, // mileage: r.mileage ?? 0,
comment: r.comment, // comment: r.comment,
is_approved: r.is_approved, // is_approved: r.is_approved,
bank_code: r.bank_code, // bank_code: r.bank_code,
})); // }));
}; // };
const normalizePayload = async (payload: { // const normalizePayload = async (payload: {
type: string; // type: string;
amount?: number; // amount?: number;
mileage?: number; // mileage?: number;
comment: string; // comment: string;
attachment?: string | number; // attachment?: string | number;
}): Promise<{ // }): Promise<{
type: string; // type: string;
bank_code_id: number; // bank_code_id: number;
amount: Prisma.Decimal; // amount: Prisma.Decimal;
mileage: number | null; // mileage: number | null;
comment: string; // comment: string;
attachment: number | null; // attachment: number | null;
}> => { // }> => {
const type = normalizeType(payload.type); // const type = normalizeType(payload.type);
const comment = assertAndTrimComment(payload.comment); // const comment = assertAndTrimComment(payload.comment);
const attachment = parseAttachmentId(payload.attachment); // const attachment = parseAttachmentId(payload.attachment);
const { id: bank_code_id, modifier } = await this.bankCodesResolver.findByType(type); // const { id: bank_code_id, modifier } = await this.bankCodesResolver.findByType(type);
let amount = computeAmountDecimal(type, payload, modifier); // let amount = computeAmountDecimal(type, payload, modifier);
let mileage: number | null = null; // let mileage: number | null = null;
if (type === 'MILEAGE') { // if (type === 'MILEAGE') {
mileage = Number(payload.mileage ?? 0); // mileage = Number(payload.mileage ?? 0);
if (!(mileage > 0)) { // if (!(mileage > 0)) {
throw new BadRequestException('Mileage required and must be > 0 for type MILEAGE'); // throw new BadRequestException('Mileage required and must be > 0 for type MILEAGE');
} // }
const amountNumber = computeMileageAmount(mileage, modifier); // const amountNumber = computeMileageAmount(mileage, modifier);
amount = new Prisma.Decimal(amountNumber); // amount = new Prisma.Decimal(amountNumber);
} else { // } else {
if (!(typeof payload.amount === 'number' && payload.amount >= 0)) { // if (!(typeof payload.amount === 'number' && payload.amount >= 0)) {
throw new BadRequestException('Amount required for non-MILEAGE expense'); // throw new BadRequestException('Amount required for non-MILEAGE expense');
} // }
amount = new Prisma.Decimal(payload.amount); // amount = new Prisma.Decimal(payload.amount);
} // }
if (attachment !== null) { // if (attachment !== null) {
const attachment_row = await tx.attachments.findUnique({ // const attachment_row = await tx.attachments.findUnique({
where: { id: attachment }, // where: { id: attachment },
select: { status: true }, // select: { status: true },
}); // });
if (!attachment_row || attachment_row.status !== 'ACTIVE') { // if (!attachment_row || attachment_row.status !== 'ACTIVE') {
throw new BadRequestException('Attachment not found or inactive'); // throw new BadRequestException('Attachment not found or inactive');
} // }
} // }
return { // return {
type, // type,
bank_code_id, // bank_code_id,
amount, // amount,
mileage, // mileage,
comment, // comment,
attachment // attachment
}; // };
}; // };
const findExactOld = async (norm: { // const findExactOld = async (norm: {
bank_code_id: number; // bank_code_id: number;
amount: Prisma.Decimal; // amount: Prisma.Decimal;
mileage: number | null; // mileage: number | null;
comment: string; // comment: string;
attachment: number | null; // attachment: number | null;
}) => { // }) => {
return tx.expenses.findFirst({ // return tx.expenses.findFirst({
where: { // where: {
timesheet_id: id, // timesheet_id: id,
date: date_only, // date: date_only,
bank_code_id: norm.bank_code_id, // bank_code_id: norm.bank_code_id,
amount: norm.amount, // amount: norm.amount,
comment: norm.comment, // comment: norm.comment,
attachment: norm.attachment, // attachment: norm.attachment,
...(norm.mileage !== null ? { mileage: norm.mileage } : { mileage: null }), // ...(norm.mileage !== null ? { mileage: norm.mileage } : { mileage: null }),
}, // },
select: { id: true }, // select: { id: true },
}); // });
}; // };
let action : UpsertAction; // let action : UpsertAction;
//_____________________________________________________________________________________________ // //_____________________________________________________________________________________________
// DELETE // // DELETE
//_____________________________________________________________________________________________ // //_____________________________________________________________________________________________
if(old_expense && !new_expense) { // if(old_expense && !new_expense) {
const old_norm = await normalizePayload(old_expense); // const old_norm = await normalizePayload(old_expense);
const existing = await findExactOld(old_norm); // const existing = await findExactOld(old_norm);
if(!existing) { // if(!existing) {
throw new NotFoundException({ // throw new NotFoundException({
error_code: 'EXPENSE_STALE', // error_code: 'EXPENSE_STALE',
message: 'The expense was modified or deleted by someone else', // message: 'The expense was modified or deleted by someone else',
}); // });
} // }
await tx.expenses.delete({where: { id: existing.id } }); // await tx.expenses.delete({where: { id: existing.id } });
action = 'delete'; // action = 'delete';
} // }
//_____________________________________________________________________________________________ // //_____________________________________________________________________________________________
// CREATE // // CREATE
//_____________________________________________________________________________________________ // //_____________________________________________________________________________________________
else if (!old_expense && new_expense) { // else if (!old_expense && new_expense) {
const new_exp = await normalizePayload(new_expense); // const new_exp = await normalizePayload(new_expense);
await tx.expenses.create({ // await tx.expenses.create({
data: { // data: {
timesheet_id: id, // timesheet_id: id,
date: date_only, // date: date_only,
bank_code_id: new_exp.bank_code_id, // bank_code_id: new_exp.bank_code_id,
amount: new_exp.amount, // amount: new_exp.amount,
mileage: new_exp.mileage, // mileage: new_exp.mileage,
comment: new_exp.comment, // comment: new_exp.comment,
attachment: new_exp.attachment, // attachment: new_exp.attachment,
is_approved: false, // is_approved: false,
}, // },
}); // });
action = 'create'; // action = 'create';
} // }
//_____________________________________________________________________________________________ // //_____________________________________________________________________________________________
// UPDATE // // UPDATE
//_____________________________________________________________________________________________ // //_____________________________________________________________________________________________
else if(old_expense && new_expense) { // else if(old_expense && new_expense) {
const old_norm = await normalizePayload(old_expense); // const old_norm = await normalizePayload(old_expense);
const existing = await findExactOld(old_norm); // const existing = await findExactOld(old_norm);
if(!existing) { // if(!existing) {
throw new NotFoundException({ // throw new NotFoundException({
error_code: 'EXPENSE_STALE', // error_code: 'EXPENSE_STALE',
message: 'The expense was modified or deleted by someone else', // message: 'The expense was modified or deleted by someone else',
}); // });
} // }
const new_exp = await normalizePayload(new_expense); // const new_exp = await normalizePayload(new_expense);
await tx.expenses.update({ // await tx.expenses.update({
where: { id: existing.id }, // where: { id: existing.id },
data: { // data: {
bank_code_id: new_exp.bank_code_id, // bank_code_id: new_exp.bank_code_id,
amount: new_exp.amount, // amount: new_exp.amount,
mileage: new_exp.mileage, // mileage: new_exp.mileage,
comment: new_exp.comment, // comment: new_exp.comment,
attachment: new_exp.attachment, // attachment: new_exp.attachment,
}, // },
}); // });
action = 'update'; // action = 'update';
} // }
else { // else {
throw new BadRequestException('Invalid upsert combination'); // throw new BadRequestException('Invalid upsert combination');
} // }
const day = await loadDay(); // const day = await loadDay();
return { action, day }; // return { action, day };
}); // });
} // }
} // }

View File

@ -1,174 +1,171 @@
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 { DayExpensesDto as ExpenseListResponseDto, ExpenseDto } from "src/modules/timesheets/~misc_deprecated-files/timesheet-period.dto"; // import { EmailToIdResolver } from "src/modules/shared/utils/resolve-email-id.utils";
import { round2, toUTCDateOnly } from "src/modules/timesheets/~misc_deprecated-files/utils-helpers-others/timesheet.helpers";
import { EXPENSE_TYPES } from "src/modules/timesheets/~misc_deprecated-files/utils-helpers-others/timesheet.types";
import { EmailToIdResolver } from "src/modules/shared/utils/resolve-email-id.utils";
@Injectable() // @Injectable()
export class ExpensesQueryService { // export class ExpensesQueryService {
constructor( // constructor(
private readonly prisma: PrismaService, // private readonly prisma: PrismaService,
private readonly employeeRepo: EmailToIdResolver, // private readonly employeeRepo: EmailToIdResolver,
) {} // ) {}
//fetchs all expenses for a selected employee using email, pay-period-year and number // //fetchs all expenses for a selected employee using email, pay-period-year and number
async findExpenseListByPayPeriodAndEmail( // async findExpenseListByPayPeriodAndEmail(
email: string, // email: string,
year: number, // year: number,
period_no: number // period_no: number
): Promise<ExpenseListResponseDto> { // ): Promise<ExpenseListResponseDto> {
//fetch employe_id using email // //fetch employe_id using email
const employee_id = await this.employeeRepo.findIdByEmail(email); // const employee_id = await this.employeeRepo.findIdByEmail(email);
if(!employee_id) throw new NotFoundException(`Employee with email: ${email} not found`); // if(!employee_id) throw new NotFoundException(`Employee with email: ${email} not found`);
//fetch pay-period using year and period_no // //fetch pay-period using year and period_no
const pay_period = await this.prisma.payPeriods.findFirst({ // const pay_period = await this.prisma.payPeriods.findFirst({
where: { // where: {
pay_year: year, // pay_year: year,
pay_period_no: period_no // pay_period_no: period_no
}, // },
select: { period_start: true, period_end: true }, // select: { period_start: true, period_end: true },
}); // });
if(!pay_period) throw new NotFoundException(`Pay period ${year}- ${period_no} not found`); // if(!pay_period) throw new NotFoundException(`Pay period ${year}- ${period_no} not found`);
const start = toUTCDateOnly(pay_period.period_start); // const start = toUTCDateOnly(pay_period.period_start);
const end = toUTCDateOnly(pay_period.period_end); // const end = toUTCDateOnly(pay_period.period_end);
//sets rows data // //sets rows data
const rows = await this.prisma.expenses.findMany({ // const rows = await this.prisma.expenses.findMany({
where: { // where: {
date: { gte: start, lte: end }, // date: { gte: start, lte: end },
timesheet: { is: { employee_id } }, // timesheet: { is: { employee_id } },
}, // },
orderBy: { date: 'asc'}, // orderBy: { date: 'asc'},
select: { // select: {
amount: true, // amount: true,
mileage: true, // mileage: true,
comment: true, // comment: true,
is_approved: true, // is_approved: true,
supervisor_comment: true, // supervisor_comment: true,
bank_code: {select: { type: true } }, // bank_code: {select: { type: true } },
}, // },
}); // });
//declare return values // //declare return values
const expenses: ExpenseDto[] = []; // const expenses: ExpenseDto[] = [];
let total_amount = 0; // let total_amount = 0;
let total_mileage = 0; // let total_mileage = 0;
//set rows // //set rows
for(const row of rows) { // for(const row of rows) {
const type = (row.bank_code?.type ?? '').toUpperCase(); // const type = (row.bank_code?.type ?? '').toUpperCase();
const amount = round2(Number(row.amount ?? 0)); // const amount = round2(Number(row.amount ?? 0));
const mileage = round2(Number(row.mileage ?? 0)); // const mileage = round2(Number(row.mileage ?? 0));
if(type === EXPENSE_TYPES.MILEAGE) { // if(type === EXPENSE_TYPES.MILEAGE) {
total_mileage += mileage; // total_mileage += mileage;
} else { // } else {
total_amount += amount; // total_amount += amount;
} // }
//fills rows array // //fills rows array
expenses.push({ // expenses.push({
type, // type,
amount, // amount,
mileage, // mileage,
comment: row.comment ?? '', // comment: row.comment ?? '',
is_approved: row.is_approved ?? false, // is_approved: row.is_approved ?? false,
supervisor_comment: row.supervisor_comment ?? '', // supervisor_comment: row.supervisor_comment ?? '',
}); // });
} // }
return { // return {
expenses, // expenses,
total_expense: round2(total_amount), // total_expense: round2(total_amount),
total_mileage: round2(total_mileage), // total_mileage: round2(total_mileage),
}; // };
} // }
//_____________________________________________________________________________________________ // //_____________________________________________________________________________________________
// Deprecated or unused methods // // Deprecated or unused methods
//_____________________________________________________________________________________________ // //_____________________________________________________________________________________________
// async create(dto: CreateExpenseDto): Promise<Expenses> { // // async create(dto: CreateExpenseDto): Promise<Expenses> {
// const { timesheet_id, bank_code_id, date, amount:rawAmount, // // const { timesheet_id, bank_code_id, date, amount:rawAmount,
// comment, is_approved,supervisor_comment} = dto; // // comment, is_approved,supervisor_comment} = dto;
// //fetches type and modifier // // //fetches type and modifier
// const bank_code = await this.prisma.bankCodes.findUnique({ // // const bank_code = await this.prisma.bankCodes.findUnique({
// where: { id: bank_code_id }, // // where: { id: bank_code_id },
// select: { type: true, modifier: true }, // // select: { type: true, modifier: true },
// }); // // });
// if(!bank_code) throw new NotFoundException(`bank_code #${bank_code_id} not found`); // // if(!bank_code) throw new NotFoundException(`bank_code #${bank_code_id} not found`);
// //if mileage -> service, otherwise the ratio is amount:1 // // //if mileage -> service, otherwise the ratio is amount:1
// let final_amount: number; // // let final_amount: number;
// if(bank_code.type === 'mileage') { // // if(bank_code.type === 'mileage') {
// final_amount = await this.mileageService.calculateReimbursement(rawAmount, bank_code_id); // // final_amount = await this.mileageService.calculateReimbursement(rawAmount, bank_code_id);
// }else { // // }else {
// final_amount = parseFloat( (rawAmount * bank_code.modifier).toFixed(2)); // // final_amount = parseFloat( (rawAmount * bank_code.modifier).toFixed(2));
// } // // }
// return this.prisma.expenses.create({ // // return this.prisma.expenses.create({
// data: { // // data: {
// timesheet_id, // // timesheet_id,
// bank_code_id, // // bank_code_id,
// date, // // date,
// amount: final_amount, // // amount: final_amount,
// comment, // // comment,
// is_approved, // // is_approved,
// supervisor_comment // // supervisor_comment
// }, // // },
// include: { timesheet: { include: { employee: { include: { user: true }}}}, // // include: { timesheet: { include: { employee: { include: { user: true }}}},
// bank_code: true, // // bank_code: true,
// }, // // },
// }) // // })
// } // // }
// async findAll(filters: SearchExpensesDto): Promise<Expenses[]> { // // async findAll(filters: SearchExpensesDto): Promise<Expenses[]> {
// const where = buildPrismaWhere(filters); // // const where = buildPrismaWhere(filters);
// const expenses = await this.prisma.expenses.findMany({ where }) // // const expenses = await this.prisma.expenses.findMany({ where })
// return expenses; // // return expenses;
// } // // }
// async findOne(id: number): Promise<Expenses> { // // async findOne(id: number): Promise<Expenses> {
// const expense = await this.prisma.expenses.findUnique({ // // const expense = await this.prisma.expenses.findUnique({
// where: { id }, // // where: { id },
// include: { timesheet: { include: { employee: { include: { user:true } } } }, // // include: { timesheet: { include: { employee: { include: { user:true } } } },
// bank_code: true, // // bank_code: true,
// }, // // },
// }); // // });
// if (!expense) { // // if (!expense) {
// throw new NotFoundException(`Expense #${id} not found`); // // throw new NotFoundException(`Expense #${id} not found`);
// } // // }
// return expense; // // return expense;
// } // // }
// async update(id: number, dto: UpdateExpenseDto): Promise<Expenses> { // // async update(id: number, dto: UpdateExpenseDto): Promise<Expenses> {
// await this.findOne(id); // // await this.findOne(id);
// const { timesheet_id, bank_code_id, date, amount, // // const { timesheet_id, bank_code_id, date, amount,
// comment, is_approved, supervisor_comment} = dto; // // comment, is_approved, supervisor_comment} = dto;
// return this.prisma.expenses.update({ // // return this.prisma.expenses.update({
// where: { id }, // // where: { id },
// data: { // // data: {
// ...(timesheet_id !== undefined && { timesheet_id}), // // ...(timesheet_id !== undefined && { timesheet_id}),
// ...(bank_code_id !== undefined && { bank_code_id }), // // ...(bank_code_id !== undefined && { bank_code_id }),
// ...(date !== undefined && { date }), // // ...(date !== undefined && { date }),
// ...(amount !== undefined && { amount }), // // ...(amount !== undefined && { amount }),
// ...(comment !== undefined && { comment }), // // ...(comment !== undefined && { comment }),
// ...(is_approved !== undefined && { is_approved }), // // ...(is_approved !== undefined && { is_approved }),
// ...(supervisor_comment !== undefined && { supervisor_comment }), // // ...(supervisor_comment !== undefined && { supervisor_comment }),
// }, // // },
// include: { timesheet: { include: { employee: { include: { user: true } } } }, // // include: { timesheet: { include: { employee: { include: { user: true } } } },
// bank_code: true, // // bank_code: true,
// }, // // },
// }); // // });
// } // // }
// async remove(id: number): Promise<Expenses> { // // async remove(id: number): Promise<Expenses> {
// await this.findOne(id); // // await this.findOne(id);
// return this.prisma.expenses.delete({ where: { id } }); // // return this.prisma.expenses.delete({ where: { id } });
// } // // }
} // }

View File

@ -1,30 +1,30 @@
import { Body, Controller, Post } from "@nestjs/common"; // import { Body, Controller, Post } from "@nestjs/common";
import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; // import { ApiBearerAuth, ApiTags } from "@nestjs/swagger";
import { LeaveRequestsService } from "../services/leave-request.service"; // import { LeaveRequestsService } from "../services/leave-request.service";
import { UpsertLeaveRequestDto } from "../dtos/upsert-leave-request.dto"; // import { UpsertLeaveRequestDto } from "../dtos/upsert-leave-request.dto";
import { LeaveTypes } from "@prisma/client"; // import { LeaveTypes } from "@prisma/client";
@ApiTags('Leave Requests') // @ApiTags('Leave Requests')
@ApiBearerAuth('access-token') // @ApiBearerAuth('access-token')
// @UseGuards() // // @UseGuards()
@Controller('leave-requests') // @Controller('leave-requests')
export class LeaveRequestController { // export class LeaveRequestController {
constructor(private readonly leave_service: LeaveRequestsService){} // constructor(private readonly leave_service: LeaveRequestsService){}
@Post('upsert') // @Post('upsert')
async upsertLeaveRequest(@Body() dto: UpsertLeaveRequestDto) { // async upsertLeaveRequest(@Body() dto: UpsertLeaveRequestDto) {
const { action, leave_requests } = await this.leave_service.handle(dto); // const { action, leave_requests } = await this.leave_service.handle(dto);
return { action, leave_requests }; // return { action, leave_requests };
}q // }q
//TODO: // //TODO:
/* // /*
@Get('archive') // @Get('archive')
findAllArchived(){...} // findAllArchived(){...}
@Get('archive/:id') // @Get('archive/:id')
findOneArchived(id){...} // findOneArchived(id){...}
*/ // */
} // }

View File

@ -1,29 +1,29 @@
import { PrismaService } from "src/prisma/prisma.service"; // import { PrismaService } from "src/prisma/prisma.service";
import { LeaveRequestController } from "./controllers/leave-requests.controller"; // import { LeaveRequestController } from "./controllers/leave-requests.controller";
import { HolidayLeaveRequestsService } from "./services/holiday-leave-requests.service"; // import { HolidayLeaveRequestsService } from "./services/holiday-leave-requests.service";
import { Module } from "@nestjs/common"; // import { Module } from "@nestjs/common";
import { BusinessLogicsModule } from "src/modules/business-logics/business-logics.module"; // import { BusinessLogicsModule } from "src/modules/business-logics/business-logics.module";
import { VacationLeaveRequestsService } from "./services/vacation-leave-requests.service"; // import { VacationLeaveRequestsService } from "./services/vacation-leave-requests.service";
import { SickLeaveRequestsService } from "./services/sick-leave-requests.service"; // import { SickLeaveRequestsService } from "./services/sick-leave-requests.service";
import { LeaveRequestsService } from "./services/leave-request.service"; // import { LeaveRequestsService } from "./services/leave-request.service";
import { ShiftsModule } from "../shifts/shifts.module"; // import { ShiftsModule } from "../shifts/shifts.module";
import { LeaveRequestsUtils } from "./utils/leave-request.util"; // import { LeaveRequestsUtils } from "./utils/leave-request.util";
import { SharedModule } from "../shared/shared.module"; // import { SharedModule } from "../shared/shared.module";
@Module({ // @Module({
imports: [BusinessLogicsModule, ShiftsModule, SharedModule], // imports: [BusinessLogicsModule, ShiftsModule, SharedModule],
controllers: [LeaveRequestController], // controllers: [LeaveRequestController],
providers: [ // providers: [
VacationLeaveRequestsService, // VacationLeaveRequestsService,
SickLeaveRequestsService, // SickLeaveRequestsService,
HolidayLeaveRequestsService, // HolidayLeaveRequestsService,
LeaveRequestsService, // LeaveRequestsService,
PrismaService, // PrismaService,
LeaveRequestsUtils, // LeaveRequestsUtils,
], // ],
exports: [ // exports: [
LeaveRequestsService, // LeaveRequestsService,
], // ],
}) // })
export class LeaveRequestsModule {} // export class LeaveRequestsModule {}

View File

@ -1,78 +1,78 @@
import { UpsertLeaveRequestDto, UpsertResult } from '../dtos/upsert-leave-request.dto'; // import { UpsertLeaveRequestDto, UpsertResult } from '../dtos/upsert-leave-request.dto';
import { LeaveRequestViewDto } from '../dtos/leave-request-view.dto'; // import { LeaveRequestViewDto } from '../dtos/leave-request-view.dto';
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; // import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { LeaveApprovalStatus, LeaveTypes } from '@prisma/client'; // import { LeaveApprovalStatus, LeaveTypes } from '@prisma/client';
import { HolidayService } from 'src/modules/business-logics/services/holiday.service'; // import { HolidayService } from 'src/modules/business-logics/services/holiday.service';
import { PrismaService } from 'src/prisma/prisma.service'; // import { PrismaService } from 'src/prisma/prisma.service';
import { mapRowToView } from '../mappers/leave-requests.mapper'; // import { mapRowToView } from '../mappers/leave-requests.mapper';
import { leaveRequestsSelect } from '../utils/leave-requests.select'; // import { leaveRequestsSelect } from '../utils/leave-requests.select';
import { LeaveRequestsUtils} from '../utils/leave-request.util'; // import { LeaveRequestsUtils} from '../utils/leave-request.util';
import { normalizeDates, toDateOnly } from 'src/modules/shared/helpers/date-time.helpers'; // import { normalizeDates, toDateOnly } from 'src/modules/shared/helpers/date-time.helpers';
import { BankCodesResolver } from 'src/modules/shared/utils/resolve-bank-type-id.utils'; // import { BankCodesResolver } from 'src/modules/shared/utils/resolve-bank-type-id.utils';
import { EmailToIdResolver } from 'src/modules/shared/utils/resolve-email-id.utils'; // import { EmailToIdResolver } from 'src/modules/shared/utils/resolve-email-id.utils';
@Injectable() // @Injectable()
export class HolidayLeaveRequestsService { // export class HolidayLeaveRequestsService {
constructor( // constructor(
private readonly prisma: PrismaService, // private readonly prisma: PrismaService,
private readonly holidayService: HolidayService, // private readonly holidayService: HolidayService,
private readonly leaveUtils: LeaveRequestsUtils, // private readonly leaveUtils: LeaveRequestsUtils,
private readonly emailResolver: EmailToIdResolver, // private readonly emailResolver: EmailToIdResolver,
private readonly typeResolver: BankCodesResolver, // private readonly typeResolver: BankCodesResolver,
) {} // ) {}
async create(dto: UpsertLeaveRequestDto): Promise<UpsertResult> { // async create(dto: UpsertLeaveRequestDto): Promise<UpsertResult> {
const email = dto.email.trim(); // const email = dto.email.trim();
const employee_id = await this.emailResolver.findIdByEmail(email); // const employee_id = await this.emailResolver.findIdByEmail(email);
const bank_code = await this.typeResolver.findByType(LeaveTypes.HOLIDAY); // const bank_code = await this.typeResolver.findByType(LeaveTypes.HOLIDAY);
if(!bank_code) throw new NotFoundException(`bank_code not found`); // if(!bank_code) throw new NotFoundException(`bank_code not found`);
const dates = normalizeDates(dto.dates); // const dates = normalizeDates(dto.dates);
if (!dates.length) throw new BadRequestException('Dates array must not be empty'); // if (!dates.length) throw new BadRequestException('Dates array must not be empty');
const created: LeaveRequestViewDto[] = []; // const created: LeaveRequestViewDto[] = [];
for (const iso_date of dates) { // for (const iso_date of dates) {
const date = toDateOnly(iso_date); // const date = toDateOnly(iso_date);
const existing = await this.prisma.leaveRequests.findUnique({ // const existing = await this.prisma.leaveRequests.findUnique({
where: { // where: {
leave_per_employee_date: { // leave_per_employee_date: {
employee_id: employee_id, // employee_id: employee_id,
leave_type: LeaveTypes.HOLIDAY, // leave_type: LeaveTypes.HOLIDAY,
date, // date,
}, // },
}, // },
select: { id: true }, // select: { id: true },
}); // });
if (existing) { // if (existing) {
throw new BadRequestException(`Holiday request already exists for ${iso_date}`); // throw new BadRequestException(`Holiday request already exists for ${iso_date}`);
} // }
const payable = await this.holidayService.calculateHolidayPay(email, date, bank_code.modifier); // const payable = await this.holidayService.calculateHolidayPay(email, date, bank_code.modifier);
const row = await this.prisma.leaveRequests.create({ // const row = await this.prisma.leaveRequests.create({
data: { // data: {
employee_id: employee_id, // employee_id: employee_id,
bank_code_id: bank_code.id, // bank_code_id: bank_code.id,
leave_type: LeaveTypes.HOLIDAY, // leave_type: LeaveTypes.HOLIDAY,
date, // date,
comment: dto.comment ?? '', // comment: dto.comment ?? '',
requested_hours: dto.requested_hours ?? 8, // requested_hours: dto.requested_hours ?? 8,
payable_hours: payable, // payable_hours: payable,
approval_status: dto.approval_status ?? LeaveApprovalStatus.PENDING, // approval_status: dto.approval_status ?? LeaveApprovalStatus.PENDING,
}, // },
select: leaveRequestsSelect, // select: leaveRequestsSelect,
}); // });
const hours = Number(row.payable_hours ?? row.requested_hours ?? 0); // const hours = Number(row.payable_hours ?? row.requested_hours ?? 0);
if (row.approval_status === LeaveApprovalStatus.APPROVED) { // if (row.approval_status === LeaveApprovalStatus.APPROVED) {
await this.leaveUtils.syncShift(email, employee_id, iso_date, hours,LeaveTypes.HOLIDAY, row.comment); // await this.leaveUtils.syncShift(email, employee_id, iso_date, hours,LeaveTypes.HOLIDAY, row.comment);
} // }
created.push({ ...mapRowToView(row), action: 'create' }); // created.push({ ...mapRowToView(row), action: 'create' });
} // }
return { action: 'create', leave_requests: created }; // return { action: 'create', leave_requests: created };
} // }
} // }

View File

@ -1,248 +1,248 @@
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; // import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
import { LeaveApprovalStatus, LeaveTypes } from "@prisma/client"; // import { LeaveApprovalStatus, LeaveTypes } from "@prisma/client";
import { roundToQuarterHour } from "src/common/utils/date-utils"; // import { roundToQuarterHour } from "src/common/utils/date-utils";
import { UpsertLeaveRequestDto, UpsertResult } from "../dtos/upsert-leave-request.dto"; // import { UpsertLeaveRequestDto, UpsertResult } from "../dtos/upsert-leave-request.dto";
import { LeaveRequestViewDto } from "../dtos/leave-request-view.dto"; // import { LeaveRequestViewDto } from "../dtos/leave-request-view.dto";
import { mapRowToView } from "../mappers/leave-requests.mapper"; // import { mapRowToView } from "../mappers/leave-requests.mapper";
import { leaveRequestsSelect } from "../utils/leave-requests.select"; // import { leaveRequestsSelect } from "../utils/leave-requests.select";
import { HolidayLeaveRequestsService } from "./holiday-leave-requests.service"; // import { HolidayLeaveRequestsService } from "./holiday-leave-requests.service";
import { SickLeaveRequestsService } from "./sick-leave-requests.service"; // import { SickLeaveRequestsService } from "./sick-leave-requests.service";
import { VacationLeaveRequestsService } from "./vacation-leave-requests.service"; // import { VacationLeaveRequestsService } from "./vacation-leave-requests.service";
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 { PrismaService } from "src/prisma/prisma.service"; // import { PrismaService } from "src/prisma/prisma.service";
import { LeaveRequestsUtils } from "../utils/leave-request.util"; // import { LeaveRequestsUtils } from "../utils/leave-request.util";
import { normalizeDates, toDateOnly, toISODateKey } from "src/modules/shared/helpers/date-time.helpers"; // import { normalizeDates, toDateOnly, toISODateKey } from "src/modules/shared/helpers/date-time.helpers";
import { EmailToIdResolver } from "src/modules/shared/utils/resolve-email-id.utils"; // import { EmailToIdResolver } from "src/modules/shared/utils/resolve-email-id.utils";
import { BankCodesResolver } from "src/modules/shared/utils/resolve-bank-type-id.utils"; // import { BankCodesResolver } from "src/modules/shared/utils/resolve-bank-type-id.utils";
@Injectable() // @Injectable()
export class LeaveRequestsService { // export class LeaveRequestsService {
constructor( // constructor(
private readonly prisma: PrismaService, // private readonly prisma: PrismaService,
private readonly holidayLeaveService: HolidayLeaveRequestsService, // private readonly holidayLeaveService: HolidayLeaveRequestsService,
private readonly holidayService: HolidayService, // private readonly holidayService: HolidayService,
private readonly sickLogic: SickLeaveService, // private readonly sickLogic: SickLeaveService,
private readonly sickLeaveService: SickLeaveRequestsService, // private readonly sickLeaveService: SickLeaveRequestsService,
private readonly vacationLeaveService: VacationLeaveRequestsService, // private readonly vacationLeaveService: VacationLeaveRequestsService,
private readonly vacationLogic: VacationService, // private readonly vacationLogic: VacationService,
private readonly leaveUtils: LeaveRequestsUtils, // private readonly leaveUtils: LeaveRequestsUtils,
private readonly emailResolver: EmailToIdResolver, // private readonly emailResolver: EmailToIdResolver,
private readonly typeResolver: BankCodesResolver, // private readonly typeResolver: BankCodesResolver,
) {} // ) {}
//handle distribution to the right service according to the selected type and action // //handle distribution to the right service according to the selected type and action
async handle(dto: UpsertLeaveRequestDto): Promise<UpsertResult> { // async handle(dto: UpsertLeaveRequestDto): Promise<UpsertResult> {
switch (dto.type) { // switch (dto.type) {
case LeaveTypes.HOLIDAY: // case LeaveTypes.HOLIDAY:
if( dto.action === 'create'){ // if( dto.action === 'create'){
return this.holidayLeaveService.create(dto); // return this.holidayLeaveService.create(dto);
} else if (dto.action === 'update') { // } else if (dto.action === 'update') {
return this.update(dto, LeaveTypes.HOLIDAY); // return this.update(dto, LeaveTypes.HOLIDAY);
} else if (dto.action === 'delete'){ // } else if (dto.action === 'delete'){
return this.delete(dto, LeaveTypes.HOLIDAY); // return this.delete(dto, LeaveTypes.HOLIDAY);
} // }
case LeaveTypes.VACATION: // case LeaveTypes.VACATION:
if( dto.action === 'create'){ // if( dto.action === 'create'){
return this.vacationLeaveService.create(dto); // return this.vacationLeaveService.create(dto);
} else if (dto.action === 'update') { // } else if (dto.action === 'update') {
return this.update(dto, LeaveTypes.VACATION); // return this.update(dto, LeaveTypes.VACATION);
} else if (dto.action === 'delete'){ // } else if (dto.action === 'delete'){
return this.delete(dto, LeaveTypes.VACATION); // return this.delete(dto, LeaveTypes.VACATION);
} // }
case LeaveTypes.SICK: // case LeaveTypes.SICK:
if( dto.action === 'create'){ // if( dto.action === 'create'){
return this.sickLeaveService.create(dto); // return this.sickLeaveService.create(dto);
} else if (dto.action === 'update') { // } else if (dto.action === 'update') {
return this.update(dto, LeaveTypes.SICK); // return this.update(dto, LeaveTypes.SICK);
} else if (dto.action === 'delete'){ // } else if (dto.action === 'delete'){
return this.delete(dto, LeaveTypes.SICK); // return this.delete(dto, LeaveTypes.SICK);
} // }
default: // default:
throw new BadRequestException(`Unsupported leave type: ${dto.type} or action: ${dto.action}`); // throw new BadRequestException(`Unsupported leave type: ${dto.type} or action: ${dto.action}`);
} // }
} // }
async delete(dto: UpsertLeaveRequestDto, type: LeaveTypes): Promise<UpsertResult> { // async delete(dto: UpsertLeaveRequestDto, type: LeaveTypes): Promise<UpsertResult> {
const email = dto.email.trim(); // const email = dto.email.trim();
const dates = normalizeDates(dto.dates); // const dates = normalizeDates(dto.dates);
const employee_id = await this.emailResolver.findIdByEmail(email); // const employee_id = await this.emailResolver.findIdByEmail(email);
if (!dates.length) throw new BadRequestException("Dates array must not be empty"); // if (!dates.length) throw new BadRequestException("Dates array must not be empty");
const rows = await this.prisma.leaveRequests.findMany({ // const rows = await this.prisma.leaveRequests.findMany({
where: { // where: {
employee_id: employee_id, // employee_id: employee_id,
leave_type: type, // leave_type: type,
date: { in: dates.map((d) => toDateOnly(d)) }, // date: { in: dates.map((d) => toDateOnly(d)) },
}, // },
select: leaveRequestsSelect, // select: leaveRequestsSelect,
}); // });
if (rows.length !== dates.length) { // if (rows.length !== dates.length) {
const missing = dates.filter((isoDate) => !rows.some((row) => toISODateKey(row.date) === isoDate)); // const missing = dates.filter((isoDate) => !rows.some((row) => toISODateKey(row.date) === isoDate));
throw new NotFoundException(`No Leave request found for: ${missing.join(", ")}`); // throw new NotFoundException(`No Leave request found for: ${missing.join(", ")}`);
} // }
for (const row of rows) { // for (const row of rows) {
if (row.approval_status === LeaveApprovalStatus.APPROVED) { // if (row.approval_status === LeaveApprovalStatus.APPROVED) {
const iso = toISODateKey(row.date); // const iso = toISODateKey(row.date);
await this.leaveUtils.removeShift(email, employee_id, iso, type); // await this.leaveUtils.removeShift(email, employee_id, iso, type);
} // }
} // }
await this.prisma.leaveRequests.deleteMany({ // await this.prisma.leaveRequests.deleteMany({
where: { id: { in: rows.map((row) => row.id) } }, // where: { id: { in: rows.map((row) => row.id) } },
}); // });
const deleted = rows.map((row) => ({ ...mapRowToView(row), action: "delete" as const })); // const deleted = rows.map((row) => ({ ...mapRowToView(row), action: "delete" as const }));
return { action: "delete", leave_requests: deleted }; // return { action: "delete", leave_requests: deleted };
} // }
async update(dto: UpsertLeaveRequestDto, type: LeaveTypes): Promise<UpsertResult> { // async update(dto: UpsertLeaveRequestDto, type: LeaveTypes): Promise<UpsertResult> {
const email = dto.email.trim(); // const email = dto.email.trim();
const employee_id = await this.emailResolver.findIdByEmail(email); // const employee_id = await this.emailResolver.findIdByEmail(email);
const bank_code = await this.typeResolver.findByType(type); // const bank_code = await this.typeResolver.findByType(type);
if(!bank_code) throw new NotFoundException(`bank_code not found`); // if(!bank_code) throw new NotFoundException(`bank_code not found`);
const modifier = Number(bank_code.modifier ?? 1); // const modifier = Number(bank_code.modifier ?? 1);
const dates = normalizeDates(dto.dates); // const dates = normalizeDates(dto.dates);
if (!dates.length) { // if (!dates.length) {
throw new BadRequestException("Dates array must not be empty"); // throw new BadRequestException("Dates array must not be empty");
} // }
const entries = await Promise.all( // const entries = await Promise.all(
dates.map(async (iso_date) => { // dates.map(async (iso_date) => {
const date = toDateOnly(iso_date); // const date = toDateOnly(iso_date);
const existing = await this.prisma.leaveRequests.findUnique({ // const existing = await this.prisma.leaveRequests.findUnique({
where: { // where: {
leave_per_employee_date: { // leave_per_employee_date: {
employee_id: employee_id, // employee_id: employee_id,
leave_type: type, // leave_type: type,
date, // date,
}, // },
}, // },
select: leaveRequestsSelect, // select: leaveRequestsSelect,
}); // });
if (!existing) throw new NotFoundException(`No Leave request found for ${iso_date}`); // if (!existing) throw new NotFoundException(`No Leave request found for ${iso_date}`);
return { iso_date, date, existing }; // return { iso_date, date, existing };
}), // }),
); // );
const updated: LeaveRequestViewDto[] = []; // const updated: LeaveRequestViewDto[] = [];
if (type === LeaveTypes.SICK) { // if (type === LeaveTypes.SICK) {
const firstExisting = entries[0].existing; // const firstExisting = entries[0].existing;
const fallbackRequested = // const fallbackRequested =
firstExisting.requested_hours !== null && firstExisting.requested_hours !== undefined // firstExisting.requested_hours !== null && firstExisting.requested_hours !== undefined
? Number(firstExisting.requested_hours) // ? Number(firstExisting.requested_hours)
: 8; // : 8;
const requested_hours_per_day = dto.requested_hours ?? fallbackRequested; // const requested_hours_per_day = dto.requested_hours ?? fallbackRequested;
const reference_date = entries.reduce( // const reference_date = entries.reduce(
(latest, entry) => (entry.date > latest ? entry.date : latest), // (latest, entry) => (entry.date > latest ? entry.date : latest),
entries[0].date, // entries[0].date,
); // );
const total_payable_hours = await this.sickLogic.calculateSickLeavePay( // const total_payable_hours = await this.sickLogic.calculateSickLeavePay(
employee_id, // employee_id,
reference_date, // reference_date,
entries.length, // entries.length,
requested_hours_per_day, // requested_hours_per_day,
modifier, // modifier,
); // );
let remaining_payable_hours = roundToQuarterHour(Math.max(0, total_payable_hours)); // let remaining_payable_hours = roundToQuarterHour(Math.max(0, total_payable_hours));
const daily_payable_cap = roundToQuarterHour(requested_hours_per_day * modifier); // const daily_payable_cap = roundToQuarterHour(requested_hours_per_day * modifier);
for (const { iso_date, existing } of entries) { // for (const { iso_date, existing } of entries) {
const previous_status = existing.approval_status; // const previous_status = existing.approval_status;
const payable = Math.min(remaining_payable_hours, daily_payable_cap); // const payable = Math.min(remaining_payable_hours, daily_payable_cap);
const payable_rounded = roundToQuarterHour(Math.max(0, payable)); // const payable_rounded = roundToQuarterHour(Math.max(0, payable));
remaining_payable_hours = roundToQuarterHour( // remaining_payable_hours = roundToQuarterHour(
Math.max(0, remaining_payable_hours - payable_rounded), // Math.max(0, remaining_payable_hours - payable_rounded),
); // );
const row = await this.prisma.leaveRequests.update({ // const row = await this.prisma.leaveRequests.update({
where: { id: existing.id }, // where: { id: existing.id },
data: { // data: {
comment: dto.comment ?? existing.comment, // comment: dto.comment ?? existing.comment,
requested_hours: requested_hours_per_day, // requested_hours: requested_hours_per_day,
payable_hours: payable_rounded, // payable_hours: payable_rounded,
bank_code_id: bank_code.id, // bank_code_id: bank_code.id,
approval_status: dto.approval_status ?? existing.approval_status, // approval_status: dto.approval_status ?? existing.approval_status,
}, // },
select: leaveRequestsSelect, // select: leaveRequestsSelect,
}); // });
const was_approved = previous_status === LeaveApprovalStatus.APPROVED; // const was_approved = previous_status === LeaveApprovalStatus.APPROVED;
const is_approved = row.approval_status === LeaveApprovalStatus.APPROVED; // const is_approved = row.approval_status === LeaveApprovalStatus.APPROVED;
const hours = Number(row.payable_hours ?? row.requested_hours ?? 0); // const hours = Number(row.payable_hours ?? row.requested_hours ?? 0);
if (!was_approved && is_approved) { // if (!was_approved && is_approved) {
await this.leaveUtils.syncShift(email, employee_id, iso_date, hours, type, row.comment); // await this.leaveUtils.syncShift(email, employee_id, iso_date, hours, type, row.comment);
} else if (was_approved && !is_approved) { // } else if (was_approved && !is_approved) {
await this.leaveUtils.removeShift(email, employee_id, iso_date, type); // await this.leaveUtils.removeShift(email, employee_id, iso_date, type);
} else if (was_approved && is_approved) { // } else if (was_approved && is_approved) {
await this.leaveUtils.syncShift(email, employee_id, iso_date, hours, type, row.comment); // await this.leaveUtils.syncShift(email, employee_id, iso_date, hours, type, row.comment);
} // }
updated.push({ ...mapRowToView(row), action: "update" }); // updated.push({ ...mapRowToView(row), action: "update" });
} // }
return { action: "update", leave_requests: updated }; // return { action: "update", leave_requests: updated };
} // }
for (const { iso_date, date, existing } of entries) { // for (const { iso_date, date, existing } of entries) {
const previous_status = existing.approval_status; // const previous_status = existing.approval_status;
const fallbackRequested = // const fallbackRequested =
existing.requested_hours !== null && existing.requested_hours !== undefined // existing.requested_hours !== null && existing.requested_hours !== undefined
? Number(existing.requested_hours) // ? Number(existing.requested_hours)
: 8; // : 8;
const requested_hours = dto.requested_hours ?? fallbackRequested; // const requested_hours = dto.requested_hours ?? fallbackRequested;
let payable: number; // let payable: number;
switch (type) { // switch (type) {
case LeaveTypes.HOLIDAY: // case LeaveTypes.HOLIDAY:
payable = await this.holidayService.calculateHolidayPay(email, date, modifier); // payable = await this.holidayService.calculateHolidayPay(email, date, modifier);
break; // break;
case LeaveTypes.VACATION: { // case LeaveTypes.VACATION: {
const days_requested = requested_hours / 8; // const days_requested = requested_hours / 8;
payable = await this.vacationLogic.calculateVacationPay( // payable = await this.vacationLogic.calculateVacationPay(
employee_id, // employee_id,
date, // date,
Math.max(0, days_requested), // Math.max(0, days_requested),
modifier, // modifier,
); // );
break; // break;
} // }
default: // default:
payable = existing.payable_hours !== null && existing.payable_hours !== undefined // payable = existing.payable_hours !== null && existing.payable_hours !== undefined
? Number(existing.payable_hours) // ? Number(existing.payable_hours)
: requested_hours; // : requested_hours;
} // }
const row = await this.prisma.leaveRequests.update({ // const row = await this.prisma.leaveRequests.update({
where: { id: existing.id }, // where: { id: existing.id },
data: { // data: {
requested_hours, // requested_hours,
comment: dto.comment ?? existing.comment, // comment: dto.comment ?? existing.comment,
payable_hours: payable, // payable_hours: payable,
bank_code_id: bank_code.id, // bank_code_id: bank_code.id,
approval_status: dto.approval_status ?? existing.approval_status, // approval_status: dto.approval_status ?? existing.approval_status,
}, // },
select: leaveRequestsSelect, // select: leaveRequestsSelect,
}); // });
const was_approved = previous_status === LeaveApprovalStatus.APPROVED; // const was_approved = previous_status === LeaveApprovalStatus.APPROVED;
const is_approved = row.approval_status === LeaveApprovalStatus.APPROVED; // const is_approved = row.approval_status === LeaveApprovalStatus.APPROVED;
const hours = Number(row.payable_hours ?? row.requested_hours ?? 0); // const hours = Number(row.payable_hours ?? row.requested_hours ?? 0);
if (!was_approved && is_approved) { // if (!was_approved && is_approved) {
await this.leaveUtils.syncShift(email, employee_id, iso_date, hours, type, row.comment); // await this.leaveUtils.syncShift(email, employee_id, iso_date, hours, type, row.comment);
} else if (was_approved && !is_approved) { // } else if (was_approved && !is_approved) {
await this.leaveUtils.removeShift(email, employee_id, iso_date, type); // await this.leaveUtils.removeShift(email, employee_id, iso_date, type);
} else if (was_approved && is_approved) { // } else if (was_approved && is_approved) {
await this.leaveUtils.syncShift(email, employee_id, iso_date, hours, type, row.comment); // await this.leaveUtils.syncShift(email, employee_id, iso_date, hours, type, row.comment);
} // }
updated.push({ ...mapRowToView(row), action: "update" }); // updated.push({ ...mapRowToView(row), action: "update" });
} // }
return { action: "update", leave_requests: updated }; // return { action: "update", leave_requests: updated };
} // }
} // }

View File

@ -1,98 +1,98 @@
import { UpsertLeaveRequestDto, UpsertResult } from "../dtos/upsert-leave-request.dto"; // import { UpsertLeaveRequestDto, UpsertResult } from "../dtos/upsert-leave-request.dto";
import { LeaveRequestViewDto } from "../dtos/leave-request-view.dto"; // import { LeaveRequestViewDto } from "../dtos/leave-request-view.dto";
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; // import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
import { LeaveApprovalStatus, LeaveTypes } from "@prisma/client"; // import { LeaveApprovalStatus, LeaveTypes } from "@prisma/client";
import { leaveRequestsSelect } from "../utils/leave-requests.select"; // import { leaveRequestsSelect } from "../utils/leave-requests.select";
import { mapRowToView } from "../mappers/leave-requests.mapper"; // import { mapRowToView } from "../mappers/leave-requests.mapper";
import { PrismaService } from "src/prisma/prisma.service"; // import { PrismaService } from "src/prisma/prisma.service";
import { SickLeaveService } from "src/modules/business-logics/services/sick-leave.service"; // import { SickLeaveService } from "src/modules/business-logics/services/sick-leave.service";
import { roundToQuarterHour } from "src/common/utils/date-utils"; // import { roundToQuarterHour } from "src/common/utils/date-utils";
import { LeaveRequestsUtils } from "../utils/leave-request.util"; // import { LeaveRequestsUtils } from "../utils/leave-request.util";
import { normalizeDates, toDateOnly } from "src/modules/shared/helpers/date-time.helpers"; // import { normalizeDates, toDateOnly } from "src/modules/shared/helpers/date-time.helpers";
import { EmailToIdResolver } from "src/modules/shared/utils/resolve-email-id.utils"; // import { EmailToIdResolver } from "src/modules/shared/utils/resolve-email-id.utils";
import { BankCodesResolver } from "src/modules/shared/utils/resolve-bank-type-id.utils"; // import { BankCodesResolver } from "src/modules/shared/utils/resolve-bank-type-id.utils";
@Injectable() // @Injectable()
export class SickLeaveRequestsService { // export class SickLeaveRequestsService {
constructor( // constructor(
private readonly prisma: PrismaService, // private readonly prisma: PrismaService,
private readonly sickService: SickLeaveService, // private readonly sickService: SickLeaveService,
private readonly leaveUtils: LeaveRequestsUtils, // private readonly leaveUtils: LeaveRequestsUtils,
private readonly emailResolver: EmailToIdResolver, // private readonly emailResolver: EmailToIdResolver,
private readonly typeResolver: BankCodesResolver, // private readonly typeResolver: BankCodesResolver,
) {} // ) {}
async create(dto: UpsertLeaveRequestDto): Promise<UpsertResult> { // async create(dto: UpsertLeaveRequestDto): Promise<UpsertResult> {
const email = dto.email.trim(); // const email = dto.email.trim();
const employee_id = await this.emailResolver.findIdByEmail(email); // const employee_id = await this.emailResolver.findIdByEmail(email);
const bank_code = await this.typeResolver.findByType(LeaveTypes.SICK); // const bank_code = await this.typeResolver.findByType(LeaveTypes.SICK);
if(!bank_code) throw new NotFoundException(`bank_code not found`); // if(!bank_code) throw new NotFoundException(`bank_code not found`);
const modifier = bank_code.modifier ?? 1; // const modifier = bank_code.modifier ?? 1;
const dates = normalizeDates(dto.dates); // const dates = normalizeDates(dto.dates);
if (!dates.length) throw new BadRequestException("Dates array must not be empty"); // if (!dates.length) throw new BadRequestException("Dates array must not be empty");
const requested_hours_per_day = dto.requested_hours ?? 8; // const requested_hours_per_day = dto.requested_hours ?? 8;
const entries = dates.map((iso) => ({ iso, date: toDateOnly(iso) })); // const entries = dates.map((iso) => ({ iso, date: toDateOnly(iso) }));
const reference_date = entries.reduce( // const reference_date = entries.reduce(
(latest, entry) => (entry.date > latest ? entry.date : latest), // (latest, entry) => (entry.date > latest ? entry.date : latest),
entries[0].date, // entries[0].date,
); // );
const total_payable_hours = await this.sickService.calculateSickLeavePay( // const total_payable_hours = await this.sickService.calculateSickLeavePay(
employee_id, // employee_id,
reference_date, // reference_date,
entries.length, // entries.length,
requested_hours_per_day, // requested_hours_per_day,
modifier, // modifier,
); // );
let remaining_payable_hours = roundToQuarterHour(Math.max(0, total_payable_hours)); // let remaining_payable_hours = roundToQuarterHour(Math.max(0, total_payable_hours));
const daily_payable_cap = roundToQuarterHour(requested_hours_per_day * modifier); // const daily_payable_cap = roundToQuarterHour(requested_hours_per_day * modifier);
const created: LeaveRequestViewDto[] = []; // const created: LeaveRequestViewDto[] = [];
for (const { iso, date } of entries) { // for (const { iso, date } of entries) {
const existing = await this.prisma.leaveRequests.findUnique({ // const existing = await this.prisma.leaveRequests.findUnique({
where: { // where: {
leave_per_employee_date: { // leave_per_employee_date: {
employee_id: employee_id, // employee_id: employee_id,
leave_type: LeaveTypes.SICK, // leave_type: LeaveTypes.SICK,
date, // date,
}, // },
}, // },
select: { id: true }, // select: { id: true },
}); // });
if (existing) { // if (existing) {
throw new BadRequestException(`Sick request already exists for ${iso}`); // throw new BadRequestException(`Sick request already exists for ${iso}`);
} // }
const payable = Math.min(remaining_payable_hours, daily_payable_cap); // const payable = Math.min(remaining_payable_hours, daily_payable_cap);
const payable_rounded = roundToQuarterHour(Math.max(0, payable)); // const payable_rounded = roundToQuarterHour(Math.max(0, payable));
remaining_payable_hours = roundToQuarterHour( // remaining_payable_hours = roundToQuarterHour(
Math.max(0, remaining_payable_hours - payable_rounded), // Math.max(0, remaining_payable_hours - payable_rounded),
); // );
const row = await this.prisma.leaveRequests.create({ // const row = await this.prisma.leaveRequests.create({
data: { // data: {
employee_id: employee_id, // employee_id: employee_id,
bank_code_id: bank_code.id, // bank_code_id: bank_code.id,
leave_type: LeaveTypes.SICK, // leave_type: LeaveTypes.SICK,
comment: dto.comment ?? "", // comment: dto.comment ?? "",
requested_hours: requested_hours_per_day, // requested_hours: requested_hours_per_day,
payable_hours: payable_rounded, // payable_hours: payable_rounded,
approval_status: dto.approval_status ?? LeaveApprovalStatus.PENDING, // approval_status: dto.approval_status ?? LeaveApprovalStatus.PENDING,
date, // date,
}, // },
select: leaveRequestsSelect, // select: leaveRequestsSelect,
}); // });
const hours = Number(row.payable_hours ?? row.requested_hours ?? 0); // const hours = Number(row.payable_hours ?? row.requested_hours ?? 0);
if (row.approval_status === LeaveApprovalStatus.APPROVED) { // if (row.approval_status === LeaveApprovalStatus.APPROVED) {
await this.leaveUtils.syncShift(email, employee_id, iso, hours,LeaveTypes.SICK, row.comment); // await this.leaveUtils.syncShift(email, employee_id, iso, hours,LeaveTypes.SICK, row.comment);
} // }
created.push({ ...mapRowToView(row), action: "create" }); // created.push({ ...mapRowToView(row), action: "create" });
} // }
return { action: "create", leave_requests: created }; // return { action: "create", leave_requests: created };
} // }
} // }

View File

@ -1,93 +1,93 @@
 
import { UpsertLeaveRequestDto, UpsertResult } from "../dtos/upsert-leave-request.dto"; // import { UpsertLeaveRequestDto, UpsertResult } from "../dtos/upsert-leave-request.dto";
import { LeaveRequestViewDto } from "../dtos/leave-request-view.dto"; // import { LeaveRequestViewDto } from "../dtos/leave-request-view.dto";
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; // import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
import { LeaveApprovalStatus, LeaveTypes } from "@prisma/client"; // import { LeaveApprovalStatus, LeaveTypes } from "@prisma/client";
import { VacationService } from "src/modules/business-logics/services/vacation.service"; // import { VacationService } from "src/modules/business-logics/services/vacation.service";
import { PrismaService } from "src/prisma/prisma.service"; // import { PrismaService } from "src/prisma/prisma.service";
import { mapRowToView } from "../mappers/leave-requests.mapper"; // import { mapRowToView } from "../mappers/leave-requests.mapper";
import { leaveRequestsSelect } from "../utils/leave-requests.select"; // import { leaveRequestsSelect } from "../utils/leave-requests.select";
import { roundToQuarterHour } from "src/common/utils/date-utils"; // import { roundToQuarterHour } from "src/common/utils/date-utils";
import { LeaveRequestsUtils } from "../utils/leave-request.util"; // import { LeaveRequestsUtils } from "../utils/leave-request.util";
import { normalizeDates, toDateOnly } from "src/modules/shared/helpers/date-time.helpers"; // import { normalizeDates, toDateOnly } from "src/modules/shared/helpers/date-time.helpers";
import { EmailToIdResolver } from "src/modules/shared/utils/resolve-email-id.utils"; // import { EmailToIdResolver } from "src/modules/shared/utils/resolve-email-id.utils";
import { BankCodesResolver } from "src/modules/shared/utils/resolve-bank-type-id.utils"; // import { BankCodesResolver } from "src/modules/shared/utils/resolve-bank-type-id.utils";
@Injectable() // @Injectable()
export class VacationLeaveRequestsService { // export class VacationLeaveRequestsService {
constructor( // constructor(
private readonly prisma: PrismaService, // private readonly prisma: PrismaService,
private readonly vacationService: VacationService, // private readonly vacationService: VacationService,
private readonly leaveUtils: LeaveRequestsUtils, // private readonly leaveUtils: LeaveRequestsUtils,
private readonly emailResolver: EmailToIdResolver, // private readonly emailResolver: EmailToIdResolver,
private readonly typeResolver: BankCodesResolver, // private readonly typeResolver: BankCodesResolver,
) {} // ) {}
async create(dto: UpsertLeaveRequestDto): Promise<UpsertResult> { // async create(dto: UpsertLeaveRequestDto): Promise<UpsertResult> {
const email = dto.email.trim(); // const email = dto.email.trim();
const employee_id = await this.emailResolver.findIdByEmail(email); // const employee_id = await this.emailResolver.findIdByEmail(email);
const bank_code = await this.typeResolver.findByType(LeaveTypes.VACATION); // const bank_code = await this.typeResolver.findByType(LeaveTypes.VACATION);
if(!bank_code) throw new NotFoundException(`bank_code not found`); // if(!bank_code) throw new NotFoundException(`bank_code not found`);
const modifier = bank_code.modifier ?? 1; // const modifier = bank_code.modifier ?? 1;
const dates = normalizeDates(dto.dates); // const dates = normalizeDates(dto.dates);
const requested_hours_per_day = dto.requested_hours ?? 8; // const requested_hours_per_day = dto.requested_hours ?? 8;
if (!dates.length) throw new BadRequestException("Dates array must not be empty"); // if (!dates.length) throw new BadRequestException("Dates array must not be empty");
const entries = dates // const entries = dates
.map((iso) => ({ iso, date: toDateOnly(iso) })) // .map((iso) => ({ iso, date: toDateOnly(iso) }))
.sort((a, b) => a.date.getTime() - b.date.getTime()); // .sort((a, b) => a.date.getTime() - b.date.getTime());
const start_date = entries[0].date; // const start_date = entries[0].date;
const total_payable_hours = await this.vacationService.calculateVacationPay( // const total_payable_hours = await this.vacationService.calculateVacationPay(
employee_id, // employee_id,
start_date, // start_date,
entries.length, // entries.length,
modifier, // modifier,
); // );
let remaining_payable_hours = roundToQuarterHour(Math.max(0, total_payable_hours)); // let remaining_payable_hours = roundToQuarterHour(Math.max(0, total_payable_hours));
const daily_payable_cap = roundToQuarterHour(requested_hours_per_day * modifier); // const daily_payable_cap = roundToQuarterHour(requested_hours_per_day * modifier);
const created: LeaveRequestViewDto[] = []; // const created: LeaveRequestViewDto[] = [];
for (const { iso, date } of entries) { // for (const { iso, date } of entries) {
const existing = await this.prisma.leaveRequests.findUnique({ // const existing = await this.prisma.leaveRequests.findUnique({
where: { // where: {
leave_per_employee_date: { // leave_per_employee_date: {
employee_id: employee_id, // employee_id: employee_id,
leave_type: LeaveTypes.VACATION, // leave_type: LeaveTypes.VACATION,
date, // date,
}, // },
}, // },
select: { id: true }, // select: { id: true },
}); // });
if (existing) throw new BadRequestException(`Vacation request already exists for ${iso}`); // if (existing) throw new BadRequestException(`Vacation request already exists for ${iso}`);
const payable = Math.min(remaining_payable_hours, daily_payable_cap); // const payable = Math.min(remaining_payable_hours, daily_payable_cap);
const payable_rounded = roundToQuarterHour(Math.max(0, payable)); // const payable_rounded = roundToQuarterHour(Math.max(0, payable));
remaining_payable_hours = roundToQuarterHour( // remaining_payable_hours = roundToQuarterHour(
Math.max(0, remaining_payable_hours - payable_rounded), // Math.max(0, remaining_payable_hours - payable_rounded),
); // );
const row = await this.prisma.leaveRequests.create({ // const row = await this.prisma.leaveRequests.create({
data: { // data: {
employee_id: employee_id, // employee_id: employee_id,
bank_code_id: bank_code.id, // bank_code_id: bank_code.id,
payable_hours: payable_rounded, // payable_hours: payable_rounded,
requested_hours: requested_hours_per_day, // requested_hours: requested_hours_per_day,
leave_type: LeaveTypes.VACATION, // leave_type: LeaveTypes.VACATION,
comment: dto.comment ?? "", // comment: dto.comment ?? "",
approval_status: dto.approval_status ?? LeaveApprovalStatus.PENDING, // approval_status: dto.approval_status ?? LeaveApprovalStatus.PENDING,
date, // date,
}, // },
select: leaveRequestsSelect, // select: leaveRequestsSelect,
}); // });
const hours = Number(row.payable_hours ?? row.requested_hours ?? 0); // const hours = Number(row.payable_hours ?? row.requested_hours ?? 0);
if (row.approval_status === LeaveApprovalStatus.APPROVED) { // if (row.approval_status === LeaveApprovalStatus.APPROVED) {
await this.leaveUtils.syncShift(email, employee_id, iso, hours, LeaveTypes.VACATION, row.comment); // await this.leaveUtils.syncShift(email, employee_id, iso, hours, LeaveTypes.VACATION, row.comment);
} // }
created.push({ ...mapRowToView(row), action: "create" }); // created.push({ ...mapRowToView(row), action: "create" });
} // }
return { action: "create", leave_requests: created }; // return { action: "create", leave_requests: created };
} // }
} // }

View File

@ -1,105 +1,104 @@
import { hhmmFromLocal, toDateOnly, toStringFromDate } from "src/modules/shared/helpers/date-time.helpers"; // import { hhmmFromLocal, toDateOnly, toStringFromDate } from "src/modules/shared/helpers/date-time.helpers";
import { BadRequestException, Injectable } from "@nestjs/common"; // import { BadRequestException, Injectable } from "@nestjs/common";
import { ShiftsCommandService } from "src/modules/shifts/_deprecated-files/shifts-command.service"; // import { PrismaService } from "src/prisma/prisma.service";
import { PrismaService } from "src/prisma/prisma.service"; // import { LeaveTypes } from "@prisma/client";
import { LeaveTypes } from "@prisma/client"; // import { UpsertAction } from "src/modules/shared/types/upsert-actions.types";
import { UpsertAction } from "src/modules/shared/types/upsert-actions.types";
@Injectable() // @Injectable()
export class LeaveRequestsUtils { // export class LeaveRequestsUtils {
constructor( // constructor(
private readonly prisma: PrismaService, // private readonly prisma: PrismaService,
private readonly shiftsCommand: ShiftsCommandService, // private readonly shiftsCommand: ShiftsCommandService,
){} // ){}
async syncShift( // async syncShift(
email: string, // email: string,
employee_id: number, // employee_id: number,
date: string, // date: string,
hours: number, // hours: number,
type: LeaveTypes, // type: LeaveTypes,
comment?: string, // comment?: string,
) { // ) {
if (hours <= 0) return; // if (hours <= 0) return;
const duration_minutes = Math.round(hours * 60); // const duration_minutes = Math.round(hours * 60);
if (duration_minutes > 8 * 60) { // if (duration_minutes > 8 * 60) {
throw new BadRequestException("Amount of hours cannot exceed 8 hours per day."); // throw new BadRequestException("Amount of hours cannot exceed 8 hours per day.");
} // }
const date_only = toDateOnly(date); // const date_only = toDateOnly(date);
const yyyy_mm_dd = toStringFromDate(date_only); // const yyyy_mm_dd = toStringFromDate(date_only);
const start_minutes = 8 * 60; // const start_minutes = 8 * 60;
const end_minutes = start_minutes + duration_minutes; // const end_minutes = start_minutes + duration_minutes;
const toHHmm = (total: number) => // const toHHmm = (total: number) =>
`${String(Math.floor(total / 60)).padStart(2, "0")}:${String(total % 60).padStart(2, "0")}`; // `${String(Math.floor(total / 60)).padStart(2, "0")}:${String(total % 60).padStart(2, "0")}`;
const existing = await this.prisma.shifts.findFirst({ // const existing = await this.prisma.shifts.findFirst({
where: { // where: {
date: date_only, // date: date_only,
bank_code: { type }, // bank_code: { type },
timesheet: { employee_id: employee_id }, // timesheet: { employee_id: employee_id },
}, // },
include: { bank_code: true }, // include: { bank_code: true },
}); // });
const action: UpsertAction = existing ? 'update' : 'create'; // const action: UpsertAction = existing ? 'update' : 'create';
await this.shiftsCommand.upsertShifts(email, action, { // await this.shiftsCommand.upsertShifts(email, action, {
old_shift: existing // old_shift: existing
? { // ? {
date: yyyy_mm_dd, // date: yyyy_mm_dd,
start_time: existing.start_time.toISOString().slice(11, 16), // start_time: existing.start_time.toISOString().slice(11, 16),
end_time: existing.end_time.toISOString().slice(11, 16), // end_time: existing.end_time.toISOString().slice(11, 16),
type: existing.bank_code?.type ?? type, // type: existing.bank_code?.type ?? type,
is_remote: existing.is_remote, // is_remote: existing.is_remote,
is_approved:existing.is_approved, // is_approved:existing.is_approved,
comment: existing.comment ?? undefined, // comment: existing.comment ?? undefined,
} // }
: undefined, // : undefined,
new_shift: { // new_shift: {
date: yyyy_mm_dd, // date: yyyy_mm_dd,
start_time: toHHmm(start_minutes), // start_time: toHHmm(start_minutes),
end_time: toHHmm(end_minutes), // end_time: toHHmm(end_minutes),
is_remote: existing?.is_remote ?? false, // is_remote: existing?.is_remote ?? false,
is_approved:existing?.is_approved ?? false, // is_approved:existing?.is_approved ?? false,
comment: comment ?? existing?.comment ?? "", // comment: comment ?? existing?.comment ?? "",
type: type, // type: type,
}, // },
}); // });
} // }
async removeShift( // async removeShift(
email: string, // email: string,
employee_id: number, // employee_id: number,
iso_date: string, // iso_date: string,
type: LeaveTypes, // type: LeaveTypes,
) { // ) {
const date_only = toDateOnly(iso_date); // const date_only = toDateOnly(iso_date);
const yyyy_mm_dd = toStringFromDate(date_only); // const yyyy_mm_dd = toStringFromDate(date_only);
const existing = await this.prisma.shifts.findFirst({ // const existing = await this.prisma.shifts.findFirst({
where: { // where: {
date: date_only, // date: date_only,
bank_code: { type }, // bank_code: { type },
timesheet: { employee_id: employee_id }, // timesheet: { employee_id: employee_id },
}, // },
include: { bank_code: true }, // include: { bank_code: true },
}); // });
if (!existing) return; // if (!existing) return;
await this.shiftsCommand.upsertShifts(email, 'delete', { // await this.shiftsCommand.upsertShifts(email, 'delete', {
old_shift: { // old_shift: {
date: yyyy_mm_dd, // date: yyyy_mm_dd,
start_time: hhmmFromLocal(existing.start_time), // start_time: hhmmFromLocal(existing.start_time),
end_time: hhmmFromLocal(existing.end_time), // end_time: hhmmFromLocal(existing.end_time),
type: existing.bank_code?.type ?? type, // type: existing.bank_code?.type ?? type,
is_remote: existing.is_remote, // is_remote: existing.is_remote,
is_approved:existing.is_approved, // is_approved:existing.is_approved,
comment: existing.comment ?? undefined, // comment: existing.comment ?? undefined,
}, // },
}); // });
} // }
} // }

View File

@ -1,33 +1,27 @@
import { PrismaModule } from "src/prisma/prisma.module"; // import { PrismaModule } from "src/prisma/prisma.module";
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/~misc_deprecated-files/timesheets-command.service"; // import { ExpensesCommandService } from "../expenses/services/expenses-command.service";
import { ExpensesCommandService } from "../expenses/services/expenses-command.service"; // import { SharedModule } from "../shared/shared.module";
import { ShiftsCommandService } from "../shifts/_deprecated-files/shifts-command.service"; // import { PrismaService } from "src/prisma/prisma.service";
import { SharedModule } from "../shared/shared.module"; // import { BusinessLogicsModule } from "../business-logics/business-logics.module";
import { PrismaService } from "src/prisma/prisma.service";
import { BusinessLogicsModule } from "../business-logics/business-logics.module";
import { ShiftsHelpersService } from "../shifts/_deprecated-files/shifts.helpers";
@Module({ // @Module({
imports: [PrismaModule, TimesheetsModule, SharedModule, BusinessLogicsModule], // imports: [PrismaModule, TimesheetsModule, SharedModule, BusinessLogicsModule],
providers: [ // providers: [
PayPeriodsQueryService, // PayPeriodsQueryService,
PayPeriodsCommandService, // PayPeriodsCommandService,
TimesheetsCommandService, // ExpensesCommandService,
ExpensesCommandService, // PrismaService,
ShiftsCommandService, // ],
PrismaService, // controllers: [PayPeriodsController],
ShiftsHelpersService, // exports: [
], // PayPeriodsQueryService,
controllers: [PayPeriodsController], // PayPeriodsCommandService,
exports: [ // ]
PayPeriodsQueryService, // })
PayPeriodsCommandService,
]
})
export class PayperiodsModule {} // export class PayperiodsModule {}

View File

@ -1,14 +1,14 @@
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common"; import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
import { TimesheetsCommandService } from "src/modules/timesheets/~misc_deprecated-files/timesheets-command.service";
import { PrismaService } from "src/prisma/prisma.service"; import { PrismaService } from "src/prisma/prisma.service";
import { BulkCrewApprovalDto } from "../dtos/bulk-crew-approval.dto"; import { BulkCrewApprovalDto } from "../dtos/bulk-crew-approval.dto";
import { PayPeriodsQueryService } from "./pay-periods-query.service"; import { PayPeriodsQueryService } from "./pay-periods-query.service";
import { TimesheetApprovalService } from "src/modules/timesheets/services/timesheet-approval.service";
@Injectable() @Injectable()
export class PayPeriodsCommandService { export class PayPeriodsCommandService {
constructor( constructor(
private readonly prisma: PrismaService, private readonly prisma: PrismaService,
private readonly timesheets_approval: TimesheetsCommandService, private readonly timesheets_approval: TimesheetApprovalService,
private readonly query: PayPeriodsQueryService, private readonly query: PayPeriodsQueryService,
) {} ) {}

View File

@ -0,0 +1,38 @@
import { Injectable } from "@nestjs/common";
import { Prisma, Timesheets } from "@prisma/client";
import { BaseApprovalService } from "src/common/shared/base-approval.service";
import { PrismaService } from "src/prisma/prisma.service";
@Injectable()
export class TimesheetApprovalService extends BaseApprovalService<Timesheets>{
constructor(prisma: PrismaService){super(prisma)}
//_____________________________________________________________________________________________
// APPROVAL AND DELEGATE METHODS
//_____________________________________________________________________________________________
protected get delegate() {
return this.prisma.timesheets;
}
protected delegateFor(transaction: Prisma.TransactionClient) {
return transaction.timesheets;
}
async updateApproval(id: number, isApproved: boolean): Promise<Timesheets> {
return this.prisma.$transaction((transaction) =>
this.updateApprovalWithTransaction(transaction, id, isApproved),
);
}
async cascadeApprovalWithtx(transaction: Prisma.TransactionClient, timesheetId: number, isApproved: boolean): Promise<Timesheets> {
const timesheet = await this.updateApprovalWithTransaction(transaction, timesheetId, isApproved);
await transaction.shifts.updateMany({
where: { timesheet_id: timesheetId },
data: { is_approved: isApproved },
});
await transaction.expenses.updateManyAndReturn({
where: { timesheet_id: timesheetId },
data: { is_approved: isApproved },
});
return timesheet;
}
}

View File

@ -1,33 +1,33 @@
import { Type } from "class-transformer"; // import { Type } from "class-transformer";
import { IsArray, IsOptional, IsString, Length, Matches, ValidateNested } from "class-validator"; // import { IsArray, IsOptional, IsString, Length, Matches, ValidateNested } from "class-validator";
export class CreateTimesheetDto { // export class CreateTimesheetDto {
@IsString() // @IsString()
@Matches(/^\d{4}-\d{2}-\d{2}$/) // @Matches(/^\d{4}-\d{2}-\d{2}$/)
date!: string; // date!: string;
@IsString() // @IsString()
@Length(1,64) // @Length(1,64)
type!: string; // type!: string;
@IsString() // @IsString()
@Matches(/^\d{2}:\d{2}$/) // @Matches(/^\d{2}:\d{2}$/)
start_time!: string; // start_time!: string;
@IsString() // @IsString()
@Matches(/^\d{2}:\d{2}$/) // @Matches(/^\d{2}:\d{2}$/)
end_time!: string; // end_time!: string;
@IsOptional() // @IsOptional()
@IsString() // @IsString()
@Length(0,512) // @Length(0,512)
comment?: string; // comment?: string;
} // }
export class CreateWeekShiftsDto { // export class CreateWeekShiftsDto {
@IsArray() // @IsArray()
@ValidateNested({each:true}) // @ValidateNested({each:true})
@Type(()=> CreateTimesheetDto) // @Type(()=> CreateTimesheetDto)
shifts!: CreateTimesheetDto[]; // shifts!: CreateTimesheetDto[];
} // }

View File

@ -1,20 +1,20 @@
import { Type } from "class-transformer"; // import { Type } from "class-transformer";
import { IsBoolean, IsInt, IsOptional } from "class-validator"; // import { IsBoolean, IsInt, IsOptional } from "class-validator";
export class SearchTimesheetDto { // export class SearchTimesheetDto {
@IsOptional() // @IsOptional()
@Type(() => Number) // @Type(() => Number)
@IsInt() // @IsInt()
timesheet_id?: number; // timesheet_id?: number;
@IsOptional() // @IsOptional()
@Type(()=> Number) // @Type(()=> Number)
@IsInt() // @IsInt()
employee_id?: number; // employee_id?: number;
@IsOptional() // @IsOptional()
@Type(()=> Boolean) // @Type(()=> Boolean)
@IsBoolean() // @IsBoolean()
is_approved?: boolean; // is_approved?: boolean;
} // }

View File

@ -1,75 +1,75 @@
export class TimesheetDto { // export class TimesheetDto {
start_day: string; // start_day: string;
end_day: string; // end_day: string;
label: string; // label: string;
shifts: ShiftDto[]; // shifts: ShiftDto[];
expenses: ExpenseDto[] // expenses: ExpenseDto[]
is_approved: boolean; // is_approved: boolean;
} // }
export class ShiftDto { // export class ShiftDto {
date: string; // date: string;
type: string; // type: string;
start_time: string; // start_time: string;
end_time : string; // end_time : string;
comment: string; // comment: string;
is_approved: boolean; // is_approved: boolean;
is_remote: boolean; // is_remote: boolean;
} // }
export class ExpenseDto { // export class ExpenseDto {
type: string; // type: string;
amount: number; // amount: number;
mileage: number; // mileage: number;
comment: string; // comment: string;
is_approved: boolean; // is_approved: boolean;
supervisor_comment: string; // supervisor_comment: string;
} // }
export type DayShiftsDto = ShiftDto[]; // export type DayShiftsDto = ShiftDto[];
export class DetailedShifts { // export class DetailedShifts {
shifts: DayShiftsDto; // shifts: DayShiftsDto;
regular_hours: number; // regular_hours: number;
evening_hours: number; // evening_hours: number;
overtime_hours: number; // overtime_hours: number;
emergency_hours: number; // emergency_hours: number;
comment: string; // comment: string;
short_date: string; // short_date: string;
break_durations?: number; // break_durations?: number;
} // }
export class DayExpensesDto { // export class DayExpensesDto {
expenses: ExpenseDto[] = []; // expenses: ExpenseDto[] = [];
total_mileage: number; // total_mileage: number;
total_expense: number; // total_expense: number;
} // }
export class WeekDto { // export class WeekDto {
is_approved: boolean; // is_approved: boolean;
shifts: { // shifts: {
sun: DetailedShifts; // sun: DetailedShifts;
mon: DetailedShifts; // mon: DetailedShifts;
tue: DetailedShifts; // tue: DetailedShifts;
wed: DetailedShifts; // wed: DetailedShifts;
thu: DetailedShifts; // thu: DetailedShifts;
fri: DetailedShifts; // fri: DetailedShifts;
sat: DetailedShifts; // sat: DetailedShifts;
} // }
expenses: { // expenses: {
sun: DayExpensesDto; // sun: DayExpensesDto;
mon: DayExpensesDto; // mon: DayExpensesDto;
tue: DayExpensesDto; // tue: DayExpensesDto;
wed: DayExpensesDto; // wed: DayExpensesDto;
thu: DayExpensesDto; // thu: DayExpensesDto;
fri: DayExpensesDto; // fri: DayExpensesDto;
sat: DayExpensesDto; // sat: DayExpensesDto;
} // }
} // }
export class TimesheetPeriodDto { // export class TimesheetPeriodDto {
weeks: WeekDto[]; // weeks: WeekDto[];
employee_full_name: string; // employee_full_name: string;
} // }

View File

@ -1,67 +1,67 @@
import { MS_PER_DAY } from "src/modules/shared/constants/date-time.constant"; // import { MS_PER_DAY } from "src/modules/shared/constants/date-time.constant";
import { DAY_KEYS, DayKey } from "./timesheet.types"; // import { DAY_KEYS, DayKey } from "./timesheet.types";
export function toUTCDateOnly(date: Date | string): Date { // export function toUTCDateOnly(date: Date | string): Date {
const d = new Date(date); // const d = new Date(date);
return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate())); // return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
} // }
export function addDays(date:Date, days: number): Date { // export function addDays(date:Date, days: number): Date {
return new Date(date.getTime() + days * MS_PER_DAY); // return new Date(date.getTime() + days * MS_PER_DAY);
} // }
export function endOfDayUTC(date: Date | string): Date { // export function endOfDayUTC(date: Date | string): Date {
const d = toUTCDateOnly(date); // const d = toUTCDateOnly(date);
return new Date(d.getTime() + MS_PER_DAY - 1); // return new Date(d.getTime() + MS_PER_DAY - 1);
} // }
export function isBetweenUTC(date: Date, start: Date, end_inclusive: Date): boolean { // export function isBetweenUTC(date: Date, start: Date, end_inclusive: Date): boolean {
const time = date.getTime(); // const time = date.getTime();
return time >= start.getTime() && time <= end_inclusive.getTime(); // return time >= start.getTime() && time <= end_inclusive.getTime();
} // }
export function toTimeString(date: Date): string { // export function toTimeString(date: Date): string {
const hours = String(date.getUTCHours()).padStart(2,'0'); // const hours = String(date.getUTCHours()).padStart(2,'0');
const minutes = String(date.getUTCMinutes()).padStart(2,'0'); // const minutes = String(date.getUTCMinutes()).padStart(2,'0');
return `${hours}:${minutes}`; // return `${hours}:${minutes}`;
} // }
export function round2(num: number) { // export function round2(num: number) {
return Math.round(num * 100) / 100; // return Math.round(num * 100) / 100;
} // }
export function shortDate(date:Date): string { // export function shortDate(date:Date): string {
const mm = String(date.getUTCMonth()+1).padStart(2,'0'); // const mm = String(date.getUTCMonth()+1).padStart(2,'0');
const dd = String(date.getUTCDate()).padStart(2,'0'); // const dd = String(date.getUTCDate()).padStart(2,'0');
return `${mm}/${dd}`; // return `${mm}/${dd}`;
} // }
export function dayKeyFromDate(date: Date, useUTC = true): DayKey { // export function dayKeyFromDate(date: Date, useUTC = true): DayKey {
const index = useUTC ? date.getUTCDay() : date.getDay(); // 0=Sunday..6=Saturday // const index = useUTC ? date.getUTCDay() : date.getDay(); // 0=Sunday..6=Saturday
return DAY_KEYS[index]; // return DAY_KEYS[index];
} // }
export const toHHmm = (date: Date) => date.toISOString().slice(11, 16); // export const toHHmm = (date: Date) => date.toISOString().slice(11, 16);
export function parseISODate(iso: string): Date { // export function parseISODate(iso: string): Date {
const [ y, m, d ] = iso.split('-').map(Number); // const [ y, m, d ] = iso.split('-').map(Number);
return new Date(y, (m ?? 1) - 1, d ?? 1); // return new Date(y, (m ?? 1) - 1, d ?? 1);
} // }
export function parseHHmm(t: string): Date { // export function parseHHmm(t: string): Date {
const [ hh, mm ] = t.split(':').map(Number); // const [ hh, mm ] = t.split(':').map(Number);
return new Date(1970, 0, 1, hh || 0, mm || 0, 0, 0); // return new Date(1970, 0, 1, hh || 0, mm || 0, 0, 0);
} // }
export const toNum = (value: any) => // export const toNum = (value: any) =>
value && typeof value.toNumber === 'function' ? value.toNumber() : // value && typeof value.toNumber === 'function' ? value.toNumber() :
typeof value === 'number' ? value : // typeof value === 'number' ? value :
value ? Number(value) : 0; // value ? Number(value) : 0;
export const upper = (s?: string | null) => String(s ?? '').toUpperCase(); // export const upper = (s?: string | null) => String(s ?? '').toUpperCase();
export const toRangeFromPeriod = (period: { period_start: Date; period_end: Date }) => ({ // export const toRangeFromPeriod = (period: { period_start: Date; period_end: Date }) => ({
from: toUTCDateOnly(period.period_start), // from: toUTCDateOnly(period.period_start),
to: endOfDayUTC(period.period_end), // to: endOfDayUTC(period.period_end),
}); // });

View File

@ -1,111 +1,111 @@
import { DayExpensesDto, WeekDto, DetailedShifts, TimesheetPeriodDto } from "../dtos/timesheet-period.dto"; // import { DayExpensesDto, WeekDto, DetailedShifts, TimesheetPeriodDto } from "../dtos/timesheet-period.dto";
import { ShiftRow, ExpenseRow, ExpensesAmount, TimesheetMap } from "./timesheet.types"; // import { ShiftRow, ExpenseRow, ExpensesAmount, TimesheetMap } from "./timesheet.types";
import { addDays, shortDate, toNum, upper } from "./timesheet.helpers"; // import { addDays, shortDate, toNum, upper } from "./timesheet.helpers";
import { Prisma } from "@prisma/client"; // import { Prisma } from "@prisma/client";
//mappers // //mappers
export const mapShiftRow = (shift: { // export const mapShiftRow = (shift: {
date: Date; // date: Date;
start_time: Date; // start_time: Date;
end_time: Date; // end_time: Date;
comment?: string | null; // comment?: string | null;
is_approved: boolean; // is_approved: boolean;
is_remote: boolean; // is_remote: boolean;
bank_code: { type: string }; // bank_code: { type: string };
}): ShiftRow => ({ // }): ShiftRow => ({
date: shift.date, // date: shift.date,
start_time: shift.start_time, // start_time: shift.start_time,
end_time: shift.end_time, // end_time: shift.end_time,
comment: shift.comment ?? '', // comment: shift.comment ?? '',
is_approved: shift.is_approved, // is_approved: shift.is_approved,
is_remote: shift.is_remote, // is_remote: shift.is_remote,
type: upper(shift.bank_code.type), // type: upper(shift.bank_code.type),
}); // });
export const mapExpenseRow = (expense: { // export const mapExpenseRow = (expense: {
date: Date; // date: Date;
amount: Prisma.Decimal | number | null; // amount: Prisma.Decimal | number | null;
mileage: Prisma.Decimal | number | null; // mileage: Prisma.Decimal | number | null;
comment?: string | null; // comment?: string | null;
is_approved: boolean; // is_approved: boolean;
supervisor_comment?: string|null; // supervisor_comment?: string|null;
bank_code: { type: string }, // bank_code: { type: string },
}): ExpenseRow => ({ // }): ExpenseRow => ({
date: expense.date, // date: expense.date,
amount: toNum(expense.amount), // amount: toNum(expense.amount),
mileage: toNum(expense.mileage), // mileage: toNum(expense.mileage),
comment: expense.comment ?? '', // comment: expense.comment ?? '',
is_approved: expense.is_approved, // is_approved: expense.is_approved,
supervisor_comment: expense.supervisor_comment ?? '', // supervisor_comment: expense.supervisor_comment ?? '',
type: upper(expense.bank_code.type), // type: upper(expense.bank_code.type),
}); // });
// Factories // // Factories
export function makeEmptyDayExpenses(): DayExpensesDto { // export function makeEmptyDayExpenses(): DayExpensesDto {
return { // return {
expenses: [], // expenses: [],
total_expense: -1, // total_expense: -1,
total_mileage: -1, // total_mileage: -1,
}; // };
} // }
export function makeEmptyWeek(week_start: Date): WeekDto { // export function makeEmptyWeek(week_start: Date): WeekDto {
const make_empty_shifts = (offset: number): DetailedShifts => ({ // const make_empty_shifts = (offset: number): DetailedShifts => ({
shifts: [], // shifts: [],
regular_hours: 0, // regular_hours: 0,
evening_hours: 0, // evening_hours: 0,
emergency_hours: 0, // emergency_hours: 0,
overtime_hours: 0, // overtime_hours: 0,
comment: '', // comment: '',
short_date: shortDate(addDays(week_start, offset)), // short_date: shortDate(addDays(week_start, offset)),
break_durations: 0, // break_durations: 0,
}); // });
return { // return {
is_approved: true, // is_approved: true,
shifts: { // shifts: {
sun: make_empty_shifts(0), // sun: make_empty_shifts(0),
mon: make_empty_shifts(1), // mon: make_empty_shifts(1),
tue: make_empty_shifts(2), // tue: make_empty_shifts(2),
wed: make_empty_shifts(3), // wed: make_empty_shifts(3),
thu: make_empty_shifts(4), // thu: make_empty_shifts(4),
fri: make_empty_shifts(5), // fri: make_empty_shifts(5),
sat: make_empty_shifts(6), // sat: make_empty_shifts(6),
}, // },
expenses: { // expenses: {
sun: makeEmptyDayExpenses(), // sun: makeEmptyDayExpenses(),
mon: makeEmptyDayExpenses(), // mon: makeEmptyDayExpenses(),
tue: makeEmptyDayExpenses(), // tue: makeEmptyDayExpenses(),
wed: makeEmptyDayExpenses(), // wed: makeEmptyDayExpenses(),
thu: makeEmptyDayExpenses(), // thu: makeEmptyDayExpenses(),
fri: makeEmptyDayExpenses(), // fri: makeEmptyDayExpenses(),
sat: makeEmptyDayExpenses(), // sat: makeEmptyDayExpenses(),
}, // },
}; // };
} // }
export function makeEmptyPeriod(): TimesheetPeriodDto { // export function makeEmptyPeriod(): TimesheetPeriodDto {
return { weeks: [makeEmptyWeek(new Date()), makeEmptyWeek(new Date())], employee_full_name: '' }; // return { weeks: [makeEmptyWeek(new Date()), makeEmptyWeek(new Date())], employee_full_name: '' };
} // }
export const makeAmounts = (): ExpensesAmount => ({ // export const makeAmounts = (): ExpensesAmount => ({
expense: 0, // expense: 0,
mileage: 0, // mileage: 0,
}); // });
export function makeEmptyTimesheet(params: { // export function makeEmptyTimesheet(params: {
start_day: string; // start_day: string;
end_day: string; // end_day: string;
label: string; // label: string;
is_approved?: boolean; // is_approved?: boolean;
}): TimesheetMap { // }): TimesheetMap {
const { start_day, end_day, label, is_approved = false } = params; // const { start_day, end_day, label, is_approved = false } = params;
return { // return {
start_day, // start_day,
end_day, // end_day,
label, // label,
shifts: [], // shifts: [],
expenses: [], // expenses: [],
is_approved, // is_approved,
}; // };
} // }

View File

@ -1,46 +1,46 @@
import { EXPENSE_ASC_ORDER, EXPENSE_SELECT } from "../../../shared/selects/expenses.select"; // import { EXPENSE_ASC_ORDER, EXPENSE_SELECT } from "../../../shared/selects/expenses.select";
import { Injectable, NotFoundException } from "@nestjs/common"; // import { Injectable, NotFoundException } from "@nestjs/common";
import { SHIFT_ASC_ORDER, SHIFT_SELECT } from "../../../shared/selects/shifts.select"; // import { SHIFT_ASC_ORDER, SHIFT_SELECT } from "../../../shared/selects/shifts.select";
import { PAY_PERIOD_SELECT } from "../../../shared/selects/pay-periods.select"; // import { PAY_PERIOD_SELECT } from "../../../shared/selects/pay-periods.select";
import { PrismaService } from "src/prisma/prisma.service"; // import { PrismaService } from "src/prisma/prisma.service";
@Injectable() // @Injectable()
export class TimesheetSelectorsService { // export class TimesheetSelectorsService {
constructor(readonly prisma: PrismaService){} // constructor(readonly prisma: PrismaService){}
async getPayPeriod(pay_year: number, pay_period_no: number) { // async getPayPeriod(pay_year: number, pay_period_no: number) {
const period = await this.prisma.payPeriods.findFirst({ // const period = await this.prisma.payPeriods.findFirst({
where: { pay_year, pay_period_no }, // where: { pay_year, pay_period_no },
select: PAY_PERIOD_SELECT , // select: PAY_PERIOD_SELECT ,
}); // });
if(!period) throw new NotFoundException(`period ${pay_year}-${pay_period_no} not found`); // if(!period) throw new NotFoundException(`period ${pay_year}-${pay_period_no} not found`);
return period; // return period;
} // }
async getShifts(employee_id: number, from: Date, to: Date) { // async getShifts(employee_id: number, from: Date, to: Date) {
return this.prisma.shifts.findMany({ // return this.prisma.shifts.findMany({
where: {timesheet: { is: { employee_id } }, date: { gte: from, lte: to } }, // where: {timesheet: { is: { employee_id } }, date: { gte: from, lte: to } },
select: SHIFT_SELECT, // select: SHIFT_SELECT,
orderBy: SHIFT_ASC_ORDER, // orderBy: SHIFT_ASC_ORDER,
}); // });
} // }
async getExpenses(employee_id: number, from: Date, to: Date) { // async getExpenses(employee_id: number, from: Date, to: Date) {
return this.prisma.expenses.findMany({ // return this.prisma.expenses.findMany({
where: { timesheet: {is: { employee_id } }, date: { gte: from, lte: to } }, // where: { timesheet: {is: { employee_id } }, date: { gte: from, lte: to } },
select: EXPENSE_SELECT, // select: EXPENSE_SELECT,
orderBy: EXPENSE_ASC_ORDER, // orderBy: EXPENSE_ASC_ORDER,
}); // });
} // }
async getTimesheetWithShiftsAndExpenses(employee_id: number, start_date_week: Date) { // async getTimesheetWithShiftsAndExpenses(employee_id: number, start_date_week: Date) {
return this.prisma.timesheets.findUnique({ // return this.prisma.timesheets.findUnique({
where: { employee_id_start_date: { employee_id, start_date: start_date_week } }, // where: { employee_id_start_date: { employee_id, start_date: start_date_week } },
select: { // select: {
is_approved: true, // is_approved: true,
shift: { select: SHIFT_SELECT, orderBy: SHIFT_ASC_ORDER }, // shift: { select: SHIFT_SELECT, orderBy: SHIFT_ASC_ORDER },
expense: { select: EXPENSE_SELECT, orderBy: EXPENSE_ASC_ORDER }, // expense: { select: EXPENSE_SELECT, orderBy: EXPENSE_ASC_ORDER },
}, // },
}); // });
} // }
} // }

View File

@ -1,74 +1,74 @@
export type ShiftRow = { // export type ShiftRow = {
date: Date; // date: Date;
start_time: Date; // start_time: Date;
end_time: Date; // end_time: Date;
comment: string; // comment: string;
is_approved?: boolean; // is_approved?: boolean;
is_remote: boolean; // is_remote: boolean;
type: string // type: string
}; // };
export type ExpenseRow = { // export type ExpenseRow = {
date: Date; // date: Date;
amount: number; // amount: number;
mileage?: number | null; // mileage?: number | null;
comment: string; // comment: string;
type: string; // type: string;
is_approved?: boolean; // is_approved?: boolean;
supervisor_comment: string; // supervisor_comment: string;
}; // };
export type TimesheetMap = { // export type TimesheetMap = {
start_day: string; // start_day: string;
end_day: string; // end_day: string;
label: string; // label: string;
shifts: ShiftRow[]; // shifts: ShiftRow[];
expenses: ExpenseRow[] // expenses: ExpenseRow[]
is_approved: boolean; // is_approved: boolean;
} // }
// Types // // Types
export const SHIFT_TYPES = { // export const SHIFT_TYPES = {
REGULAR: 'REGULAR', // REGULAR: 'REGULAR',
EVENING: 'EVENING', // EVENING: 'EVENING',
OVERTIME: 'OVERTIME', // OVERTIME: 'OVERTIME',
EMERGENCY: 'EMERGENCY', // EMERGENCY: 'EMERGENCY',
HOLIDAY: 'HOLIDAY', // HOLIDAY: 'HOLIDAY',
VACATION: 'VACATION', // VACATION: 'VACATION',
SICK: 'SICK', // SICK: 'SICK',
} as const; // } as const;
export const EXPENSE_TYPES = { // export const EXPENSE_TYPES = {
MILEAGE: 'MILEAGE', // MILEAGE: 'MILEAGE',
EXPENSE: 'EXPENSES', // EXPENSE: 'EXPENSES',
PER_DIEM: 'PER_DIEM', // PER_DIEM: 'PER_DIEM',
ON_CALL: 'ON_CALL', // ON_CALL: 'ON_CALL',
} as const; // } as const;
//makes the strings indexes for arrays // //makes the strings indexes for arrays
export const DAY_KEYS = ['sun','mon','tue','wed','thu','fri','sat'] as const; // export const DAY_KEYS = ['sun','mon','tue','wed','thu','fri','sat'] as const;
export type DayKey = typeof DAY_KEYS[number]; // export type DayKey = typeof DAY_KEYS[number];
//shifts's hour by type // //shifts's hour by type
export type ShiftsHours = { // export type ShiftsHours = {
regular: number; // regular: number;
evening: number; // evening: number;
overtime: number; // overtime: number;
emergency: number; // emergency: number;
sick: number; // sick: number;
vacation: number; // vacation: number;
holiday: number; // holiday: number;
}; // };
export const make_hours = (): ShiftsHours => ({ // export const make_hours = (): ShiftsHours => ({
regular: 0, // regular: 0,
evening: 0, // evening: 0,
overtime: 0, // overtime: 0,
emergency: 0, // emergency: 0,
sick: 0, // sick: 0,
vacation: 0, // vacation: 0,
holiday: 0, // holiday: 0,
}); // });
export type ExpensesAmount = { // export type ExpensesAmount = {
expense: number; // expense: number;
mileage: number; // mileage: number;
}; // };

View File

@ -1,171 +1,171 @@
import { // import {
DayKey, DAY_KEYS, EXPENSE_TYPES, ExpenseRow, // DayKey, DAY_KEYS, EXPENSE_TYPES, ExpenseRow,
SHIFT_TYPES, ShiftRow, make_hours, ShiftsHours, ExpensesAmount // SHIFT_TYPES, ShiftRow, make_hours, ShiftsHours, ExpensesAmount
} from "./timesheet.types"; // } from "./timesheet.types";
import { // import {
isBetweenUTC, dayKeyFromDate, toTimeString, round2, // isBetweenUTC, dayKeyFromDate, toTimeString, round2,
toUTCDateOnly, endOfDayUTC, addDays // toUTCDateOnly, endOfDayUTC, addDays
} from "./timesheet.helpers"; // } from "./timesheet.helpers";
import { WeekDto, ShiftDto, TimesheetPeriodDto, DayExpensesDto, ExpenseDto } from "../dtos/timesheet-period.dto"; // import { WeekDto, ShiftDto, TimesheetPeriodDto, DayExpensesDto, ExpenseDto } from "../dtos/timesheet-period.dto";
import { getWeekStart, getWeekEnd, formatDateISO } from "src/common/utils/date-utils"; // import { getWeekStart, getWeekEnd, formatDateISO } from "src/common/utils/date-utils";
import { makeAmounts, makeEmptyWeek } from "./timesheet.mappers"; // import { makeAmounts, makeEmptyWeek } from "./timesheet.mappers";
import { toDateString } from "src/modules/pay-periods/utils/pay-year.util"; // import { toDateString } from "src/modules/pay-periods/utils/pay-year.util";
import { MS_PER_HOUR } from "src/modules/shared/constants/date-time.constant"; // import { MS_PER_HOUR } from "src/modules/shared/constants/date-time.constant";
export function computeWeekRange(week_offset = 0){ // export function computeWeekRange(week_offset = 0){
//sets current week Sunday -> Saturday // //sets current week Sunday -> Saturday
const base = new Date(); // const base = new Date();
const offset = new Date(base); // const offset = new Date(base);
offset.setDate(offset.getDate() + (week_offset * 7)); // offset.setDate(offset.getDate() + (week_offset * 7));
const start = getWeekStart(offset, 0); // const start = getWeekStart(offset, 0);
const end = getWeekEnd(start); // const end = getWeekEnd(start);
const start_day = formatDateISO(start); // const start_day = formatDateISO(start);
const end_day = formatDateISO(end); // const end_day = formatDateISO(end);
const label = `${(start_day)}.${(end_day)}`; // const label = `${(start_day)}.${(end_day)}`;
return { start, end, start_day, end_day, label } // return { start, end, start_day, end_day, label }
}; // };
export function buildWeek( // export function buildWeek(
week_start: Date, // week_start: Date,
week_end: Date, // week_end: Date,
shifts: ShiftRow[], // shifts: ShiftRow[],
expenses: ExpenseRow[], // expenses: ExpenseRow[],
): WeekDto { // ): WeekDto {
const week = makeEmptyWeek(week_start); // const week = makeEmptyWeek(week_start);
let all_approved = true; // let all_approved = true;
const day_times: Record<DayKey, Array<{ start: Date; end: Date }>> = DAY_KEYS.reduce((acc, key) => { // const day_times: Record<DayKey, Array<{ start: Date; end: Date }>> = DAY_KEYS.reduce((acc, key) => {
acc[key] = []; return acc; // acc[key] = []; return acc;
}, {} as Record<DayKey, Array<{ start: Date; end: Date}>>); // }, {} as Record<DayKey, Array<{ start: Date; end: Date}>>);
const day_hours: Record<DayKey, ShiftsHours> = DAY_KEYS.reduce((acc, key) => { // const day_hours: Record<DayKey, ShiftsHours> = DAY_KEYS.reduce((acc, key) => {
acc[key] = make_hours(); return acc; // acc[key] = make_hours(); return acc;
}, {} as Record<DayKey, ShiftsHours>); // }, {} as Record<DayKey, ShiftsHours>);
const day_amounts: Record<DayKey, ExpensesAmount> = DAY_KEYS.reduce((acc, key) => { // const day_amounts: Record<DayKey, ExpensesAmount> = DAY_KEYS.reduce((acc, key) => {
acc[key] = makeAmounts(); return acc; // acc[key] = makeAmounts(); return acc;
}, {} as Record<DayKey, ExpensesAmount>); // }, {} as Record<DayKey, ExpensesAmount>);
const day_expense_rows: Record<DayKey, DayExpensesDto> = DAY_KEYS.reduce((acc, key) => { // const day_expense_rows: Record<DayKey, DayExpensesDto> = DAY_KEYS.reduce((acc, key) => {
acc[key] = { // acc[key] = {
expenses: [{ // expenses: [{
type: '', // type: '',
amount: -1, // amount: -1,
mileage: -1, // mileage: -1,
comment: '', // comment: '',
is_approved: false, // is_approved: false,
supervisor_comment: '', // supervisor_comment: '',
}], // }],
total_expense: -1, // total_expense: -1,
total_mileage: -1, // total_mileage: -1,
}; // };
return acc; // return acc;
}, {} as Record<DayKey, DayExpensesDto>); // }, {} as Record<DayKey, DayExpensesDto>);
//regroup hours per type of shifts // //regroup hours per type of shifts
const week_shifts = shifts.filter(shift => isBetweenUTC(shift.date, week_start, week_end)); // const week_shifts = shifts.filter(shift => isBetweenUTC(shift.date, week_start, week_end));
for (const shift of week_shifts) { // for (const shift of week_shifts) {
const key = dayKeyFromDate(shift.date, true); // const key = dayKeyFromDate(shift.date, true);
week.shifts[key].shifts.push({ // week.shifts[key].shifts.push({
date: toDateString(shift.date), // date: toDateString(shift.date),
type: shift.type, // type: shift.type,
start_time: toTimeString(shift.start_time), // start_time: toTimeString(shift.start_time),
end_time: toTimeString(shift.end_time), // end_time: toTimeString(shift.end_time),
comment: shift.comment, // comment: shift.comment,
is_approved: shift.is_approved ?? true, // is_approved: shift.is_approved ?? true,
is_remote: shift.is_remote, // is_remote: shift.is_remote,
} as ShiftDto); // } as ShiftDto);
day_times[key].push({ start: shift.start_time, end: shift.end_time}); // day_times[key].push({ start: shift.start_time, end: shift.end_time});
const duration = Math.max(0, (shift.end_time.getTime() - shift.start_time.getTime())/ MS_PER_HOUR); // const duration = Math.max(0, (shift.end_time.getTime() - shift.start_time.getTime())/ MS_PER_HOUR);
const type = (shift.type || '').toUpperCase(); // const type = (shift.type || '').toUpperCase();
if ( type === SHIFT_TYPES.REGULAR) day_hours[key].regular += duration; // if ( type === SHIFT_TYPES.REGULAR) day_hours[key].regular += duration;
else if( type === SHIFT_TYPES.EVENING) day_hours[key].evening += duration; // else if( type === SHIFT_TYPES.EVENING) day_hours[key].evening += duration;
else if( type === SHIFT_TYPES.EMERGENCY) day_hours[key].emergency += duration; // else if( type === SHIFT_TYPES.EMERGENCY) day_hours[key].emergency += duration;
else if( type === SHIFT_TYPES.OVERTIME) day_hours[key].overtime += duration; // else if( type === SHIFT_TYPES.OVERTIME) day_hours[key].overtime += duration;
else if( type === SHIFT_TYPES.SICK) day_hours[key].sick += duration; // else if( type === SHIFT_TYPES.SICK) day_hours[key].sick += duration;
else if( type === SHIFT_TYPES.VACATION) day_hours[key].vacation += duration; // else if( type === SHIFT_TYPES.VACATION) day_hours[key].vacation += duration;
else if( type === SHIFT_TYPES.HOLIDAY) day_hours[key].holiday += duration; // else if( type === SHIFT_TYPES.HOLIDAY) day_hours[key].holiday += duration;
all_approved = all_approved && (shift.is_approved ?? true ); // all_approved = all_approved && (shift.is_approved ?? true );
} // }
//regroupe amounts to type of expenses // //regroupe amounts to type of expenses
const week_expenses = expenses.filter(expense => isBetweenUTC(expense.date, week_start, week_end)); // const week_expenses = expenses.filter(expense => isBetweenUTC(expense.date, week_start, week_end));
for (const expense of week_expenses) { // for (const expense of week_expenses) {
const key = dayKeyFromDate(expense.date, true); // const key = dayKeyFromDate(expense.date, true);
const type = (expense.type || '').toUpperCase(); // const type = (expense.type || '').toUpperCase();
const row: ExpenseDto = { // const row: ExpenseDto = {
type, // type,
amount: round2(expense.amount ?? 0), // amount: round2(expense.amount ?? 0),
mileage: round2(expense.mileage ?? 0), // mileage: round2(expense.mileage ?? 0),
comment: expense.comment ?? '', // comment: expense.comment ?? '',
is_approved: expense.is_approved ?? true, // is_approved: expense.is_approved ?? true,
supervisor_comment: expense.supervisor_comment ?? '', // supervisor_comment: expense.supervisor_comment ?? '',
}; // };
day_expense_rows[key].expenses.push(row); // day_expense_rows[key].expenses.push(row);
if(type === EXPENSE_TYPES.MILEAGE) { // if(type === EXPENSE_TYPES.MILEAGE) {
day_amounts[key].mileage += row.mileage ?? 0; // day_amounts[key].mileage += row.mileage ?? 0;
} else { // } else {
day_amounts[key].expense += row.amount; // day_amounts[key].expense += row.amount;
} // }
all_approved = all_approved && row.is_approved; // all_approved = all_approved && row.is_approved;
} // }
for (const key of DAY_KEYS) { // for (const key of DAY_KEYS) {
//return exposed dto data // //return exposed dto data
week.shifts[key].regular_hours = round2(day_hours[key].regular); // week.shifts[key].regular_hours = round2(day_hours[key].regular);
week.shifts[key].evening_hours = round2(day_hours[key].evening); // week.shifts[key].evening_hours = round2(day_hours[key].evening);
week.shifts[key].overtime_hours = round2(day_hours[key].overtime); // week.shifts[key].overtime_hours = round2(day_hours[key].overtime);
week.shifts[key].emergency_hours = round2(day_hours[key].emergency); // week.shifts[key].emergency_hours = round2(day_hours[key].emergency);
//calculate gaps between shifts // //calculate gaps between shifts
const times = day_times[key].sort((a,b) => a.start.getTime() - b.start.getTime()); // const times = day_times[key].sort((a,b) => a.start.getTime() - b.start.getTime());
let gaps = 0; // let gaps = 0;
for (let i = 1; i < times.length; i++) { // for (let i = 1; i < times.length; i++) {
const gap = (times[i].start.getTime() - times[i - 1].end.getTime()) / MS_PER_HOUR; // const gap = (times[i].start.getTime() - times[i - 1].end.getTime()) / MS_PER_HOUR;
if(gap > 0) gaps += gap; // if(gap > 0) gaps += gap;
} // }
week.shifts[key].break_durations = round2(gaps); // week.shifts[key].break_durations = round2(gaps);
//daily totals // //daily totals
const totals = day_amounts[key]; // const totals = day_amounts[key];
day_expense_rows[key].total_mileage = round2(totals.mileage); // day_expense_rows[key].total_mileage = round2(totals.mileage);
day_expense_rows[key].total_expense = round2(totals.expense); // day_expense_rows[key].total_expense = round2(totals.expense);
} // }
week.is_approved = all_approved; // week.is_approved = all_approved;
return week; // return week;
} // }
export function buildPeriod( // export function buildPeriod(
period_start: Date, // period_start: Date,
period_end: Date, // period_end: Date,
shifts: ShiftRow[], // shifts: ShiftRow[],
expenses: ExpenseRow[], // expenses: ExpenseRow[],
employeeFullName = '' // employeeFullName = ''
): TimesheetPeriodDto { // ): TimesheetPeriodDto {
const week1_start = toUTCDateOnly(period_start); // const week1_start = toUTCDateOnly(period_start);
const week1_end = endOfDayUTC(addDays(week1_start, 6)); // const week1_end = endOfDayUTC(addDays(week1_start, 6));
const week2_start = toUTCDateOnly(addDays(week1_start, 7)); // const week2_start = toUTCDateOnly(addDays(week1_start, 7));
const week2_end = endOfDayUTC(period_end); // const week2_end = endOfDayUTC(period_end);
const weeks: WeekDto[] = [ // const weeks: WeekDto[] = [
buildWeek(week1_start, week1_end, shifts, expenses), // buildWeek(week1_start, week1_end, shifts, expenses),
buildWeek(week2_start, week2_end, shifts, expenses), // buildWeek(week2_start, week2_end, shifts, expenses),
]; // ];
return { // return {
weeks, // weeks,
employee_full_name: employeeFullName, // employee_full_name: employeeFullName,
}; // };
} // }

View File

@ -1,137 +1,137 @@
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; // import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
import { EmployeeTimesheetResolver } from "src/modules/shared/utils/resolve-timesheet.utils"; // import { EmployeeTimesheetResolver } from "src/modules/shared/utils/resolve-timesheet.utils";
import { getWeekEnd, getWeekStart } from "src/common/utils/date-utils"; // import { getWeekEnd, getWeekStart } from "src/common/utils/date-utils";
import { parseISODate, parseHHmm } from "./utils-helpers-others/timesheet.helpers"; // import { parseISODate, parseHHmm } from "./utils-helpers-others/timesheet.helpers";
import { TimesheetsQueryService } from "./timesheets-query.service"; // import { TimesheetsQueryService } from "./timesheets-query.service";
import { BaseApprovalService } from "src/common/shared/base-approval.service"; // import { BaseApprovalService } from "src/common/shared/base-approval.service";
import { Prisma, Timesheets } from "@prisma/client"; // import { Prisma, Timesheets } from "@prisma/client";
import { CreateTimesheetDto } from "./create-timesheet.dto"; // import { CreateTimesheetDto } from "./create-timesheet.dto";
import { EmailToIdResolver } from "src/modules/shared/utils/resolve-email-id.utils"; // import { EmailToIdResolver } from "src/modules/shared/utils/resolve-email-id.utils";
import { BankCodesResolver } from "src/modules/shared/utils/resolve-bank-type-id.utils"; // import { BankCodesResolver } from "src/modules/shared/utils/resolve-bank-type-id.utils";
import { PrismaService } from "src/prisma/prisma.service"; // import { PrismaService } from "src/prisma/prisma.service";
import { TimesheetMap } from "./utils-helpers-others/timesheet.types"; // import { TimesheetMap } from "./utils-helpers-others/timesheet.types";
import { Shift, Expense } from "../dtos/timesheet.dto"; // import { Shift, Expense } from "../dtos/timesheet.dto";
@Injectable() // @Injectable()
export class TimesheetsCommandService extends BaseApprovalService<Timesheets>{ // export class TimesheetsCommandService extends BaseApprovalService<Timesheets>{
constructor( // constructor(
prisma: PrismaService, // prisma: PrismaService,
private readonly query: TimesheetsQueryService, // private readonly query: TimesheetsQueryService,
private readonly emailResolver: EmailToIdResolver, // private readonly emailResolver: EmailToIdResolver,
private readonly timesheetResolver: EmployeeTimesheetResolver, // private readonly timesheetResolver: EmployeeTimesheetResolver,
private readonly bankTypeResolver: BankCodesResolver, // private readonly bankTypeResolver: BankCodesResolver,
) {super(prisma);} // ) {super(prisma);}
//_____________________________________________________________________________________________ // //_____________________________________________________________________________________________
// APPROVAL AND DELEGATE METHODS // // APPROVAL AND DELEGATE METHODS
//_____________________________________________________________________________________________ // //_____________________________________________________________________________________________
protected get delegate() { // protected get delegate() {
return this.prisma.timesheets; // return this.prisma.timesheets;
} // }
protected delegateFor(transaction: Prisma.TransactionClient) { // protected delegateFor(transaction: Prisma.TransactionClient) {
return transaction.timesheets; // return transaction.timesheets;
} // }
async updateApproval(id: number, isApproved: boolean): Promise<Timesheets> { // async updateApproval(id: number, isApproved: boolean): Promise<Timesheets> {
return this.prisma.$transaction((transaction) => // return this.prisma.$transaction((transaction) =>
this.updateApprovalWithTransaction(transaction, id, isApproved), // this.updateApprovalWithTransaction(transaction, id, isApproved),
); // );
} // }
async cascadeApprovalWithtx(transaction: Prisma.TransactionClient, timesheetId: number, isApproved: boolean): Promise<Timesheets> { // async cascadeApprovalWithtx(transaction: Prisma.TransactionClient, timesheetId: number, isApproved: boolean): Promise<Timesheets> {
const timesheet = await this.updateApprovalWithTransaction(transaction, timesheetId, isApproved); // const timesheet = await this.updateApprovalWithTransaction(transaction, timesheetId, isApproved);
await transaction.shifts.updateMany({ // await transaction.shifts.updateMany({
where: { timesheet_id: timesheetId }, // where: { timesheet_id: timesheetId },
data: { is_approved: isApproved }, // data: { is_approved: isApproved },
}); // });
await transaction.expenses.updateManyAndReturn({ // await transaction.expenses.updateManyAndReturn({
where: { timesheet_id: timesheetId }, // where: { timesheet_id: timesheetId },
data: { is_approved: isApproved }, // data: { is_approved: isApproved },
}); // });
return timesheet; // return timesheet;
} // }
/**_____________________________________________________________________________________________ // /**_____________________________________________________________________________________________
create/update/delete shifts and expenses from 1 or many timesheet(s) // create/update/delete shifts and expenses from 1 or many timesheet(s)
-this function receives an email and an array of timesheets // -this function receives an email and an array of timesheets
-this function will find the timesheets with all shifts and expenses // -this function will find the timesheets with all shifts and expenses
-this function will calculate total hours, total expenses, filtered by types, // -this function will calculate total hours, total expenses, filtered by types,
cumulate in daily and weekly. // cumulate in daily and weekly.
-the timesheet_id will be determined using the employee email // -the timesheet_id will be determined using the employee email
-with the timesheet_id, all shifts and expenses will be fetched // -with the timesheet_id, all shifts and expenses will be fetched
-with shift_id and expense_id, this function will compare both // -with shift_id and expense_id, this function will compare both
datas from the DB and from the body of the function and then: // datas from the DB and from the body of the function and then:
-it will create a shift if no shift is found in the DB // -it will create a shift if no shift is found in the DB
-it will update a shift if a shift is found in the DB // -it will update a shift if a shift is found in the DB
-it will delete a shift if a shift is found and no data is received from the frontend // -it will delete a shift if a shift is found and no data is received from the frontend
This function will be used for the Timesheet Page for an employee to enter, modify or delete and entry // This function will be used for the Timesheet Page for an employee to enter, modify or delete and entry
This function will also be used in the modal of the timesheet validation page to // This function will also be used in the modal of the timesheet validation page to
allow a supervisor to enter, modify or delete and entry of a selected employee // allow a supervisor to enter, modify or delete and entry of a selected employee
_____________________________________________________________________________________________*/ // _____________________________________________________________________________________________*/
async findTimesheetsByEmailAndPayPeriod(email: string, year: number, period_no: number, timesheets: Timesheets[]): Promise<Timesheets[]> { // async findTimesheetsByEmailAndPayPeriod(email: string, year: number, period_no: number, timesheets: Timesheets[]): Promise<Timesheets[]> {
const employee_id = await this.emailResolver.findIdByEmail(email); // const employee_id = await this.emailResolver.findIdByEmail(email);
return timesheets; // return timesheets;
} // }
async upsertOrDeleteShiftsByEmailAndDate(email:string, shift_ids: Shift[]) {} // async upsertOrDeleteShiftsByEmailAndDate(email:string, shift_ids: Shift[]) {}
async upsertOrDeleteExpensesByEmailAndDate(email:string, expenses_id: Expense[]) {} // async upsertOrDeleteExpensesByEmailAndDate(email:string, expenses_id: Expense[]) {}
//_____________________________________________________________________________________________ // //_____________________________________________________________________________________________
// // //
//_____________________________________________________________________________________________ // //_____________________________________________________________________________________________
async createWeekShiftsAndReturnOverview( // async createWeekShiftsAndReturnOverview(
email:string, // email:string,
shifts: CreateTimesheetDto[], // shifts: CreateTimesheetDto[],
week_offset = 0, // week_offset = 0,
): Promise<TimesheetMap> { // ): Promise<TimesheetMap> {
//fetchs employee matchint user's email // //fetchs employee matchint user's email
const employee_id = await this.emailResolver.findIdByEmail(email); // const employee_id = await this.emailResolver.findIdByEmail(email);
if(!employee_id) throw new NotFoundException(`employee for ${ email } not found`); // if(!employee_id) throw new NotFoundException(`employee for ${ email } not found`);
//insure that the week starts on sunday and finishes on saturday // //insure that the week starts on sunday and finishes on saturday
const base = new Date(); // const base = new Date();
base.setDate(base.getDate() + week_offset * 7); // base.setDate(base.getDate() + week_offset * 7);
const start_week = getWeekStart(base, 0); // const start_week = getWeekStart(base, 0);
const end_week = getWeekEnd(start_week); // const end_week = getWeekEnd(start_week);
const timesheet = await this.timesheetResolver.findTimesheetIdByEmail(email, base) // const timesheet = await this.timesheetResolver.findTimesheetIdByEmail(email, base)
if(!timesheet) throw new NotFoundException(`no timesheet found for employe ${employee_id}`); // if(!timesheet) throw new NotFoundException(`no timesheet found for employe ${employee_id}`);
//validations and insertions // //validations and insertions
for(const shift of shifts) { // for(const shift of shifts) {
const date = parseISODate(shift.date); // const date = parseISODate(shift.date);
if (date < start_week || date > end_week) throw new BadRequestException(`date ${shift.date} not in current week`); // if (date < start_week || date > end_week) throw new BadRequestException(`date ${shift.date} not in current week`);
const bank_code = await this.bankTypeResolver.findByType(shift.type) // const bank_code = await this.bankTypeResolver.findByType(shift.type)
if(!bank_code) throw new BadRequestException(`Invalid bank_code type: ${shift.type}`); // if(!bank_code) throw new BadRequestException(`Invalid bank_code type: ${shift.type}`);
await this.prisma.shifts.create({ // await this.prisma.shifts.create({
data: { // data: {
timesheet_id: timesheet.id, // timesheet_id: timesheet.id,
bank_code_id: bank_code.id, // bank_code_id: bank_code.id,
date: date, // date: date,
start_time: parseHHmm(shift.start_time), // start_time: parseHHmm(shift.start_time),
end_time: parseHHmm(shift.end_time), // end_time: parseHHmm(shift.end_time),
comment: shift.comment ?? null, // comment: shift.comment ?? null,
is_approved: false, // is_approved: false,
is_remote: false, // is_remote: false,
}, // },
}); // });
} // }
return this.query.getTimesheetByEmail(email, week_offset); // return this.query.getTimesheetByEmail(email, week_offset);
} // }
} // }

View File

@ -1,54 +1,54 @@
import { makeEmptyTimesheet, mapExpenseRow, mapShiftRow } from './utils-helpers-others/timesheet.mappers'; // import { makeEmptyTimesheet, mapExpenseRow, mapShiftRow } from './utils-helpers-others/timesheet.mappers';
import { buildPeriod, computeWeekRange } from './utils-helpers-others/timesheet.utils'; // import { buildPeriod, computeWeekRange } from './utils-helpers-others/timesheet.utils';
import { TimesheetSelectorsService } from './utils-helpers-others/timesheet.selectors'; // import { TimesheetSelectorsService } from './utils-helpers-others/timesheet.selectors';
import { TimesheetPeriodDto } from './timesheet-period.dto'; // import { TimesheetPeriodDto } from './timesheet-period.dto';
import { toRangeFromPeriod } from './utils-helpers-others/timesheet.helpers'; // import { toRangeFromPeriod } from './utils-helpers-others/timesheet.helpers';
import { EmailToIdResolver } from 'src/modules/shared/utils/resolve-email-id.utils'; // import { EmailToIdResolver } from 'src/modules/shared/utils/resolve-email-id.utils';
import { FullNameResolver } from 'src/modules/shared/utils/resolve-full-name.utils'; // import { FullNameResolver } from 'src/modules/shared/utils/resolve-full-name.utils';
import { PrismaService } from 'src/prisma/prisma.service'; // import { PrismaService } from 'src/prisma/prisma.service';
import { TimesheetMap } from './utils-helpers-others/timesheet.types'; // import { TimesheetMap } from './utils-helpers-others/timesheet.types';
import { Injectable } from '@nestjs/common'; // import { Injectable } from '@nestjs/common';
@Injectable() // @Injectable()
export class TimesheetsQueryService { // export class TimesheetsQueryService {
constructor( // constructor(
private readonly prisma: PrismaService, // private readonly prisma: PrismaService,
private readonly emailResolver: EmailToIdResolver, // private readonly emailResolver: EmailToIdResolver,
private readonly fullNameResolver: FullNameResolver, // private readonly fullNameResolver: FullNameResolver,
private readonly selectors: TimesheetSelectorsService, // private readonly selectors: TimesheetSelectorsService,
) {} // ) {}
async findAll(year: number, period_no: number, email: string): Promise<TimesheetPeriodDto> { // async findAll(year: number, period_no: number, email: string): Promise<TimesheetPeriodDto> {
const employee_id = await this.emailResolver.findIdByEmail(email); //finds the employee using email // const employee_id = await this.emailResolver.findIdByEmail(email); //finds the employee using email
const full_name = await this.fullNameResolver.resolveFullName(employee_id); //finds the employee full name using employee_id // const full_name = await this.fullNameResolver.resolveFullName(employee_id); //finds the employee full name using employee_id
const period = await this.selectors.getPayPeriod(year, period_no);//finds the pay period using year and period_no // const period = await this.selectors.getPayPeriod(year, period_no);//finds the pay period using year and period_no
const{ from, to } = toRangeFromPeriod(period); //finds start and end dates // const{ from, to } = toRangeFromPeriod(period); //finds start and end dates
//finds all shifts from selected period // //finds all shifts from selected period
const [raw_shifts, raw_expenses] = await Promise.all([ // const [raw_shifts, raw_expenses] = await Promise.all([
this.selectors.getShifts(employee_id, from, to), // this.selectors.getShifts(employee_id, from, to),
this.selectors.getExpenses(employee_id, from, to), // this.selectors.getExpenses(employee_id, from, to),
]); // ]);
// data mapping // // data mapping
const shifts = raw_shifts.map(mapShiftRow); // const shifts = raw_shifts.map(mapShiftRow);
const expenses = raw_expenses.map(mapExpenseRow); // const expenses = raw_expenses.map(mapExpenseRow);
return buildPeriod(period.period_start, period.period_end, shifts , expenses, full_name); // return buildPeriod(period.period_start, period.period_end, shifts , expenses, full_name);
} // }
async getTimesheetByEmail(email: string, week_offset = 0): Promise<TimesheetMap> { // async getTimesheetByEmail(email: string, week_offset = 0): Promise<TimesheetMap> {
const employee_id = await this.emailResolver.findIdByEmail(email); //finds the employee using email // const employee_id = await this.emailResolver.findIdByEmail(email); //finds the employee using email
const { start, start_day, end_day, label } = computeWeekRange(week_offset); // const { start, start_day, end_day, label } = computeWeekRange(week_offset);
const timesheet = await this.selectors.getTimesheetWithShiftsAndExpenses(employee_id, start); //fetch timesheet shifts and expenses // const timesheet = await this.selectors.getTimesheetWithShiftsAndExpenses(employee_id, start); //fetch timesheet shifts and expenses
if(!timesheet) return makeEmptyTimesheet({ start_day, end_day, label}); // if(!timesheet) return makeEmptyTimesheet({ start_day, end_day, label});
//maps all shifts of selected timesheet // //maps all shifts of selected timesheet
const shifts = timesheet.shift.map(mapShiftRow); // const shifts = timesheet.shift.map(mapShiftRow);
const expenses = timesheet.expense.map(mapExpenseRow); // const expenses = timesheet.expense.map(mapExpenseRow);
return { start_day, end_day, label, shifts, expenses, is_approved: timesheet.is_approved}; // return { start_day, end_day, label, shifts, expenses, is_approved: timesheet.is_approved};
} // }
} // }

View File

@ -1,51 +1,51 @@
import { BadRequestException, Body, Controller, Get, Param, ParseIntPipe, Post, Query } from '@nestjs/common'; // import { BadRequestException, Body, Controller, Get, Param, ParseIntPipe, Post, Query } from '@nestjs/common';
import { TimesheetsQueryService } from './timesheets-query.service'; // import { TimesheetsQueryService } from './timesheets-query.service';
import { CreateWeekShiftsDto } from './create-timesheet.dto'; // import { CreateWeekShiftsDto } from './create-timesheet.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, ApiTags } from '@nestjs/swagger'; // import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { TimesheetsCommandService } from './timesheets-command.service'; // import { TimesheetsCommandService } from './timesheets-command.service';
import { TimesheetPeriodDto } from './timesheet-period.dto'; // import { TimesheetPeriodDto } from './timesheet-period.dto';
import { TimesheetMap } from './timesheet.types'; // import { TimesheetMap } from './timesheet.types';
@ApiTags('Timesheets') // @ApiTags('Timesheets')
@ApiBearerAuth('access-token') // @ApiBearerAuth('access-token')
// @UseGuards() // // @UseGuards()
@Controller('timesheets') // @Controller('timesheets')
export class TimesheetsController { // export class TimesheetsController {
constructor( // constructor(
private readonly timesheetsQuery: TimesheetsQueryService, // private readonly timesheetsQuery: TimesheetsQueryService,
private readonly timesheetsCommand: TimesheetsCommandService, // private readonly timesheetsCommand: TimesheetsCommandService,
) {} // ) {}
@Get() // @Get()
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR) // //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
async getPeriodByQuery( // async getPeriodByQuery(
@Query('year', ParseIntPipe ) year: number, // @Query('year', ParseIntPipe ) year: number,
@Query('period_no', ParseIntPipe ) period_no: number, // @Query('period_no', ParseIntPipe ) period_no: number,
@Query('email') email?: string // @Query('email') email?: string
): Promise<TimesheetPeriodDto> { // ): Promise<TimesheetPeriodDto> {
if(!email || !(email = email.trim())) throw new BadRequestException('Query param "email" is mandatory for this route.'); // if(!email || !(email = email.trim())) throw new BadRequestException('Query param "email" is mandatory for this route.');
return this.timesheetsQuery.findAll(year, period_no, email); // return this.timesheetsQuery.findAll(year, period_no, email);
} // }
@Get('/:email') // @Get('/:email')
async getByEmail( // async getByEmail(
@Param('email') email: string, // @Param('email') email: string,
@Query('offset') offset?: string, // @Query('offset') offset?: string,
): Promise<TimesheetMap> { // ): Promise<TimesheetMap> {
const week_offset = Number.isFinite(Number(offset)) ? Number(offset) : 0; // const week_offset = Number.isFinite(Number(offset)) ? Number(offset) : 0;
return this.timesheetsQuery.getTimesheetByEmail(email, week_offset); // return this.timesheetsQuery.getTimesheetByEmail(email, week_offset);
} // }
@Post('shifts/:email') // @Post('shifts/:email')
async createTimesheetShifts( // async createTimesheetShifts(
@Param('email') email: string, // @Param('email') email: string,
@Body() dto: CreateWeekShiftsDto, // @Body() dto: CreateWeekShiftsDto,
@Query('offset') offset?: string, // @Query('offset') offset?: string,
): Promise<TimesheetMap> { // ): Promise<TimesheetMap> {
const week_offset = Number.isFinite(Number(offset)) ? Number(offset) : 0; // const week_offset = Number.isFinite(Number(offset)) ? Number(offset) : 0;
return this.timesheetsCommand.createWeekShiftsAndReturnOverview(email, dto.shifts, week_offset); // return this.timesheetsCommand.createWeekShiftsAndReturnOverview(email, dto.shifts, week_offset);
} // }
} // }