fix(shifts): changed UTC comparison for ISOString

This commit is contained in:
Matthieu Haineault 2025-10-09 15:45:57 -04:00
parent 1954d206a8
commit af8ea95cc4
3 changed files with 54 additions and 50 deletions

View File

@ -1,23 +1,27 @@
export function timeFromHHMMUTC(hhmm: string): Date {
const [hour, min] = hhmm.split(':').map(Number);
return new Date(Date.UTC(1970,0,1,hour, min,0));
export function timeFromHHMM(hhmm: string): Date {
const [hour, min] = hhmm.split(':').map(Number);
return new Date(1970, 0, 1, hour, min, 0, 0);
}
export function weekStartSundayUTC(d: Date): Date {
const day = d.getUTCDay();
const start = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
start.setUTCDate(start.getUTCDate()- day);
return start;
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);
start.setHours(0, 0, 0, 0);
return start;
}
export function toDateOnlyUTC(input: string | Date): Date {
const date = new Date(input);
return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
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.getUTCHours()).padStart(2,'0');
const mm = String(time.getUTCMinutes()).padStart(2,'0');
return `${hh}:${mm}`;
const hh = String(time.getHours()).padStart(2,'0');
const mm = String(time.getMinutes()).padStart(2,'0');
return `${hh}:${mm}`;
}

View File

