feat(employees): ajusted employees module, added an update method and route

This commit is contained in:
Matthieu Haineault 2025-12-02 12:57:49 -05:00
parent 0180fa3fdd
commit fb0187c117
13 changed files with 278 additions and 208 deletions

View File

@ -514,7 +514,7 @@
] ]
} }
}, },
"/employees": { "/employees/create": {
"post": { "post": {
"operationId": "EmployeesController_createEmployee", "operationId": "EmployeesController_createEmployee",
"parameters": [], "parameters": [],
@ -538,6 +538,20 @@
] ]
} }
}, },
"/employees/update": {
"patch": {
"operationId": "EmployeesController_updateEmployee",
"parameters": [],
"responses": {
"200": {
"description": ""
}
},
"tags": [
"Employees"
]
}
},
"/preferences/update": { "/preferences/update": {
"patch": { "patch": {
"operationId": "PreferencesController_updatePreferences", "operationId": "PreferencesController_updatePreferences",

View File

@ -1,52 +1,50 @@
import { Controller, Get, Query, Body, Post } from "@nestjs/common"; import { Controller, Get, Query, Body, Post, Patch } from "@nestjs/common";
import { Access } from "src/common/decorators/module-access.decorators"; import { Access } from "src/common/decorators/module-access.decorators";
import { Result } from "src/common/errors/result-error.factory"; import { Result } from "src/common/errors/result-error.factory";
import { EmployeeDetailedDto } from "src/identity-and-account/employees/dtos/employee-detailed.dto"; import { EmployeeDetailedDto } from "src/identity-and-account/employees/dtos/employee-detailed.dto";
import { EmployeeDto } from "src/identity-and-account/employees/dtos/employee.dto"; import { EmployeeDto } from "src/identity-and-account/employees/dtos/employee.dto";
import { EmployeesService } from "src/identity-and-account/employees/services/employees.service"; import { EmployeesGetService } from "src/identity-and-account/employees/services/employees-get.service";
import { ModuleAccessAllowed } from "src/common/decorators/modules-guard.decorators"; import { ModuleAccessAllowed } from "src/common/decorators/modules-guard.decorators";
import { Modules as ModulesEnum } from ".prisma/client"; import { Modules as ModulesEnum } from ".prisma/client";
import { EmployeesCreateService } from "src/identity-and-account/employees/services/employees-create.service";
import { EmployeesUpdateService } from "src/identity-and-account/employees/services/employees-update.service";
@Controller('employees') @Controller('employees')
export class EmployeesController { export class EmployeesController {
constructor(private readonly employeesService: EmployeesService) { } constructor(
private readonly getService: EmployeesGetService,
private readonly createService: EmployeesCreateService,
private readonly updateService: EmployeesUpdateService,
) { }
@Get('personal-profile') @Get('personal-profile')
@ModuleAccessAllowed(ModulesEnum.personal_profile) @ModuleAccessAllowed(ModulesEnum.personal_profile)
async findOwnProfile(@Access('email') email: string): Promise<Result<Partial<EmployeeDetailedDto>, string>> { async findOwnProfile(@Access('email') email: string): Promise<Result<Partial<EmployeeDetailedDto>, string>> {
return await this.employeesService.findOwnProfile(email); return await this.getService.findOwnProfile(email);
} }
@Get('profile') @Get('profile')
@ModuleAccessAllowed(ModulesEnum.employee_management) @ModuleAccessAllowed(ModulesEnum.employee_management)
async findProfile(@Access('email') email: string, @Query('employee_email') employee_email?: string, async findProfile(@Access('email') email: string, @Query('employee_email') employee_email?: string,
): Promise<Result<Partial<EmployeeDetailedDto>, string>> { ): Promise<Result<Partial<EmployeeDetailedDto>, string>> {
return await this.employeesService.findOneDetailedProfile(email, employee_email); return await this.getService.findOneDetailedProfile(email, employee_email);
} }
@Get('employee-list') @Get('employee-list')
@ModuleAccessAllowed(ModulesEnum.employee_list) @ModuleAccessAllowed(ModulesEnum.employee_list)
async findListEmployees(): Promise<Result<EmployeeDto[], string>> { async findListEmployees(): Promise<Result<EmployeeDto[], string>> {
return this.employeesService.findListEmployees(); return this.getService.findListEmployees();
} }
@Post() @Post('create')
@ModuleAccessAllowed(ModulesEnum.employee_management) @ModuleAccessAllowed(ModulesEnum.employee_management)
async createEmployee(@Body() dto: EmployeeDetailedDto): Promise<Result<boolean, string>> { async createEmployee(@Body() dto: EmployeeDetailedDto): Promise<Result<boolean, string>> {
return await this.employeesService.createEmployee(dto); return await this.createService.createEmployee(dto);
} }
// @Patch() @Patch('update')
// async updateOrArchiveOrRestore(@Req() req, @Body() dto: UpdateEmployeeDto,) { @ModuleAccessAllowed(ModulesEnum.employee_management)
// // if last_work_day is set => archive the employee async updateEmployee(@Access('email') email:string, dto:EmployeeDetailedDto, employee_email?: string){
// // else if employee is archived and first_work_day or last_work_day = null => restore return await this.updateService.updateEmployee(email, dto, employee_email);
// //otherwise => standard update }
// const email = req.user?.email;
// const result = await this.archiveService.patchEmployee(email, dto);
// if (!result) {
// throw new NotFoundException(`Employee with email: ${email} is not found in active or archive.`)
// }
// return result;
//
} }

View File

@ -1,13 +1,21 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { EmployeesController } from './controllers/employees.controller'; import { EmployeesController } from './controllers/employees.controller';
import { EmployeesService } from './services/employees.service'; import { EmployeesGetService } from './services/employees-get.service';
import { AccessGetService } from 'src/identity-and-account/user-module-access/services/module-access-get.service'; import { AccessGetService } from 'src/identity-and-account/user-module-access/services/module-access-get.service';
import { EmailToIdResolver } from 'src/common/mappers/email-id.mapper'; import { EmailToIdResolver } from 'src/common/mappers/email-id.mapper';
import { EmployeesUpdateService } from 'src/identity-and-account/employees/services/employees-update.service';
import { EmployeesCreateService } from 'src/identity-and-account/employees/services/employees-create.service';
@Module({ @Module({
imports: [], imports: [],
controllers: [EmployeesController], controllers: [EmployeesController],
providers: [EmployeesService, AccessGetService, EmailToIdResolver], providers: [
exports: [EmployeesService ], EmployeesGetService,
EmployeesUpdateService,
EmployeesCreateService,
AccessGetService,
EmailToIdResolver
],
exports: [EmployeesGetService],
}) })
export class EmployeesModule {} export class EmployeesModule { }

View File

@ -0,0 +1,61 @@
import { Injectable } from "@nestjs/common";
import { Users } from "@prisma/client";
import { Result } from "src/common/errors/result-error.factory";
import { toBooleanFromString } from "src/common/mappers/module-access.mapper";
import { EmployeeDetailedDto } from "src/identity-and-account/employees/dtos/employee-detailed.dto";
import { toCompanyCodeFromString } from "src/identity-and-account/employees/utils/employee.utils";
import { PrismaService } from "src/prisma/prisma.service";
@Injectable()
export class EmployeesCreateService {
constructor(private readonly prisma: PrismaService) { }
async createEmployee(dto: EmployeeDetailedDto): Promise<Result<boolean, string>> {
const normalized_access = toBooleanFromString(dto.user_module_access);
const supervisor_id = await this.toIdFromFullName(dto.supervisor_full_name);
const company_code = toCompanyCodeFromString(dto.company_name)
await this.prisma.$transaction(async (tx) => {
const user: Users = await tx.users.create({
data: {
first_name: dto.first_name,
last_name: dto.last_name,
email: dto.email,
phone_number: dto.phone_number,
residence: dto.residence,
user_module_access: {
create: {
dashboard: normalized_access.dashboard,
employee_list: normalized_access.employee_list,
employee_management: normalized_access.employee_management,
personal_profile: normalized_access.personal_profile,
timesheets: normalized_access.timesheets,
timesheets_approval: normalized_access.timesheets_approval,
},
},
},
});
return tx.employees.create({
data: {
user_id: user.id,
company_code: company_code,
job_title: dto.job_title,
first_work_day: dto.first_work_day,
last_work_day: dto.last_work_day,
is_supervisor: dto.is_supervisor,
supervisor_id: supervisor_id,
},
});
});
return { success: true, data: true }
}
private toIdFromFullName = async (full_name: string) => {
const [first_name, last_name] = full_name.split(' ', 2);
let supervisor = await this.prisma.users.findFirst({
where: { first_name, last_name },
select: { employee: { select: { id: true } } }
});
if (!supervisor) supervisor = null;
return supervisor?.employee?.id;
}
}

View File

@ -1,15 +1,15 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from "@nestjs/common";
import { Users } from "@prisma/client";
import { Result } from "src/common/errors/result-error.factory"; import { Result } from "src/common/errors/result-error.factory";
import { EmailToIdResolver } from "src/common/mappers/email-id.mapper"; import { EmailToIdResolver } from "src/common/mappers/email-id.mapper";
import { module_list, Modules, toBooleanFromString, toStringFromBoolean } from "src/common/mappers/module-access.mapper"; import { Modules, toStringFromBoolean } from "src/common/mappers/module-access.mapper";
import { toStringFromDate } from "src/common/utils/date-utils"; import { toStringFromDate } from "src/common/utils/date-utils";
import { EmployeeDetailedDto } from "src/identity-and-account/employees/dtos/employee-detailed.dto"; import { EmployeeDetailedDto } from "src/identity-and-account/employees/dtos/employee-detailed.dto";
import { EmployeeDto } from "src/identity-and-account/employees/dtos/employee.dto"; import { EmployeeDto } from "src/identity-and-account/employees/dtos/employee.dto";
import { toStringFromCompanyCode } from "src/identity-and-account/employees/utils/employee.utils";
import { PrismaService } from "src/prisma/prisma.service"; import { PrismaService } from "src/prisma/prisma.service";
@Injectable() @Injectable()
export class EmployeesService { export class EmployeesGetService {
constructor( constructor(
private readonly prisma: PrismaService, private readonly prisma: PrismaService,
private readonly emailResolver: EmailToIdResolver, private readonly emailResolver: EmailToIdResolver,
@ -88,11 +88,7 @@ export class EmployeesService {
}); });
if (!existing_profile) return { success: false, error: 'EMPLOYEE_NOT_FOUND' }; if (!existing_profile) return { success: false, error: 'EMPLOYEE_NOT_FOUND' };
let company_name = 'Solucom'; const company_name = toStringFromCompanyCode(existing_profile.company_code);
if (existing_profile.company_code === 271583) {
company_name = 'Targo';
}
return { return {
success: true, data: { success: true, data: {
first_name: existing_profile.user.first_name, first_name: existing_profile.user.first_name,
@ -186,49 +182,4 @@ export class EmployeesService {
}, },
}; };
} }
async createEmployee(dto: EmployeeDetailedDto): Promise<Result<boolean, string>> {
let company_code = 271585;
if (dto.company_name === 'Targo') {
company_code = 271583;
}
const normalized_access = toBooleanFromString(dto.user_module_access)
await this.prisma.$transaction(async (tx) => {
const user: Users = await tx.users.create({
data: {
first_name: dto.first_name,
last_name: dto.last_name,
email: dto.email,
phone_number: dto.phone_number,
residence: dto.residence,
user_module_access: {
create: {
dashboard: normalized_access.dashboard,
employee_list: normalized_access.employee_list,
employee_management: normalized_access.employee_management,
personal_profile: normalized_access.personal_profile,
timesheets: normalized_access.timesheets,
timesheets_approval: normalized_access.timesheets_approval,
},
},
},
});
return tx.employees.create({
data: {
user_id: user.id,
company_code: company_code,
job_title: dto.job_title,
first_work_day: dto.first_work_day,
last_work_day: dto.last_work_day,
is_supervisor: dto.is_supervisor,
},
})
});
return { success: true, data: true }
}
// async updateEmployeeProfile = () => {
// }
} }

View File

@ -0,0 +1,72 @@
import { Injectable } from "@nestjs/common";
import { Result } from "src/common/errors/result-error.factory";
import { EmailToIdResolver } from "src/common/mappers/email-id.mapper";
import { toBooleanFromString } from "src/common/mappers/module-access.mapper";
import { EmployeeDetailedDto } from "src/identity-and-account/employees/dtos/employee-detailed.dto";
import { toCompanyCodeFromString } from "src/identity-and-account/employees/utils/employee.utils";
import { PrismaService } from "src/prisma/prisma.service";
@Injectable()
export class EmployeesUpdateService {
constructor(
private readonly prisma: PrismaService,
private readonly emailResolver: EmailToIdResolver,
) { }
async updateEmployee(email: string, dto: EmployeeDetailedDto, employee_email?: string): Promise<Result<boolean, string>> {
const account_email = employee_email ?? email;
const user_id = await this.emailResolver.resolveUserIdWithEmail(account_email);
if (!user_id.success) return { success: false, error: 'EMPLOYEE_NOT_FOUND' }
const company_code = toCompanyCodeFromString(dto.company_name);
const supervisor_id = await this.toIdFromFullName(dto.supervisor_full_name);
const normalized_access = await toBooleanFromString(dto.user_module_access);
await this.prisma.$transaction(async (tx) => {
await tx.users.update({
where: { id: user_id.data },
data: {
first_name: dto.first_name,
last_name: dto.last_name,
email: dto.email,
phone_number: dto.phone_number,
residence: dto.residence,
user_module_access: {
update: {
dashboard: normalized_access.dashboard,
employee_list: normalized_access.employee_list,
employee_management: normalized_access.employee_management,
personal_profile: normalized_access.personal_profile,
timesheets: normalized_access.timesheets,
timesheets_approval: normalized_access.timesheets_approval,
},
},
},
});
return tx.employees.update({
where: { user_id: user_id.data },
data: {
company_code: company_code,
job_title: dto.job_title,
first_work_day: dto.first_work_day,
last_work_day: dto.last_work_day,
is_supervisor: dto.is_supervisor,
supervisor_id: supervisor_id,
},
});
});
return { success: true, data: true };
}
private toIdFromFullName = async (full_name: string) => {
const [first_name, last_name] = full_name.split(' ', 2);
let supervisor = await this.prisma.users.findFirst({
where: { first_name, last_name },
select: { employee: { select: { id: true } } }
});
if (!supervisor) supervisor = null;
return supervisor?.employee?.id;
}
}

View File

@ -7,3 +7,19 @@ export function toDateOrUndefined(v?: string | null): Date | undefined {
const day = toDateOrNull(v ?? undefined); const day = toDateOrNull(v ?? undefined);
return day === null ? undefined : day; return day === null ? undefined : day;
} }
export function toCompanyCodeFromString(company_name: string) {
let company_code = 271585;
if (company_name === 'Targo') {
company_code = 271583;
}
return company_code;
}
export function toStringFromCompanyCode(company_code: number) {
let company_name = 'Solucom';
if (company_code === 271583) {
company_name = 'Targo';
}
return company_name;
}

View File

@ -2,7 +2,7 @@ import { Module } from "@nestjs/common";
import { EmailToIdResolver } from "src/common/mappers/email-id.mapper"; import { EmailToIdResolver } from "src/common/mappers/email-id.mapper";
import { EmployeesController } from "src/identity-and-account/employees/controllers/employees.controller"; import { EmployeesController } from "src/identity-and-account/employees/controllers/employees.controller";
import { EmployeesModule } from "src/identity-and-account/employees/employees.module"; import { EmployeesModule } from "src/identity-and-account/employees/employees.module";
import { EmployeesService } from "src/identity-and-account/employees/services/employees.service"; import { EmployeesGetService } from "src/identity-and-account/employees/services/employees-get.service";
import { PreferencesController } from "src/identity-and-account/preferences/controllers/preferences.controller"; import { PreferencesController } from "src/identity-and-account/preferences/controllers/preferences.controller";
import { PreferencesModule } from "src/identity-and-account/preferences/preferences.module"; import { PreferencesModule } from "src/identity-and-account/preferences/preferences.module";
import { PreferencesService } from "src/identity-and-account/preferences/services/preferences.service"; import { PreferencesService } from "src/identity-and-account/preferences/services/preferences.service";
@ -12,6 +12,8 @@ import { AccessGetService } from "src/identity-and-account/user-module-access/se
import { AccessUpdateService } from "src/identity-and-account/user-module-access/services/module-access-update.service"; import { AccessUpdateService } from "src/identity-and-account/user-module-access/services/module-access-update.service";
import { UsersService } from "src/identity-and-account/users-management/services/users.service"; import { UsersService } from "src/identity-and-account/users-management/services/users.service";
import { UsersModule } from "src/identity-and-account/users-management/users.module"; import { UsersModule } from "src/identity-and-account/users-management/users.module";
import { EmployeesCreateService } from "src/identity-and-account/employees/services/employees-create.service";
import { EmployeesUpdateService } from "src/identity-and-account/employees/services/employees-update.service";
@Module({ @Module({
imports: [ imports: [
@ -26,7 +28,9 @@ import { UsersModule } from "src/identity-and-account/users-management/users.mod
ModuleAccessController, ModuleAccessController,
], ],
providers: [ providers: [
EmployeesService, EmployeesGetService,
EmployeesCreateService,
EmployeesUpdateService,
PreferencesService, PreferencesService,
UsersService, UsersService,
EmailToIdResolver, EmailToIdResolver,

View File

@ -1,4 +1,4 @@
import { IsEmail, IsArray, ArrayNotEmpty, ArrayUnique, IsISO8601, IsOptional, IsString, IsNumber, Min, Max, IsEnum } from "class-validator"; import { IsEmail, IsArray, IsOptional, IsString, IsNumber, IsEnum, IsDateString } from "class-validator";
import { LeaveApprovalStatus, LeaveTypes } from "@prisma/client"; import { LeaveApprovalStatus, LeaveTypes } from "@prisma/client";
import { Type } from "class-transformer"; import { Type } from "class-transformer";
@ -7,13 +7,11 @@ export const REQUEST_TYPES = Object.values(LeaveTypes) as readonly LeaveTypes
export type RequestTypes = (typeof REQUEST_TYPES)[number]; export type RequestTypes = (typeof REQUEST_TYPES)[number];
export class LeaveRequestDto { export class LeaveRequestDto {
@IsEmail() @IsEmail() @IsString()
email!: string; email!: string;
@IsArray() @IsArray()
@ArrayNotEmpty() @IsDateString()
@ArrayUnique()
@IsISO8601({}, { each: true })
dates!: string[]; dates!: string[];
@IsEnum(LeaveTypes) @IsEnum(LeaveTypes)
@ -25,8 +23,6 @@ export class LeaveRequestDto {
@IsOptional() @IsOptional()
@Type(() => Number) @Type(() => Number)
@IsNumber({ maxDecimalPlaces: 2 }) @IsNumber({ maxDecimalPlaces: 2 })
@Min(0)
@Max(24)
requested_hours?: number; requested_hours?: number;
@IsEnum(LeaveApprovalStatus) @IsEnum(LeaveApprovalStatus)

View File

@ -1,25 +0,0 @@
import { LeaveRequestViewDto } from "../dtos/leave-request-view.dto";
import { Prisma } from "@prisma/client";
import { leave_requests_select } from "src/time-and-attendance/utils/selects.utils";
type LeaveRequestRow = Prisma.LeaveRequestsGetPayload<{ select: typeof leave_requests_select}>;
const toNum = (value?: Prisma.Decimal | null) =>
value !== null && value !== undefined ? Number(value) : undefined;
// export function mapRowToView(row: LeaveRequestRow): LeaveRequestViewDto {
// const iso_date = row.dates?.toISOString().slice(0, 10);
// if (!iso_date) throw new Error(`Leave request #${row.id} has no date set.`);
// return {
// id: row.id,
// leave_type: row.leave_type,
// date: iso_date,
// payable_hours: toNum(row.payable_hours),
// requested_hours: toNum(row.requested_hours),
// comment: row.comment,
// approval_status: row.approval_status,
// email: row.employee.user.email,
// employee_full_name: `${row.employee.user.first_name} ${row.employee.user.last_name}`,
// };
// }

View File

@ -58,7 +58,6 @@ export class PayPeriodsController {
@Param('periodNumber', ParseIntPipe) period_no: number, @Param('periodNumber', ParseIntPipe) period_no: number,
@Query('includeSubtree', new ParseBoolPipe({ optional: true })) include_subtree = false, @Query('includeSubtree', new ParseBoolPipe({ optional: true })) include_subtree = false,
): Promise<Result<PayPeriodOverviewDto, string>> { ): Promise<Result<PayPeriodOverviewDto, string>> {
if (!email) throw new UnauthorizedException(`Session infos not found`);
return this.queryService.getCrewOverview(year, period_no, email, include_subtree); return this.queryService.getCrewOverview(year, period_no, email, include_subtree);
} }

View File

@ -15,7 +15,7 @@ export class SchedulePresetsApplyService {
) { } ) { }
async applyToTimesheet(email: string, id: number, start_date_iso: string): Promise<Result<ApplyResult, string>> { 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' }; if (!DATE_ISO_FORMAT.test(start_date_iso)) return { success: false, error: 'INVALID_PRESET' };
const employee_id = await this.emailResolver.findIdByEmail(email); const employee_id = await this.emailResolver.findIdByEmail(email);
if (!employee_id.success) return { success: false, error: employee_id.error } if (!employee_id.success) return { success: false, error: employee_id.error }
@ -37,7 +37,7 @@ export class SchedulePresetsApplyService {
}, },
}, },
}); });
if (!preset) return { success: false, error: `Schedule preset with id: ${id} not found` }; if (!preset) return { success: false, error: `PRESET_NOT_FOUND` };
const start_date = new Date(`${start_date_iso}T00:00:00.000Z`); const start_date = new Date(`${start_date_iso}T00:00:00.000Z`);
@ -91,7 +91,7 @@ export class SchedulePresetsApplyService {
if (shift.end_time.getTime() <= shift.start_time.getTime()) { if (shift.end_time.getTime() <= shift.start_time.getTime()) {
return { return {
success: false, success: false,
error: `Invalid time range in preset day: ${week_day}, order: ${shift.sort_order}` error: `INVALID_PRESET_SHIFT`
}; };
} }
const conflict = existing.find((existe) => overlaps( const conflict = existing.find((existe) => overlaps(
@ -101,7 +101,7 @@ export class SchedulePresetsApplyService {
if (conflict) if (conflict)
return { return {
success: false, success: false,
error: `[SHIFT_OVERLAP] :Preset shift overlaps existing shift on ${start_date} + ${i}(week day ${week_day}) ` error: `OVERLAPING_SHIFT`
}; };
payload.push({ payload.push({

View File

@ -95,31 +95,7 @@ export class SchedulePresetsCreateService {
}); });
return { success: true, data: true } return { success: true, data: true }
} catch (error) { } catch (error) {
return { success: false, error: 'INVALID_SCHEDULE_PRESET'} return { success: false, error: 'INVALID_SCHEDULE_PRESET' }
} }
} }
// //PRIVATE HELPERS
//resolve bank_code_id using type and convert hours to TIME and valid shifts end/start
// private async normalizePresetShifts(preset_shift: SchedulePresetShiftsDto, schedul_preset: SchedulePresetsDto): Promise<Result<Normalized, string>> {
// const bank_code = await this.typeResolver.findIdAndModifierByType(preset_shift.type);
// if (!bank_code.success) return { success: false, error: 'INVALID_SCHEDULE_PRESET_SHIFT' };
// const start = await toDateFromHHmm(preset_shift.start_time);
// const end = await toDateFromHHmm(preset_shift.end_time);
// //TODO: add a way to fetch
// const normalized_preset_shift:Normalized = {
// date: ,
// start_time : start,
// end_time: end,
// bank_code_id: bank_code.data.id,
// }
// return { success: true data: normalized_preset_shift }
// }
} }