feat(schedulePresets): ajusted the create function. added validation of the name and overlaps checking

This commit is contained in:
Matthieu Haineault 2025-11-25 16:32:20 -05:00
parent 35665d49dd
commit c5c96cce22
5 changed files with 291 additions and 300 deletions

View File

@ -1,64 +1,66 @@
// import { Controller, Param, Query, Body, Get, Post, ParseIntPipe, Delete, Patch, Req } from "@nestjs/common"; import { Controller, Param, Query, Body, Get, Post, ParseIntPipe, Delete, Patch, Req } from "@nestjs/common";
// import { RolesAllowed } from "src/common/decorators/roles.decorators"; import { RolesAllowed } from "src/common/decorators/roles.decorators";
// import { GLOBAL_CONTROLLER_ROLES, MANAGER_ROLES } from "src/common/shared/role-groupes"; import { GLOBAL_CONTROLLER_ROLES, MANAGER_ROLES } from "src/common/shared/role-groupes";
// import { SchedulePresetsDto } from "src/time-and-attendance/schedule-presets/dtos/create-schedule-presets.dto"; import { SchedulePresetsDto } from "src/time-and-attendance/schedule-presets/dtos/create-schedule-presets.dto";
// import { SchedulePresetsUpdateDto } from "src/time-and-attendance/schedule-presets/dtos/update-schedule-presets.dto"; // import { SchedulePresetsUpdateDto } from "src/time-and-attendance/schedule-presets/dtos/update-schedule-presets.dto";
// import { SchedulePresetsApplyService } from "src/time-and-attendance/schedule-presets/services/schedule-presets-apply.service"; import { SchedulePresetsApplyService } from "src/time-and-attendance/schedule-presets/services/schedule-presets-apply.service";
// import { SchedulePresetsGetService } from "src/time-and-attendance/schedule-presets/services/schedule-presets-get.service"; import { SchedulePresetsGetService } from "src/time-and-attendance/schedule-presets/services/schedule-presets-get.service";
// import { SchedulePresetsUpsertService } from "src/time-and-attendance/schedule-presets/services/schedule-presets-upsert.service"; import { SchedulePresetsUpsertService } from "src/time-and-attendance/schedule-presets/services/schedule-presets-upsert.service";
// @Controller('schedule-presets') @Controller('schedule-presets')
// @RolesAllowed(...GLOBAL_CONTROLLER_ROLES) @RolesAllowed(...GLOBAL_CONTROLLER_ROLES)
// export class SchedulePresetsController { export class SchedulePresetsController {
// constructor( constructor(
// private readonly upsertService: SchedulePresetsUpsertService, private readonly upsertService: SchedulePresetsUpsertService,
// private readonly getService: SchedulePresetsGetService, private readonly getService: SchedulePresetsGetService,
// private readonly applyPresetsService: SchedulePresetsApplyService, private readonly applyPresetsService: SchedulePresetsApplyService,
// ) { } ) { }
// //used to create a schedule preset // used to create a schedule preset
// @Post('create') @Post('create')
// @RolesAllowed(...MANAGER_ROLES) @RolesAllowed(...MANAGER_ROLES)
// async createPreset(@Req() req, @Body() dto: SchedulePresetsDto) { async createPreset(@Req() req, @Body() dto: SchedulePresetsDto) {
// const email = req.user?.email; const email = req.user?.email;
// return await this.upsertService.createPreset(email, dto); return await this.upsertService.createPreset(email, dto);
// } }
// // //used to update an already existing schedule preset // //used to update an already existing schedule preset
// // @Patch('update/:preset_id') // @Patch('update/:preset_id')
// // @RolesAllowed(...MANAGER_ROLES) // @RolesAllowed(...MANAGER_ROLES)
// // async updatePreset( // async updatePreset(
// // @Param('preset_id', ParseIntPipe) preset_id: number, // @Param('preset_id', ParseIntPipe) preset_id: number,
// // @Body() dto: SchedulePresetsUpdateDto // @Body() dto: SchedulePresetsUpdateDto
// // ) { // ) {
// // return await this.upsertService.updatePreset(preset_id, dto); // return await this.upsertService.updatePreset(preset_id, dto);
// // } // }
// //used to delete a schedule preset //used to delete a schedule preset
// @Delete('delete/:preset_id') // @Delete('delete/:preset_id')
// @RolesAllowed(...MANAGER_ROLES) // @RolesAllowed(...MANAGER_ROLES)
// async deletePreset( // async deletePreset(
// @Param('preset_id', ParseIntPipe) preset_id: number) { // @Param('preset_id', ParseIntPipe) preset_id: number) {
// return await this.upsertService.deletePreset(preset_id); // return await this.upsertService.deletePreset(preset_id);
// } // }
// //used to show the list of available schedule presets //used to show the list of available schedule presets
// @Get('find-list') @Get('find-list')
// @RolesAllowed(...MANAGER_ROLES) @RolesAllowed(...MANAGER_ROLES)
// async findListById(@Req() req) { async findListById(
// const email = req.user?.email; @Req() req
// return this.getService.getSchedulePresets(email); ) {
// } const email = req.user?.email;
return this.getService.getSchedulePresets(email);
}
// //used to apply a preset to a timesheet //used to apply a preset to a timesheet
// @Post('apply-presets') @Post('apply-presets')
// async applyPresets( async applyPresets(
// @Req() req, @Req() req,
// @Body('preset') preset_id: number, @Body('preset') preset_id: number,
// @Body('start') start_date: string @Body('start') start_date: string
// ) { ) {
// const email = req.user?.email; const email = req.user?.email;
// return this.applyPresetsService.applyToTimesheet(email, preset_id, start_date); return this.applyPresetsService.applyToTimesheet(email, preset_id, start_date);
// } }
// } }

