Merge branch 'main' of git.targo.ca:Targo/targo_backend
This commit is contained in:
commit
4b39240606
|
|
@ -1,10 +1,21 @@
|
||||||
import { Controller } from "@nestjs/common";
|
import { Body, Controller, Param, ParseIntPipe, Post } from "@nestjs/common";
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
import { ExpenseDto } from "../dtos/expense.dto";
|
||||||
|
import { CreateResult, ExpenseUpsertService } from "../services/expense-upsert.service";
|
||||||
|
|
||||||
|
|
||||||
@Controller('expense')
|
@Controller('expense')
|
||||||
export class ExpenseController {
|
export class ExpenseController {
|
||||||
constructor(private readonly prisma: PrismaService){}
|
constructor(
|
||||||
|
private readonly prisma: PrismaService,
|
||||||
|
private readonly upsert_service: ExpenseUpsertService,
|
||||||
|
){}
|
||||||
|
|
||||||
|
|
||||||
|
// @Post(':timesheet_id')
|
||||||
|
// create(
|
||||||
|
// @Param('timesheet_id', ParseIntPipe) timesheet_id: number,
|
||||||
|
// @Body() dto: ExpenseDto): Promise<CreateResult>{
|
||||||
|
// return this.upsert_service.createExpense(timesheet_id, dto);
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { IsBoolean, IsInt, IsOptional, IsString, MaxLength } from "class-validator";
|
||||||
|
|
||||||
|
export class ExpenseDto {
|
||||||
|
@IsInt() bank_code_id!: number;
|
||||||
|
@IsInt() timesheet_id!: number;
|
||||||
|
@IsString() @IsOptional() attachment?: string;
|
||||||
|
|
||||||
|
@IsString() date!: string;
|
||||||
|
@IsInt() @IsOptional() amount?: number;
|
||||||
|
@IsInt() @IsOptional() mileage?: number;
|
||||||
|
@IsString() @MaxLength(280) comment!: string;
|
||||||
|
@IsBoolean() is_approved!: boolean;
|
||||||
|
@IsString() @MaxLength(280) @IsOptional() supervisor_comment?: string
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
export class GetExpenseDto {
|
||||||
|
timesheet_id: number;
|
||||||
|
bank_code_id: number;
|
||||||
|
attachment?: string;
|
||||||
|
date: string;
|
||||||
|
comment: string;
|
||||||
|
mileage?: number;
|
||||||
|
amount?: number;
|
||||||
|
supervisor_comment?: string;
|
||||||
|
is_approved: boolean;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { OmitType, PartialType } from "@nestjs/swagger";
|
||||||
|
import { ExpenseDto } from "./expense.dto";
|
||||||
|
|
||||||
|
export class updateExpenseDto extends PartialType (
|
||||||
|
OmitType(ExpenseDto, ['is_approved', 'timesheet_id'] as const)
|
||||||
|
){}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export const toDateFromString = (ymd: string): Date => {
|
||||||
|
return new Date(`${ymd}T00:00:00:000Z`);
|
||||||
|
}
|
||||||
46
src/modules/expenses/services/expense-upsert.service.ts
Normal file
46
src/modules/expenses/services/expense-upsert.service.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
import { GetExpenseDto } from "../dtos/get-expense.dto";
|
||||||
|
import { updateExpenseDto } from "../dtos/update-expense.dto";
|
||||||
|
import { ExpenseDto } from "../dtos/expense.dto";
|
||||||
|
import { toDateFromString } from "../helpers/expenses-date-time-helpers";
|
||||||
|
|
||||||
|
type Normalized = { date: Date; comment: string; supervisor_comment: string; };
|
||||||
|
|
||||||
|
export type CreateResult = { ok: true; data: GetExpenseDto } | { ok: false; error: any };
|
||||||
|
export type UpdatePayload = { id: number; dto: updateExpenseDto };
|
||||||
|
export type UpdateResult = { ok: true; id: number; data: GetExpenseDto } | { ok: false; id: number; error: any };
|
||||||
|
export type DeleteResult = { ok: true; id: number; } | { ok: false; id: number; error: any };
|
||||||
|
|
||||||
|
type NormedOk = { dto: GetExpenseDto; normed: Normalized };
|
||||||
|
type NormedErr = { error: any };
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ExpenseUpsertService {
|
||||||
|
constructor(private readonly prisma: PrismaService){}
|
||||||
|
|
||||||
|
//_________________________________________________________________
|
||||||
|
// CREATE
|
||||||
|
//_________________________________________________________________
|
||||||
|
//normalized frontend data to match DB
|
||||||
|
async createExpense(timesheet_id: number, dto: ExpenseDto){
|
||||||
|
const normed_expense = this.normalizeExpenseDto(dto)
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//_________________________________________________________________
|
||||||
|
// LOCAL HELPERS
|
||||||
|
//_________________________________________________________________
|
||||||
|
private normalizeExpenseDto(dto: ExpenseDto): Normalized {
|
||||||
|
const date = toDateFromString(dto.date);
|
||||||
|
const comment = this.truncate280(dto.comment);
|
||||||
|
const supervisor_comment = this.truncate280(dto.supervisor_comment? dto.supervisor_comment : '');
|
||||||
|
return { date, comment, supervisor_comment };
|
||||||
|
}
|
||||||
|
|
||||||
|
//makes sure that a string cannot exceed 280 chars
|
||||||
|
private truncate280 = (input: string): string => {
|
||||||
|
return input.length > 280 ? input.slice(0, 280) : input;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -333,7 +333,7 @@ export class ShiftsUpsertService {
|
||||||
//_________________________________________________________________
|
//_________________________________________________________________
|
||||||
// DELETE
|
// DELETE
|
||||||
//_________________________________________________________________
|
//_________________________________________________________________
|
||||||
//finds shift using shit_ids
|
//finds shifts using shit_ids
|
||||||
//recalc overtime shifts after delete
|
//recalc overtime shifts after delete
|
||||||
//blocs deletion if approved
|
//blocs deletion if approved
|
||||||
async deleteShift(shift_id: number) {
|
async deleteShift(shift_id: number) {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,11 @@
|
||||||
|
export function weekStartSunday(date_local: Date): Date {
|
||||||
|
const start = new Date(Date.UTC(date_local.getFullYear(), date_local.getMonth(), date_local.getDate()));
|
||||||
|
const dow = start.getDay();
|
||||||
|
start.setDate(start.getDate() - dow);
|
||||||
|
start.setHours(0, 0, 0, 0);
|
||||||
|
return start;
|
||||||
|
}
|
||||||
|
|
||||||
export const toDateFromString = ( date: Date | string):Date => {
|
export const toDateFromString = ( 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()));
|
||||||
|
|
@ -10,6 +18,7 @@ export const sevenDaysFrom = (date: Date | string): Date[] => {
|
||||||
return d;
|
return d;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const toStringFromDate = (date: Date | string): string => {
|
export const toStringFromDate = (date: Date | string): string => {
|
||||||
const d = toDateFromString(date);
|
const d = toDateFromString(date);
|
||||||
const year = d.getUTCFullYear();
|
const year = d.getUTCFullYear();
|
||||||
|
|
@ -24,3 +33,4 @@ export const toHHmmFromDate = (input: Date | string): string => {
|
||||||
const mm = String(date.getUTCMinutes()).padStart(2, '0');
|
const mm = String(date.getUTCMinutes()).padStart(2, '0');
|
||||||
return `${hh}:${mm}`;
|
return `${hh}:${mm}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,32 +19,52 @@ type TotalExpenses = {
|
||||||
mileage: number;
|
mileage: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const NUMBER_OF_TIMESHEETS_TO_RETURN = 2;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GetTimesheetsOverviewService {
|
export class GetTimesheetsOverviewService {
|
||||||
constructor(private readonly prisma: PrismaService) { }
|
constructor(private readonly prisma: PrismaService) { }
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------------
|
||||||
|
// GET TIMESHEETS FOR A SELECTED EMPLOYEE
|
||||||
|
//-----------------------------------------------------------------------------------
|
||||||
async getTimesheetsForEmployeeByPeriod(employee_id: number, pay_year: number, pay_period_no: number) {
|
async getTimesheetsForEmployeeByPeriod(employee_id: number, pay_year: number, pay_period_no: number) {
|
||||||
//find period using year and period_no
|
//find period using year and period_no
|
||||||
const period = await this.prisma.payPeriods.findFirst({
|
const period = await this.prisma.payPeriods.findFirst({ where: { pay_year, pay_period_no } });
|
||||||
where: { pay_year, pay_period_no },
|
|
||||||
});
|
|
||||||
if (!period) throw new NotFoundException(`Pay period ${pay_year}-${pay_period_no} not found`);
|
if (!period) throw new NotFoundException(`Pay period ${pay_year}-${pay_period_no} not found`);
|
||||||
|
|
||||||
//loads the timesheets related to the fetched pay-period
|
//loads the timesheets related to the fetched pay-period
|
||||||
const rows = await this.loadTimesheets({
|
const timesheet_range = { employee_id, start_date: { gte: period.period_start, lte: period.period_end } };
|
||||||
employee_id,
|
let rows = await this.loadTimesheets(timesheet_range);
|
||||||
start_date: { gte: period.period_start, lte: period.period_end },
|
|
||||||
});
|
//Normalized dates from pay-period
|
||||||
|
const normalized_start = toDateFromString(period.period_start);
|
||||||
|
const normalized_end = toDateFromString(period.period_end);
|
||||||
|
|
||||||
|
//creates empty timesheet to make sure to return desired amount of timesheet
|
||||||
|
for (let i = 0; i < NUMBER_OF_TIMESHEETS_TO_RETURN; i++) {
|
||||||
|
const week_start = new Date(normalized_start);
|
||||||
|
week_start.setUTCDate(week_start.getUTCDate() + i * 7);
|
||||||
|
|
||||||
|
if (week_start.getTime() > normalized_end.getTime()) break;
|
||||||
|
|
||||||
|
const exists = rows.some(
|
||||||
|
(row) => toDateFromString(row.start_date).getTime() === week_start.getTime()
|
||||||
|
);
|
||||||
|
if (!exists) await this.ensureTimesheet(employee_id, week_start);
|
||||||
|
}
|
||||||
|
rows = await this.loadTimesheets(timesheet_range);
|
||||||
|
|
||||||
|
|
||||||
//find user infos using the employee_id
|
//find user infos using the employee_id
|
||||||
const employee = await this.prisma.employees.findUnique({
|
const employee = await this.prisma.employees.findUnique({
|
||||||
where: { id: employee_id },
|
where: { id: employee_id },
|
||||||
include: { user: true },
|
include: { user: true },
|
||||||
});
|
});
|
||||||
if (!employee) throw new NotFoundException(`Employee #${employee_id} not found`);
|
if (!employee) throw new NotFoundException(`Employee #${employee_id} not found`);
|
||||||
|
|
||||||
//builds employee full name
|
//builds employee full name
|
||||||
const user = employee.user;
|
const user = employee.user;
|
||||||
const employee_fullname = `${user.first_name} ${user.last_name}`.trim();
|
const employee_fullname = `${user.first_name} ${user.last_name}`.trim();
|
||||||
|
|
||||||
//maps all timesheet's infos
|
//maps all timesheet's infos
|
||||||
|
|
@ -54,16 +74,17 @@ export class GetTimesheetsOverviewService {
|
||||||
|
|
||||||
|
|
||||||
//-----------------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------------
|
||||||
// MAPPERS & HELPERS
|
// MAPPERS & HELPERS
|
||||||
//-----------------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------------
|
||||||
|
|
||||||
//fetch timesheet's infos
|
//fetch timesheet's infos
|
||||||
private async loadTimesheets(where: any) {
|
private async loadTimesheets(where: any) {
|
||||||
return this.prisma.timesheets.findMany({
|
return this.prisma.timesheets.findMany({
|
||||||
where,
|
where,
|
||||||
include: {
|
include: {
|
||||||
employee: { include: { user: true } },
|
employee: { include: { user: true } },
|
||||||
shift: { include: { bank_code: true } },
|
shift: { include: { bank_code: true } },
|
||||||
expense: { include: { bank_code: true, attachment_record: true } },
|
expense: { include: { bank_code: true, attachment_record: true } },
|
||||||
},
|
},
|
||||||
orderBy: { start_date: 'asc' },
|
orderBy: { start_date: 'asc' },
|
||||||
});
|
});
|
||||||
|
|
@ -71,14 +92,14 @@ export class GetTimesheetsOverviewService {
|
||||||
|
|
||||||
private mapOneTimesheet(timesheet: any) {
|
private mapOneTimesheet(timesheet: any) {
|
||||||
//converts string to UTC date format
|
//converts string to UTC date format
|
||||||
const start = toDateFromString(timesheet.start_date);
|
const start = toDateFromString(timesheet.start_date);
|
||||||
const day_dates = sevenDaysFrom(start);
|
const day_dates = sevenDaysFrom(start);
|
||||||
|
|
||||||
//map of shifts by days
|
//map of shifts by days
|
||||||
const shifts_by_date = new Map<string, any[]>();
|
const shifts_by_date = new Map<string, any[]>();
|
||||||
for (const shift of timesheet.shift) {
|
for (const shift of timesheet.shift) {
|
||||||
const date = toStringFromDate(shift.date);
|
const date = toStringFromDate(shift.date);
|
||||||
const arr = shifts_by_date.get(date) ?? [];
|
const arr = shifts_by_date.get(date) ?? [];
|
||||||
arr.push(shift);
|
arr.push(shift);
|
||||||
shifts_by_date.set(date, arr);
|
shifts_by_date.set(date, arr);
|
||||||
}
|
}
|
||||||
|
|
@ -86,40 +107,40 @@ export class GetTimesheetsOverviewService {
|
||||||
const expenses_by_date = new Map<string, any[]>();
|
const expenses_by_date = new Map<string, any[]>();
|
||||||
for (const expense of timesheet.expense) {
|
for (const expense of timesheet.expense) {
|
||||||
const date = toStringFromDate(expense.date);
|
const date = toStringFromDate(expense.date);
|
||||||
const arr = expenses_by_date.get(date) ?? [];
|
const arr = expenses_by_date.get(date) ?? [];
|
||||||
arr.push(expense);
|
arr.push(expense);
|
||||||
expenses_by_date.set(date, arr);
|
expenses_by_date.set(date, arr);
|
||||||
}
|
}
|
||||||
//weekly totals
|
//weekly totals
|
||||||
const weekly_hours: TotalHours[] = [emptyHours()];
|
const weekly_hours: TotalHours[] = [emptyHours()];
|
||||||
const weekly_expenses: TotalExpenses[] = [emptyExpenses()];
|
const weekly_expenses: TotalExpenses[] = [emptyExpenses()];
|
||||||
|
|
||||||
//map of days
|
//map of days
|
||||||
const days = day_dates.map((date) => {
|
const days = day_dates.map((date) => {
|
||||||
const date_iso = toStringFromDate(date);
|
const date_iso = toStringFromDate(date);
|
||||||
const shifts_source = shifts_by_date.get(date_iso) ?? [];
|
const shifts_source = shifts_by_date.get(date_iso) ?? [];
|
||||||
const expenses_source = expenses_by_date.get(date_iso) ?? [];
|
const expenses_source = expenses_by_date.get(date_iso) ?? [];
|
||||||
//inner map of shifts
|
//inner map of shifts
|
||||||
const shifts = shifts_source.map((shift) => ({
|
const shifts = shifts_source.map((shift) => ({
|
||||||
date: toStringFromDate(shift.date),
|
date: toStringFromDate(shift.date),
|
||||||
start_time: toHHmmFromDate(shift.start_time),
|
start_time: toHHmmFromDate(shift.start_time),
|
||||||
end_time: toHHmmFromDate(shift.end_time),
|
end_time: toHHmmFromDate(shift.end_time),
|
||||||
type: shift.bank_code?.type ?? '',
|
type: shift.bank_code?.type ?? '',
|
||||||
is_remote: shift.is_remote ?? false,
|
is_remote: shift.is_remote ?? false,
|
||||||
is_approved: shift.is_approved ?? false,
|
is_approved: shift.is_approved ?? false,
|
||||||
shift_id: shift.id ?? null,
|
shift_id: shift.id ?? null,
|
||||||
comment: shift.comment ?? null,
|
comment: shift.comment ?? null,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
//inner map of expenses
|
//inner map of expenses
|
||||||
const expenses = expenses_source.map((expense) => ({
|
const expenses = expenses_source.map((expense) => ({
|
||||||
date: toStringFromDate(expense.date),
|
date: toStringFromDate(expense.date),
|
||||||
amount: expense.amount ? Number(expense.amount) : undefined,
|
amount: expense.amount ? Number(expense.amount) : undefined,
|
||||||
mileage: expense.mileage ? Number(expense.mileage) : undefined,
|
mileage: expense.mileage ? Number(expense.mileage) : undefined,
|
||||||
expense_id: expense.id ?? null,
|
expense_id: expense.id ?? null,
|
||||||
attachment: expense.attachment_record ? String(expense.attachment_record.id) : undefined,
|
attachment: expense.attachment_record ? String(expense.attachment_record.id) : undefined,
|
||||||
is_approved: expense.is_approved ?? false,
|
is_approved: expense.is_approved ?? false,
|
||||||
comment: expense.comment ?? '',
|
comment: expense.comment ?? '',
|
||||||
supervisor_comment: expense.supervisor_comment,
|
supervisor_comment: expense.supervisor_comment,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
@ -131,7 +152,7 @@ export class GetTimesheetsOverviewService {
|
||||||
for (const shift of shifts_source) {
|
for (const shift of shifts_source) {
|
||||||
const hours = diffOfHours(shift.start_time, shift.end_time);
|
const hours = diffOfHours(shift.start_time, shift.end_time);
|
||||||
const subgroup = hoursSubGroupFromBankCode(shift.bank_code);
|
const subgroup = hoursSubGroupFromBankCode(shift.bank_code);
|
||||||
daily_hours[0][subgroup] += hours;
|
daily_hours[0][subgroup] += hours;
|
||||||
weekly_hours[0][subgroup] += hours;
|
weekly_hours[0][subgroup] += hours;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -166,12 +187,44 @@ export class GetTimesheetsOverviewService {
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
timesheet_id: timesheet.id,
|
timesheet_id: timesheet.id,
|
||||||
is_approved: timesheet.is_approved ?? false,
|
is_approved: timesheet.is_approved ?? false,
|
||||||
days,
|
days,
|
||||||
weekly_hours,
|
weekly_hours,
|
||||||
weekly_expenses,
|
weekly_expenses,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ensureTimesheet = async (employee_id: number, start_date: Date | string) => {
|
||||||
|
const start = toDateFromString(start_date);
|
||||||
|
|
||||||
|
let row = await this.prisma.timesheets.findFirst({
|
||||||
|
where: { employee_id, start_date: start },
|
||||||
|
include: {
|
||||||
|
employee: { include: { user: true } },
|
||||||
|
shift: { include: { bank_code: true } },
|
||||||
|
expense: { include: { bank_code: true, attachment_record: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (row) return row;
|
||||||
|
|
||||||
|
await this.prisma.timesheets.create({
|
||||||
|
data: {
|
||||||
|
employee_id,
|
||||||
|
start_date: start,
|
||||||
|
is_approved: false
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
row = await this.prisma.timesheets.findFirst({
|
||||||
|
where: { employee_id, start_date: start },
|
||||||
|
include: {
|
||||||
|
employee: { include: { user: true } },
|
||||||
|
shift: { include: { bank_code: true } },
|
||||||
|
expense: { include: { bank_code: true, attachment_record: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return row!;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//filled array with default values
|
//filled array with default values
|
||||||
|
|
@ -190,20 +243,20 @@ const num = (value: any): number => { return value ? Number(value) : 0 };
|
||||||
// shift's subgroup types
|
// shift's subgroup types
|
||||||
const hoursSubGroupFromBankCode = (bank_code: any): keyof TotalHours => {
|
const hoursSubGroupFromBankCode = (bank_code: any): keyof TotalHours => {
|
||||||
const type = bank_code.type;
|
const type = bank_code.type;
|
||||||
if (type.includes('EVENING')) return 'evening';
|
if (type.includes('EVENING')) return 'evening';
|
||||||
if (type.includes('EMERGENCY')) return 'emergency';
|
if (type.includes('EMERGENCY')) return 'emergency';
|
||||||
if (type.includes('OVERTIME')) return 'overtime';
|
if (type.includes('OVERTIME')) return 'overtime';
|
||||||
if (type.includes('VACATION')) return 'vacation';
|
if (type.includes('VACATION')) return 'vacation';
|
||||||
if (type.includes('HOLIDAY')) return 'holiday';
|
if (type.includes('HOLIDAY')) return 'holiday';
|
||||||
if (type.includes('SICK')) return 'sick';
|
if (type.includes('SICK')) return 'sick';
|
||||||
return 'regular'
|
return 'regular'
|
||||||
}
|
}
|
||||||
|
|
||||||
// expense's subgroup types
|
// expense's subgroup types
|
||||||
const expenseSubgroupFromBankCode = (bank_code: any): keyof TotalExpenses => {
|
const expenseSubgroupFromBankCode = (bank_code: any): keyof TotalExpenses => {
|
||||||
const type = bank_code.type;
|
const type = bank_code.type;
|
||||||
if (type.includes('MILEAGE')) return 'mileage';
|
if (type.includes('MILEAGE')) return 'mileage';
|
||||||
if (type.includes('PER_DIEM')) return 'per_diem';
|
if (type.includes('PER_DIEM')) return 'per_diem';
|
||||||
if (type.includes('ON_CALL')) return 'on_call';
|
if (type.includes('ON_CALL')) return 'on_call';
|
||||||
return 'expenses';
|
return 'expenses';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user