refactor(helpers): moved helpers to a shared file

This commit is contained in:
Matthieu Haineault 2025-10-09 16:41:34 -04:00
parent 78a335a47c
commit 71d86f7fed
12 changed files with 99 additions and 66 deletions

View File

@ -437,7 +437,7 @@
] ]
} }
}, },
"/shifts/upsert/{email}/{date}": { "/shifts/upsert/{email}": {
"put": { "put": {
"operationId": "ShiftsController_upsert_by_date", "operationId": "ShiftsController_upsert_by_date",
"parameters": [ "parameters": [
@ -448,14 +448,6 @@
"schema": { "schema": {
"type": "string" "type": "string"
} }
},
{
"name": "date",
"required": true,
"in": "path",
"schema": {
"type": "string"
}
} }
], ],
"requestBody": { "requestBody": {

View File

@ -6,7 +6,8 @@ import { HolidayService } from 'src/modules/business-logics/services/holid
import { PrismaService } from 'src/prisma/prisma.service'; import { PrismaService } from 'src/prisma/prisma.service';
import { mapRowToView } from '../mappers/leave-requests.mapper'; import { mapRowToView } from '../mappers/leave-requests.mapper';
import { leaveRequestsSelect } from '../utils/leave-requests.select'; 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() @Injectable()

View File

@ -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 { SickLeaveService } from "src/modules/business-logics/services/sick-leave.service";
import { VacationService } from "src/modules/business-logics/services/vacation.service"; import { VacationService } from "src/modules/business-logics/services/vacation.service";
import { PrismaService } from "src/prisma/prisma.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() @Injectable()
export class LeaveRequestsService { export class LeaveRequestsService {

View File

@ -7,7 +7,8 @@ import { mapRowToView } from "../mappers/leave-requests.mapper";
import { PrismaService } from "src/prisma/prisma.service"; import { PrismaService } from "src/prisma/prisma.service";
import { SickLeaveService } from "src/modules/business-logics/services/sick-leave.service"; import { SickLeaveService } from "src/modules/business-logics/services/sick-leave.service";
import { roundToQuarterHour } from "src/common/utils/date-utils"; 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() @Injectable()
export class SickLeaveRequestsService { export class SickLeaveRequestsService {

View File

@ -8,7 +8,8 @@ import { PrismaService } from "src/prisma/prisma.service";
import { mapRowToView } from "../mappers/leave-requests.mapper"; import { mapRowToView } from "../mappers/leave-requests.mapper";
import { leaveRequestsSelect } from "../utils/leave-requests.select"; import { leaveRequestsSelect } from "../utils/leave-requests.select";
import { roundToQuarterHour } from "src/common/utils/date-utils"; 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() @Injectable()
export class VacationLeaveRequestsService { export class VacationLeaveRequestsService {

View File

@ -1,5 +1,6 @@
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
import { LeaveTypes } from "@prisma/client"; 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 { ShiftsCommandService } from "src/modules/shifts/services/shifts-command.service";
import { PrismaService } from "src/prisma/prisma.service"; import { PrismaService } from "src/prisma/prisma.service";
@ -33,7 +34,7 @@ export class LeaveRequestsUtils {
async syncShift( async syncShift(
email: string, email: string,
employee_id: number, employee_id: number,
iso_date: string, date: string,
hours: number, hours: number,
type: LeaveTypes, type: LeaveTypes,
comment?: string, comment?: string,
@ -44,6 +45,10 @@ export class LeaveRequestsUtils {
if (duration_minutes > 8 * 60) { if (duration_minutes > 8 * 60) {
throw new BadRequestException("Amount of hours cannot exceed 8 hours per day."); 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 start_minutes = 8 * 60;
const end_minutes = start_minutes + duration_minutes; const end_minutes = start_minutes + duration_minutes;
@ -52,25 +57,27 @@ export class LeaveRequestsUtils {
const existing = await this.prisma.shifts.findFirst({ const existing = await this.prisma.shifts.findFirst({
where: { where: {
date: new Date(iso_date), date: date_only,
bank_code: { type }, bank_code: { type },
timesheet: { employee_id: employee_id }, timesheet: { employee_id: employee_id },
}, },
include: { bank_code: true }, include: { bank_code: true },
}); });
await this.shiftsCommand.upsertShiftsByDate(email, iso_date, { await this.shiftsCommand.upsertShiftsByDate(email, {
old_shift: existing old_shift: existing
? { ? {
start_time: existing.start_time.toISOString().slice(11, 16), date: yyyy_mm_dd,
end_time: existing.end_time.toISOString().slice(11, 16), start_time: existing.start_time.toISOString().slice(11, 16),
type: existing.bank_code?.type ?? type, end_time: existing.end_time.toISOString().slice(11, 16),
is_remote: existing.is_remote, type: existing.bank_code?.type ?? type,
is_approved:existing.is_approved, is_remote: existing.is_remote,
comment: existing.comment ?? undefined, is_approved:existing.is_approved,
} comment: existing.comment ?? undefined,
}
: undefined, : undefined,
new_shift: { new_shift: {
date: yyyy_mm_dd,
start_time: toHHmm(start_minutes), start_time: toHHmm(start_minutes),
end_time: toHHmm(end_minutes), end_time: toHHmm(end_minutes),
is_remote: existing?.is_remote ?? false, is_remote: existing?.is_remote ?? false,
@ -87,9 +94,11 @@ export class LeaveRequestsUtils {
iso_date: string, iso_date: string,
type: LeaveTypes, type: LeaveTypes,
) { ) {
const date_only = toDateOnly(iso_date);
const yyyy_mm_dd = toStringFromDate(date_only);
const existing = await this.prisma.shifts.findFirst({ const existing = await this.prisma.shifts.findFirst({
where: { where: {
date: new Date(iso_date), date: date_only,
bank_code: { type }, bank_code: { type },
timesheet: { employee_id: employee_id }, timesheet: { employee_id: employee_id },
}, },
@ -97,10 +106,11 @@ export class LeaveRequestsUtils {
}); });
if (!existing) return; if (!existing) return;
await this.shiftsCommand.upsertShiftsByDate(email, iso_date, { await this.shiftsCommand.upsertShiftsByDate(email, {
old_shift: { old_shift: {
start_time: existing.start_time.toISOString().slice(11, 16), date: yyyy_mm_dd,
end_time: existing.end_time.toISOString().slice(11, 16), start_time: hhmmFromLocal(existing.start_time),
end_time: hhmmFromLocal(existing.end_time),
type: existing.bank_code?.type ?? type, type: existing.bank_code?.type ?? type,
is_remote: existing.is_remote, is_remote: existing.is_remote,
is_approved:existing.is_approved, 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)))));

View File

@ -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)))));

View File

@ -18,13 +18,12 @@ export class ShiftsController {
private readonly shiftsCommandService: ShiftsCommandService, private readonly shiftsCommandService: ShiftsCommandService,
){} ){}
@Put('upsert/:email/:date') @Put('upsert/:email')
async upsert_by_date( async upsert_by_date(
@Param('email') email_param: string, @Param('email') email_param: string,
@Param('date') date_param: string,
@Body() payload: UpsertShiftDto, @Body() payload: UpsertShiftDto,
) { ) {
return this.shiftsCommandService.upsertShiftsByDate(email_param, date_param, payload); return this.shiftsCommandService.upsertShiftsByDate(email_param, payload);
} }
@Patch('approval/:id') @Patch('approval/:id')

View File

@ -5,6 +5,9 @@ export const COMMENT_MAX_LENGTH = 280;
export class ShiftPayloadDto { 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)$/) @Matches(/^([01]\d|2[0-3]):([0-5]\d)$/)
start_time!: string; start_time!: string;

View File

@ -1,27 +1,25 @@
export function timeFromHHMM(hhmm: string): Date { export function timeFromHHMM(hhmm: string): Date {
const [hour, min] = hhmm.split(':').map(Number); const [h, m] = hhmm.split(':').map(Number);
return new Date(1970, 0, 1, hour, min, 0, 0); return new Date(1970, 0, 1, h, m, 0, 0);
} }
export function weekStartSunday(d: Date): Date { export function toDateOnly(ymd: string): Date {
const start = new Date(d.getFullYear(), d.getMonth(), d.getDate()); const y = Number(ymd.slice(0, 4));
const day = start.getDay(); // 0 = dimanche const m = Number(ymd.slice(5, 7)) - 1;
start.setDate(start.getDate() - day); 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); start.setHours(0, 0, 0, 0);
return start; return start;
} }
export function toDateOnly(input: string | Date): Date { export function formatHHmm(t: Date): string {
const base = (typeof input === 'string') ? new Date(input) : new Date(input); const hh = String(t.getHours()).padStart(2, '0');
const y = (typeof input === 'string') ? Number(input.slice(0,4)) : base.getFullYear(); const mm = String(t.getMinutes()).padStart(2, '0');
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');
return `${hh}:${mm}`; return `${hh}:${mm}`;
} }

View File

@ -41,7 +41,7 @@ export class ShiftsCommandService extends BaseApprovalService<Shifts> {
//_____________________________________________________________________________________________ //_____________________________________________________________________________________________
// MASTER CRUD METHOD // MASTER CRUD METHOD
//_____________________________________________________________________________________________ //_____________________________________________________________________________________________
async upsertShiftsByDate(email:string, date_string: string, dto: UpsertShiftDto): async upsertShiftsByDate(email:string, dto: UpsertShiftDto):
Promise<{ action: UpsertAction; day: DayShiftResponse[] }> { Promise<{ action: UpsertAction; day: DayShiftResponse[] }> {
const { old_shift, new_shift } = dto; const { old_shift, new_shift } = dto;
@ -49,7 +49,14 @@ export class ShiftsCommandService extends BaseApprovalService<Shifts> {
throw new BadRequestException('At least one of old or new shift must be provided'); 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); const employee_id = await this.emailResolver.findIdByEmail(email);
return this.prisma.$transaction(async (tx) => { return this.prisma.$transaction(async (tx) => {

View File

@ -24,6 +24,7 @@ export function resolveBankCodeByType(type: string): Promise<number> {
export function normalizeShiftPayload(payload: ShiftPayloadDto) { export function normalizeShiftPayload(payload: ShiftPayloadDto) {
//normalize shift's infos //normalize shift's infos
const date = payload.date;
const start_time = timeFromHHMM(payload.start_time); const start_time = timeFromHHMM(payload.start_time);
const end_time = timeFromHHMM(payload.end_time ); const end_time = timeFromHHMM(payload.end_time );
const type = (payload.type || '').trim().toUpperCase(); const type = (payload.type || '').trim().toUpperCase();
@ -34,5 +35,5 @@ export function resolveBankCodeByType(type: string): Promise<number> {
const trimmed = typeof raw_comment === 'string' ? raw_comment.trim() : null; const trimmed = typeof raw_comment === 'string' ? raw_comment.trim() : null;
const comment = trimmed && trimmed.length > 0 ? trimmed: 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 };
} }