Merge branch 'main' of git.targo.ca:Targo/targo_backend into auth-session-fullstack
This commit is contained in:
commit
e575b9bd54
|
|
@ -125,46 +125,6 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/employees": {
|
|
||||||
"post": {
|
|
||||||
"operationId": "EmployeesController_create",
|
|
||||||
"parameters": [],
|
|
||||||
"requestBody": {
|
|
||||||
"required": true,
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/CreateEmployeeDto"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"responses": {
|
|
||||||
"201": {
|
|
||||||
"description": "Employee created",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/CreateEmployeeDto"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "Incomplete task or invalid data"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"access-token": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"summary": "Create employee",
|
|
||||||
"tags": [
|
|
||||||
"Employees"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/employees/employee-list": {
|
"/employees/employee-list": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "EmployeesController_findListEmployees",
|
"operationId": "EmployeesController_findListEmployees",
|
||||||
|
|
@ -1127,6 +1087,81 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"schemas": {
|
"schemas": {
|
||||||
|
"EmployeeListItemDto": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {}
|
||||||
|
},
|
||||||
|
"UpdateEmployeeDto": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "number",
|
||||||
|
"example": 1,
|
||||||
|
"description": "Unique ID of an employee(primary-key, auto-incremented)"
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "0e6e2e1f-b157-4c7c-ae3f-999b3e4f914d",
|
||||||
|
"description": "UUID of the user linked to that employee"
|
||||||
|
},
|
||||||
|
"first_name": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Frodo",
|
||||||
|
"description": "Employee`s first name"
|
||||||
|
},
|
||||||
|
"last_name": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Baggins",
|
||||||
|
"description": "Employee`s last name"
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "i_cant_do_this_sam@targointernet.com",
|
||||||
|
"description": "Employee`s email"
|
||||||
|
},
|
||||||
|
"phone_number": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "82538437464",
|
||||||
|
"description": "Employee`s phone number"
|
||||||
|
},
|
||||||
|
"residence": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "1 Bagshot Row, Hobbiton, The Shire, Middle-earth",
|
||||||
|
"description": "Employee`s residence"
|
||||||
|
},
|
||||||
|
"external_payroll_id": {
|
||||||
|
"type": "number",
|
||||||
|
"example": 7464,
|
||||||
|
"description": "external ID for the pay system"
|
||||||
|
},
|
||||||
|
"company_code": {
|
||||||
|
"type": "number",
|
||||||
|
"example": 335567447,
|
||||||
|
"description": "Employee`s company code"
|
||||||
|
},
|
||||||
|
"job_title": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "technicient",
|
||||||
|
"description": "employee`s job title"
|
||||||
|
},
|
||||||
|
"first_work_day": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string",
|
||||||
|
"example": "23/09/3018",
|
||||||
|
"description": "New hire date or undefined"
|
||||||
|
},
|
||||||
|
"last_work_day": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string",
|
||||||
|
"example": "25/03/3019",
|
||||||
|
"description": "Termination date (null to restore)"
|
||||||
|
},
|
||||||
|
"supervisor_id": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Supervisor ID"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"CreateEmployeeDto": {
|
"CreateEmployeeDto": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -1204,81 +1239,6 @@
|
||||||
"first_work_day"
|
"first_work_day"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"EmployeeListItemDto": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {}
|
|
||||||
},
|
|
||||||
"UpdateEmployeeDto": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"id": {
|
|
||||||
"type": "number",
|
|
||||||
"example": 1,
|
|
||||||
"description": "Unique ID of an employee(primary-key, auto-incremented)"
|
|
||||||
},
|
|
||||||
"user_id": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "0e6e2e1f-b157-4c7c-ae3f-999b3e4f914d",
|
|
||||||
"description": "UUID of the user linked to that employee"
|
|
||||||
},
|
|
||||||
"first_name": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "Frodo",
|
|
||||||
"description": "Employee`s first name"
|
|
||||||
},
|
|
||||||
"last_name": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "Baggins",
|
|
||||||
"description": "Employee`s last name"
|
|
||||||
},
|
|
||||||
"email": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "i_cant_do_this_sam@targointernet.com",
|
|
||||||
"description": "Employee`s email"
|
|
||||||
},
|
|
||||||
"phone_number": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "82538437464",
|
|
||||||
"description": "Employee`s phone number"
|
|
||||||
},
|
|
||||||
"residence": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "1 Bagshot Row, Hobbiton, The Shire, Middle-earth",
|
|
||||||
"description": "Employee`s residence"
|
|
||||||
},
|
|
||||||
"external_payroll_id": {
|
|
||||||
"type": "number",
|
|
||||||
"example": 7464,
|
|
||||||
"description": "external ID for the pay system"
|
|
||||||
},
|
|
||||||
"company_code": {
|
|
||||||
"type": "number",
|
|
||||||
"example": 335567447,
|
|
||||||
"description": "Employee`s company code"
|
|
||||||
},
|
|
||||||
"job_title": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "technicient",
|
|
||||||
"description": "employee`s job title"
|
|
||||||
},
|
|
||||||
"first_work_day": {
|
|
||||||
"format": "date-time",
|
|
||||||
"type": "string",
|
|
||||||
"example": "23/09/3018",
|
|
||||||
"description": "New hire date or undefined"
|
|
||||||
},
|
|
||||||
"last_work_day": {
|
|
||||||
"format": "date-time",
|
|
||||||
"type": "string",
|
|
||||||
"example": "25/03/3019",
|
|
||||||
"description": "Termination date (null to restore)"
|
|
||||||
},
|
|
||||||
"supervisor_id": {
|
|
||||||
"type": "number",
|
|
||||||
"description": "Supervisor ID"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"CreateWeekShiftsDto": {
|
"CreateWeekShiftsDto": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {}
|
"properties": {}
|
||||||
|
|
|
||||||
|
|
@ -2,20 +2,20 @@ import { Controller, Get, NotFoundException, Param, ParseIntPipe, UseGuards } fr
|
||||||
import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
|
import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
|
||||||
import { RolesAllowed } from "src/common/decorators/roles.decorators";
|
import { RolesAllowed } from "src/common/decorators/roles.decorators";
|
||||||
import { EmployeesArchive, Roles as RoleEnum } from '@prisma/client';
|
import { EmployeesArchive, Roles as RoleEnum } from '@prisma/client';
|
||||||
import { EmployeesService } from "src/modules/employees/services/employees.service";
|
import { EmployeesArchivalService } from "src/modules/employees/services/employees-archival.service";
|
||||||
|
|
||||||
@ApiTags('Employee Archives')
|
@ApiTags('Employee Archives')
|
||||||
// @UseGuards()
|
// @UseGuards()
|
||||||
@Controller('archives/employees')
|
@Controller('archives/employees')
|
||||||
export class EmployeesArchiveController {
|
export class EmployeesArchiveController {
|
||||||
constructor(private readonly employeesService: EmployeesService) {}
|
constructor(private readonly employeesArchiveService: EmployeesArchivalService) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
//@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
//@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||||
@ApiOperation({ summary: 'List of archived employees'})
|
@ApiOperation({ summary: 'List of archived employees'})
|
||||||
@ApiResponse({ status: 200, description: 'List of archived employees', isArray: true })
|
@ApiResponse({ status: 200, description: 'List of archived employees', isArray: true })
|
||||||
async findAllArchived(): Promise<EmployeesArchive[]> {
|
async findAllArchived(): Promise<EmployeesArchive[]> {
|
||||||
return this.employeesService.findAllArchived();
|
return this.employeesArchiveService.findAllArchived();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
|
|
@ -24,7 +24,7 @@ export class EmployeesArchiveController {
|
||||||
@ApiResponse({ status: 200, description: 'Archived employee found'})
|
@ApiResponse({ status: 200, description: 'Archived employee found'})
|
||||||
async findOneArchived(@Param('id', ParseIntPipe) id: number ): Promise<EmployeesArchive> {
|
async findOneArchived(@Param('id', ParseIntPipe) id: number ): Promise<EmployeesArchive> {
|
||||||
try{
|
try{
|
||||||
return await this.employeesService.findOneArchived(id);
|
return await this.employeesArchiveService.findOneArchived(id);
|
||||||
}catch {
|
}catch {
|
||||||
throw new NotFoundException(`Archived employee #${id} not found`);
|
throw new NotFoundException(`Archived employee #${id} not found`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,13 @@ import { UseGuards, Controller, Get, Param, ParseIntPipe, NotFoundException } fr
|
||||||
import { ApiTags, ApiOperation, ApiResponse } from "@nestjs/swagger";
|
import { ApiTags, ApiOperation, ApiResponse } from "@nestjs/swagger";
|
||||||
import { ExpensesArchive,Roles as RoleEnum } from "@prisma/client";
|
import { ExpensesArchive,Roles as RoleEnum } from "@prisma/client";
|
||||||
import { RolesAllowed } from "src/common/decorators/roles.decorators";
|
import { RolesAllowed } from "src/common/decorators/roles.decorators";
|
||||||
import { ExpensesQueryService } from "src/modules/expenses/services/expenses-query.service";
|
import { ExpensesArchivalService } from "src/modules/expenses/services/expenses-archival.service";
|
||||||
|
|
||||||
@ApiTags('Expense Archives')
|
@ApiTags('Expense Archives')
|
||||||
// @UseGuards()
|
// @UseGuards()
|
||||||
@Controller('archives/expenses')
|
@Controller('archives/expenses')
|
||||||
export class ExpensesArchiveController {
|
export class ExpensesArchiveController {
|
||||||
constructor(private readonly expensesService: ExpensesQueryService) {}
|
constructor(private readonly expensesService: ExpensesArchivalService) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
//@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
//@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,13 @@ import { Get, Param, ParseIntPipe, NotFoundException, Controller, UseGuards } fr
|
||||||
import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
|
import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
|
||||||
import { ShiftsArchive, Roles as RoleEnum } from "@prisma/client";
|
import { ShiftsArchive, Roles as RoleEnum } from "@prisma/client";
|
||||||
import { RolesAllowed } from "src/common/decorators/roles.decorators";
|
import { RolesAllowed } from "src/common/decorators/roles.decorators";
|
||||||
import { ShiftsQueryService } from "src/modules/shifts/services/shifts-query.service";
|
import { ShiftsArchivalService } from "src/modules/shifts/services/shifts-archival.service";
|
||||||
|
|
||||||
@ApiTags('Shift Archives')
|
@ApiTags('Shift Archives')
|
||||||
// @UseGuards()
|
// @UseGuards()
|
||||||
@Controller('archives/shifts')
|
@Controller('archives/shifts')
|
||||||
export class ShiftsArchiveController {
|
export class ShiftsArchiveController {
|
||||||
constructor(private readonly shiftsService:ShiftsQueryService) {}
|
constructor(private readonly shiftsService: ShiftsArchivalService) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
//@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
//@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,13 @@ import { Controller, Get, NotFoundException, Param, ParseIntPipe, UseGuards } fr
|
||||||
import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
|
import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
|
||||||
import { RolesAllowed } from "src/common/decorators/roles.decorators";
|
import { RolesAllowed } from "src/common/decorators/roles.decorators";
|
||||||
import { TimesheetsArchive, Roles as RoleEnum } from '@prisma/client';
|
import { TimesheetsArchive, Roles as RoleEnum } from '@prisma/client';
|
||||||
import { TimesheetsQueryService } from "src/modules/timesheets/services/timesheets-query.service";
|
import { TimesheetArchiveService } from "src/modules/timesheets/services/timesheet-archive.service";
|
||||||
|
|
||||||
@ApiTags('Timesheet Archives')
|
@ApiTags('Timesheet Archives')
|
||||||
// @UseGuards()
|
// @UseGuards()
|
||||||
@Controller('archives/timesheets')
|
@Controller('archives/timesheets')
|
||||||
export class TimesheetsArchiveController {
|
export class TimesheetsArchiveController {
|
||||||
constructor(private readonly timesheetsService: TimesheetsQueryService) {}
|
constructor(private readonly timesheetsService: TimesheetArchiveService) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
//@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
//@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
import { Injectable, Logger } from "@nestjs/common";
|
import { Injectable, Logger } from "@nestjs/common";
|
||||||
import { Cron } from "@nestjs/schedule";
|
import { Cron } from "@nestjs/schedule";
|
||||||
import { ExpensesQueryService } from "src/modules/expenses/services/expenses-query.service";
|
import { ExpensesArchivalService } from "src/modules/expenses/services/expenses-archival.service";
|
||||||
import { ShiftsQueryService } from "src/modules/shifts/services/shifts-query.service";
|
import { ShiftsArchivalService } from "src/modules/shifts/services/shifts-archival.service";
|
||||||
import { TimesheetsQueryService } from "src/modules/timesheets/services/timesheets-query.service";
|
import { TimesheetArchiveService } from "src/modules/timesheets/services/timesheet-archive.service";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ArchivalService {
|
export class ArchivalService {
|
||||||
private readonly logger = new Logger(ArchivalService.name);
|
private readonly logger = new Logger(ArchivalService.name);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly timesheetsService: TimesheetsQueryService,
|
private readonly timesheetsService: TimesheetArchiveService,
|
||||||
private readonly expensesService: ExpensesQueryService,
|
private readonly expensesService: ExpensesArchivalService,
|
||||||
private readonly shiftsService: ShiftsQueryService,
|
private readonly shiftsService: ShiftsArchivalService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Cron('0 0 3 * * 1', {timeZone:'America/Toronto'}) // chaque premier lundi du mois à 03h00
|
@Cron('0 0 3 * * 1', {timeZone:'America/Toronto'}) // chaque premier lundi du mois à 03h00
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import {
|
||||||
Matches,
|
Matches,
|
||||||
MaxLength,
|
MaxLength,
|
||||||
Min,
|
Min,
|
||||||
ValidateIf,
|
ValidateIf,
|
||||||
ValidateNested
|
ValidateNested
|
||||||
} from "class-validator";
|
} from "class-validator";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ export class ExpensesDto {
|
||||||
bank_type: string;
|
bank_type: string;
|
||||||
date: string;
|
date: string;
|
||||||
amount: number;
|
amount: number;
|
||||||
|
mileage: number;
|
||||||
km: number;
|
km: number;
|
||||||
comment: string;
|
comment: string;
|
||||||
supervisor_comment: string;
|
supervisor_comment: string;
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,22 @@
|
||||||
export class ShiftDto {
|
export class ShiftDto {
|
||||||
date: string;
|
date: string;
|
||||||
type: string;
|
type: string;
|
||||||
start_time: string;
|
start_time: string;
|
||||||
end_time : string;
|
end_time : string;
|
||||||
comment: string;
|
comment: string;
|
||||||
is_approved: boolean;
|
is_approved: boolean;
|
||||||
is_remote: boolean;
|
is_remote: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ExpenseDto {
|
export class ExpenseDto {
|
||||||
amount: number;
|
type: string;
|
||||||
comment: string;
|
amount: number;
|
||||||
supervisor_comment: string;
|
mileage: number;
|
||||||
|
comment: string;
|
||||||
total_mileage: number;
|
total_mileage: number;
|
||||||
total_expense: number;
|
total_expense: number;
|
||||||
is_approved: boolean;
|
is_approved: boolean;
|
||||||
|
supervisor_comment: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DayShiftsDto = ShiftDto[];
|
export type DayShiftsDto = ShiftDto[];
|
||||||
|
|
@ -31,9 +33,10 @@ export class DetailedShifts {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DayExpensesDto {
|
export class DayExpensesDto {
|
||||||
cash: ExpenseDto[] = [];
|
expense: ExpenseDto[] = [];
|
||||||
km : ExpenseDto[] = [];
|
mileage: ExpenseDto[] = [];
|
||||||
[otherType:string]: ExpenseDto[] | any;
|
per_diem: ExpenseDto[] = [];
|
||||||
|
on_call: ExpenseDto[] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class WeekDto {
|
export class WeekDto {
|
||||||
|
|
@ -62,3 +65,5 @@ export class TimesheetPeriodDto {
|
||||||
weeks: WeekDto[];
|
weeks: WeekDto[];
|
||||||
employee_full_name: string;
|
employee_full_name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
56
src/modules/timesheets/mappers/timesheet.mappers.ts
Normal file
56
src/modules/timesheets/mappers/timesheet.mappers.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
import { DayExpensesDto, WeekDto, DetailedShifts, TimesheetPeriodDto } from "../dtos/timesheet-period.dto";
|
||||||
|
import { ExpensesAmount } from "../types/timesheet.types";
|
||||||
|
import { addDays, shortDate } from "../utils/timesheet.helpers";
|
||||||
|
|
||||||
|
// Factories
|
||||||
|
export function makeEmptyDayExpenses(): DayExpensesDto {
|
||||||
|
return {
|
||||||
|
expense: [],
|
||||||
|
mileage: [],
|
||||||
|
per_diem: [],
|
||||||
|
on_call: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeEmptyWeek(week_start: Date): WeekDto {
|
||||||
|
const make_empty_shifts = (offset: number): DetailedShifts => ({
|
||||||
|
shifts: [],
|
||||||
|
regular_hours: 0,
|
||||||
|
evening_hours: 0,
|
||||||
|
emergency_hours: 0,
|
||||||
|
overtime_hours: 0,
|
||||||
|
comment: '',
|
||||||
|
short_date: shortDate(addDays(week_start, offset)),
|
||||||
|
break_durations: 0,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
is_approved: true,
|
||||||
|
shifts: {
|
||||||
|
sun: make_empty_shifts(0),
|
||||||
|
mon: make_empty_shifts(1),
|
||||||
|
tue: make_empty_shifts(2),
|
||||||
|
wed: make_empty_shifts(3),
|
||||||
|
thu: make_empty_shifts(4),
|
||||||
|
fri: make_empty_shifts(5),
|
||||||
|
sat: make_empty_shifts(6),
|
||||||
|
},
|
||||||
|
expenses: {
|
||||||
|
sun: makeEmptyDayExpenses(),
|
||||||
|
mon: makeEmptyDayExpenses(),
|
||||||
|
tue: makeEmptyDayExpenses(),
|
||||||
|
wed: makeEmptyDayExpenses(),
|
||||||
|
thu: makeEmptyDayExpenses(),
|
||||||
|
fri: makeEmptyDayExpenses(),
|
||||||
|
sat: makeEmptyDayExpenses(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeEmptyPeriod(): TimesheetPeriodDto {
|
||||||
|
return { weeks: [makeEmptyWeek(new Date()), makeEmptyWeek(new Date())], employee_full_name: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const makeAmounts = (): ExpensesAmount => ({
|
||||||
|
expense: 0,
|
||||||
|
mileage: 0,
|
||||||
|
});
|
||||||
|
|
@ -47,8 +47,9 @@ export class TimesheetsCommandService extends BaseApprovalService<Timesheets>{
|
||||||
return timesheet;
|
return timesheet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//_____________________________________________________________________________________________
|
||||||
|
//
|
||||||
|
//_____________________________________________________________________________________________
|
||||||
|
|
||||||
async createWeekShiftsAndReturnOverview(
|
async createWeekShiftsAndReturnOverview(
|
||||||
email:string,
|
email:string,
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import { buildPeriod, endOfDayUTC, toHHmm, toUTCDateOnly } from '../utils/timesheet.helpers';
|
import { endOfDayUTC, toHHmm, toUTCDateOnly } from '../utils/timesheet.helpers';
|
||||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
import { formatDateISO, getWeekEnd, getWeekStart } from 'src/common/utils/date-utils';
|
import { formatDateISO, getWeekEnd, getWeekStart } from 'src/common/utils/date-utils';
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
import { OvertimeService } from 'src/modules/business-logics/services/overtime.service';
|
import { OvertimeService } from 'src/modules/business-logics/services/overtime.service';
|
||||||
import { TimesheetDto } from '../dtos/overview-timesheet.dto';
|
import { TimesheetDto } from '../dtos/overview-timesheet.dto';
|
||||||
import { TimesheetPeriodDto } from '../dtos/timesheet-period.dto';
|
import { TimesheetPeriodDto } from '../dtos/timesheet-period.dto';
|
||||||
import type { ShiftRow, ExpenseRow } from '../utils/timesheet.helpers';
|
import { ShiftRow, ExpenseRow } from '../types/timesheet.types';
|
||||||
|
import { buildPeriod } from '../utils/timesheet.utils';
|
||||||
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|
@ -18,7 +19,9 @@ export class TimesheetsQueryService {
|
||||||
async findAll(year: number, period_no: number, email: string): Promise<TimesheetPeriodDto> {
|
async findAll(year: number, period_no: number, email: string): Promise<TimesheetPeriodDto> {
|
||||||
//finds the employee
|
//finds the employee
|
||||||
const employee = await this.prisma.employees.findFirst({
|
const employee = await this.prisma.employees.findFirst({
|
||||||
where: { user: { is: { email } } },
|
where: {
|
||||||
|
user: { is: { email } }
|
||||||
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
user_id: true,
|
user_id: true,
|
||||||
|
|
@ -38,8 +41,14 @@ export class TimesheetsQueryService {
|
||||||
|
|
||||||
//finds the period
|
//finds the period
|
||||||
const period = await this.prisma.payPeriods.findFirst({
|
const period = await this.prisma.payPeriods.findFirst({
|
||||||
where: { pay_year: year, pay_period_no: period_no },
|
where: {
|
||||||
select: { period_start: true, period_end: true },
|
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`);
|
if(!period) throw new NotFoundException(`Period ${year}-${period_no} not found`);
|
||||||
|
|
||||||
|
|
@ -52,12 +61,12 @@ export class TimesheetsQueryService {
|
||||||
date: { gte: from, lte: to },
|
date: { gte: from, lte: to },
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
date: true,
|
date: true,
|
||||||
start_time: true,
|
start_time: true,
|
||||||
end_time: true,
|
end_time: true,
|
||||||
comment: true,
|
comment: true,
|
||||||
is_approved: true,
|
is_approved: true,
|
||||||
is_remote: true,
|
is_remote: true,
|
||||||
bank_code: { select: { type: true } },
|
bank_code: { select: { type: true } },
|
||||||
},
|
},
|
||||||
orderBy:[ { date:'asc'}, { start_time: 'asc'} ],
|
orderBy:[ { date:'asc'}, { start_time: 'asc'} ],
|
||||||
|
|
@ -69,15 +78,16 @@ export class TimesheetsQueryService {
|
||||||
date: { gte: from, lte: to },
|
date: { gte: from, lte: to },
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
date: true,
|
date: true,
|
||||||
amount: true,
|
amount: true,
|
||||||
comment: true,
|
mileage: true,
|
||||||
supervisor_comment: true,
|
comment: true,
|
||||||
is_approved: true,
|
is_approved: true,
|
||||||
|
supervisor_comment: true,
|
||||||
bank_code: { select: { type: true } },
|
bank_code: { select: { type: true } },
|
||||||
},
|
},
|
||||||
orderBy: { date: 'asc' },
|
orderBy: { date: 'asc' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const toNum = (value: any) =>
|
const toNum = (value: any) =>
|
||||||
value && typeof value.toNumber === 'function' ? value.toNumber() :
|
value && typeof value.toNumber === 'function' ? value.toNumber() :
|
||||||
|
|
@ -86,22 +96,23 @@ export class TimesheetsQueryService {
|
||||||
|
|
||||||
// data mapping
|
// data mapping
|
||||||
const shifts: ShiftRow[] = raw_shifts.map(shift => ({
|
const shifts: ShiftRow[] = raw_shifts.map(shift => ({
|
||||||
date: shift.date,
|
date: shift.date,
|
||||||
start_time: shift.start_time,
|
start_time: shift.start_time,
|
||||||
end_time: shift.end_time,
|
end_time: shift.end_time,
|
||||||
comment: shift.comment ?? '',
|
comment: shift.comment ?? '',
|
||||||
is_approved: shift.is_approved ?? true,
|
is_approved: shift.is_approved ?? true,
|
||||||
is_remote: shift.is_remote ?? true,
|
is_remote: shift.is_remote ?? true,
|
||||||
type: String(shift.bank_code?.type ?? '').toUpperCase(),
|
type: String(shift.bank_code?.type ?? '').toUpperCase(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const expenses: ExpenseRow[] = raw_expenses.map(expense => ({
|
const expenses: ExpenseRow[] = raw_expenses.map(expense => ({
|
||||||
date: expense.date,
|
type: String(expense.bank_code?.type ?? '').toUpperCase(),
|
||||||
amount: toNum(expense.amount),
|
date: expense.date,
|
||||||
comment: expense.comment ?? '',
|
amount: toNum(expense.amount),
|
||||||
supervisor_comment: expense.supervisor_comment ?? '',
|
mileage: toNum(expense.mileage),
|
||||||
|
comment: expense.comment ?? '',
|
||||||
is_approved: expense.is_approved ?? true,
|
is_approved: expense.is_approved ?? true,
|
||||||
type: String(expense.bank_code?.type ?? '').toUpperCase(),
|
supervisor_comment: expense.supervisor_comment ?? '',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return buildPeriod(period.period_start, period.period_end, shifts , expenses, employee_full_name);
|
return buildPeriod(period.period_start, period.period_end, shifts , expenses, employee_full_name);
|
||||||
|
|
@ -111,27 +122,27 @@ export class TimesheetsQueryService {
|
||||||
|
|
||||||
//fetch user related to email
|
//fetch user related to email
|
||||||
const user = await this.prisma.users.findUnique({
|
const user = await this.prisma.users.findUnique({
|
||||||
where: { email },
|
where: { email },
|
||||||
select: { id: true },
|
select: { id: true },
|
||||||
});
|
});
|
||||||
if(!user) throw new NotFoundException(`user with email ${email} not found`);
|
if(!user) throw new NotFoundException(`user with email ${email} not found`);
|
||||||
|
|
||||||
//fetch employee_id matching the email
|
//fetch employee_id matching the email
|
||||||
const employee = await this.prisma.employees.findFirst({
|
const employee = await this.prisma.employees.findFirst({
|
||||||
where: { user_id: user.id },
|
where: { user_id: user.id },
|
||||||
select: { id: true },
|
select: { id: true },
|
||||||
});
|
});
|
||||||
if(!employee) throw new NotFoundException(`Employee with email: ${email} not found`);
|
if(!employee) throw new NotFoundException(`Employee with email: ${email} not found`);
|
||||||
|
|
||||||
//sets current week Sunday -> Saturday
|
//sets current week Sunday -> Saturday
|
||||||
const base = new Date();
|
const base = new Date();
|
||||||
const offset = new Date(base);
|
const offset = new Date(base);
|
||||||
offset.setDate(offset.getDate() + (week_offset * 7));
|
offset.setDate(offset.getDate() + (week_offset * 7));
|
||||||
|
|
||||||
const start_date_week = getWeekStart(offset, 0);
|
const start_date_week = getWeekStart(offset, 0);
|
||||||
const end_date_week = getWeekEnd(start_date_week);
|
const end_date_week = getWeekEnd(start_date_week);
|
||||||
const start_day = formatDateISO(start_date_week);
|
const start_day = formatDateISO(start_date_week);
|
||||||
const end_day = formatDateISO(end_date_week);
|
const end_day = formatDateISO(end_date_week);
|
||||||
|
|
||||||
//build the label MM/DD/YYYY.MM/DD.YYYY
|
//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 mm_dd = (date: Date) => `${String(date.getFullYear())}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2,'0')}`;
|
||||||
|
|
@ -173,7 +184,7 @@ export class TimesheetsQueryService {
|
||||||
|
|
||||||
|
|
||||||
//maps all shifts of selected timesheet
|
//maps all shifts of selected timesheet
|
||||||
const shifts = timesheet.shift.map((shift_row) => ({
|
const shifts = timesheet.shift.map((shift_row) => ({
|
||||||
bank_type: shift_row.bank_code?.type ?? '',
|
bank_type: shift_row.bank_code?.type ?? '',
|
||||||
date: formatDateISO(shift_row.date),
|
date: formatDateISO(shift_row.date),
|
||||||
start_time: toHHmm(shift_row.start_time),
|
start_time: toHHmm(shift_row.start_time),
|
||||||
|
|
@ -188,9 +199,9 @@ export class TimesheetsQueryService {
|
||||||
bank_type: exp.bank_code?.type ?? '',
|
bank_type: exp.bank_code?.type ?? '',
|
||||||
date: formatDateISO(exp.date),
|
date: formatDateISO(exp.date),
|
||||||
amount: Number(exp.amount) || 0,
|
amount: Number(exp.amount) || 0,
|
||||||
|
mileage: exp.mileage != null ? Number(exp.mileage) : 0,
|
||||||
comment: exp.comment ?? '',
|
comment: exp.comment ?? '',
|
||||||
is_approved: exp.is_approved ?? false,
|
is_approved: exp.is_approved ?? false,
|
||||||
km: 0,
|
|
||||||
supervisor_comment: exp.supervisor_comment ?? '',
|
supervisor_comment: exp.supervisor_comment ?? '',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
|
||||||
73
src/modules/timesheets/types/timesheet.types.ts
Normal file
73
src/modules/timesheets/types/timesheet.types.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
export type ShiftRow = {
|
||||||
|
date: Date;
|
||||||
|
start_time: Date;
|
||||||
|
end_time: Date;
|
||||||
|
comment: string;
|
||||||
|
is_approved?: boolean;
|
||||||
|
is_remote: boolean;
|
||||||
|
type: string
|
||||||
|
};
|
||||||
|
export type ExpenseRow = {
|
||||||
|
date: Date;
|
||||||
|
amount: number;
|
||||||
|
mileage?: number | null;
|
||||||
|
comment: string;
|
||||||
|
type: string;
|
||||||
|
is_approved?: boolean;
|
||||||
|
supervisor_comment: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
//Date & Format
|
||||||
|
export const MS_PER_DAY = 86_400_000;
|
||||||
|
export const MS_PER_HOUR = 3_600_000;
|
||||||
|
|
||||||
|
// Types
|
||||||
|
export const SHIFT_TYPES = {
|
||||||
|
REGULAR: 'REGULAR',
|
||||||
|
EVENING: 'EVENING',
|
||||||
|
OVERTIME: 'OVERTIME',
|
||||||
|
EMERGENCY: 'EMERGENCY',
|
||||||
|
HOLIDAY: 'HOLIDAY',
|
||||||
|
VACATION: 'VACATION',
|
||||||
|
SICK: 'SICK',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const EXPENSE_TYPES = {
|
||||||
|
MILEAGE: 'MILEAGE',
|
||||||
|
EXPENSE: 'EXPENSES',
|
||||||
|
PER_DIEM: 'PER_DIEM',
|
||||||
|
ON_CALL: 'ON_CALL',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
//makes the strings indexes for arrays
|
||||||
|
export const DAY_KEYS = ['sun','mon','tue','wed','thu','fri','sat'] as const;
|
||||||
|
export type DayKey = typeof DAY_KEYS[number];
|
||||||
|
|
||||||
|
export const EXPENSE_BUCKETS = ['expense', 'mileage'] as const;
|
||||||
|
export type ExpenseBucketKey = typeof EXPENSE_BUCKETS[number];
|
||||||
|
|
||||||
|
|
||||||
|
//shifts's hour by type
|
||||||
|
export type ShiftsHours = {
|
||||||
|
regular: number;
|
||||||
|
evening: number;
|
||||||
|
overtime: number;
|
||||||
|
emergency: number;
|
||||||
|
sick: number;
|
||||||
|
vacation: number;
|
||||||
|
holiday: number;
|
||||||
|
};
|
||||||
|
export const make_hours = (): ShiftsHours => ({
|
||||||
|
regular: 0,
|
||||||
|
evening: 0,
|
||||||
|
overtime: 0,
|
||||||
|
emergency: 0,
|
||||||
|
sick: 0,
|
||||||
|
vacation: 0,
|
||||||
|
holiday: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ExpensesAmount = {
|
||||||
|
expense: number;
|
||||||
|
mileage: number;
|
||||||
|
};
|
||||||
|
|
@ -1,71 +1,6 @@
|
||||||
import { toDateString } from "src/modules/pay-periods/utils/pay-year.util";
|
import { MS_PER_DAY, DayKey, DAY_KEYS } from "../types/timesheet.types";
|
||||||
import { DayExpensesDto, DetailedShifts, ShiftDto, TimesheetPeriodDto, WeekDto } from "../dtos/timesheet-period.dto";
|
|
||||||
|
|
||||||
//makes the strings indexes for arrays
|
|
||||||
export const DAY_KEYS = ['sun','mon','tue','wed','thu','fri','sat'] as const;
|
|
||||||
export type DayKey = typeof DAY_KEYS[number];
|
|
||||||
|
|
||||||
export function dayKeyFromDate(date: Date, useUTC = true): DayKey {
|
|
||||||
const index = useUTC ? date.getUTCDay() : date.getDay(); // 0=Sunday..6=Saturday
|
|
||||||
return DAY_KEYS[index];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const toHHmm = (date: Date) => date.toISOString().slice(11, 16);
|
|
||||||
|
|
||||||
//create shifts within timesheet's week - employee overview functions
|
|
||||||
export function parseISODate(iso: string): Date {
|
|
||||||
const [ y, m, d ] = iso.split('-').map(Number);
|
|
||||||
return new Date(y, (m ?? 1) - 1, d ?? 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseHHmm(t: string): Date {
|
|
||||||
const [ hh, mm ] = t.split(':').map(Number);
|
|
||||||
return new Date(1970, 0, 1, hh || 0, mm || 0, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
//Date & Format
|
|
||||||
const MS_PER_DAY = 86_400_000;
|
|
||||||
const MS_PER_HOUR = 3_600_000;
|
|
||||||
|
|
||||||
// Types
|
|
||||||
const SHIFT_TYPES = {
|
|
||||||
REGULAR: 'REGULAR',
|
|
||||||
EVENING: 'EVENING',
|
|
||||||
OVERTIME: 'OVERTIME',
|
|
||||||
EMERGENCY: 'EMERGENCY',
|
|
||||||
HOLIDAY: 'HOLIDAY',
|
|
||||||
VACATION: 'VACATION',
|
|
||||||
SICK: 'SICK',
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
const EXPENSE_TYPES = {
|
|
||||||
MILEAGE: 'MILEAGE',
|
|
||||||
EXPENSE: 'EXPENSES',
|
|
||||||
PER_DIEM: 'PER_DIEM',
|
|
||||||
COMMISSION: 'COMMISSION',
|
|
||||||
PRIME_DISPO: 'PRIME_DISPO',
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
//DB line types
|
|
||||||
export type ShiftRow = {
|
|
||||||
date: Date;
|
|
||||||
start_time: Date;
|
|
||||||
end_time: Date;
|
|
||||||
comment: string;
|
|
||||||
is_approved?: boolean;
|
|
||||||
is_remote: boolean;
|
|
||||||
type: string
|
|
||||||
};
|
|
||||||
export type ExpenseRow = {
|
|
||||||
date: Date;
|
|
||||||
amount: number;
|
|
||||||
comment: string;
|
|
||||||
supervisor_comment: string;
|
|
||||||
is_approved?: boolean;
|
|
||||||
type: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
//helper functions
|
|
||||||
export function toUTCDateOnly(date: Date | string): Date {
|
export function toUTCDateOnly(date: Date | string): Date {
|
||||||
const d = new Date(date);
|
const d = new Date(date);
|
||||||
return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
|
return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
|
||||||
|
|
@ -95,240 +30,27 @@ export function round2(num: number) {
|
||||||
return Math.round(num * 100) / 100;
|
return Math.round(num * 100) / 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function shortDate(date:Date): string {
|
||||||
function shortDate(date:Date): string {
|
|
||||||
const mm = String(date.getUTCMonth()+1).padStart(2,'0');
|
const mm = String(date.getUTCMonth()+1).padStart(2,'0');
|
||||||
const dd = String(date.getUTCDate()).padStart(2,'0');
|
const dd = String(date.getUTCDate()).padStart(2,'0');
|
||||||
return `${mm}/${dd}`;
|
return `${mm}/${dd}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Factories
|
export function dayKeyFromDate(date: Date, useUTC = true): DayKey {
|
||||||
export function makeEmptyDayExpenses(): DayExpensesDto { return {cash: [], km: []}; }
|
const index = useUTC ? date.getUTCDay() : date.getDay(); // 0=Sunday..6=Saturday
|
||||||
|
return DAY_KEYS[index];
|
||||||
export function makeEmptyWeek(week_start: Date): WeekDto {
|
|
||||||
const make_empty_shifts = (offset: number): DetailedShifts => ({
|
|
||||||
shifts: [],
|
|
||||||
regular_hours: 0,
|
|
||||||
evening_hours: 0,
|
|
||||||
emergency_hours: 0,
|
|
||||||
overtime_hours: 0,
|
|
||||||
comment: '',
|
|
||||||
short_date: shortDate(addDays(week_start, offset)),
|
|
||||||
break_durations: 0,
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
is_approved: true,
|
|
||||||
shifts: {
|
|
||||||
sun: make_empty_shifts(0),
|
|
||||||
mon: make_empty_shifts(1),
|
|
||||||
tue: make_empty_shifts(2),
|
|
||||||
wed: make_empty_shifts(3),
|
|
||||||
thu: make_empty_shifts(4),
|
|
||||||
fri: make_empty_shifts(5),
|
|
||||||
sat: make_empty_shifts(6),
|
|
||||||
},
|
|
||||||
expenses: {
|
|
||||||
sun: makeEmptyDayExpenses(),
|
|
||||||
mon: makeEmptyDayExpenses(),
|
|
||||||
tue: makeEmptyDayExpenses(),
|
|
||||||
wed: makeEmptyDayExpenses(),
|
|
||||||
thu: makeEmptyDayExpenses(),
|
|
||||||
fri: makeEmptyDayExpenses(),
|
|
||||||
sat: makeEmptyDayExpenses(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeEmptyPeriod(): TimesheetPeriodDto {
|
export const toHHmm = (date: Date) => date.toISOString().slice(11, 16);
|
||||||
return { weeks: [ makeEmptyWeek(new Date()), makeEmptyWeek(new Date())], employee_full_name: " " };
|
|
||||||
|
//create shifts within timesheet's week - employee overview functions
|
||||||
|
export function parseISODate(iso: string): Date {
|
||||||
|
const [ y, m, d ] = iso.split('-').map(Number);
|
||||||
|
return new Date(y, (m ?? 1) - 1, d ?? 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildWeek(
|
export function parseHHmm(t: string): Date {
|
||||||
week_start: Date,
|
const [ hh, mm ] = t.split(':').map(Number);
|
||||||
week_end: Date,
|
return new Date(1970, 0, 1, hh || 0, mm || 0, 0, 0);
|
||||||
shifts: ShiftRow[],
|
|
||||||
expenses: ExpenseRow[],
|
|
||||||
): WeekDto {
|
|
||||||
const week = makeEmptyWeek(week_start);
|
|
||||||
let all_approved = true;
|
|
||||||
|
|
||||||
//breaks
|
|
||||||
const day_times: Record<DayKey, Array<{ start: Date; end: Date }>> = DAY_KEYS.reduce((acc, key) => {
|
|
||||||
acc[key] = []; return acc;
|
|
||||||
}, {} as Record<DayKey, Array<{ start: Date; end: Date}>>);
|
|
||||||
|
|
||||||
//shifts's hour by type
|
|
||||||
type ShiftsHours = {
|
|
||||||
regular: number;
|
|
||||||
evening: number;
|
|
||||||
overtime: number;
|
|
||||||
emergency: number;
|
|
||||||
sick: number;
|
|
||||||
vacation: number;
|
|
||||||
holiday: number;
|
|
||||||
};
|
|
||||||
const make_hours = (): ShiftsHours => ({
|
|
||||||
regular: 0,
|
|
||||||
evening: 0,
|
|
||||||
overtime: 0,
|
|
||||||
emergency: 0,
|
|
||||||
sick: 0,
|
|
||||||
vacation: 0,
|
|
||||||
holiday: 0
|
|
||||||
});
|
|
||||||
const day_hours: Record<DayKey, ShiftsHours> = DAY_KEYS.reduce((acc, key) => {
|
|
||||||
acc[key] = make_hours(); return acc;
|
|
||||||
}, {} as Record<DayKey, ShiftsHours>);
|
|
||||||
|
|
||||||
//expenses's amount by type
|
|
||||||
type ExpensesAmount = {
|
|
||||||
mileage: number;
|
|
||||||
expense: number;
|
|
||||||
per_diem: number;
|
|
||||||
commission: number;
|
|
||||||
prime_dispo: number
|
|
||||||
};
|
|
||||||
|
|
||||||
const make_amounts = (): ExpensesAmount => ({
|
|
||||||
mileage: 0,
|
|
||||||
expense: 0,
|
|
||||||
per_diem: 0,
|
|
||||||
commission: 0,
|
|
||||||
prime_dispo: 0
|
|
||||||
});
|
|
||||||
const day_amounts: Record<DayKey, ExpensesAmount> = DAY_KEYS.reduce((acc, key) => {
|
|
||||||
acc[key] = make_amounts(); return acc;
|
|
||||||
}, {} as Record<DayKey, ExpensesAmount>);
|
|
||||||
|
|
||||||
const dayExpenseRows: Record<DayKey, { km: ExpenseRow[]; cash: ExpenseRow[] }> = DAY_KEYS.reduce((acc, key) => {
|
|
||||||
acc[key] = {km: [], cash: [] }; return acc;
|
|
||||||
}, {} as Record<DayKey, { km: ExpenseRow[], cash: ExpenseRow[] }>);
|
|
||||||
|
|
||||||
//regroup hours per type of shifts
|
|
||||||
const week_shifts = shifts.filter(shift => isBetweenUTC(shift.date, week_start, week_end));
|
|
||||||
for (const shift of week_shifts) {
|
|
||||||
const key = dayKeyFromDate(shift.date, true);
|
|
||||||
week.shifts[key].shifts.push({
|
|
||||||
date: toDateString(shift.date),
|
|
||||||
type: shift.type,
|
|
||||||
start_time: toTimeString(shift.start_time),
|
|
||||||
end_time: toTimeString(shift.end_time),
|
|
||||||
comment: shift.comment,
|
|
||||||
is_approved: shift.is_approved ?? true,
|
|
||||||
is_remote: shift.is_remote,
|
|
||||||
} as ShiftDto);
|
|
||||||
|
|
||||||
day_times[key].push({ start: shift.start_time, end: shift.end_time});
|
|
||||||
|
|
||||||
const duration = Math.max(0, (shift.end_time.getTime() - shift.start_time.getTime())/ MS_PER_HOUR);
|
|
||||||
const type = (shift.type || '').toUpperCase();
|
|
||||||
|
|
||||||
if ( type === SHIFT_TYPES.REGULAR) day_hours[key].regular += duration;
|
|
||||||
else if( type === SHIFT_TYPES.EVENING) day_hours[key].evening += duration;
|
|
||||||
else if( type === SHIFT_TYPES.EMERGENCY) day_hours[key].emergency += duration;
|
|
||||||
else if( type === SHIFT_TYPES.OVERTIME) day_hours[key].overtime += duration;
|
|
||||||
else if( type === SHIFT_TYPES.SICK) day_hours[key].sick += duration;
|
|
||||||
else if( type === SHIFT_TYPES.VACATION) day_hours[key].vacation += duration;
|
|
||||||
else if( type === SHIFT_TYPES.HOLIDAY) day_hours[key].holiday += duration;
|
|
||||||
|
|
||||||
all_approved = all_approved && (shift.is_approved ?? true );
|
|
||||||
}
|
|
||||||
|
|
||||||
//regroupe amounts to type of expenses
|
|
||||||
const week_expenses = expenses.filter(expense => isBetweenUTC(expense.date, week_start, week_end));
|
|
||||||
for (const expense of week_expenses) {
|
|
||||||
const key = dayKeyFromDate(expense.date, true);
|
|
||||||
const type = (expense.type || '').toUpperCase();
|
|
||||||
|
|
||||||
if (type === EXPENSE_TYPES.MILEAGE) {
|
|
||||||
day_amounts[key].mileage += expense.amount;
|
|
||||||
dayExpenseRows[key].km.push(expense);
|
|
||||||
} else if (type === EXPENSE_TYPES.EXPENSE) {
|
|
||||||
day_amounts[key].expense += expense.amount;
|
|
||||||
dayExpenseRows[key].cash.push(expense)
|
|
||||||
} else if (type === EXPENSE_TYPES.PER_DIEM) {
|
|
||||||
day_amounts[key].per_diem += expense.amount;
|
|
||||||
dayExpenseRows[key].cash.push(expense)
|
|
||||||
} else if (type === EXPENSE_TYPES.COMMISSION) {
|
|
||||||
day_amounts[key].commission += expense.amount;
|
|
||||||
dayExpenseRows[key].cash.push(expense)
|
|
||||||
} else if (type === EXPENSE_TYPES.PRIME_DISPO) {
|
|
||||||
day_amounts[key].prime_dispo += expense.amount;
|
|
||||||
dayExpenseRows[key].cash.push(expense)
|
|
||||||
}
|
|
||||||
all_approved = all_approved && (expense.is_approved ?? true );
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
for (const key of DAY_KEYS) {
|
|
||||||
//return exposed dto data
|
|
||||||
week.shifts[key].regular_hours = round2(day_hours[key].regular);
|
|
||||||
week.shifts[key].evening_hours = round2(day_hours[key].evening);
|
|
||||||
week.shifts[key].overtime_hours = round2(day_hours[key].overtime);
|
|
||||||
week.shifts[key].emergency_hours = round2(day_hours[key].emergency);
|
|
||||||
|
|
||||||
//calculate gaps between shifts
|
|
||||||
const times = day_times[key].sort((a,b) => a.start.getTime() - b.start.getTime());
|
|
||||||
let gaps = 0;
|
|
||||||
for (let i = 1; i < times.length; i++) {
|
|
||||||
const gap = (times[i].start.getTime() - times[i - 1].end.getTime()) / MS_PER_HOUR;
|
|
||||||
if(gap > 0) gaps += gap;
|
|
||||||
}
|
|
||||||
week.shifts[key].break_durations = round2(gaps);
|
|
||||||
|
|
||||||
//daily totals
|
|
||||||
const totals = day_amounts[key];
|
|
||||||
const total_mileage = totals.mileage;
|
|
||||||
const total_expense = totals.expense + totals.per_diem + totals.commission + totals.prime_dispo;
|
|
||||||
|
|
||||||
//pushing mileage rows
|
|
||||||
for(const row of dayExpenseRows[key].km) {
|
|
||||||
week.expenses[key].km.push({
|
|
||||||
amount: round2(row.amount),
|
|
||||||
comment: row.comment,
|
|
||||||
supervisor_comment: row.supervisor_comment,
|
|
||||||
total_mileage: round2(total_mileage),
|
|
||||||
total_expense: round2(total_expense),
|
|
||||||
is_approved: row.is_approved ?? true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//pushing expense rows
|
|
||||||
for(const row of dayExpenseRows[key].cash) {
|
|
||||||
week.expenses[key].cash.push({
|
|
||||||
amount: round2(row.amount),
|
|
||||||
comment: row.comment,
|
|
||||||
supervisor_comment: row.supervisor_comment,
|
|
||||||
total_mileage: round2(total_mileage),
|
|
||||||
total_expense: round2(total_expense),
|
|
||||||
is_approved: row.is_approved ?? true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
week.is_approved = all_approved;
|
|
||||||
return week;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildPeriod(
|
|
||||||
period_start: Date,
|
|
||||||
period_end: Date,
|
|
||||||
shifts: ShiftRow[],
|
|
||||||
expenses: ExpenseRow[],
|
|
||||||
employee_full_name: string,
|
|
||||||
): TimesheetPeriodDto {
|
|
||||||
const week1_start = toUTCDateOnly(period_start);
|
|
||||||
const week1_end = endOfDayUTC(addDays(week1_start, 6));
|
|
||||||
const week2_start = toUTCDateOnly(addDays(week1_start, 7));
|
|
||||||
const week2_end = endOfDayUTC(period_end);
|
|
||||||
|
|
||||||
return {
|
|
||||||
weeks: [
|
|
||||||
buildWeek(week1_start, week1_end, shifts, expenses),
|
|
||||||
buildWeek(week2_start, week2_end, shifts, expenses),
|
|
||||||
],
|
|
||||||
employee_full_name,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
151
src/modules/timesheets/utils/timesheet.utils.ts
Normal file
151
src/modules/timesheets/utils/timesheet.utils.ts
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
import { DayKey, DAY_KEYS, EXPENSE_BUCKETS, EXPENSE_TYPES, ExpenseBucketKey, ExpenseRow, MS_PER_HOUR, SHIFT_TYPES, ShiftRow, make_hours, ShiftsHours, ExpensesAmount } from "../types/timesheet.types";
|
||||||
|
import { isBetweenUTC, dayKeyFromDate, toTimeString, round2, toUTCDateOnly, endOfDayUTC, addDays } from "./timesheet.helpers";
|
||||||
|
import { WeekDto, ShiftDto, ExpenseDto, TimesheetPeriodDto } from "../dtos/timesheet-period.dto";
|
||||||
|
import { makeAmounts, makeEmptyWeek } from "../mappers/timesheet.mappers";
|
||||||
|
import { toDateString } from "src/modules/pay-periods/utils/pay-year.util";
|
||||||
|
|
||||||
|
export function buildWeek(
|
||||||
|
week_start: Date,
|
||||||
|
week_end: Date,
|
||||||
|
shifts: ShiftRow[],
|
||||||
|
expenses: ExpenseRow[],
|
||||||
|
): WeekDto {
|
||||||
|
const week = makeEmptyWeek(week_start);
|
||||||
|
let all_approved = true;
|
||||||
|
|
||||||
|
const day_times: Record<DayKey, Array<{ start: Date; end: Date }>> = DAY_KEYS.reduce((acc, key) => {
|
||||||
|
acc[key] = []; return acc;
|
||||||
|
}, {} as Record<DayKey, Array<{ start: Date; end: Date}>>);
|
||||||
|
|
||||||
|
const day_hours: Record<DayKey, ShiftsHours> = DAY_KEYS.reduce((acc, key) => {
|
||||||
|
acc[key] = make_hours(); return acc;
|
||||||
|
}, {} as Record<DayKey, ShiftsHours>);
|
||||||
|
|
||||||
|
const day_amounts: Record<DayKey, ExpensesAmount> = DAY_KEYS.reduce((acc, key) => {
|
||||||
|
acc[key] = makeAmounts(); return acc;
|
||||||
|
}, {} as Record<DayKey, ExpensesAmount>);
|
||||||
|
|
||||||
|
const day_expense_rows: Record<DayKey, Record<ExpenseBucketKey, ExpenseRow[]>> = DAY_KEYS.reduce((acc, key) => {
|
||||||
|
acc[key] = {
|
||||||
|
expense: [],
|
||||||
|
mileage: [],
|
||||||
|
};
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<DayKey, Record<ExpenseBucketKey, ExpenseRow[]>>);
|
||||||
|
|
||||||
|
//regroup hours per type of shifts
|
||||||
|
const week_shifts = shifts.filter(shift => isBetweenUTC(shift.date, week_start, week_end));
|
||||||
|
for (const shift of week_shifts) {
|
||||||
|
const key = dayKeyFromDate(shift.date, true);
|
||||||
|
week.shifts[key].shifts.push({
|
||||||
|
date: toDateString(shift.date),
|
||||||
|
type: shift.type,
|
||||||
|
start_time: toTimeString(shift.start_time),
|
||||||
|
end_time: toTimeString(shift.end_time),
|
||||||
|
comment: shift.comment,
|
||||||
|
is_approved: shift.is_approved ?? true,
|
||||||
|
is_remote: shift.is_remote,
|
||||||
|
} as ShiftDto);
|
||||||
|
|
||||||
|
day_times[key].push({ start: shift.start_time, end: shift.end_time});
|
||||||
|
|
||||||
|
const duration = Math.max(0, (shift.end_time.getTime() - shift.start_time.getTime())/ MS_PER_HOUR);
|
||||||
|
const type = (shift.type || '').toUpperCase();
|
||||||
|
|
||||||
|
if ( type === SHIFT_TYPES.REGULAR) day_hours[key].regular += duration;
|
||||||
|
else if( type === SHIFT_TYPES.EVENING) day_hours[key].evening += duration;
|
||||||
|
else if( type === SHIFT_TYPES.EMERGENCY) day_hours[key].emergency += duration;
|
||||||
|
else if( type === SHIFT_TYPES.OVERTIME) day_hours[key].overtime += duration;
|
||||||
|
else if( type === SHIFT_TYPES.SICK) day_hours[key].sick += duration;
|
||||||
|
else if( type === SHIFT_TYPES.VACATION) day_hours[key].vacation += duration;
|
||||||
|
else if( type === SHIFT_TYPES.HOLIDAY) day_hours[key].holiday += duration;
|
||||||
|
|
||||||
|
all_approved = all_approved && (shift.is_approved ?? true );
|
||||||
|
}
|
||||||
|
|
||||||
|
//regroupe amounts to type of expenses
|
||||||
|
const week_expenses = expenses.filter(expense => isBetweenUTC(expense.date, week_start, week_end));
|
||||||
|
for (const expense of week_expenses) {
|
||||||
|
const key = dayKeyFromDate(expense.date, true);
|
||||||
|
const type = (expense.type || '').toUpperCase();
|
||||||
|
|
||||||
|
let bucket: ExpenseBucketKey;
|
||||||
|
|
||||||
|
if(type === EXPENSE_TYPES.MILEAGE) {
|
||||||
|
bucket = 'mileage';
|
||||||
|
day_amounts[key].mileage += expense.mileage ?? 0;
|
||||||
|
} else {
|
||||||
|
bucket = 'expense';
|
||||||
|
day_amounts[key].expense += expense.amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
day_expense_rows[key][bucket].push(expense);
|
||||||
|
all_approved = all_approved && (expense.is_approved ?? true );
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of DAY_KEYS) {
|
||||||
|
//return exposed dto data
|
||||||
|
week.shifts[key].regular_hours = round2(day_hours[key].regular);
|
||||||
|
week.shifts[key].evening_hours = round2(day_hours[key].evening);
|
||||||
|
week.shifts[key].overtime_hours = round2(day_hours[key].overtime);
|
||||||
|
week.shifts[key].emergency_hours = round2(day_hours[key].emergency);
|
||||||
|
|
||||||
|
//calculate gaps between shifts
|
||||||
|
const times = day_times[key].sort((a,b) => a.start.getTime() - b.start.getTime());
|
||||||
|
let gaps = 0;
|
||||||
|
for (let i = 1; i < times.length; i++) {
|
||||||
|
const gap = (times[i].start.getTime() - times[i - 1].end.getTime()) / MS_PER_HOUR;
|
||||||
|
if(gap > 0) gaps += gap;
|
||||||
|
}
|
||||||
|
week.shifts[key].break_durations = round2(gaps);
|
||||||
|
|
||||||
|
//daily totals
|
||||||
|
const totals = day_amounts[key];
|
||||||
|
const total_mileage = round2(totals.mileage);
|
||||||
|
const total_expense = round2(totals.expense);
|
||||||
|
|
||||||
|
const target_buckets = week.expenses[key] as Record<ExpenseBucketKey, ExpenseDto[]>;
|
||||||
|
const source_buckets = day_expense_rows[key];
|
||||||
|
|
||||||
|
for (const bucket of EXPENSE_BUCKETS) {
|
||||||
|
for (const row of source_buckets[bucket]) {
|
||||||
|
target_buckets[bucket].push({
|
||||||
|
type: (row.type || '').toUpperCase(),
|
||||||
|
amount: round2(row.amount),
|
||||||
|
mileage: round2(row.mileage ?? 0),
|
||||||
|
comment: row.comment,
|
||||||
|
is_approved: row.is_approved ?? true,
|
||||||
|
supervisor_comment: row.supervisor_comment,
|
||||||
|
total_mileage,
|
||||||
|
total_expense,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
week.is_approved = all_approved;
|
||||||
|
return week;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildPeriod(
|
||||||
|
period_start: Date,
|
||||||
|
period_end: Date,
|
||||||
|
shifts: ShiftRow[],
|
||||||
|
expenses: ExpenseRow[],
|
||||||
|
employeeFullName = ''
|
||||||
|
): TimesheetPeriodDto {
|
||||||
|
const week1_start = toUTCDateOnly(period_start);
|
||||||
|
const week1_end = endOfDayUTC(addDays(week1_start, 6));
|
||||||
|
const week2_start = toUTCDateOnly(addDays(week1_start, 7));
|
||||||
|
const week2_end = endOfDayUTC(period_end);
|
||||||
|
|
||||||
|
const weeks: WeekDto[] = [
|
||||||
|
buildWeek(week1_start, week1_end, shifts, expenses),
|
||||||
|
buildWeek(week2_start, week2_end, shifts, expenses),
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
weeks,
|
||||||
|
employee_full_name: employeeFullName,
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user