Merge branch 'dev/setup/modules/MatthieuH' of git.targo.ca:Targo/targo_backend
This commit is contained in:
commit
c336e024d5
|
|
@ -1536,29 +1536,6 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/exports/csv": {
|
||||
"get": {
|
||||
"operationId": "CsvExportController_exportCsv",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "period",
|
||||
"required": true,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"CsvExport"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/customers": {
|
||||
"post": {
|
||||
"operationId": "CustomersController_create",
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { ArchivalModule } from './modules/archival/archival.module';
|
|||
import { AuthenticationModule } from './modules/authentication/auth.module';
|
||||
import { BankCodesModule } from './modules/bank-codes/bank-codes.module';
|
||||
import { BusinessLogicsModule } from './modules/business-logics/business-logics.module';
|
||||
import { CsvExportModule } from './modules/exports/csv-exports.module';
|
||||
// import { CsvExportModule } from './modules/exports/csv-exports.module';
|
||||
import { CustomersModule } from './modules/customers/customers.module';
|
||||
import { EmployeesModule } from './modules/employees/employees.module';
|
||||
import { ExpensesModule } from './modules/expenses/expenses.module';
|
||||
|
|
@ -30,7 +30,7 @@ import { ConfigModule } from '@nestjs/config';
|
|||
BankCodesModule,
|
||||
BusinessLogicsModule,
|
||||
ConfigModule.forRoot({isGlobal: true}),
|
||||
CsvExportModule,
|
||||
// CsvExportModule,
|
||||
CustomersModule,
|
||||
EmployeesModule,
|
||||
ExpensesModule,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { Injectable, Logger, NotFoundException } from "@nestjs/common";
|
||||
import { PrismaService } from "../../../prisma/prisma.service";
|
||||
import { computeHours, getWeekStart } from "src/common/utils/date-utils";
|
||||
|
||||
|
|
@ -8,7 +8,23 @@ export class HolidayService {
|
|||
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
//switch employeeId for email
|
||||
//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;
|
||||
}
|
||||
|
||||
private async computeHoursPrevious4WeeksByEmail(email: string, holiday_date: Date): Promise<number> {
|
||||
const employee_id = await this.resolveEmployeeByEmail(email);
|
||||
return this.computeHoursPrevious4Weeks(employee_id, holiday_date)
|
||||
}
|
||||
|
||||
private async computeHoursPrevious4Weeks(employee_id: number, holiday_date: Date): Promise<number> {
|
||||
//sets the end of the window to 1ms before the week with the holiday
|
||||
const holiday_week_start = getWeekStart(holiday_date);
|
||||
|
|
@ -32,9 +48,8 @@ export class HolidayService {
|
|||
return daily_hours;
|
||||
}
|
||||
|
||||
//switch employeeId for email
|
||||
async calculateHolidayPay( employee_id: number, holiday_date: Date, modifier: number): Promise<number> {
|
||||
const hours = await this.computeHoursPrevious4Weeks(employee_id, holiday_date);
|
||||
async calculateHolidayPay( email: string, holiday_date: Date, modifier: number): Promise<number> {
|
||||
const hours = await this.computeHoursPrevious4WeeksByEmail(email, holiday_date);
|
||||
const daily_rate = Math.min(hours, 8);
|
||||
this.logger.debug(`Holiday pay calculation: hours= ${hours.toFixed(2)}`);
|
||||
return daily_rate * modifier;
|
||||
|
|
|
|||
|
|
@ -56,7 +56,6 @@ export class CreateCustomerDto {
|
|||
description: 'Customer`s phone number',
|
||||
})
|
||||
@IsString()
|
||||
@IsPositive()
|
||||
phone_number: string;
|
||||
|
||||
@ApiProperty({
|
||||
|
|
|
|||
|
|
@ -63,7 +63,6 @@ export class CreateEmployeeDto {
|
|||
description: 'Employee`s phone number',
|
||||
})
|
||||
@IsString()
|
||||
@IsPositive()
|
||||
phone_number: string;
|
||||
|
||||
@ApiProperty({
|
||||
|
|
|
|||
|
|
@ -17,6 +17,6 @@ export class UpdateEmployeeDto extends PartialType(CreateEmployeeDto) {
|
|||
@IsOptional()
|
||||
supervisor_id?: number;
|
||||
|
||||
@Max(2147483647)
|
||||
@IsOptional()
|
||||
phone_number: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { Controller, Get, Header, Query, UseGuards } from "@nestjs/common";
|
|||
import { RolesGuard } from "src/common/guards/roles.guard";
|
||||
import { Roles as RoleEnum } from '.prisma/client';
|
||||
import { CsvExportService } from "../services/csv-exports.service";
|
||||
import { ExportCompany, ExportCsvOptionsDto, ExportType } from "../dtos/export-csv-options.dto";
|
||||
// import { ExportCompany, ExportCsvOptionsDto, ExportType } from "../dtos/export-csv-options.dto";
|
||||
import { RolesAllowed } from "src/common/decorators/roles.decorators";
|
||||
|
||||
|
||||
|
|
@ -11,35 +11,34 @@ import { RolesAllowed } from "src/common/decorators/roles.decorators";
|
|||
export class CsvExportController {
|
||||
constructor(private readonly csvService: CsvExportService) {}
|
||||
|
||||
@Get('csv')
|
||||
@Header('Content-Type', 'text/csv; charset=utf-8')
|
||||
@Header('Content-Dispoition', 'attachment; filename="export.csv"')
|
||||
//@RolesAllowed(RoleEnum.ADMIN, RoleEnum.ACCOUNTING, RoleEnum.HR)
|
||||
async exportCsv(@Query() options: ExportCsvOptionsDto,
|
||||
@Query('period') periodId: string ): Promise<Buffer> {
|
||||
|
||||
//sets default values
|
||||
const companies = options.companies && options.companies.length ? options.companies :
|
||||
[ ExportCompany.TARGO, ExportCompany.SOLUCOM];
|
||||
const types = options.type && options.type.length ? options.type :
|
||||
Object.values(ExportType);
|
||||
// @Get('csv/:year/:period_no')
|
||||
// @Header('Content-Type', 'text/csv; charset=utf-8')
|
||||
// @Header('Content-Disposition', 'attachment; filename="export.csv"')
|
||||
// //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.ACCOUNTING, RoleEnum.HR)
|
||||
// async exportCsv(@Query() options: ExportCsvOptionsDto,
|
||||
// @Query('period') periodId: string ): Promise<Buffer> {
|
||||
// //modify to accept year and period_number
|
||||
// //sets default values
|
||||
// const companies = options.companies && options.companies.length ? options.companies :
|
||||
// [ ExportCompany.TARGO, ExportCompany.SOLUCOM];
|
||||
// const types = options.type && options.type.length ? options.type :
|
||||
// Object.values(ExportType);
|
||||
|
||||
//collects all
|
||||
const all = await this.csvService.collectTransaction(Number(periodId), companies);
|
||||
// //collects all
|
||||
// const all = await this.csvService.collectTransaction(Number(periodId), companies);
|
||||
|
||||
//filters by type
|
||||
const filtered = all.filter(r => {
|
||||
switch (r.bank_code.toLocaleLowerCase()) {
|
||||
case 'holiday' : return types.includes(ExportType.HOLIDAY);
|
||||
case 'vacation' : return types.includes(ExportType.VACATION);
|
||||
case 'sick-leave': return types.includes(ExportType.SICK_LEAVE);
|
||||
case 'expenses' : return types.includes(ExportType.EXPENSES);
|
||||
default : return types.includes(ExportType.SHIFTS);
|
||||
}
|
||||
});
|
||||
// //filters by type
|
||||
// const filtered = all.filter(row => {
|
||||
// switch (row.bank_code.toLocaleLowerCase()) {
|
||||
// case 'holiday' : return types.includes(ExportType.HOLIDAY);
|
||||
// case 'vacation' : return types.includes(ExportType.VACATION);
|
||||
// case 'expenses' : return types.includes(ExportType.EXPENSES);
|
||||
// default : return types.includes(ExportType.SHIFTS);
|
||||
// }
|
||||
// });
|
||||
|
||||
//generating the csv file
|
||||
return this.csvService.generateCsv(filtered);
|
||||
}
|
||||
// //generating the csv file
|
||||
// return this.csvService.generateCsv(filtered);
|
||||
// }
|
||||
|
||||
}
|
||||
|
|
@ -1,26 +1,47 @@
|
|||
import { IsArray, IsEnum, IsOptional } from "class-validator";
|
||||
import { Transform } from "class-transformer";
|
||||
import { IsBoolean, IsInt, IsOptional, Max, Min } from "class-validator";
|
||||
|
||||
export enum ExportType {
|
||||
SHIFTS = 'Quart de travail',
|
||||
EXPENSES = 'Depenses',
|
||||
HOLIDAY = 'Ferie',
|
||||
VACATION = 'Vacance',
|
||||
SICK_LEAVE = 'Absence'
|
||||
}
|
||||
|
||||
export enum ExportCompany {
|
||||
TARGO = 'Targo',
|
||||
SOLUCOM = 'Solucom',
|
||||
function toBoolean(v: any) {
|
||||
if(typeof v === 'boolean') return v;
|
||||
if(typeof v === 'string') return ['true', '1', 'on','yes'].includes(v.toLowerCase());
|
||||
return false;
|
||||
}
|
||||
|
||||
export class ExportCsvOptionsDto {
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsEnum(ExportCompany, { each: true })
|
||||
companies?: ExportCompany[];
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsEnum(ExportType, { each: true })
|
||||
type?: ExportType[];
|
||||
@Transform(({ value }) => parseInt(value,10))
|
||||
@IsInt() @Min(2023)
|
||||
year! : number;
|
||||
|
||||
@Transform(({ value }) => parseInt(value,10))
|
||||
@IsInt() @Min(1) @Max(26)
|
||||
period_no!: number;
|
||||
|
||||
@IsOptional() @IsBoolean()
|
||||
@Transform(({ value }) => toBoolean(value))
|
||||
approved? : boolean = true;
|
||||
|
||||
@IsOptional() @IsBoolean()
|
||||
@Transform(({ value }) => toBoolean(value))
|
||||
shifts? : boolean = true;
|
||||
|
||||
@IsOptional() @IsBoolean()
|
||||
@Transform(({ value }) => toBoolean(value))
|
||||
expenses? : boolean = true;
|
||||
|
||||
@IsOptional() @IsBoolean()
|
||||
@Transform(({ value }) => toBoolean(value))
|
||||
holiday? : boolean = true;
|
||||
|
||||
@IsOptional() @IsBoolean()
|
||||
@Transform(({ value }) => toBoolean(value))
|
||||
vacation? : boolean = true;
|
||||
|
||||
@IsOptional() @IsBoolean()
|
||||
@Transform(({ value }) => toBoolean(value))
|
||||
targo? : boolean = true;
|
||||
|
||||
@IsOptional() @IsBoolean()
|
||||
@Transform(({ value }) => toBoolean(value))
|
||||
solucom? : boolean = true;
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { ExportCompany } from "../dtos/export-csv-options.dto";
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
|
||||
|
||||
export interface CsvRow {
|
||||
company_code: number;
|
||||
|
|
@ -14,146 +13,203 @@ export interface CsvRow {
|
|||
holiday_date?: string;
|
||||
}
|
||||
|
||||
type Filters = {
|
||||
types: {
|
||||
shifts: boolean;
|
||||
expenses: boolean;
|
||||
holiday: boolean;
|
||||
vacation: boolean;
|
||||
};
|
||||
companies: {
|
||||
targo: boolean;
|
||||
solucom: boolean;
|
||||
};
|
||||
approved: boolean;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class CsvExportService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async collectTransaction( period_id: number, companies: ExportCompany[], approved: boolean = true):
|
||||
Promise<CsvRow[]> {
|
||||
// async collectTransaction(
|
||||
// year: number,
|
||||
// period_no: number,
|
||||
// filters: Filters,
|
||||
// approved: boolean = true
|
||||
// ): Promise<CsvRow[]> {
|
||||
//fetch period
|
||||
// const period = await this.prisma.payPeriods.findFirst({
|
||||
// where: { pay_year: year, pay_period_no: period_no },
|
||||
// select: { period_start: true, period_end: true },
|
||||
// });
|
||||
// if(!period) throw new NotFoundException(`Pay period ${ year }-${ period_no } not found`);
|
||||
|
||||
const company_codes = companies.map(c => c === ExportCompany.TARGO ? 1 : 2);
|
||||
// const start = period.period_start;
|
||||
// const end = period.period_end;
|
||||
|
||||
const period = await this.prisma.payPeriods.findFirst({
|
||||
where: { pay_period_no: period_id },
|
||||
});
|
||||
if(!period) throw new NotFoundException(`Pay period ${period_id} not found`);
|
||||
// //fetch company codes from .env
|
||||
// const comapany_codes = this.resolveCompanyCodes(filters.companies);
|
||||
// if(comapany_codes.length === 0) throw new BadRequestException('No company selected');
|
||||
|
||||
const start_date = period.period_start;
|
||||
const end_date = period.period_end;
|
||||
// //Flag types
|
||||
// const { shifts: want_shifts, expenses: want_expense, holiday: want_holiday, vacation: want_vacation } = filters.types;
|
||||
// if(!want_shifts && !want_expense && !want_holiday && !want_vacation) {
|
||||
// throw new BadRequestException(' No export type selected ');
|
||||
// }
|
||||
|
||||
const approved_filter = approved ? { is_approved: true } : {};
|
||||
// const approved_filter = filters.approved? { is_approved: true } : {};
|
||||
|
||||
//fetching shifts
|
||||
const shifts = await this.prisma.shifts.findMany({
|
||||
where: {
|
||||
date: { gte: start_date, lte: end_date },
|
||||
...approved_filter,
|
||||
timesheet: {
|
||||
employee: { company_code: { in: company_codes} } },
|
||||
},
|
||||
include: {
|
||||
bank_code: true,
|
||||
timesheet: { include: {
|
||||
employee: { include: {
|
||||
user:true,
|
||||
supervisor: { include: {
|
||||
user:true } } } } } },
|
||||
},
|
||||
});
|
||||
// //Prisma queries
|
||||
// const [shifts, expenses] = await Promise.all([
|
||||
// want_shifts || want_expense || want_holiday || want_vacation
|
||||
// ])
|
||||
|
||||
//fetching expenses
|
||||
const expenses = await this.prisma.expenses.findMany({
|
||||
where: {
|
||||
date: { gte: start_date, lte: end_date },
|
||||
...approved_filter,
|
||||
timesheet: { employee: { company_code: { in: company_codes} } },
|
||||
},
|
||||
include: { bank_code: true,
|
||||
timesheet: { include: {
|
||||
employee: { include: {
|
||||
user: true,
|
||||
supervisor: { include: {
|
||||
user:true } } } } } },
|
||||
},
|
||||
});
|
||||
|
||||
//fetching leave requests
|
||||
const leaves = await this.prisma.leaveRequests.findMany({
|
||||
where : {
|
||||
start_date_time: { gte: start_date, lte: end_date },
|
||||
employee: { company_code: { in: company_codes } },
|
||||
},
|
||||
include: {
|
||||
bank_code: true,
|
||||
employee: { include: {
|
||||
user: true,
|
||||
supervisor: { include: {
|
||||
user: true } } } },
|
||||
},
|
||||
});
|
||||
|
||||
const rows: CsvRow[] = [];
|
||||
|
||||
//Shifts Mapping
|
||||
for (const shift of shifts) {
|
||||
const emp = shift.timesheet.employee;
|
||||
const week_number = this.computeWeekNumber(start_date, shift.date);
|
||||
const hours = this.computeHours(shift.start_time, shift.end_time);
|
||||
|
||||
rows.push({
|
||||
company_code: emp.company_code,
|
||||
external_payroll_id: emp.external_payroll_id,
|
||||
full_name: `${emp.user.first_name} ${emp.user.last_name}`,
|
||||
bank_code: shift.bank_code.bank_code,
|
||||
quantity_hours: hours,
|
||||
amount: undefined,
|
||||
week_number,
|
||||
pay_date: this.formatDate(end_date),
|
||||
holiday_date: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
//Expenses Mapping
|
||||
for (const e of expenses) {
|
||||
const emp = e.timesheet.employee;
|
||||
const week_number = this.computeWeekNumber(start_date, e.date);
|
||||
|
||||
rows.push({
|
||||
company_code: emp.company_code,
|
||||
external_payroll_id: emp.external_payroll_id,
|
||||
full_name: `${emp.user.first_name} ${emp.user.last_name}`,
|
||||
bank_code: e.bank_code.bank_code,
|
||||
quantity_hours: undefined,
|
||||
amount: Number(e.amount),
|
||||
week_number,
|
||||
pay_date: this.formatDate(end_date),
|
||||
holiday_date: undefined,
|
||||
});
|
||||
}
|
||||
// const company_codes = companies.map(c => c === ExportCompany.TARGO ? 1 : 2);
|
||||
|
||||
//Leaves Mapping
|
||||
for(const l of leaves) {
|
||||
if(!l.bank_code) continue;
|
||||
const emp = l.employee;
|
||||
const start = l.start_date_time;
|
||||
const end = l.end_date_time ?? start;
|
||||
// const period = await this.prisma.payPeriods.findFirst({
|
||||
// where: { pay_period_no: period_id },
|
||||
// });
|
||||
// if(!period) throw new NotFoundException(`Pay period ${period_id} not found`);
|
||||
|
||||
const week_number = this.computeWeekNumber(start_date, start);
|
||||
const hours = this.computeHours(start, end);
|
||||
// const start_date = period.period_start;
|
||||
// const end_date = period.period_end;
|
||||
|
||||
rows.push({
|
||||
company_code: emp.company_code,
|
||||
external_payroll_id: emp.external_payroll_id,
|
||||
full_name: `${emp.user.first_name} ${emp.user.last_name}`,
|
||||
bank_code: l.bank_code.bank_code,
|
||||
quantity_hours: hours,
|
||||
amount: undefined,
|
||||
week_number,
|
||||
pay_date: this.formatDate(end_date),
|
||||
holiday_date: undefined,
|
||||
});
|
||||
}
|
||||
// const included_shifts = await this.prisma.shifts.findMany({
|
||||
// where: { }
|
||||
// })
|
||||
|
||||
// const approved_filter = approved ? { is_approved: true } : {};
|
||||
|
||||
// //fetching shifts
|
||||
// const shifts = await this.prisma.shifts.findMany({
|
||||
// where: {
|
||||
// date: { gte: start_date, lte: end_date },
|
||||
// ...approved_filter,
|
||||
// timesheet: {
|
||||
// employee: { company_code: { in: company_codes} } },
|
||||
// },
|
||||
// include: {
|
||||
// bank_code: true,
|
||||
// timesheet: { include: {
|
||||
// employee: { include: {
|
||||
// user:true,
|
||||
// supervisor: { include: {
|
||||
// user:true } } } } } },
|
||||
// },
|
||||
// });
|
||||
|
||||
// //fetching expenses
|
||||
// const expenses = await this.prisma.expenses.findMany({
|
||||
// where: {
|
||||
// date: { gte: start_date, lte: end_date },
|
||||
// ...approved_filter,
|
||||
// timesheet: { employee: { company_code: { in: company_codes} } },
|
||||
// },
|
||||
// include: { bank_code: true,
|
||||
// timesheet: { include: {
|
||||
// employee: { include: {
|
||||
// user: true,
|
||||
// supervisor: { include: {
|
||||
// user:true } } } } } },
|
||||
// },
|
||||
// });
|
||||
|
||||
// //fetching leave requests
|
||||
// const leaves = await this.prisma.leaveRequests.findMany({
|
||||
// where : {
|
||||
// start_date_time: { gte: start_date, lte: end_date },
|
||||
// employee: { company_code: { in: company_codes } },
|
||||
// },
|
||||
// include: {
|
||||
// bank_code: true,
|
||||
// employee: { include: {
|
||||
// user: true,
|
||||
// supervisor: { include: {
|
||||
// user: true } } } },
|
||||
// },
|
||||
// });
|
||||
|
||||
// const rows: CsvRow[] = [];
|
||||
|
||||
// //Shifts Mapping
|
||||
// for (const shift of shifts) {
|
||||
// const emp = shift.timesheet.employee;
|
||||
// const week_number = this.computeWeekNumber(start_date, shift.date);
|
||||
// const hours = this.computeHours(shift.start_time, shift.end_time);
|
||||
|
||||
// rows.push({
|
||||
// company_code: emp.company_code,
|
||||
// external_payroll_id: emp.external_payroll_id,
|
||||
// full_name: `${emp.user.first_name} ${emp.user.last_name}`,
|
||||
// bank_code: shift.bank_code.bank_code,
|
||||
// quantity_hours: hours,
|
||||
// amount: undefined,
|
||||
// week_number,
|
||||
// pay_date: this.formatDate(end_date),
|
||||
// holiday_date: undefined,
|
||||
// });
|
||||
// }
|
||||
|
||||
// //Expenses Mapping
|
||||
// for (const e of expenses) {
|
||||
// const emp = e.timesheet.employee;
|
||||
// const week_number = this.computeWeekNumber(start_date, e.date);
|
||||
|
||||
// rows.push({
|
||||
// company_code: emp.company_code,
|
||||
// external_payroll_id: emp.external_payroll_id,
|
||||
// full_name: `${emp.user.first_name} ${emp.user.last_name}`,
|
||||
// bank_code: e.bank_code.bank_code,
|
||||
// quantity_hours: undefined,
|
||||
// amount: Number(e.amount),
|
||||
// week_number,
|
||||
// pay_date: this.formatDate(end_date),
|
||||
// holiday_date: undefined,
|
||||
// });
|
||||
// }
|
||||
|
||||
// //Leaves Mapping
|
||||
// for(const l of leaves) {
|
||||
// if(!l.bank_code) continue;
|
||||
// const emp = l.employee;
|
||||
// const start = l.start_date_time;
|
||||
// const end = l.end_date_time ?? start;
|
||||
|
||||
// const week_number = this.computeWeekNumber(start_date, start);
|
||||
// const hours = this.computeHours(start, end);
|
||||
|
||||
// rows.push({
|
||||
// company_code: emp.company_code,
|
||||
// external_payroll_id: emp.external_payroll_id,
|
||||
// full_name: `${emp.user.first_name} ${emp.user.last_name}`,
|
||||
// bank_code: l.bank_code.bank_code,
|
||||
// quantity_hours: hours,
|
||||
// amount: undefined,
|
||||
// week_number,
|
||||
// pay_date: this.formatDate(end_date),
|
||||
// holiday_date: undefined,
|
||||
// });
|
||||
// }
|
||||
|
||||
//Final Mapping and sorts
|
||||
return rows.sort((a,b) => {
|
||||
if(a.external_payroll_id !== b.external_payroll_id) {
|
||||
return a.external_payroll_id - b.external_payroll_id;
|
||||
}
|
||||
if(a.bank_code !== b.bank_code) {
|
||||
return a.bank_code.localeCompare(b.bank_code);
|
||||
}
|
||||
return a.week_number - b.week_number;
|
||||
});
|
||||
// return rows.sort((a,b) => {
|
||||
// if(a.external_payroll_id !== b.external_payroll_id) {
|
||||
// return a.external_payroll_id - b.external_payroll_id;
|
||||
// }
|
||||
// if(a.bank_code !== b.bank_code) {
|
||||
// return a.bank_code.localeCompare(b.bank_code);
|
||||
// }
|
||||
// return a.week_number - b.week_number;
|
||||
// });
|
||||
// }
|
||||
resolveCompanyCodes(companies: { targo: boolean; solucom: boolean; }) {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
generateCsv(rows: CsvRow[]): Buffer {
|
||||
|
|
@ -172,7 +228,7 @@ export class CsvExportService {
|
|||
const body = rows.map(r => [
|
||||
r.company_code,
|
||||
r.external_payroll_id,
|
||||
`${r.full_name.replace(/"/g, '""')}"`,
|
||||
`${r.full_name.replace(/"/g, '""')}`,
|
||||
r.bank_code,
|
||||
r.quantity_hours?.toFixed(2) ?? '',
|
||||
r.week_number,
|
||||
|
|
|
|||
|
|
@ -36,8 +36,8 @@ export class TimesheetsController {
|
|||
@Query('period_no', ParseIntPipe ) period_no: number,
|
||||
@Query('email') email?: string
|
||||
): Promise<TimesheetPeriodDto> {
|
||||
if(!email || !email.trim()) throw new BadRequestException('Query param "email" is mandatory for this route.');
|
||||
return this.timesheetsQuery.findAll(year, period_no, email.trim());
|
||||
if(!email || !(email = email.trim())) throw new BadRequestException('Query param "email" is mandatory for this route.');
|
||||
return this.timesheetsQuery.findAll(year, period_no, email);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
|
|
|
|||
|
|
@ -11,6 +11,13 @@ export class ExpenseDto {
|
|||
|
||||
export type DayShiftsDto = ShiftDto[];
|
||||
|
||||
export class DetailedShifts {
|
||||
shifts: DayShiftsDto;
|
||||
total_hours: number;
|
||||
short_date: string;
|
||||
break_durations?: number;
|
||||
}
|
||||
|
||||
export class DayExpensesDto {
|
||||
cash: ExpenseDto[] = [];
|
||||
km : ExpenseDto[] = [];
|
||||
|
|
@ -20,13 +27,13 @@ export class DayExpensesDto {
|
|||
export class WeekDto {
|
||||
is_approved: boolean;
|
||||
shifts: {
|
||||
sun: DayShiftsDto;
|
||||
mon: DayShiftsDto;
|
||||
tue: DayShiftsDto;
|
||||
wed: DayShiftsDto;
|
||||
thu: DayShiftsDto;
|
||||
fri: DayShiftsDto;
|
||||
sat: DayShiftsDto;
|
||||
sun: DetailedShifts;
|
||||
mon: DetailedShifts;
|
||||
tue: DetailedShifts;
|
||||
wed: DetailedShifts;
|
||||
thu: DetailedShifts;
|
||||
fri: DetailedShifts;
|
||||
sat: DetailedShifts;
|
||||
}
|
||||
expenses: {
|
||||
sun: DayExpensesDto;
|
||||
|
|
|
|||
|
|
@ -5,14 +5,10 @@ import { Timesheets, TimesheetsArchive } from '@prisma/client';
|
|||
import { UpdateTimesheetDto } from '../dtos/update-timesheet.dto';
|
||||
import { OvertimeService } from 'src/modules/business-logics/services/overtime.service';
|
||||
import { computeHours } from 'src/common/utils/date-utils';
|
||||
import { buildPrismaWhere } from 'src/common/shared/build-prisma-where';
|
||||
import { SearchTimesheetDto } from '../dtos/search-timesheet.dto';
|
||||
import { TimesheetPeriodDto } from '../dtos/timesheet-period.dto';
|
||||
import { buildPeriod, endOfDayUTC, toUTCDateOnly } from '../utils/timesheet.helpers';
|
||||
import type { ShiftRow, ExpenseRow } from '../utils/timesheet.helpers';
|
||||
|
||||
// deprecated (used with old findAll) const ROUND_TO = 5;
|
||||
type ShiftRow = { date: Date; start_time: Date; end_time: Date; is_approved?: boolean };
|
||||
type ExpenseRow = { date: Date; amount: number; type: string; is_approved?: boolean };
|
||||
|
||||
@Injectable()
|
||||
export class TimesheetsQueryService {
|
||||
|
|
@ -21,8 +17,6 @@ export class TimesheetsQueryService {
|
|||
private readonly overtime: OvertimeService,
|
||||
) {}
|
||||
|
||||
|
||||
|
||||
async create(dto : CreateTimesheetDto): Promise<Timesheets> {
|
||||
const { employee_id, is_approved } = dto;
|
||||
return this.prisma.timesheets.create({
|
||||
|
|
@ -74,7 +68,7 @@ export class TimesheetsQueryService {
|
|||
}),
|
||||
]);
|
||||
|
||||
//Shift data mapping
|
||||
// data mapping
|
||||
const shifts: ShiftRow[] = raw_shifts.map(shift => ({
|
||||
date: shift.date,
|
||||
start_time: shift.start_time,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { DayExpensesDto, DayShiftsDto, ShiftDto, TimesheetPeriodDto, WeekDto } from "../dtos/timesheet-period.dto";
|
||||
import { DayExpensesDto, DayShiftsDto, 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 = 'sun'|'mon'|'tue'|'wed'|'thu'|'fri'|'sat';
|
||||
export type DayKey = typeof DAY_KEYS[number];
|
||||
|
||||
//DB line types
|
||||
type ShiftRow = { date: Date; start_time: Date; end_time: Date; is_approved?: boolean };
|
||||
type ExpenseRow = {date: Date; amount: number; type: string; is_approved?: boolean };
|
||||
export type ShiftRow = { date: Date; start_time: Date; end_time: Date; is_approved?: boolean };
|
||||
export type ExpenseRow = { date: Date; amount: number; type: string; is_approved?: boolean };
|
||||
|
||||
export function dayKeyFromDate(date: Date, useUTC = true): DayKey {
|
||||
const index = useUTC ? date.getUTCDay() : date.getDay(); // 0=Sunday..6=Saturday
|
||||
|
|
@ -14,6 +14,7 @@ export function dayKeyFromDate(date: Date, useUTC = true): DayKey {
|
|||
}
|
||||
|
||||
const MS_PER_DAY = 86_400_000;
|
||||
const MS_PER_HOUR = 3_600_000;
|
||||
|
||||
export function toUTCDateOnly(date: Date | string): Date {
|
||||
const d = new Date(date);
|
||||
|
|
@ -34,12 +35,6 @@ export function isBetweenUTC(date: Date, start: Date, end_inclusive: Date): bool
|
|||
return time >= start.getTime() && time <= end_inclusive.getTime();
|
||||
}
|
||||
|
||||
export function dayIndex(week_start: Date, date: Date): 1|2|3|4|5|6|7 {
|
||||
const diff = Math.floor((toUTCDateOnly(date).getTime() - toUTCDateOnly(week_start).getTime())/ MS_PER_DAY);
|
||||
const index = Math.min(6, Math.max(0, diff)) + 1;
|
||||
return index as 1|2|3|4|5|6|7;
|
||||
}
|
||||
|
||||
export function toTimeString(date: Date): string {
|
||||
const hours = String(date.getUTCHours()).padStart(2,'0');
|
||||
const minutes = String(date.getUTCMinutes()).padStart(2,'0');
|
||||
|
|
@ -50,21 +45,33 @@ export function round2(num: number) {
|
|||
return Math.round(num * 100) / 100;
|
||||
}
|
||||
|
||||
export function makeEmptyDayShifts(): DayShiftsDto { return []; }
|
||||
function shortDate(date:Date): string {
|
||||
const mm = String(date.getUTCMonth()+1).padStart(2,'0');
|
||||
const dd = String(date.getUTCDate()).padStart(2,'0');
|
||||
return `${mm}/${dd}`;
|
||||
}
|
||||
|
||||
// export function makeEmptyDayShifts(): DayShiftsDto { return []; }
|
||||
|
||||
export function makeEmptyDayExpenses(): DayExpensesDto { return {cash: [], km: []}; }
|
||||
|
||||
export function makeEmptyWeek(): WeekDto {
|
||||
export function makeEmptyWeek(week_start: Date): WeekDto {
|
||||
const make_empty_shifts = (offset: number): DetailedShifts => ({
|
||||
shifts: [],
|
||||
total_hours: 0,
|
||||
short_date: shortDate(addDays(week_start, offset)),
|
||||
break_durations: undefined,
|
||||
});
|
||||
return {
|
||||
is_approved: true,
|
||||
shifts: {
|
||||
sun: makeEmptyDayShifts(),
|
||||
mon: makeEmptyDayShifts(),
|
||||
tue: makeEmptyDayShifts(),
|
||||
wed: makeEmptyDayShifts(),
|
||||
thu: makeEmptyDayShifts(),
|
||||
fri: makeEmptyDayShifts(),
|
||||
sat: makeEmptyDayShifts(),
|
||||
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(),
|
||||
|
|
@ -79,7 +86,7 @@ export function makeEmptyWeek(): WeekDto {
|
|||
}
|
||||
|
||||
export function makeEmptyPeriod(): TimesheetPeriodDto {
|
||||
return { week1: makeEmptyWeek(), week2: makeEmptyWeek() };
|
||||
return { week1: makeEmptyWeek(new Date()), week2: makeEmptyWeek(new Date()) };
|
||||
}
|
||||
|
||||
//needs ajusting according to DB's data for expenses types
|
||||
|
|
@ -89,15 +96,27 @@ export function normalizeExpenseBucket(db_type: string): 'km' | 'cash' {
|
|||
return 'cash';
|
||||
}
|
||||
|
||||
export function buildWeek( week_start: Date, week_end: Date, shifts: ShiftRow[], expenses: ExpenseRow[]): WeekDto {
|
||||
const week = makeEmptyWeek();
|
||||
export function buildWeek(
|
||||
week_start: Date,
|
||||
week_end: Date,
|
||||
shifts: ShiftRow[],
|
||||
expenses: ExpenseRow[],
|
||||
): WeekDto {
|
||||
const week = makeEmptyWeek(week_start);
|
||||
let all_approved = true;
|
||||
|
||||
//array of shifts per day ( to check for break_gaps and calculate daily total hours )
|
||||
const dayTimes: Record<DayKey, Array<{start:Date; end: Date;}>> = {
|
||||
sun: [], mon: [], tue: [], wed: [],thu: [], fri: [], sat: [],
|
||||
};
|
||||
|
||||
|
||||
//Shifts mapped and filtered by dates
|
||||
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].push({
|
||||
dayTimes[key].push({start: shift.start_time, end:shift.end_time });
|
||||
week.shifts[key].shifts.push({
|
||||
start: toTimeString(shift.start_time),
|
||||
end : toTimeString(shift.end_time),
|
||||
is_approved: shift.is_approved ?? true,
|
||||
|
|
@ -117,11 +136,38 @@ export function buildWeek( week_start: Date, week_end: Date, shifts: ShiftRow[],
|
|||
});
|
||||
all_approved = all_approved && (expense.is_approved ?? true);
|
||||
}
|
||||
|
||||
for (const key of DAY_KEYS) {
|
||||
//sorts shifts in chronological order
|
||||
const times = dayTimes[key].sort((a,b) => a.start.getTime() - b.start.getTime());
|
||||
|
||||
//daily total hours
|
||||
const total = times.reduce((sum, time) => {
|
||||
const duration = (time.end.getTime() - time.start.getTime()) / MS_PER_HOUR;
|
||||
return sum + Math.max(0, duration);
|
||||
}, 0);
|
||||
week.shifts[key].total_hours = round2(total);
|
||||
|
||||
//break_duration
|
||||
if (times.length >= 2) {
|
||||
let break_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) break_gaps += gap;
|
||||
}
|
||||
if(break_gaps > 0) week.shifts[key].break_durations = round2(break_gaps);
|
||||
}
|
||||
}
|
||||
week.is_approved = all_approved;
|
||||
return week;
|
||||
}
|
||||
|
||||
export function buildPeriod( period_start: Date, period_end: Date, shifts: ShiftRow[], expenses: ExpenseRow[]): TimesheetPeriodDto {
|
||||
export function buildPeriod(
|
||||
period_start: Date,
|
||||
period_end: Date,
|
||||
shifts: ShiftRow[],
|
||||
expenses: ExpenseRow[]
|
||||
): TimesheetPeriodDto {
|
||||
const week1_start = toUTCDateOnly(period_start);
|
||||
const week1_end = endOfDayUTC(addDays(week1_start, 6));
|
||||
const week2_start = toUTCDateOnly(addDays(week1_start, 7));
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user