import { endOfDayUTC, toHHmm, toUTCDateOnly } from '../utils/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 { OvertimeService } from 'src/modules/business-logics/services/overtime.service'; import { TimesheetDto } from '../dtos/overview-timesheet.dto'; import { TimesheetPeriodDto } from '../dtos/timesheet-period.dto'; import { ShiftRow, ExpenseRow } from '../types/timesheet.types'; import { buildPeriod } from '../utils/timesheet.utils'; @Injectable() export class TimesheetsQueryService { constructor( private readonly prisma: PrismaService, // private readonly overtime: OvertimeService, ) {} async findAll(year: number, period_no: number, email: string): Promise { //finds the employee const employee = await this.prisma.employees.findFirst({ where: { user: { is: { email } } }, select: { id: true, user_id: true, }, }); if(!employee) throw new NotFoundException(`no employee with email ${email} found`); //gets the employee's full name const user = await this.prisma.users.findFirst({ where: { id: employee.user_id }, select: { first_name: true, last_name: true, } }); const employee_full_name: string = ( user?.first_name + " " + user?.last_name ) || " "; //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; // 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 ?? '', })); return buildPeriod(period.period_start, period.period_end, shifts , expenses, employee_full_name); } async getTimesheetByEmail(email: string, week_offset = 0): Promise { //fetch user related to email const user = await this.prisma.users.findUnique({ where: { email }, select: { id: true }, }); if(!user) throw new NotFoundException(`user with email ${email} not found`); //fetch employee_id matching the email const employee = await this.prisma.employees.findFirst({ where: { user_id: user.id }, select: { id: true }, }); if(!employee) 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; } //small helper to format hours:minutes //maps all shifts of selected timesheet const shifts = timesheet.shift.map((shift_row) => ({ bank_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) => ({ bank_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 { is_approved: timesheet.is_approved, start_day, end_day, label, shifts, expenses, } 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 } }); // } }