From 2de8db62129de90a89a68454ef672935f56f9669 Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Fri, 10 Oct 2025 11:34:39 -0400 Subject: [PATCH] refactor(timesheets): dried findAll methods to centralize shareable methods --- .../services/expenses-query.service.ts | 4 +- .../shared/selects/employees.select.ts | 0 src/modules/shared/selects/expenses.select.ts | 11 ++ .../shared/selects/pay-periods.select.ts | 4 + src/modules/shared/selects/shifts.select.ts | 12 +++ .../shared/selects/timesheets.select.ts | 0 .../services/timesheets-command.service.ts | 2 +- .../services/timesheets-query.service.ts | 101 ++++-------------- .../{utils => }/timesheet.helpers.ts | 16 ++- .../{mappers => }/timesheet.mappers.ts | 46 +++++++- src/modules/timesheets/timesheet.selectors.ts | 35 ++++++ .../timesheets/{types => }/timesheet.types.ts | 0 .../timesheets/{utils => }/timesheet.utils.ts | 6 +- 13 files changed, 145 insertions(+), 92 deletions(-) create mode 100644 src/modules/shared/selects/employees.select.ts create mode 100644 src/modules/shared/selects/expenses.select.ts create mode 100644 src/modules/shared/selects/pay-periods.select.ts create mode 100644 src/modules/shared/selects/shifts.select.ts create mode 100644 src/modules/shared/selects/timesheets.select.ts rename src/modules/timesheets/{utils => }/timesheet.helpers.ts (76%) rename src/modules/timesheets/{mappers => }/timesheet.mappers.ts (54%) create mode 100644 src/modules/timesheets/timesheet.selectors.ts rename src/modules/timesheets/{types => }/timesheet.types.ts (100%) rename src/modules/timesheets/{utils => }/timesheet.utils.ts (97%) diff --git a/src/modules/expenses/services/expenses-query.service.ts b/src/modules/expenses/services/expenses-query.service.ts index 35cdde5..18be585 100644 --- a/src/modules/expenses/services/expenses-query.service.ts +++ b/src/modules/expenses/services/expenses-query.service.ts @@ -1,8 +1,8 @@ import { Injectable, NotFoundException } from "@nestjs/common"; import { PrismaService } from "src/prisma/prisma.service"; import { DayExpensesDto as ExpenseListResponseDto, ExpenseDto } from "src/modules/timesheets/dtos/timesheet-period.dto"; -import { round2, toUTCDateOnly } from "src/modules/timesheets/utils/timesheet.helpers"; -import { EXPENSE_TYPES } from "src/modules/timesheets/types/timesheet.types"; +import { round2, toUTCDateOnly } from "src/modules/timesheets/timesheet.helpers"; +import { EXPENSE_TYPES } from "src/modules/timesheets/timesheet.types"; import { EmailToIdResolver } from "src/modules/shared/utils/resolve-email-id.utils"; @Injectable() diff --git a/src/modules/shared/selects/employees.select.ts b/src/modules/shared/selects/employees.select.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/shared/selects/expenses.select.ts b/src/modules/shared/selects/expenses.select.ts new file mode 100644 index 0000000..540d98f --- /dev/null +++ b/src/modules/shared/selects/expenses.select.ts @@ -0,0 +1,11 @@ +export const EXPENSE_SELECT = { + date: true, + amount: true, + mileage: true, + comment: true, + is_approved: true, + supervisor_comment: true, + bank_code: { select: { type: true } }, +} as const; + +export const EXPENSE_ASC_ORDER = { date: 'asc' as const }; \ No newline at end of file diff --git a/src/modules/shared/selects/pay-periods.select.ts b/src/modules/shared/selects/pay-periods.select.ts new file mode 100644 index 0000000..a76f09b --- /dev/null +++ b/src/modules/shared/selects/pay-periods.select.ts @@ -0,0 +1,4 @@ +export const PAY_PERIOD_SELECT = { + period_start: true, + period_end: true, +} as const; \ No newline at end of file diff --git a/src/modules/shared/selects/shifts.select.ts b/src/modules/shared/selects/shifts.select.ts new file mode 100644 index 0000000..8c738e1 --- /dev/null +++ b/src/modules/shared/selects/shifts.select.ts @@ -0,0 +1,12 @@ +export const SHIFT_SELECT = { + date: true, + start_time: true, + end_time: true, + comment: true, + is_approved: true, + is_remote: true, + bank_code: {select: { type: true } }, +} as const; + +export const SHIFT_ASC_ORDER = [{date: 'asc' as const}, {start_time: 'asc' as const}]; + diff --git a/src/modules/shared/selects/timesheets.select.ts b/src/modules/shared/selects/timesheets.select.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/timesheets/services/timesheets-command.service.ts b/src/modules/timesheets/services/timesheets-command.service.ts index b6de4cb..2db914e 100644 --- a/src/modules/timesheets/services/timesheets-command.service.ts +++ b/src/modules/timesheets/services/timesheets-command.service.ts @@ -5,7 +5,7 @@ import { PrismaService } from "src/prisma/prisma.service"; import { TimesheetsQueryService } from "./timesheets-query.service"; import { CreateTimesheetDto } from "../dtos/create-timesheet.dto"; import { getWeekEnd, getWeekStart } from "src/common/utils/date-utils"; -import { parseISODate, parseHHmm } from "../utils/timesheet.helpers"; +import { parseISODate, parseHHmm } from "../timesheet.helpers"; import { TimesheetDto } from "../dtos/timesheet-period.dto"; import { EmailToIdResolver } from "src/modules/shared/utils/resolve-email-id.utils"; import { EmployeeTimesheetResolver } from "src/modules/shared/utils/resolve-employee-timesheet.utils"; diff --git a/src/modules/timesheets/services/timesheets-query.service.ts b/src/modules/timesheets/services/timesheets-query.service.ts index 1092c3b..58dfd72 100644 --- a/src/modules/timesheets/services/timesheets-query.service.ts +++ b/src/modules/timesheets/services/timesheets-query.service.ts @@ -1,12 +1,14 @@ -import { endOfDayUTC, toHHmm, toUTCDateOnly } from '../utils/timesheet.helpers'; +import { endOfDayUTC, toHHmm, toNum, toRangeFromPeriod, toUTCDateOnly } from '../timesheet.helpers'; import { Injectable, NotFoundException } from '@nestjs/common'; import { formatDateISO, getWeekEnd, getWeekStart } from 'src/common/utils/date-utils'; import { PrismaService } from 'src/prisma/prisma.service'; import { TimesheetDto, TimesheetPeriodDto } from '../dtos/timesheet-period.dto'; -import { ShiftRow, ExpenseRow } from '../types/timesheet.types'; -import { buildPeriod } from '../utils/timesheet.utils'; +import { ShiftRow, ExpenseRow } from '../timesheet.types'; +import { buildPeriod } from '../timesheet.utils'; import { EmailToIdResolver } from 'src/modules/shared/utils/resolve-email-id.utils'; import { FullNameResolver } from 'src/modules/shared/utils/resolve-full-name.utils'; +import { TimesheetSelectors } from '../timesheet.selectors'; +import { mapExpenseRow, mapShiftRow } from '../timesheet.mappers'; @Injectable() @@ -14,93 +16,32 @@ export class TimesheetsQueryService { constructor( private readonly prisma: PrismaService, private readonly emailResolver: EmailToIdResolver, - private readonly fullNameResolver: FullNameResolver + private readonly fullNameResolver: FullNameResolver, + private readonly selectors: TimesheetSelectors, ) {} async findAll(year: number, period_no: number, email: string): Promise { //finds the employee using email const employee_id = await this.emailResolver.findIdByEmail(email); - if(!employee_id) throw new NotFoundException(`employee with email : ${email} not found`); - + //finds the employee full name using employee_id const full_name = await this.fullNameResolver.resolveFullName(employee_id); - if(!full_name) throw new NotFoundException(`employee with id: ${employee_id} not found`) - - //finds the period - const period = await this.prisma.payPeriods.findFirst({ - where: { - pay_year: year, - pay_period_no: period_no - }, - select: { - period_start: true, - period_end: true - }, - }); - if(!period) throw new NotFoundException(`Period ${year}-${period_no} not found`); - - const from = toUTCDateOnly(period.period_start); - const to = endOfDayUTC(period.period_end); - - const raw_shifts = await this.prisma.shifts.findMany({ - where: { - timesheet: { is: { employee_id: employee_id } }, - date: { gte: from, lte: to }, - }, - select: { - date: true, - start_time: true, - end_time: true, - comment: true, - is_approved: true, - is_remote: true, - bank_code: { select: { type: true } }, - }, - orderBy:[ { date:'asc'}, { start_time: 'asc'} ], - }); - - const raw_expenses = await this.prisma.expenses.findMany({ - where: { - timesheet: { is: { employee_id: employee_id } }, - date: { gte: from, lte: to }, - }, - select: { - date: true, - amount: true, - mileage: true, - comment: true, - is_approved: true, - supervisor_comment: true, - bank_code: { select: { type: true } }, - }, - orderBy: { date: 'asc' }, - }); - const toNum = (value: any) => - value && typeof value.toNumber === 'function' ? value.toNumber() : - typeof value === 'number' ? value : - value ? Number(value) : 0; + //finds the pay period using year and period_no + const period = await this.selectors.getPayPeriod(year, period_no); + //finds start and end dates + const{ from, to } = toRangeFromPeriod(period); + + //finds all shifts from selected period + const [raw_shifts, raw_expenses] = await Promise.all([ + this.selectors.getShifts(employee_id, from, to), + this.selectors.getExpenses(employee_id, from, to), + ]); + // data mapping - const shifts: ShiftRow[] = raw_shifts.map(shift => ({ - date: shift.date, - start_time: shift.start_time, - end_time: shift.end_time, - comment: shift.comment ?? '', - is_approved: shift.is_approved ?? true, - is_remote: shift.is_remote ?? true, - type: String(shift.bank_code?.type ?? '').toUpperCase(), - })); - - const expenses: ExpenseRow[] = raw_expenses.map(expense => ({ - type: String(expense.bank_code?.type ?? '').toUpperCase(), - date: expense.date, - amount: toNum(expense.amount), - mileage: toNum(expense.mileage), - comment: expense.comment ?? '', - is_approved: expense.is_approved ?? true, - supervisor_comment: expense.supervisor_comment ?? '', - })); + const shifts = raw_shifts.map(mapShiftRow); + const expenses = raw_expenses.map(mapExpenseRow); return buildPeriod(period.period_start, period.period_end, shifts , expenses, full_name); } diff --git a/src/modules/timesheets/utils/timesheet.helpers.ts b/src/modules/timesheets/timesheet.helpers.ts similarity index 76% rename from src/modules/timesheets/utils/timesheet.helpers.ts rename to src/modules/timesheets/timesheet.helpers.ts index 3ba862d..249b3a0 100644 --- a/src/modules/timesheets/utils/timesheet.helpers.ts +++ b/src/modules/timesheets/timesheet.helpers.ts @@ -1,5 +1,4 @@ -import { MS_PER_DAY, DayKey, DAY_KEYS } from "../types/timesheet.types"; - +import { MS_PER_DAY, DayKey, DAY_KEYS } from "./timesheet.types"; export function toUTCDateOnly(date: Date | string): Date { const d = new Date(date); @@ -43,7 +42,6 @@ export function dayKeyFromDate(date: Date, useUTC = true): DayKey { export const toHHmm = (date: Date) => date.toISOString().slice(11, 16); -//create shifts within timesheet's week - employee overview functions export function parseISODate(iso: string): Date { const [ y, m, d ] = iso.split('-').map(Number); return new Date(y, (m ?? 1) - 1, d ?? 1); @@ -54,3 +52,15 @@ export function parseHHmm(t: string): Date { return new Date(1970, 0, 1, hh || 0, mm || 0, 0, 0); } +export const toNum = (value: any) => + value && typeof value.toNumber === 'function' ? value.toNumber() : + typeof value === 'number' ? value : + value ? Number(value) : 0; + + +export const upper = (s?: string | null) => String(s ?? '').toUpperCase(); + +export const toRangeFromPeriod = (period: { period_start: Date; period_end: Date }) => ({ + from: toUTCDateOnly(period.period_start), + to: endOfDayUTC(period.period_end), +}); \ No newline at end of file diff --git a/src/modules/timesheets/mappers/timesheet.mappers.ts b/src/modules/timesheets/timesheet.mappers.ts similarity index 54% rename from src/modules/timesheets/mappers/timesheet.mappers.ts rename to src/modules/timesheets/timesheet.mappers.ts index c9f04c4..5807863 100644 --- a/src/modules/timesheets/mappers/timesheet.mappers.ts +++ b/src/modules/timesheets/timesheet.mappers.ts @@ -1,6 +1,46 @@ -import { DayExpensesDto, WeekDto, DetailedShifts, TimesheetPeriodDto } from "../dtos/timesheet-period.dto"; -import { ExpensesAmount } from "../types/timesheet.types"; -import { addDays, shortDate } from "../utils/timesheet.helpers"; +import { DayExpensesDto, WeekDto, DetailedShifts, TimesheetPeriodDto } from "./dtos/timesheet-period.dto"; +import { ExpenseRow, ExpensesAmount, ShiftRow } from "./timesheet.types"; +import { addDays, shortDate, toNum, upper } from "./timesheet.helpers"; +import { Prisma } from "@prisma/client"; + + +//mappers +export const mapShiftRow = (shift: { + date: Date; + start_time: Date; + end_time: Date; + comment?: string | null; + is_approved: boolean; + is_remote: boolean; + bank_code: { type: string }; +}): ShiftRow => ({ + date: shift.date, + start_time: shift.start_time, + end_time: shift.end_time, + comment: shift.comment ?? '', + is_approved: shift.is_approved, + is_remote: shift.is_remote, + type: upper(shift.bank_code.type), +}); + +export const mapExpenseRow = (expense: { + date: Date; + amount: Prisma.Decimal | number | null; + mileage: Prisma.Decimal | number | null; + comment?: string | null; + is_approved: boolean; + supervisor_comment?: string|null; + bank_code: { type: string }, +}): ExpenseRow => ({ + date: expense.date, + amount: toNum(expense.amount), + mileage: toNum(expense.mileage), + comment: expense.comment ?? '', + is_approved: expense.is_approved, + supervisor_comment: expense.supervisor_comment ?? '', + type: upper(expense.bank_code.type), +}); + // Factories export function makeEmptyDayExpenses(): DayExpensesDto { diff --git a/src/modules/timesheets/timesheet.selectors.ts b/src/modules/timesheets/timesheet.selectors.ts new file mode 100644 index 0000000..1c309d4 --- /dev/null +++ b/src/modules/timesheets/timesheet.selectors.ts @@ -0,0 +1,35 @@ +import { Injectable, NotFoundException } from "@nestjs/common"; +import { PrismaService } from "src/prisma/prisma.service"; +import { SHIFT_ASC_ORDER, SHIFT_SELECT } from "../shared/selects/shifts.select"; +import { PAY_PERIOD_SELECT } from "../shared/selects/pay-periods.select"; +import { EXPENSE_ASC_ORDER, EXPENSE_SELECT } from "../shared/selects/expenses.select"; + +@Injectable() +export class TimesheetSelectors { + constructor(readonly prisma: PrismaService){} + + async getPayPeriod(pay_year: number, pay_period_no: number) { + const period = await this.prisma.payPeriods.findFirst({ + where: { pay_year, pay_period_no }, + select: PAY_PERIOD_SELECT , + }); + if(!period) throw new NotFoundException(`period ${pay_year}-${pay_period_no} not found`); + return period; + } + + async getShifts(employee_id: number, from: Date, to: Date) { + return this.prisma.shifts.findMany({ + where: {timesheet: { is: { employee_id } }, date: { gte: from, lte: to } }, + select: SHIFT_SELECT, + orderBy: SHIFT_ASC_ORDER, + }); + } + + async getExpenses(employee_id: number, from: Date, to: Date) { + return this.prisma.expenses.findMany({ + where: { timesheet: {is: { employee_id } }, date: { gte: from, lte: to } }, + select: EXPENSE_SELECT, + orderBy: EXPENSE_ASC_ORDER, + }); + } +} \ No newline at end of file diff --git a/src/modules/timesheets/types/timesheet.types.ts b/src/modules/timesheets/timesheet.types.ts similarity index 100% rename from src/modules/timesheets/types/timesheet.types.ts rename to src/modules/timesheets/timesheet.types.ts diff --git a/src/modules/timesheets/utils/timesheet.utils.ts b/src/modules/timesheets/timesheet.utils.ts similarity index 97% rename from src/modules/timesheets/utils/timesheet.utils.ts rename to src/modules/timesheets/timesheet.utils.ts index fa09eac..3d762c1 100644 --- a/src/modules/timesheets/utils/timesheet.utils.ts +++ b/src/modules/timesheets/timesheet.utils.ts @@ -1,13 +1,13 @@ import { DayKey, DAY_KEYS, EXPENSE_TYPES, ExpenseRow, MS_PER_HOUR, SHIFT_TYPES, ShiftRow, make_hours, ShiftsHours, ExpensesAmount -} from "../types/timesheet.types"; +} from "./timesheet.types"; import { isBetweenUTC, dayKeyFromDate, toTimeString, round2, toUTCDateOnly, endOfDayUTC, addDays } from "./timesheet.helpers"; -import { WeekDto, ShiftDto, TimesheetPeriodDto, DayExpensesDto, ExpenseDto } from "../dtos/timesheet-period.dto"; -import { makeAmounts, makeEmptyWeek } from "../mappers/timesheet.mappers"; +import { WeekDto, ShiftDto, TimesheetPeriodDto, DayExpensesDto, ExpenseDto } from "./dtos/timesheet-period.dto"; +import { makeAmounts, makeEmptyWeek } from "./timesheet.mappers"; import { toDateString } from "src/modules/pay-periods/utils/pay-year.util"; export function buildWeek(