254 lines
9.9 KiB
TypeScript
254 lines
9.9 KiB
TypeScript
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<TimesheetPeriodDto> {
|
|
//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<TimesheetDto> {
|
|
|
|
//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<any> {
|
|
// 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<Timesheets> {
|
|
// await this.findOne(id);
|
|
// return this.prisma.timesheets.delete({ where: { id } });
|
|
// }
|
|
}
|