Merge branch 'dev/matthieu/error-handling' of git.targo.ca:Targo/targo_backend
This commit is contained in:
commit
d88f8727ad
12
src/common/errors/result-error.factory.ts
Normal file
12
src/common/errors/result-error.factory.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
export type Result<T, E> =
|
||||||
|
| { success: true; data: T }
|
||||||
|
| { success: false; error: E };
|
||||||
|
|
||||||
|
// const success = <T>(data: T): Result<T, never> => {
|
||||||
|
// return { success: true, data };
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const failure = <E>(error: E): Result<never, E> => {
|
||||||
|
// return { success: false, error };
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import { Injectable, Logger, NotFoundException } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
import { computeHours, getWeekStart } from "src/common/utils/date-utils";
|
import { computeHours, getWeekStart } from "src/common/utils/date-utils";
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
|
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
|
||||||
import { MS_PER_WEEK } from "src/time-and-attendance/utils/constants.utils";
|
import { MS_PER_WEEK } from "src/time-and-attendance/utils/constants.utils";
|
||||||
|
import { Result } from "src/common/errors/result-error.factory";
|
||||||
/*
|
/*
|
||||||
le calcul est 1/20 des 4 dernières semaines, précédent la semaine incluant le férier.
|
le calcul est 1/20 des 4 dernières semaines, précédent la semaine incluant le férier.
|
||||||
Un maximum de 08h00 est allouable pour le férier
|
Un maximum de 08h00 est allouable pour le férier
|
||||||
|
|
@ -12,19 +13,19 @@ import { MS_PER_WEEK } from "src/time-and-attendance/utils/constants.utils";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class HolidayService {
|
export class HolidayService {
|
||||||
private readonly logger = new Logger(HolidayService.name);
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
private readonly emailResolver: EmailToIdResolver,
|
private readonly emailResolver: EmailToIdResolver,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
private async computeHoursPrevious4WeeksByEmail(email: string, holiday_date: Date): Promise<number> {
|
private async computeHoursPrevious4WeeksByEmail(email: string, holiday_date: Date): Promise<Result<number, string>> {
|
||||||
const employee_id = await this.emailResolver.findIdByEmail(email);
|
const employee_id = await this.emailResolver.findIdByEmail(email);
|
||||||
return this.computeHoursPrevious4Weeks(employee_id, holiday_date);
|
if (!employee_id.success) return { success: false, error: employee_id.error };
|
||||||
|
return this.computeHoursPrevious4Weeks(employee_id.data, holiday_date);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async computeHoursPrevious4Weeks(employee_id: number, holiday_date: Date): Promise<number> {
|
private async computeHoursPrevious4Weeks(employee_id: number, holiday_date: Date): Promise<Result<number, string>> {
|
||||||
|
try {
|
||||||
const holiday_week_start = getWeekStart(holiday_date);
|
const holiday_week_start = getWeekStart(holiday_date);
|
||||||
const window_start = new Date(holiday_week_start.getTime() - 4 * MS_PER_WEEK);
|
const window_start = new Date(holiday_week_start.getTime() - 4 * MS_PER_WEEK);
|
||||||
const window_end = new Date(holiday_week_start.getTime() - 1);
|
const window_end = new Date(holiday_week_start.getTime() - 1);
|
||||||
|
|
@ -57,13 +58,17 @@ export class HolidayService {
|
||||||
}
|
}
|
||||||
|
|
||||||
const average_daily_hours = capped_total / 20;
|
const average_daily_hours = capped_total / 20;
|
||||||
return average_daily_hours;
|
return { success: true, data: average_daily_hours };
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: `an error occureded during holiday calculation` }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async calculateHolidayPay( email: string, holiday_date: Date, modifier: number): Promise<number> {
|
async calculateHolidayPay(email: string, holiday_date: Date, modifier: number): Promise<Result<number, string>> {
|
||||||
const average_daily_hours = await this.computeHoursPrevious4WeeksByEmail(email, holiday_date);
|
const average_daily_hours = await this.computeHoursPrevious4WeeksByEmail(email, holiday_date);
|
||||||
const daily_rate = Math.min(average_daily_hours, 8);
|
if (!average_daily_hours.success) return { success: false, error: average_daily_hours.error };
|
||||||
this.logger.debug(`Holiday pay calculation: cappedHoursPerDay= ${average_daily_hours.toFixed(2)}, appliedDailyRate= ${daily_rate.toFixed(2)}`);
|
|
||||||
return daily_rate * modifier;
|
const daily_rate = (Math.min(average_daily_hours.data, 8)) * modifier;
|
||||||
|
return { success: true, data: daily_rate };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,10 +1,28 @@
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { Prisma, PrismaClient } from '@prisma/client';
|
||||||
import { getWeekStart, getWeekEnd, computeHours } from 'src/common/utils/date-utils';
|
import { getWeekStart, getWeekEnd, computeHours } from 'src/common/utils/date-utils';
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
import { DAILY_LIMIT_HOURS, WEEKLY_LIMIT_HOURS } from 'src/time-and-attendance/utils/constants.utils';
|
import { DAILY_LIMIT_HOURS, WEEKLY_LIMIT_HOURS } from 'src/time-and-attendance/utils/constants.utils';
|
||||||
import { Tx, WeekOvertimeSummary } from 'src/time-and-attendance/utils/type.utils';
|
|
||||||
|
|
||||||
|
|
||||||
|
type Tx = Prisma.TransactionClient | PrismaClient;
|
||||||
|
|
||||||
|
type WeekOvertimeSummary = {
|
||||||
|
week_start:string;
|
||||||
|
week_end: string;
|
||||||
|
week_total_hours: number;
|
||||||
|
weekly_overtime: number;
|
||||||
|
daily_overtime_kept: number;
|
||||||
|
total_overtime: number;
|
||||||
|
breakdown: Array<{
|
||||||
|
date:string;
|
||||||
|
day_hours: number;
|
||||||
|
day_overtime: number;
|
||||||
|
daily_kept: number;
|
||||||
|
running_total_before: number;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class OvertimeService {
|
export class OvertimeService {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import { Controller, Post, Param, Body, Patch, Delete, Req, UnauthorizedException } from "@nestjs/common";
|
import { Controller, Post, Param, Body, Patch, Delete, Req, UnauthorizedException } from "@nestjs/common";
|
||||||
import { CreateExpenseResult } from "src/time-and-attendance/utils/type.utils";
|
|
||||||
import { ExpenseUpsertService } from "src/time-and-attendance/expenses/services/expense-upsert.service";
|
import { ExpenseUpsertService } from "src/time-and-attendance/expenses/services/expense-upsert.service";
|
||||||
import { ExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-create.dto";
|
import { ExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-create.dto";
|
||||||
import { RolesAllowed } from "src/common/decorators/roles.decorators";
|
import { RolesAllowed } from "src/common/decorators/roles.decorators";
|
||||||
import { GLOBAL_CONTROLLER_ROLES } from "src/common/shared/role-groupes";
|
import { GLOBAL_CONTROLLER_ROLES } from "src/common/shared/role-groupes";
|
||||||
|
import { Result } from "src/common/errors/result-error.factory";
|
||||||
|
import { GetExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-get.dto";
|
||||||
|
|
||||||
@Controller('expense')
|
@Controller('expense')
|
||||||
@RolesAllowed(...GLOBAL_CONTROLLER_ROLES)
|
@RolesAllowed(...GLOBAL_CONTROLLER_ROLES)
|
||||||
|
|
@ -11,19 +12,19 @@ export class ExpenseController {
|
||||||
constructor(private readonly upsert_service: ExpenseUpsertService) { }
|
constructor(private readonly upsert_service: ExpenseUpsertService) { }
|
||||||
|
|
||||||
@Post('create')
|
@Post('create')
|
||||||
create( @Req() req, @Body() dto: ExpenseDto): Promise<CreateExpenseResult>{
|
create(@Req() req, @Body() dto: ExpenseDto): Promise<Result<GetExpenseDto, string>> {
|
||||||
const email = req.user?.email;
|
const email = req.user?.email;
|
||||||
if (!email) throw new UnauthorizedException('Unauthorized User');
|
if (!email) throw new UnauthorizedException('Unauthorized User');
|
||||||
return this.upsert_service.createExpense(dto, email);
|
return this.upsert_service.createExpense(dto, email);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch('update')
|
@Patch('update')
|
||||||
update(@Body() dto: ExpenseDto): Promise<ExpenseDto>{
|
update(@Body() dto: ExpenseDto): Promise<Result<ExpenseDto, string>> {
|
||||||
return this.upsert_service.updateExpense(dto);
|
return this.upsert_service.updateExpense(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete('delete/:expense_id')
|
@Delete('delete/:expense_id')
|
||||||
remove(@Param('expense_id') expense_id: number) {
|
remove(@Param('expense_id') expense_id: number): Promise<Result<number, string>> {
|
||||||
return this.upsert_service.deleteExpense(expense_id);
|
return this.upsert_service.deleteExpense(expense_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
import { CreateExpenseResult, DeleteExpenseResult, NormalizedExpense } from "src/time-and-attendance/utils/type.utils";
|
|
||||||
import { toDateFromString, toStringFromDate, weekStartSunday } from "src/time-and-attendance/utils/date-time.utils";
|
import { toDateFromString, toStringFromDate, weekStartSunday } from "src/time-and-attendance/utils/date-time.utils";
|
||||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
|
||||||
|
// import { NormalizedExpense } from "src/time-and-attendance/utils/type.utils";
|
||||||
import { expense_select } from "src/time-and-attendance/utils/selects.utils";
|
import { expense_select } from "src/time-and-attendance/utils/selects.utils";
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
import { ExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-create.dto";
|
|
||||||
import { GetExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-get.dto";
|
import { GetExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-get.dto";
|
||||||
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
|
|
||||||
import { ExpenseEntity } from "src/time-and-attendance/expenses/dtos/expense-entity.dto";
|
import { ExpenseEntity } from "src/time-and-attendance/expenses/dtos/expense-entity.dto";
|
||||||
|
import { ExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-create.dto";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { Result } from "src/common/errors/result-error.factory";
|
||||||
|
import { NormalizedExpense } from "src/time-and-attendance/utils/type.utils";
|
||||||
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|
@ -19,10 +21,11 @@ export class ExpenseUpsertService {
|
||||||
//_________________________________________________________________
|
//_________________________________________________________________
|
||||||
// CREATE
|
// CREATE
|
||||||
//_________________________________________________________________
|
//_________________________________________________________________
|
||||||
async createExpense(dto: ExpenseDto, email: string): Promise<CreateExpenseResult> {
|
async createExpense(dto: ExpenseDto, email: string): Promise<Result<GetExpenseDto, string>> {
|
||||||
try {
|
try {
|
||||||
//fetch employee_id using req.user.email
|
//fetch employee_id using req.user.email
|
||||||
const employee_id = await this.emailResolver.findIdByEmail(email);
|
const employee_id = await this.emailResolver.findIdByEmail(email);
|
||||||
|
if (!employee_id.success) return { success: false, error: employee_id.error };
|
||||||
|
|
||||||
//normalize strings and dates and Parse numbers
|
//normalize strings and dates and Parse numbers
|
||||||
const normed_expense = this.normalizeAndParseExpenseDto(dto);
|
const normed_expense = this.normalizeAndParseExpenseDto(dto);
|
||||||
|
|
@ -30,10 +33,10 @@ export class ExpenseUpsertService {
|
||||||
//finds the timesheet using expense.date by finding the sunday
|
//finds the timesheet using expense.date by finding the sunday
|
||||||
const start_date = weekStartSunday(normed_expense.date);
|
const start_date = weekStartSunday(normed_expense.date);
|
||||||
const timesheet = await this.prisma.timesheets.findFirst({
|
const timesheet = await this.prisma.timesheets.findFirst({
|
||||||
where: { start_date, employee_id },
|
where: { start_date, employee_id: employee_id.data },
|
||||||
select: { id: true, employee_id: true },
|
select: { id: true, employee_id: true },
|
||||||
});
|
});
|
||||||
if (!timesheet) throw new NotFoundException(`Timesheet with id ${dto.timesheet_id} not found`);
|
if (!timesheet) return { success: false, error: `Timesheet with id : ${dto.timesheet_id} not found` };
|
||||||
|
|
||||||
//create a new expense
|
//create a new expense
|
||||||
const expense = await this.prisma.expenses.create({
|
const expense = await this.prisma.expenses.create({
|
||||||
|
|
@ -46,6 +49,7 @@ export class ExpenseUpsertService {
|
||||||
//return the newly created expense with id
|
//return the newly created expense with id
|
||||||
select: expense_select,
|
select: expense_select,
|
||||||
});
|
});
|
||||||
|
if (!expense) return { success: false, error: `An error occured during creation. Expense is invalid` };
|
||||||
|
|
||||||
//build an object to return to the frontend to display
|
//build an object to return to the frontend to display
|
||||||
const created: GetExpenseDto = {
|
const created: GetExpenseDto = {
|
||||||
|
|
@ -56,17 +60,17 @@ export class ExpenseUpsertService {
|
||||||
attachment: expense.attachment ?? undefined,
|
attachment: expense.attachment ?? undefined,
|
||||||
supervisor_comment: expense.supervisor_comment ?? undefined,
|
supervisor_comment: expense.supervisor_comment ?? undefined,
|
||||||
};
|
};
|
||||||
return { ok: true, data: created }
|
return { success: true, data: created };
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { ok: false, error: error }
|
return { success: false, error: `An error occured during creation. Expense not created : ` + error };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//_________________________________________________________________
|
//_________________________________________________________________
|
||||||
// UPDATE
|
// UPDATE
|
||||||
//_________________________________________________________________
|
//_________________________________________________________________
|
||||||
async updateExpense(dto: ExpenseDto): Promise<ExpenseDto> {
|
async updateExpense(dto: ExpenseDto): Promise<Result<ExpenseDto, string>> {
|
||||||
try {
|
try {
|
||||||
//normalize string , date format and parse numbers
|
//normalize string , date format and parse numbers
|
||||||
const normed_expense = this.normalizeAndParseExpenseDto(dto);
|
const normed_expense = this.normalizeAndParseExpenseDto(dto);
|
||||||
|
|
@ -79,6 +83,7 @@ export class ExpenseUpsertService {
|
||||||
bank_code_id: dto.bank_code_id,
|
bank_code_id: dto.bank_code_id,
|
||||||
is_approved: dto.is_approved,
|
is_approved: dto.is_approved,
|
||||||
};
|
};
|
||||||
|
if (!data) return { success: false, error: `An error occured during normalization. Expense with id: ${dto.id} is invalid` }
|
||||||
|
|
||||||
//push updates and get updated datas
|
//push updates and get updated datas
|
||||||
const expense = await this.prisma.expenses.update({
|
const expense = await this.prisma.expenses.update({
|
||||||
|
|
@ -86,6 +91,7 @@ export class ExpenseUpsertService {
|
||||||
data,
|
data,
|
||||||
select: expense_select,
|
select: expense_select,
|
||||||
});
|
});
|
||||||
|
if (!expense) return { success: false, error: `An error occured during update. Expense with id: ${data.id} was not updated` }
|
||||||
|
|
||||||
//build an object to return to the frontend
|
//build an object to return to the frontend
|
||||||
const updated: GetExpenseDto = {
|
const updated: GetExpenseDto = {
|
||||||
|
|
@ -96,29 +102,29 @@ export class ExpenseUpsertService {
|
||||||
attachment: expense.attachment ?? undefined,
|
attachment: expense.attachment ?? undefined,
|
||||||
supervisor_comment: expense.supervisor_comment ?? undefined,
|
supervisor_comment: expense.supervisor_comment ?? undefined,
|
||||||
};
|
};
|
||||||
return updated;
|
return { success: true, data: updated };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return error;
|
return { success: false, error: (`Expense with id: ${dto.id} generated an error:` + error) };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//_________________________________________________________________
|
//_________________________________________________________________
|
||||||
// DELETE
|
// DELETE
|
||||||
//_________________________________________________________________
|
//_________________________________________________________________
|
||||||
async deleteExpense(expense_id: number): Promise<DeleteExpenseResult> {
|
async deleteExpense(expense_id: number): Promise<Result<number, string>> {
|
||||||
try {
|
try {
|
||||||
await this.prisma.$transaction(async (tx) => {
|
await this.prisma.$transaction(async (tx) => {
|
||||||
const expense = await tx.expenses.findUnique({
|
const expense = await tx.expenses.findUnique({
|
||||||
where: { id: expense_id },
|
where: { id: expense_id },
|
||||||
select: { id: true },
|
select: { id: true },
|
||||||
});
|
});
|
||||||
if (!expense) throw new NotFoundException(`Expense with id: ${expense_id} not found`);
|
if (!expense) return { success: false, error: `An error occured during removal. Expense with id :${expense_id} was not found ` };
|
||||||
|
|
||||||
await tx.expenses.delete({ where: { id: expense_id } });
|
await tx.expenses.delete({ where: { id: expense_id } });
|
||||||
return { success: true };
|
return { success: true };
|
||||||
});
|
});
|
||||||
return { ok: true, id: expense_id };
|
return { success: true, data: expense_id };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { ok: false, id: expense_id, error };
|
return { success: false, error: `An error occured during removal. Expense with id :${expense_id} generated an error: ` + error };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,20 @@
|
||||||
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 { UpsertLeaveRequestDto } from "../dtos/upsert-leave-request.dto";
|
// import { UpsertLeaveRequestDto } from "../dtos/upsert-leave-request.dto";
|
||||||
import { LeaveRequestsService } from "../services/leave-request.service";
|
// import { LeaveRequestsService } from "../services/leave-request.service";
|
||||||
|
|
||||||
@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 };
|
||||||
}
|
// }
|
||||||
|
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
import { LeaveRequestController } from "src/time-and-attendance/leave-requests/controllers/leave-requests.controller";
|
// import { LeaveRequestController } from "src/time-and-attendance/leave-requests/controllers/leave-requests.controller";
|
||||||
import { LeaveRequestsService } from "src/time-and-attendance/leave-requests/services/leave-request.service";
|
// import { LeaveRequestsService } from "src/time-and-attendance/leave-requests/services/leave-request.service";
|
||||||
import { BusinessLogicsModule } from "src/time-and-attendance/domains/business-logics.module";
|
// import { BusinessLogicsModule } from "src/time-and-attendance/domains/business-logics.module";
|
||||||
import { ShiftsModule } from "src/time-and-attendance/time-tracker/shifts/shifts.module";
|
// import { ShiftsModule } from "src/time-and-attendance/time-tracker/shifts/shifts.module";
|
||||||
import { Module } from "@nestjs/common";
|
// import { Module } from "@nestjs/common";
|
||||||
|
|
||||||
@Module({
|
// @Module({
|
||||||
imports: [
|
// imports: [
|
||||||
BusinessLogicsModule,
|
// BusinessLogicsModule,
|
||||||
ShiftsModule,
|
// ShiftsModule,
|
||||||
],
|
// ],
|
||||||
controllers: [LeaveRequestController],
|
// controllers: [LeaveRequestController],
|
||||||
providers: [LeaveRequestsService],
|
// providers: [LeaveRequestsService],
|
||||||
})
|
// })
|
||||||
|
|
||||||
export class LeaveRequestsModule {}
|
// export class LeaveRequestsModule {}
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import { LeaveRequestViewDto } from "../dtos/leave-request-view.dto";
|
import { LeaveRequestViewDto } from "../dtos/leave-request-view.dto";
|
||||||
import { LeaveRequestRow } from "src/time-and-attendance/utils/type.utils";
|
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
|
import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils";
|
||||||
|
|
||||||
|
type LeaveRequestRow = Prisma.LeaveRequestsGetPayload<{ select: typeof leaveRequestsSelect}>;
|
||||||
|
|
||||||
const toNum = (value?: Prisma.Decimal | null) =>
|
const toNum = (value?: Prisma.Decimal | null) =>
|
||||||
value !== null && value !== undefined ? Number(value) : undefined;
|
value !== null && value !== undefined ? Number(value) : undefined;
|
||||||
|
|
|
||||||
|
|
@ -1,79 +1,84 @@
|
||||||
import { Injectable, NotFoundException, BadRequestException } from "@nestjs/common";
|
// import { Injectable, NotFoundException, BadRequestException } from "@nestjs/common";
|
||||||
import { UpsertLeaveRequestDto, UpsertResult } from "src/time-and-attendance/leave-requests/dtos/upsert-leave-request.dto";
|
// import { UpsertLeaveRequestDto, UpsertResult } from "src/time-and-attendance/leave-requests/dtos/upsert-leave-request.dto";
|
||||||
import { LeaveTypes, LeaveApprovalStatus } from "@prisma/client";
|
// import { LeaveTypes, LeaveApprovalStatus } from "@prisma/client";
|
||||||
import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils";
|
// import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils";
|
||||||
import { LeaveRequestViewDto } from "src/time-and-attendance/leave-requests/dtos/leave-request-view.dto";
|
// import { LeaveRequestViewDto } from "src/time-and-attendance/leave-requests/dtos/leave-request-view.dto";
|
||||||
import { LeaveRequestsUtils } from "src/time-and-attendance/leave-requests/utils/leave-request.util";
|
// import { LeaveRequestsUtils } from "src/time-and-attendance/leave-requests/utils/leave-request.util";
|
||||||
import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils";
|
// import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils";
|
||||||
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
|
// import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
|
||||||
import { HolidayService } from "src/time-and-attendance/domains/services/holiday.service";
|
// import { HolidayService } from "src/time-and-attendance/domains/services/holiday.service";
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
// import { PrismaService } from "src/prisma/prisma.service";
|
||||||
import { mapRowToView } from "src/time-and-attendance/leave-requests/mappers/leave-requests.mapper";
|
// import { mapRowToView } from "src/time-and-attendance/leave-requests/mappers/leave-requests.mapper";
|
||||||
import { normalizeDates, toDateOnly } from "src/time-and-attendance/utils/date-time.utils";
|
// import { normalizeDates, toDateOnly } from "src/time-and-attendance/utils/date-time.utils";
|
||||||
|
// import { Result } from "src/common/errors/result-error.factory";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@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<Result<UpsertResult, string>> {
|
||||||
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.findIdAndModifierByType(LeaveTypes.HOLIDAY);
|
// if (!employee_id.success) return { success: false, error: employee_id.error }
|
||||||
const dates = normalizeDates(dto.dates);
|
|
||||||
if (!bank_code) throw new NotFoundException(`bank_code not found`);
|
|
||||||
if (!dates.length) throw new BadRequestException('Dates array must not be empty');
|
|
||||||
|
|
||||||
const created: LeaveRequestViewDto[] = [];
|
// const bank_code = await this.typeResolver.findIdAndModifierByType(LeaveTypes.HOLIDAY);
|
||||||
|
// const dates = normalizeDates(dto.dates);
|
||||||
|
// if (!bank_code) throw new NotFoundException(`bank_code not found`);
|
||||||
|
// if (!dates.length) throw new BadRequestException('Dates array must not be empty');
|
||||||
|
|
||||||
for (const iso_date of dates) {
|
// const created: LeaveRequestViewDto[] = [];
|
||||||
const date = toDateOnly(iso_date);
|
|
||||||
|
|
||||||
const existing = await this.prisma.leaveRequests.findUnique({
|
// for (const iso_date of dates) {
|
||||||
where: {
|
// const date = toDateOnly(iso_date);
|
||||||
leave_per_employee_date: {
|
|
||||||
employee_id: employee_id,
|
|
||||||
leave_type: LeaveTypes.HOLIDAY,
|
|
||||||
date,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
select: { id: true },
|
|
||||||
});
|
|
||||||
if (existing) {
|
|
||||||
throw new BadRequestException(`Holiday request already exists for ${iso_date}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const payable = await this.holidayService.calculateHolidayPay(email, date, bank_code.modifier);
|
// const existing = await this.prisma.leaveRequests.findUnique({
|
||||||
const row = await this.prisma.leaveRequests.create({
|
// where: {
|
||||||
data: {
|
// leave_per_employee_date: {
|
||||||
employee_id: employee_id,
|
// employee_id: employee_id.data,
|
||||||
bank_code_id: bank_code.id,
|
// leave_type: LeaveTypes.HOLIDAY,
|
||||||
leave_type: LeaveTypes.HOLIDAY,
|
// date,
|
||||||
date,
|
// },
|
||||||
comment: dto.comment ?? '',
|
// },
|
||||||
requested_hours: dto.requested_hours ?? 8,
|
// select: { id: true },
|
||||||
payable_hours: payable,
|
// });
|
||||||
approval_status: dto.approval_status ?? LeaveApprovalStatus.PENDING,
|
// if (existing) {
|
||||||
},
|
// throw new BadRequestException(`Holiday request already exists for ${iso_date}`);
|
||||||
select: leaveRequestsSelect,
|
// }
|
||||||
});
|
|
||||||
|
|
||||||
const hours = Number(row.payable_hours ?? row.requested_hours ?? 0);
|
// const payable = await this.holidayService.calculateHolidayPay(email, date, bank_code.modifier);
|
||||||
if (row.approval_status === LeaveApprovalStatus.APPROVED) {
|
// if (!payable) return { success: false, error: `An error occured during calculation` };
|
||||||
await this.leaveUtils.syncShift(email, employee_id, iso_date, hours,LeaveTypes.HOLIDAY, row.comment);
|
|
||||||
}
|
|
||||||
|
|
||||||
created.push({ ...mapRowToView(row), action: 'create' });
|
// const row = await this.prisma.leaveRequests.create({
|
||||||
}
|
// data: {
|
||||||
|
// employee_id: employee_id.data,
|
||||||
|
// bank_code_id: bank_code.id,
|
||||||
|
// leave_type: LeaveTypes.HOLIDAY,
|
||||||
|
// date,
|
||||||
|
// comment: dto.comment ?? '',
|
||||||
|
// requested_hours: dto.requested_hours ?? 8,
|
||||||
|
// payable_hours: payable,
|
||||||
|
// approval_status: dto.approval_status ?? LeaveApprovalStatus.PENDING,
|
||||||
|
// },
|
||||||
|
// select: leaveRequestsSelect,
|
||||||
|
// });
|
||||||
|
|
||||||
return { action: 'create', leave_requests: created };
|
// const hours = Number(row.payable_hours ?? row.requested_hours ?? 0);
|
||||||
}
|
// if (row.approval_status === LeaveApprovalStatus.APPROVED) {
|
||||||
}
|
// await this.leaveUtils.syncShift(email, employee_id.data, iso_date, hours, LeaveTypes.HOLIDAY, row.comment);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// created.push({ ...mapRowToView(row), action: 'create' });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return { success: true, data: { action: 'create', leave_requests: created } };
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,241 +1,241 @@
|
||||||
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
|
// import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
|
||||||
import { UpsertLeaveRequestDto, UpsertResult } from "../dtos/upsert-leave-request.dto";
|
// import { UpsertLeaveRequestDto, UpsertResult } from "../dtos/upsert-leave-request.dto";
|
||||||
import { LeaveApprovalStatus, LeaveTypes } from "@prisma/client";
|
// import { LeaveApprovalStatus, LeaveTypes } from "@prisma/client";
|
||||||
import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils";
|
// import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils";
|
||||||
import { LeaveRequestViewDto } from "../dtos/leave-request-view.dto";
|
// import { LeaveRequestViewDto } from "../dtos/leave-request-view.dto";
|
||||||
import { roundToQuarterHour } from "src/common/utils/date-utils";
|
// import { roundToQuarterHour } from "src/common/utils/date-utils";
|
||||||
import { LeaveRequestsUtils } from "src/time-and-attendance/leave-requests/utils/leave-request.util";
|
// import { LeaveRequestsUtils } from "src/time-and-attendance/leave-requests/utils/leave-request.util";
|
||||||
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
|
// import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
|
||||||
import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils";
|
// import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils";
|
||||||
import { SickLeaveService } from "src/time-and-attendance/domains/services/sick-leave.service";
|
// import { SickLeaveService } from "src/time-and-attendance/domains/services/sick-leave.service";
|
||||||
import { VacationService } from "src/time-and-attendance/domains/services/vacation.service";
|
// import { VacationService } from "src/time-and-attendance/domains/services/vacation.service";
|
||||||
import { HolidayService } from "src/time-and-attendance/domains/services/holiday.service";
|
// import { HolidayService } from "src/time-and-attendance/domains/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 { normalizeDates, toDateOnly, toISODateKey } from "src/time-and-attendance/utils/date-time.utils";
|
// import { normalizeDates, toDateOnly, toISODateKey } from "src/time-and-attendance/utils/date-time.utils";
|
||||||
@Injectable()
|
// @Injectable()
|
||||||
export class LeaveRequestsService {
|
// export class LeaveRequestsService {
|
||||||
constructor(
|
// constructor(
|
||||||
private readonly prisma: PrismaService,
|
// private readonly prisma: PrismaService,
|
||||||
private readonly holidayService: HolidayService,
|
// private readonly holidayService: HolidayService,
|
||||||
private readonly sickLogic: SickLeaveService,
|
// private readonly sickLogic: SickLeaveService,
|
||||||
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.holidayService.create(dto);
|
// // return this.holidayService.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.vacationService.create(dto);
|
// // return this.vacationService.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.findIdAndModifierByType(type);
|
// const bank_code = await this.typeResolver.findIdAndModifierByType(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 };
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,98 +1,98 @@
|
||||||
import { Injectable, NotFoundException, BadRequestException } from "@nestjs/common";
|
// import { Injectable, NotFoundException, BadRequestException } from "@nestjs/common";
|
||||||
import { UpsertLeaveRequestDto, UpsertResult } from "src/time-and-attendance/leave-requests/dtos/upsert-leave-request.dto";
|
// import { UpsertLeaveRequestDto, UpsertResult } from "src/time-and-attendance/leave-requests/dtos/upsert-leave-request.dto";
|
||||||
import { LeaveTypes, LeaveApprovalStatus } from "@prisma/client";
|
// import { LeaveTypes, LeaveApprovalStatus } from "@prisma/client";
|
||||||
import { LeaveRequestViewDto } from "src/time-and-attendance/leave-requests/dtos/leave-request-view.dto";
|
// import { LeaveRequestViewDto } from "src/time-and-attendance/leave-requests/dtos/leave-request-view.dto";
|
||||||
import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils";
|
// import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils";
|
||||||
import { roundToQuarterHour } from "src/common/utils/date-utils";
|
// import { roundToQuarterHour } from "src/common/utils/date-utils";
|
||||||
import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils";
|
// import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils";
|
||||||
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
|
// import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
|
||||||
import { SickLeaveService } from "src/time-and-attendance/domains/services/sick-leave.service";
|
// import { SickLeaveService } from "src/time-and-attendance/domains/services/sick-leave.service";
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
// import { PrismaService } from "src/prisma/prisma.service";
|
||||||
import { mapRowToView } from "src/time-and-attendance/leave-requests/mappers/leave-requests.mapper";
|
// import { mapRowToView } from "src/time-and-attendance/leave-requests/mappers/leave-requests.mapper";
|
||||||
import { normalizeDates, toDateOnly } from "src/time-and-attendance/utils/date-time.utils";
|
// import { normalizeDates, toDateOnly } from "src/time-and-attendance/utils/date-time.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 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.findIdAndModifierByType(LeaveTypes.SICK);
|
// const bank_code = await this.typeResolver.findIdAndModifierByType(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 };
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
|
||||||
|
|
@ -1,91 +1,91 @@
|
||||||
import { Injectable, NotFoundException, BadRequestException } from "@nestjs/common";
|
// import { Injectable, NotFoundException, BadRequestException } from "@nestjs/common";
|
||||||
import { UpsertLeaveRequestDto, UpsertResult } from "src/time-and-attendance/leave-requests/dtos/upsert-leave-request.dto";
|
// import { UpsertLeaveRequestDto, UpsertResult } from "src/time-and-attendance/leave-requests/dtos/upsert-leave-request.dto";
|
||||||
import { LeaveTypes, LeaveApprovalStatus } from "@prisma/client";
|
// import { LeaveTypes, LeaveApprovalStatus } from "@prisma/client";
|
||||||
import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils";
|
// import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils";
|
||||||
import { LeaveRequestViewDto } from "src/time-and-attendance/leave-requests/dtos/leave-request-view.dto";
|
// import { LeaveRequestViewDto } from "src/time-and-attendance/leave-requests/dtos/leave-request-view.dto";
|
||||||
import { roundToQuarterHour } from "src/common/utils/date-utils";
|
// import { roundToQuarterHour } from "src/common/utils/date-utils";
|
||||||
import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils";
|
// import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils";
|
||||||
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
|
// import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
|
||||||
import { VacationService } from "src/time-and-attendance/domains/services/vacation.service";
|
// import { VacationService } from "src/time-and-attendance/domains/services/vacation.service";
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
// import { PrismaService } from "src/prisma/prisma.service";
|
||||||
import { mapRowToView } from "src/time-and-attendance/leave-requests/mappers/leave-requests.mapper";
|
// import { mapRowToView } from "src/time-and-attendance/leave-requests/mappers/leave-requests.mapper";
|
||||||
import { normalizeDates, toDateOnly } from "src/time-and-attendance/utils/date-time.utils";
|
// import { normalizeDates, toDateOnly } from "src/time-and-attendance/utils/date-time.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 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.findIdAndModifierByType(LeaveTypes.VACATION);
|
// const bank_code = await this.typeResolver.findIdAndModifierByType(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 };
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
import { LeaveRequestViewDto } from "src/time-and-attendance/leave-requests/dtos/leave-request-view.dto";
|
import { LeaveRequestViewDto } from "src/time-and-attendance/leave-requests/dtos/leave-request-view.dto";
|
||||||
import { mapArchiveRowToView } from "src/time-and-attendance/leave-requests/mappers/leave-requests-archive.mapper";
|
import { mapArchiveRowToView } from "src/time-and-attendance/leave-requests/mappers/leave-requests-archive.mapper";
|
||||||
import { mapRowToView } from "src/time-and-attendance/leave-requests/mappers/leave-requests.mapper";
|
import { mapRowToView } from "src/time-and-attendance/leave-requests/mappers/leave-requests.mapper";
|
||||||
import { LeaveRequestArchiveRow } from "src/time-and-attendance/leave-requests/utils/leave-requests-archive.select";
|
import { LeaveRequestArchiveRow } from "src/time-and-attendance/leave-requests/utils/leave-requests-archive.select";
|
||||||
import { LeaveRequestRow } from "src/time-and-attendance/utils/type.utils";
|
import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils";
|
||||||
|
|
||||||
|
export type LeaveRequestRow = Prisma.LeaveRequestsGetPayload<{ select: typeof leaveRequestsSelect}>;
|
||||||
|
|
||||||
/** Active (table leave_requests) : proxy to base mapper */
|
/** Active (table leave_requests) : proxy to base mapper */
|
||||||
export function mapRowToViewWithDays(row: LeaveRequestRow): LeaveRequestViewDto {
|
export function mapRowToViewWithDays(row: LeaveRequestRow): LeaveRequestViewDto {
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,9 @@ import { SchedulePresetsApplyService } from "src/time-and-attendance/time-tracke
|
||||||
import { SchedulePresetsGetService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-get.service";
|
import { SchedulePresetsGetService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-get.service";
|
||||||
import { SchedulePresetsUpsertService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-upsert.service";
|
import { SchedulePresetsUpsertService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-upsert.service";
|
||||||
import { ShiftController } from "src/time-and-attendance/time-tracker/shifts/controllers/shift.controller";
|
import { ShiftController } from "src/time-and-attendance/time-tracker/shifts/controllers/shift.controller";
|
||||||
|
import { ShiftsCreateService } from "src/time-and-attendance/time-tracker/shifts/services/shifts-create.service";
|
||||||
import { ShiftsGetService } from "src/time-and-attendance/time-tracker/shifts/services/shifts-get.service";
|
import { ShiftsGetService } from "src/time-and-attendance/time-tracker/shifts/services/shifts-get.service";
|
||||||
import { ShiftsUpsertService } from "src/time-and-attendance/time-tracker/shifts/services/shifts-upsert.service";
|
import { ShiftsUpdateDeleteService } from "src/time-and-attendance/time-tracker/shifts/services/shifts-update-delete.service";
|
||||||
import { TimesheetController } from "src/time-and-attendance/time-tracker/timesheets/controllers/timesheet.controller";
|
import { TimesheetController } from "src/time-and-attendance/time-tracker/timesheets/controllers/timesheet.controller";
|
||||||
import { TimesheetApprovalService } from "src/time-and-attendance/time-tracker/timesheets/services/timesheet-approval.service";
|
import { TimesheetApprovalService } from "src/time-and-attendance/time-tracker/timesheets/services/timesheet-approval.service";
|
||||||
import { GetTimesheetsOverviewService } from "src/time-and-attendance/time-tracker/timesheets/services/timesheet-get-overview.service";
|
import { GetTimesheetsOverviewService } from "src/time-and-attendance/time-tracker/timesheets/services/timesheet-get-overview.service";
|
||||||
|
|
@ -33,7 +34,8 @@ import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-i
|
||||||
providers: [
|
providers: [
|
||||||
GetTimesheetsOverviewService,
|
GetTimesheetsOverviewService,
|
||||||
ShiftsGetService,
|
ShiftsGetService,
|
||||||
ShiftsUpsertService,
|
ShiftsCreateService,
|
||||||
|
ShiftsUpdateDeleteService,
|
||||||
ExpenseUpsertService,
|
ExpenseUpsertService,
|
||||||
SchedulePresetsUpsertService,
|
SchedulePresetsUpsertService,
|
||||||
SchedulePresetsGetService,
|
SchedulePresetsGetService,
|
||||||
|
|
|
||||||
|
|
@ -5,20 +5,21 @@ import { PrismaService } from "src/prisma/prisma.service";
|
||||||
import { ApplyResult } from "src/time-and-attendance/utils/type.utils";
|
import { ApplyResult } from "src/time-and-attendance/utils/type.utils";
|
||||||
import { WEEKDAY } from "src/time-and-attendance/utils/mappers.utils";
|
import { WEEKDAY } from "src/time-and-attendance/utils/mappers.utils";
|
||||||
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
|
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
|
||||||
|
import { Result } from "src/common/errors/result-error.factory";
|
||||||
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SchedulePresetsApplyService {
|
export class SchedulePresetsApplyService {
|
||||||
constructor(private readonly prisma: PrismaService, private readonly emailResolver: EmailToIdResolver) { }
|
constructor(private readonly prisma: PrismaService, private readonly emailResolver: EmailToIdResolver) { }
|
||||||
|
|
||||||
async applyToTimesheet( email: string, id: number, start_date_iso: string ): Promise<ApplyResult> {
|
async applyToTimesheet(email: string, id: number, start_date_iso: string): Promise<Result<ApplyResult, string>> {
|
||||||
if(!id) throw new BadRequestException(`Schedule preset with id: ${id} not found`);
|
if (!DATE_ISO_FORMAT.test(start_date_iso)) return { success: false, error: 'start_date must be of format :YYYY-MM-DD' };
|
||||||
if(!DATE_ISO_FORMAT.test(start_date_iso)) throw new BadRequestException('start_date must be of format :YYYY-MM-DD');
|
|
||||||
|
|
||||||
const employee_id = await this.emailResolver.findIdByEmail(email);
|
const employee_id = await this.emailResolver.findIdByEmail(email);
|
||||||
|
if (!employee_id.success) return { success: false, error: employee_id.error }
|
||||||
|
|
||||||
const preset = await this.prisma.schedulePresets.findFirst({
|
const preset = await this.prisma.schedulePresets.findFirst({
|
||||||
where: { employee_id, id },
|
where: { employee_id: employee_id.data, id },
|
||||||
include: {
|
include: {
|
||||||
shifts: {
|
shifts: {
|
||||||
orderBy: [{ week_day: 'asc' }, { sort_order: 'asc' }],
|
orderBy: [{ week_day: 'asc' }, { sort_order: 'asc' }],
|
||||||
|
|
@ -34,13 +35,14 @@ export class SchedulePresetsApplyService {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if(!preset) throw new NotFoundException(`Preset ${preset} not found`);
|
if (!preset) return { success: false, error: `Schedule preset with id: ${id} not found` };
|
||||||
|
|
||||||
|
|
||||||
const start_date = new Date(`${start_date_iso}T00:00:00.000Z`);
|
const start_date = new Date(`${start_date_iso}T00:00:00.000Z`);
|
||||||
const timesheet = await this.prisma.timesheets.upsert({
|
const timesheet = await this.prisma.timesheets.upsert({
|
||||||
where: { employee_id_start_date: { employee_id, start_date: start_date} },
|
where: { employee_id_start_date: { employee_id: employee_id.data, start_date: start_date } },
|
||||||
update: {},
|
update: {},
|
||||||
create: { employee_id, start_date: start_date },
|
create: { employee_id: employee_id.data, start_date: start_date },
|
||||||
select: { id: true },
|
select: { id: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -85,22 +87,21 @@ export class SchedulePresetsApplyService {
|
||||||
|
|
||||||
for (const shift of shifts) {
|
for (const shift of shifts) {
|
||||||
if (shift.end_time.getTime() <= shift.start_time.getTime()) {
|
if (shift.end_time.getTime() <= shift.start_time.getTime()) {
|
||||||
throw new ConflictException(`Invalid time range in preset day: ${week_day}, order: ${shift.sort_order}`);
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `Invalid time range in preset day: ${week_day}, order: ${shift.sort_order}`
|
||||||
|
};
|
||||||
}
|
}
|
||||||
const conflict = existing.find((existe) => overlaps(
|
const conflict = existing.find((existe) => overlaps(
|
||||||
shift.start_time, shift.end_time,
|
shift.start_time, shift.end_time,
|
||||||
existe.start_time, existe.end_time,
|
existe.start_time, existe.end_time,
|
||||||
));
|
));
|
||||||
if(conflict) {
|
if (conflict)
|
||||||
throw new ConflictException({
|
return {
|
||||||
error_code: 'SHIFT_OVERLAP_WITH_EXISTING',
|
success: false,
|
||||||
mesage: `Preset shift overlaps existing shift on ${start_date} + ${i}(week day ${week_day})`,
|
error: `[SHIFT_OVERLAP] :Preset shift overlaps existing shift on ${start_date} + ${i}(week day ${week_day}) `
|
||||||
conflict: {
|
};
|
||||||
existing_start: conflict.start_time.toISOString().slice(11,16),
|
|
||||||
existing_end: conflict.end_time.toISOString().slice(11,16),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
payload.push({
|
payload.push({
|
||||||
timesheet_id: timesheet.id,
|
timesheet_id: timesheet.id,
|
||||||
date: date,
|
date: date,
|
||||||
|
|
@ -118,6 +119,6 @@ export class SchedulePresetsApplyService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return { timesheet_id: timesheet.id, created, skipped };
|
return { success: true, data: { timesheet_id: timesheet.id, created, skipped } };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { PresetResponse, ShiftResponse } from "src/time-and-attendance/utils/type.utils";
|
import { PresetResponse, ShiftResponse } from "src/time-and-attendance/utils/type.utils";
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
import { Prisma } from "@prisma/client";
|
|
||||||
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
|
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
|
||||||
|
import { Result } from "src/common/errors/result-error.factory";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SchedulePresetsGetService {
|
export class SchedulePresetsGetService {
|
||||||
|
|
@ -11,11 +11,12 @@ export class SchedulePresetsGetService {
|
||||||
private readonly emailResolver: EmailToIdResolver,
|
private readonly emailResolver: EmailToIdResolver,
|
||||||
){}
|
){}
|
||||||
|
|
||||||
async getSchedulePresets(email: string): Promise<PresetResponse[]> {
|
async getSchedulePresets(email: string): Promise<Result<PresetResponse[], string>> {
|
||||||
try {
|
try {
|
||||||
const employee_id = await this.emailResolver.findIdByEmail(email);
|
const employee_id = await this.emailResolver.findIdByEmail(email);
|
||||||
|
if(!employee_id.success) return { success: false, error: employee_id.error }
|
||||||
const presets = await this.prisma.schedulePresets.findMany({
|
const presets = await this.prisma.schedulePresets.findMany({
|
||||||
where: { employee_id },
|
where: { employee_id: employee_id.data },
|
||||||
orderBy: [{is_default: 'desc' }, { name: 'asc' }],
|
orderBy: [{is_default: 'desc' }, { name: 'asc' }],
|
||||||
include: {
|
include: {
|
||||||
shifts: {
|
shifts: {
|
||||||
|
|
@ -39,10 +40,9 @@ export class SchedulePresetsGetService {
|
||||||
type: shift.bank_code?.type,
|
type: shift.bank_code?.type,
|
||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
return response;
|
return { success: true, data:response};
|
||||||
} catch ( error: unknown) {
|
} catch ( error) {
|
||||||
if(error instanceof Prisma.PrismaClientKnownRequestError) {}
|
return { success: false, error: `Schedule presets for employee with email ${email} not found`};
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { Injectable, BadRequestException, NotFoundException, ConflictException } from "@nestjs/common";
|
import { Injectable, BadRequestException, NotFoundException, ConflictException } from "@nestjs/common";
|
||||||
import { CreatePresetResult, DeletePresetResult, UpdatePresetResult } from "src/time-and-attendance/utils/type.utils";
|
|
||||||
import { Prisma, Weekday } from "@prisma/client";
|
import { Prisma, Weekday } from "@prisma/client";
|
||||||
import { toHHmmFromDate } from "src/time-and-attendance/utils/date-time.utils";
|
import { toDateFromString, toHHmmFromDate } from "src/time-and-attendance/utils/date-time.utils";
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils";
|
import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils";
|
||||||
import { SchedulePresetsDto } from "src/time-and-attendance/time-tracker/schedule-presets/dtos/create-schedule-presets.dto";
|
import { SchedulePresetsDto } from "src/time-and-attendance/time-tracker/schedule-presets/dtos/create-schedule-presets.dto";
|
||||||
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
|
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
|
||||||
|
import { Result } from "src/common/errors/result-error.factory";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SchedulePresetsUpsertService {
|
export class SchedulePresetsUpsertService {
|
||||||
|
|
@ -17,41 +17,42 @@ export class SchedulePresetsUpsertService {
|
||||||
//_________________________________________________________________
|
//_________________________________________________________________
|
||||||
// CREATE
|
// CREATE
|
||||||
//_________________________________________________________________
|
//_________________________________________________________________
|
||||||
async createPreset(email: string, dto: SchedulePresetsDto): Promise<CreatePresetResult> {
|
async createPreset(email: string, dto: SchedulePresetsDto): Promise<Result<SchedulePresetsDto, string>> {
|
||||||
try {
|
try {
|
||||||
const shifts_data = await this.resolveAndBuildPresetShifts(dto);
|
const shifts_data = await this.normalizePresetShifts(dto);
|
||||||
const employee_id = await this.emailResolver.findIdByEmail(email);
|
if (!shifts_data.success) return { success: false, error: `Employee with email: ${email} or dto not found` };
|
||||||
if (!shifts_data) throw new BadRequestException(`Employee with email: ${email} or dto not found`);
|
|
||||||
|
|
||||||
await this.prisma.$transaction(async (tx) => {
|
const employee_id = await this.emailResolver.findIdByEmail(email);
|
||||||
|
if (!employee_id.success) return { success: false, error: employee_id.error };
|
||||||
|
|
||||||
|
const created = await this.prisma.$transaction(async (tx) => {
|
||||||
if (dto.is_default) {
|
if (dto.is_default) {
|
||||||
await tx.schedulePresets.updateMany({
|
await tx.schedulePresets.updateMany({
|
||||||
where: { is_default: true, employee_id },
|
where: { is_default: true, employee_id: employee_id.data },
|
||||||
data: { is_default: false },
|
data: { is_default: false },
|
||||||
});
|
});
|
||||||
}
|
await tx.schedulePresets.create({
|
||||||
const created = await tx.schedulePresets.create({
|
|
||||||
data: {
|
data: {
|
||||||
id: dto.id,
|
id: dto.id,
|
||||||
employee_id,
|
employee_id: employee_id.data,
|
||||||
name: dto.name,
|
name: dto.name,
|
||||||
is_default: !!dto.is_default,
|
is_default: !!dto.is_default,
|
||||||
shifts: { create: shifts_data },
|
shifts: { create: shifts_data.data },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return created;
|
return { success: true, data: created }
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return { ok: true };
|
return { success: true, data: created }
|
||||||
|
} catch (error) {
|
||||||
} catch (error: unknown) {
|
return { success: false, error: ' An error occured during create. Invalid Schedule data' };
|
||||||
return { ok: false, error };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//_________________________________________________________________
|
//_________________________________________________________________
|
||||||
// UPDATE
|
// UPDATE
|
||||||
//_________________________________________________________________
|
//_________________________________________________________________
|
||||||
async updatePreset(preset_id: number, dto: SchedulePresetsDto): Promise<UpdatePresetResult> {
|
async updatePreset(preset_id: number, dto: SchedulePresetsDto): Promise<Result<SchedulePresetsDto, string>> {
|
||||||
try {
|
try {
|
||||||
const existing = await this.prisma.schedulePresets.findFirst({
|
const existing = await this.prisma.schedulePresets.findFirst({
|
||||||
where: { id: preset_id },
|
where: { id: preset_id },
|
||||||
|
|
@ -61,9 +62,11 @@ export class SchedulePresetsUpsertService {
|
||||||
employee_id: true,
|
employee_id: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!existing) throw new NotFoundException(`Preset "${dto.name}" not found`);
|
if (!existing) return { success: false, error: `Preset "${dto.name}" not found` };
|
||||||
|
|
||||||
|
const shifts_data = await this.normalizePresetShifts(dto);
|
||||||
|
if(!shifts_data.success) return { success: false, error: 'An error occured during normalization'}
|
||||||
|
|
||||||
const shifts_data = await this.resolveAndBuildPresetShifts(dto);
|
|
||||||
await this.prisma.$transaction(async (tx) => {
|
await this.prisma.$transaction(async (tx) => {
|
||||||
if (typeof dto.is_default === 'boolean') {
|
if (typeof dto.is_default === 'boolean') {
|
||||||
if (dto.is_default) {
|
if (dto.is_default) {
|
||||||
|
|
@ -84,27 +87,34 @@ export class SchedulePresetsUpsertService {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (shifts_data.length <= 0) throw new BadRequestException('Preset shifts to update not found');
|
if (shifts_data.data.length <= 0) return { success: false, error: 'Preset shifts to update not found' };
|
||||||
|
|
||||||
await tx.schedulePresetShifts.deleteMany({ where: { preset_id: existing.id } });
|
await tx.schedulePresetShifts.deleteMany({ where: { preset_id: existing.id } });
|
||||||
|
|
||||||
const create_many_data: Prisma.SchedulePresetShiftsCreateManyInput[] =
|
// try {
|
||||||
shifts_data.map((shift) => {
|
// const create_many_data: Result<Prisma.SchedulePresetShiftsCreateManyInput[], string> =
|
||||||
if (!shift.bank_code || !('connect' in shift.bank_code) || typeof shift.bank_code.connect?.id !== 'number') {
|
// shifts_data.data.map((shift) => {
|
||||||
throw new NotFoundException(`Bank code is required for updates( ${shift.week_day}, ${shift.sort_order})`);
|
// if (!shift.bank_code || !('connect' in shift.bank_code) || typeof shift.bank_code.connect?.id !== 'number') {
|
||||||
}
|
// return { success: false, error: `Bank code is required for updates( ${shift.week_day}, ${shift.sort_order})`}
|
||||||
const bank_code_id = shift.bank_code.connect.id;
|
// }
|
||||||
return {
|
// const bank_code_id = shift.bank_code.connect.id;
|
||||||
preset_id: existing.id,
|
// return {
|
||||||
week_day: shift.week_day,
|
// preset_id: existing.id,
|
||||||
sort_order: shift.sort_order,
|
// week_day: shift.week_day,
|
||||||
start_time: shift.start_time,
|
// sort_order: shift.sort_order,
|
||||||
end_time: shift.end_time,
|
// start_time: shift.start_time,
|
||||||
is_remote: shift.is_remote ?? false,
|
// end_time: shift.end_time,
|
||||||
bank_code_id: bank_code_id,
|
// is_remote: shift.is_remote ?? false,
|
||||||
};
|
// bank_code_id: bank_code_id,
|
||||||
});
|
// };
|
||||||
await tx.schedulePresetShifts.createMany({ data: create_many_data });
|
// });
|
||||||
|
// if(!create_many_data.success) return { success: false, error: 'Invalid data'}
|
||||||
|
// await tx.schedulePresetShifts.createMany({ data: create_many_data.data });
|
||||||
|
|
||||||
|
// return { success: true, data: create_many_data }
|
||||||
|
// } catch (error) {
|
||||||
|
// return { success: false, error: 'An error occured. Invalid data detected. ' };
|
||||||
|
// }
|
||||||
});
|
});
|
||||||
|
|
||||||
const saved = await this.prisma.schedulePresets.findUnique({
|
const saved = await this.prisma.schedulePresets.findUnique({
|
||||||
|
|
@ -116,7 +126,7 @@ export class SchedulePresetsUpsertService {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!saved) throw new NotFoundException(`Preset with id: ${existing.id} not found`);
|
if (!saved) return { success: false, error: `Preset with id: ${existing.id} not found` };
|
||||||
|
|
||||||
const response_dto: SchedulePresetsDto = {
|
const response_dto: SchedulePresetsDto = {
|
||||||
id: saved.id,
|
id: saved.id,
|
||||||
|
|
@ -133,52 +143,50 @@ export class SchedulePresetsUpsertService {
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
return { ok: true, id: existing.id, data: response_dto };
|
return { success: true, data: response_dto };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { ok: false, id: preset_id, error }
|
return { success: false, error: 'An error occured during update. Invalid data' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//_________________________________________________________________
|
//_________________________________________________________________
|
||||||
// DELETE
|
// DELETE
|
||||||
//_________________________________________________________________
|
//_________________________________________________________________
|
||||||
async deletePreset(preset_id: number): Promise<DeletePresetResult> {
|
async deletePreset(preset_id: number): Promise<Result<number, string>> {
|
||||||
try {
|
try {
|
||||||
await this.prisma.$transaction(async (tx) => {
|
await this.prisma.$transaction(async (tx) => {
|
||||||
const preset = await tx.schedulePresets.findFirst({
|
const preset = await tx.schedulePresets.findFirst({
|
||||||
where: { id: preset_id },
|
where: { id: preset_id },
|
||||||
select: { id: true },
|
select: { id: true },
|
||||||
});
|
});
|
||||||
if (!preset) throw new NotFoundException(`Preset with id ${preset_id} not found`);
|
if (!preset) return { success: false, error: `Preset with id ${preset_id} not found` };
|
||||||
await tx.schedulePresets.delete({ where: { id: preset_id } });
|
await tx.schedulePresets.delete({ where: { id: preset_id } });
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
});
|
});
|
||||||
return { ok: true, id: preset_id };
|
return { success: true, data: preset_id };
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error) throw new NotFoundException(`Preset schedule with id ${preset_id} not found`);
|
return { success: false, error: `Preset schedule with id ${preset_id} not found` };
|
||||||
return { ok: false, id: preset_id, error };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//PRIVATE HELPERS
|
//PRIVATE HELPERS
|
||||||
|
|
||||||
//resolve bank_code_id using type and convert hours to TIME and valid shifts end/start
|
//resolve bank_code_id using type and convert hours to TIME and valid shifts end/start
|
||||||
private async resolveAndBuildPresetShifts(
|
private async normalizePresetShifts(
|
||||||
dto: SchedulePresetsDto
|
dto: SchedulePresetsDto
|
||||||
): Promise<Prisma.SchedulePresetShiftsCreateWithoutPresetInput[]> {
|
): Promise<Result<Prisma.SchedulePresetShiftsCreateWithoutPresetInput[], string>> {
|
||||||
|
|
||||||
if (!dto.preset_shifts?.length) throw new NotFoundException(`Empty or preset shifts not found`);
|
if (!dto.preset_shifts?.length) throw new NotFoundException(`Empty or preset shifts not found`);
|
||||||
|
|
||||||
const types = Array.from(new Set(dto.preset_shifts.map((shift) => shift.type)));
|
const types = Array.from(new Set(dto.preset_shifts.map((shift) => shift.type)));
|
||||||
const bank_code_set = new Map<string, number>();
|
const bank_code_set = new Map<string, number>();
|
||||||
|
|
||||||
for (const type of types) {
|
for (const type of types) {
|
||||||
const { id } = await this.typeResolver.findIdAndModifierByType(type);
|
const bank_code = await this.typeResolver.findIdAndModifierByType(type);
|
||||||
bank_code_set.set(type, id)
|
if (!bank_code.success) return { success: false, error: 'Bank_code not found' }
|
||||||
|
bank_code_set.set(type, bank_code.data.id);
|
||||||
}
|
}
|
||||||
const toTime = (hhmm: string) => new Date(`1970-01-01T${hhmm}:00.000Z`);
|
|
||||||
|
|
||||||
const pair_set = new Set<string>();
|
const pair_set = new Set<string>();
|
||||||
for (const shift of dto.preset_shifts) {
|
for (const shift of dto.preset_shifts) {
|
||||||
|
|
@ -195,8 +203,8 @@ export class SchedulePresetsUpsertService {
|
||||||
if (!shift.start_time || !shift.end_time) {
|
if (!shift.start_time || !shift.end_time) {
|
||||||
throw new BadRequestException(`start_time and end_time are required for (${shift.week_day}, ${shift.sort_order})`);
|
throw new BadRequestException(`start_time and end_time are required for (${shift.week_day}, ${shift.sort_order})`);
|
||||||
}
|
}
|
||||||
const start = toTime(shift.start_time);
|
const start = toDateFromString(shift.start_time);
|
||||||
const end = toTime(shift.end_time);
|
const end = toDateFromString(shift.end_time);
|
||||||
if (end.getTime() <= start.getTime()) {
|
if (end.getTime() <= start.getTime()) {
|
||||||
throw new ConflictException(`end_time must be > start_time ( day: ${shift.week_day}, order: ${shift.sort_order})`);
|
throw new ConflictException(`end_time must be > start_time ( day: ${shift.week_day}, order: ${shift.sort_order})`);
|
||||||
}
|
}
|
||||||
|
|
@ -210,6 +218,6 @@ export class SchedulePresetsUpsertService {
|
||||||
is_remote: !!shift.is_remote,
|
is_remote: !!shift.is_remote,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
return items;
|
return { success: true, data: items };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,34 @@
|
||||||
import { BadRequestException, Body, Controller, Delete, Param, Patch, Post, Req } from "@nestjs/common";
|
import { Body, Controller, Delete, Param, Patch, Post, Req } from "@nestjs/common";
|
||||||
import { ShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-create.dto";
|
import { ShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-create.dto";
|
||||||
import { ShiftsUpsertService } from "src/time-and-attendance/time-tracker/shifts/services/shifts-upsert.service";
|
|
||||||
import { CreateShiftResult, UpdateShiftResult } from "src/time-and-attendance/utils/type.utils";
|
|
||||||
import { RolesAllowed } from "src/common/decorators/roles.decorators";
|
import { RolesAllowed } from "src/common/decorators/roles.decorators";
|
||||||
import { GLOBAL_CONTROLLER_ROLES } from "src/common/shared/role-groupes";
|
import { GLOBAL_CONTROLLER_ROLES } from "src/common/shared/role-groupes";
|
||||||
|
import { Result } from "src/common/errors/result-error.factory";
|
||||||
|
import { ShiftsCreateService } from "src/time-and-attendance/time-tracker/shifts/services/shifts-create.service";
|
||||||
|
import { ShiftsUpdateDeleteService } from "src/time-and-attendance/time-tracker/shifts/services/shifts-update-delete.service";
|
||||||
|
|
||||||
|
|
||||||
@Controller('shift')
|
@Controller('shift')
|
||||||
@RolesAllowed(...GLOBAL_CONTROLLER_ROLES)
|
@RolesAllowed(...GLOBAL_CONTROLLER_ROLES)
|
||||||
export class ShiftController {
|
export class ShiftController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly upsert_service: ShiftsUpsertService,
|
private readonly create_service: ShiftsCreateService,
|
||||||
|
private readonly update_delete_service: ShiftsUpdateDeleteService,
|
||||||
){}
|
){}
|
||||||
|
|
||||||
@Post('create')
|
@Post('create')
|
||||||
createBatch( @Req() req, @Body()dtos: ShiftDto[]): Promise<CreateShiftResult[]> {
|
createBatch( @Req() req, @Body()dtos: ShiftDto[]): Promise<Result<ShiftDto[], string>> {
|
||||||
const email = req.user?.email;
|
const email = req.user?.email;
|
||||||
const list = Array.isArray(dtos) ? dtos : [];
|
return this.create_service.createOneOrManyShifts(email, dtos)
|
||||||
if(list.length === 0) throw new BadRequestException('Body is missing or invalid (create shifts)');
|
|
||||||
return this.upsert_service.createShifts(email, dtos)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch('update')
|
@Patch('update')
|
||||||
updateBatch( @Body() dtos: ShiftDto[]): Promise<UpdateShiftResult[]>{
|
updateBatch( @Body() dtos: ShiftDto[]): Promise<Result<ShiftDto[], string>>{
|
||||||
const list = Array.isArray(dtos) ? dtos: [];
|
return this.update_delete_service.updateOneOrManyShifts(dtos);
|
||||||
if(list.length === 0) throw new BadRequestException('Body is missing or invalid (update shifts)');
|
|
||||||
return this.upsert_service.updateShifts(dtos);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':shift_id')
|
@Delete(':shift_id')
|
||||||
remove(@Param('shift_id') shift_id: number ) {
|
remove(@Param('shift_id') shift_id: number ): Promise<Result<number, string>> {
|
||||||
return this.upsert_service.deleteShift(shift_id);
|
return this.update_delete_service.deleteShift(shift_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,160 @@
|
||||||
|
import { Normalized } from "src/time-and-attendance/utils/type.utils";
|
||||||
|
import { overlaps, toStringFromHHmm, toStringFromDate, toDateFromString, toHHmmFromString } from "src/time-and-attendance/utils/date-time.utils";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { timesheet_select } from "src/time-and-attendance/utils/selects.utils";
|
||||||
|
import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils";
|
||||||
|
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
|
||||||
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
import { ShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-create.dto";
|
||||||
|
import { Result } from "src/common/errors/result-error.factory";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ShiftsCreateService {
|
||||||
|
constructor(
|
||||||
|
private readonly prisma: PrismaService,
|
||||||
|
private readonly emailResolver: EmailToIdResolver,
|
||||||
|
private readonly typeResolver: BankCodesResolver,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
//_________________________________________________________________
|
||||||
|
// CREATE WRAPPER FUNCTION FOR ONE OR MANY INPUT
|
||||||
|
//_________________________________________________________________
|
||||||
|
async createOneOrManyShifts(email: string, shifts: ShiftDto[]): Promise<Result<ShiftDto[], string>> {
|
||||||
|
try {
|
||||||
|
//verify if array is empty or not
|
||||||
|
if (!Array.isArray(shifts) || shifts.length === 0) return { success: false, error: 'No data received' };
|
||||||
|
|
||||||
|
//verify if email is valid or not
|
||||||
|
const employee_id = await this.emailResolver.findIdByEmail(email);
|
||||||
|
if (!employee_id.success) return { success: false, error: employee_id.error };
|
||||||
|
|
||||||
|
//calls the create functions and await the return of successfull result or not
|
||||||
|
const results = await Promise.allSettled(shifts.map(shift => this.createShift(employee_id.data, shift)));
|
||||||
|
|
||||||
|
//return arrays of created shifts or errors
|
||||||
|
const created_shifts: ShiftDto[] = [];
|
||||||
|
const errors: string[] = [];
|
||||||
|
|
||||||
|
//filters results into created_shifts or errors arrays depending on the return from "allSettled" Promise
|
||||||
|
for (const result of results) {
|
||||||
|
if (result.status === 'fulfilled') {
|
||||||
|
if (result.value.success) {
|
||||||
|
created_shifts.push(result.value.data);
|
||||||
|
} else {
|
||||||
|
errors.push(result.value.error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
errors.push(result.reason instanceof Error ? result.reason.message : String(result.reason));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//verify if shifts were created and returns an array of errors if needed
|
||||||
|
if (created_shifts.length === 0) return { success: false, error: errors.join(' | ') || 'No shift created' };
|
||||||
|
|
||||||
|
// returns array of created shifts
|
||||||
|
return { success: true, data: created_shifts }
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//_________________________________________________________________
|
||||||
|
// CREATE
|
||||||
|
//_________________________________________________________________
|
||||||
|
async createShift(employee_id: number, dto: ShiftDto): Promise<Result<ShiftDto, string>> {
|
||||||
|
try {
|
||||||
|
//transform string format to date and HHmm
|
||||||
|
const normed_shift = await this.normalizeShiftDto(dto);
|
||||||
|
if(!normed_shift.success) return { success: false, error: 'An error occured during normalization' }
|
||||||
|
if (normed_shift.data.end_time <= normed_shift.data.start_time) return {
|
||||||
|
success: false,
|
||||||
|
error: `INVALID_SHIFT - `
|
||||||
|
+ `start_time: ${toStringFromHHmm(normed_shift.data.start_time)},`
|
||||||
|
+ `end_time: ${toStringFromHHmm(normed_shift.data.end_time)},`
|
||||||
|
+ `date: ${toStringFromDate(normed_shift.data.date)}.`
|
||||||
|
}
|
||||||
|
//fetch the right timesheet
|
||||||
|
const timesheet = await this.prisma.timesheets.findUnique({
|
||||||
|
where: { id: dto.timesheet_id, employee_id },
|
||||||
|
select: timesheet_select,
|
||||||
|
});
|
||||||
|
if (!timesheet) return {
|
||||||
|
success: false,
|
||||||
|
error: `INVALID_TIMESHEET -`
|
||||||
|
+ `start_time: ${toStringFromHHmm(normed_shift.data.start_time)},`
|
||||||
|
+ `end_time: ${toStringFromHHmm(normed_shift.data.end_time)},`
|
||||||
|
+ `date: ${toStringFromDate(normed_shift.data.date)}.`
|
||||||
|
}
|
||||||
|
//finds bank_code_id using the type
|
||||||
|
const bank_code_id = await this.typeResolver.findBankCodeIDByType(dto.type);
|
||||||
|
if (!bank_code_id.success) return { success: false, error: bank_code_id.error };
|
||||||
|
|
||||||
|
//fetchs existing shifts from DB to check for overlaps
|
||||||
|
const existing_shifts = await this.prisma.shifts.findMany({
|
||||||
|
where: { timesheet_id: timesheet.id, date: normed_shift.data.date },
|
||||||
|
select: { id: true, date: true, start_time: true, end_time: true },
|
||||||
|
});
|
||||||
|
for (const existing of existing_shifts) {
|
||||||
|
const existing_start = await toDateFromString(existing.start_time);
|
||||||
|
const existing_end = await toDateFromString(existing.end_time);
|
||||||
|
const existing_date = await toDateFromString(existing.date);
|
||||||
|
|
||||||
|
const has_overlap = overlaps(
|
||||||
|
{ start: normed_shift.data.start_time, end: normed_shift.data.end_time, date: normed_shift.data.date },
|
||||||
|
{ start: existing_start, end: existing_end, date: existing_date },
|
||||||
|
);
|
||||||
|
if (has_overlap) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `SHIFT_OVERLAP`
|
||||||
|
+ `new shift: ${toStringFromHHmm(normed_shift.data.start_time)}–${toStringFromHHmm(normed_shift.data.end_time)} `
|
||||||
|
+ `existing shift: ${toStringFromHHmm(existing.start_time)}–${toStringFromHHmm(existing.end_time)} `
|
||||||
|
+ `date: ${toStringFromDate(normed_shift.data.date)})`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//sends data for creation of a shift in db
|
||||||
|
const created_shift = await this.prisma.shifts.create({
|
||||||
|
data: {
|
||||||
|
timesheet_id: timesheet.id,
|
||||||
|
bank_code_id: bank_code_id.data,
|
||||||
|
date: normed_shift.data.date,
|
||||||
|
start_time: normed_shift.data.start_time,
|
||||||
|
end_time: normed_shift.data.end_time,
|
||||||
|
is_approved: dto.is_approved,
|
||||||
|
is_remote: dto.is_remote,
|
||||||
|
comment: dto.comment ?? '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
//builds an object to return for display in the frontend
|
||||||
|
const shift: ShiftDto = {
|
||||||
|
id: created_shift.id,
|
||||||
|
timesheet_id: timesheet.id,
|
||||||
|
type: dto.type,
|
||||||
|
date: toStringFromDate(created_shift.date),
|
||||||
|
start_time: toStringFromHHmm(created_shift.start_time),
|
||||||
|
end_time: toStringFromHHmm(created_shift.end_time),
|
||||||
|
is_approved: created_shift.is_approved,
|
||||||
|
is_remote: created_shift.is_remote,
|
||||||
|
comment: created_shift.comment ?? '',
|
||||||
|
}
|
||||||
|
return { success: true, data: shift };
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: `An error occured during creation, invalid data` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//_________________________________________________________________
|
||||||
|
// LOCAL HELPERS
|
||||||
|
//_________________________________________________________________
|
||||||
|
//converts all string hours and date to Date and HHmm formats
|
||||||
|
private normalizeShiftDto = async (dto: ShiftDto): Promise<Result<Normalized, string>> => {
|
||||||
|
const bank_code_id = await this.typeResolver.findBankCodeIDByType(dto.type);
|
||||||
|
if(!bank_code_id.success) return { success: false, error: 'Bank_code not found'}
|
||||||
|
|
||||||
|
const date = toDateFromString(dto.date);
|
||||||
|
const start_time = toHHmmFromString(dto.start_time);
|
||||||
|
const end_time = toHHmmFromString(dto.end_time);
|
||||||
|
|
||||||
|
return { success: true, data: {date, start_time, end_time, bank_code_id: bank_code_id.data} };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,175 @@
|
||||||
|
import { Result } from "src/common/errors/result-error.factory";
|
||||||
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
import { ShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-create.dto";
|
||||||
|
import { toDateFromString, toHHmmFromString, toStringFromHHmm, toStringFromDate, overlaps } from "src/time-and-attendance/utils/date-time.utils";
|
||||||
|
import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils";
|
||||||
|
import { shift_select } from "src/time-and-attendance/utils/selects.utils";
|
||||||
|
import { Normalized } from "src/time-and-attendance/utils/type.utils";
|
||||||
|
|
||||||
|
export class ShiftsUpdateDeleteService {
|
||||||
|
constructor(
|
||||||
|
private readonly prisma: PrismaService,
|
||||||
|
private readonly typeResolver: BankCodesResolver,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
async updateOneOrManyShifts(shifts: ShiftDto[]): Promise<Result<ShiftDto[], string>> {
|
||||||
|
try {
|
||||||
|
//verify if array is empty or not
|
||||||
|
if (!Array.isArray(shifts) || shifts.length === 0) return { success: false, error: 'No data received' };
|
||||||
|
|
||||||
|
//calls the update functions and await the return of successfull result or not
|
||||||
|
const results = await Promise.allSettled(shifts.map(shift => this.updateShift(shift)));
|
||||||
|
|
||||||
|
//return arrays of updated shifts or errors
|
||||||
|
const updated_shifts: ShiftDto[] = [];
|
||||||
|
const errors: string[] = [];
|
||||||
|
|
||||||
|
//filters results into updated_shifts or errors arrays depending on the return from "allSettled" Promise
|
||||||
|
for (const result of results) {
|
||||||
|
if (result.status === 'fulfilled') {
|
||||||
|
if (result.value.success) {
|
||||||
|
updated_shifts.push(result.value.data);
|
||||||
|
} else {
|
||||||
|
errors.push(result.value.error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
errors.push(result.reason instanceof Error ? result.reason.message : String(result.reason));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//verify if shifts were updated and returns an array of errors if needed
|
||||||
|
if (updated_shifts.length === 0) return { success: false, error: errors.join(' | ') || 'No shift updated' };
|
||||||
|
|
||||||
|
// returns array of updated shifts
|
||||||
|
return { success: true, data: updated_shifts }
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//_________________________________________________________________
|
||||||
|
// UPDATE
|
||||||
|
//_________________________________________________________________
|
||||||
|
async updateShift(dto: ShiftDto): Promise<Result<ShiftDto, string>> {
|
||||||
|
try {
|
||||||
|
//finds original shift
|
||||||
|
const original = await this.prisma.shifts.findFirst({
|
||||||
|
where: { id: dto.id },
|
||||||
|
select: shift_select,
|
||||||
|
});
|
||||||
|
if (!original) return { success: false, error: `Shift with id: ${dto.id} not found` };
|
||||||
|
|
||||||
|
//transform string format to date and HHmm
|
||||||
|
const normed_shift = await this.normalizeShiftDto(dto);
|
||||||
|
if (!normed_shift.success) return { success: false, error: 'An error occured during normalization' }
|
||||||
|
if (normed_shift.data.end_time <= normed_shift.data.start_time) return {
|
||||||
|
success: false,
|
||||||
|
error: `INVALID_SHIFT - `
|
||||||
|
+ `start_time: ${toStringFromHHmm(normed_shift.data.start_time)},`
|
||||||
|
+ `end_time: ${toStringFromHHmm(normed_shift.data.end_time)},`
|
||||||
|
+ `date: ${toStringFromDate(normed_shift.data.date)}.`
|
||||||
|
};
|
||||||
|
const overlap_check = await this.overlapChecker(normed_shift.data);
|
||||||
|
if(!overlap_check.success) return { success: false, error: 'Invalid shift, overlaps with existing shifts'}
|
||||||
|
|
||||||
|
//finds bank_code_id using the type
|
||||||
|
const bank_code = await this.typeResolver.findBankCodeIDByType(dto.type);
|
||||||
|
if (!bank_code.success) return { success: false, error: 'No bank_code_id found' };
|
||||||
|
|
||||||
|
//updates sent to DB
|
||||||
|
const updated = await this.prisma.shifts.update({
|
||||||
|
where: { id: original.id },
|
||||||
|
data: {
|
||||||
|
date: normed_shift.data.date,
|
||||||
|
start_time: normed_shift.data.start_time,
|
||||||
|
end_time: normed_shift.data.end_time,
|
||||||
|
bank_code_id: bank_code.data,
|
||||||
|
comment: dto.comment,
|
||||||
|
is_approved: dto.is_approved,
|
||||||
|
is_remote: dto.is_remote,
|
||||||
|
},
|
||||||
|
select: shift_select,
|
||||||
|
});
|
||||||
|
if (!updated) return { success: false, error: ' An error occured during update, Invalid Datas' };
|
||||||
|
|
||||||
|
// builds an object to return for display in the frontend
|
||||||
|
const shift: ShiftDto = {
|
||||||
|
id: updated.id,
|
||||||
|
timesheet_id: updated.timesheet_id,
|
||||||
|
type: dto.type,
|
||||||
|
date: toStringFromDate(updated.date),
|
||||||
|
start_time: toStringFromHHmm(updated.start_time),
|
||||||
|
end_time: toStringFromHHmm(updated.end_time),
|
||||||
|
is_approved: updated.is_approved,
|
||||||
|
is_remote: updated.is_remote,
|
||||||
|
comment: updated.comment ?? '',
|
||||||
|
}
|
||||||
|
return { success: true, data: shift };
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: `An error occured during update. Invalid Data` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//_________________________________________________________________
|
||||||
|
// DELETE
|
||||||
|
//_________________________________________________________________
|
||||||
|
//finds shifts using shit_ids
|
||||||
|
//blocs deletion if approved
|
||||||
|
async deleteShift(shift_id: number): Promise<Result<number, string>> {
|
||||||
|
try {
|
||||||
|
return await this.prisma.$transaction(async (tx) => {
|
||||||
|
const shift = await tx.shifts.findUnique({
|
||||||
|
where: { id: shift_id },
|
||||||
|
select: { id: true, date: true, timesheet_id: true },
|
||||||
|
});
|
||||||
|
if (!shift) return { success: false, error: `shift with id ${shift_id} not found ` };
|
||||||
|
|
||||||
|
await tx.shifts.delete({ where: { id: shift_id } });
|
||||||
|
return { success: true, data: shift.id };
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: `INVALID_SHIFT, shift with id ${shift_id} not found` }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//_________________________________________________________________
|
||||||
|
// helpers
|
||||||
|
//_________________________________________________________________
|
||||||
|
private normalizeShiftDto = async (dto: ShiftDto): Promise<Result<Normalized, string>> => {
|
||||||
|
const bank_code_id = await this.typeResolver.findBankCodeIDByType(dto.type);
|
||||||
|
if (!bank_code_id.success) return { success: false, error: 'Bank_code not found' }
|
||||||
|
|
||||||
|
const date = toDateFromString(dto.date);
|
||||||
|
const start_time = toHHmmFromString(dto.start_time);
|
||||||
|
const end_time = toHHmmFromString(dto.end_time);
|
||||||
|
|
||||||
|
return { success: true, data: { date, start_time, end_time, bank_code_id: bank_code_id.data } };
|
||||||
|
}
|
||||||
|
|
||||||
|
private overlapChecker = async (dto: Normalized): Promise<Result<void, string>> => {
|
||||||
|
|
||||||
|
const existing_shifts = await this.prisma.shifts.findMany({
|
||||||
|
where: { date: dto.date },
|
||||||
|
select: { id: true, date: true, start_time: true, end_time: true },
|
||||||
|
});
|
||||||
|
for (const existing of existing_shifts) {
|
||||||
|
const existing_start = toDateFromString(existing.start_time);
|
||||||
|
const existing_end = toDateFromString(existing.end_time);
|
||||||
|
const existing_date = toDateFromString(existing.date);
|
||||||
|
|
||||||
|
const has_overlap = overlaps(
|
||||||
|
{ start: dto.start_time, end: dto.end_time, date: dto.date },
|
||||||
|
{ start: existing_start, end: existing_end, date: existing_date },
|
||||||
|
);
|
||||||
|
if (has_overlap) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `SHIFT_OVERLAP`
|
||||||
|
+ `new shift: ${toStringFromHHmm(dto.start_time)}–${toStringFromHHmm(dto.end_time)} `
|
||||||
|
+ `existing shift: ${toStringFromHHmm(existing.start_time)}–${toStringFromHHmm(existing.end_time)} `
|
||||||
|
+ `date: ${toStringFromDate(dto.date)})`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { success: true, data: undefined }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,463 +0,0 @@
|
||||||
import { CreateShiftResult, NormedOk, UpdateShiftResult, Normalized } from "src/time-and-attendance/utils/type.utils";
|
|
||||||
import { overlaps, toStringFromHHmm, toStringFromDate, toDateFromString, toHHmmFromString } from "src/time-and-attendance/utils/date-time.utils";
|
|
||||||
import { Injectable, BadRequestException, ConflictException, NotFoundException } from "@nestjs/common";
|
|
||||||
import { shift_select, timesheet_select } from "src/time-and-attendance/utils/selects.utils";
|
|
||||||
import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils";
|
|
||||||
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
|
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
|
||||||
import { GetShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-get.dto";
|
|
||||||
import { ShiftEntity } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-entity.dto";
|
|
||||||
import { ShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-create.dto";
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class ShiftsUpsertService {
|
|
||||||
constructor(
|
|
||||||
private readonly prisma: PrismaService,
|
|
||||||
private readonly emailResolver: EmailToIdResolver,
|
|
||||||
private readonly typeResolver: BankCodesResolver,
|
|
||||||
) { }
|
|
||||||
|
|
||||||
//_________________________________________________________________
|
|
||||||
// CREATE
|
|
||||||
//_________________________________________________________________
|
|
||||||
//normalized frontend data to match DB
|
|
||||||
//loads all shifts from a selected day to check for overlaping shifts
|
|
||||||
//checks for overlaping shifts
|
|
||||||
//create new shifts
|
|
||||||
async createShifts(email: string, dtos: ShiftDto[]): Promise<CreateShiftResult[]> {
|
|
||||||
if (!Array.isArray(dtos) || dtos.length === 0) return [];
|
|
||||||
|
|
||||||
const employee_id = await this.emailResolver.findIdByEmail(email);
|
|
||||||
const results: CreateShiftResult[] = [];
|
|
||||||
const normed_shifts: (NormedOk | undefined)[] = await Promise.all(dtos.map(async (dto, index) => {
|
|
||||||
try {
|
|
||||||
const normed = await this.normalizeShiftDto(dto);
|
|
||||||
if (normed.end_time <= normed.start_time) {
|
|
||||||
const error = {
|
|
||||||
error_code: 'SHIFT_OVERLAP',
|
|
||||||
conflicts: {
|
|
||||||
start_time: toStringFromHHmm(normed.start_time),
|
|
||||||
end_time: toStringFromHHmm(normed.end_time),
|
|
||||||
date: toStringFromDate(normed.date),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
results.push({ ok: false, error });
|
|
||||||
}
|
|
||||||
|
|
||||||
const timesheet = await this.prisma.timesheets.findUnique({
|
|
||||||
where: { id: dto.timesheet_id, employee_id },
|
|
||||||
select: timesheet_select,
|
|
||||||
});
|
|
||||||
if (!timesheet) {
|
|
||||||
const error = {
|
|
||||||
error_code: 'INVALID_TIMESHEET',
|
|
||||||
conflicts: {
|
|
||||||
start_time: toStringFromHHmm(normed.start_time),
|
|
||||||
end_time: toStringFromHHmm(normed.end_time),
|
|
||||||
date: toStringFromDate(normed.date),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
results.push({ ok: false, error });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const bank_code = await this.typeResolver.findBankCodeIDByType(dto.type);
|
|
||||||
const date = await toDateFromString(dto.date);
|
|
||||||
const start_time = await toHHmmFromString(dto.start_time);
|
|
||||||
const end_time = await toHHmmFromString(dto.end_time);
|
|
||||||
const entity: ShiftEntity = {
|
|
||||||
timesheet_id: timesheet.id,
|
|
||||||
bank_code_id: bank_code.id,
|
|
||||||
date,
|
|
||||||
start_time,
|
|
||||||
end_time,
|
|
||||||
id: dto.id,
|
|
||||||
is_approved: dto.is_approved,
|
|
||||||
is_remote: dto.is_remote,
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
index,
|
|
||||||
dto: entity,
|
|
||||||
normed,
|
|
||||||
timesheet_id: timesheet.id,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
results.push({ ok: false, error });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
}));
|
|
||||||
|
|
||||||
const ok_items = normed_shifts.filter((item) => item !== undefined);
|
|
||||||
|
|
||||||
|
|
||||||
const regroup_by_date = new Map<string, number[]>();
|
|
||||||
ok_items.forEach(({ index, normed, timesheet_id }) => {
|
|
||||||
const day = new Date(normed.date.getUTCFullYear(), normed.date.getUTCMonth(), normed.date.getUTCDate()).getTime();
|
|
||||||
const key = `${timesheet_id}|${day}`;
|
|
||||||
if (!regroup_by_date.has(key)) regroup_by_date.set(key, []);
|
|
||||||
regroup_by_date.get(key)!.push(index);
|
|
||||||
});
|
|
||||||
|
|
||||||
const timesheet_keys = Array.from(regroup_by_date.keys()).map((raw) => {
|
|
||||||
const [timesheet, day] = raw.split('|');
|
|
||||||
return {
|
|
||||||
timesheet_id: Number(timesheet),
|
|
||||||
day: Number(day),
|
|
||||||
key: raw,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const indices of regroup_by_date.values()) {
|
|
||||||
const ordered = indices
|
|
||||||
.map(index => {
|
|
||||||
const item = normed_shifts[index] as NormedOk & { timesheet_id: number };
|
|
||||||
return {
|
|
||||||
index: index,
|
|
||||||
start: item.normed.start_time,
|
|
||||||
end: item.normed.end_time,
|
|
||||||
date: item.normed.date,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.sort((a, b) => a.start.getTime() - b.start.getTime());
|
|
||||||
|
|
||||||
for (let j = 1; j < ordered.length; j++) {
|
|
||||||
if (
|
|
||||||
overlaps(
|
|
||||||
{ start: ordered[j - 1].start, end: ordered[j - 1].end, date: ordered[j - 1].date },
|
|
||||||
{ start: ordered[j].start, end: ordered[j].end, date: ordered[j].date },
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
const error = new ConflictException({
|
|
||||||
error_code: 'SHIFT_OVERLAP',
|
|
||||||
conflicts: {
|
|
||||||
start_time: toStringFromHHmm(ordered[j].start),
|
|
||||||
end_time: toStringFromHHmm(ordered[j].end),
|
|
||||||
date: toStringFromDate(ordered[j].date),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return dtos.map((_dto, key) =>
|
|
||||||
indices.includes(key)
|
|
||||||
? ({ ok: false, error } as CreateShiftResult)
|
|
||||||
: ({ ok: false, error }),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return this.prisma.$transaction(async (tx) => {
|
|
||||||
const results: CreateShiftResult[] = Array.from(
|
|
||||||
{ length: dtos.length },
|
|
||||||
() => ({ ok: false, error: new Error('uninitialized') }));
|
|
||||||
|
|
||||||
const existing_map = new Map<string, { start_time: Date; end_time: Date, date: Date }[]>();
|
|
||||||
|
|
||||||
for (const { timesheet_id, day, key } of timesheet_keys) {
|
|
||||||
const day_date = new Date(day);
|
|
||||||
const rows = await tx.shifts.findMany({
|
|
||||||
where: { timesheet_id, date: day_date },
|
|
||||||
select: { start_time: true, end_time: true, id: true, date: true },
|
|
||||||
});
|
|
||||||
existing_map.set(key, rows.map((row) => ({ start_time: row.start_time, end_time: row.end_time, date: row.date })));
|
|
||||||
}
|
|
||||||
|
|
||||||
ok_items.forEach((x, i) => {
|
|
||||||
if ("error" in x) results[i] = { ok: false, error: x.error };
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const item of ok_items) {
|
|
||||||
const { index, dto, normed, timesheet_id } = item;
|
|
||||||
const day_key = new Date(normed.date.getUTCFullYear(), normed.date.getUTCMonth(), normed.date.getUTCDate()).getTime();
|
|
||||||
const map_key = `${timesheet_id}|${day_key}`;
|
|
||||||
let existing = existing_map.get(map_key);
|
|
||||||
if (!existing) {
|
|
||||||
existing = [];
|
|
||||||
existing_map.set(map_key, existing);
|
|
||||||
}
|
|
||||||
const hit = existing.find(exist => overlaps({ start: exist.start_time, end: exist.end_time, date: exist.date },
|
|
||||||
{ start: normed.start_time, end: normed.end_time, date: normed.date }));
|
|
||||||
if (hit) {
|
|
||||||
results[index] = {
|
|
||||||
ok: false,
|
|
||||||
error: {
|
|
||||||
error_code: 'SHIFT_OVERLAP',
|
|
||||||
conflicts: {
|
|
||||||
start_time: toStringFromHHmm(hit.start_time),
|
|
||||||
end_time: toStringFromHHmm(hit.end_time),
|
|
||||||
date: toStringFromDate(hit.date),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const row = await tx.shifts.create({
|
|
||||||
data: {
|
|
||||||
timesheet_id: timesheet_id,
|
|
||||||
bank_code_id: normed.bank_code_id,
|
|
||||||
date: normed.date,
|
|
||||||
start_time: normed.start_time,
|
|
||||||
end_time: normed.end_time,
|
|
||||||
is_remote: dto.is_remote,
|
|
||||||
comment: dto.comment ?? undefined,
|
|
||||||
},
|
|
||||||
select: shift_select,
|
|
||||||
});
|
|
||||||
const normalizeHHmm = (value: Date) => toHHmmFromString(toStringFromHHmm(value));
|
|
||||||
const normalized_row = {
|
|
||||||
start_time: normalizeHHmm(row.start_time),
|
|
||||||
end_time: normalizeHHmm(row.end_time),
|
|
||||||
date: toDateFromString(row.date),
|
|
||||||
};
|
|
||||||
existing.push(normalized_row);
|
|
||||||
existing_map.set(map_key, existing);
|
|
||||||
|
|
||||||
|
|
||||||
const { type: bank_type } = await this.typeResolver.findTypeByBankCodeId(row.bank_code_id);
|
|
||||||
|
|
||||||
const shift: GetShiftDto = {
|
|
||||||
shift_id: row.id,
|
|
||||||
timesheet_id: timesheet_id,
|
|
||||||
type: bank_type,
|
|
||||||
date: toStringFromDate(row.date),
|
|
||||||
start_time: toStringFromHHmm(row.start_time),
|
|
||||||
end_time: toStringFromHHmm(row.end_time),
|
|
||||||
is_remote: row.is_remote,
|
|
||||||
is_approved: false,
|
|
||||||
comment: row.comment ?? undefined,
|
|
||||||
};
|
|
||||||
results[index] = { ok: true, data: shift };
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//_________________________________________________________________
|
|
||||||
// UPDATE
|
|
||||||
//_________________________________________________________________
|
|
||||||
// finds existing shifts in DB
|
|
||||||
// verify if shifts are already approved
|
|
||||||
// normalized Date and Time format to string
|
|
||||||
// check for valid start and end times
|
|
||||||
// check for overlaping possibility
|
|
||||||
// buil a set of data to manipulate modified data only
|
|
||||||
// update shifts in DB
|
|
||||||
// return an updated version to display
|
|
||||||
async updateShifts(dtos: ShiftDto[]): Promise<UpdateShiftResult[]> {
|
|
||||||
if (!Array.isArray(dtos) || dtos.length === 0) throw new BadRequestException({ error_code: 'SHIFT_MISSING' });
|
|
||||||
|
|
||||||
const updates: ShiftEntity[] = await Promise.all(dtos.map(async (item) => {
|
|
||||||
try {
|
|
||||||
const date = await toDateFromString(item.date);
|
|
||||||
const start_time = await toHHmmFromString(item.start_time);
|
|
||||||
const end_time = await toHHmmFromString(item.end_time);
|
|
||||||
const bank_code = await this.typeResolver.findBankCodeIDByType(item.type);
|
|
||||||
return {
|
|
||||||
id: item.id,
|
|
||||||
timesheet_id: item.timesheet_id,
|
|
||||||
bank_code_id: bank_code.id,
|
|
||||||
date,
|
|
||||||
start_time,
|
|
||||||
end_time,
|
|
||||||
is_remote: item.is_remote,
|
|
||||||
is_approved: item.is_approved,
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
throw new BadRequestException('INVALID_SHIFT');
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
return this.prisma.$transaction(async (tx) => {
|
|
||||||
|
|
||||||
const shift_ids = updates.map(update_shift => update_shift.id);
|
|
||||||
const rows = await tx.shifts.findMany({
|
|
||||||
where: { id: { in: shift_ids } },
|
|
||||||
select: shift_select,
|
|
||||||
});
|
|
||||||
const regroup_id = new Map(rows.map(r => [r.id, r]));
|
|
||||||
|
|
||||||
for (const update of updates) {
|
|
||||||
const existing = regroup_id.get(update.id);
|
|
||||||
if (!existing) {
|
|
||||||
return updates.map(exist => exist.id === update.id
|
|
||||||
? ({ ok: false, id: update.id, error: new NotFoundException({ error_code: 'SHIFT_MISSING' }) } as UpdateShiftResult)
|
|
||||||
: ({ ok: false, id: exist.id, error: new BadRequestException({ error_code: 'SHIFT_INVALID' }) })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (existing.is_approved) {
|
|
||||||
return updates.map(exist => exist.id === update.id
|
|
||||||
? ({ ok: false, id: update.id, error: new BadRequestException({ error_code: 'SHIFT_INVALID' }) } as UpdateShiftResult)
|
|
||||||
: ({ ok: false, id: exist.id, error: new BadRequestException({ error_code: 'SHIFT_INVALID' }) })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const planned_updates = updates.map(update => {
|
|
||||||
const exist_shift = regroup_id.get(update.id)!;
|
|
||||||
const normed: Normalized = {
|
|
||||||
date: update.date,
|
|
||||||
start_time: update.start_time,
|
|
||||||
end_time: update.end_time,
|
|
||||||
bank_code_id: update.bank_code_id,
|
|
||||||
};
|
|
||||||
return { update, exist_shift, normed };
|
|
||||||
});
|
|
||||||
|
|
||||||
const groups = new Map<string, { existing: { start: Date; end: Date; id: number; date: Date; }[], incoming: typeof planned_updates }>();
|
|
||||||
function key(timesheet: number, d: Date) {
|
|
||||||
const day_date = new Date(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
|
|
||||||
return `${timesheet}|${day_date.getTime()}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const unique_pairs = new Map<string, { timesheet_id: number; date: Date }>();
|
|
||||||
for (const { exist_shift, normed } of planned_updates) {
|
|
||||||
unique_pairs.set(key(exist_shift.timesheet_id, exist_shift.date), { timesheet_id: exist_shift.timesheet_id, date: exist_shift.date });
|
|
||||||
unique_pairs.set(key(exist_shift.timesheet_id, normed.date), { timesheet_id: exist_shift.timesheet_id, date: normed.date });
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const group of unique_pairs.values()) {
|
|
||||||
const day_date = new Date(group.date.getUTCFullYear(), group.date.getUTCMonth(), group.date.getUTCDate());
|
|
||||||
const existing = await tx.shifts.findMany({
|
|
||||||
where: { timesheet_id: group.timesheet_id, date: day_date },
|
|
||||||
select: { id: true, start_time: true, end_time: true, date: true },
|
|
||||||
});
|
|
||||||
groups.set(key(group.timesheet_id, day_date), {
|
|
||||||
existing: existing.map(row => ({
|
|
||||||
id: row.id,
|
|
||||||
start: row.start_time,
|
|
||||||
end: row.end_time,
|
|
||||||
date: row.date,
|
|
||||||
})), incoming: planned_updates
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const planned of planned_updates) {
|
|
||||||
const keys = key(planned.exist_shift.timesheet_id, planned.normed.date);
|
|
||||||
const group = groups.get(keys)!;
|
|
||||||
|
|
||||||
const conflict = group.existing.find(row =>
|
|
||||||
row.id !== planned.exist_shift.id && overlaps({ start: row.start, end: row.end, date: row.date },
|
|
||||||
{ start: planned.normed.start_time, end: planned.normed.end_time, date: planned.normed.date })
|
|
||||||
);
|
|
||||||
if (conflict) {
|
|
||||||
return updates.map(exist => exist.id === planned.exist_shift.id
|
|
||||||
? ({
|
|
||||||
ok: false, id: exist.id, error: {
|
|
||||||
error_code: 'SHIFT_OVERLAP',
|
|
||||||
conflicts: {
|
|
||||||
start_time: toStringFromHHmm(conflict.start),
|
|
||||||
end_time: toStringFromHHmm(conflict.end),
|
|
||||||
date: toStringFromDate(conflict.date),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
} as UpdateShiftResult)
|
|
||||||
: ({ ok: false, id: exist.id, error: new BadRequestException('Batch aborted due to overlap in another update') })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const regoup_by_day = new Map<string, { id: number; start: Date; end: Date; date: Date }[]>();
|
|
||||||
for (const planned of planned_updates) {
|
|
||||||
const keys = key(planned.exist_shift.timesheet_id, planned.normed.date);
|
|
||||||
if (!regoup_by_day.has(keys)) regoup_by_day.set(keys, []);
|
|
||||||
regoup_by_day.get(keys)!.push({
|
|
||||||
id: planned.exist_shift.id,
|
|
||||||
start: planned.normed.start_time,
|
|
||||||
end: planned.normed.end_time,
|
|
||||||
date: planned.normed.date
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const arr of regoup_by_day.values()) {
|
|
||||||
arr.sort((a, b) => a.start.getTime() - b.start.getTime());
|
|
||||||
for (let i = 1; i < arr.length; i++) {
|
|
||||||
if (overlaps(
|
|
||||||
{ start: arr[i - 1].start, end: arr[i - 1].end, date: arr[i - 1].date },
|
|
||||||
{ start: arr[i].start, end: arr[i].end, date: arr[i].date })
|
|
||||||
) {
|
|
||||||
const error = {
|
|
||||||
error_code: 'SHIFT_OVERLAP',
|
|
||||||
conflicts: {
|
|
||||||
start_time: toStringFromHHmm(arr[i].start),
|
|
||||||
end_time: toStringFromHHmm(arr[i].end),
|
|
||||||
date: toStringFromDate(arr[i].date),
|
|
||||||
},
|
|
||||||
|
|
||||||
};
|
|
||||||
return updates.map(exist => ({ ok: false, id: exist.id, error: error }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const results: UpdateShiftResult[] = [];
|
|
||||||
for (const planned of planned_updates) {
|
|
||||||
try {
|
|
||||||
const data: Partial<ShiftEntity> = {
|
|
||||||
bank_code_id: planned.normed.bank_code_id,
|
|
||||||
date: planned.normed.date,
|
|
||||||
start_time: planned.normed.start_time,
|
|
||||||
end_time: planned.normed.end_time,
|
|
||||||
is_remote: planned.update.is_remote,
|
|
||||||
is_approved: planned.exist_shift.is_approved,
|
|
||||||
comment: planned.update.comment,
|
|
||||||
};
|
|
||||||
|
|
||||||
const row = await tx.shifts.update({
|
|
||||||
where: { id: planned.exist_shift.id },
|
|
||||||
data,
|
|
||||||
select: shift_select,
|
|
||||||
});
|
|
||||||
|
|
||||||
const type = await this.typeResolver.findTypeByBankCodeId(row.bank_code_id);
|
|
||||||
|
|
||||||
const dto: GetShiftDto = {
|
|
||||||
shift_id: row.id,
|
|
||||||
timesheet_id: row.timesheet_id,
|
|
||||||
type: type.type,
|
|
||||||
date: toStringFromDate(row.date),
|
|
||||||
start_time: toStringFromHHmm(row.start_time),
|
|
||||||
end_time: toStringFromHHmm(row.end_time),
|
|
||||||
is_approved: row.is_approved,
|
|
||||||
is_remote: row.is_remote,
|
|
||||||
comment: row.comment ?? undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
results.push({ ok: true, id: planned.exist_shift.id, data: dto });
|
|
||||||
} catch (error) {
|
|
||||||
throw new BadRequestException('INVALID_SHIFT');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//_________________________________________________________________
|
|
||||||
// DELETE
|
|
||||||
//_________________________________________________________________
|
|
||||||
//finds shifts using shit_ids
|
|
||||||
//blocs deletion if approved
|
|
||||||
async deleteShift(shift_id: number) {
|
|
||||||
return await this.prisma.$transaction(async (tx) => {
|
|
||||||
const shift = await tx.shifts.findUnique({
|
|
||||||
where: { id: shift_id },
|
|
||||||
select: { id: true, date: true, timesheet_id: true },
|
|
||||||
});
|
|
||||||
if (!shift) throw new ConflictException({ error_code: 'SHIFT_INVALID' });
|
|
||||||
|
|
||||||
await tx.shifts.delete({ where: { id: shift_id } });
|
|
||||||
return { success: true };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//_________________________________________________________________
|
|
||||||
// LOCAL HELPERS
|
|
||||||
//_________________________________________________________________
|
|
||||||
//converts all string hours and date to Date and HHmm formats
|
|
||||||
private normalizeShiftDto = async (dto: ShiftDto): Promise<Normalized> => {
|
|
||||||
const { id: bank_code_id } = await this.typeResolver.findBankCodeIDByType(dto.type);
|
|
||||||
const date = toDateFromString(dto.date);
|
|
||||||
const start_time = toHHmmFromString(dto.start_time);
|
|
||||||
const end_time = toHHmmFromString(dto.end_time);
|
|
||||||
return { date, start_time, end_time, bank_code_id: bank_code_id };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { ShiftController } from 'src/time-and-attendance/time-tracker/shifts/controllers/shift.controller';
|
import { ShiftController } from 'src/time-and-attendance/time-tracker/shifts/controllers/shift.controller';
|
||||||
import { ShiftsUpsertService } from 'src/time-and-attendance/time-tracker/shifts/services/shifts-upsert.service';
|
import { ShiftsCreateService } from 'src/time-and-attendance/time-tracker/shifts/services/shifts-create.service';
|
||||||
|
import { ShiftsUpdateDeleteService } from 'src/time-and-attendance/time-tracker/shifts/services/shifts-update-delete.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [ShiftController],
|
controllers: [ShiftController],
|
||||||
providers: [ ShiftsUpsertService ],
|
providers: [ ShiftsCreateService, ShiftsUpdateDeleteService ],
|
||||||
exports: [ ShiftsUpsertService ],
|
exports: [ ShiftsCreateService, ShiftsUpdateDeleteService ],
|
||||||
})
|
})
|
||||||
export class ShiftsModule {}
|
export class ShiftsModule {}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,27 @@
|
||||||
import { sevenDaysFrom, toStringFromDate, toHHmmFromDate, toDateFromString } from "src/time-and-attendance/utils/date-time.utils";
|
import { sevenDaysFrom, toStringFromDate, toHHmmFromDate, toDateFromString } from "src/time-and-attendance/utils/date-time.utils";
|
||||||
import { NUMBER_OF_TIMESHEETS_TO_RETURN } from "src/time-and-attendance/utils/constants.utils";
|
import { NUMBER_OF_TIMESHEETS_TO_RETURN } from "src/time-and-attendance/utils/constants.utils";
|
||||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
import { TotalExpenses, TotalHours } from "src/time-and-attendance/utils/type.utils";
|
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
|
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
|
||||||
|
import { Timesheets } from "src/time-and-attendance/time-tracker/timesheets/dtos/timesheet.dto";
|
||||||
|
import { Result } from "src/common/errors/result-error.factory";
|
||||||
|
|
||||||
|
export type TotalHours = {
|
||||||
|
regular: number;
|
||||||
|
evening: number;
|
||||||
|
emergency: number;
|
||||||
|
overtime: number;
|
||||||
|
vacation: number;
|
||||||
|
holiday: number;
|
||||||
|
sick: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TotalExpenses = {
|
||||||
|
expenses: number;
|
||||||
|
per_diem: number;
|
||||||
|
on_call: number;
|
||||||
|
mileage: number;
|
||||||
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GetTimesheetsOverviewService {
|
export class GetTimesheetsOverviewService {
|
||||||
|
|
@ -15,13 +33,15 @@ export class GetTimesheetsOverviewService {
|
||||||
//-----------------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------------
|
||||||
// GET TIMESHEETS FOR A SELECTED EMPLOYEE
|
// GET TIMESHEETS FOR A SELECTED EMPLOYEE
|
||||||
//-----------------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------------
|
||||||
async getTimesheetsForEmployeeByPeriod(email: string, pay_year: number, pay_period_no: number) {
|
async getTimesheetsForEmployeeByPeriod(email: string, pay_year: number, pay_period_no: number): Promise<Result<Timesheets, string>> {
|
||||||
//find period using year and period_no
|
try { //find period using year and period_no
|
||||||
const period = await this.prisma.payPeriods.findFirst({ where: { pay_year, pay_period_no } });
|
const period = await this.prisma.payPeriods.findFirst({ where: { pay_year, pay_period_no } });
|
||||||
if (!period) throw new NotFoundException(`Pay period ${pay_year}-${pay_period_no} not found`);
|
if (!period) return { success: false, error: `Pay period ${pay_year}-${pay_period_no} not found`};
|
||||||
|
|
||||||
//fetch the employee_id using the email
|
//fetch the employee_id using the email
|
||||||
const employee_id = await this.emailResolver.findIdByEmail(email);
|
const employee_id = await this.emailResolver.findIdByEmail(email);
|
||||||
|
if (!employee_id.success) return { success: false, error: employee_id.error }
|
||||||
|
|
||||||
//loads the timesheets related to the fetched pay-period
|
//loads the timesheets related to the fetched pay-period
|
||||||
const timesheet_range = { employee_id, start_date: { gte: period.period_start, lte: period.period_end } };
|
const timesheet_range = { employee_id, start_date: { gte: period.period_start, lte: period.period_end } };
|
||||||
let rows = await this.loadTimesheets(timesheet_range);
|
let rows = await this.loadTimesheets(timesheet_range);
|
||||||
|
|
@ -40,27 +60,30 @@ export class GetTimesheetsOverviewService {
|
||||||
const exists = rows.some(
|
const exists = rows.some(
|
||||||
(row) => toDateFromString(row.start_date).getTime() === week_start.getTime()
|
(row) => toDateFromString(row.start_date).getTime() === week_start.getTime()
|
||||||
);
|
);
|
||||||
if (!exists) await this.ensureTimesheet(employee_id, week_start);
|
if (!exists) await this.ensureTimesheet(employee_id.data, week_start);
|
||||||
}
|
}
|
||||||
rows = await this.loadTimesheets(timesheet_range);
|
rows = await this.loadTimesheets(timesheet_range);
|
||||||
|
|
||||||
|
|
||||||
//find user infos using the employee_id
|
//find user infos using the employee_id
|
||||||
const employee = await this.prisma.employees.findUnique({
|
const employee = await this.prisma.employees.findUnique({
|
||||||
where: { id: employee_id },
|
where: { id: employee_id.data },
|
||||||
include: { user: true },
|
include: { user: true },
|
||||||
});
|
});
|
||||||
if (!employee) throw new NotFoundException(`Employee #${employee_id} not found`);
|
if (!employee) return { success: false, error:`Employee #${employee_id} not found`}
|
||||||
|
|
||||||
//builds employee full name
|
//builds employee full name
|
||||||
const user = employee.user;
|
const user = employee.user;
|
||||||
const employee_fullname = `${user.first_name} ${user.last_name}`.trim();
|
const employee_fullname = `${user.first_name} ${user.last_name}`.trim();
|
||||||
|
|
||||||
//maps all timesheet's infos
|
//maps all timesheet's infos
|
||||||
const timesheets = rows.map((timesheet) => this.mapOneTimesheet(timesheet));
|
const timesheets = await Promise.all(rows.map((timesheet) => this.mapOneTimesheet(timesheet)));
|
||||||
return { employee_fullname, timesheets };
|
|
||||||
}
|
|
||||||
|
|
||||||
|
return { success: true, data:{ employee_fullname, timesheets} };
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//-----------------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------------
|
||||||
// MAPPERS & HELPERS
|
// MAPPERS & HELPERS
|
||||||
|
|
@ -104,11 +127,14 @@ export class GetTimesheetsOverviewService {
|
||||||
const weekly_hours: TotalHours[] = [emptyHours()];
|
const weekly_hours: TotalHours[] = [emptyHours()];
|
||||||
const weekly_expenses: TotalExpenses[] = [emptyExpenses()];
|
const weekly_expenses: TotalExpenses[] = [emptyExpenses()];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//map of days
|
//map of days
|
||||||
const days = day_dates.map((date) => {
|
const days = day_dates.map((date) => {
|
||||||
const date_iso = toStringFromDate(date);
|
const date_iso = toStringFromDate(date);
|
||||||
const shifts_source = shifts_by_date.get(date_iso) ?? [];
|
const shifts_source = shifts_by_date.get(date_iso) ?? [];
|
||||||
const expenses_source = expenses_by_date.get(date_iso) ?? [];
|
const expenses_source = expenses_by_date.get(date_iso) ?? [];
|
||||||
|
|
||||||
//inner map of shifts
|
//inner map of shifts
|
||||||
const shifts = shifts_source.map((shift) => ({
|
const shifts = shifts_source.map((shift) => ({
|
||||||
timesheet_id: shift.timesheet_id,
|
timesheet_id: shift.timesheet_id,
|
||||||
|
|
@ -132,6 +158,7 @@ export class GetTimesheetsOverviewService {
|
||||||
is_approved: expense.is_approved ?? false,
|
is_approved: expense.is_approved ?? false,
|
||||||
comment: expense.comment ?? '',
|
comment: expense.comment ?? '',
|
||||||
supervisor_comment: expense.supervisor_comment,
|
supervisor_comment: expense.supervisor_comment,
|
||||||
|
type: expense.type,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
//daily totals
|
//daily totals
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ export const toHHmmFromDate = (input: Date | string): string => {
|
||||||
return `${hh}:${mm}`;
|
return `${hh}:${mm}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
//converts Date format to string
|
//converts to Date format from string
|
||||||
export const toDateFromString = (ymd: string | Date): Date => {
|
export const toDateFromString = (ymd: string | Date): Date => {
|
||||||
return new Date(ymd);
|
return new Date(ymd);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||||
import { Prisma, PrismaClient } from "@prisma/client";
|
import { Prisma, PrismaClient } from "@prisma/client";
|
||||||
|
import { Result } from "src/common/errors/result-error.factory";
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
|
||||||
type Tx = Prisma.TransactionClient | PrismaClient;
|
type Tx = Prisma.TransactionClient | PrismaClient;
|
||||||
|
|
@ -10,35 +11,37 @@ export class BankCodesResolver {
|
||||||
|
|
||||||
//find id and modifier by type
|
//find id and modifier by type
|
||||||
readonly findIdAndModifierByType = async (type: string, client?: Tx
|
readonly findIdAndModifierByType = async (type: string, client?: Tx
|
||||||
): Promise<{id:number; modifier: number }> => {
|
): Promise<Result<{ id: number; modifier: number }, string>> => {
|
||||||
const db = client ?? this.prisma;
|
const db = client ?? this.prisma;
|
||||||
const bank = await db.bankCodes.findFirst({
|
const bank = await db.bankCodes.findFirst({
|
||||||
where: { type },
|
where: { type },
|
||||||
select: { id: true, modifier: true },
|
select: { id: true, modifier: true },
|
||||||
});
|
});
|
||||||
|
if (!bank) return { success: false, error: `Unknown bank code type: ${type}` };
|
||||||
|
|
||||||
if(!bank) throw new NotFoundException(`Unknown bank code type: ${type}`);
|
return { success: true, data: { id: bank.id, modifier: bank.modifier } };
|
||||||
return { id: bank.id, modifier: bank.modifier };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
//finds only id by type
|
//finds only id by type
|
||||||
readonly findBankCodeIDByType = async (type: string, client?: Tx) => {
|
readonly findBankCodeIDByType = async (type: string, client?: Tx): Promise<Result<number, string>> => {
|
||||||
const db = client ?? this.prisma;
|
const db = client ?? this.prisma;
|
||||||
const bank_code_id = await db.bankCodes.findFirst({
|
const bank_code = await db.bankCodes.findFirst({
|
||||||
where: { type },
|
where: { type },
|
||||||
select: { id: true },
|
select: { id: true },
|
||||||
});
|
});
|
||||||
if(!bank_code_id) throw new NotFoundException(`Unkown bank type: ${type}`);
|
if (!bank_code) return { success: false, error:`Unkown bank type: ${type}`};
|
||||||
return bank_code_id;
|
|
||||||
|
return { success: true, data: bank_code.id};
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly findTypeByBankCodeId = async (bank_code_id: number, client?: Tx) => {
|
readonly findTypeByBankCodeId = async (bank_code_id: number, client?: Tx): Promise<Result<string, string>> => {
|
||||||
const db = client ?? this.prisma;
|
const db = client ?? this.prisma;
|
||||||
const type = await db.bankCodes.findFirst({
|
const bank_code = await db.bankCodes.findFirst({
|
||||||
where: { id: bank_code_id },
|
where: { id: bank_code_id },
|
||||||
select: { type: true },
|
select: { type: true },
|
||||||
});
|
});
|
||||||
if(!type) throw new NotFoundException(`Type with id : ${bank_code_id} not found`);
|
if (!bank_code) return {success: false, error: `Type with id : ${bank_code_id} not found` }
|
||||||
return type;
|
|
||||||
|
return {success: true, data: bank_code.type};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
|
||||||
import { Prisma, PrismaClient } from "@prisma/client";
|
import { Prisma, PrismaClient } from "@prisma/client";
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { Result } from "src/common/errors/result-error.factory";
|
||||||
|
|
||||||
type Tx = Prisma.TransactionClient | PrismaClient;
|
type Tx = Prisma.TransactionClient | PrismaClient;
|
||||||
|
|
||||||
|
|
@ -10,26 +11,25 @@ export class EmailToIdResolver {
|
||||||
constructor(private readonly prisma: PrismaService) { }
|
constructor(private readonly prisma: PrismaService) { }
|
||||||
|
|
||||||
// find employee_id using email
|
// find employee_id using email
|
||||||
readonly findIdByEmail = async ( email: string, client?: Tx
|
readonly findIdByEmail = async (email: string, client?: Tx): Promise<Result<number, string>> => {
|
||||||
): Promise<number> => {
|
|
||||||
const db = client ?? this.prisma;
|
const db = client ?? this.prisma;
|
||||||
const employee = await db.employees.findFirst({
|
const employee = await db.employees.findFirst({
|
||||||
where: { user: { email } },
|
where: { user: { email } },
|
||||||
select: { id: true },
|
select: { id: true },
|
||||||
});
|
});
|
||||||
if(!employee)throw new NotFoundException(`Employee with email: ${email} not found`);
|
if (!employee) return { success: false, error: `Employee with email:${email} not found` };
|
||||||
return employee.id;
|
return { data: employee.id, success: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
// find user_id using email
|
// find user_id using email
|
||||||
readonly resolveUserIdWithEmail = async (email: string, client?: Tx
|
readonly resolveUserIdWithEmail = async (email: string, client?: Tx
|
||||||
): Promise<string> => {
|
): Promise<Result<string, string>> => {
|
||||||
const db = client ?? this.prisma;
|
const db = client ?? this.prisma;
|
||||||
const user = await db.users.findFirst({
|
const user = await db.users.findFirst({
|
||||||
where: { email },
|
where: { email },
|
||||||
select: { id: true },
|
select: { id: true },
|
||||||
});
|
});
|
||||||
if(!user) throw new NotFoundException(`User with email ${ email } not found`);
|
if (!user) return { success: false, error: `User with email:${email} not found` };
|
||||||
return user.id;
|
return { success: true, data: user.id };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
import { Prisma, PrismaClient } from "@prisma/client";
|
import { Prisma, PrismaClient } from "@prisma/client";
|
||||||
|
import { Result } from "src/common/errors/result-error.factory";
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
|
||||||
type Tx = Prisma.TransactionClient | PrismaClient;
|
type Tx = Prisma.TransactionClient | PrismaClient;
|
||||||
|
|
@ -8,15 +9,15 @@ type Tx = Prisma.TransactionClient | PrismaClient;
|
||||||
export class FullNameResolver {
|
export class FullNameResolver {
|
||||||
constructor(private readonly prisma: PrismaService){}
|
constructor(private readonly prisma: PrismaService){}
|
||||||
|
|
||||||
readonly resolveFullName = async (employee_id: number, client?: Tx): Promise<string> =>{
|
readonly resolveFullName = async (employee_id: number, client?: Tx): Promise<Result<string, string>> =>{
|
||||||
const db = client ?? this.prisma;
|
const db = client ?? this.prisma;
|
||||||
const employee = await db.employees.findUnique({
|
const employee = await db.employees.findUnique({
|
||||||
where: { id: employee_id },
|
where: { id: employee_id },
|
||||||
select: { user: { select: {first_name: true, last_name: true} } },
|
select: { user: { select: {first_name: true, last_name: true} } },
|
||||||
});
|
});
|
||||||
if(!employee) throw new NotFoundException(`Unknown user with name: ${employee_id}`)
|
if(!employee) return { success: false, error: `Unknown user with id ${employee_id}`}
|
||||||
|
|
||||||
const full_name = ( employee.user.first_name + " " + employee.user.last_name ) || " ";
|
const full_name = ( employee.user.first_name + " " + employee.user.last_name ) || " ";
|
||||||
return full_name;
|
return {success: true, data: full_name };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,14 +1,23 @@
|
||||||
import { Prisma, PrismaClient } from "@prisma/client";
|
import { Prisma, PrismaClient } from "@prisma/client";
|
||||||
import { NotFoundException } from "@nestjs/common";
|
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
import { ShiftKey } from "src/time-and-attendance/utils/type.utils";
|
import { Result } from "src/common/errors/result-error.factory";
|
||||||
|
|
||||||
type Tx = Prisma.TransactionClient | PrismaClient;
|
type Tx = Prisma.TransactionClient | PrismaClient;
|
||||||
|
|
||||||
|
interface ShiftKey {
|
||||||
|
timesheet_id: number;
|
||||||
|
date: Date;
|
||||||
|
start_time: Date;
|
||||||
|
end_time: Date;
|
||||||
|
bank_code_id: number;
|
||||||
|
is_remote: boolean;
|
||||||
|
comment?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export class ShiftIdResolver {
|
export class ShiftIdResolver {
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
readonly findShiftIdByData = async ( key: ShiftKey, client?: Tx ): Promise<{id:number}> => {
|
readonly findShiftIdByData = async ( key: ShiftKey, client?: Tx ): Promise<Result<number, string>> => {
|
||||||
const db = client ?? this.prisma;
|
const db = client ?? this.prisma;
|
||||||
const shift = await db.shifts.findFirst({
|
const shift = await db.shifts.findFirst({
|
||||||
where: {
|
where: {
|
||||||
|
|
@ -23,7 +32,8 @@ export class ShiftIdResolver {
|
||||||
select: { id: true },
|
select: { id: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
if(!shift) throw new NotFoundException(`shift not found`);
|
if(!shift) return { success: false, error: `shift not found`}
|
||||||
return { id: shift.id };
|
|
||||||
|
return { success: true, data: shift.id };
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ import { Prisma, PrismaClient } from "@prisma/client";
|
||||||
import { weekStartSunday } from "src/time-and-attendance/utils/date-time.utils";
|
import { weekStartSunday } from "src/time-and-attendance/utils/date-time.utils";
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
import { EmailToIdResolver } from "./resolve-email-id.utils";
|
import { EmailToIdResolver } from "./resolve-email-id.utils";
|
||||||
|
import { Result } from "src/common/errors/result-error.factory";
|
||||||
|
|
||||||
|
|
||||||
type Tx = Prisma.TransactionClient | PrismaClient;
|
type Tx = Prisma.TransactionClient | PrismaClient;
|
||||||
|
|
@ -14,15 +15,16 @@ export class EmployeeTimesheetResolver {
|
||||||
private readonly emailResolver: EmailToIdResolver,
|
private readonly emailResolver: EmailToIdResolver,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
readonly findTimesheetIdByEmail = async (email: string, date: Date, client?: Tx): Promise<{id: number}> => {
|
readonly findTimesheetIdByEmail = async (email: string, date: Date, client?: Tx): Promise<Result<{id: number}, string>> => {
|
||||||
const db = client ?? this.prisma;
|
const db = client ?? this.prisma;
|
||||||
const employee_id = await this.emailResolver.findIdByEmail(email);
|
const employee_id = await this.emailResolver.findIdByEmail(email);
|
||||||
|
if(!employee_id.success) return { success: false, error: employee_id.error}
|
||||||
const start_date = weekStartSunday(date);
|
const start_date = weekStartSunday(date);
|
||||||
const timesheet = await db.timesheets.findFirst({
|
const timesheet = await db.timesheets.findFirst({
|
||||||
where: { employee_id : employee_id, start_date: start_date },
|
where: { employee_id : employee_id.data, start_date: start_date },
|
||||||
select: { id: true },
|
select: { id: true },
|
||||||
});
|
});
|
||||||
if(!timesheet) throw new NotFoundException(`timesheet not found`);
|
if(!timesheet) throw new NotFoundException(`timesheet not found`);
|
||||||
return { id: timesheet.id };
|
return { success: true, data: {id: timesheet.id} };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -51,36 +51,6 @@ export const leaveRequestsSelect = {
|
||||||
},
|
},
|
||||||
} satisfies Prisma.LeaveRequestsSelect;
|
} satisfies Prisma.LeaveRequestsSelect;
|
||||||
|
|
||||||
|
|
||||||
export const EXPENSE_SELECT = {
|
|
||||||
date: true,
|
|
||||||
amount: true,
|
|
||||||
mileage: true,
|
|
||||||
comment: true,
|
|
||||||
is_approved: true,
|
|
||||||
supervisor_comment: true,
|
|
||||||
bank_code: { select: { type: true } },
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const EXPENSE_ASC_ORDER = { date: 'asc' as const };
|
|
||||||
|
|
||||||
export const PAY_PERIOD_SELECT = {
|
|
||||||
period_start: true,
|
|
||||||
period_end: true,
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const SHIFT_SELECT = {
|
|
||||||
date: true,
|
|
||||||
start_time: true,
|
|
||||||
end_time: true,
|
|
||||||
comment: true,
|
|
||||||
is_approved: true,
|
|
||||||
is_remote: true,
|
|
||||||
bank_code: {select: { type: true } },
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const SHIFT_ASC_ORDER = [{date: 'asc' as const}, {start_time: 'asc' as const}];
|
|
||||||
|
|
||||||
export const timesheet_select = {
|
export const timesheet_select = {
|
||||||
id: true,
|
id: true,
|
||||||
employee_id: true,
|
employee_id: true,
|
||||||
|
|
|
||||||
|
|
@ -1,47 +1,9 @@
|
||||||
import { Prisma, PrismaClient } from "@prisma/client";
|
|
||||||
import { GetExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-get.dto";
|
|
||||||
import { SchedulePresetsDto } from "src/time-and-attendance/time-tracker/schedule-presets/dtos/create-schedule-presets.dto";
|
|
||||||
import { GetShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-get.dto";
|
|
||||||
import { ShiftEntity } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-entity.dto";
|
|
||||||
import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils";
|
|
||||||
|
|
||||||
|
|
||||||
export type TotalHours = {
|
|
||||||
regular: number;
|
|
||||||
evening: number;
|
|
||||||
emergency: number;
|
|
||||||
overtime: number;
|
|
||||||
vacation: number;
|
|
||||||
holiday: number;
|
|
||||||
sick: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TotalExpenses = {
|
|
||||||
expenses: number;
|
|
||||||
per_diem: number;
|
|
||||||
on_call: number;
|
|
||||||
mileage: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Normalized = {
|
export type Normalized = {
|
||||||
date: Date;
|
date: Date;
|
||||||
start_time: Date;
|
start_time: Date;
|
||||||
end_time: Date;
|
end_time: Date;
|
||||||
bank_code_id: number;
|
bank_code_id: number;
|
||||||
};
|
};
|
||||||
export type CreateShiftResult = { ok: true; data: GetShiftDto } | { ok: false; error: any };
|
|
||||||
export type UpdateShiftResult = { ok: true; id: number; data: GetShiftDto } | { ok: false; id: number; error: any };
|
|
||||||
export type NormedOk = {
|
|
||||||
index: number;
|
|
||||||
dto: ShiftEntity;
|
|
||||||
normed: Normalized;
|
|
||||||
timesheet_id: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type DeletePresetResult = { ok: true; id: number; } | { ok: false; id: number; error: any };
|
|
||||||
export type CreatePresetResult = { ok: true; } | { ok: false; error: any };
|
|
||||||
export type UpdatePresetResult = { ok: true; id: number; data: SchedulePresetsDto } | { ok: false; id: number; error: any };
|
|
||||||
|
|
||||||
|
|
||||||
export type NormalizedExpense = {
|
export type NormalizedExpense = {
|
||||||
date: Date;
|
date: Date;
|
||||||
|
|
@ -51,9 +13,6 @@ export type NormalizedExpense = {
|
||||||
parsed_mileage?: number;
|
parsed_mileage?: number;
|
||||||
parsed_attachment?: number;
|
parsed_attachment?: number;
|
||||||
};
|
};
|
||||||
export type CreateExpenseResult = { ok: true; data: GetExpenseDto } | { ok: false; error: any };
|
|
||||||
export type DeleteExpenseResult = { ok: true; id: number; } | { ok: false; id: number; error: any };
|
|
||||||
|
|
||||||
|
|
||||||
export type ShiftResponse = {
|
export type ShiftResponse = {
|
||||||
week_day: string;
|
week_day: string;
|
||||||
|
|
@ -76,33 +35,3 @@ export type ApplyResult = {
|
||||||
created: number;
|
created: number;
|
||||||
skipped: number;
|
skipped: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LeaveRequestRow = Prisma.LeaveRequestsGetPayload<{ select: typeof leaveRequestsSelect}>;
|
|
||||||
|
|
||||||
export type Tx = Prisma.TransactionClient | PrismaClient;
|
|
||||||
|
|
||||||
export type WeekOvertimeSummary = {
|
|
||||||
week_start:string;
|
|
||||||
week_end: string;
|
|
||||||
week_total_hours: number;
|
|
||||||
weekly_overtime: number;
|
|
||||||
daily_overtime_kept: number;
|
|
||||||
total_overtime: number;
|
|
||||||
breakdown: Array<{
|
|
||||||
date:string;
|
|
||||||
day_hours: number;
|
|
||||||
day_overtime: number;
|
|
||||||
daily_kept: number;
|
|
||||||
running_total_before: number;
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface ShiftKey {
|
|
||||||
timesheet_id: number;
|
|
||||||
date: Date;
|
|
||||||
start_time: Date;
|
|
||||||
end_time: Date;
|
|
||||||
bank_code_id: number;
|
|
||||||
is_remote: boolean;
|
|
||||||
comment?: string | null;
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user