refactor(shifts): modified return and switched bank_code_id for types

This commit is contained in:
Matthieu Haineault 2025-11-04 08:31:38 -05:00
parent bdbec4f68c
commit 6adb614931
22 changed files with 203 additions and 118 deletions

View File

@ -279,6 +279,20 @@
] ]
} }
}, },
"/timesheets/timesheet-approval": {
"patch": {
"operationId": "TimesheetController_approveTimesheet",
"parameters": [],
"responses": {
"200": {
"description": ""
}
},
"tags": [
"Timesheet"
]
}
},
"/preferences": { "/preferences": {
"patch": { "patch": {
"operationId": "PreferencesController_updatePreferences", "operationId": "PreferencesController_updatePreferences",

View File

@ -18,14 +18,14 @@ export abstract class BaseApprovalService<T> {
//returns the corresponding Prisma delegate //returns the corresponding Prisma delegate
protected abstract get delegate(): UpdatableDelegate<T>; protected abstract get delegate(): UpdatableDelegate<T>;
protected abstract delegateFor(transaction: Prisma.TransactionClient): UpdatableDelegate<T>; protected abstract delegateFor(tx: Prisma.TransactionClient): UpdatableDelegate<T>;
//standard update Aproval //standard update Aproval
async updateApproval(id: number, isApproved: boolean): Promise<T> { async updateApproval(id: number, is_approved: boolean): Promise<T> {
try{ try{
return await this.delegate.update({ return await this.delegate.update({
where: { id }, where: { id },
data: { is_approved: isApproved }, data: { is_approved: is_approved },
}); });
}catch (error: any) { }catch (error: any) {
if (error instanceof PrismaClientKnownRequestError && error.code === "P2025") { if (error instanceof PrismaClientKnownRequestError && error.code === "P2025") {
@ -36,11 +36,11 @@ export abstract class BaseApprovalService<T> {
} }
//approval with transaction to avoid many requests //approval with transaction to avoid many requests
async updateApprovalWithTransaction(transaction: Prisma.TransactionClient, id: number, isApproved: boolean): Promise<T> { async updateApprovalWithTransaction(tx: Prisma.TransactionClient, id: number, is_approved: boolean): Promise<T> {
try { try {
return await this.delegateFor(transaction).update({ return await this.delegateFor(tx).update({
where: { id }, where: { id },
data: { is_approved: isApproved }, data: { is_approved: is_approved },
}); });
} catch (error: any ){ } catch (error: any ){
if(error instanceof PrismaClientKnownRequestError && error.code === 'P2025') { if(error instanceof PrismaClientKnownRequestError && error.code === 'P2025') {

View File

@ -26,7 +26,7 @@ export class HolidayLeaveRequestsService {
async create(dto: UpsertLeaveRequestDto): Promise<UpsertResult> { async create(dto: UpsertLeaveRequestDto): Promise<UpsertResult> {
const email = dto.email.trim(); const email = dto.email.trim();
const employee_id = await this.emailResolver.findIdByEmail(email); const employee_id = await this.emailResolver.findIdByEmail(email);
const bank_code = await this.typeResolver.findByType(LeaveTypes.HOLIDAY); const bank_code = await this.typeResolver.findIdAndModifierByType(LeaveTypes.HOLIDAY);
const dates = normalizeDates(dto.dates); const dates = normalizeDates(dto.dates);
if (!bank_code) throw new NotFoundException(`bank_code not found`); if (!bank_code) throw new NotFoundException(`bank_code not found`);
if (!dates.length) throw new BadRequestException('Dates array must not be empty'); if (!dates.length) throw new BadRequestException('Dates array must not be empty');

View File

@ -95,7 +95,7 @@ export class LeaveRequestsService {
async update(dto: UpsertLeaveRequestDto, type: LeaveTypes): Promise<UpsertResult> { async update(dto: UpsertLeaveRequestDto, type: LeaveTypes): Promise<UpsertResult> {
const email = dto.email.trim(); const email = dto.email.trim();
const employee_id = await this.emailResolver.findIdByEmail(email); const employee_id = await this.emailResolver.findIdByEmail(email);
const bank_code = await this.typeResolver.findByType(type); const bank_code = await this.typeResolver.findIdAndModifierByType(type);
if(!bank_code) throw new NotFoundException(`bank_code not found`); if(!bank_code) throw new NotFoundException(`bank_code not found`);
const modifier = Number(bank_code.modifier ?? 1); const modifier = Number(bank_code.modifier ?? 1);
const dates = normalizeDates(dto.dates); const dates = normalizeDates(dto.dates);

View File

@ -25,7 +25,7 @@ export class SickLeaveRequestsService {
async create(dto: UpsertLeaveRequestDto): Promise<UpsertResult> { async create(dto: UpsertLeaveRequestDto): Promise<UpsertResult> {
const email = dto.email.trim(); const email = dto.email.trim();
const employee_id = await this.emailResolver.findIdByEmail(email); const employee_id = await this.emailResolver.findIdByEmail(email);
const bank_code = await this.typeResolver.findByType(LeaveTypes.SICK); const bank_code = await this.typeResolver.findIdAndModifierByType(LeaveTypes.SICK);
if(!bank_code) throw new NotFoundException(`bank_code not found`); if(!bank_code) throw new NotFoundException(`bank_code not found`);
const modifier = bank_code.modifier ?? 1; const modifier = bank_code.modifier ?? 1;

View File

@ -24,7 +24,7 @@ export class VacationLeaveRequestsService {
async create(dto: UpsertLeaveRequestDto): Promise<UpsertResult> { async create(dto: UpsertLeaveRequestDto): Promise<UpsertResult> {
const email = dto.email.trim(); const email = dto.email.trim();
const employee_id = await this.emailResolver.findIdByEmail(email); const employee_id = await this.emailResolver.findIdByEmail(email);
const bank_code = await this.typeResolver.findByType(LeaveTypes.VACATION); const bank_code = await this.typeResolver.findIdAndModifierByType(LeaveTypes.VACATION);
if(!bank_code) throw new NotFoundException(`bank_code not found`); if(!bank_code) throw new NotFoundException(`bank_code not found`);
const modifier = bank_code.modifier ?? 1; const modifier = bank_code.modifier ?? 1;

View File

@ -4,7 +4,6 @@ import { Module } from "@nestjs/common";
import { PayPeriodsCommandService } from "src/time-and-attendance/pay-period/services/pay-periods-command.service"; import { PayPeriodsCommandService } from "src/time-and-attendance/pay-period/services/pay-periods-command.service";
import { TimesheetsModule } from "src/time-and-attendance/time-tracker/timesheets/timesheets.module"; import { TimesheetsModule } from "src/time-and-attendance/time-tracker/timesheets/timesheets.module";
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
import { TimesheetApprovalService } from "src/time-and-attendance/time-tracker/timesheets/services/timesheet-approval.service";
@Module({ @Module({
imports:[TimesheetsModule], imports:[TimesheetsModule],
@ -13,7 +12,6 @@ import { TimesheetApprovalService } from "src/time-and-attendance/time-tracker/t
PayPeriodsQueryService, PayPeriodsQueryService,
PayPeriodsCommandService, PayPeriodsCommandService,
EmailToIdResolver, EmailToIdResolver,
TimesheetApprovalService,
], ],
}) })

View File

@ -8,7 +8,7 @@ import { TimesheetApprovalService } from "src/time-and-attendance/time-tracker/t
export class PayPeriodsCommandService { export class PayPeriodsCommandService {
constructor( constructor(
private readonly prisma: PrismaService, private readonly prisma: PrismaService,
private readonly timesheets_approval: TimesheetApprovalService, private readonly timesheetsApproval: TimesheetApprovalService,
private readonly query: PayPeriodsQueryService, private readonly query: PayPeriodsQueryService,
) {} ) {}
@ -49,7 +49,7 @@ export class PayPeriodsCommandService {
for(const item of items) { for(const item of items) {
const { period_start, period_end } = await getPeriod(item.pay_year, item.period_no); const { period_start, period_end } = await getPeriod(item.pay_year, item.period_no);
const t_sheets = await transaction.timesheets.findMany({ const timesheets = await transaction.timesheets.findMany({
where: { where: {
employee: { user: { email: item.employee_email } }, employee: { user: { email: item.employee_email } },
OR: [ OR: [
@ -60,8 +60,8 @@ export class PayPeriodsCommandService {
select: { id: true }, select: { id: true },
}); });
for(const { id } of t_sheets) { for(const { id } of timesheets) {
await this.timesheets_approval.cascadeApprovalWithtx(transaction, id, item.approve); await this.timesheetsApproval.cascadeApprovalWithtx(transaction, id, item.approve);
updated++; updated++;
} }

View File

@ -12,7 +12,9 @@ import { ShiftController } from "src/time-and-attendance/time-tracker/shifts/con
import { ShiftsGetService } from "src/time-and-attendance/time-tracker/shifts/services/shifts-get.service"; import { ShiftsGetService } from "src/time-and-attendance/time-tracker/shifts/services/shifts-get.service";
import { ShiftsUpsertService } from "src/time-and-attendance/time-tracker/shifts/services/shifts-upsert.service"; import { ShiftsUpsertService } from "src/time-and-attendance/time-tracker/shifts/services/shifts-upsert.service";
import { TimesheetController } from "src/time-and-attendance/time-tracker/timesheets/controllers/timesheet.controller"; import { TimesheetController } from "src/time-and-attendance/time-tracker/timesheets/controllers/timesheet.controller";
import { TimesheetApprovalService } from "src/time-and-attendance/time-tracker/timesheets/services/timesheet-approval.service";
import { GetTimesheetsOverviewService } from "src/time-and-attendance/time-tracker/timesheets/services/timesheet-get-overview.service"; import { GetTimesheetsOverviewService } from "src/time-and-attendance/time-tracker/timesheets/services/timesheet-get-overview.service";
import { TimesheetsModule } from "src/time-and-attendance/time-tracker/timesheets/timesheets.module";
import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils"; import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils";
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
@ -20,6 +22,7 @@ import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-i
imports: [ imports: [
BusinessLogicsModule, BusinessLogicsModule,
PayperiodsModule, PayperiodsModule,
TimesheetsModule,
], ],
controllers: [ controllers: [
TimesheetController, TimesheetController,
@ -37,6 +40,7 @@ import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-i
SchedulePresetsApplyService, SchedulePresetsApplyService,
EmailToIdResolver, EmailToIdResolver,
BankCodesResolver, BankCodesResolver,
TimesheetApprovalService,
], ],
exports: [], exports: [TimesheetApprovalService ],
}) export class TimeAndAttendanceModule { }; }) export class TimeAndAttendanceModule { };

View File

@ -171,7 +171,7 @@ export class SchedulePresetsUpsertService {
const bank_code_set = new Map<string, number>(); const bank_code_set = new Map<string, number>();
for (const type of types) { for (const type of types) {
const { id } = await this.typeResolver.findByType(type); const { id } = await this.typeResolver.findIdAndModifierByType(type);
bank_code_set.set(type, id) bank_code_set.set(type, id)
} }
const toTime = (hhmm: string) => new Date(`1970-01-01T${hhmm}:00.000Z`); const toTime = (hhmm: string) => new Date(`1970-01-01T${hhmm}:00.000Z`);

View File

@ -5,10 +5,14 @@ import { ShiftsUpsertService } from "src/time-and-attendance/time-tracker/shifts
import { CreateShiftResult, UpdateShiftResult } from "src/time-and-attendance/utils/type.utils"; import { CreateShiftResult, UpdateShiftResult } from "src/time-and-attendance/utils/type.utils";
import { Roles as RoleEnum } from '.prisma/client'; import { Roles as RoleEnum } from '.prisma/client';
import { RolesAllowed } from "src/common/decorators/roles.decorators"; import { RolesAllowed } from "src/common/decorators/roles.decorators";
import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils";
@Controller('shift') @Controller('shift')
export class ShiftController { export class ShiftController {
constructor( private readonly upsert_service: ShiftsUpsertService ){} constructor(
private readonly upsert_service: ShiftsUpsertService,
private readonly typeResolver: BankCodesResolver,
){}
@Post('create') @Post('create')
@RolesAllowed(RoleEnum.EMPLOYEE, RoleEnum.ACCOUNTING, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ADMIN) @RolesAllowed(RoleEnum.EMPLOYEE, RoleEnum.ACCOUNTING, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ADMIN)

View File

@ -3,7 +3,7 @@ import { IsBoolean, IsInt, IsOptional, IsString, MaxLength } from "class-validat
export class ShiftDto { export class ShiftDto {
@IsInt() @IsOptional() id: number; @IsInt() @IsOptional() id: number;
@IsInt() timesheet_id!: number; @IsInt() timesheet_id!: number;
@IsInt() bank_code_id!: number; @IsString() type!: string;
@IsString() date!: string; @IsString() date!: string;
@IsString() start_time!: string; @IsString() start_time!: string;

View File

@ -1,6 +1,6 @@
export class GetShiftDto { export class GetShiftDto {
timesheet_id: number; timesheet_id: number;
bank_code_id: number; type: string;
date: string; date: string;
start_time: string; start_time: string;
end_time: string; end_time: string;

View File

@ -44,7 +44,7 @@ export class ShiftsGetService {
const shift = row_by_id.get(id)!; const shift = row_by_id.get(id)!;
return { return {
timesheet_id: shift.timesheet_id, timesheet_id: shift.timesheet_id,
bank_code_id: shift.bank_code_id, type: shift.bank_code.type,
date: toStringFromDate(shift.date), date: toStringFromDate(shift.date),
start_time: toStringFromHHmm(shift.start_time), start_time: toStringFromHHmm(shift.start_time),
end_time: toStringFromHHmm(shift.end_time), end_time: toStringFromHHmm(shift.end_time),

View File

@ -7,7 +7,8 @@ import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-i
import { ShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-create.dto"; import { ShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-create.dto";
import { GetShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-get.dto"; import { GetShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-get.dto";
import { UpdateShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-update.dto"; import { UpdateShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-update.dto";
import { shift_select } from "src/time-and-attendance/utils/selects.utils"; 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";
@ -17,6 +18,7 @@ export class ShiftsUpsertService {
private readonly prisma: PrismaService, private readonly prisma: PrismaService,
private readonly overtime: OvertimeService, private readonly overtime: OvertimeService,
private readonly emailResolver: EmailToIdResolver, private readonly emailResolver: EmailToIdResolver,
private readonly typeResolver: BankCodesResolver,
) { } ) { }
//_________________________________________________________________ //_________________________________________________________________
@ -35,7 +37,7 @@ export class ShiftsUpsertService {
const normed_shifts = await Promise.all( const normed_shifts = await Promise.all(
dtos.map(async (dto, index) => { dtos.map(async (dto, index) => {
try { try {
const normed = this.normalizeShiftDto(dto); const normed = await this.normalizeShiftDto(dto);
if (normed.end_time <= normed.start_time) { if (normed.end_time <= normed.start_time) {
return { return {
index, index,
@ -45,18 +47,12 @@ export class ShiftsUpsertService {
}; };
} }
const start_date = weekStartSunday(normed.date); const timesheet = await this.prisma.timesheets.findUnique({
where: { id: dto.timesheet_id, employee_id },
const timesheet = await this.prisma.timesheets.findFirst({ select: timesheet_select,
where: { start_date, employee_id },
select: { id: true },
}); });
if (!timesheet) { if (!timesheet) {
return { return { index, error: new NotFoundException(`Timesheet not found`)};
index,
error: new NotFoundException(`Timesheet not found`),
};
} }
return { return {
@ -181,7 +177,7 @@ export class ShiftsUpsertService {
const row = await tx.shifts.create({ const row = await tx.shifts.create({
data: { data: {
timesheet_id: timesheet_id, timesheet_id: timesheet_id,
bank_code_id: dto.bank_code_id, bank_code_id: normed.id,
date: normed.date, date: normed.date,
start_time: normed.start_time, start_time: normed.start_time,
end_time: normed.end_time, end_time: normed.end_time,
@ -194,10 +190,12 @@ export class ShiftsUpsertService {
existing.push({ start_time: row.start_time, end_time: row.end_time }); existing.push({ start_time: row.start_time, end_time: row.end_time });
existing_map.set(map_key, existing); existing_map.set(map_key, existing);
const {type: bank_type} = await this.typeResolver.findTypeByBankCodeId(row.bank_code_id);
const summary = await this.overtime.getWeekOvertimeSummary(timesheet_id, normed.date, tx); const summary = await this.overtime.getWeekOvertimeSummary(timesheet_id, normed.date, tx);
const shift: GetShiftDto = { const shift: GetShiftDto = {
timesheet_id: timesheet_id, timesheet_id: timesheet_id,
bank_code_id: row.bank_code_id, type: bank_type,
date: toStringFromDate(row.date), date: toStringFromDate(row.date),
start_time: toStringFromHHmm(row.start_time), start_time: toStringFromHHmm(row.start_time),
end_time: toStringFromHHmm(row.end_time), end_time: toStringFromHHmm(row.end_time),
@ -227,7 +225,7 @@ export class ShiftsUpsertService {
async updateShifts(dtos: UpdateShiftDto[]): Promise<UpdateShiftResult[]> { async updateShifts(dtos: UpdateShiftDto[]): Promise<UpdateShiftResult[]> {
if (!Array.isArray(dtos) || dtos.length === 0) return []; if (!Array.isArray(dtos) || dtos.length === 0) return [];
const updates: UpdateShiftPayload[] = dtos.map((item) => { const updates: UpdateShiftPayload[] = await Promise.all(dtos.map((item) => {
const { id, ...rest } = item; const { id, ...rest } = item;
if (!Number.isInteger(id)) { if (!Number.isInteger(id)) {
throw new BadRequestException('Update shift payload is missing a valid id'); throw new BadRequestException('Update shift payload is missing a valid id');
@ -237,12 +235,12 @@ export class ShiftsUpsertService {
if (rest.date !== undefined) changes.date = rest.date; if (rest.date !== undefined) changes.date = rest.date;
if (rest.start_time !== undefined) changes.start_time = rest.start_time; if (rest.start_time !== undefined) changes.start_time = rest.start_time;
if (rest.end_time !== undefined) changes.end_time = rest.end_time; if (rest.end_time !== undefined) changes.end_time = rest.end_time;
if (rest.bank_code_id !== undefined) changes.bank_code_id = rest.bank_code_id; if (rest.type !== undefined) changes.type = rest.type;
if (rest.is_remote !== undefined) changes.is_remote = rest.is_remote; if (rest.is_remote !== undefined) changes.is_remote = rest.is_remote;
if (rest.comment !== undefined) changes.comment = rest.comment; if (rest.comment !== undefined) changes.comment = rest.comment;
return { id, dto: changes }; return { id, dto: changes };
}); }));
return this.prisma.$transaction(async (tx) => { return this.prisma.$transaction(async (tx) => {
const shift_ids = updates.map(update_shift => update_shift.id); const shift_ids = updates.map(update_shift => update_shift.id);
@ -266,7 +264,7 @@ export class ShiftsUpsertService {
} }
} }
const planned_updates = updates.map(update => { const planned_updates = updates.map( update => {
const exist_shift = regroup_id.get(update.id)!; const exist_shift = regroup_id.get(update.id)!;
const date_string = update.dto.date ?? toStringFromDate(exist_shift.date); const date_string = update.dto.date ?? toStringFromDate(exist_shift.date);
const start_string = update.dto.start_time ?? toStringFromHHmm(exist_shift.start_time); const start_string = update.dto.start_time ?? toStringFromHHmm(exist_shift.start_time);
@ -275,6 +273,7 @@ export class ShiftsUpsertService {
date: toDateFromString(date_string), date: toDateFromString(date_string),
start_time: toHHmmFromString(start_string), start_time: toHHmmFromString(start_string),
end_time: toHHmmFromString(end_string), end_time: toHHmmFromString(end_string),
id: exist_shift.id,
}; };
return { update, exist_shift, normed }; return { update, exist_shift, normed };
}); });
@ -345,7 +344,7 @@ export class ShiftsUpsertService {
if (dto.date !== undefined) data.date = planned.normed.date; if (dto.date !== undefined) data.date = planned.normed.date;
if (dto.start_time !== undefined) data.start_time = planned.normed.start_time; if (dto.start_time !== undefined) data.start_time = planned.normed.start_time;
if (dto.end_time !== undefined) data.end_time = planned.normed.end_time; if (dto.end_time !== undefined) data.end_time = planned.normed.end_time;
if (dto.bank_code_id !== undefined) data.bank_code_id = dto.bank_code_id; if (dto.type !== undefined) data.type = dto.type;
if (dto.is_remote !== undefined) data.is_remote = dto.is_remote; if (dto.is_remote !== undefined) data.is_remote = dto.is_remote;
if (dto.comment !== undefined) data.comment = dto.comment ?? null; if (dto.comment !== undefined) data.comment = dto.comment ?? null;
@ -362,7 +361,7 @@ export class ShiftsUpsertService {
const shift: GetShiftDto = { const shift: GetShiftDto = {
timesheet_id: row.timesheet_id, timesheet_id: row.timesheet_id,
bank_code_id: row.bank_code_id, type: data.type,
date: toStringFromDate(row.date), date: toStringFromDate(row.date),
start_time: toStringFromHHmm(row.start_time), start_time: toStringFromHHmm(row.start_time),
end_time: toStringFromHHmm(row.end_time), end_time: toStringFromHHmm(row.end_time),
@ -406,10 +405,11 @@ export class ShiftsUpsertService {
// LOCAL HELPERS // LOCAL HELPERS
//_________________________________________________________________ //_________________________________________________________________
//converts all string hours and date to Date and HHmm formats //converts all string hours and date to Date and HHmm formats
private normalizeShiftDto = (dto: ShiftDto): Normalized => { private normalizeShiftDto = async (dto: ShiftDto): Promise<Normalized> => {
const { id: bank_code_id} = await this.typeResolver.findBankCodeIDByType(dto.type);
const date = toDateFromString(dto.date); const date = toDateFromString(dto.date);
const start_time = toHHmmFromString(dto.start_time); const start_time = toHHmmFromString(dto.start_time);
const end_time = toHHmmFromString(dto.end_time); const end_time = toHHmmFromString(dto.end_time);
return { date, start_time, end_time }; return { date, start_time, end_time, id: bank_code_id };
} }
} }

View File

@ -1,19 +1,33 @@
import { Controller, Get, ParseIntPipe, Query, Req, UnauthorizedException} from "@nestjs/common"; import { Body, Controller, Get, ParseBoolPipe, ParseIntPipe, Patch, Query, Req, UnauthorizedException} from "@nestjs/common";
import { RolesAllowed } from "src/common/decorators/roles.decorators"; import { RolesAllowed } from "src/common/decorators/roles.decorators";
import { GetTimesheetsOverviewService } from "src/time-and-attendance/time-tracker/timesheets/services/timesheet-get-overview.service"; import { GetTimesheetsOverviewService } from "src/time-and-attendance/time-tracker/timesheets/services/timesheet-get-overview.service";
import { Roles as RoleEnum } from '.prisma/client'; import { Roles as RoleEnum } from '.prisma/client';
import { TimesheetApprovalService } from "src/time-and-attendance/time-tracker/timesheets/services/timesheet-approval.service";
@Controller('timesheets') @Controller('timesheets')
export class TimesheetController { export class TimesheetController {
constructor( private readonly timesheetOverview: GetTimesheetsOverviewService ){} constructor(
private readonly timesheetOverview: GetTimesheetsOverviewService,
private readonly approvalService: TimesheetApprovalService,
){}
@Get() @Get()
@RolesAllowed(RoleEnum.SUPERVISOR, RoleEnum.HR, RoleEnum.ACCOUNTING, RoleEnum.ADMIN) @RolesAllowed(RoleEnum.SUPERVISOR, RoleEnum.HR, RoleEnum.ACCOUNTING, RoleEnum.ADMIN)
async getTimesheetByIds( async getTimesheetByIds(
@Req() req, @Query('year', ParseIntPipe) year:number, @Query('period_number', ParseIntPipe) period_number: number) { @Req() req, @Query('year', ParseIntPipe) year:number, @Query('period_number', ParseIntPipe) period_number: number) {
const email = req.user?.email; const email = req.user?.email;
if(!email) throw new UnauthorizedException('Unauthorized User');  if(!email) throw new UnauthorizedException('Unauthorized User');
return this.timesheetOverview.getTimesheetsForEmployeeByPeriod(email, year, period_number); return this.timesheetOverview.getTimesheetsForEmployeeByPeriod(email, year, period_number);
} }
@Patch('timesheet-approval')
@RolesAllowed(RoleEnum.SUPERVISOR, RoleEnum.HR, RoleEnum.ACCOUNTING, RoleEnum.ADMIN)
async approveTimesheet(
@Body('timesheet_id', ParseIntPipe) timesheet_id: number,
@Body('is_approved' , ParseBoolPipe) is_approved: boolean,
) {
return this.approvalService.approveTimesheetById(timesheet_id, is_approved);
}
} }

View File

@ -1,11 +1,15 @@
import { BaseApprovalService } from "src/common/shared/base-approval.service"; import { BaseApprovalService } from "src/common/shared/base-approval.service";
import { Prisma, Timesheets } from "@prisma/client"; import { Prisma, Timesheets } from "@prisma/client";
import { PrismaService } from "src/prisma/prisma.service"; import { PrismaService } from "src/prisma/prisma.service";
import { Injectable } from "@nestjs/common"; import { Injectable, NotFoundException } from "@nestjs/common";
import { timesheet_select } from "src/time-and-attendance/utils/selects.utils";
@Injectable() @Injectable()
export class TimesheetApprovalService extends BaseApprovalService<Timesheets>{ export class TimesheetApprovalService extends BaseApprovalService<Timesheets>{
constructor(prisma: PrismaService){super(prisma)} constructor(
prisma: PrismaService,
){super(prisma)}
//_____________________________________________________________________________________________ //_____________________________________________________________________________________________
// APPROVAL AND DELEGATE METHODS // APPROVAL AND DELEGATE METHODS
//_____________________________________________________________________________________________ //_____________________________________________________________________________________________
@ -13,26 +17,43 @@ import { Injectable } from "@nestjs/common";
return this.prisma.timesheets; return this.prisma.timesheets;
} }
protected delegateFor(transaction: Prisma.TransactionClient) { protected delegateFor(tx: Prisma.TransactionClient) {
return transaction.timesheets; return tx.timesheets;
} }
async updateApproval(id: number, isApproved: boolean): Promise<Timesheets> { async updateApproval(id: number, is_approved: boolean): Promise<Timesheets> {
return this.prisma.$transaction((transaction) => return this.prisma.$transaction((tx) =>
this.updateApprovalWithTransaction(transaction, id, isApproved), this.updateApprovalWithTransaction(tx, id, is_approved),
); );
} }
async cascadeApprovalWithtx(transaction: Prisma.TransactionClient, timesheetId: number, isApproved: boolean): Promise<Timesheets> { async cascadeApprovalWithtx(tx: Prisma.TransactionClient, timesheet_id: number, is_approved: boolean): Promise<Timesheets> {
const timesheet = await this.updateApprovalWithTransaction(transaction, timesheetId, isApproved); const timesheet = await this.updateApprovalWithTransaction(tx, timesheet_id, is_approved);
await transaction.shifts.updateMany({ await tx.shifts.updateMany({
where: { timesheet_id: timesheetId }, where: { timesheet_id: timesheet_id },
data: { is_approved: isApproved }, data: { is_approved: is_approved },
}); });
await transaction.expenses.updateManyAndReturn({ await tx.expenses.updateManyAndReturn({
where: { timesheet_id: timesheetId }, where: { timesheet_id: timesheet_id },
data: { is_approved: isApproved }, data: { is_approved: is_approved },
}); });
return timesheet; return timesheet;
} }
async approveTimesheetById( timesheet_id: number, is_approved: boolean){
return this.prisma.$transaction(async (tx) => {
const timesheet = await tx.timesheets.findUnique({
where: { id: timesheet_id },
select: { id: true },
});
if(!timesheet) throw new NotFoundException(`Timesheet with id: ${timesheet_id} not found`);
await this.cascadeApprovalWithtx(tx, timesheet_id, is_approved);
return tx.timesheets.findUnique({
where: { id: timesheet_id },
select: timesheet_select,
});
});
}
} }

View File

@ -1,49 +1,49 @@
import { TimesheetsArchive } from "@prisma/client"; // import { TimesheetsArchive } from "@prisma/client";
import { PrismaService } from "src/prisma/prisma.service"; // import { PrismaService } from "src/prisma/prisma.service";
export class TimesheetArchiveService { // export class TimesheetArchiveService {
constructor(private readonly prisma: PrismaService){} // constructor(private readonly prisma: PrismaService){}
async archiveOld(): Promise<void> { // async archiveOld(): Promise<void> {
//calcul du cutoff pour archivation // //calcul du cutoff pour archivation
const cutoff = new Date(); // const cutoff = new Date();
cutoff.setMonth(cutoff.getMonth() - 6) // cutoff.setMonth(cutoff.getMonth() - 6)
await this.prisma.$transaction(async transaction => { // await this.prisma.$transaction(async transaction => {
//fetches all timesheets to cutoff // //fetches all timesheets to cutoff
const oldSheets = await transaction.timesheets.findMany({ // const oldSheets = await transaction.timesheets.findMany({
where: { shift: { some: { date: { lt: cutoff } } }, // where: { shift: { some: { date: { lt: cutoff } } },
}, // },
select: { // select: {
id: true, // id: true,
employee_id: true, // employee_id: true,
is_approved: true, // is_approved: true,
}, // },
}); // });
if( oldSheets.length === 0) return; // if( oldSheets.length === 0) return;
//preping data for archivation // //preping data for archivation
const archiveDate = oldSheets.map(sheet => ({ // const archiveDate = oldSheets.map(sheet => ({
timesheet_id: sheet.id, // timesheet_id: sheet.id,
employee_id: sheet.employee_id, // employee_id: sheet.employee_id,
is_approved: sheet.is_approved, // is_approved: sheet.is_approved,
})); // }));
//copying data from timesheets table to archive table // //copying data from timesheets table to archive table
await transaction.timesheetsArchive.createMany({ data: archiveDate }); // await transaction.timesheetsArchive.createMany({ data: archiveDate });
//removing data from timesheets table // //removing data from timesheets table
await transaction.timesheets.deleteMany({ where: { id: { in: oldSheets.map(s => s.id) } } }); // await transaction.timesheets.deleteMany({ where: { id: { in: oldSheets.map(s => s.id) } } });
}); // });
} // }
//fetches all archived timesheets // //fetches all archived timesheets
async findAllArchived(): Promise<TimesheetsArchive[]> { // async findAllArchived(): Promise<TimesheetsArchive[]> {
return this.prisma.timesheetsArchive.findMany(); // return this.prisma.timesheetsArchive.findMany();
} // }
//fetches an archived timesheet // //fetches an archived timesheet
async findOneArchived(id: number): Promise<TimesheetsArchive> { // async findOneArchived(id: number): Promise<TimesheetsArchive> {
return this.prisma.timesheetsArchive.findUniqueOrThrow({ where: { id } }); // return this.prisma.timesheetsArchive.findUniqueOrThrow({ where: { id } });
} // }
} // }

View File

@ -2,19 +2,16 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { TimesheetController } from 'src/time-and-attendance/time-tracker/timesheets/controllers/timesheet.controller'; import { TimesheetController } from 'src/time-and-attendance/time-tracker/timesheets/controllers/timesheet.controller';
import { TimesheetApprovalService } from 'src/time-and-attendance/time-tracker/timesheets/services/timesheet-approval.service'; import { TimesheetApprovalService } from 'src/time-and-attendance/time-tracker/timesheets/services/timesheet-approval.service';
import { TimesheetArchiveService } from 'src/time-and-attendance/time-tracker/timesheets/services/timesheet-archive.service';
import { GetTimesheetsOverviewService } from 'src/time-and-attendance/time-tracker/timesheets/services/timesheet-get-overview.service'; import { GetTimesheetsOverviewService } from 'src/time-and-attendance/time-tracker/timesheets/services/timesheet-get-overview.service';
import { EmailToIdResolver } from 'src/time-and-attendance/utils/resolve-email-id.utils'; import { EmailToIdResolver } from 'src/time-and-attendance/utils/resolve-email-id.utils';
@Module({ @Module({
controllers: [TimesheetController], controllers: [TimesheetController],
providers: [ providers: [
TimesheetArchiveService,
GetTimesheetsOverviewService, GetTimesheetsOverviewService,
TimesheetApprovalService,
EmailToIdResolver, EmailToIdResolver,
TimesheetApprovalService,
], ],
exports: [], exports: [TimesheetApprovalService],
}) })
export class TimesheetsModule {} export class TimesheetsModule {}

View File

@ -9,7 +9,7 @@ export class BankCodesResolver {
constructor(private readonly prisma: PrismaService) {} constructor(private readonly prisma: PrismaService) {}
//find id and modifier by type //find id and modifier by type
readonly findByType = async ( type: string, client?: Tx readonly findIdAndModifierByType = async ( type: string, client?: Tx
): Promise<{id:number; modifier: number }> => { ): Promise<{id:number; modifier: number }> => {
const db = client ?? this.prisma; const db = client ?? this.prisma;
const bank = await db.bankCodes.findFirst({ const bank = await db.bankCodes.findFirst({
@ -20,4 +20,25 @@ export class BankCodesResolver {
if(!bank) throw new NotFoundException(`Unknown bank code type: ${type}`); if(!bank) throw new NotFoundException(`Unknown bank code type: ${type}`);
return { id: bank.id, modifier: bank.modifier }; return { id: bank.id, modifier: bank.modifier };
}; };
//finds only id by type
readonly findBankCodeIDByType = async (type: string, client?: Tx) => {
const db = client ?? this.prisma;
const bank_code_id = await db.bankCodes.findFirst({
where: { type },
select: {id: true},
});
if(!bank_code_id) throw new NotFoundException(`Unkown bank type: ${type}`);
return bank_code_id;
}
readonly findTypeByBankCodeId = async (bank_code_id: number, client?: Tx) => {
const db = client ?? this.prisma;
const type = 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;
}
} }

View File

@ -1,4 +1,5 @@
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
import { dmmfToRuntimeDataModel } from "@prisma/client/runtime/library";
export const expense_select = { export const expense_select = {
id: true, id: true,
@ -17,6 +18,9 @@ export const shift_select = {
id: true, id: true,
timesheet_id: true, timesheet_id: true,
bank_code_id: true, bank_code_id: true,
bank_code: {
select: { type: true },
},
date: true, date: true,
start_time: true, start_time: true,
end_time: true, end_time: true,
@ -78,3 +82,11 @@ export const SHIFT_SELECT = {
export const SHIFT_ASC_ORDER = [{date: 'asc' as const}, {start_time: 'asc' 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,
shift: true,
expense: true,
start_date: true,
is_approved: true,
} satisfies Prisma.TimesheetsSelect;

View File

@ -25,7 +25,7 @@ export type TotalExpenses = {
mileage: number; mileage: number;
}; };
export type Normalized = { date: Date; start_time: Date; end_time: Date; }; export type Normalized = { date: Date; start_time: Date; end_time: Date; id: number};
export type ShiftWithOvertimeDto = { export type ShiftWithOvertimeDto = {
shift: GetShiftDto; shift: GetShiftDto;