@ -1,5 +1,4 @@
import { BadRequestException, ConflictException, Injectable, Logger, NotFoundException, UnprocessableEntityException } from "@nestjs/common";
import { formatHHmm, toDateOnlyUTC, weekStartSundayUTC } from "../helpers/shifts-date-time-helpers";
import { normalizeShiftPayload, overlaps } from "../utils/shifts.utils";
import { DayShiftResponse, UpsertAction } from "../types-and-interfaces/shifts-upsert.types";
import { EmployeeIdEmailResolver } from "src/modules/shared/utils/resolve-email-id.utils";
@ -9,6 +8,7 @@ import { UpsertShiftDto } from "../dtos/upsert-shift.dto";
import { BaseApprovalService } from "src/common/shared/base-approval.service";
import { PrismaService } from "src/prisma/prisma.service";
import { OvertimeService } from "src/modules/business-logics/services/overtime.service";
import { formatHHmm, toDateOnly, weekStartSunday } from "../helpers/shifts-date-time-helpers";
@Injectable()
export class ShiftsCommandService extends BaseApprovalService<Shifts> {
@ -49,11 +49,11 @@ export class ShiftsCommandService extends BaseApprovalService<Shifts> {
throw new BadRequestException('At least one of old or new shift must be provided');
}
const date_only = toDateOnlyUTC(date_string);
const date_only = toDateOnly(date_string);
const employee_id = await this.emailResolver.findIdByEmail(email);
return this.prisma.$transaction(async (tx) => {
const start_of_week = weekStartSundayUTC(date_only);
const start_of_week = weekStartSunday(date_only);
const timesheet = await tx.timesheets.upsert({
where: { employee_id_start_date: { employee_id, start_date: start_of_week } },
@ -65,16 +65,16 @@ export class ShiftsCommandService extends BaseApprovalService<Shifts> {
//validation/sanitation
//resolve bank_code_id using type
const old_norm_shift = old_shift ? await normalizeShiftPayload(old_shift) : undefined;
// if (old_norm_shift && old_norm_shift.end_time.getTime() <= old_norm_shift.start_time.getTime()) {
// throw new UnprocessableEntityException(' old_shift.end_time must be > old_shift.start_time');
// }
if (old_norm_shift && old_norm_shift.end_time.getTime() <= old_norm_shift.start_time.getTime()) {
throw new UnprocessableEntityException(' old_shift.end_time must be > old_shift.start_time');
}
const old_bank_code_id: number | undefined = old_norm_shift ? (await this.bankTypeResolver.findByType(old_norm_shift.type, tx))?.id : undefined;
const new_norm_shift = new_shift ? await normalizeShiftPayload(new_shift) : undefined;
// if (new_norm_shift && new_norm_shift.end_time.getTime() <= new_norm_shift.start_time.getTime()) {
// throw new UnprocessableEntityException(' new_shift.end_time must be > new_shift.start_time');
// }
if (new_norm_shift && new_norm_shift.end_time.getTime() <= new_norm_shift.start_time.getTime()) {
throw new UnprocessableEntityException(' new_shift.end_time must be > new_shift.start_time');
}
const new_bank_code_id: number | undefined = new_norm_shift ? (await this.bankTypeResolver.findByType(new_norm_shift.type, tx))?.id : undefined;
@ -105,28 +105,28 @@ export class ShiftsCommandService extends BaseApprovalService<Shifts> {
});
};
// //checks for overlaping shifts
// const assertNoOverlap = (exclude_shift_id?: number)=> {
// if (!new_norm_shift) return;
// const overlap_with = day_shifts.filter((shift)=> {
// if(exclude_shift_id && shift.id === exclude_shift_id) return false;
// return overlaps(
// new_norm_shift.start_time.getTime(),
// new_norm_shift.end_time.getTime(),
// shift.start_time.getTime(),
// shift.end_time.getTime(),
// );
// });
//checks for overlaping shifts
const assertNoOverlap = (exclude_shift_id?: number)=> {
if (!new_norm_shift) return;
const overlap_with = day_shifts.filter((shift)=> {
if(exclude_shift_id && shift.id === exclude_shift_id) return false;
return overlaps(
new_norm_shift.start_time.getTime(),
new_norm_shift.end_time.getTime(),
shift.start_time.getTime(),
shift.end_time.getTime(),
);
});
// if(overlap_with.length > 0) {
// const conflicts = overlap_with.map((shift)=> ({
// start_time: formatHHmm(shift.start_time),
// end_time: formatHHmm(shift.end_time),
// type: shift.bank_code?.type ?? 'UNKNOWN',
// }));
// throw new ConflictException({ error_code: 'SHIFT_OVERLAP', message: 'New shift overlaps with existing shift(s)', conflicts});
// }
// };
if(overlap_with.length > 0) {
const conflicts = overlap_with.map((shift)=> ({
start_time: formatHHmm(shift.start_time),
end_time: formatHHmm(shift.end_time),
type: shift.bank_code?.type ?? 'UNKNOWN',
}));
throw new ConflictException({ error_code: 'SHIFT_OVERLAP', message: 'New shift overlaps with existing shift(s)', conflicts});
}
};
let action: UpsertAction;
//_____________________________________________________________________________________________
// DELETE
@ -148,7 +148,7 @@ export class ShiftsCommandService extends BaseApprovalService<Shifts> {
//_____________________________________________________________________________________________
else if (!old_shift && new_shift) {
if (new_bank_code_id === undefined) throw new NotFoundException(`bank code not found for new_shift.type: ${new_norm_shift?.type ?? ''}`);
// assertNoOverlap();
assertNoOverlap();
await tx.shifts.create({
data: {
timesheet_id: timesheet.id,
@ -170,7 +170,7 @@ export class ShiftsCommandService extends BaseApprovalService<Shifts> {
if (new_bank_code_id === undefined) throw new NotFoundException(`bank code not found for new_shift.type: ${new_norm_shift?.type ?? ''}`);
const existing = await findExactOldShift();
if(!existing) throw new NotFoundException({ error_code: 'SHIFT_STALE', message: 'The shift was modified or deleted by someone else'});
// assertNoOverlap(existing.id);
assertNoOverlap(existing.id);
await tx.shifts.update({
where: {

View File

@ -1,6 +1,6 @@
import { NotFoundException } from "@nestjs/common";
import { ShiftPayloadDto } from "../dtos/upsert-shift.dto";
import { timeFromHHMMUTC } from "../helpers/shifts-date-time-helpers";
import { timeFromHHMM } from "../helpers/shifts-date-time-helpers";
export function overlaps(
a_start_ms: number,
@ -24,8 +24,8 @@ export function resolveBankCodeByType(type: string): Promise<number> {
export function normalizeShiftPayload(payload: ShiftPayloadDto) {
//normalize shift's infos
const start_time = payload.start_time;
const end_time = payload.end_time;
const start_time = timeFromHHMM(payload.start_time);
const end_time = timeFromHHMM(payload.end_time );
const type = (payload.type || '').trim().toUpperCase();
const is_remote = payload.is_remote === true;
const is_approved = payload.is_approved === false;
@ -34,5 +34,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, comment, is_approved };
return { start_time, end_time, type, is_remote, is_approved, comment };
}