Merge branch 'dev/matthieu/error-handling' of git.targo.ca:Targo/targo_backend

This commit is contained in:
Matthieu Haineault 2025-11-12 09:21:42 -05:00
commit d88f8727ad
34 changed files with 1200 additions and 1325 deletions

0
npm
View File

View 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 };
// }

View File

@ -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 };
}
}

View File

@ -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 {

View File

@ -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);
}
}

View File

@ -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,12 +133,12 @@ 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,

View File

@ -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 };
// }
}
// }

View File

@ -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 {}

View File

@ -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;

View File

@ -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 } };
// }
// }

View File

@ -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 };
// }
// }

View File

@ -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 };
// }
// }

View File

@ -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 };
// }
// }

View File

@ -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 {

View File

@ -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,

View File

@ -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 },
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 },
});
@ -62,12 +64,12 @@ export class SchedulePresetsApplyService {
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 } };
}
}

View File

@ -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`};
}
}

View File

@ -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 };
}
}

View File

@ -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);
}
}

View File

@ -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} };
}
}

View File

@ -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 }
}
}

View File

@ -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 };
}
}

View File

@ -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 {}

View File

@ -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);
//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);
//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);
//Normalized dates from pay-period
const normalized_start = toDateFromString(period.period_start);
const normalized_end = toDateFromString(period.period_end);
if (week_start.getTime() > normalized_end.getTime()) break;
//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);
const exists = rows.some(
(row) => toDateFromString(row.start_date).getTime() === week_start.getTime()
);
if (!exists) await this.ensureTimesheet(employee_id, week_start);
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

View File

@ -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);
}

View File

@ -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};
}
}

View File

@ -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 };
}
}

View File

@ -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 };
}
}

View File

@ -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 };
};
}

View File

@ -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} };
}
}

View File

@ -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,

View File

@ -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;
}

View File

0
tsx
View File