fix(contracts): added a contract class to employee and timesheet overview dtos and ajusted queries

This commit is contained in:
Matthieu Haineault 2026-03-23 14:21:36 -04:00
parent c47dcb1f2f
commit a5bd7d54fe
13 changed files with 3036 additions and 79 deletions

4
.gitignore vendored
View File

@ -55,9 +55,5 @@ pids
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Generated prisma folders (from -> npm run prisma:generated)
prisma/mariadb/generated/prisma/client/mariadb/
prisma/postgres/generated/prisma/client/postgres/
prisma/prisma-legacy/generated/prisma/client/legacy/
!swagger-spec.json

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -88,9 +88,9 @@ model Contracts {
employee_id Int @unique
daily_expected_hours Int @default(24)
applicable_overtime ApplicableOvertime[] @default([WEEKLY])
phone_allocation Decimal @default(0.00)
on_call_allocation Decimal @default(0.00)
weekend_on_call_allocation Decimal @default(0.00)
phone_allocation Decimal @default(0.00) @db.Decimal(5, 2)
on_call_allocation Decimal @default(0.00) @db.Decimal(5, 2)
weekend_on_call_allocation Decimal @default(0.00) @db.Decimal(5, 2)
employee Employees @relation("EmployeeContract", fields: [employee_id], references: [id])

View File

@ -7,4 +7,5 @@ export class ContractController {
}

View File

@ -3,7 +3,6 @@ import { IsInt, IsString } from "class-validator";
import { ApplicableOvertime } from "prisma/postgres/generated/prisma/client/postgres/enums";
export class Contract {
@IsInt() employee_id: number;
@IsInt() daily_expected_hours: number;
@IsString() applicable_overtime: ApplicableOvertime;
@Type(() => Number) phone_allocation: number;

View File

@ -2,6 +2,7 @@ import { IsArray, IsBoolean, IsDateString, IsEmail, IsInt, IsNotEmpty, IsOptiona
import { Type } from 'class-transformer';
import { PaidTimeOffDto } from 'src/time-and-attendance/paid-time-off/paid-time-off.dto';
import { Prisma } from 'prisma/postgres/generated/prisma/client/postgres/client';
import { Contract } from 'src/identity-and-account/contract/contract.dto';
export class EmployeeDetailedDto {
@IsString() @IsNotEmpty() first_name: string;
@ -14,9 +15,9 @@ export class EmployeeDetailedDto {
@IsEmail() @IsOptional() email: string;
@IsString() phone_number: string;
@IsDateString() first_work_day: string;
@IsInt() daily_expected_hours: number;
@IsDateString() @IsOptional() last_work_day?: string | null;
@IsString() @IsOptional() residence?: string;
@IsInt() @Type(() => Contract) contract: Partial<Contract>;
@IsOptional() @Type(() => PaidTimeOffDto) paid_time_off?: Partial<PaidTimeOffDto>;
@IsInt() @IsPositive() @Type(() => Number) external_payroll_id: number;
@IsArray() @IsString({ each: true }) user_module_access: string[];
@ -34,9 +35,9 @@ export class EmployeeDetailedUpsertDto {
@IsEmail() @IsOptional() email: string;
@IsString() phone_number: string;
@IsDateString() first_work_day: string;
@IsInt() daily_expected_hours: number;
@IsDateString() @IsOptional() last_work_day?: string | null;
@IsString() @IsOptional() residence?: string;
@IsInt() @Type(() => Contract) contract: Partial<Contract>;
@IsOptional() @Type(() => PaidTimeOffDto) paid_time_off?: PaidTimeOffDto;
@IsInt() @IsPositive() @Type(() => Number) external_payroll_id: number;
@IsArray() @IsString({ each: true }) user_module_access: string[];
@ -74,9 +75,26 @@ export type EmployeeWithDetails = Prisma.EmployeesGetPayload<{
first_work_day: true,
last_work_day: true,
external_payroll_id: true,
paid_time_off: true,
paid_time_off: {
select: {
id: true,
employee_id: true,
sick_hours: true,
vacation_hours: true,
banked_hours: true,
last_updated: true,
},
},
is_supervisor: true,
daily_expected_hours: true,
contracts: {
select: {
daily_expected_hours: true,
applicable_overtime: true,
weekend_on_call_allocation: true,
on_call_allocation: true,
phone_allocation: true,
},
},
schedule_preset_id: true,
schedule_preset: { select: { id: true } }
}

View File

@ -44,13 +44,20 @@ export class EmployeesCreateService {
data: {
user_id: user.id,
external_payroll_id: dto.external_payroll_id,
daily_expected_hours: dto.daily_expected_hours,
company_code: company_code,
job_title: dto.job_title,
first_work_day: first_work_day,
is_supervisor: dto.is_supervisor,
supervisor_id: supervisor_id,
schedule_preset_id: dto.preset_id,
contracts: {
create: {
daily_expected_hours: dto.contract.daily_expected_hours,
phone_allocation: dto.contract.phone_allocation,
on_call_allocation: dto.contract.on_call_allocation,
weekend_on_call_allocation: dto.contract.weekend_on_call_allocation,
},
},
},
});
});

View File

@ -37,8 +37,12 @@ export class EmployeesGetService {
},
},
},
contracts: {
select: {
daily_expected_hours: true,
},
},
is_supervisor: true,
daily_expected_hours: true,
job_title: true,
company_code: true,
external_payroll_id: true,
@ -47,23 +51,29 @@ export class EmployeesGetService {
schedule_preset_id: true,
}
}).then(rows => rows.map(r => ({
first_name: r.user.first_name,
last_name: r.user.last_name,
email: r.user.email,
phone_number: r.user.phone_number,
company_name: toStringFromCompanyCode(r.company_code),
job_title: r.job_title ?? '',
daily_expected_hours: r.daily_expected_hours,
external_payroll_id: r.external_payroll_id,
employee_full_name: `${r.user.first_name} ${r.user.last_name}`,
is_supervisor: r.is_supervisor,
supervisor_full_name: `${r.supervisor?.user.first_name} ${r.supervisor?.user.last_name}`,
first_work_day: toStringFromDate(r.first_work_day),
last_work_day: r.last_work_day ? toStringFromDate(r.last_work_day) : null,
preset_id: r.schedule_preset_id ?? undefined,
})));
return { success: true, data: employee_list };
});
const employeeDetailedList = employee_list.map(r => {
return {
first_name: r.user.first_name,
last_name: r.user.last_name,
email: r.user.email,
phone_number: r.user.phone_number,
company_name: toStringFromCompanyCode(r.company_code),
job_title: r.job_title ?? '',
contract: {
daily_expected_hours: r.contracts?.daily_expected_hours ?? 24,
},
external_payroll_id: r.external_payroll_id,
employee_full_name: `${r.user.first_name} ${r.user.last_name}`,
is_supervisor: r.is_supervisor,
supervisor_full_name: `${r.supervisor?.user.first_name} ${r.supervisor?.user.last_name}`,
first_work_day: toStringFromDate(r.first_work_day),
last_work_day: r.last_work_day ? toStringFromDate(r.last_work_day) : null,
preset_id: r.schedule_preset_id ?? undefined,
}
});
return { success: true, data: employeeDetailedList };
};
async findOwnProfile(email: string): Promise<Result<Partial<EmployeeDetailedDto>, string>> {
@ -89,7 +99,11 @@ export class EmployeesGetService {
paid_time_off: true,
is_supervisor: true,
schedule_preset_id: true,
daily_expected_hours: true,
contracts: {
select: {
daily_expected_hours: true,
},
},
supervisor: {
select: {
id: true, user: {
@ -103,6 +117,7 @@ export class EmployeesGetService {
},
});
if (!existing_profile) return { success: false, error: 'EMPLOYEE_NOT_FOUND' };
if (!existing_profile.contracts) return { success: false, error: 'CONTRACT_NOT_FOUND' };
const company_name = toStringFromCompanyCode(existing_profile.company_code);
@ -113,7 +128,9 @@ export class EmployeesGetService {
email: existing_profile.user.email,
supervisor_full_name: `${existing_profile.supervisor?.user.first_name} ${existing_profile.supervisor?.user.last_name}`,
company_name: company_name,
daily_expected_hours: existing_profile.daily_expected_hours,
contract: {
daily_expected_hours: existing_profile.contracts?.daily_expected_hours ?? 24,
},
job_title: existing_profile.job_title ?? '',
external_payroll_id: existing_profile.external_payroll_id,
paid_time_off: {
@ -170,6 +187,11 @@ export class EmployeesGetService {
},
},
},
contracts: {
select: {
daily_expected_hours: true,
}
},
job_title: true,
company_code: true,
first_work_day: true,
@ -177,7 +199,6 @@ export class EmployeesGetService {
external_payroll_id: true,
paid_time_off: true,
is_supervisor: true,
daily_expected_hours: true,
schedule_preset_id: true,
schedule_preset: {
select: {
@ -196,35 +217,36 @@ export class EmployeesGetService {
const company_name = toStringFromCompanyCode(employee.company_code);
return {
success: true,
data: {
first_name: employee.user.first_name,
last_name: employee.user.last_name,
email: employee.user.email,
residence: employee.user.residence ?? '',
phone_number: employee.user.phone_number,
company_name: company_name,
is_supervisor: employee.is_supervisor ?? false,
job_title: employee.job_title ?? '',
external_payroll_id: employee.external_payroll_id,
paid_time_off: {
id: employee.paid_time_off?.id ?? -1,
employee_id: employee.paid_time_off?.employee_id ?? -1,
sick_hours: employee.paid_time_off?.sick_hours.toNumber() ?? 0,
vacation_hours: employee.paid_time_off?.vacation_hours.toNumber() ?? 0,
banked_hours: employee.paid_time_off?.banked_hours.toNumber() ?? 0,
last_updated: employee.paid_time_off?.last_updated?.toISOString() ?? null,
},
employee_full_name: `${employee.user.first_name} ${employee.user.last_name}`,
first_work_day: toStringFromDate(employee.first_work_day),
last_work_day: employee.last_work_day ? toStringFromDate(employee.last_work_day) : undefined,
supervisor_full_name: employee.supervisor ? `${employee.supervisor?.user.first_name} ${employee.supervisor?.user.last_name}` : '',
user_module_access: module_access_array,
daily_expected_hours: employee.daily_expected_hours,
preset_id: employee.schedule_preset_id ? employee.schedule_preset_id : undefined,
const detailed_employee: EmployeeDetailedDto = {
first_name: employee.user.first_name,
last_name: employee.user.last_name,
email: employee.user.email,
residence: employee.user.residence ?? '',
phone_number: employee.user.phone_number,
company_name: company_name,
is_supervisor: employee.is_supervisor ?? false,
job_title: employee.job_title ?? '',
external_payroll_id: employee.external_payroll_id,
paid_time_off: {
id: employee.paid_time_off?.id ?? -1,
employee_id: employee.paid_time_off?.employee_id ?? -1,
sick_hours: employee.paid_time_off?.sick_hours.toNumber() ?? 0,
vacation_hours: employee.paid_time_off?.vacation_hours.toNumber() ?? 0,
banked_hours: employee.paid_time_off?.banked_hours.toNumber() ?? 0,
last_updated: employee.paid_time_off?.last_updated?.toISOString() ?? null,
},
};
employee_full_name: `${employee.user.first_name} ${employee.user.last_name}`,
first_work_day: toStringFromDate(employee.first_work_day),
last_work_day: employee.last_work_day ? toStringFromDate(employee.last_work_day) : undefined,
supervisor_full_name: employee.supervisor ? `${employee.supervisor?.user.first_name} ${employee.supervisor?.user.last_name}` : '',
user_module_access: module_access_array,
contract: {
daily_expected_hours: employee.contracts?.daily_expected_hours ?? 24,
},
preset_id: employee.schedule_preset_id ? employee.schedule_preset_id : undefined,
}
return { success: true, data: detailed_employee };
};
}

View File

@ -98,13 +98,20 @@ export class EmployeesUpdateService {
data: {
company_code: company_code,
job_title: dto.job_title,
daily_expected_hours: dto.daily_expected_hours,
first_work_day: toDateFromString(dto.first_work_day),
last_work_day: last_work_day,
is_supervisor: dto.is_supervisor,
supervisor_id: supervisor_id,
schedule_preset_id: dto.preset_id,
external_payroll_id: dto.external_payroll_id,
contracts: {
update: {
daily_expected_hours: dto.contract.daily_expected_hours,
on_call_allocation: dto.contract.on_call_allocation,
weekend_on_call_allocation: dto.contract.weekend_on_call_allocation,
phone_allocation: dto.contract.phone_allocation,
},
},
},
});

View File

@ -1,4 +1,3 @@
import { NUMBER_OF_TIMESHEETS_TO_RETURN } from "src/common/utils/constants.utils";
import { Injectable } from "@nestjs/common";
import { PrismaPostgresService } from "prisma/postgres/prisma-postgres.service";
@ -56,16 +55,28 @@ export class GetTimesheetsOverviewService {
//find user infos using the employee_id
const employee = await this.prisma.employees.findUnique({
where: { id: employee_id.data },
select: { daily_expected_hours: true, schedule_preset: true, user: true },
select: { schedule_preset: true, user: true, id: true }
});
if (!employee) return { success: false, error: `EMPLOYEE_NOT_FOUND` }
if (!employee) return { success: false, error: `EMPLOYEE_NOT_FOUND` };
if (!employee.user) return { success: false, error: 'USER_NOT_FOUND' };
const contractDetails = await this.prisma.contracts.findUnique({
where: { employee_id: employee.id },
select: {
daily_expected_hours: true,
applicable_overtime: true,
phone_allocation: true,
on_call_allocation: true,
weekend_on_call_allocation: true,
}
});
if (!contractDetails) return { success: false, error: 'CONTRACT_NOT_FOUND' };
//builds employee details
const has_preset_schedule = employee.schedule_preset !== null;
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) => mapOneTimesheet(timesheet)));
if (!timesheets) return { success: false, error: 'INVALID_TIMESHEET' }
@ -73,7 +84,13 @@ export class GetTimesheetsOverviewService {
const data: Timesheets = {
has_preset_schedule,
employee_fullname,
daily_expected_hours: employee.daily_expected_hours,
contract: {
daily_expected_hours: contractDetails.daily_expected_hours,
applicable_overtime: contractDetails.applicable_overtime,
phone_allocation: Number(contractDetails.phone_allocation),
on_call_allocation: Number(contractDetails.on_call_allocation),
weekend_on_call_allocation: Number(contractDetails.weekend_on_call_allocation),
},
timesheets,
}

View File

@ -1,5 +1,5 @@
import { Type } from "class-transformer";
import { IsBoolean, IsDate, IsInt, IsOptional, IsString } from "class-validator";
import { IsArray, IsBoolean, IsDate, IsInt, IsOptional, IsString } from "class-validator";
export class TimesheetEntity {
@IsInt() id: number;
@ -11,10 +11,19 @@ export class TimesheetEntity {
export class Timesheets {
@IsBoolean() has_preset_schedule: boolean;
@IsString() employee_fullname: string;
@IsInt() daily_expected_hours: number;
@Type(() => Contract) contract: Contract;
@Type(() => Number)
@Type(() => Timesheet) timesheets: Timesheet[];
}
export class Contract {
@Type(() => Number) daily_expected_hours: number;
@Type(() => Number) phone_allocation: number;
@Type(() => Number) on_call_allocation: number;
@Type(() => Number) weekend_on_call_allocation: number;
@IsArray() @IsString() applicable_overtime: string[];
}
export class Timesheet {
@IsInt() timesheet_id: number;
@IsBoolean() is_approved: boolean;