From c0189dc61d601a10e3d2466ceffe47b9bbe4eab3 Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Thu, 6 Nov 2025 11:04:55 -0500 Subject: [PATCH] fix(shifts): rework update and create to match ShiftEntity --- .../controllers/employees.controller.ts | 30 +-- .../shifts/controllers/shift.controller.ts | 2 +- .../time-tracker/shifts/dtos/shift-get.dto.ts | 1 + .../shifts/dtos/shift-payload.dto.ts | 11 + .../shifts/services/shifts-upsert.service.ts | 242 +++++++++--------- src/time-and-attendance/utils/type.utils.ts | 5 +- 6 files changed, 149 insertions(+), 142 deletions(-) create mode 100644 src/time-and-attendance/time-tracker/shifts/dtos/shift-payload.dto.ts diff --git a/src/identity-and-account/employees/controllers/employees.controller.ts b/src/identity-and-account/employees/controllers/employees.controller.ts index fa45089..8b58d4a 100644 --- a/src/identity-and-account/employees/controllers/employees.controller.ts +++ b/src/identity-and-account/employees/controllers/employees.controller.ts @@ -15,6 +15,11 @@ export class EmployeesController { private readonly archiveService: EmployeesArchivalService, ) { } + @Get('profile/:email') + findOneProfile(@Param('email') email: string): Promise { + return this.employeesService.findOneProfile(email); + } + @Get('employee-list') @RolesAllowed(...MANAGER_ROLES) findListEmployees(): Promise { @@ -34,6 +39,8 @@ export class EmployeesController { return result; } + + //_____________________________________________________________________________________________ // Deprecated or unused methods //_____________________________________________________________________________________________ @@ -46,29 +53,6 @@ export class EmployeesController { // create(@Body() dto: CreateEmployeeDto): Promise { // return this.employeesService.create(dto); // } - // @Get() - // //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ACCOUNTING) - // @ApiOperation({summary: 'Find all employees' }) - // @ApiResponse({ status: 200, description: 'List of employees found', type: CreateEmployeeDto, isArray: true }) - // @ApiResponse({ status: 400, description: 'List of employees not found' }) - // findAll(): Promise { - // return this.employeesService.findAll(); - // } - - - // @Get(':email') - // //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR,RoleEnum.ACCOUNTING ) - // @ApiOperation({summary: 'Find employee' }) - // @ApiResponse({ status: 200, description: 'Employee found', type: CreateEmployeeDto }) - // @ApiResponse({ status: 400, description: 'Employee not found' }) - // findOne(@Param('email', ParseIntPipe) email: string): Promise { - // return this.employeesService.findOne(email); - // } - - @Get('profile/:email') - findOneProfile(@Param('email') email: string): Promise { - return this.employeesService.findOneProfile(email); - } // @Delete(':email') // //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR ) diff --git a/src/time-and-attendance/time-tracker/shifts/controllers/shift.controller.ts b/src/time-and-attendance/time-tracker/shifts/controllers/shift.controller.ts index 886536b..70c3034 100644 --- a/src/time-and-attendance/time-tracker/shifts/controllers/shift.controller.ts +++ b/src/time-and-attendance/time-tracker/shifts/controllers/shift.controller.ts @@ -23,7 +23,7 @@ export class ShiftController { } @Patch('update') - updateBatch( @Body() dtos: UpdateShiftDto[]): Promise{ + updateBatch( @Body() dtos: ShiftDto[]): Promise{ const list = Array.isArray(dtos) ? dtos: []; if(list.length === 0) throw new BadRequestException('Body is missing or invalid (update shifts)'); return this.upsert_service.updateShifts(dtos); diff --git a/src/time-and-attendance/time-tracker/shifts/dtos/shift-get.dto.ts b/src/time-and-attendance/time-tracker/shifts/dtos/shift-get.dto.ts index 969ca98..c5fd877 100644 --- a/src/time-and-attendance/time-tracker/shifts/dtos/shift-get.dto.ts +++ b/src/time-and-attendance/time-tracker/shifts/dtos/shift-get.dto.ts @@ -1,4 +1,5 @@ export class GetShiftDto { + shift_id: number; timesheet_id: number; type: string; date: string; diff --git a/src/time-and-attendance/time-tracker/shifts/dtos/shift-payload.dto.ts b/src/time-and-attendance/time-tracker/shifts/dtos/shift-payload.dto.ts new file mode 100644 index 0000000..ce2363b --- /dev/null +++ b/src/time-and-attendance/time-tracker/shifts/dtos/shift-payload.dto.ts @@ -0,0 +1,11 @@ +export class ShiftEntity { + id: number; + timesheet_id: number; + bank_code_id: number; + date: string; + start_time: string; + end_time: string; + is_remote: boolean; + is_approved: boolean; + comment?: string; +} diff --git a/src/time-and-attendance/time-tracker/shifts/services/shifts-upsert.service.ts b/src/time-and-attendance/time-tracker/shifts/services/shifts-upsert.service.ts index 12705a1..d02bed5 100644 --- a/src/time-and-attendance/time-tracker/shifts/services/shifts-upsert.service.ts +++ b/src/time-and-attendance/time-tracker/shifts/services/shifts-upsert.service.ts @@ -1,16 +1,15 @@ -import { CreateShiftResult, NormedOk, UpdateShiftResult, UpdateShiftPayload, UpdateShiftChanges, Normalized } from "src/time-and-attendance/utils/type.utils"; +import { CreateShiftResult, NormedOk, UpdateShiftResult, Normalized } from "src/time-and-attendance/utils/type.utils"; import { overlaps, toStringFromHHmm, toStringFromDate, toDateFromString, toHHmmFromString } from "src/time-and-attendance/utils/date-time.utils"; import { Injectable, BadRequestException, ConflictException, NotFoundException } from "@nestjs/common"; 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"; import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; import { OvertimeService } from "src/time-and-attendance/domains/services/overtime.service"; -import { UpdateShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-update.dto"; import { PrismaService } from "src/prisma/prisma.service"; import { GetShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-get.dto"; +import { ShiftEntity } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-payload.dto"; import { ShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-create.dto"; - - +import { response } from "express"; @Injectable() export class ShiftsUpsertService { @@ -33,54 +32,59 @@ export class ShiftsUpsertService { if (!Array.isArray(dtos) || dtos.length === 0) return []; const employee_id = await this.emailResolver.findIdByEmail(email); - - const normed_shifts = await Promise.all( - dtos.map(async (dto, index) => { - try { - const normed = await this.normalizeShiftDto(dto); - if (normed.end_time <= normed.start_time) { - const error = { - error_code: 'SHIFT_OVERLAP', - conflicts: { - start_time: toStringFromHHmm(normed.start_time), - end_time: toStringFromHHmm(normed.end_time), - date: toStringFromDate(normed.date), - }, - }; - return { index, error }; - } - if (!normed.end_time) throw new BadRequestException('A shift needs an end_time'); - if (!normed.start_time) throw new BadRequestException('A shift needs a start_time'); - - const timesheet = await this.prisma.timesheets.findUnique({ - where: { id: dto.timesheet_id, employee_id }, - select: timesheet_select, - }); - if (!timesheet) { - const error = { - error_code: 'INVALID_TIMESHEET', - conflicts: { - start_time: toStringFromHHmm(normed.start_time), - end_time: toStringFromHHmm(normed.end_time), - date: toStringFromDate(normed.date), - }, - }; - return { index, error }; - } - - return { - index, - dto, - normed, - timesheet_id: timesheet.id, + const results: CreateShiftResult[] = []; + const normed_shifts: (NormedOk | undefined)[] = await Promise.all(dtos.map(async (dto, index) => { + try { + const normed = await this.normalizeShiftDto(dto); + if (normed.end_time <= normed.start_time) { + const error = { + error_code: 'SHIFT_OVERLAP', + conflicts: { + start_time: toStringFromHHmm(normed.start_time), + end_time: toStringFromHHmm(normed.end_time), + date: toStringFromDate(normed.date), + }, }; - } catch (error) { - return { index, error }; + results.push({ ok: false, error }); } - })); - const ok_items = normed_shifts.filter( - (item): item is NormedOk & { timesheet_id: number } => "normed" in item); + const timesheet = await this.prisma.timesheets.findUnique({ + where: { id: dto.timesheet_id, employee_id }, + select: timesheet_select, + }); + if (!timesheet) { + const error = { + error_code: 'INVALID_TIMESHEET', + conflicts: { + start_time: toStringFromHHmm(normed.start_time), + end_time: toStringFromHHmm(normed.end_time), + date: toStringFromDate(normed.date), + }, + }; + results.push({ ok: false, error }); + return; + } + const bank_code = await this.typeResolver.findBankCodeIDByType(dto.type); + const entity: ShiftEntity = { + bank_code_id: bank_code.id, + ...dto, + }; + + return { + index, + dto: entity, + normed, + timesheet_id: timesheet.id, + }; + } catch (error) { + results.push({ ok: false, error }); + return; + } + + })); + + const ok_items = normed_shifts.filter((item) => item !== undefined); + const regroup_by_date = new Map(); ok_items.forEach(({ index, normed, timesheet_id }) => { @@ -151,7 +155,7 @@ export class ShiftsUpsertService { existing_map.set(key, rows.map((row) => ({ start_time: row.start_time, end_time: row.end_time, date: row.date }))); } - normed_shifts.forEach((x, i) => { + ok_items.forEach((x, i) => { if ("error" in x) results[i] = { ok: false, error: x.error }; }); @@ -184,7 +188,7 @@ export class ShiftsUpsertService { const row = await tx.shifts.create({ data: { timesheet_id: timesheet_id, - bank_code_id: normed.id, + bank_code_id: normed.bank_code_id, date: normed.date, start_time: normed.start_time, end_time: normed.end_time, @@ -207,6 +211,7 @@ export class ShiftsUpsertService { const summary = await this.overtime.getWeekOvertimeSummary(timesheet_id, normed.date, tx); const shift: GetShiftDto = { + shift_id: row.id, timesheet_id: timesheet_id, type: bank_type, date: toStringFromDate(row.date), @@ -235,26 +240,24 @@ export class ShiftsUpsertService { // update shifts in DB // recalculate overtime after update // return an updated version to display - async updateShifts(dtos: UpdateShiftDto[]): Promise { + async updateShifts(dtos: ShiftDto[]): Promise { if (!Array.isArray(dtos) || dtos.length === 0) throw new BadRequestException({ error_code: 'SHIFT_MISSING' }); - const updates: UpdateShiftPayload[] = await Promise.all(dtos.map((item) => { - const { shift_id, ...rest } = item; - if (!shift_id) throw new BadRequestException({ error_code: 'SHIFT_INVALID' }); - - const changes: UpdateShiftChanges = {}; - if (rest.date !== undefined) changes.date = rest.date; - if (rest.start_time !== undefined) changes.start_time = rest.start_time; - if (rest.end_time !== undefined) changes.end_time = rest.end_time; - if (rest.type !== undefined) changes.type = rest.type; - if (rest.is_remote !== undefined) changes.is_remote = rest.is_remote; - if (rest.comment !== undefined) changes.comment = rest.comment; - - return { shift_id, dto: changes }; + const updates: ShiftEntity[] = await Promise.all(dtos.map(async (item) => { + try { + const bank_code = await this.typeResolver.findBankCodeIDByType(item.type); + return { + bank_code_id: bank_code.id, + ...item, + } + } catch (error) { + throw new BadRequestException('INVALID_SHIFT'); + } })); return this.prisma.$transaction(async (tx) => { - const shift_ids = updates.map(update_shift => update_shift.shift_id); + + const shift_ids = updates.map(update_shift => update_shift.id); const rows = await tx.shifts.findMany({ where: { id: { in: shift_ids } }, select: shift_select, @@ -262,31 +265,28 @@ export class ShiftsUpsertService { const regroup_id = new Map(rows.map(r => [r.id, r])); for (const update of updates) { - const existing = regroup_id.get(update.shift_id); + const existing = regroup_id.get(update.id); if (!existing) { - return updates.map(exist => exist.shift_id === update.shift_id - ? ({ ok: false, id: update.shift_id, error: new NotFoundException({ error_code: 'SHIFT_MISSING' }) } as UpdateShiftResult) - : ({ ok: false, id: exist.shift_id, error: new BadRequestException({ error_code: 'SHIFT_INVALID' }) }) + return updates.map(exist => exist.id === update.id + ? ({ ok: false, id: update.id, error: new NotFoundException({ error_code: 'SHIFT_MISSING' }) } as UpdateShiftResult) + : ({ ok: false, id: exist.id, error: new BadRequestException({ error_code: 'SHIFT_INVALID' }) }) ); } if (existing.is_approved) { - return updates.map(exist => exist.shift_id === update.shift_id - ? ({ ok: false, id: update.shift_id, error: new BadRequestException({ error_code: 'SHIFT_INVALID' }) } as UpdateShiftResult) - : ({ ok: false, id: exist.shift_id, error: new BadRequestException({ error_code: 'SHIFT_INVALID' }) }) + return updates.map(exist => exist.id === update.id + ? ({ ok: false, id: update.id, error: new BadRequestException({ error_code: 'SHIFT_INVALID' }) } as UpdateShiftResult) + : ({ ok: false, id: exist.id, error: new BadRequestException({ error_code: 'SHIFT_INVALID' }) }) ); } } const planned_updates = updates.map(update => { - const exist_shift = regroup_id.get(update.shift_id)!; - const date_string = update.dto.date ?? toStringFromDate(exist_shift.date); - const start_string = update.dto.start_time ?? toStringFromHHmm(exist_shift.start_time); - const end_string = update.dto.end_time ?? toStringFromHHmm(exist_shift.end_time); + const exist_shift = regroup_id.get(update.id)!; const normed: Normalized = { - date: toDateFromString(date_string), - start_time: toHHmmFromString(start_string), - end_time: toHHmmFromString(end_string), - id: exist_shift.id, + date: toDateFromString(update.date), + start_time: toHHmmFromString(update.start_time), + end_time: toHHmmFromString(update.end_time), + bank_code_id: exist_shift.bank_code_id, }; return { update, exist_shift, normed }; }); @@ -329,9 +329,9 @@ export class ShiftsUpsertService { ); if (conflict) { return updates.map(exist => - exist.shift_id === planned.exist_shift.id + exist.id === planned.exist_shift.id ? ({ - ok: false, id: exist.shift_id, error:{ + ok: false, id: exist.id, error: { error_code: 'SHIFT_OVERLAP', conflicts: { start_time: toStringFromHHmm(conflict.start), @@ -340,7 +340,7 @@ export class ShiftsUpsertService { }, } } as UpdateShiftResult) - : ({ ok: false, id: exist.shift_id, error: new BadRequestException('Batch aborted due to overlap in another update') }) + : ({ ok: false, id: exist.id, error: new BadRequestException('Batch aborted due to overlap in another update') }) ); } } @@ -373,49 +373,59 @@ export class ShiftsUpsertService { }, }; - return updates.map(exist => ({ ok: false, id: exist.shift_id, error: error })); + return updates.map(exist => ({ ok: false, id: exist.id, error: error })); } } } const results: UpdateShiftResult[] = []; for (const planned of planned_updates) { - const data: any = {}; - const { dto } = planned.update; - if (dto.date !== undefined) data.date = planned.normed.date; - 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.type !== undefined) data.type = dto.type; - if (dto.is_remote !== undefined) data.is_remote = dto.is_remote; - if (dto.comment !== undefined) data.comment = dto.comment ?? null; + try { + const date = toStringFromDate(planned.normed.date); + const start_time = toStringFromHHmm(planned.normed.start_time); + const end_time = toStringFromHHmm(planned.normed.end_time); - const row = await tx.shifts.update({ - where: { id: planned.exist_shift.id }, - data, - select: shift_select, - }); + const data: Partial = { + bank_code_id: planned.normed.bank_code_id, + date: date, + start_time: start_time, + end_time: end_time, + is_remote: planned.update.is_remote, + is_approved: planned.exist_shift.is_approved, + comment: planned.update.comment, + }; - const summary_new = await this.overtime.getWeekOvertimeSummary(row.timesheet_id, planned.exist_shift.date, tx); - if (row.date.getTime() !== planned.exist_shift.date.getTime()) { - await this.overtime.getWeekOvertimeSummary(row.timesheet_id, row.date, tx); + const row = await tx.shifts.update({ + where: { id: planned.exist_shift.id }, + data, + select: shift_select, + }); + const summary_new = await this.overtime.getWeekOvertimeSummary(row.timesheet_id, planned.exist_shift.date, tx); + if (row.date.getTime() !== planned.exist_shift.date.getTime()) { + await this.overtime.getWeekOvertimeSummary(row.timesheet_id, row.date, tx); + } + + const type = await this.typeResolver.findTypeByBankCodeId(row.bank_code_id); + + const dto: GetShiftDto = { + shift_id: row.id, + timesheet_id: row.timesheet_id, + type: type.type, + date: toStringFromDate(row.date), + start_time: toStringFromHHmm(row.start_time), + end_time: toStringFromHHmm(row.end_time), + is_approved: row.is_approved, + is_remote: row.is_remote, + comment: row.comment ?? undefined, + }; + + results.push({ ok: true, id: planned.exist_shift.id, data: { shift: dto, overtime: summary_new } }); + } catch (error) { + throw new BadRequestException('INVALID_SHIFT'); } - - const shift: GetShiftDto = { - timesheet_id: row.timesheet_id, - type: data.type, - date: toStringFromDate(row.date), - start_time: toStringFromHHmm(row.start_time), - end_time: toStringFromHHmm(row.end_time), - is_approved: row.is_approved, - is_remote: row.is_remote, - comment: row.comment ?? undefined, - }; - - results.push({ ok: true, id: planned.exist_shift.id, data: { shift, overtime: summary_new } }); } return results; }); - } //_________________________________________________________________ @@ -451,6 +461,6 @@ export class ShiftsUpsertService { const date = toDateFromString(dto.date); const start_time = toHHmmFromString(dto.start_time); const end_time = toHHmmFromString(dto.end_time); - return { date, start_time, end_time, id: bank_code_id }; + return { date, start_time, end_time, bank_code_id: bank_code_id }; } } diff --git a/src/time-and-attendance/utils/type.utils.ts b/src/time-and-attendance/utils/type.utils.ts index a4844bf..140ee97 100644 --- a/src/time-and-attendance/utils/type.utils.ts +++ b/src/time-and-attendance/utils/type.utils.ts @@ -4,6 +4,7 @@ import { updateExpenseDto } from "src/time-and-attendance/expenses/dtos/expense- import { SchedulePresetsDto } from "src/time-and-attendance/time-tracker/schedule-presets/dtos/create-schedule-presets.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 { ShiftEntity } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-payload.dto"; import { UpdateShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-update.dto"; import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils"; @@ -25,7 +26,7 @@ export type TotalExpenses = { mileage: number; }; -export type Normalized = { date: Date; start_time: Date; end_time: Date; id: number}; +export type Normalized = { date: Date; start_time: Date; end_time: Date; bank_code_id: number}; export type ShiftWithOvertimeDto = { shift: GetShiftDto; @@ -51,7 +52,7 @@ export type DeleteExpenseResult = { ok: true; id: number; } | { ok: false; id: n -export type NormedOk = { index: number; dto: ShiftDto; normed: Normalized }; +export type NormedOk = { index: number; dto: ShiftEntity; normed: Normalized, timesheet_id: number }; export type NormedErr = { index: number; error: any };