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 { PrismaService } from "src/prisma/prisma.service";
|
||||
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 { 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.
|
||||
Un maximum de 08h00 est allouable pour le férier
|
||||
|
|
@ -12,58 +13,62 @@ import { MS_PER_WEEK } from "src/time-and-attendance/utils/constants.utils";
|
|||
|
||||
@Injectable()
|
||||
export class HolidayService {
|
||||
private readonly logger = new Logger(HolidayService.name);
|
||||
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
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);
|
||||
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> {
|
||||
const holiday_week_start = getWeekStart(holiday_date);
|
||||
const window_start = new Date(holiday_week_start.getTime() - 4 * MS_PER_WEEK);
|
||||
const window_end = new Date(holiday_week_start.getTime() - 1);
|
||||
private async computeHoursPrevious4Weeks(employee_id: number, holiday_date: Date): Promise<Result<number, string>> {
|
||||
try {
|
||||
const holiday_week_start = getWeekStart(holiday_date);
|
||||
const window_start = new Date(holiday_week_start.getTime() - 4 * MS_PER_WEEK);
|
||||
const window_end = new Date(holiday_week_start.getTime() - 1);
|
||||
|
||||
const valid_codes = ['G1', 'G43', 'G56', 'G104', 'G105', 'G700'];
|
||||
const shifts = await this.prisma.shifts.findMany({
|
||||
where: {
|
||||
timesheet: { employee_id: employee_id },
|
||||
date: { gte: window_start, lte: window_end },
|
||||
bank_code: { bank_code: { in: valid_codes } },
|
||||
},
|
||||
select: { date: true, start_time: true, end_time: true },
|
||||
});
|
||||
const valid_codes = ['G1', 'G43', 'G56', 'G104', 'G105', 'G700'];
|
||||
const shifts = await this.prisma.shifts.findMany({
|
||||
where: {
|
||||
timesheet: { employee_id: employee_id },
|
||||
date: { gte: window_start, lte: window_end },
|
||||
bank_code: { bank_code: { in: valid_codes } },
|
||||
},
|
||||
select: { date: true, start_time: true, end_time: true },
|
||||
});
|
||||
|
||||
const hours_by_week = new Map<number, number>();
|
||||
for(const shift of shifts) {
|
||||
const hours = computeHours(shift.start_time, shift.end_time);
|
||||
if(hours <= 0) continue;
|
||||
const shift_week_start = getWeekStart(shift.date);
|
||||
const key = shift_week_start.getTime();
|
||||
hours_by_week.set(key, (hours_by_week.get(key) ?? 0) + hours);
|
||||
const hours_by_week = new Map<number, number>();
|
||||
for (const shift of shifts) {
|
||||
const hours = computeHours(shift.start_time, shift.end_time);
|
||||
if (hours <= 0) continue;
|
||||
const shift_week_start = getWeekStart(shift.date);
|
||||
const key = shift_week_start.getTime();
|
||||
hours_by_week.set(key, (hours_by_week.get(key) ?? 0) + hours);
|
||||
}
|
||||
|
||||
let capped_total = 0;
|
||||
for (let offset = 1; offset <= 4; offset++) {
|
||||
const week_start = new Date(holiday_week_start.getTime() - offset * MS_PER_WEEK);
|
||||
const key = week_start.getTime();
|
||||
const weekly_hours = hours_by_week.get(key) ?? 0;
|
||||
capped_total += Math.min(weekly_hours, 40);
|
||||
}
|
||||
|
||||
const average_daily_hours = capped_total / 20;
|
||||
return { success: true, data: average_daily_hours };
|
||||
} catch (error) {
|
||||
return { success: false, error: `an error occureded during holiday calculation` }
|
||||
}
|
||||
|
||||
let capped_total = 0;
|
||||
for(let offset = 1; offset <= 4; offset++) {
|
||||
const week_start = new Date(holiday_week_start.getTime() - offset * MS_PER_WEEK);
|
||||
const key = week_start.getTime();
|
||||
const weekly_hours = hours_by_week.get(key) ?? 0;
|
||||
capped_total += Math.min(weekly_hours, 40);
|
||||
}
|
||||
|
||||
const average_daily_hours = capped_total / 20;
|
||||
return average_daily_hours;
|
||||
}
|
||||
|
||||
async calculateHolidayPay( email: string, holiday_date: Date, modifier: number): Promise<number> {
|
||||
async calculateHolidayPay(email: string, holiday_date: Date, modifier: number): Promise<Result<number, string>> {
|
||||
const average_daily_hours = await this.computeHoursPrevious4WeeksByEmail(email, holiday_date);
|
||||
const daily_rate = Math.min(average_daily_hours, 8);
|
||||
this.logger.debug(`Holiday pay calculation: cappedHoursPerDay= ${average_daily_hours.toFixed(2)}, appliedDailyRate= ${daily_rate.toFixed(2)}`);
|
||||
return daily_rate * modifier;
|
||||
if (!average_daily_hours.success) return { success: false, error: average_daily_hours.error };
|
||||
|
||||
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 { Prisma, PrismaClient } from '@prisma/client';
|
||||
import { getWeekStart, getWeekEnd, computeHours } from 'src/common/utils/date-utils';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
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()
|
||||
export class OvertimeService {
|
||||
|
||||
|
|
|
|||
|
|
@ -1,29 +1,30 @@
|
|||
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 { ExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-create.dto";
|
||||
import { RolesAllowed } from "src/common/decorators/roles.decorators";
|
||||
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')
|
||||
@RolesAllowed(...GLOBAL_CONTROLLER_ROLES)
|
||||
export class ExpenseController {
|
||||
constructor( private readonly upsert_service: ExpenseUpsertService ){}
|
||||
constructor(private readonly upsert_service: ExpenseUpsertService) { }
|
||||
|
||||
@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;
|
||||
if(!email) throw new UnauthorizedException('Unauthorized User');
|
||||
if (!email) throw new UnauthorizedException('Unauthorized User');
|
||||
return this.upsert_service.createExpense(dto, email);
|
||||
}
|
||||
|
||||
@Patch('update')
|
||||
update(@Body() dto: ExpenseDto): Promise<ExpenseDto>{
|
||||
update(@Body() dto: ExpenseDto): Promise<Result<ExpenseDto, string>> {
|
||||
return this.upsert_service.updateExpense(dto);
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 { 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 { 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 { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
|
||||
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()
|
||||
|
|
@ -19,10 +21,11 @@ export class ExpenseUpsertService {
|
|||
//_________________________________________________________________
|
||||
// CREATE
|
||||
//_________________________________________________________________
|
||||
async createExpense(dto: ExpenseDto, email: string): Promise<CreateExpenseResult> {
|
||||
async createExpense(dto: ExpenseDto, email: string): Promise<Result<GetExpenseDto, string>> {
|
||||
try {
|
||||
//fetch employee_id using req.user.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
|
||||
const normed_expense = this.normalizeAndParseExpenseDto(dto);
|
||||
|
|
@ -30,10 +33,10 @@ export class ExpenseUpsertService {
|
|||
//finds the timesheet using expense.date by finding the sunday
|
||||
const start_date = weekStartSunday(normed_expense.date);
|
||||
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 },
|
||||
});
|
||||
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
|
||||
const expense = await this.prisma.expenses.create({
|
||||
|
|
@ -46,6 +49,7 @@ export class ExpenseUpsertService {
|
|||
//return the newly created expense with id
|
||||
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
|
||||
const created: GetExpenseDto = {
|
||||
|
|
@ -56,17 +60,17 @@ export class ExpenseUpsertService {
|
|||
attachment: expense.attachment ?? undefined,
|
||||
supervisor_comment: expense.supervisor_comment ?? undefined,
|
||||
};
|
||||
return { ok: true, data: created }
|
||||
return { success: true, data: created };
|
||||
|
||||
} catch (error) {
|
||||
return { ok: false, error: error }
|
||||
return { success: false, error: `An error occured during creation. Expense not created : ` + error };
|
||||
}
|
||||
}
|
||||
|
||||
//_________________________________________________________________
|
||||
// UPDATE
|
||||
//_________________________________________________________________
|
||||
async updateExpense(dto: ExpenseDto): Promise<ExpenseDto> {
|
||||
async updateExpense(dto: ExpenseDto): Promise<Result<ExpenseDto, string>> {
|
||||
try {
|
||||
//normalize string , date format and parse numbers
|
||||
const normed_expense = this.normalizeAndParseExpenseDto(dto);
|
||||
|
|
@ -79,6 +83,7 @@ export class ExpenseUpsertService {
|
|||
bank_code_id: dto.bank_code_id,
|
||||
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
|
||||
const expense = await this.prisma.expenses.update({
|
||||
|
|
@ -86,6 +91,7 @@ export class ExpenseUpsertService {
|
|||
data,
|
||||
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
|
||||
const updated: GetExpenseDto = {
|
||||
|
|
@ -96,29 +102,29 @@ export class ExpenseUpsertService {
|
|||
attachment: expense.attachment ?? undefined,
|
||||
supervisor_comment: expense.supervisor_comment ?? undefined,
|
||||
};
|
||||
return updated;
|
||||
return { success: true, data: updated };
|
||||
} catch (error) {
|
||||
return error;
|
||||
return { success: false, error: (`Expense with id: ${dto.id} generated an error:` + error) };
|
||||
}
|
||||
}
|
||||
//_________________________________________________________________
|
||||
// DELETE
|
||||
//_________________________________________________________________
|
||||
async deleteExpense(expense_id: number): Promise<DeleteExpenseResult> {
|
||||
async deleteExpense(expense_id: number): Promise<Result<number, string>> {
|
||||
try {
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
const expense = await tx.expenses.findUnique({
|
||||
where: { id: expense_id },
|
||||
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 } });
|
||||
return { success: true };
|
||||
});
|
||||
return { ok: true, id: expense_id };
|
||||
return { success: true, data: expense_id };
|
||||
} 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 };
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -127,20 +133,20 @@ export class ExpenseUpsertService {
|
|||
//_________________________________________________________________
|
||||
//makes sure that comments are the right length the date is of Date type
|
||||
private normalizeAndParseExpenseDto(dto: ExpenseDto): NormalizedExpense {
|
||||
const parsed_attachment = this.parseOptionalNumber(dto.attachment, "attachment");
|
||||
const parsed_mileage = this.parseOptionalNumber(dto.mileage, "mileage");
|
||||
const parsed_amount = this.parseOptionalNumber(dto.amount, "amount");
|
||||
const comment = this.truncate280(dto.comment);
|
||||
const parsed_attachment = this.parseOptionalNumber(dto.attachment, "attachment");
|
||||
const parsed_mileage = this.parseOptionalNumber(dto.mileage, "mileage");
|
||||
const parsed_amount = this.parseOptionalNumber(dto.amount, "amount");
|
||||
const comment = this.truncate280(dto.comment);
|
||||
const supervisor_comment = dto.supervisor_comment && dto.supervisor_comment.trim()
|
||||
? this.truncate280(dto.supervisor_comment.trim()) : undefined;
|
||||
? this.truncate280(dto.supervisor_comment.trim()) : undefined;
|
||||
const date = toDateFromString(dto.date);
|
||||
return {
|
||||
date,
|
||||
comment,
|
||||
supervisor_comment,
|
||||
parsed_amount,
|
||||
parsed_attachment,
|
||||
parsed_mileage
|
||||
return {
|
||||
date,
|
||||
comment,
|
||||
supervisor_comment,
|
||||
parsed_amount,
|
||||
parsed_attachment,
|
||||
parsed_mileage
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
import { Body, Controller, Post } from "@nestjs/common";
|
||||
import { ApiBearerAuth, ApiTags } from "@nestjs/swagger";
|
||||
import { UpsertLeaveRequestDto } from "../dtos/upsert-leave-request.dto";
|
||||
import { LeaveRequestsService } from "../services/leave-request.service";
|
||||
// import { Body, Controller, Post } from "@nestjs/common";
|
||||
// import { ApiBearerAuth, ApiTags } from "@nestjs/swagger";
|
||||
// import { UpsertLeaveRequestDto } from "../dtos/upsert-leave-request.dto";
|
||||
// import { LeaveRequestsService } from "../services/leave-request.service";
|
||||
|
||||
@ApiTags('Leave Requests')
|
||||
@ApiBearerAuth('access-token')
|
||||
// @UseGuards()
|
||||
@Controller('leave-requests')
|
||||
export class LeaveRequestController {
|
||||
constructor(private readonly leave_service: LeaveRequestsService){}
|
||||
// @ApiTags('Leave Requests')
|
||||
// @ApiBearerAuth('access-token')
|
||||
// // @UseGuards()
|
||||
// @Controller('leave-requests')
|
||||
// export class LeaveRequestController {
|
||||
// constructor(private readonly leave_service: LeaveRequestsService){}
|
||||
|
||||
@Post('upsert')
|
||||
async upsertLeaveRequest(@Body() dto: UpsertLeaveRequestDto) {
|
||||
const { action, leave_requests } = await this.leave_service.handle(dto);
|
||||
return { action, leave_requests };
|
||||
}
|
||||
// @Post('upsert')
|
||||
// async upsertLeaveRequest(@Body() dto: UpsertLeaveRequestDto) {
|
||||
// const { action, leave_requests } = await this.leave_service.handle(dto);
|
||||
// return { action, leave_requests };
|
||||
// }
|
||||
|
||||
}
|
||||
// }
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
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 { BusinessLogicsModule } from "src/time-and-attendance/domains/business-logics.module";
|
||||
import { ShiftsModule } from "src/time-and-attendance/time-tracker/shifts/shifts.module";
|
||||
import { Module } from "@nestjs/common";
|
||||
// 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 { BusinessLogicsModule } from "src/time-and-attendance/domains/business-logics.module";
|
||||
// import { ShiftsModule } from "src/time-and-attendance/time-tracker/shifts/shifts.module";
|
||||
// import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
BusinessLogicsModule,
|
||||
ShiftsModule,
|
||||
],
|
||||
controllers: [LeaveRequestController],
|
||||
providers: [LeaveRequestsService],
|
||||
})
|
||||
// @Module({
|
||||
// imports: [
|
||||
// BusinessLogicsModule,
|
||||
// ShiftsModule,
|
||||
// ],
|
||||
// controllers: [LeaveRequestController],
|
||||
// providers: [LeaveRequestsService],
|
||||
// })
|
||||
|
||||
export class LeaveRequestsModule {}
|
||||
// export class LeaveRequestsModule {}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
import { LeaveRequestViewDto } from "../dtos/leave-request-view.dto";
|
||||
import { LeaveRequestRow } from "src/time-and-attendance/utils/type.utils";
|
||||
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) =>
|
||||
value !== null && value !== undefined ? Number(value) : undefined;
|
||||
|
|
|
|||
|
|
@ -1,79 +1,84 @@
|
|||
import { Injectable, NotFoundException, BadRequestException } from "@nestjs/common";
|
||||
import { UpsertLeaveRequestDto, UpsertResult } from "src/time-and-attendance/leave-requests/dtos/upsert-leave-request.dto";
|
||||
import { LeaveTypes, LeaveApprovalStatus } from "@prisma/client";
|
||||
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 { 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 { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
|
||||
import { HolidayService } from "src/time-and-attendance/domains/services/holiday.service";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
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 { Injectable, NotFoundException, BadRequestException } from "@nestjs/common";
|
||||
// import { UpsertLeaveRequestDto, UpsertResult } from "src/time-and-attendance/leave-requests/dtos/upsert-leave-request.dto";
|
||||
// import { LeaveTypes, LeaveApprovalStatus } from "@prisma/client";
|
||||
// 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 { 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 { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
|
||||
// import { HolidayService } from "src/time-and-attendance/domains/services/holiday.service";
|
||||
// import { PrismaService } from "src/prisma/prisma.service";
|
||||
// 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 { Result } from "src/common/errors/result-error.factory";
|
||||
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class HolidayLeaveRequestsService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly holidayService: HolidayService,
|
||||
private readonly leaveUtils: LeaveRequestsUtils,
|
||||
private readonly emailResolver: EmailToIdResolver,
|
||||
private readonly typeResolver: BankCodesResolver,
|
||||
) {}
|
||||
// @Injectable()
|
||||
// export class HolidayLeaveRequestsService {
|
||||
// constructor(
|
||||
// private readonly prisma: PrismaService,
|
||||
// private readonly holidayService: HolidayService,
|
||||
// private readonly leaveUtils: LeaveRequestsUtils,
|
||||
// private readonly emailResolver: EmailToIdResolver,
|
||||
// private readonly typeResolver: BankCodesResolver,
|
||||
// ) { }
|
||||
|
||||
async create(dto: UpsertLeaveRequestDto): Promise<UpsertResult> {
|
||||
const email = dto.email.trim();
|
||||
const employee_id = await this.emailResolver.findIdByEmail(email);
|
||||
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');
|
||||
// async create(dto: UpsertLeaveRequestDto): Promise<Result<UpsertResult, string>> {
|
||||
// const email = dto.email.trim();
|
||||
// const employee_id = await this.emailResolver.findIdByEmail(email);
|
||||
// if (!employee_id.success) return { success: false, error: employee_id.error }
|
||||
|
||||
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 date = toDateOnly(iso_date);
|
||||
// const created: LeaveRequestViewDto[] = [];
|
||||
|
||||
const existing = await this.prisma.leaveRequests.findUnique({
|
||||
where: {
|
||||
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}`);
|
||||
}
|
||||
// for (const iso_date of dates) {
|
||||
// const date = toDateOnly(iso_date);
|
||||
|
||||
const payable = await this.holidayService.calculateHolidayPay(email, date, bank_code.modifier);
|
||||
const row = await this.prisma.leaveRequests.create({
|
||||
data: {
|
||||
employee_id: employee_id,
|
||||
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,
|
||||
});
|
||||
// const existing = await this.prisma.leaveRequests.findUnique({
|
||||
// where: {
|
||||
// leave_per_employee_date: {
|
||||
// employee_id: employee_id.data,
|
||||
// leave_type: LeaveTypes.HOLIDAY,
|
||||
// date,
|
||||
// },
|
||||
// },
|
||||
// select: { id: true },
|
||||
// });
|
||||
// if (existing) {
|
||||
// throw new BadRequestException(`Holiday request already exists for ${iso_date}`);
|
||||
// }
|
||||
|
||||
const hours = Number(row.payable_hours ?? row.requested_hours ?? 0);
|
||||
if (row.approval_status === LeaveApprovalStatus.APPROVED) {
|
||||
await this.leaveUtils.syncShift(email, employee_id, iso_date, hours,LeaveTypes.HOLIDAY, row.comment);
|
||||
}
|
||||
// const payable = await this.holidayService.calculateHolidayPay(email, date, bank_code.modifier);
|
||||
// if (!payable) return { success: false, error: `An error occured during calculation` };
|
||||
|
||||
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 { UpsertLeaveRequestDto, UpsertResult } from "../dtos/upsert-leave-request.dto";
|
||||
import { LeaveApprovalStatus, LeaveTypes } from "@prisma/client";
|
||||
import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils";
|
||||
import { LeaveRequestViewDto } from "../dtos/leave-request-view.dto";
|
||||
import { roundToQuarterHour } from "src/common/utils/date-utils";
|
||||
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 { 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 { VacationService } from "src/time-and-attendance/domains/services/vacation.service";
|
||||
import { HolidayService } from "src/time-and-attendance/domains/services/holiday.service";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { mapRowToView } from "../mappers/leave-requests.mapper";
|
||||
import { normalizeDates, toDateOnly, toISODateKey } from "src/time-and-attendance/utils/date-time.utils";
|
||||
@Injectable()
|
||||
export class LeaveRequestsService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly holidayService: HolidayService,
|
||||
private readonly sickLogic: SickLeaveService,
|
||||
private readonly vacationLogic: VacationService,
|
||||
private readonly leaveUtils: LeaveRequestsUtils,
|
||||
private readonly emailResolver: EmailToIdResolver,
|
||||
private readonly typeResolver: BankCodesResolver,
|
||||
) {}
|
||||
// import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
|
||||
// import { UpsertLeaveRequestDto, UpsertResult } from "../dtos/upsert-leave-request.dto";
|
||||
// import { LeaveApprovalStatus, LeaveTypes } from "@prisma/client";
|
||||
// import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils";
|
||||
// import { LeaveRequestViewDto } from "../dtos/leave-request-view.dto";
|
||||
// import { roundToQuarterHour } from "src/common/utils/date-utils";
|
||||
// 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 { 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 { VacationService } from "src/time-and-attendance/domains/services/vacation.service";
|
||||
// import { HolidayService } from "src/time-and-attendance/domains/services/holiday.service";
|
||||
// import { PrismaService } from "src/prisma/prisma.service";
|
||||
// import { mapRowToView } from "../mappers/leave-requests.mapper";
|
||||
// import { normalizeDates, toDateOnly, toISODateKey } from "src/time-and-attendance/utils/date-time.utils";
|
||||
// @Injectable()
|
||||
// export class LeaveRequestsService {
|
||||
// constructor(
|
||||
// private readonly prisma: PrismaService,
|
||||
// private readonly holidayService: HolidayService,
|
||||
// private readonly sickLogic: SickLeaveService,
|
||||
// private readonly vacationLogic: VacationService,
|
||||
// private readonly leaveUtils: LeaveRequestsUtils,
|
||||
// private readonly emailResolver: EmailToIdResolver,
|
||||
// private readonly typeResolver: BankCodesResolver,
|
||||
// ) {}
|
||||
|
||||
// handle distribution to the right service according to the selected type and action
|
||||
async handle(dto: UpsertLeaveRequestDto): Promise<UpsertResult> {
|
||||
switch (dto.type) {
|
||||
case LeaveTypes.HOLIDAY:
|
||||
if( dto.action === 'create'){
|
||||
// return this.holidayService.create(dto);
|
||||
} else if (dto.action === 'update') {
|
||||
return this.update(dto, LeaveTypes.HOLIDAY);
|
||||
} else if (dto.action === 'delete'){
|
||||
return this.delete(dto, LeaveTypes.HOLIDAY);
|
||||
}
|
||||
case LeaveTypes.VACATION:
|
||||
if( dto.action === 'create'){
|
||||
// return this.vacationService.create(dto);
|
||||
} else if (dto.action === 'update') {
|
||||
return this.update(dto, LeaveTypes.VACATION);
|
||||
} else if (dto.action === 'delete'){
|
||||
return this.delete(dto, LeaveTypes.VACATION);
|
||||
}
|
||||
case LeaveTypes.SICK:
|
||||
if( dto.action === 'create'){
|
||||
// return this.sickLeaveService.create(dto);
|
||||
} else if (dto.action === 'update') {
|
||||
return this.update(dto, LeaveTypes.SICK);
|
||||
} else if (dto.action === 'delete'){
|
||||
return this.delete(dto, LeaveTypes.SICK);
|
||||
}
|
||||
default:
|
||||
throw new BadRequestException(`Unsupported leave type: ${dto.type} or action: ${dto.action}`);
|
||||
}
|
||||
}
|
||||
// // handle distribution to the right service according to the selected type and action
|
||||
// async handle(dto: UpsertLeaveRequestDto): Promise<UpsertResult> {
|
||||
// switch (dto.type) {
|
||||
// case LeaveTypes.HOLIDAY:
|
||||
// if( dto.action === 'create'){
|
||||
// // return this.holidayService.create(dto);
|
||||
// } else if (dto.action === 'update') {
|
||||
// return this.update(dto, LeaveTypes.HOLIDAY);
|
||||
// } else if (dto.action === 'delete'){
|
||||
// return this.delete(dto, LeaveTypes.HOLIDAY);
|
||||
// }
|
||||
// case LeaveTypes.VACATION:
|
||||
// if( dto.action === 'create'){
|
||||
// // return this.vacationService.create(dto);
|
||||
// } else if (dto.action === 'update') {
|
||||
// return this.update(dto, LeaveTypes.VACATION);
|
||||
// } else if (dto.action === 'delete'){
|
||||
// return this.delete(dto, LeaveTypes.VACATION);
|
||||
// }
|
||||
// case LeaveTypes.SICK:
|
||||
// if( dto.action === 'create'){
|
||||
// // return this.sickLeaveService.create(dto);
|
||||
// } else if (dto.action === 'update') {
|
||||
// return this.update(dto, LeaveTypes.SICK);
|
||||
// } else if (dto.action === 'delete'){
|
||||
// return this.delete(dto, LeaveTypes.SICK);
|
||||
// }
|
||||
// default:
|
||||
// throw new BadRequestException(`Unsupported leave type: ${dto.type} or action: ${dto.action}`);
|
||||
// }
|
||||
// }
|
||||
|
||||
async delete(dto: UpsertLeaveRequestDto, type: LeaveTypes): Promise<UpsertResult> {
|
||||
const email = dto.email.trim();
|
||||
const dates = normalizeDates(dto.dates);
|
||||
const employee_id = await this.emailResolver.findIdByEmail(email);
|
||||
if (!dates.length) throw new BadRequestException("Dates array must not be empty");
|
||||
// async delete(dto: UpsertLeaveRequestDto, type: LeaveTypes): Promise<UpsertResult> {
|
||||
// const email = dto.email.trim();
|
||||
// const dates = normalizeDates(dto.dates);
|
||||
// const employee_id = await this.emailResolver.findIdByEmail(email);
|
||||
// if (!dates.length) throw new BadRequestException("Dates array must not be empty");
|
||||
|
||||
const rows = await this.prisma.leaveRequests.findMany({
|
||||
where: {
|
||||
employee_id: employee_id,
|
||||
leave_type: type,
|
||||
date: { in: dates.map((d) => toDateOnly(d)) },
|
||||
},
|
||||
select: leaveRequestsSelect,
|
||||
});
|
||||
// const rows = await this.prisma.leaveRequests.findMany({
|
||||
// where: {
|
||||
// employee_id: employee_id,
|
||||
// leave_type: type,
|
||||
// date: { in: dates.map((d) => toDateOnly(d)) },
|
||||
// },
|
||||
// select: leaveRequestsSelect,
|
||||
// });
|
||||
|
||||
if (rows.length !== dates.length) {
|
||||
const missing = dates.filter((isoDate) => !rows.some((row) => toISODateKey(row.date) === isoDate));
|
||||
throw new NotFoundException(`No Leave request found for: ${missing.join(", ")}`);
|
||||
}
|
||||
// if (rows.length !== dates.length) {
|
||||
// const missing = dates.filter((isoDate) => !rows.some((row) => toISODateKey(row.date) === isoDate));
|
||||
// throw new NotFoundException(`No Leave request found for: ${missing.join(", ")}`);
|
||||
// }
|
||||
|
||||
for (const row of rows) {
|
||||
if (row.approval_status === LeaveApprovalStatus.APPROVED) {
|
||||
const iso = toISODateKey(row.date);
|
||||
await this.leaveUtils.removeShift(email, employee_id, iso, type);
|
||||
}
|
||||
}
|
||||
// for (const row of rows) {
|
||||
// if (row.approval_status === LeaveApprovalStatus.APPROVED) {
|
||||
// const iso = toISODateKey(row.date);
|
||||
// await this.leaveUtils.removeShift(email, employee_id, iso, type);
|
||||
// }
|
||||
// }
|
||||
|
||||
await this.prisma.leaveRequests.deleteMany({
|
||||
where: { id: { in: rows.map((row) => row.id) } },
|
||||
});
|
||||
// await this.prisma.leaveRequests.deleteMany({
|
||||
// where: { id: { in: rows.map((row) => row.id) } },
|
||||
// });
|
||||
|
||||
const deleted = rows.map((row) => ({ ...mapRowToView(row), action: "delete" as const }));
|
||||
return { action: "delete", leave_requests: deleted };
|
||||
}
|
||||
// const deleted = rows.map((row) => ({ ...mapRowToView(row), action: "delete" as const }));
|
||||
// return { action: "delete", leave_requests: deleted };
|
||||
// }
|
||||
|
||||
async update(dto: UpsertLeaveRequestDto, type: LeaveTypes): Promise<UpsertResult> {
|
||||
const email = dto.email.trim();
|
||||
const employee_id = await this.emailResolver.findIdByEmail(email);
|
||||
const bank_code = await this.typeResolver.findIdAndModifierByType(type);
|
||||
if(!bank_code) throw new NotFoundException(`bank_code not found`);
|
||||
const modifier = Number(bank_code.modifier ?? 1);
|
||||
const dates = normalizeDates(dto.dates);
|
||||
if (!dates.length) {
|
||||
throw new BadRequestException("Dates array must not be empty");
|
||||
}
|
||||
// async update(dto: UpsertLeaveRequestDto, type: LeaveTypes): Promise<UpsertResult> {
|
||||
// const email = dto.email.trim();
|
||||
// const employee_id = await this.emailResolver.findIdByEmail(email);
|
||||
// const bank_code = await this.typeResolver.findIdAndModifierByType(type);
|
||||
// if(!bank_code) throw new NotFoundException(`bank_code not found`);
|
||||
// const modifier = Number(bank_code.modifier ?? 1);
|
||||
// const dates = normalizeDates(dto.dates);
|
||||
// if (!dates.length) {
|
||||
// throw new BadRequestException("Dates array must not be empty");
|
||||
// }
|
||||
|
||||
const entries = await Promise.all(
|
||||
dates.map(async (iso_date) => {
|
||||
const date = toDateOnly(iso_date);
|
||||
const existing = await this.prisma.leaveRequests.findUnique({
|
||||
where: {
|
||||
leave_per_employee_date: {
|
||||
employee_id: employee_id,
|
||||
leave_type: type,
|
||||
date,
|
||||
},
|
||||
},
|
||||
select: leaveRequestsSelect,
|
||||
});
|
||||
if (!existing) throw new NotFoundException(`No Leave request found for ${iso_date}`);
|
||||
return { iso_date, date, existing };
|
||||
}),
|
||||
);
|
||||
// const entries = await Promise.all(
|
||||
// dates.map(async (iso_date) => {
|
||||
// const date = toDateOnly(iso_date);
|
||||
// const existing = await this.prisma.leaveRequests.findUnique({
|
||||
// where: {
|
||||
// leave_per_employee_date: {
|
||||
// employee_id: employee_id,
|
||||
// leave_type: type,
|
||||
// date,
|
||||
// },
|
||||
// },
|
||||
// select: leaveRequestsSelect,
|
||||
// });
|
||||
// if (!existing) throw new NotFoundException(`No Leave request found for ${iso_date}`);
|
||||
// return { iso_date, date, existing };
|
||||
// }),
|
||||
// );
|
||||
|
||||
const updated: LeaveRequestViewDto[] = [];
|
||||
// const updated: LeaveRequestViewDto[] = [];
|
||||
|
||||
if (type === LeaveTypes.SICK) {
|
||||
const firstExisting = entries[0].existing;
|
||||
const fallbackRequested =
|
||||
firstExisting.requested_hours !== null && firstExisting.requested_hours !== undefined
|
||||
? Number(firstExisting.requested_hours)
|
||||
: 8;
|
||||
const requested_hours_per_day = dto.requested_hours ?? fallbackRequested;
|
||||
const reference_date = entries.reduce(
|
||||
(latest, entry) => (entry.date > latest ? entry.date : latest),
|
||||
entries[0].date,
|
||||
);
|
||||
const total_payable_hours = await this.sickLogic.calculateSickLeavePay(
|
||||
employee_id,
|
||||
reference_date,
|
||||
entries.length,
|
||||
requested_hours_per_day,
|
||||
modifier,
|
||||
);
|
||||
let remaining_payable_hours = roundToQuarterHour(Math.max(0, total_payable_hours));
|
||||
const daily_payable_cap = roundToQuarterHour(requested_hours_per_day * modifier);
|
||||
// if (type === LeaveTypes.SICK) {
|
||||
// const firstExisting = entries[0].existing;
|
||||
// const fallbackRequested =
|
||||
// firstExisting.requested_hours !== null && firstExisting.requested_hours !== undefined
|
||||
// ? Number(firstExisting.requested_hours)
|
||||
// : 8;
|
||||
// const requested_hours_per_day = dto.requested_hours ?? fallbackRequested;
|
||||
// const reference_date = entries.reduce(
|
||||
// (latest, entry) => (entry.date > latest ? entry.date : latest),
|
||||
// entries[0].date,
|
||||
// );
|
||||
// const total_payable_hours = await this.sickLogic.calculateSickLeavePay(
|
||||
// employee_id,
|
||||
// reference_date,
|
||||
// entries.length,
|
||||
// requested_hours_per_day,
|
||||
// modifier,
|
||||
// );
|
||||
// let remaining_payable_hours = roundToQuarterHour(Math.max(0, total_payable_hours));
|
||||
// const daily_payable_cap = roundToQuarterHour(requested_hours_per_day * modifier);
|
||||
|
||||
for (const { iso_date, existing } of entries) {
|
||||
const previous_status = existing.approval_status;
|
||||
const payable = Math.min(remaining_payable_hours, daily_payable_cap);
|
||||
const payable_rounded = roundToQuarterHour(Math.max(0, payable));
|
||||
remaining_payable_hours = roundToQuarterHour(
|
||||
Math.max(0, remaining_payable_hours - payable_rounded),
|
||||
);
|
||||
// for (const { iso_date, existing } of entries) {
|
||||
// const previous_status = existing.approval_status;
|
||||
// const payable = Math.min(remaining_payable_hours, daily_payable_cap);
|
||||
// const payable_rounded = roundToQuarterHour(Math.max(0, payable));
|
||||
// remaining_payable_hours = roundToQuarterHour(
|
||||
// Math.max(0, remaining_payable_hours - payable_rounded),
|
||||
// );
|
||||
|
||||
const row = await this.prisma.leaveRequests.update({
|
||||
where: { id: existing.id },
|
||||
data: {
|
||||
comment: dto.comment ?? existing.comment,
|
||||
requested_hours: requested_hours_per_day,
|
||||
payable_hours: payable_rounded,
|
||||
bank_code_id: bank_code.id,
|
||||
approval_status: dto.approval_status ?? existing.approval_status,
|
||||
},
|
||||
select: leaveRequestsSelect,
|
||||
});
|
||||
// const row = await this.prisma.leaveRequests.update({
|
||||
// where: { id: existing.id },
|
||||
// data: {
|
||||
// comment: dto.comment ?? existing.comment,
|
||||
// requested_hours: requested_hours_per_day,
|
||||
// payable_hours: payable_rounded,
|
||||
// bank_code_id: bank_code.id,
|
||||
// approval_status: dto.approval_status ?? existing.approval_status,
|
||||
// },
|
||||
// select: leaveRequestsSelect,
|
||||
// });
|
||||
|
||||
const was_approved = previous_status === LeaveApprovalStatus.APPROVED;
|
||||
const is_approved = row.approval_status === LeaveApprovalStatus.APPROVED;
|
||||
const hours = Number(row.payable_hours ?? row.requested_hours ?? 0);
|
||||
// const was_approved = previous_status === LeaveApprovalStatus.APPROVED;
|
||||
// const is_approved = row.approval_status === LeaveApprovalStatus.APPROVED;
|
||||
// const hours = Number(row.payable_hours ?? row.requested_hours ?? 0);
|
||||
|
||||
if (!was_approved && is_approved) {
|
||||
await this.leaveUtils.syncShift(email, employee_id, iso_date, hours, type, row.comment);
|
||||
} else if (was_approved && !is_approved) {
|
||||
await this.leaveUtils.removeShift(email, employee_id, iso_date, type);
|
||||
} else if (was_approved && is_approved) {
|
||||
await this.leaveUtils.syncShift(email, employee_id, iso_date, hours, type, row.comment);
|
||||
}
|
||||
updated.push({ ...mapRowToView(row), action: "update" });
|
||||
}
|
||||
return { action: "update", leave_requests: updated };
|
||||
}
|
||||
// if (!was_approved && is_approved) {
|
||||
// await this.leaveUtils.syncShift(email, employee_id, iso_date, hours, type, row.comment);
|
||||
// } else if (was_approved && !is_approved) {
|
||||
// await this.leaveUtils.removeShift(email, employee_id, iso_date, type);
|
||||
// } else if (was_approved && is_approved) {
|
||||
// await this.leaveUtils.syncShift(email, employee_id, iso_date, hours, type, row.comment);
|
||||
// }
|
||||
// updated.push({ ...mapRowToView(row), action: "update" });
|
||||
// }
|
||||
// return { action: "update", leave_requests: updated };
|
||||
// }
|
||||
|
||||
for (const { iso_date, date, existing } of entries) {
|
||||
const previous_status = existing.approval_status;
|
||||
const fallbackRequested =
|
||||
existing.requested_hours !== null && existing.requested_hours !== undefined
|
||||
? Number(existing.requested_hours)
|
||||
: 8;
|
||||
const requested_hours = dto.requested_hours ?? fallbackRequested;
|
||||
// for (const { iso_date, date, existing } of entries) {
|
||||
// const previous_status = existing.approval_status;
|
||||
// const fallbackRequested =
|
||||
// existing.requested_hours !== null && existing.requested_hours !== undefined
|
||||
// ? Number(existing.requested_hours)
|
||||
// : 8;
|
||||
// const requested_hours = dto.requested_hours ?? fallbackRequested;
|
||||
|
||||
let payable: number;
|
||||
switch (type) {
|
||||
case LeaveTypes.HOLIDAY:
|
||||
payable = await this.holidayService.calculateHolidayPay(email, date, modifier);
|
||||
break;
|
||||
case LeaveTypes.VACATION: {
|
||||
const days_requested = requested_hours / 8;
|
||||
payable = await this.vacationLogic.calculateVacationPay(
|
||||
employee_id,
|
||||
date,
|
||||
Math.max(0, days_requested),
|
||||
modifier,
|
||||
);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
payable = existing.payable_hours !== null && existing.payable_hours !== undefined
|
||||
? Number(existing.payable_hours)
|
||||
: requested_hours;
|
||||
}
|
||||
// let payable: number;
|
||||
// switch (type) {
|
||||
// case LeaveTypes.HOLIDAY:
|
||||
// payable = await this.holidayService.calculateHolidayPay(email, date, modifier);
|
||||
// break;
|
||||
// case LeaveTypes.VACATION: {
|
||||
// const days_requested = requested_hours / 8;
|
||||
// payable = await this.vacationLogic.calculateVacationPay(
|
||||
// employee_id,
|
||||
// date,
|
||||
// Math.max(0, days_requested),
|
||||
// modifier,
|
||||
// );
|
||||
// break;
|
||||
// }
|
||||
// default:
|
||||
// payable = existing.payable_hours !== null && existing.payable_hours !== undefined
|
||||
// ? Number(existing.payable_hours)
|
||||
// : requested_hours;
|
||||
// }
|
||||
|
||||
const row = await this.prisma.leaveRequests.update({
|
||||
where: { id: existing.id },
|
||||
data: {
|
||||
requested_hours,
|
||||
comment: dto.comment ?? existing.comment,
|
||||
payable_hours: payable,
|
||||
bank_code_id: bank_code.id,
|
||||
approval_status: dto.approval_status ?? existing.approval_status,
|
||||
},
|
||||
select: leaveRequestsSelect,
|
||||
});
|
||||
// const row = await this.prisma.leaveRequests.update({
|
||||
// where: { id: existing.id },
|
||||
// data: {
|
||||
// requested_hours,
|
||||
// comment: dto.comment ?? existing.comment,
|
||||
// payable_hours: payable,
|
||||
// bank_code_id: bank_code.id,
|
||||
// approval_status: dto.approval_status ?? existing.approval_status,
|
||||
// },
|
||||
// select: leaveRequestsSelect,
|
||||
// });
|
||||
|
||||
const was_approved = previous_status === LeaveApprovalStatus.APPROVED;
|
||||
const is_approved = row.approval_status === LeaveApprovalStatus.APPROVED;
|
||||
const hours = Number(row.payable_hours ?? row.requested_hours ?? 0);
|
||||
// const was_approved = previous_status === LeaveApprovalStatus.APPROVED;
|
||||
// const is_approved = row.approval_status === LeaveApprovalStatus.APPROVED;
|
||||
// const hours = Number(row.payable_hours ?? row.requested_hours ?? 0);
|
||||
|
||||
if (!was_approved && is_approved) {
|
||||
await this.leaveUtils.syncShift(email, employee_id, iso_date, hours, type, row.comment);
|
||||
} else if (was_approved && !is_approved) {
|
||||
await this.leaveUtils.removeShift(email, employee_id, iso_date, type);
|
||||
} else if (was_approved && is_approved) {
|
||||
await this.leaveUtils.syncShift(email, employee_id, iso_date, hours, type, row.comment);
|
||||
}
|
||||
updated.push({ ...mapRowToView(row), action: "update" });
|
||||
}
|
||||
return { action: "update", leave_requests: updated };
|
||||
}
|
||||
}
|
||||
// if (!was_approved && is_approved) {
|
||||
// await this.leaveUtils.syncShift(email, employee_id, iso_date, hours, type, row.comment);
|
||||
// } else if (was_approved && !is_approved) {
|
||||
// await this.leaveUtils.removeShift(email, employee_id, iso_date, type);
|
||||
// } else if (was_approved && is_approved) {
|
||||
// await this.leaveUtils.syncShift(email, employee_id, iso_date, hours, type, row.comment);
|
||||
// }
|
||||
// updated.push({ ...mapRowToView(row), action: "update" });
|
||||
// }
|
||||
// return { action: "update", leave_requests: updated };
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,98 +1,98 @@
|
|||
import { Injectable, NotFoundException, BadRequestException } from "@nestjs/common";
|
||||
import { UpsertLeaveRequestDto, UpsertResult } from "src/time-and-attendance/leave-requests/dtos/upsert-leave-request.dto";
|
||||
import { LeaveTypes, LeaveApprovalStatus } from "@prisma/client";
|
||||
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 { roundToQuarterHour } from "src/common/utils/date-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 { SickLeaveService } from "src/time-and-attendance/domains/services/sick-leave.service";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
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 { Injectable, NotFoundException, BadRequestException } from "@nestjs/common";
|
||||
// import { UpsertLeaveRequestDto, UpsertResult } from "src/time-and-attendance/leave-requests/dtos/upsert-leave-request.dto";
|
||||
// import { LeaveTypes, LeaveApprovalStatus } from "@prisma/client";
|
||||
// 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 { roundToQuarterHour } from "src/common/utils/date-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 { SickLeaveService } from "src/time-and-attendance/domains/services/sick-leave.service";
|
||||
// import { PrismaService } from "src/prisma/prisma.service";
|
||||
// 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";
|
||||
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class SickLeaveRequestsService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly sickService: SickLeaveService,
|
||||
private readonly emailResolver: EmailToIdResolver,
|
||||
private readonly typeResolver: BankCodesResolver,
|
||||
) {}
|
||||
// @Injectable()
|
||||
// export class SickLeaveRequestsService {
|
||||
// constructor(
|
||||
// private readonly prisma: PrismaService,
|
||||
// private readonly sickService: SickLeaveService,
|
||||
// private readonly emailResolver: EmailToIdResolver,
|
||||
// private readonly typeResolver: BankCodesResolver,
|
||||
// ) {}
|
||||
|
||||
async create(dto: UpsertLeaveRequestDto): Promise<UpsertResult> {
|
||||
const email = dto.email.trim();
|
||||
const employee_id = await this.emailResolver.findIdByEmail(email);
|
||||
const bank_code = await this.typeResolver.findIdAndModifierByType(LeaveTypes.SICK);
|
||||
if(!bank_code) throw new NotFoundException(`bank_code not found`);
|
||||
// async create(dto: UpsertLeaveRequestDto): Promise<UpsertResult> {
|
||||
// const email = dto.email.trim();
|
||||
// const employee_id = await this.emailResolver.findIdByEmail(email);
|
||||
// const bank_code = await this.typeResolver.findIdAndModifierByType(LeaveTypes.SICK);
|
||||
// if(!bank_code) throw new NotFoundException(`bank_code not found`);
|
||||
|
||||
const modifier = bank_code.modifier ?? 1;
|
||||
const dates = normalizeDates(dto.dates);
|
||||
if (!dates.length) throw new BadRequestException("Dates array must not be empty");
|
||||
const requested_hours_per_day = dto.requested_hours ?? 8;
|
||||
// const modifier = bank_code.modifier ?? 1;
|
||||
// const dates = normalizeDates(dto.dates);
|
||||
// if (!dates.length) throw new BadRequestException("Dates array must not be empty");
|
||||
// const requested_hours_per_day = dto.requested_hours ?? 8;
|
||||
|
||||
const entries = dates.map((iso) => ({ iso, date: toDateOnly(iso) }));
|
||||
const reference_date = entries.reduce(
|
||||
(latest, entry) => (entry.date > latest ? entry.date : latest),
|
||||
entries[0].date,
|
||||
);
|
||||
const total_payable_hours = await this.sickService.calculateSickLeavePay(
|
||||
employee_id,
|
||||
reference_date,
|
||||
entries.length,
|
||||
requested_hours_per_day,
|
||||
modifier,
|
||||
);
|
||||
let remaining_payable_hours = roundToQuarterHour(Math.max(0, total_payable_hours));
|
||||
const daily_payable_cap = roundToQuarterHour(requested_hours_per_day * modifier);
|
||||
// const entries = dates.map((iso) => ({ iso, date: toDateOnly(iso) }));
|
||||
// const reference_date = entries.reduce(
|
||||
// (latest, entry) => (entry.date > latest ? entry.date : latest),
|
||||
// entries[0].date,
|
||||
// );
|
||||
// const total_payable_hours = await this.sickService.calculateSickLeavePay(
|
||||
// employee_id,
|
||||
// reference_date,
|
||||
// entries.length,
|
||||
// requested_hours_per_day,
|
||||
// modifier,
|
||||
// );
|
||||
// let remaining_payable_hours = roundToQuarterHour(Math.max(0, total_payable_hours));
|
||||
// const daily_payable_cap = roundToQuarterHour(requested_hours_per_day * modifier);
|
||||
|
||||
const created: LeaveRequestViewDto[] = [];
|
||||
// const created: LeaveRequestViewDto[] = [];
|
||||
|
||||
for (const { iso, date } of entries) {
|
||||
const existing = await this.prisma.leaveRequests.findUnique({
|
||||
where: {
|
||||
leave_per_employee_date: {
|
||||
employee_id: employee_id,
|
||||
leave_type: LeaveTypes.SICK,
|
||||
date,
|
||||
},
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
if (existing) {
|
||||
throw new BadRequestException(`Sick request already exists for ${iso}`);
|
||||
}
|
||||
// for (const { iso, date } of entries) {
|
||||
// const existing = await this.prisma.leaveRequests.findUnique({
|
||||
// where: {
|
||||
// leave_per_employee_date: {
|
||||
// employee_id: employee_id,
|
||||
// leave_type: LeaveTypes.SICK,
|
||||
// date,
|
||||
// },
|
||||
// },
|
||||
// select: { id: true },
|
||||
// });
|
||||
// if (existing) {
|
||||
// throw new BadRequestException(`Sick request already exists for ${iso}`);
|
||||
// }
|
||||
|
||||
const payable = Math.min(remaining_payable_hours, daily_payable_cap);
|
||||
const payable_rounded = roundToQuarterHour(Math.max(0, payable));
|
||||
remaining_payable_hours = roundToQuarterHour(
|
||||
Math.max(0, remaining_payable_hours - payable_rounded),
|
||||
);
|
||||
// const payable = Math.min(remaining_payable_hours, daily_payable_cap);
|
||||
// const payable_rounded = roundToQuarterHour(Math.max(0, payable));
|
||||
// remaining_payable_hours = roundToQuarterHour(
|
||||
// Math.max(0, remaining_payable_hours - payable_rounded),
|
||||
// );
|
||||
|
||||
const row = await this.prisma.leaveRequests.create({
|
||||
data: {
|
||||
employee_id: employee_id,
|
||||
bank_code_id: bank_code.id,
|
||||
leave_type: LeaveTypes.SICK,
|
||||
comment: dto.comment ?? "",
|
||||
requested_hours: requested_hours_per_day,
|
||||
payable_hours: payable_rounded,
|
||||
approval_status: dto.approval_status ?? LeaveApprovalStatus.PENDING,
|
||||
date,
|
||||
},
|
||||
select: leaveRequestsSelect,
|
||||
});
|
||||
// const row = await this.prisma.leaveRequests.create({
|
||||
// data: {
|
||||
// employee_id: employee_id,
|
||||
// bank_code_id: bank_code.id,
|
||||
// leave_type: LeaveTypes.SICK,
|
||||
// comment: dto.comment ?? "",
|
||||
// requested_hours: requested_hours_per_day,
|
||||
// payable_hours: payable_rounded,
|
||||
// approval_status: dto.approval_status ?? LeaveApprovalStatus.PENDING,
|
||||
// date,
|
||||
// },
|
||||
// select: leaveRequestsSelect,
|
||||
// });
|
||||
|
||||
const hours = Number(row.payable_hours ?? row.requested_hours ?? 0);
|
||||
if (row.approval_status === LeaveApprovalStatus.APPROVED) {
|
||||
// await this.leaveUtils.syncShift(email, employee_id, iso, hours,LeaveTypes.SICK, row.comment);
|
||||
}
|
||||
// const hours = Number(row.payable_hours ?? row.requested_hours ?? 0);
|
||||
// if (row.approval_status === LeaveApprovalStatus.APPROVED) {
|
||||
// // 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 { UpsertLeaveRequestDto, UpsertResult } from "src/time-and-attendance/leave-requests/dtos/upsert-leave-request.dto";
|
||||
import { LeaveTypes, LeaveApprovalStatus } from "@prisma/client";
|
||||
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 { roundToQuarterHour } from "src/common/utils/date-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 { VacationService } from "src/time-and-attendance/domains/services/vacation.service";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
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 { Injectable, NotFoundException, BadRequestException } from "@nestjs/common";
|
||||
// import { UpsertLeaveRequestDto, UpsertResult } from "src/time-and-attendance/leave-requests/dtos/upsert-leave-request.dto";
|
||||
// import { LeaveTypes, LeaveApprovalStatus } from "@prisma/client";
|
||||
// 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 { roundToQuarterHour } from "src/common/utils/date-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 { VacationService } from "src/time-and-attendance/domains/services/vacation.service";
|
||||
// import { PrismaService } from "src/prisma/prisma.service";
|
||||
// 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";
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class VacationLeaveRequestsService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly vacationService: VacationService,
|
||||
private readonly emailResolver: EmailToIdResolver,
|
||||
private readonly typeResolver: BankCodesResolver,
|
||||
) {}
|
||||
// @Injectable()
|
||||
// export class VacationLeaveRequestsService {
|
||||
// constructor(
|
||||
// private readonly prisma: PrismaService,
|
||||
// private readonly vacationService: VacationService,
|
||||
// private readonly emailResolver: EmailToIdResolver,
|
||||
// private readonly typeResolver: BankCodesResolver,
|
||||
// ) {}
|
||||
|
||||
async create(dto: UpsertLeaveRequestDto): Promise<UpsertResult> {
|
||||
const email = dto.email.trim();
|
||||
const employee_id = await this.emailResolver.findIdByEmail(email);
|
||||
const bank_code = await this.typeResolver.findIdAndModifierByType(LeaveTypes.VACATION);
|
||||
if(!bank_code) throw new NotFoundException(`bank_code not found`);
|
||||
// async create(dto: UpsertLeaveRequestDto): Promise<UpsertResult> {
|
||||
// const email = dto.email.trim();
|
||||
// const employee_id = await this.emailResolver.findIdByEmail(email);
|
||||
// const bank_code = await this.typeResolver.findIdAndModifierByType(LeaveTypes.VACATION);
|
||||
// if(!bank_code) throw new NotFoundException(`bank_code not found`);
|
||||
|
||||
const modifier = bank_code.modifier ?? 1;
|
||||
const dates = normalizeDates(dto.dates);
|
||||
const requested_hours_per_day = dto.requested_hours ?? 8;
|
||||
if (!dates.length) throw new BadRequestException("Dates array must not be empty");
|
||||
// const modifier = bank_code.modifier ?? 1;
|
||||
// const dates = normalizeDates(dto.dates);
|
||||
// const requested_hours_per_day = dto.requested_hours ?? 8;
|
||||
// if (!dates.length) throw new BadRequestException("Dates array must not be empty");
|
||||
|
||||
const entries = dates
|
||||
.map((iso) => ({ iso, date: toDateOnly(iso) }))
|
||||
.sort((a, b) => a.date.getTime() - b.date.getTime());
|
||||
const start_date = entries[0].date;
|
||||
const total_payable_hours = await this.vacationService.calculateVacationPay(
|
||||
employee_id,
|
||||
start_date,
|
||||
entries.length,
|
||||
modifier,
|
||||
);
|
||||
let remaining_payable_hours = roundToQuarterHour(Math.max(0, total_payable_hours));
|
||||
const daily_payable_cap = roundToQuarterHour(requested_hours_per_day * modifier);
|
||||
// const entries = dates
|
||||
// .map((iso) => ({ iso, date: toDateOnly(iso) }))
|
||||
// .sort((a, b) => a.date.getTime() - b.date.getTime());
|
||||
// const start_date = entries[0].date;
|
||||
// const total_payable_hours = await this.vacationService.calculateVacationPay(
|
||||
// employee_id,
|
||||
// start_date,
|
||||
// entries.length,
|
||||
// modifier,
|
||||
// );
|
||||
// let remaining_payable_hours = roundToQuarterHour(Math.max(0, total_payable_hours));
|
||||
// const daily_payable_cap = roundToQuarterHour(requested_hours_per_day * modifier);
|
||||
|
||||
const created: LeaveRequestViewDto[] = [];
|
||||
// const created: LeaveRequestViewDto[] = [];
|
||||
|
||||
for (const { iso, date } of entries) {
|
||||
const existing = await this.prisma.leaveRequests.findUnique({
|
||||
where: {
|
||||
leave_per_employee_date: {
|
||||
employee_id: employee_id,
|
||||
leave_type: LeaveTypes.VACATION,
|
||||
date,
|
||||
},
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
if (existing) throw new BadRequestException(`Vacation request already exists for ${iso}`);
|
||||
// for (const { iso, date } of entries) {
|
||||
// const existing = await this.prisma.leaveRequests.findUnique({
|
||||
// where: {
|
||||
// leave_per_employee_date: {
|
||||
// employee_id: employee_id,
|
||||
// leave_type: LeaveTypes.VACATION,
|
||||
// date,
|
||||
// },
|
||||
// },
|
||||
// select: { id: true },
|
||||
// });
|
||||
// if (existing) throw new BadRequestException(`Vacation request already exists for ${iso}`);
|
||||
|
||||
const payable = Math.min(remaining_payable_hours, daily_payable_cap);
|
||||
const payable_rounded = roundToQuarterHour(Math.max(0, payable));
|
||||
remaining_payable_hours = roundToQuarterHour(
|
||||
Math.max(0, remaining_payable_hours - payable_rounded),
|
||||
);
|
||||
// const payable = Math.min(remaining_payable_hours, daily_payable_cap);
|
||||
// const payable_rounded = roundToQuarterHour(Math.max(0, payable));
|
||||
// remaining_payable_hours = roundToQuarterHour(
|
||||
// Math.max(0, remaining_payable_hours - payable_rounded),
|
||||
// );
|
||||
|
||||
const row = await this.prisma.leaveRequests.create({
|
||||
data: {
|
||||
employee_id: employee_id,
|
||||
bank_code_id: bank_code.id,
|
||||
payable_hours: payable_rounded,
|
||||
requested_hours: requested_hours_per_day,
|
||||
leave_type: LeaveTypes.VACATION,
|
||||
comment: dto.comment ?? "",
|
||||
approval_status: dto.approval_status ?? LeaveApprovalStatus.PENDING,
|
||||
date,
|
||||
},
|
||||
select: leaveRequestsSelect,
|
||||
});
|
||||
// const row = await this.prisma.leaveRequests.create({
|
||||
// data: {
|
||||
// employee_id: employee_id,
|
||||
// bank_code_id: bank_code.id,
|
||||
// payable_hours: payable_rounded,
|
||||
// requested_hours: requested_hours_per_day,
|
||||
// leave_type: LeaveTypes.VACATION,
|
||||
// comment: dto.comment ?? "",
|
||||
// approval_status: dto.approval_status ?? LeaveApprovalStatus.PENDING,
|
||||
// date,
|
||||
// },
|
||||
// select: leaveRequestsSelect,
|
||||
// });
|
||||
|
||||
const hours = Number(row.payable_hours ?? row.requested_hours ?? 0);
|
||||
if (row.approval_status === LeaveApprovalStatus.APPROVED) {
|
||||
// await this.leaveUtils.syncShift(email, employee_id, iso, hours, LeaveTypes.VACATION, row.comment);
|
||||
}
|
||||
created.push({ ...mapRowToView(row), action: "create" });
|
||||
}
|
||||
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, iso, hours, LeaveTypes.VACATION, row.comment);
|
||||
// }
|
||||
// created.push({ ...mapRowToView(row), action: "create" });
|
||||
// }
|
||||
// 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 { 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 { 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 */
|
||||
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 { 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 { 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 { 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 { 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";
|
||||
|
|
@ -33,7 +34,8 @@ import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-i
|
|||
providers: [
|
||||
GetTimesheetsOverviewService,
|
||||
ShiftsGetService,
|
||||
ShiftsUpsertService,
|
||||
ShiftsCreateService,
|
||||
ShiftsUpdateDeleteService,
|
||||
ExpenseUpsertService,
|
||||
SchedulePresetsUpsertService,
|
||||
SchedulePresetsGetService,
|
||||
|
|
|
|||
|
|
@ -5,23 +5,24 @@ import { PrismaService } from "src/prisma/prisma.service";
|
|||
import { ApplyResult } from "src/time-and-attendance/utils/type.utils";
|
||||
import { WEEKDAY } from "src/time-and-attendance/utils/mappers.utils";
|
||||
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
|
||||
import { Result } from "src/common/errors/result-error.factory";
|
||||
|
||||
|
||||
@Injectable()
|
||||
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> {
|
||||
if(!id) throw new BadRequestException(`Schedule preset with id: ${id} not found`);
|
||||
if(!DATE_ISO_FORMAT.test(start_date_iso)) throw new BadRequestException('start_date must be of format :YYYY-MM-DD');
|
||||
async applyToTimesheet(email: string, id: number, start_date_iso: string): Promise<Result<ApplyResult, string>> {
|
||||
if (!DATE_ISO_FORMAT.test(start_date_iso)) return { success: false, error: 'start_date must be of format :YYYY-MM-DD' };
|
||||
|
||||
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({
|
||||
where: { employee_id, id },
|
||||
include: {
|
||||
where: { employee_id: employee_id.data, id },
|
||||
include: {
|
||||
shifts: {
|
||||
orderBy: [{ week_day: 'asc'}, { sort_order: 'asc'}],
|
||||
orderBy: [{ week_day: 'asc' }, { sort_order: 'asc' }],
|
||||
select: {
|
||||
id: true,
|
||||
week_day: true,
|
||||
|
|
@ -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 timesheet = await this.prisma.timesheets.upsert({
|
||||
where: { employee_id_start_date: { employee_id, start_date: start_date} },
|
||||
const timesheet = await this.prisma.timesheets.upsert({
|
||||
where: { employee_id_start_date: { employee_id: employee_id.data, start_date: start_date } },
|
||||
update: {},
|
||||
create: { employee_id, start_date: start_date },
|
||||
create: { employee_id: employee_id.data, start_date: start_date },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
|
|
@ -52,22 +54,22 @@ export class SchedulePresetsApplyService {
|
|||
index_by_day.set(shift.week_day, list);
|
||||
}
|
||||
|
||||
const addDays = (date: Date, days: number) =>
|
||||
const addDays = (date: Date, days: number) =>
|
||||
new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate() + days));
|
||||
|
||||
const overlaps = (aStart: Date, aEnd: Date, bStart: Date, bEnd: Date) =>
|
||||
aStart.getTime() < bEnd.getTime() && aEnd.getTime() > bStart.getTime();
|
||||
|
||||
|
||||
let created = 0;
|
||||
let skipped = 0;
|
||||
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
for(let i = 0; i < 7; i++) {
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const date = addDays(start_date, i);
|
||||
const week_day = WEEKDAY[date.getUTCDay()];
|
||||
const shifts = index_by_day.get(week_day) ?? [];
|
||||
|
||||
if(shifts.length === 0) continue;
|
||||
|
||||
if (shifts.length === 0) continue;
|
||||
|
||||
const existing = await tx.shifts.findMany({
|
||||
where: { timesheet_id: timesheet.id, date: date },
|
||||
|
|
@ -83,24 +85,23 @@ export class SchedulePresetsApplyService {
|
|||
|
||||
const payload: Prisma.ShiftsCreateManyInput[] = [];
|
||||
|
||||
for(const shift of shifts) {
|
||||
if(shift.end_time.getTime() <= shift.start_time.getTime()) {
|
||||
throw new ConflictException(`Invalid time range in preset day: ${week_day}, order: ${shift.sort_order}`);
|
||||
for (const shift of shifts) {
|
||||
if (shift.end_time.getTime() <= shift.start_time.getTime()) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Invalid time range in preset day: ${week_day}, order: ${shift.sort_order}`
|
||||
};
|
||||
}
|
||||
const conflict = existing.find((existe)=> overlaps(
|
||||
shift.start_time, shift.end_time ,
|
||||
const conflict = existing.find((existe) => overlaps(
|
||||
shift.start_time, shift.end_time,
|
||||
existe.start_time, existe.end_time,
|
||||
));
|
||||
if(conflict) {
|
||||
throw new ConflictException({
|
||||
error_code: 'SHIFT_OVERLAP_WITH_EXISTING',
|
||||
mesage: `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),
|
||||
},
|
||||
});
|
||||
}
|
||||
if (conflict)
|
||||
return {
|
||||
success: false,
|
||||
error: `[SHIFT_OVERLAP] :Preset shift overlaps existing shift on ${start_date} + ${i}(week day ${week_day}) `
|
||||
};
|
||||
|
||||
payload.push({
|
||||
timesheet_id: timesheet.id,
|
||||
date: date,
|
||||
|
|
@ -111,13 +112,13 @@ export class SchedulePresetsApplyService {
|
|||
bank_code_id: shift.bank_code_id,
|
||||
});
|
||||
}
|
||||
if(payload.length) {
|
||||
if (payload.length) {
|
||||
const response = await tx.shifts.createMany({ data: payload, skipDuplicates: true });
|
||||
created += response.count;
|
||||
skipped += payload.length - response.count;
|
||||
}
|
||||
}
|
||||
});
|
||||
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 { PrismaService } from "src/prisma/prisma.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
|
||||
import { Result } from "src/common/errors/result-error.factory";
|
||||
|
||||
@Injectable()
|
||||
export class SchedulePresetsGetService {
|
||||
|
|
@ -11,11 +11,12 @@ export class SchedulePresetsGetService {
|
|||
private readonly emailResolver: EmailToIdResolver,
|
||||
){}
|
||||
|
||||
async getSchedulePresets(email: string): Promise<PresetResponse[]> {
|
||||
async getSchedulePresets(email: string): Promise<Result<PresetResponse[], string>> {
|
||||
try {
|
||||
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({
|
||||
where: { employee_id },
|
||||
where: { employee_id: employee_id.data },
|
||||
orderBy: [{is_default: 'desc' }, { name: 'asc' }],
|
||||
include: {
|
||||
shifts: {
|
||||
|
|
@ -39,10 +40,9 @@ export class SchedulePresetsGetService {
|
|||
type: shift.bank_code?.type,
|
||||
})),
|
||||
}));
|
||||
return response;
|
||||
} catch ( error: unknown) {
|
||||
if(error instanceof Prisma.PrismaClientKnownRequestError) {}
|
||||
throw error;
|
||||
return { success: true, data:response};
|
||||
} catch ( error) {
|
||||
return { success: false, error: `Schedule presets for employee with email ${email} not found`};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
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 { 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 { 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 { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
|
||||
import { Result } from "src/common/errors/result-error.factory";
|
||||
|
||||
@Injectable()
|
||||
export class SchedulePresetsUpsertService {
|
||||
|
|
@ -17,41 +17,42 @@ export class SchedulePresetsUpsertService {
|
|||
//_________________________________________________________________
|
||||
// CREATE
|
||||
//_________________________________________________________________
|
||||
async createPreset(email: string, dto: SchedulePresetsDto): Promise<CreatePresetResult> {
|
||||
async createPreset(email: string, dto: SchedulePresetsDto): Promise<Result<SchedulePresetsDto, string>> {
|
||||
try {
|
||||
const shifts_data = await this.resolveAndBuildPresetShifts(dto);
|
||||
const employee_id = await this.emailResolver.findIdByEmail(email);
|
||||
if (!shifts_data) throw new BadRequestException(`Employee with email: ${email} or dto not found`);
|
||||
const shifts_data = await this.normalizePresetShifts(dto);
|
||||
if (!shifts_data.success) return { success: false, error: `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) {
|
||||
await tx.schedulePresets.updateMany({
|
||||
where: { is_default: true, employee_id },
|
||||
where: { is_default: true, employee_id: employee_id.data },
|
||||
data: { is_default: false },
|
||||
});
|
||||
await tx.schedulePresets.create({
|
||||
data: {
|
||||
id: dto.id,
|
||||
employee_id: employee_id.data,
|
||||
name: dto.name,
|
||||
is_default: !!dto.is_default,
|
||||
shifts: { create: shifts_data.data },
|
||||
},
|
||||
});
|
||||
return { success: true, data: created }
|
||||
}
|
||||
const created = await tx.schedulePresets.create({
|
||||
data: {
|
||||
id: dto.id,
|
||||
employee_id,
|
||||
name: dto.name,
|
||||
is_default: !!dto.is_default,
|
||||
shifts: { create: shifts_data },
|
||||
},
|
||||
});
|
||||
return created;
|
||||
});
|
||||
return { ok: true };
|
||||
|
||||
} catch (error: unknown) {
|
||||
return { ok: false, error };
|
||||
return { success: true, data: created }
|
||||
} catch (error) {
|
||||
return { success: false, error: ' An error occured during create. Invalid Schedule data' };
|
||||
}
|
||||
}
|
||||
|
||||
//_________________________________________________________________
|
||||
// UPDATE
|
||||
//_________________________________________________________________
|
||||
async updatePreset(preset_id: number, dto: SchedulePresetsDto): Promise<UpdatePresetResult> {
|
||||
async updatePreset(preset_id: number, dto: SchedulePresetsDto): Promise<Result<SchedulePresetsDto, string>> {
|
||||
try {
|
||||
const existing = await this.prisma.schedulePresets.findFirst({
|
||||
where: { id: preset_id },
|
||||
|
|
@ -61,9 +62,11 @@ export class SchedulePresetsUpsertService {
|
|||
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) => {
|
||||
if (typeof dto.is_default === 'boolean') {
|
||||
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 } });
|
||||
|
||||
const create_many_data: Prisma.SchedulePresetShiftsCreateManyInput[] =
|
||||
shifts_data.map((shift) => {
|
||||
if (!shift.bank_code || !('connect' in shift.bank_code) || typeof shift.bank_code.connect?.id !== 'number') {
|
||||
throw new NotFoundException(`Bank code is required for updates( ${shift.week_day}, ${shift.sort_order})`);
|
||||
}
|
||||
const bank_code_id = shift.bank_code.connect.id;
|
||||
return {
|
||||
preset_id: existing.id,
|
||||
week_day: shift.week_day,
|
||||
sort_order: shift.sort_order,
|
||||
start_time: shift.start_time,
|
||||
end_time: shift.end_time,
|
||||
is_remote: shift.is_remote ?? false,
|
||||
bank_code_id: bank_code_id,
|
||||
};
|
||||
});
|
||||
await tx.schedulePresetShifts.createMany({ data: create_many_data });
|
||||
// try {
|
||||
// const create_many_data: Result<Prisma.SchedulePresetShiftsCreateManyInput[], string> =
|
||||
// shifts_data.data.map((shift) => {
|
||||
// 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 {
|
||||
// preset_id: existing.id,
|
||||
// week_day: shift.week_day,
|
||||
// sort_order: shift.sort_order,
|
||||
// start_time: shift.start_time,
|
||||
// end_time: shift.end_time,
|
||||
// is_remote: shift.is_remote ?? false,
|
||||
// bank_code_id: bank_code_id,
|
||||
// };
|
||||
// });
|
||||
// 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({
|
||||
|
|
@ -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 = {
|
||||
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) {
|
||||
return { ok: false, id: preset_id, error }
|
||||
return { success: false, error: 'An error occured during update. Invalid data' }
|
||||
}
|
||||
}
|
||||
|
||||
//_________________________________________________________________
|
||||
// DELETE
|
||||
//_________________________________________________________________
|
||||
async deletePreset(preset_id: number): Promise<DeletePresetResult> {
|
||||
async deletePreset(preset_id: number): Promise<Result<number, string>> {
|
||||
try {
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
const preset = await tx.schedulePresets.findFirst({
|
||||
where: { id: preset_id },
|
||||
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 } });
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
return { ok: true, id: preset_id };
|
||||
return { success: true, data: preset_id };
|
||||
|
||||
} catch (error) {
|
||||
if (error) throw new NotFoundException(`Preset schedule with id ${preset_id} not found`);
|
||||
return { ok: false, id: preset_id, error };
|
||||
return { success: false, error: `Preset schedule with id ${preset_id} not found` };
|
||||
}
|
||||
}
|
||||
|
||||
//PRIVATE HELPERS
|
||||
|
||||
//resolve bank_code_id using type and convert hours to TIME and valid shifts end/start
|
||||
private async resolveAndBuildPresetShifts(
|
||||
private async normalizePresetShifts(
|
||||
dto: SchedulePresetsDto
|
||||
): Promise<Prisma.SchedulePresetShiftsCreateWithoutPresetInput[]> {
|
||||
|
||||
): Promise<Result<Prisma.SchedulePresetShiftsCreateWithoutPresetInput[], string>> {
|
||||
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 bank_code_set = new Map<string, number>();
|
||||
|
||||
for (const type of types) {
|
||||
const { id } = await this.typeResolver.findIdAndModifierByType(type);
|
||||
bank_code_set.set(type, id)
|
||||
const bank_code = await this.typeResolver.findIdAndModifierByType(type);
|
||||
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>();
|
||||
for (const shift of dto.preset_shifts) {
|
||||
|
|
@ -195,8 +203,8 @@ export class SchedulePresetsUpsertService {
|
|||
if (!shift.start_time || !shift.end_time) {
|
||||
throw new BadRequestException(`start_time and end_time are required for (${shift.week_day}, ${shift.sort_order})`);
|
||||
}
|
||||
const start = toTime(shift.start_time);
|
||||
const end = toTime(shift.end_time);
|
||||
const start = toDateFromString(shift.start_time);
|
||||
const end = toDateFromString(shift.end_time);
|
||||
if (end.getTime() <= start.getTime()) {
|
||||
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,
|
||||
};
|
||||
});
|
||||
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 { 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 { 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')
|
||||
@RolesAllowed(...GLOBAL_CONTROLLER_ROLES)
|
||||
export class ShiftController {
|
||||
constructor(
|
||||
private readonly upsert_service: ShiftsUpsertService,
|
||||
private readonly create_service: ShiftsCreateService,
|
||||
private readonly update_delete_service: ShiftsUpdateDeleteService,
|
||||
){}
|
||||
|
||||
@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 list = Array.isArray(dtos) ? dtos : [];
|
||||
if(list.length === 0) throw new BadRequestException('Body is missing or invalid (create shifts)');
|
||||
return this.upsert_service.createShifts(email, dtos)
|
||||
return this.create_service.createOneOrManyShifts(email, dtos)
|
||||
}
|
||||
|
||||
@Patch('update')
|
||||
updateBatch( @Body() dtos: ShiftDto[]): Promise<UpdateShiftResult[]>{
|
||||
const list = Array.isArray(dtos) ? dtos: [];
|
||||
if(list.length === 0) throw new BadRequestException('Body is missing or invalid (update shifts)');
|
||||
return this.upsert_service.updateShifts(dtos);
|
||||
updateBatch( @Body() dtos: ShiftDto[]): Promise<Result<ShiftDto[], string>>{
|
||||
return this.update_delete_service.updateOneOrManyShifts(dtos);
|
||||
}
|
||||
|
||||
@Delete(':shift_id')
|
||||
remove(@Param('shift_id') shift_id: number ) {
|
||||
return this.upsert_service.deleteShift(shift_id);
|
||||
remove(@Param('shift_id') shift_id: number ): Promise<Result<number, string>> {
|
||||
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 { 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({
|
||||
controllers: [ShiftController],
|
||||
providers: [ ShiftsUpsertService ],
|
||||
exports: [ ShiftsUpsertService ],
|
||||
providers: [ ShiftsCreateService, ShiftsUpdateDeleteService ],
|
||||
exports: [ ShiftsCreateService, ShiftsUpdateDeleteService ],
|
||||
})
|
||||
export class ShiftsModule {}
|
||||
|
|
|
|||
|
|
@ -1,67 +1,90 @@
|
|||
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 { Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { TotalExpenses, TotalHours } from "src/time-and-attendance/utils/type.utils";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
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()
|
||||
export class GetTimesheetsOverviewService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly emailResolver : EmailToIdResolver,
|
||||
private readonly emailResolver: EmailToIdResolver,
|
||||
) { }
|
||||
|
||||
//-----------------------------------------------------------------------------------
|
||||
// GET TIMESHEETS FOR A SELECTED EMPLOYEE
|
||||
//-----------------------------------------------------------------------------------
|
||||
async getTimesheetsForEmployeeByPeriod(email: string, pay_year: number, pay_period_no: number) {
|
||||
//find period using year and 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`);
|
||||
async getTimesheetsForEmployeeByPeriod(email: string, pay_year: number, pay_period_no: number): Promise<Result<Timesheets, string>> {
|
||||
try { //find period using year and period_no
|
||||
const period = await this.prisma.payPeriods.findFirst({ where: { pay_year, pay_period_no } });
|
||||
if (!period) return { success: false, error: `Pay period ${pay_year}-${pay_period_no} not found`};
|
||||
|
||||
//fetch the employee_id using the email
|
||||
const employee_id = await this.emailResolver.findIdByEmail(email);
|
||||
//loads the timesheets related to the fetched pay-period
|
||||
const timesheet_range = { employee_id, start_date: { gte: period.period_start, lte: period.period_end } };
|
||||
let rows = await this.loadTimesheets(timesheet_range);
|
||||
//fetch the employee_id using the email
|
||||
const employee_id = await this.emailResolver.findIdByEmail(email);
|
||||
if (!employee_id.success) return { success: false, error: employee_id.error }
|
||||
|
||||
//Normalized dates from pay-period
|
||||
const normalized_start = toDateFromString(period.period_start);
|
||||
const normalized_end = toDateFromString(period.period_end);
|
||||
|
||||
//creates empty timesheet to make sure to return desired amount of timesheet
|
||||
for (let i = 0; i < NUMBER_OF_TIMESHEETS_TO_RETURN; i++) {
|
||||
const week_start = new Date(normalized_start);
|
||||
week_start.setUTCDate(week_start.getUTCDate() + i * 7);
|
||||
//loads the timesheets related to the fetched pay-period
|
||||
const timesheet_range = { employee_id, start_date: { gte: period.period_start, lte: period.period_end } };
|
||||
let rows = await this.loadTimesheets(timesheet_range);
|
||||
|
||||
if (week_start.getTime() > normalized_end.getTime()) break;
|
||||
//Normalized dates from pay-period
|
||||
const normalized_start = toDateFromString(period.period_start);
|
||||
const normalized_end = toDateFromString(period.period_end);
|
||||
|
||||
const exists = rows.some(
|
||||
(row) => toDateFromString(row.start_date).getTime() === week_start.getTime()
|
||||
);
|
||||
if (!exists) await this.ensureTimesheet(employee_id, week_start);
|
||||
//creates empty timesheet to make sure to return desired amount of timesheet
|
||||
for (let i = 0; i < NUMBER_OF_TIMESHEETS_TO_RETURN; i++) {
|
||||
const week_start = new Date(normalized_start);
|
||||
week_start.setUTCDate(week_start.getUTCDate() + i * 7);
|
||||
|
||||
if (week_start.getTime() > normalized_end.getTime()) break;
|
||||
|
||||
const exists = rows.some(
|
||||
(row) => toDateFromString(row.start_date).getTime() === week_start.getTime()
|
||||
);
|
||||
if (!exists) await this.ensureTimesheet(employee_id.data, week_start);
|
||||
}
|
||||
rows = await this.loadTimesheets(timesheet_range);
|
||||
|
||||
|
||||
//find user infos using the employee_id
|
||||
const employee = await this.prisma.employees.findUnique({
|
||||
where: { id: employee_id.data },
|
||||
include: { user: true },
|
||||
});
|
||||
if (!employee) return { success: false, error:`Employee #${employee_id} not found`}
|
||||
|
||||
//builds employee full name
|
||||
const user = employee.user;
|
||||
const employee_fullname = `${user.first_name} ${user.last_name}`.trim();
|
||||
|
||||
//maps all timesheet's infos
|
||||
const timesheets = await Promise.all(rows.map((timesheet) => this.mapOneTimesheet(timesheet)));
|
||||
|
||||
return { success: true, data:{ employee_fullname, timesheets} };
|
||||
} catch (error) {
|
||||
return { success: false, error}
|
||||
}
|
||||
rows = await this.loadTimesheets(timesheet_range);
|
||||
|
||||
|
||||
//find user infos using the employee_id
|
||||
const employee = await this.prisma.employees.findUnique({
|
||||
where: { id: employee_id },
|
||||
include: { user: true },
|
||||
});
|
||||
if (!employee) throw new NotFoundException(`Employee #${employee_id} not found`);
|
||||
|
||||
//builds employee full name
|
||||
const user = employee.user;
|
||||
const employee_fullname = `${user.first_name} ${user.last_name}`.trim();
|
||||
|
||||
//maps all timesheet's infos
|
||||
const timesheets = rows.map((timesheet) => this.mapOneTimesheet(timesheet));
|
||||
return { employee_fullname, timesheets };
|
||||
}
|
||||
|
||||
|
||||
//-----------------------------------------------------------------------------------
|
||||
// MAPPERS & HELPERS
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
|
@ -104,11 +127,14 @@ export class GetTimesheetsOverviewService {
|
|||
const weekly_hours: TotalHours[] = [emptyHours()];
|
||||
const weekly_expenses: TotalExpenses[] = [emptyExpenses()];
|
||||
|
||||
|
||||
|
||||
//map of days
|
||||
const days = day_dates.map((date) => {
|
||||
const date_iso = toStringFromDate(date);
|
||||
const shifts_source = shifts_by_date.get(date_iso) ?? [];
|
||||
const expenses_source = expenses_by_date.get(date_iso) ?? [];
|
||||
|
||||
//inner map of shifts
|
||||
const shifts = shifts_source.map((shift) => ({
|
||||
timesheet_id: shift.timesheet_id,
|
||||
|
|
@ -132,6 +158,7 @@ export class GetTimesheetsOverviewService {
|
|||
is_approved: expense.is_approved ?? false,
|
||||
comment: expense.comment ?? '',
|
||||
supervisor_comment: expense.supervisor_comment,
|
||||
type: expense.type,
|
||||
}));
|
||||
|
||||
//daily totals
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ export const toHHmmFromDate = (input: Date | string): string => {
|
|||
return `${hh}:${mm}`;
|
||||
}
|
||||
|
||||
//converts Date format to string
|
||||
//converts to Date format from string
|
||||
export const toDateFromString = (ymd: string | Date): Date => {
|
||||
return new Date(ymd);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,44 +1,47 @@
|
|||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { Prisma, PrismaClient } from "@prisma/client";
|
||||
import { Result } from "src/common/errors/result-error.factory";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
|
||||
type Tx = Prisma.TransactionClient | PrismaClient;
|
||||
|
||||
@Injectable()
|
||||
export class BankCodesResolver {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
constructor(private readonly prisma: PrismaService) { }
|
||||
|
||||
//find id and modifier by type
|
||||
readonly findIdAndModifierByType = async ( type: string, client?: Tx
|
||||
): Promise<{id:number; modifier: number }> => {
|
||||
readonly findIdAndModifierByType = async (type: string, client?: Tx
|
||||
): Promise<Result<{ id: number; modifier: number }, string>> => {
|
||||
const db = client ?? this.prisma;
|
||||
const bank = await db.bankCodes.findFirst({
|
||||
where: { type },
|
||||
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 { id: bank.id, modifier: bank.modifier };
|
||||
return { success: true, data: { id: bank.id, modifier: bank.modifier } };
|
||||
};
|
||||
|
||||
//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 bank_code_id = await db.bankCodes.findFirst({
|
||||
const bank_code = await db.bankCodes.findFirst({
|
||||
where: { type },
|
||||
select: {id: true},
|
||||
select: { id: true },
|
||||
});
|
||||
if(!bank_code_id) throw new NotFoundException(`Unkown bank type: ${type}`);
|
||||
return bank_code_id;
|
||||
if (!bank_code) return { success: false, error:`Unkown bank type: ${type}`};
|
||||
|
||||
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 type = await db.bankCodes.findFirst({
|
||||
const bank_code = await db.bankCodes.findFirst({
|
||||
where: { id: bank_code_id },
|
||||
select: { type: true },
|
||||
});
|
||||
if(!type) throw new NotFoundException(`Type with id : ${bank_code_id} not found`);
|
||||
return type;
|
||||
if (!bank_code) return {success: false, error: `Type with id : ${bank_code_id} not found` }
|
||||
|
||||
return {success: true, data: bank_code.type};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,35 +1,35 @@
|
|||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { Prisma, PrismaClient } from "@prisma/client";
|
||||
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;
|
||||
|
||||
@Injectable()
|
||||
export class EmailToIdResolver {
|
||||
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
constructor(private readonly prisma: PrismaService) { }
|
||||
|
||||
// find employee_id using email
|
||||
readonly findIdByEmail = async ( email: string, client?: Tx
|
||||
): Promise<number> => {
|
||||
readonly findIdByEmail = async (email: string, client?: Tx): Promise<Result<number, string>> => {
|
||||
const db = client ?? this.prisma;
|
||||
const employee = await db.employees.findFirst({
|
||||
where: { user: { email } },
|
||||
select: { id: true },
|
||||
});
|
||||
if(!employee)throw new NotFoundException(`Employee with email: ${email} not found`);
|
||||
return employee.id;
|
||||
if (!employee) return { success: false, error: `Employee with email:${email} not found` };
|
||||
return { data: employee.id, success: true };
|
||||
}
|
||||
|
||||
|
||||
// find user_id using email
|
||||
readonly resolveUserIdWithEmail = async (email: string, client?: Tx
|
||||
): Promise<string> => {
|
||||
): Promise<Result<string, string>> => {
|
||||
const db = client ?? this.prisma;
|
||||
const user = await db.users.findFirst({
|
||||
where: { email },
|
||||
select: { id: true },
|
||||
});
|
||||
if(!user) throw new NotFoundException(`User with email ${ email } not found`);
|
||||
return user.id;
|
||||
if (!user) return { success: false, error: `User with email:${email} not found` };
|
||||
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 { Result } from "src/common/errors/result-error.factory";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
|
||||
type Tx = Prisma.TransactionClient | PrismaClient;
|
||||
|
|
@ -8,15 +9,15 @@ type Tx = Prisma.TransactionClient | PrismaClient;
|
|||
export class FullNameResolver {
|
||||
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 employee = await db.employees.findUnique({
|
||||
where: { id: employee_id },
|
||||
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 ) || " ";
|
||||
return full_name;
|
||||
return {success: true, data: full_name };
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,23 @@
|
|||
import { Prisma, PrismaClient } from "@prisma/client";
|
||||
import { NotFoundException } from "@nestjs/common";
|
||||
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;
|
||||
|
||||
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 {
|
||||
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 shift = await db.shifts.findFirst({
|
||||
where: {
|
||||
|
|
@ -23,7 +32,8 @@ export class ShiftIdResolver {
|
|||
select: { id: true },
|
||||
});
|
||||
|
||||
if(!shift) throw new NotFoundException(`shift not found`);
|
||||
return { id: shift.id };
|
||||
if(!shift) return { success: false, error: `shift not found`}
|
||||
|
||||
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 { PrismaService } from "src/prisma/prisma.service";
|
||||
import { EmailToIdResolver } from "./resolve-email-id.utils";
|
||||
import { Result } from "src/common/errors/result-error.factory";
|
||||
|
||||
|
||||
type Tx = Prisma.TransactionClient | PrismaClient;
|
||||
|
|
@ -14,15 +15,16 @@ export class EmployeeTimesheetResolver {
|
|||
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 employee_id = await this.emailResolver.findIdByEmail(email);
|
||||
if(!employee_id.success) return { success: false, error: employee_id.error}
|
||||
const start_date = weekStartSunday(date);
|
||||
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 },
|
||||
});
|
||||
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;
|
||||
|
||||
|
||||
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 = {
|
||||
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 = {
|
||||
date: Date;
|
||||
start_time: Date;
|
||||
end_time: Date;
|
||||
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 = {
|
||||
date: Date;
|
||||
|
|
@ -51,9 +13,6 @@ export type NormalizedExpense = {
|
|||
parsed_mileage?: 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 = {
|
||||
week_day: string;
|
||||
|
|
@ -76,33 +35,3 @@ export type ApplyResult = {
|
|||
created: 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