feat(presets): small ajustements

This commit is contained in:
Matthieu Haineault 2025-11-10 11:16:10 -05:00
parent 03d9fa2cf4
commit 6332a42fa7
4 changed files with 80 additions and 71 deletions

View File

@ -55,10 +55,10 @@ export class SchedulePresetsController {
@Post('apply-presets') @Post('apply-presets')
async applyPresets( async applyPresets(
@Req() req, @Req() req,
@Body('preset') preset_name: string, @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_name, start_date); return this.applyPresetsService.applyToTimesheet(email, preset_id, start_date);
} }
} }

View File

@ -1,7 +1,11 @@
import { ArrayMinSize, IsArray, IsBoolean, IsOptional, IsString } from "class-validator"; import { ArrayMinSize, IsArray, IsBoolean, IsInt, IsOptional, IsString } from "class-validator";
import { SchedulePresetShiftsDto } from "src/time-and-attendance/time-tracker/schedule-presets/dtos/create-schedule-preset-shifts.dto"; import { SchedulePresetShiftsDto } from "src/time-and-attendance/time-tracker/schedule-presets/dtos/create-schedule-preset-shifts.dto";
export class SchedulePresetsDto { export class SchedulePresetsDto {
@IsInt()
id!: number;
@IsString() @IsString()
name!: string; name!: string;

View File

@ -11,18 +11,19 @@ import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-i
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, preset_name: string, start_date_iso: string ): Promise<ApplyResult> { async applyToTimesheet( email: string, id: number, start_date_iso: string ): Promise<ApplyResult> {
if(!preset_name?.trim()) throw new BadRequestException('A preset_name is required'); if(!id) throw new BadRequestException(`Schedule preset with id: ${id} not found`);
if(!DATE_ISO_FORMAT.test(start_date_iso)) throw new BadRequestException('start_date must be of format :YYYY-MM-DD'); if(!DATE_ISO_FORMAT.test(start_date_iso)) throw new BadRequestException('start_date must be of format :YYYY-MM-DD');
const employee_id = await this.emailResolver.findIdByEmail(email); const employee_id = await this.emailResolver.findIdByEmail(email);
const preset = await this.prisma.schedulePresets.findFirst({ const preset = await this.prisma.schedulePresets.findFirst({
where: { employee_id, name: preset_name }, where: { employee_id, id },
include: { include: {
shifts: { shifts: {
orderBy: [{ week_day: 'asc'}, { sort_order: 'asc'}], orderBy: [{ week_day: 'asc'}, { sort_order: 'asc'}],
select: { select: {
id: true,
week_day: true, week_day: true,
sort_order: true, sort_order: true,
start_time: true, start_time: true,

View File

@ -10,127 +10,131 @@ import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-i
@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<CreatePresetResult> { async createPreset(email: string, dto: SchedulePresetsDto): Promise<CreatePresetResult> {
try { try {
const shifts_data = await this.resolveAndBuildPresetShifts(dto); const shifts_data = await this.resolveAndBuildPresetShifts(dto);
const employee_id = await this.emailResolver.findIdByEmail(email); const employee_id = await this.emailResolver.findIdByEmail(email);
if(!shifts_data) throw new BadRequestException(`Employee with email: ${email} or dto not found`); if (!shifts_data) throw new BadRequestException(`Employee with email: ${email} or dto not found`);
await this.prisma.$transaction(async (tx)=> { await this.prisma.$transaction(async (tx) => {
if(dto.is_default) { if (dto.is_default) {
await tx.schedulePresets.updateMany({ await tx.schedulePresets.updateMany({
where: { is_default: true, employee_id }, where: { is_default: true, employee_id },
data: { is_default: false }, data: { is_default: false },
}); });
} }
const created = await tx.schedulePresets.create({ const created = await tx.schedulePresets.create({
data: { data: {
id: dto.id,
employee_id, employee_id,
name: dto.name, name: dto.name,
is_default: !!dto.is_default, is_default: !!dto.is_default,
shifts: { create: shifts_data}, shifts: { create: shifts_data },
}, },
}); });
return created; return created;
}); });
return { ok: true }; return { ok: true };
} catch (error: unknown) { } catch (error: unknown) {
return { ok: false, error }; return { ok: false, error };
} }
} }
//_________________________________________________________________ //_________________________________________________________________
// UPDATE // UPDATE
//_________________________________________________________________ //_________________________________________________________________
async updatePreset( preset_id: number, dto: SchedulePresetsDto ): Promise<UpdatePresetResult> { async updatePreset(preset_id: number, dto: SchedulePresetsDto): Promise<UpdatePresetResult> {
try { try {
const existing = await this.prisma.schedulePresets.findFirst({ const existing = await this.prisma.schedulePresets.findFirst({
where: { id: preset_id }, where: { id: preset_id },
select: { select: {
id:true, id: true,
is_default: true, is_default: true,
employee_id: true, employee_id: true,
}, },
}); });
if(!existing) throw new NotFoundException(`Preset "${dto.name}" not found`); if (!existing) throw new NotFoundException(`Preset "${dto.name}" not found`);
const shifts_data = await this.resolveAndBuildPresetShifts(dto); const shifts_data = await this.resolveAndBuildPresetShifts(dto);
await this.prisma.$transaction(async (tx) => { await this.prisma.$transaction(async (tx) => {
if(typeof dto.is_default === 'boolean'){ if (typeof dto.is_default === 'boolean') {
if(dto.is_default) { if (dto.is_default) {
await tx.schedulePresets.updateMany({ await tx.schedulePresets.updateMany({
where: { where: {
employee_id: existing.employee_id, employee_id: existing.employee_id,
is_default: true, is_default: true,
NOT: { id: existing.id }, NOT: { id: existing.id },
}, },
data: { is_default: false }, data: { is_default: false },
}); });
} }
await tx.schedulePresets.update({ await tx.schedulePresets.update({
where: { id: existing.id }, where: { id: existing.id },
data: { data: {
is_default: dto.is_default, is_default: dto.is_default,
name: dto.name, name: dto.name,
}, },
}); });
} }
if(shifts_data.length <= 0) throw new BadRequestException('Preset shifts to update not found'); if (shifts_data.length <= 0) throw new BadRequestException('Preset shifts to update not found');
await tx.schedulePresetShifts.deleteMany({ where: { preset_id: existing.id } }); await tx.schedulePresetShifts.deleteMany({ where: { preset_id: existing.id } });
const create_many_data: Prisma.SchedulePresetShiftsCreateManyInput[] = const create_many_data: Prisma.SchedulePresetShiftsCreateManyInput[] =
shifts_data.map((shift)=> { shifts_data.map((shift) => {
if(!shift.bank_code || !('connect' in shift.bank_code) || typeof shift.bank_code.connect?.id !=='number'){ if (!shift.bank_code || !('connect' in shift.bank_code) || typeof shift.bank_code.connect?.id !== 'number') {
throw new NotFoundException(`Bank code is required for updates( ${shift.week_day}, ${shift.sort_order})`); throw new NotFoundException(`Bank code is required for updates( ${shift.week_day}, ${shift.sort_order})`);
} }
const bank_code_id = shift.bank_code.connect.id; const bank_code_id = shift.bank_code.connect.id;
return { return {
preset_id: existing.id, preset_id: existing.id,
week_day: shift.week_day, week_day: shift.week_day,
sort_order: shift.sort_order, sort_order: shift.sort_order,
start_time: shift.start_time, start_time: shift.start_time,
end_time: shift.end_time, end_time: shift.end_time,
is_remote: shift.is_remote ?? false, is_remote: shift.is_remote ?? false,
bank_code_id: bank_code_id, bank_code_id: bank_code_id,
}; };
}); });
await tx.schedulePresetShifts.createMany({data: create_many_data}); await tx.schedulePresetShifts.createMany({ data: create_many_data });
}); });
const saved = await this.prisma.schedulePresets.findUnique({ const saved = await this.prisma.schedulePresets.findUnique({
where: { id: existing.id }, where: { id: existing.id },
include: { shifts: { include: {
orderBy: [{ week_day: 'asc' }, { sort_order: 'asc' }], shifts: {
include: { bank_code: { select: { type: true }}}, orderBy: [{ week_day: 'asc' }, { sort_order: 'asc' }],
}}, include: { bank_code: { select: { type: true } } },
}
},
}); });
if(!saved) throw new NotFoundException(`Preset with id: ${existing.id} not found`); if (!saved) throw new NotFoundException(`Preset with id: ${existing.id} not found`);
const response_dto: SchedulePresetsDto = { const response_dto: SchedulePresetsDto = {
id: saved.id,
name: saved.name, name: saved.name,
is_default: saved.is_default, is_default: saved.is_default,
preset_shifts: saved.shifts.map((shift) => ({ preset_shifts: saved.shifts.map((shift) => ({
preset_id: shift.preset_id, preset_id: shift.preset_id,
week_day: shift.week_day, week_day: shift.week_day,
sort_order: shift.sort_order, sort_order: shift.sort_order,
type: shift.bank_code.type, type: shift.bank_code.type,
start_time: toHHmmFromDate(shift.start_time), start_time: toHHmmFromDate(shift.start_time),
end_time: toHHmmFromDate(shift.end_time), end_time: toHHmmFromDate(shift.end_time),
is_remote: shift.is_remote, is_remote: shift.is_remote,
})), })),
}; };
return { ok: true, id: existing.id, data: response_dto }; return { ok: true, id: existing.id, data: response_dto };
} catch (error){ } catch (error) {
return { ok: false, id: preset_id, error } return { ok: false, id: preset_id, error }
} }
} }
@ -138,22 +142,22 @@ export class SchedulePresetsUpsertService {
//_________________________________________________________________ //_________________________________________________________________
// DELETE // DELETE
//_________________________________________________________________ //_________________________________________________________________
async deletePreset( preset_id: number ): Promise <DeletePresetResult> { async deletePreset(preset_id: number): Promise<DeletePresetResult> {
try { try {
await this.prisma.$transaction(async (tx) => { await this.prisma.$transaction(async (tx) => {
const preset = await tx.schedulePresets.findFirst({ const preset = await tx.schedulePresets.findFirst({
where: { id: preset_id }, where: { id: preset_id },
select: { id: true }, select: { id: true },
}); });
if(!preset) throw new NotFoundException(`Preset with id ${ preset_id } not found`); if (!preset) throw new NotFoundException(`Preset with id ${preset_id} not found`);
await tx.schedulePresets.delete({where: { id: preset_id } }); await tx.schedulePresets.delete({ where: { id: preset_id } });
return { success: true }; return { success: true };
}); });
return { ok: true, id: preset_id }; return { ok: true, id: preset_id };
} catch (error) { } catch (error) {
if(error) throw new NotFoundException(`Preset schedule with id ${ preset_id } not found`); if (error) throw new NotFoundException(`Preset schedule with id ${preset_id} not found`);
return { ok: false, id: preset_id, error }; return { ok: false, id: preset_id, error };
} }
} }
@ -161,19 +165,19 @@ export class SchedulePresetsUpsertService {
//PRIVATE HELPERS //PRIVATE HELPERS
//resolve bank_code_id using type and convert hours to TIME and valid shifts end/start //resolve bank_code_id using type and convert hours to TIME and valid shifts end/start
private async resolveAndBuildPresetShifts( private async resolveAndBuildPresetShifts(
dto: SchedulePresetsDto dto: SchedulePresetsDto
): Promise<Prisma.SchedulePresetShiftsCreateWithoutPresetInput[]>{ ): Promise<Prisma.SchedulePresetShiftsCreateWithoutPresetInput[]> {
if(!dto.preset_shifts?.length) throw new NotFoundException(`Empty or preset shifts not found`); if (!dto.preset_shifts?.length) throw new NotFoundException(`Empty or preset shifts not found`);
const types = Array.from(new Set(dto.preset_shifts.map((shift)=> shift.type))); const types = Array.from(new Set(dto.preset_shifts.map((shift) => shift.type)));
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.findIdAndModifierByType(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`);
const pair_set = new Set<string>(); const pair_set = new Set<string>();
@ -185,25 +189,25 @@ export class SchedulePresetsUpsertService {
pair_set.add(key); pair_set.add(key);
} }
const items: Prisma.SchedulePresetShiftsCreateWithoutPresetInput[] = dto.preset_shifts.map((shift)=> { const items: Prisma.SchedulePresetShiftsCreateWithoutPresetInput[] = dto.preset_shifts.map((shift) => {
const bank_code_id = bank_code_set.get(shift.type); const bank_code_id = bank_code_set.get(shift.type);
if(!bank_code_id) throw new NotFoundException(`Bank code not found for type ${shift.type}`); if (!bank_code_id) throw new NotFoundException(`Bank code not found for type ${shift.type}`);
if (!shift.start_time || !shift.end_time) { if (!shift.start_time || !shift.end_time) {
throw new BadRequestException(`start_time and end_time are required for (${shift.week_day}, ${shift.sort_order})`); throw new BadRequestException(`start_time and end_time are required for (${shift.week_day}, ${shift.sort_order})`);
} }
const start = toTime(shift.start_time); const start = toTime(shift.start_time);
const end = toTime(shift.end_time); const end = toTime(shift.end_time);
if(end.getTime() <= start.getTime()) { if (end.getTime() <= start.getTime()) {
throw new ConflictException(`end_time must be > start_time ( day: ${shift.week_day}, order: ${shift.sort_order})`); throw new ConflictException(`end_time must be > start_time ( day: ${shift.week_day}, order: ${shift.sort_order})`);
} }
return { return {
week_day: shift.week_day as Weekday, week_day: shift.week_day as Weekday,
sort_order: shift.sort_order, sort_order: shift.sort_order,
bank_code: { connect: { id: bank_code_id} }, bank_code: { connect: { id: bank_code_id } },
start_time: start, start_time: start,
end_time: end, end_time: end,
is_remote: !!shift.is_remote, is_remote: !!shift.is_remote,
}; };
}); });
return items; return items;