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": {
"operationId": "ShiftsController_upsert_by_date",
"parameters": [
@ -448,14 +448,6 @@
"schema": {
"type": "string"
}
},
{
"name": "date",
"required": true,
"in": "path",
"schema": {
"type": "string"
}
}
],
"requestBody": {

View File

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

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 { 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 {

View File

@ -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 {

View File

@ -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 {

View File

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

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,
){}
@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')

View File

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

View File

@ -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}`;
}

View File

@ -41,7 +41,7 @@ export class ShiftsCommandService extends BaseApprovalService<Shifts> {
//_____________________________________________________________________________________________
// 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<Shifts> {
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) => {

View File

@ -24,6 +24,7 @@ export function resolveBankCodeByType(type: string): Promise<number> {
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<number> {
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 };
}