View File

@ -3,28 +3,11 @@ import { HH_MM_REGEX } from "src/common/utils/constants.utils";
import { Weekday } from "@prisma/client"; import { Weekday } from "@prisma/client";
export class SchedulePresetShiftsDto { export class SchedulePresetShiftsDto {
@IsEnum(Weekday) @IsInt() preset_id!: number;
week_day!: Weekday; @IsEnum(Weekday) week_day!: Weekday;
@IsInt() @Min(1) sort_order!: number;
@IsInt() @IsString() type!: string;
preset_id!: number; @IsString() @Matches(HH_MM_REGEX) start_time!: string;
@IsString() @Matches(HH_MM_REGEX) end_time!: string;
@IsInt() @IsOptional() @IsBoolean() is_remote?: boolean;
@Min(1)
sort_order!: number;
@IsString()
type!: string;
@IsString()
@Matches(HH_MM_REGEX)
start_time!: string;
@IsString()
@Matches(HH_MM_REGEX)
end_time!: string;
@IsOptional()
@IsBoolean()
is_remote?: boolean;
} }

View File

@ -2,18 +2,8 @@ import { ArrayMinSize, IsArray, IsBoolean, IsInt, IsOptional, IsString } from "c
import { SchedulePresetShiftsDto } from "src/time-and-attendance/schedule-presets/dtos/create-schedule-preset-shifts.dto"; import { SchedulePresetShiftsDto } from "src/time-and-attendance/schedule-presets/dtos/create-schedule-preset-shifts.dto";
export class SchedulePresetsDto { export class SchedulePresetsDto {
@IsInt() id!: number;
@IsInt() @IsString() name!: string;
id!: number; @IsBoolean() @IsOptional() is_default: boolean;
@IsArray() @ArrayMinSize(1) preset_shifts: SchedulePresetShiftsDto[];
@IsString()
name!: string;
@IsBoolean()
@IsOptional()
is_default: boolean;
@IsArray()
@ArrayMinSize(1)
preset_shifts: SchedulePresetShiftsDto[];
} }

View File

@ -9,7 +9,10 @@ import { Result } from "src/common/errors/result-error.factory";
@Injectable() @Injectable()
export class SchedulePresetsApplyService { 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<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: 'start_date must be of format :YYYY-MM-DD' };

View File

