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 '../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() export class TimesheetsQueryService { constructor( private readonly prisma: PrismaService, private readonly emailResolver: EmailToIdResolver, 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); //finds the employee full name using employee_id const full_name = await this.fullNameResolver.resolveFullName(employee_id); //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 = raw_shifts.map(mapShiftRow); const expenses = raw_expenses.map(mapExpenseRow); return buildPeriod(period.period_start, period.period_end, shifts , expenses, full_name); } async getTimesheetByEmail(email: string, week_offset = 0): Promise { const employee_id = await this.emailResolver.findIdByEmail(email); if(!employee_id) throw new NotFoundException(`Employee with email: ${email} not found`); //sets current week Sunday -> Saturday const base = new Date(); const offset = new Date(base); offset.setDate(offset.getDate() + (week_offset * 7)); const start_date_week = getWeekStart(offset, 0); const end_date_week = getWeekEnd(start_date_week); const start_day = formatDateISO(start_date_week); const end_day = formatDateISO(end_date_week); //build the label MM/DD/YYYY.MM/DD.YYYY const mm_dd = (date: Date) => `${String(date.getFullYear())}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2,'0')}`; const label = `${mm_dd(start_date_week)}.${mm_dd(end_date_week)}`; //fetch timesheet shifts and expenses const timesheet = await this.prisma.timesheets.findUnique({ where: { employee_id_start_date: { employee_id: employee_id, start_date: start_date_week, }, }, include: { shift: { include: { bank_code: true }, orderBy: [{ date: 'asc'}, { start_time: 'asc'}], }, expense: { include: { bank_code: true }, orderBy: [{date: 'asc'}], }, }, }); //returns an empty timesheet if not found if(!timesheet) { return { is_approved: false, start_day, end_day, label, shifts:[], expenses: [], } as TimesheetDto; } //maps all shifts of selected timesheet const shifts = timesheet.shift.map((shift_row) => ({ type: shift_row.bank_code?.type ?? '', date: formatDateISO(shift_row.date), start_time: toHHmm(shift_row.start_time), end_time: toHHmm(shift_row.end_time), comment: shift_row.comment ?? '', is_approved: shift_row.is_approved ?? false, is_remote: shift_row.is_remote ?? false, })); //maps all expenses of selected timsheet const expenses = timesheet.expense.map((exp) => ({ type: exp.bank_code?.type ?? '', date: formatDateISO(exp.date), amount: Number(exp.amount) || 0, mileage: exp.mileage != null ? Number(exp.mileage) : 0, comment: exp.comment ?? '', is_approved: exp.is_approved ?? false, supervisor_comment: exp.supervisor_comment ?? '', })); return { start_day, end_day, label, shifts, expenses, is_approved: timesheet.is_approved, } as TimesheetDto; } //_____________________________________________________________________________________________ // Deprecated or unused methods //_____________________________________________________________________________________________ // async findOne(id: number): Promise { // const timesheet = await this.prisma.timesheets.findUnique({ // where: { id }, // include: { // shift: { include: { bank_code: true } }, // expense: { include: { bank_code: true } }, // employee: { include: { user: true } }, // }, // }); // if(!timesheet) { // throw new NotFoundException(`Timesheet #${id} not found`); // } // const detailedShifts = timesheet.shift.map( s => { // const hours = computeHours(s.start_time, s.end_time); // const regularHours = Math.min(8, hours); // const dailyOvertime = this.overtime.getDailyOvertimeHours(s.start_time, s.end_time); // const payRegular = regularHours * s.bank_code.modifier; // const payOvertime = this.overtime.calculateOvertimePay(dailyOvertime, s.bank_code.modifier); // return { ...s, hours, payRegular, payOvertime }; // }); // const weeklyOvertimeHours = detailedShifts.length // ? await this.overtime.getWeeklyOvertimeHours( // timesheet.employee_id, // timesheet.shift[0].date): 0; // return { ...timesheet, shift: detailedShifts, weeklyOvertimeHours }; // } // async remove(id: number): Promise { // await this.findOne(id); // return this.prisma.timesheets.delete({ where: { id } }); // } }