diff --git a/docs/swagger/swagger-spec.json b/docs/swagger/swagger-spec.json index 97745e0..01dbbcf 100644 --- a/docs/swagger/swagger-spec.json +++ b/docs/swagger/swagger-spec.json @@ -437,7 +437,7 @@ ] } }, - "/shifts/upsert/{email}/{date}": { + "/shifts/upsert/{email}": { "put": { "operationId": "ShiftsController_upsert_by_date", "parameters": [ @@ -448,14 +448,6 @@ "schema": { "type": "string" } - }, - { - "name": "date", - "required": true, - "in": "path", - "schema": { - "type": "string" - } } ], "requestBody": { diff --git a/src/modules/leave-requests/services/holiday-leave-requests.service.ts b/src/modules/leave-requests/services/holiday-leave-requests.service.ts index 025833c..a3c72d1 100644 --- a/src/modules/leave-requests/services/holiday-leave-requests.service.ts +++ b/src/modules/leave-requests/services/holiday-leave-requests.service.ts @@ -6,7 +6,8 @@ import { HolidayService } from 'src/modules/business-logics/services/holid import { PrismaService } from 'src/prisma/prisma.service'; import { mapRowToView } from '../mappers/leave-requests.mapper'; import { leaveRequestsSelect } from '../utils/leave-requests.select'; -import { LeaveRequestsUtils, normalizeDates, toDateOnly } from '../utils/leave-request.util'; +import { LeaveRequestsUtils} from '../utils/leave-request.util'; +import { normalizeDates, toDateOnly } from 'src/modules/shared/helpers/date-time.helpers'; @Injectable() diff --git a/src/modules/leave-requests/services/leave-request.service.ts b/src/modules/leave-requests/services/leave-request.service.ts index d7e3239..4b42f55 100644 --- a/src/modules/leave-requests/services/leave-request.service.ts +++ b/src/modules/leave-requests/services/leave-request.service.ts @@ -12,7 +12,8 @@ import { HolidayService } from "src/modules/business-logics/services/holiday.ser import { SickLeaveService } from "src/modules/business-logics/services/sick-leave.service"; import { VacationService } from "src/modules/business-logics/services/vacation.service"; import { PrismaService } from "src/prisma/prisma.service"; -import { LeaveRequestsUtils, normalizeDates, toDateOnly, toISODateKey } from "../utils/leave-request.util"; +import { LeaveRequestsUtils } from "../utils/leave-request.util"; +import { normalizeDates, toDateOnly, toISODateKey } from "src/modules/shared/helpers/date-time.helpers"; @Injectable() export class LeaveRequestsService { diff --git a/src/modules/leave-requests/services/sick-leave-requests.service.ts b/src/modules/leave-requests/services/sick-leave-requests.service.ts index cde2013..ba3d77f 100644 --- a/src/modules/leave-requests/services/sick-leave-requests.service.ts +++ b/src/modules/leave-requests/services/sick-leave-requests.service.ts @@ -7,7 +7,8 @@ import { mapRowToView } from "../mappers/leave-requests.mapper"; import { PrismaService } from "src/prisma/prisma.service"; import { SickLeaveService } from "src/modules/business-logics/services/sick-leave.service"; import { roundToQuarterHour } from "src/common/utils/date-utils"; -import { LeaveRequestsUtils, normalizeDates, toDateOnly } from "../utils/leave-request.util"; +import { LeaveRequestsUtils } from "../utils/leave-request.util"; +import { normalizeDates, toDateOnly } from "src/modules/shared/helpers/date-time.helpers"; @Injectable() export class SickLeaveRequestsService { diff --git a/src/modules/leave-requests/services/vacation-leave-requests.service.ts b/src/modules/leave-requests/services/vacation-leave-requests.service.ts index 31d1081..90126a8 100644 --- a/src/modules/leave-requests/services/vacation-leave-requests.service.ts +++ b/src/modules/leave-requests/services/vacation-leave-requests.service.ts @@ -8,7 +8,8 @@ import { PrismaService } from "src/prisma/prisma.service"; import { mapRowToView } from "../mappers/leave-requests.mapper"; import { leaveRequestsSelect } from "../utils/leave-requests.select"; import { roundToQuarterHour } from "src/common/utils/date-utils"; -import { LeaveRequestsUtils, normalizeDates, toDateOnly } from "../utils/leave-request.util"; +import { LeaveRequestsUtils } from "../utils/leave-request.util"; +import { normalizeDates, toDateOnly } from "src/modules/shared/helpers/date-time.helpers"; @Injectable() export class VacationLeaveRequestsService { diff --git a/src/modules/leave-requests/utils/leave-request.util.ts b/src/modules/leave-requests/utils/leave-request.util.ts index f5493db..a826728 100644 --- a/src/modules/leave-requests/utils/leave-request.util.ts +++ b/src/modules/leave-requests/utils/leave-request.util.ts @@ -1,5 +1,6 @@ import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; import { LeaveTypes } from "@prisma/client"; +import { hhmmFromLocal, toDateOnly, toStringFromDate } from "src/modules/shared/helpers/date-time.helpers"; import { ShiftsCommandService } from "src/modules/shifts/services/shifts-command.service"; import { PrismaService } from "src/prisma/prisma.service"; @@ -33,7 +34,7 @@ export class LeaveRequestsUtils { async syncShift( email: string, employee_id: number, - iso_date: string, + date: string, hours: number, type: LeaveTypes, comment?: string, @@ -44,6 +45,10 @@ export class LeaveRequestsUtils { if (duration_minutes > 8 * 60) { throw new BadRequestException("Amount of hours cannot exceed 8 hours per day."); } + const date_only = toDateOnly(date); + const yyyy_mm_dd = toStringFromDate(date_only); + + const start_minutes = 8 * 60; const end_minutes = start_minutes + duration_minutes; @@ -52,25 +57,27 @@ export class LeaveRequestsUtils { const existing = await this.prisma.shifts.findFirst({ where: { - date: new Date(iso_date), + date: date_only, bank_code: { type }, timesheet: { employee_id: employee_id }, }, include: { bank_code: true }, }); - await this.shiftsCommand.upsertShiftsByDate(email, iso_date, { + await this.shiftsCommand.upsertShiftsByDate(email, { old_shift: existing ? { - start_time: existing.start_time.toISOString().slice(11, 16), - end_time: existing.end_time.toISOString().slice(11, 16), - type: existing.bank_code?.type ?? type, - is_remote: existing.is_remote, - is_approved:existing.is_approved, - comment: existing.comment ?? undefined, - } + date: yyyy_mm_dd, + start_time: existing.start_time.toISOString().slice(11, 16), + end_time: existing.end_time.toISOString().slice(11, 16), + type: existing.bank_code?.type ?? type, + is_remote: existing.is_remote, + is_approved:existing.is_approved, + comment: existing.comment ?? undefined, + } : undefined, new_shift: { + date: yyyy_mm_dd, start_time: toHHmm(start_minutes), end_time: toHHmm(end_minutes), is_remote: existing?.is_remote ?? false, @@ -87,9 +94,11 @@ export class LeaveRequestsUtils { iso_date: string, type: LeaveTypes, ) { + const date_only = toDateOnly(iso_date); + const yyyy_mm_dd = toStringFromDate(date_only); const existing = await this.prisma.shifts.findFirst({ where: { - date: new Date(iso_date), + date: date_only, bank_code: { type }, timesheet: { employee_id: employee_id }, }, @@ -97,10 +106,11 @@ export class LeaveRequestsUtils { }); if (!existing) return; - await this.shiftsCommand.upsertShiftsByDate(email, iso_date, { + await this.shiftsCommand.upsertShiftsByDate(email, { old_shift: { - start_time: existing.start_time.toISOString().slice(11, 16), - end_time: existing.end_time.toISOString().slice(11, 16), + date: yyyy_mm_dd, + start_time: hhmmFromLocal(existing.start_time), + end_time: hhmmFromLocal(existing.end_time), type: existing.bank_code?.type ?? type, is_remote: existing.is_remote, is_approved:existing.is_approved, @@ -110,18 +120,3 @@ export class LeaveRequestsUtils { } } - - -export const toDateOnly = (iso: string): Date => { - const date = new Date(iso); - if (Number.isNaN(date.getTime())) { - throw new BadRequestException(`Invalid date: ${iso}`); - } - date.setHours(0, 0, 0, 0); - return date; -}; - -export const toISODateKey = (date: Date): string => date.toISOString().slice(0, 10); - -export const normalizeDates = (dates: string[]): string[] => - Array.from(new Set(dates.map((iso) => toISODateKey(toDateOnly(iso))))); \ No newline at end of file diff --git a/src/modules/shared/helpers/date-time.helpers.ts b/src/modules/shared/helpers/date-time.helpers.ts new file mode 100644 index 0000000..6716321 --- /dev/null +++ b/src/modules/shared/helpers/date-time.helpers.ts @@ -0,0 +1,34 @@ +import { BadRequestException } from "@nestjs/common"; + +export const hhmmFromLocal = (d: Date) => + `${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`; + +export const toDateOnly = (s: string): Date => { + if (/^\d{4}-\d{2}-\d{2}$/.test(s)) { + const y = Number(s.slice(0,4)); + const m = Number(s.slice(5,7)) - 1; + const d = Number(s.slice(8,10)); + return new Date(y, m, d, 0, 0, 0, 0); + } + const dt = new Date(s); + if (Number.isNaN(dt.getTime())) throw new BadRequestException(`Invalid date: ${s}`); + return new Date(dt.getFullYear(), dt.getMonth(), dt.getDate(), 0,0,0,0); +}; + +export const toStringFromDate = (d: Date) => + `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`; + + +export const toISOtoDateOnly = (iso: string): Date => { + const date = new Date(iso); + if (Number.isNaN(date.getTime())) { + throw new BadRequestException(`Invalid date: ${iso}`); + } + date.setHours(0, 0, 0, 0); + return date; +}; + +export const toISODateKey = (date: Date): string => date.toISOString().slice(0, 10); + +export const normalizeDates = (dates: string[]): string[] => + Array.from(new Set(dates.map((iso) => toISODateKey(toISOtoDateOnly(iso))))); \ No newline at end of file diff --git a/src/modules/shifts/controllers/shifts.controller.ts b/src/modules/shifts/controllers/shifts.controller.ts index f12d9ad..45545dd 100644 --- a/src/modules/shifts/controllers/shifts.controller.ts +++ b/src/modules/shifts/controllers/shifts.controller.ts @@ -18,13 +18,12 @@ export class ShiftsController { private readonly shiftsCommandService: ShiftsCommandService, ){} - @Put('upsert/:email/:date') + @Put('upsert/:email') async upsert_by_date( @Param('email') email_param: string, - @Param('date') date_param: string, @Body() payload: UpsertShiftDto, ) { - return this.shiftsCommandService.upsertShiftsByDate(email_param, date_param, payload); + return this.shiftsCommandService.upsertShiftsByDate(email_param, payload); } @Patch('approval/:id') diff --git a/src/modules/shifts/dtos/upsert-shift.dto.ts b/src/modules/shifts/dtos/upsert-shift.dto.ts index fc2e130..7809571 100644 --- a/src/modules/shifts/dtos/upsert-shift.dto.ts +++ b/src/modules/shifts/dtos/upsert-shift.dto.ts @@ -5,6 +5,9 @@ export const COMMENT_MAX_LENGTH = 280; export class ShiftPayloadDto { + @Matches(/^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/) + date!: string; + @Matches(/^([01]\d|2[0-3]):([0-5]\d)$/) start_time!: string; diff --git a/src/modules/shifts/helpers/shifts-date-time-helpers.ts b/src/modules/shifts/helpers/shifts-date-time-helpers.ts index c899dc8..d5ba369 100644 --- a/src/modules/shifts/helpers/shifts-date-time-helpers.ts +++ b/src/modules/shifts/helpers/shifts-date-time-helpers.ts @@ -1,27 +1,25 @@ export function timeFromHHMM(hhmm: string): Date { - const [hour, min] = hhmm.split(':').map(Number); - return new Date(1970, 0, 1, hour, min, 0, 0); + const [h, m] = hhmm.split(':').map(Number); + return new Date(1970, 0, 1, h, m, 0, 0); } -export function weekStartSunday(d: Date): Date { - const start = new Date(d.getFullYear(), d.getMonth(), d.getDate()); - const day = start.getDay(); // 0 = dimanche - start.setDate(start.getDate() - day); +export function toDateOnly(ymd: string): Date { + const y = Number(ymd.slice(0, 4)); + const m = Number(ymd.slice(5, 7)) - 1; + const d = Number(ymd.slice(8, 10)); + return new Date(y, m, d, 0, 0, 0, 0); +} + +export function weekStartSunday(dateLocal: Date): Date { + const start = new Date(dateLocal.getFullYear(), dateLocal.getMonth(), dateLocal.getDate()); + const dow = start.getDay(); // 0 = dimanche + start.setDate(start.getDate() - dow); start.setHours(0, 0, 0, 0); return start; } -export function toDateOnly(input: string | Date): Date { - const base = (typeof input === 'string') ? new Date(input) : new Date(input); - const y = (typeof input === 'string') ? Number(input.slice(0,4)) : base.getFullYear(); - const m = (typeof input === 'string') ? Number(input.slice(5,7)) - 1 : base.getMonth(); - const d = (typeof input === 'string') ? Number(input.slice(8,10)) : base.getDate(); - return new Date(y, m, d, 0, 0, 0, 0); -} - -export function formatHHmm(time: Date): string { - const hh = String(time.getHours()).padStart(2,'0'); - const mm = String(time.getMinutes()).padStart(2,'0'); +export function formatHHmm(t: Date): string { + const hh = String(t.getHours()).padStart(2, '0'); + const mm = String(t.getMinutes()).padStart(2, '0'); return `${hh}:${mm}`; } - diff --git a/src/modules/shifts/services/shifts-command.service.ts b/src/modules/shifts/services/shifts-command.service.ts index fc65119..ddbff29 100644 --- a/src/modules/shifts/services/shifts-command.service.ts +++ b/src/modules/shifts/services/shifts-command.service.ts @@ -41,7 +41,7 @@ export class ShiftsCommandService extends BaseApprovalService { //_____________________________________________________________________________________________ // MASTER CRUD METHOD //_____________________________________________________________________________________________ - async upsertShiftsByDate(email:string, date_string: string, dto: UpsertShiftDto): + async upsertShiftsByDate(email:string, dto: UpsertShiftDto): Promise<{ action: UpsertAction; day: DayShiftResponse[] }> { const { old_shift, new_shift } = dto; @@ -49,7 +49,14 @@ export class ShiftsCommandService extends BaseApprovalService { throw new BadRequestException('At least one of old or new shift must be provided'); } - const date_only = toDateOnly(date_string); + const date = new_shift?.date ?? old_shift?.date; + if (!date) throw new BadRequestException("A date (YYYY-MM-DD) must be provided in old_shift or new_shift"); + if (old_shift?.date + && new_shift?.date + && old_shift.date + !== new_shift.date) throw new BadRequestException("old_shift.date and new_shift.date must be identical"); + + const date_only = toDateOnly(date); const employee_id = await this.emailResolver.findIdByEmail(email); return this.prisma.$transaction(async (tx) => { diff --git a/src/modules/shifts/utils/shifts.utils.ts b/src/modules/shifts/utils/shifts.utils.ts index 2ec24c2..43c569f 100644 --- a/src/modules/shifts/utils/shifts.utils.ts +++ b/src/modules/shifts/utils/shifts.utils.ts @@ -24,6 +24,7 @@ export function resolveBankCodeByType(type: string): Promise { export function normalizeShiftPayload(payload: ShiftPayloadDto) { //normalize shift's infos + const date = payload.date; const start_time = timeFromHHMM(payload.start_time); const end_time = timeFromHHMM(payload.end_time ); const type = (payload.type || '').trim().toUpperCase(); @@ -34,5 +35,5 @@ export function resolveBankCodeByType(type: string): Promise { const trimmed = typeof raw_comment === 'string' ? raw_comment.trim() : null; const comment = trimmed && trimmed.length > 0 ? trimmed: null; - return { start_time, end_time, type, is_remote, is_approved, comment }; + return { date, start_time, end_time, type, is_remote, is_approved, comment }; } \ No newline at end of file