clean(folder): cleaning imports

This commit is contained in:
Matthieu Haineault 2025-11-03 11:47:41 -05:00
parent c59b50a829
commit f1f765b350
43 changed files with 176 additions and 696 deletions

View File

@ -91,30 +91,20 @@
"parameters": [
{
"name": "date",
"required": false,
"required": true,
"in": "query",
"description": "Override for resolving the current period",
"schema": {
"example": "2025-08-11",
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Find current and all pay periods",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PayPeriodBundleDto"
}
}
}
"description": ""
}
},
"summary": "Return current pay period and the full list",
"tags": [
"pay-periods"
"PayPeriods"
]
}
},
@ -133,22 +123,11 @@
],
"responses": {
"200": {
"description": "Pay period found for the selected date",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PayPeriodDto"
}
}
"description": ""
}
},
"404": {
"description": "Pay period not found for the selected date"
}
},
"summary": "Resolve a period by a date within it",
"tags": [
"pay-periods"
"PayPeriods"
]
}
},
@ -161,7 +140,6 @@
"required": true,
"in": "path",
"schema": {
"example": 2024,
"type": "number"
}
},
@ -169,31 +147,18 @@
"name": "periodNumber",
"required": true,
"in": "path",
"description": "1..26",
"schema": {
"example": 1,
"type": "number"
}
}
],
"responses": {
"200": {
"description": "Pay period found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PayPeriodDto"
}
}
"description": ""
}
},
"404": {
"description": "Pay period not found"
}
},
"summary": "Find pay period by year and period number",
"tags": [
"pay-periods"
"PayPeriods"
]
}
},
@ -206,7 +171,6 @@
"required": true,
"in": "path",
"schema": {
"example": 2024,
"type": "number"
}
},
@ -214,49 +178,18 @@
"name": "periodNumber",
"required": true,
"in": "path",
"description": "1..26",
"schema": {
"example": 1,
"type": "number"
}
},
{
"name": "email",
"required": true,
"in": "path",
"schema": {
"type": "string"
}
},
{
"name": "includeSubtree",
"required": false,
"in": "query",
"description": "Include indirect reports",
"schema": {
"example": false,
"type": "boolean"
}
}
],
"responses": {
"200": {
"description": "Crew overview",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PayPeriodOverviewDto"
}
}
"description": ""
}
},
"404": {
"description": "Pay period not found"
}
},
"summary": "Supervisor crew overview for a given pay period",
"tags": [
"pay-periods"
"PayPeriods"
]
}
},
@ -269,7 +202,6 @@
"required": true,
"in": "path",
"schema": {
"example": 2024,
"type": "number"
}
},
@ -277,31 +209,18 @@
"name": "periodNumber",
"required": true,
"in": "path",
"description": "1..26",
"schema": {
"example": 1,
"type": "number"
}
}
],
"responses": {
"200": {
"description": "Pay period overview found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PayPeriodOverviewDto"
}
}
"description": ""
}
},
"404": {
"description": "Pay period not found"
}
},
"summary": "Detailed view of a pay period by year + number",
"tags": [
"pay-periods"
"PayPeriods"
]
}
},
@ -690,168 +609,6 @@
}
},
"schemas": {
"PayPeriodDto": {
"type": "object",
"properties": {
"pay_period_no": {
"type": "number",
"example": 1,
"description": "numéro cyclique de la période entre 1 et 26"
},
"period_start": {
"type": "string",
"example": "2023-12-17",
"format": "date"
},
"period_end": {
"type": "string",
"example": "2023-12-30",
"format": "date"
},
"payday": {
"type": "string",
"example": "2023-01-04",
"format": "date"
},
"pay_year": {
"type": "number",
"example": 2023
},
"label": {
"type": "string",
"example": "2023-12-17 → 2023-12-30"
}
},
"required": [
"pay_period_no",
"period_start",
"period_end",
"payday",
"pay_year",
"label"
]
},
"PayPeriodBundleDto": {
"type": "object",
"properties": {
"current": {
"description": "Current pay period (resolved from date)",
"allOf": [
{
"$ref": "#/components/schemas/PayPeriodDto"
}
]
},
"periods": {
"description": "All pay periods",
"type": "array",
"items": {
"$ref": "#/components/schemas/PayPeriodDto"
}
}
},
"required": [
"current",
"periods"
]
},
"EmployeePeriodOverviewDto": {
"type": "object",
"properties": {
"employee_name": {
"type": "string",
"example": "Alex Dupont",
"description": "Nom complet de lemployé"
},
"regular_hours": {
"type": "number",
"example": 40,
"description": "pay-period`s regular hours"
},
"other_hours": {
"type": "object",
"example": 0,
"description": "pay-period`s other hours"
},
"expenses": {
"type": "number",
"example": 420.69,
"description": "pay-period`s total expenses ($)"
},
"mileage": {
"type": "number",
"example": 40,
"description": "pay-period total mileages (km)"
},
"is_approved": {
"type": "boolean",
"example": true,
"description": "Tous les timesheets de la période sont approuvés pour cet employé"
}
},
"required": [
"employee_name",
"regular_hours",
"other_hours",
"expenses",
"mileage",
"is_approved"
]
},
"PayPeriodOverviewDto": {
"type": "object",
"properties": {
"pay_period_no": {
"type": "number",
"example": 1,
"description": "Period number (126)"
},
"pay_year": {
"type": "number",
"example": 2023,
"description": "Calendar year of the period"
},
"period_start": {
"type": "string",
"example": "2023-12-17",
"format": "date",
"description": "Period start date (YYYY-MM-DD)"
},
"period_end": {
"type": "string",
"example": "2023-12-30",
"format": "date",
"description": "Period end date (YYYY-MM-DD)"
},
"payday": {
"type": "string",
"example": "2023-12-30",
"format": "date",
"description": "Period pay day(YYYY-MM-DD)"
},
"label": {
"type": "string",
"example": "2023-12-17 → 2023-12-30",
"description": "Human-readable label"
},
"employees_overview": {
"description": "Per-employee overview for the period",
"type": "array",
"items": {
"$ref": "#/components/schemas/EmployeePeriodOverviewDto"
}
}
},
"required": [
"pay_period_no",
"pay_year",
"period_start",
"period_end",
"payday",
"label",
"employees_overview"
]
},
"PreferencesDto": {
"type": "object",
"properties": {}

View File

@ -1,10 +1,9 @@
import { Module } from "@nestjs/common";
import { CsvExportController } from "./controllers/csv-exports.controller";
import { CsvExportService } from "./services/csv-exports.service";
import { SharedModule } from "src/time-and-attendance/shared/shared.module";
@Module({
providers:[CsvExportService, SharedModule],
providers:[CsvExportService],
controllers: [CsvExportController],
})
export class CsvExportModule {}

View File

@ -4,15 +4,18 @@ import { VacationService } from "./services/vacation.service";
import { HolidayService } from "./services/holiday.service";
import { MileageService } from "./services/mileage.service";
import { Module } from "@nestjs/common";
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
@Module({
imports:[],
providers: [
HolidayService,
MileageService,
OvertimeService,
SickLeaveService,
VacationService
VacationService,
EmailToIdResolver,
],
exports: [
HolidayService,

View File

@ -1,9 +1,8 @@
import { Injectable, Logger, NotFoundException } from "@nestjs/common";
import { computeHours, getWeekStart } from "src/common/utils/date-utils";
import { PrismaService } from "../../../prisma/prisma.service";
const WEEK_IN_MS = 7 * 24 * 60 * 60 * 1000;
import { PrismaService } from "src/prisma/prisma.service";
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
import { MS_PER_WEEK } from "src/time-and-attendance/utils/constants.utils";
/*
le calcul est 1/20 des 4 dernières semaines, précédent la semaine incluant le férier.
Un maximum de 08h00 est allouable pour le férier
@ -15,28 +14,19 @@ const WEEK_IN_MS = 7 * 24 * 60 * 60 * 1000;
export class HolidayService {
private readonly logger = new Logger(HolidayService.name);
constructor(private readonly prisma: PrismaService) {}
//fetch employee_id by email
private async resolveEmployeeByEmail(email: string): Promise<number> {
const employee = await this.prisma.employees.findFirst({
where: {
user: { email }
},
select: { id: true },
});
if(!employee) throw new NotFoundException(`Employee with email : ${email} not found`);
return employee.id;
}
constructor(
private readonly prisma: PrismaService,
private readonly emailResolver: EmailToIdResolver,
) {}
private async computeHoursPrevious4WeeksByEmail(email: string, holiday_date: Date): Promise<number> {
const employee_id = await this.resolveEmployeeByEmail(email);
const employee_id = await this.emailResolver.findIdByEmail(email);
return this.computeHoursPrevious4Weeks(employee_id, holiday_date);
}
private async computeHoursPrevious4Weeks(employee_id: number, holiday_date: Date): Promise<number> {
const holiday_week_start = getWeekStart(holiday_date);
const window_start = new Date(holiday_week_start.getTime() - 4 * WEEK_IN_MS);
const window_start = new Date(holiday_week_start.getTime() - 4 * MS_PER_WEEK);
const window_end = new Date(holiday_week_start.getTime() - 1);
const valid_codes = ['G1', 'G43', 'G56', 'G104', 'G105', 'G700'];
@ -60,7 +50,7 @@ export class HolidayService {
let capped_total = 0;
for(let offset = 1; offset <= 4; offset++) {
const week_start = new Date(holiday_week_start.getTime() - offset * WEEK_IN_MS);
const week_start = new Date(holiday_week_start.getTime() - offset * MS_PER_WEEK);
const key = week_start.getTime();
const weekly_hours = hours_by_week.get(key) ?? 0;
capped_total += Math.min(weekly_hours, 40);

View File

@ -1,5 +1,5 @@
import { BadRequestException, Injectable, Logger } from "@nestjs/common";
import { PrismaService } from '../../../prisma/prisma.service';
import { PrismaService } from "src/prisma/prisma.service";
@Injectable()
export class MileageService {

View File

@ -1,32 +1,15 @@
import { Injectable, Logger } from '@nestjs/common';
import { PrismaService } from '../../../prisma/prisma.service';
import { getWeekStart, getWeekEnd, computeHours } from 'src/common/utils/date-utils';
import { Prisma, PrismaClient } from '@prisma/client';
import { PrismaService } from 'src/prisma/prisma.service';
import { DAILY_LIMIT_HOURS, WEEKLY_LIMIT_HOURS } from 'src/time-and-attendance/utils/constants.utils';
import { Tx, WeekOvertimeSummary } from 'src/time-and-attendance/utils/type.utils';
type Tx = Prisma.TransactionClient | PrismaClient;
export type WeekOvertimeSummary = {
week_start:string;
week_end: string;
week_total_hours: number;
weekly_overtime: number;
daily_overtime_kept: number;
total_overtime: number;
breakdown: Array<{
date:string;
day_hours: number;
day_overtime: number;
daily_kept: number;
running_total_before: number;
}>;
};
@Injectable()
export class OvertimeService {
private logger = new Logger(OvertimeService.name);
private daily_max = 8; // maximum for regular hours per day
private weekly_max = 40; // maximum for regular hours per week
private INCLUDED_TYPES = ['EMERGENCY', 'EVENING','OVERTIME','REGULAR'] as const; // included types for weekly overtime calculation
constructor(private prisma: PrismaService) {}
@ -61,7 +44,7 @@ export class OvertimeService {
}
const week_total_hours = [ ...day_totals.values()].reduce((a,b) => a + b, 0);
const weekly_overtime = Math.max(0, week_total_hours - this.weekly_max);
const weekly_overtime = Math.max(0, week_total_hours - WEEKLY_LIMIT_HOURS);
let running = 0;
let daily_kept_sum = 0;
@ -69,9 +52,9 @@ export class OvertimeService {
for (const key of days) {
const day_hours = day_totals.get(key) ?? 0;
const day_overtime = Math.max(0, day_hours - this.daily_max);
const day_overtime = Math.max(0, day_hours - DAILY_LIMIT_HOURS);
const cap_before_40 = Math.max(0, this.weekly_max - running);
const cap_before_40 = Math.max(0, WEEKLY_LIMIT_HOURS - running);
const daily_kept = Math.min(day_overtime, cap_before_40);
breakdown.push({
@ -104,144 +87,4 @@ export class OvertimeService {
breakdown,
};
}
// //calculate daily overtime
// async getDailyOvertimeHours(timesheet_id: number, date: Date): Promise<number> {
// const shifts = await this.prisma.shifts.findMany({
// where: {
// timesheet_id,
// date: date,
// bank_code: { type: { in: this.INCLUDED_TYPES as unknown as string[] } },
// },
// select: { start_time: true, end_time: true },
// orderBy: [{ start_time: 'asc' }],
// });
// const total = shifts.map((shift)=>
// computeHours(shift.start_time, shift.end_time, 5)).
// reduce((sum, hours)=> sum + hours, 0);
// const overtime = Math.max(0, total - this.daily_max);
// this.logger.debug(`[OVERTIME]-[DAILY] total=${total.toFixed(2)}h, overtime= ${overtime.toFixed(2)}h`);
// return overtime;
// }
// //calculate Weekly overtime
// async getWeeklyOvertimeHours(timesheet_id: number, ref_date: Date): Promise<number> {
// const week_start = getWeekStart(ref_date);
// const week_end = getWeekEnd(week_start);
// //fetches all shifts from INCLUDED_TYPES array
// const included_shifts = await this.prisma.shifts.findMany({
// where: {
// timesheet_id,
// date: { gte:week_start, lte: week_end },
// bank_code: { type: { in: this.INCLUDED_TYPES as unknown as string[] } },
// },
// select: { start_time: true, end_time: true },
// orderBy: [{date: 'asc'}, {start_time:'asc'}],
// });
// //calculate total hours of those shifts minus weekly Max to find total overtime hours
// const total = included_shifts.map(shift =>
// computeHours(shift.start_time, shift.end_time, 5)).
// reduce((sum, hours)=> sum+hours, 0);
// const overtime = Math.max(0, total - this.weekly_max);
// this.logger.debug(`[OVERTIME]-[WEEKLY] total=${total.toFixed(2)}h, overtime= ${overtime.toFixed(2)}h`);
// return overtime;
// }
// //transform REGULAR shifts to OVERTIME when exceed 40hrs of included_types of shift
// async transformRegularHoursToWeeklyOvertime(
// employee_id: number,
// ref_date: Date,
// tx?: Prisma.TransactionClient,
// ): Promise<void> {
// //ensures the use of the transaction if needed. fallback to this.prisma if no transaction is detected.
// const db = tx ?? this.prisma;
// //calculate weekly overtime
// const overtime_hours = await this.getWeeklyOvertimeHours(employee_id, ref_date);
// if(overtime_hours <= 0) return;
// const convert_to_minutes = Math.round(overtime_hours * 60);
// const [regular, overtime] = await Promise.all([
// db.bankCodes.findFirst({where: { type: 'REGULAR' }, select: { id: true } }),
// db.bankCodes.findFirst({where: { type: 'OVERTIME'}, select: { id: true } }),
// ]);
// if(!regular || !overtime) return;
// const week_start = getWeekStart(ref_date);
// const week_end = getWeekEnd(week_start);
// //gets all regular shifts and order them by desc
// const regular_shifts_desc = await db.shifts.findMany({
// where: {
// date: { gte:week_start, lte: week_end },
// timesheet: { employee_id },
// bank_code_id: regular.id,
// },
// select: {
// id: true,
// timesheet_id: true,
// date: true,
// start_time: true,
// end_time: true,
// is_remote: true,
// comment: true,
// },
// orderBy: [{date: 'desc'}, {start_time:'desc'}],
// });
// let remaining_minutes = convert_to_minutes;
// for(const shift of regular_shifts_desc) {
// if(remaining_minutes <= 0) break;
// const start = shift.start_time;
// const end = shift.end_time;
// const duration_in_minutes = Math.max(0, Math.round((end.getTime() - start.getTime())/60000));
// if(duration_in_minutes === 0) continue;
// if(duration_in_minutes <= remaining_minutes) {
// await db.shifts.update({
// where: { id: shift.id },
// data: { bank_code_id: overtime.id },
// });
// remaining_minutes -= duration_in_minutes;
// continue;
// }
// //sets the start_time of the new overtime shift
// const new_overtime_start = new Date(end.getTime() - remaining_minutes * 60000);
// //shorten the regular shift
// await db.shifts.update({
// where: { id: shift.id },
// data: { end_time: new_overtime_start },
// });
// //creates the new overtime shift to replace the shorten regular shift
// await db.shifts.create({
// data: {
// timesheet_id: shift.timesheet_id,
// date: shift.date,
// start_time: new_overtime_start,
// end_time: end,
// is_remote: shift.is_remote,
// comment: shift.comment,
// bank_code_id: overtime.id,
// },
// });
// remaining_minutes = 0;
// }
// this.logger.debug(`[OVERTIME]-[WEEKLY]-[TRANSFORM] emp=${employee_id}
// week: ${week_start.toISOString().slice(0,10)}..${week_end.toISOString().slice(0,10)}
// converted= ${(convert_to_minutes-remaining_minutes)/60}h`);
// }
}

View File

@ -1,6 +1,6 @@
import { getYearStart, roundToQuarterHour } from "src/common/utils/date-utils";
import { Injectable, Logger } from "@nestjs/common";
import { PrismaService } from "../../../prisma/prisma.service";
import { PrismaService } from "src/prisma/prisma.service";
@Injectable()
export class SickLeaveService {

View File

@ -1,5 +1,5 @@
import { Injectable, Logger, NotFoundException } from "@nestjs/common";
import { PrismaService } from "../../../prisma/prisma.service";
import { PrismaService } from "src/prisma/prisma.service";
@Injectable()
export class VacationService {

View File

@ -6,7 +6,7 @@ import { expense_select } from "src/time-and-attendance/utils/selects.utils";
import { PrismaService } from "src/prisma/prisma.service";
import { ExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-create.dto";
import { GetExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-get.dto";
import { EmailToIdResolver } from "src/time-and-attendance/shared/utils/resolve-email-id.utils";
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
@Injectable()

View File

@ -1,7 +1,6 @@
import { LeaveRequestController } from "src/time-and-attendance/leave-requests/controllers/leave-requests.controller";
import { LeaveRequestsService } from "src/time-and-attendance/leave-requests/services/leave-request.service";
import { BusinessLogicsModule } from "src/time-and-attendance/domains/business-logics.module";
import { SharedModule } from "src/time-and-attendance/shared/shared.module";
import { ShiftsModule } from "src/time-and-attendance/time-tracker/shifts/shifts.module";
import { Module } from "@nestjs/common";
@ -9,7 +8,6 @@ import { Module } from "@nestjs/common";
imports: [
BusinessLogicsModule,
ShiftsModule,
SharedModule
],
controllers: [LeaveRequestController],
providers: [LeaveRequestsService],

View File

@ -1,15 +1,15 @@
import { Injectable, NotFoundException, BadRequestException } from "@nestjs/common";
import { UpsertLeaveRequestDto, UpsertResult } from "src/time-and-attendance/leave-requests/dtos/upsert-leave-request.dto";
import { LeaveTypes, LeaveApprovalStatus } from "@prisma/client";
import { normalizeDates, toDateOnly } from "src/time-and-attendance/shared/helpers/date-time.helpers";
import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils";
import { LeaveRequestViewDto } from "src/time-and-attendance/leave-requests/dtos/leave-request-view.dto";
import { LeaveRequestsUtils } from "src/time-and-attendance/leave-requests/utils/leave-request.util";
import { BankCodesResolver } from "src/time-and-attendance/shared/utils/resolve-bank-type-id.utils";
import { EmailToIdResolver } from "src/time-and-attendance/shared/utils/resolve-email-id.utils";
import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils";
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
import { HolidayService } from "src/time-and-attendance/domains/services/holiday.service";
import { PrismaService } from "src/prisma/prisma.service";
import { mapRowToView } from "src/time-and-attendance/leave-requests/mappers/leave-requests.mapper";
import { normalizeDates, toDateOnly } from "src/time-and-attendance/utils/date-time.utils";

View File

@ -1,18 +1,18 @@
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
import { normalizeDates, toDateOnly, toISODateKey } from "src/time-and-attendance/shared/helpers/date-time.helpers";
import { UpsertLeaveRequestDto, UpsertResult } from "../dtos/upsert-leave-request.dto";
import { LeaveApprovalStatus, LeaveTypes } from "@prisma/client";
import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils";
import { LeaveRequestViewDto } from "../dtos/leave-request-view.dto";
import { roundToQuarterHour } from "src/common/utils/date-utils";
import { LeaveRequestsUtils } from "src/time-and-attendance/leave-requests/utils/leave-request.util";
import { EmailToIdResolver } from "src/time-and-attendance/shared/utils/resolve-email-id.utils";
import { BankCodesResolver } from "src/time-and-attendance/shared/utils/resolve-bank-type-id.utils";
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils";
import { SickLeaveService } from "src/time-and-attendance/domains/services/sick-leave.service";
import { VacationService } from "src/time-and-attendance/domains/services/vacation.service";
import { HolidayService } from "src/time-and-attendance/domains/services/holiday.service";
import { PrismaService } from "src/prisma/prisma.service";
import { mapRowToView } from "../mappers/leave-requests.mapper";
import { normalizeDates, toDateOnly, toISODateKey } from "src/time-and-attendance/utils/date-time.utils";
@Injectable()
export class LeaveRequestsService {
constructor(

View File

@ -1,15 +1,15 @@
import { Injectable, NotFoundException, BadRequestException } from "@nestjs/common";
import { UpsertLeaveRequestDto, UpsertResult } from "src/time-and-attendance/leave-requests/dtos/upsert-leave-request.dto";
import { LeaveTypes, LeaveApprovalStatus } from "@prisma/client";
import { normalizeDates, toDateOnly } from "src/time-and-attendance/shared/helpers/date-time.helpers";
import { LeaveRequestViewDto } from "src/time-and-attendance/leave-requests/dtos/leave-request-view.dto";
import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils";
import { roundToQuarterHour } from "src/common/utils/date-utils";
import { BankCodesResolver } from "src/time-and-attendance/shared/utils/resolve-bank-type-id.utils";
import { EmailToIdResolver } from "src/time-and-attendance/shared/utils/resolve-email-id.utils";
import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils";
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
import { SickLeaveService } from "src/time-and-attendance/domains/services/sick-leave.service";
import { PrismaService } from "src/prisma/prisma.service";
import { mapRowToView } from "src/time-and-attendance/leave-requests/mappers/leave-requests.mapper";
import { normalizeDates, toDateOnly } from "src/time-and-attendance/utils/date-time.utils";

View File

@ -1,15 +1,15 @@
import { Injectable, NotFoundException, BadRequestException } from "@nestjs/common";
import { UpsertLeaveRequestDto, UpsertResult } from "src/time-and-attendance/leave-requests/dtos/upsert-leave-request.dto";
import { LeaveTypes, LeaveApprovalStatus } from "@prisma/client";
import { normalizeDates, toDateOnly } from "src/time-and-attendance/shared/helpers/date-time.helpers";
import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils";
import { LeaveRequestViewDto } from "src/time-and-attendance/leave-requests/dtos/leave-request-view.dto";
import { roundToQuarterHour } from "src/common/utils/date-utils";
import { BankCodesResolver } from "src/time-and-attendance/shared/utils/resolve-bank-type-id.utils";
import { EmailToIdResolver } from "src/time-and-attendance/shared/utils/resolve-email-id.utils";
import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils";
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
import { VacationService } from "src/time-and-attendance/domains/services/vacation.service";
import { PrismaService } from "src/prisma/prisma.service";
import { mapRowToView } from "src/time-and-attendance/leave-requests/mappers/leave-requests.mapper";
import { normalizeDates, toDateOnly } from "src/time-and-attendance/utils/date-time.utils";
@Injectable()

View File

@ -1,5 +1,4 @@
import { Body, Controller, Get, Param, ParseBoolPipe, ParseIntPipe, Patch, Query } from "@nestjs/common";
import { ApiNotFoundResponse, ApiOperation, ApiParam, ApiQuery, ApiResponse, ApiTags } from "@nestjs/swagger";
import { Controller, Get, Param, ParseBoolPipe, ParseIntPipe, Query, Req } from "@nestjs/common";
import { PayPeriodDto } from "../dtos/pay-period.dto";
import { PayPeriodOverviewDto } from "../dtos/overview-pay-period.dto";
import { PayPeriodsQueryService } from "../services/pay-periods-query.service";
@ -9,7 +8,7 @@ import { Roles as RoleEnum } from '.prisma/client';
import { PayPeriodBundleDto } from "../dtos/bundle-pay-period.dto";
import { BulkCrewApprovalDto } from "../dtos/bulk-crew-approval.dto";
@ApiTags('pay-periods')
@Controller('pay-periods')
export class PayPeriodsController {
@ -19,9 +18,6 @@ export class PayPeriodsController {
) {}
@Get('current-and-all')
@ApiOperation({summary: 'Return current pay period and the full list'})
@ApiQuery({name: 'date', required:false, example: '2025-08-11', description:'Override for resolving the current period'})
@ApiResponse({status: 200, description:'Find current and all pay periods', type: PayPeriodBundleDto})
async getCurrentAndAll(@Query('date') date?: string): Promise<PayPeriodBundleDto> {
const [current, periods] = await Promise.all([
this.queryService.findCurrent(date),
@ -31,19 +27,11 @@ export class PayPeriodsController {
}
@Get("date/:date")
@ApiOperation({ summary: "Resolve a period by a date within it" })
@ApiResponse({ status: 200, description: "Pay period found for the selected date", type: PayPeriodDto })
@ApiNotFoundResponse({ description: "Pay period not found for the selected date" })
async findByDate(@Param("date") date: string) {
return this.queryService.findByDate(date);
}
@Get(":year/:periodNumber")
@ApiOperation({ summary: "Find pay period by year and period number" })
@ApiParam({ name: "year", type: Number, example: 2024 })
@ApiParam({ name: "periodNumber", type: Number, example: 1, description: "1..26" })
@ApiResponse({ status: 200, description: "Pay period found", type: PayPeriodDto })
@ApiNotFoundResponse({ description: "Pay period not found" })
async findOneByYear(
@Param("year", ParseIntPipe) year: number,
@Param("periodNumber", ParseIntPipe) period_no: number,
@ -61,27 +49,16 @@ export class PayPeriodsController {
@Get(':year/:periodNumber/:email')
//@RolesAllowed(RoleEnum.SUPERVISOR)
@ApiOperation({ summary: 'Supervisor crew overview for a given pay period' })
@ApiParam({ name: 'year', type: Number, example: 2024 })
@ApiParam({ name: 'periodNumber', type: Number, example: 1, description: '1..26' })
@ApiQuery({ name: 'includeSubtree', required: false, type: Boolean, example: false, description: 'Include indirect reports' })
@ApiResponse({ status: 200, description: 'Crew overview', type: PayPeriodOverviewDto })
@ApiNotFoundResponse({ description: 'Pay period not found' })
async getCrewOverview(
async getCrewOverview( @Req() req,
@Param('year', ParseIntPipe) year: number,
@Param('periodNumber', ParseIntPipe) period_no: number,
@Param('email') email: string,
@Query('includeSubtree', new ParseBoolPipe({ optional: true })) include_subtree = false,
): Promise<PayPeriodOverviewDto> {
const email = req.user?.email;
return this.queryService.getCrewOverview(year, period_no, email, include_subtree);
}
@Get('overview/:year/:periodNumber')
@ApiOperation({ summary: 'Detailed view of a pay period by year + number' })
@ApiParam({ name: 'year', type: Number, example: 2024 })
@ApiParam({ name: 'periodNumber', type: Number, example: 1, description: '1..26' })
@ApiResponse({ status: 200, description: 'Pay period overview found', type: PayPeriodOverviewDto })
@ApiNotFoundResponse({ description: 'Pay period not found' })
async getOverviewByYear(
@Param('year', ParseIntPipe) year: number,
@Param('periodNumber', ParseIntPipe) period_no: number,

View File

@ -1,5 +1,5 @@
import { Type } from "class-transformer";
import { IsArray, IsBoolean, IsEmail, IsInt, IsOptional, ValidateNested } from "class-validator";
import { IsArray, IsBoolean, IsEmail, IsInt, ValidateNested } from "class-validator";
export class BulkCrewApprovalItemDto {
@IsInt()

View File

@ -1,11 +1,6 @@
import { ApiProperty } from "@nestjs/swagger";
import { PayPeriodDto } from "./pay-period.dto";
export class PayPeriodBundleDto {
@ApiProperty({ type: PayPeriodDto, description: 'Current pay period (resolved from date)' })
current: PayPeriodDto;
@ApiProperty({ type: [PayPeriodDto], description: 'All pay periods' })
periods: PayPeriodDto[];
}

View File

@ -1,27 +1,7 @@
import { ApiProperty } from '@nestjs/swagger';
export class EmployeePeriodOverviewDto {
// @ApiProperty({
// example: 42,
// description: "Employees.id (clé primaire num.)",
// })
// @Allow()
// @IsOptional()
// employee_id: number;
email: string;
@ApiProperty({
example: 'Alex Dupont',
description: 'Nom complet de lemployé',
})
employee_name: string;
@ApiProperty({ example: 40, description: 'pay-period`s regular hours' })
regular_hours: number;
@ApiProperty({ example: 0, description: 'pay-period`s other hours' })
other_hours: {
evening_hours: number;
@ -35,20 +15,9 @@ export class EmployeePeriodOverviewDto {
vacation_hours: number;
};
total_hours: number;
@ApiProperty({ example: 420.69, description: 'pay-period`s total expenses ($)' })
expenses: number;
@ApiProperty({ example: 40, description: 'pay-period total mileages (km)' })
mileage: number;
@ApiProperty({
example: true,
description: 'Tous les timesheets de la période sont approuvés pour cet employé',
})
is_approved: boolean;
is_remote: boolean;
}

View File

@ -1,46 +1,11 @@
import { ApiProperty } from '@nestjs/swagger';
import { EmployeePeriodOverviewDto } from './overview-employee-period.dto';
export class PayPeriodOverviewDto {
@ApiProperty({ example: 1, description: 'Period number (126)' })
pay_period_no: number;
@ApiProperty({ example: 2023, description: 'Calendar year of the period' })
pay_year: number;
@ApiProperty({
example: '2023-12-17',
type: String,
format: 'date',
description: "Period start date (YYYY-MM-DD)",
})
period_start: string;
@ApiProperty({
example: '2023-12-30',
type: String,
format: 'date',
description: "Period end date (YYYY-MM-DD)",
})
period_end: string;
@ApiProperty({
example: '2023-12-30',
type: String,
format: 'date',
description: "Period pay day(YYYY-MM-DD)",
})
payday: string;
@ApiProperty({
example: '2023-12-17 → 2023-12-30',
description: 'Human-readable label',
})
label: string;
@ApiProperty({
type: [EmployeePeriodOverviewDto],
description: 'Per-employee overview for the period',
})
employees_overview: EmployeePeriodOverviewDto[];
}

View File

@ -1,25 +1,8 @@
import { ApiProperty } from "@nestjs/swagger";
export class PayPeriodDto {
@ApiProperty({ example: 1,
description: 'numéro cyclique de la période entre 1 et 26' })
pay_period_no: number;
@ApiProperty({ example: '2023-12-17',
type: String, format: 'date' })
period_start: string;
@ApiProperty({ example: '2023-12-30',
type: String, format: 'date' })
period_end: string;
@ApiProperty({ example: '2023-01-04',
type: String, format: 'date' })
payday: string;
@ApiProperty({ example: 2023 })
pay_year: number;
@ApiProperty({ example: '2023-12-17 → 2023-12-30' })
label: string;
}

View File

@ -1,34 +0,0 @@
import { BadRequestException } from "@nestjs/common";
export const hhmmFromLocal = (d: Date) =>
`${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`;
export const toDateOnly = (s: string): Date => {
if (/^\d{4}-\d{2}-\d{2}$/.test(s)) {
const y = Number(s.slice(0,4));
const m = Number(s.slice(5,7)) - 1;
const d = Number(s.slice(8,10));
return new Date(y, m, d, 0, 0, 0, 0);
}
const dt = new Date(s);
if (Number.isNaN(dt.getTime())) throw new BadRequestException(`Invalid date: ${s}`);
return new Date(dt.getFullYear(), dt.getMonth(), dt.getDate(), 0,0,0,0);
};
// export const toStringFromDate = (d: Date) =>
// `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
export const toISOtoDateOnly = (iso: string): Date => {
const date = new Date(iso);
if (Number.isNaN(date.getTime())) {
throw new BadRequestException(`Invalid date: ${iso}`);
}
date.setHours(0, 0, 0, 0);
return date;
};
export const toISODateKey = (date: Date): string => date.toISOString().slice(0, 10);
export const normalizeDates = (dates: string[]): string[] =>
Array.from(new Set(dates.map((iso) => toISODateKey(toISOtoDateOnly(iso)))));

View File

@ -1,9 +0,0 @@
export interface ShiftKey {
timesheet_id: number;
date: Date;
start_time: Date;
end_time: Date;
bank_code_id: number;
is_remote: boolean;
comment?: string | null;
}

View File

@ -1,11 +0,0 @@
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 };

View File

@ -1,4 +0,0 @@
export const PAY_PERIOD_SELECT = {
period_start: true,
period_end: true,
} as const;

View File

@ -1,12 +0,0 @@
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}];

View File

@ -1,22 +0,0 @@
import { EmployeeTimesheetResolver } from "./utils/resolve-timesheet.utils";
import { EmailToIdResolver } from "./utils/resolve-email-id.utils";
import { BankCodesResolver } from "./utils/resolve-bank-type-id.utils";
import { FullNameResolver } from "./utils/resolve-full-name.utils";
import { PrismaModule } from "src/prisma/prisma.module";
import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule],
providers: [
FullNameResolver,
EmailToIdResolver,
BankCodesResolver,
EmployeeTimesheetResolver,
],
exports: [
FullNameResolver,
EmailToIdResolver,
BankCodesResolver,
EmployeeTimesheetResolver,
],
}) export class SharedModule {}

View File

@ -4,7 +4,6 @@ import { BusinessLogicsModule } from "src/time-and-attendance/domains/business-l
import { ExpenseController } from "src/time-and-attendance/expenses/controllers/expense.controller";
import { ExpenseUpsertService } from "src/time-and-attendance/expenses/services/expense-upsert.service";
import { PayperiodsModule } from "src/time-and-attendance/pay-period/pay-periods.module";
import { SharedModule } from "src/time-and-attendance/shared/shared.module";
import { SchedulePresetsController } from "src/time-and-attendance/time-tracker/schedule-presets/controller/schedule-presets.controller";
import { SchedulePresetsApplyService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-apply.service";
import { SchedulePresetsGetService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-get.service";
@ -14,12 +13,13 @@ import { ShiftsGetService } from "src/time-and-attendance/time-tracker/shifts/se
import { ShiftsUpsertService } from "src/time-and-attendance/time-tracker/shifts/services/shifts-upsert.service";
import { TimesheetController } from "src/time-and-attendance/time-tracker/timesheets/controllers/timesheet.controller";
import { GetTimesheetsOverviewService } from "src/time-and-attendance/time-tracker/timesheets/services/timesheet-get-overview.service";
import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils";
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
@Module({
imports: [
BusinessLogicsModule,
PayperiodsModule,
SharedModule,
],
controllers: [
TimesheetController,
@ -35,6 +35,8 @@ import { GetTimesheetsOverviewService } from "src/time-and-attendance/time-track
SchedulePresetsUpsertService,
SchedulePresetsGetService,
SchedulePresetsApplyService,
EmailToIdResolver,
BankCodesResolver,
],
exports: [],
}) export class TimeAndAttendanceModule{};
}) export class TimeAndAttendanceModule { };

View File

@ -1,6 +1,5 @@
import { Module } from "@nestjs/common";
import { SharedModule } from "src/time-and-attendance/shared/shared.module";
import { SchedulePresetsController } from "src/time-and-attendance/time-tracker/schedule-presets/controller/schedule-presets.controller";
import { SchedulePresetsApplyService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-apply.service";
import { SchedulePresetsGetService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-get.service";
@ -8,7 +7,6 @@ import { SchedulePresetsUpsertService } from "src/time-and-attendance/time-track
@Module({
imports: [SharedModule],
controllers: [SchedulePresetsController],
providers: [
SchedulePresetsUpsertService,

View File

@ -4,7 +4,7 @@ import { DATE_ISO_FORMAT } from "src/time-and-attendance/utils/constants.utils";
import { PrismaService } from "src/prisma/prisma.service";
import { ApplyResult } from "src/time-and-attendance/utils/type.utils";
import { WEEKDAY } from "src/time-and-attendance/utils/mappers.utils";
import { EmailToIdResolver } from "src/time-and-attendance/shared/utils/resolve-email-id.utils";
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
@Injectable()

View File

@ -2,7 +2,7 @@ import { PresetResponse, ShiftResponse } from "src/time-and-attendance/utils/typ
import { PrismaService } from "src/prisma/prisma.service";
import { Injectable } from "@nestjs/common";
import { Prisma } from "@prisma/client";
import { EmailToIdResolver } from "src/time-and-attendance/shared/utils/resolve-email-id.utils";
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
@Injectable()
export class SchedulePresetsGetService {

View File

@ -3,9 +3,9 @@ import { CreatePresetResult, DeletePresetResult, UpdatePresetResult } from "src/
import { Prisma, Weekday } from "@prisma/client";
import { toHHmmFromDate } from "src/time-and-attendance/utils/date-time.utils";
import { PrismaService } from "src/prisma/prisma.service";
import { BankCodesResolver } from "src/time-and-attendance/shared/utils/resolve-bank-type-id.utils";
import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils";
import { SchedulePresetsDto } from "src/time-and-attendance/time-tracker/schedule-presets/dtos/create-schedule-presets.dto";
import { EmailToIdResolver } from "src/time-and-attendance/shared/utils/resolve-email-id.utils";
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
@Injectable()
export class SchedulePresetsUpsertService {

View File

@ -3,7 +3,7 @@ import { overlaps, toStringFromHHmm, toStringFromDate, toDateFromString, toHHmmF
import { Injectable, BadRequestException, ConflictException, NotFoundException } from "@nestjs/common";
import { OvertimeService } from "src/time-and-attendance/domains/services/overtime.service";
import { PrismaService } from "src/prisma/prisma.service";
import { EmailToIdResolver } from "src/time-and-attendance/shared/utils/resolve-email-id.utils";
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
import { ShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-create.dto";
import { GetShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-get.dto";
import { UpdateShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-update.dto";

View File

@ -3,7 +3,7 @@ import { NUMBER_OF_TIMESHEETS_TO_RETURN } from "src/time-and-attendance/utils/co
import { Injectable, NotFoundException } from "@nestjs/common";
import { TotalExpenses, TotalHours } from "src/time-and-attendance/utils/type.utils";
import { PrismaService } from "src/prisma/prisma.service";
import { EmailToIdResolver } from "src/time-and-attendance/shared/utils/resolve-email-id.utils";
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
@Injectable()
export class GetTimesheetsOverviewService {

View File

@ -1,12 +1,10 @@
import { Module } from '@nestjs/common';
import { SharedModule } from 'src/time-and-attendance/shared/shared.module';
import { TimesheetController } from 'src/time-and-attendance/time-tracker/timesheets/controllers/timesheet.controller';
import { TimesheetArchiveService } from 'src/time-and-attendance/time-tracker/timesheets/services/timesheet-archive.service';
import { GetTimesheetsOverviewService } from 'src/time-and-attendance/time-tracker/timesheets/services/timesheet-get-overview.service';
@Module({
imports: [SharedModule],
controllers: [TimesheetController],
providers: [
TimesheetArchiveService,

View File

@ -6,6 +6,8 @@ export const ANCHOR_ISO = '2023-12-17';
export const PERIOD_DAYS = 14;
export const PERIODS_PER_YEAR = 26;
export const MS_PER_DAY = 86_400_000;
export const MS_PER_WEEK = 7 * 24 * 60 * 60 * 1000;
//REGEX CONSTANTS
export const DATE_ISO_FORMAT = /^\d{4}-\d{2}-\d{2}$/;

View File

@ -1,3 +1,4 @@
import { BadRequestException } from "@nestjs/common";
import { ANCHOR_ISO, MS_PER_DAY, PERIODS_PER_YEAR, PERIOD_DAYS } from "src/time-and-attendance/utils/constants.utils";
//ensures the week starts from sunday
@ -89,3 +90,37 @@ export function listPayYear(pay_year: number, anchorISO = ANCHOR_ISO) {
export const overlaps = (a: { start: Date; end: Date }, b: { start: Date; end: Date }) =>
!(a.end <= b.start || a.start >= b.end);
export const hhmmFromLocal = (d: Date) =>
`${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`;
export const toDateOnly = (s: string): Date => {
if (/^\d{4}-\d{2}-\d{2}$/.test(s)) {
const y = Number(s.slice(0,4));
const m = Number(s.slice(5,7)) - 1;
const d = Number(s.slice(8,10));
return new Date(y, m, d, 0, 0, 0, 0);
}
const dt = new Date(s);
if (Number.isNaN(dt.getTime())) throw new BadRequestException(`Invalid date: ${s}`);
return new Date(dt.getFullYear(), dt.getMonth(), dt.getDate(), 0,0,0,0);
};
// export const toStringFromDate = (d: Date) =>
// `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
export const toISOtoDateOnly = (iso: string): Date => {
const date = new Date(iso);
if (Number.isNaN(date.getTime())) {
throw new BadRequestException(`Invalid date: ${iso}`);
}
date.setHours(0, 0, 0, 0);
return date;
};
export const toISODateKey = (date: Date): string => date.toISOString().slice(0, 10);
export const normalizeDates = (dates: string[]): string[] =>
Array.from(new Set(dates.map((iso) => toISODateKey(toISOtoDateOnly(iso)))));

View File

@ -1,7 +1,7 @@
import { Prisma, PrismaClient } from "@prisma/client";
import { NotFoundException } from "@nestjs/common";
import { PrismaService } from "src/prisma/prisma.service";
import { ShiftKey } from "../interfaces/shifts.interface";
import { ShiftKey } from "src/time-and-attendance/utils/type.utils";
type Tx = Prisma.TransactionClient | PrismaClient;

View File

@ -47,3 +47,34 @@ export const leaveRequestsSelect = {
}
},
} satisfies Prisma.LeaveRequestsSelect;
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 };
export const PAY_PERIOD_SELECT = {
period_start: true,
period_end: true,
} as const;
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}];

View File

@ -1,5 +1,4 @@
import { Prisma } from "@prisma/client";
import { WeekOvertimeSummary } from "src/time-and-attendance/domains/services/overtime.service";
import { Prisma, PrismaClient } from "@prisma/client";
import { GetExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-get.dto";
import { updateExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-update.dto";
import { SchedulePresetsDto } from "src/time-and-attendance/time-tracker/schedule-presets/dtos/create-schedule-presets.dto";
@ -81,3 +80,31 @@ export type ApplyResult = {
export type LeaveRequestRow = Prisma.LeaveRequestsGetPayload<{ select: typeof leaveRequestsSelect}>;
export type UpsertAction = 'create' | 'update' | 'delete';
export type Tx = Prisma.TransactionClient | PrismaClient;
export type WeekOvertimeSummary = {
week_start:string;
week_end: string;
week_total_hours: number;
weekly_overtime: number;
daily_overtime_kept: number;
total_overtime: number;
breakdown: Array<{
date:string;
day_hours: number;
day_overtime: number;
daily_kept: number;
running_total_before: number;
}>;
};
export interface ShiftKey {
timesheet_id: number;
date: Date;
start_time: Date;
end_time: Date;
bank_code_id: number;
is_remote: boolean;
comment?: string | null;
}