271 lines
10 KiB
TypeScript
271 lines
10 KiB
TypeScript
import { Injectable, NotFoundException } from "@nestjs/common";
|
|
import { PrismaService } from "src/prisma/prisma.service";
|
|
import { CreateShiftDto } from "../dtos/create-shift.dto";
|
|
import { Shifts, ShiftsArchive } from "@prisma/client";
|
|
import { UpdateShiftsDto } from "../dtos/update-shift.dto";
|
|
import { buildPrismaWhere } from "src/common/shared/build-prisma-where";
|
|
import { SearchShiftsDto } from "../dtos/search-shift.dto";
|
|
import { NotificationsService } from "src/modules/notifications/services/notifications.service";
|
|
import { computeHours, hoursBetweenSameDay } from "src/common/utils/date-utils";
|
|
|
|
const DAILY_LIMIT_HOURS = Number(process.env.DAILY_LIMIT_HOURS ?? 12);
|
|
|
|
export interface OverviewRow {
|
|
full_name: string;
|
|
supervisor: string;
|
|
total_regular_hrs: number;
|
|
total_evening_hrs: number;
|
|
total_overtime_hrs: number;
|
|
total_expenses: number;
|
|
total_mileage: number;
|
|
is_approved: boolean;
|
|
}
|
|
|
|
@Injectable()
|
|
export class ShiftsQueryService {
|
|
constructor(
|
|
private readonly prisma: PrismaService,
|
|
private readonly notifs: NotificationsService,
|
|
) {}
|
|
|
|
async create(dto: CreateShiftDto): Promise<Shifts> {
|
|
const { timesheet_id, bank_code_id, date, start_time, end_time, description } = dto;
|
|
|
|
//shift creation
|
|
const shift = await this.prisma.shifts.create({
|
|
data: { timesheet_id, bank_code_id, date, start_time, end_time, description },
|
|
include: { timesheet: { include: { employee: { include: { user: true } } } },
|
|
bank_code: true,
|
|
},
|
|
});
|
|
|
|
//fetches all shifts of the same day to check for daily overtime
|
|
const same_day_shifts = await this.prisma.shifts.findMany({
|
|
where: { timesheet_id, date },
|
|
select: { id: true, date: true, start_time: true, end_time: true },
|
|
});
|
|
|
|
//sums hours of the day
|
|
const total_hours = same_day_shifts.reduce((sum, s) => {
|
|
return sum + hoursBetweenSameDay(s.date, s.start_time, s.end_time);
|
|
}, 0 );
|
|
|
|
//Notify if total hours > 8 for a single day
|
|
if(total_hours > DAILY_LIMIT_HOURS ) {
|
|
const user_id = String(shift.timesheet.employee.user.id);
|
|
const date_label = new Date(date).toLocaleDateString('fr-CA');
|
|
this.notifs.notify(user_id, {
|
|
type: 'shift.overtime.daily',
|
|
severity: 'warn',
|
|
message: `Tu viens de dépasser ${DAILY_LIMIT_HOURS.toFixed(2)}h pour la journée du ${date_label}
|
|
(total: ${total_hours.toFixed(2)}h).`,
|
|
ts: new Date().toISOString(),
|
|
meta: {
|
|
timesheet_id,
|
|
date: new Date(date).toISOString(),
|
|
total_hours,
|
|
threshold: DAILY_LIMIT_HOURS,
|
|
last_shift_id: shift.id
|
|
},
|
|
});
|
|
}
|
|
return shift;
|
|
}
|
|
|
|
async findAll(filters: SearchShiftsDto): Promise <Shifts[]> {
|
|
const where = buildPrismaWhere(filters);
|
|
const shifts = await this.prisma.shifts.findMany({ where })
|
|
return shifts;
|
|
}
|
|
|
|
async findOne(id: number): Promise<Shifts> {
|
|
const shift = await this.prisma.shifts.findUnique({
|
|
where: { id },
|
|
include: { timesheet: { include: { employee: { include: { user: true } } } },
|
|
bank_code: true,
|
|
},
|
|
});
|
|
if(!shift) {
|
|
throw new NotFoundException(`Shift #${id} not found`);
|
|
}
|
|
return shift;
|
|
}
|
|
|
|
async update(id: number, dto: UpdateShiftsDto): Promise<Shifts> {
|
|
await this.findOne(id);
|
|
const { timesheet_id, bank_code_id, date,start_time,end_time, description} = dto;
|
|
return this.prisma.shifts.update({
|
|
where: { id },
|
|
data: {
|
|
...(timesheet_id !== undefined && { timesheet_id }),
|
|
...(bank_code_id !== undefined && { bank_code_id }),
|
|
...(date !== undefined && { date }),
|
|
...(start_time !== undefined && { start_time }),
|
|
...(end_time !== undefined && { end_time }),
|
|
...(description !== undefined && { description }),
|
|
},
|
|
include: { timesheet: { include: { employee: { include: { user: true } } } },
|
|
bank_code: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
async remove(id: number): Promise<Shifts> {
|
|
await this.findOne(id);
|
|
return this.prisma.shifts.delete({ where: { id } });
|
|
}
|
|
|
|
async getSummary(period_id: number): Promise<OverviewRow[]> {
|
|
//fetch pay-period to display
|
|
const period = await this.prisma.payPeriods.findFirst({
|
|
where: { pay_period_no: period_id },
|
|
});
|
|
if(!period) {
|
|
throw new NotFoundException(`pay-period ${period_id} not found`);
|
|
}
|
|
const { period_start, period_end } = period;
|
|
|
|
//prepare shifts and expenses for display
|
|
const shifts = await this.prisma.shifts.findMany({
|
|
where: { date: { gte: period_start, lte: period_end } },
|
|
include: {
|
|
bank_code: true,
|
|
timesheet: { include: {
|
|
employee: { include: {
|
|
user:true,
|
|
supervisor: { include: { user: true } },
|
|
} },
|
|
} },
|
|
},
|
|
});
|
|
|
|
const expenses = await this.prisma.expenses.findMany({
|
|
where: { date: { gte: period_start, lte: period_end } },
|
|
include: {
|
|
bank_code: true,
|
|
timesheet: { include: { employee: {
|
|
include: { user:true,
|
|
supervisor: { include: { user:true } },
|
|
} },
|
|
} },
|
|
},
|
|
});
|
|
|
|
const mapRow = new Map<string, OverviewRow>();
|
|
|
|
for(const shift of shifts) {
|
|
const employeeId = shift.timesheet.employee.user_id;
|
|
const user = shift.timesheet.employee.user;
|
|
const sup = shift.timesheet.employee.supervisor?.user;
|
|
|
|
let row = mapRow.get(employeeId);
|
|
if(!row) {
|
|
row = {
|
|
full_name: `${user.first_name} ${user.last_name}`,
|
|
supervisor: sup? `${sup.first_name} ${sup.last_name }` : '',
|
|
total_regular_hrs: 0,
|
|
total_evening_hrs: 0,
|
|
total_overtime_hrs: 0,
|
|
total_expenses: 0,
|
|
total_mileage: 0,
|
|
is_approved: false,
|
|
};
|
|
}
|
|
const hours = computeHours(shift.start_time, shift.end_time);
|
|
|
|
switch(shift.bank_code.type) {
|
|
case 'regular' : row.total_regular_hrs += hours;
|
|
break;
|
|
case 'evening' : row.total_evening_hrs += hours;
|
|
break;
|
|
case 'overtime' : row.total_overtime_hrs += hours;
|
|
break;
|
|
default: row.total_regular_hrs += hours;
|
|
}
|
|
mapRow.set(employeeId, row);
|
|
}
|
|
|
|
for(const exp of expenses) {
|
|
const employee_id = exp.timesheet.employee.user_id;
|
|
const user = exp.timesheet.employee.user;
|
|
const sup = exp.timesheet.employee.supervisor?.user;
|
|
|
|
let row = mapRow.get(employee_id);
|
|
if(!row) {
|
|
row = {
|
|
full_name: `${user.first_name} ${user.last_name}`,
|
|
supervisor: sup? `${sup.first_name} ${sup.last_name }` : '',
|
|
total_regular_hrs: 0,
|
|
total_evening_hrs: 0,
|
|
total_overtime_hrs: 0,
|
|
total_expenses: 0,
|
|
total_mileage: 0,
|
|
is_approved: false,
|
|
};
|
|
}
|
|
const amount = Number(exp.amount);
|
|
row.total_expenses += amount;
|
|
if(exp.bank_code.type === 'mileage') {
|
|
row.total_mileage += amount;
|
|
}
|
|
mapRow.set(employee_id, row);
|
|
}
|
|
//return by default the list of employee in ascending alphabetical order
|
|
return Array.from(mapRow.values()).sort((a,b) => a.full_name.localeCompare(b.full_name));
|
|
}
|
|
|
|
//archivation functions ******************************************************
|
|
|
|
async archiveOld(): Promise<void> {
|
|
//fetches archived timesheet's Ids
|
|
const archived_timesheets = await this.prisma.timesheetsArchive.findMany({
|
|
select: { timesheet_id: true },
|
|
});
|
|
|
|
const timesheet_ids = archived_timesheets.map(sheet => sheet.timesheet_id);
|
|
if(timesheet_ids.length === 0) {
|
|
return;
|
|
}
|
|
|
|
// copy/delete transaction
|
|
await this.prisma.$transaction(async transaction => {
|
|
//fetches shifts to move to archive
|
|
const shifts_to_archive = await transaction.shifts.findMany({
|
|
where: { timesheet_id: { in: timesheet_ids } },
|
|
});
|
|
if(shifts_to_archive.length === 0) {
|
|
return;
|
|
}
|
|
|
|
//copies sent to archive table
|
|
await transaction.shiftsArchive.createMany({
|
|
data: shifts_to_archive.map(shift => ({
|
|
shift_id: shift.id,
|
|
timesheet_id: shift.timesheet_id,
|
|
bank_code_id: shift.bank_code_id,
|
|
description: shift.description ?? undefined,
|
|
date: shift.date,
|
|
start_time: shift.start_time,
|
|
end_time: shift.end_time,
|
|
})),
|
|
});
|
|
|
|
//delete from shifts table
|
|
await transaction.shifts.deleteMany({
|
|
where: { id: { in: shifts_to_archive.map(shift => shift.id) } },
|
|
})
|
|
|
|
})
|
|
}
|
|
|
|
//fetches all archived timesheets
|
|
async findAllArchived(): Promise<ShiftsArchive[]> {
|
|
return this.prisma.shiftsArchive.findMany();
|
|
}
|
|
|
|
//fetches an archived timesheet
|
|
async findOneArchived(id: number): Promise<ShiftsArchive> {
|
|
return this.prisma.shiftsArchive.findUniqueOrThrow({ where: { id } });
|
|
}
|
|
|
|
} |