@ -1,227 +1,240 @@
// import { Injectable, BadRequestException, NotFoundException, ConflictException } from "@nestjs/common"; import { Injectable } from "@nestjs/common";
// import { Prisma, Weekday } from "@prisma/client"; import { Weekday } from "@prisma/client";
// import { PrismaService } from "src/prisma/prisma.service"; import { PrismaService } from "src/prisma/prisma.service";
// import { BankCodesResolver } from "src/common/mappers/bank-type-id.mapper"; import { BankCodesResolver } from "src/common/mappers/bank-type-id.mapper";
// import { EmailToIdResolver } from "src/common/mappers/email-id.mapper"; import { EmailToIdResolver } from "src/common/mappers/email-id.mapper";
// import { Result } from "src/common/errors/result-error.factory"; import { Result } from "src/common/errors/result-error.factory";
// import { toHHmmFromDate, toDateFromString } from "src/common/utils/date-utils"; import { overlaps, toDateFromHHmm } from "src/common/utils/date-utils";
// import { SchedulePresetsDto } from "src/time-and-attendance/schedule-presets/dtos/create-schedule-presets.dto"; import { SchedulePresetsDto } from "src/time-and-attendance/schedule-presets/dtos/create-schedule-presets.dto";
// @Injectable() @Injectable()
// export class SchedulePresetsUpsertService { export class SchedulePresetsUpsertService {
// constructor( constructor(
// private readonly prisma: PrismaService, private readonly prisma: PrismaService,
// private readonly typeResolver: BankCodesResolver, private readonly typeResolver: BankCodesResolver,
// private readonly emailResolver: EmailToIdResolver, private readonly emailResolver: EmailToIdResolver,
// ) { } ) { }
// //_________________________________________________________________ //_________________________________________________________________
// // CREATE // CREATE
// //_________________________________________________________________ //_________________________________________________________________
// async createPreset(email: string, dto: SchedulePresetsDto): Promise<Result<SchedulePresetsDto, string>> { async createPreset(email: string, dto: SchedulePresetsDto): Promise<Result<boolean, string>> {
// try { //validate email and fetch employee_id
// const shifts_data = await this.normalizePresetShifts(dto); const employee_id = await this.emailResolver.findIdByEmail(email);
// if (!shifts_data.success) return { success: false, error: `Employee with email: ${email} or dto not found` }; if (!employee_id.success) return { success: false, error: employee_id.error };
// const employee_id = await this.emailResolver.findIdByEmail(email); //validate new unique name
// if (!employee_id.success) return { success: false, error: employee_id.error }; const existing = await this.prisma.schedulePresets.findFirst({
where: { name: dto.name, employee_id: employee_id.data },
select: { name: true },
});
if (!existing) return { success: false, error: 'INVALID_SCHEDULE_PRESET' };
// const created = await this.prisma.$transaction(async (tx) => { const normalized_shifts = dto.preset_shifts.map((shift) => ({
// if (dto.is_default) { ...shift,
// await tx.schedulePresets.updateMany({ start: toDateFromHHmm(shift.start_time),
// where: { is_default: true, employee_id: employee_id.data }, end: toDateFromHHmm(shift.end_time),
// 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 }
// }
// });
// return { success: true, data: created }
// } catch (error) {
// return { success: false, error: ' An error occured during create. Invalid Schedule data' };
// }
// }
// //_________________________________________________________________ for (const preset_shifts of normalized_shifts) {
// // UPDATE for (const other_shifts of normalized_shifts) {
// //_________________________________________________________________ //skip if same object or id week_day is not the same
// async updatePreset(preset_id: number, dto: SchedulePresetsDto): Promise<Result<SchedulePresetsDto, string>> { if (preset_shifts === other_shifts) continue;
// try { if (preset_shifts.week_day !== other_shifts.week_day) continue;
// const existing = await this.prisma.schedulePresets.findFirst({ //check overlaping possibilities
// where: { id: preset_id }, const has_overlap = overlaps(
// select: { { start: preset_shifts.start, end: preset_shifts.end },
// id: true, { start: other_shifts.start, end: other_shifts.end },
// is_default: true, )
// employee_id: true, if (has_overlap) return { success: false, error: 'SCHEDULE_PRESET_OVERLAP' };
// }, }
// }); }
// if (!existing) return { success: false, error: `Preset "${dto.name}" not found` }; //validate bank_code_id/type and map them
const bank_code_results = await Promise.all(dto.preset_shifts.map((shift) =>
this.typeResolver.findBankCodeIDByType(shift.type),
));
for (const result of bank_code_results) {
if (!result.success) return { success: false, error: 'INVALID_SCHEDULE_PRESET' }
}
// const shifts_data = await this.normalizePresetShifts(dto); await this.prisma.$transaction(async (tx) => {
// if (!shifts_data.success) return { success: false, error: 'An error occured during normalization' } //check if employee chose this preset has a default preset and ensure all others are false
if (dto.is_default) {
await tx.schedulePresets.updateMany({
where: { employee_id: employee_id.data, is_default: true },
data: { is_default: false },
});
}
// await this.prisma.$transaction(async (tx) => { await tx.schedulePresets.create({
// if (typeof dto.is_default === 'boolean') { data: {
// if (dto.is_default) { employee_id: employee_id.data,
// await tx.schedulePresets.updateMany({ name: dto.name,
// where: { is_default: dto.is_default ?? false,
// employee_id: existing.employee_id, shifts: {
// is_default: true, create: dto.preset_shifts.map((shift, index) => {
// NOT: { id: existing.id }, //validated bank_codes sent as a Result Array to access its data
// }, const result = bank_code_results[index] as { success: true, data: number };
// data: { is_default: false }, return {
// }); week_day: shift.week_day,
// } sort_order: shift.sort_order,
// await tx.schedulePresets.update({ start_time: toDateFromHHmm(shift.start_time),
// where: { id: existing.id }, end_time: toDateFromHHmm(shift.end_time),
// data: { is_remote: shift.is_remote ?? false,
// is_default: dto.is_default, bank_code: {
// name: dto.name, //connect uses the FK links to set the bank_code_id
// }, connect: { id: result.data },
// }); },
// } }
// if (shifts_data.data.length <= 0) return { success: false, error: 'Preset shifts to update not found' }; }),
},
},
});
});
return { success: true, data: true }
}
// await tx.schedulePresetShifts.deleteMany({ where: { preset_id: existing.id } }); // //_________________________________________________________________
// // UPDATE
// //_________________________________________________________________
// async updatePreset(preset_id: number, dto: SchedulePresetsDto): Promise<Result<SchedulePresetsDto, string>> {
// try {
// const existing = await this.prisma.schedulePresets.findFirst({
// where: { id: preset_id },
// select: {
// id: true,
// is_default: true,
// employee_id: true,
// },
// });
// if (!existing) return { success: false, error: `Preset "${dto.name}" not found` };
// try { // const shifts_data = await this.normalizePresetShifts(dto);
// const create_many_data: Result<Prisma.SchedulePresetShiftsCreateManyInput[], string> = // if (!shifts_data.success) return { success: false, error: 'An error occured during normalization' }
// 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 } // await this.prisma.$transaction(async (tx) => {
// } catch (error) { // if (typeof dto.is_default === 'boolean') {
// return { success: false, error: 'An error occured. Invalid data detected. ' }; // if (dto.is_default) {
// } // await tx.schedulePresets.updateMany({
// }); // where: {
// employee_id: existing.employee_id,
// is_default: true,
// NOT: { id: existing.id },
// },
// data: { is_default: false },
// });
// }
// await tx.schedulePresets.update({
// where: { id: existing.id },
// data: {
// is_default: dto.is_default,
// name: dto.name,
// },
// });
// }
// if (shifts_data.data.length <= 0) return { success: false, error: 'Preset shifts to update not found' };
// const saved = await this.prisma.schedulePresets.findUnique({ // await tx.schedulePresetShifts.deleteMany({ where: { preset_id: existing.id } });
// where: { id: existing.id },
// include: {
// shifts: {
// orderBy: [{ week_day: 'asc' }, { sort_order: 'asc' }],
// include: { bank_code: { select: { type: true } } },
// }
// },
// });
// if (!saved) return { success: false, error: `Preset with id: ${existing.id} not found` };
// const response_dto: SchedulePresetsDto = { // try {
// id: saved.id, // const create_many_data: Result<Prisma.SchedulePresetShiftsCreateManyInput[], string> =
// name: saved.name, // shifts_data.data.map((shift) => {
// is_default: saved.is_default, // if (!shift.bank_code || !('connect' in shift.bank_code) || typeof shift.bank_code.connect?.id !== 'number') {
// preset_shifts: saved.shifts.map((shift) => ({ // return { success: false, error: `Bank code is required for updates( ${shift.week_day}, ${shift.sort_order})`}
// preset_id: shift.preset_id, // }
// week_day: shift.week_day, // const bank_code_id = shift.bank_code.connect.id;
// sort_order: shift.sort_order, // return {
// type: shift.bank_code.type, // preset_id: existing.id,
// start_time: toHHmmFromDate(shift.start_time), // week_day: shift.week_day,
// end_time: toHHmmFromDate(shift.end_time), // sort_order: shift.sort_order,
// is_remote: shift.is_remote, // 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: response_dto }; // return { success: true, data: create_many_data }
// } catch (error) { // } catch (error) {
// return { success: false, error: 'An error occured during update. Invalid data' } // return { success: false, error: 'An error occured. Invalid data detected. ' };
// } // }
// } // });
// //_________________________________________________________________ // const saved = await this.prisma.schedulePresets.findUnique({
// // DELETE // where: { id: existing.id },
// //_________________________________________________________________ // include: {
// async deletePreset(preset_id: number): Promise<Result<number, string>> { // shifts: {
// try { // orderBy: [{ week_day: 'asc' }, { sort_order: 'asc' }],
// await this.prisma.$transaction(async (tx) => { // include: { bank_code: { select: { type: true } } },
// const preset = await tx.schedulePresets.findFirst({ // }
// where: { id: preset_id }, // },
// select: { id: true }, // });
// }); // if (!saved) return { success: false, error: `Preset with id: ${existing.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 }; // const response_dto: SchedulePresetsDto = {
// }); // id: saved.id,
// return { success: true, data: preset_id }; // name: saved.name,
// is_default: saved.is_default,
// preset_shifts: saved.shifts.map((shift) => ({
// preset_id: shift.preset_id,
// week_day: shift.week_day,
// sort_order: shift.sort_order,
// type: shift.bank_code.type,
// start_time: toHHmmFromDate(shift.start_time),
// end_time: toHHmmFromDate(shift.end_time),
// is_remote: shift.is_remote,
// })),
// };
// } catch (error) { // return { success: true, data: response_dto };
// return { success: false, error: `Preset schedule with id ${preset_id} not found` }; // } catch (error) {
// } // return { success: false, error: 'An error occured during update. Invalid data' }
// } // }
// }
// //PRIVATE HELPERS // //_________________________________________________________________
// // DELETE
// //_________________________________________________________________
// 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) return { success: false, error: `Preset with id ${preset_id} not found` };
// await tx.schedulePresets.delete({ where: { id: preset_id } });
// //resolve bank_code_id using type and convert hours to TIME and valid shifts end/start // return { success: true };
// private async normalizePresetShifts( // });
// dto: SchedulePresetsDto // return { success: true, data: preset_id };
// ): Promise<Result<Prisma.SchedulePresetShiftsCreateWithoutPresetInput[], string>> {
// if (!dto.preset_shifts?.length) return { success: false, error: `Empty or preset shifts not found` }
// const types = Array.from(new Set(dto.preset_shifts.map((shift) => shift.type))); // } catch (error) {
// const bank_code_set = new Map<string, number>(); // return { success: false, error: `Preset schedule with id ${preset_id} not found` };
// }
// }
// for (const type of types) { // //PRIVATE HELPERS
// 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 pair_set = new Set<string>(); //resolve bank_code_id using type and convert hours to TIME and valid shifts end/start
// for (const shift of dto.preset_shifts) { // private async normalizePresetShifts(preset_shift: SchedulePresetShiftsDto, schedul_preset: SchedulePresetsDto): Promise<Result<Normalized, string>> {
// const key = `${shift.week_day}:${shift.sort_order}`;
// if (pair_set.has(key)) {
// return { success: false, error: `Duplicate shift for day/order (${shift.week_day}, ${shift.sort_order})` }
// }
// pair_set.add(key);
// }
// const items = await dto.preset_shifts.map((shift) => { // const bank_code = await this.typeResolver.findIdAndModifierByType(preset_shift.type);
// try { // if (!bank_code.success) return { success: false, error: 'INVALID_SCHEDULE_PRESET_SHIFT' };
// const bank_code_id = bank_code_set.get(shift.type);
// if (!bank_code_id) return { success: false, error: `Bank code not found for type ${shift.type}` }
// if (!shift.start_time || !shift.end_time) {
// return { success: false, error: `start_time and end_time are required for (${shift.week_day}, ${shift.sort_order})` }
// }
// const start = toDateFromString(shift.start_time);
// const end = toDateFromString(shift.end_time);
// if (end.getTime() <= start.getTime()) {
// return { success: false, error: `end_time must be > start_time ( day: ${shift.week_day}, order: ${shift.sort_order})` }
// }
// return {
// sort_order: shift.sort_order,
// start_time: start,
// end_time: end,
// is_remote: !!shift.is_remote,
// week_day: shift.week_day as Weekday,
// bank_code: { connect: { id: bank_code_id } },
// }
// } catch (error) { // const start = await toDateFromHHmm(preset_shift.start_time);
// return { success: false, error: '' } // const end = await toDateFromHHmm(preset_shift.end_time);
// }
// }); // //TODO: add a way to fetch
// return { success: true, data: items};
// }
// } // 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 }
// }
}