refactor(shifts): refactor main upsert function to use shared utils and helpers
This commit is contained in:
parent
cc310e286d
commit
f6c5b2a73c
|
|
@ -42,8 +42,6 @@ export class EmployeesService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async findOneProfile(email: string): Promise<EmployeeProfileItemDto> {
|
async findOneProfile(email: string): Promise<EmployeeProfileItemDto> {
|
||||||
const emp = await this.prisma.employees.findFirst({
|
const emp = await this.prisma.employees.findFirst({
|
||||||
where: { user: { email } },
|
where: { user: { email } },
|
||||||
|
|
|
||||||
|
|
@ -3,28 +3,20 @@ import { Module } from "@nestjs/common";
|
||||||
import { ExpensesQueryService } from "./services/expenses-query.service";
|
import { ExpensesQueryService } from "./services/expenses-query.service";
|
||||||
import { BusinessLogicsModule } from "src/modules/business-logics/business-logics.module";
|
import { BusinessLogicsModule } from "src/modules/business-logics/business-logics.module";
|
||||||
import { ExpensesCommandService } from "./services/expenses-command.service";
|
import { ExpensesCommandService } from "./services/expenses-command.service";
|
||||||
import { BankCodesRepo } from "./repos/bank-codes.repo";
|
|
||||||
import { TimesheetsRepo } from "./repos/timesheets.repo";
|
|
||||||
import { EmployeesRepo } from "./repos/employee.repo";
|
|
||||||
import { ExpensesArchivalService } from "./services/expenses-archival.service";
|
import { ExpensesArchivalService } from "./services/expenses-archival.service";
|
||||||
|
import { SharedModule } from "../shared/shared.module";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [BusinessLogicsModule],
|
imports: [BusinessLogicsModule, SharedModule],
|
||||||
controllers: [ExpensesController],
|
controllers: [ExpensesController],
|
||||||
providers: [
|
providers: [
|
||||||
ExpensesQueryService,
|
ExpensesQueryService,
|
||||||
ExpensesArchivalService,
|
ExpensesArchivalService,
|
||||||
ExpensesCommandService,
|
ExpensesCommandService,
|
||||||
BankCodesRepo,
|
|
||||||
TimesheetsRepo,
|
|
||||||
EmployeesRepo,
|
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
ExpensesQueryService,
|
ExpensesQueryService,
|
||||||
ExpensesArchivalService,
|
ExpensesArchivalService,
|
||||||
BankCodesRepo,
|
|
||||||
TimesheetsRepo,
|
|
||||||
EmployeesRepo,
|
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,16 @@ import { BaseApprovalService } from "src/common/shared/base-approval.service";
|
||||||
import { Expenses, Prisma } from "@prisma/client";
|
import { Expenses, Prisma } from "@prisma/client";
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
import { UpsertExpenseDto } from "../dtos/upsert-expense.dto";
|
import { UpsertExpenseDto } from "../dtos/upsert-expense.dto";
|
||||||
import { BankCodesRepo } from "../repos/bank-codes.repo";
|
|
||||||
import { TimesheetsRepo } from "../repos/timesheets.repo";
|
|
||||||
import { EmployeesRepo } from "../repos/employee.repo";
|
|
||||||
import { toDateOnlyUTC } from "src/modules/shifts/helpers/shifts-date-time-helpers";
|
import { toDateOnlyUTC } from "src/modules/shifts/helpers/shifts-date-time-helpers";
|
||||||
|
import { BankCodesResolver } from "src/modules/shared/utils/resolve-bank-type-id.utils";
|
||||||
|
import { ExpenseResponse, UpsertAction } from "../types and interfaces/expenses.types.interfaces";
|
||||||
|
import { EmployeeIdEmailResolver } from "src/modules/shared/utils/resolve-email-id.utils";
|
||||||
|
import { EmployeeTimesheetResolver } from "src/modules/shared/utils/resolve-employee-timesheet.utils";
|
||||||
import {
|
import {
|
||||||
BadRequestException,
|
BadRequestException,
|
||||||
Injectable,
|
Injectable,
|
||||||
NotFoundException
|
NotFoundException
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { ExpenseResponse, UpsertAction } from "../types and interfaces/expenses.types.interfaces";
|
|
||||||
import {
|
import {
|
||||||
assertAndTrimComment,
|
assertAndTrimComment,
|
||||||
computeAmountDecimal,
|
computeAmountDecimal,
|
||||||
|
|
@ -25,9 +25,9 @@ import {
|
||||||
export class ExpensesCommandService extends BaseApprovalService<Expenses> {
|
export class ExpensesCommandService extends BaseApprovalService<Expenses> {
|
||||||
constructor(
|
constructor(
|
||||||
prisma: PrismaService,
|
prisma: PrismaService,
|
||||||
private readonly bankCodesRepo: BankCodesRepo,
|
private readonly bankCodesResolver: BankCodesResolver,
|
||||||
private readonly timesheetsRepo: TimesheetsRepo,
|
private readonly timesheetsResolver: EmployeeTimesheetResolver,
|
||||||
private readonly employeesRepo: EmployeesRepo,
|
private readonly emailResolver: EmployeeIdEmailResolver,
|
||||||
) { super(prisma); }
|
) { super(prisma); }
|
||||||
|
|
||||||
//_____________________________________________________________________________________________
|
//_____________________________________________________________________________________________
|
||||||
|
|
@ -56,27 +56,25 @@ export class ExpensesCommandService extends BaseApprovalService<Expenses> {
|
||||||
|
|
||||||
//validates if there is an existing expense, at least 1 old or new
|
//validates if there is an existing expense, at least 1 old or new
|
||||||
const { old_expense, new_expense } = dto ?? {};
|
const { old_expense, new_expense } = dto ?? {};
|
||||||
if(!old_expense && !new_expense) {
|
if(!old_expense && !new_expense) throw new BadRequestException('At least one expense must be provided');
|
||||||
throw new BadRequestException('At least one expense must be provided');
|
|
||||||
}
|
|
||||||
|
|
||||||
//validate date format
|
//validate date format
|
||||||
const date_only = toDateOnlyUTC(date);
|
const date_only = toDateOnlyUTC(date);
|
||||||
if(Number.isNaN(date_only.getTime())) {
|
if(Number.isNaN(date_only.getTime())) throw new BadRequestException('Invalid date format (expected: YYYY-MM-DD)');
|
||||||
throw new BadRequestException('Invalid date format (expected: YYYY-MM-DD)');
|
|
||||||
}
|
|
||||||
|
|
||||||
//resolve employee_id by email
|
//resolve employee_id by email
|
||||||
const employee_id = await this.resolveEmployeeIdByEmail(email);
|
const employee_id = await this.emailResolver.findIdByEmail(email);
|
||||||
|
|
||||||
//make sure a timesheet existes
|
//make sure a timesheet existes
|
||||||
const timesheet_id = await this.ensureTimesheetForDate(employee_id, date_only);
|
const timesheet_id = await this.timesheetsResolver.ensureForDate(employee_id, date_only);
|
||||||
|
if(!timesheet_id) throw new NotFoundException(`no timesheet found for employee #${employee_id}`)
|
||||||
|
const {id} = timesheet_id;
|
||||||
|
|
||||||
return this.prisma.$transaction(async (tx) => {
|
return this.prisma.$transaction(async (tx) => {
|
||||||
const loadDay = async (): Promise<ExpenseResponse[]> => {
|
const loadDay = async (): Promise<ExpenseResponse[]> => {
|
||||||
const rows = await tx.expenses.findMany({
|
const rows = await tx.expenses.findMany({
|
||||||
where: {
|
where: {
|
||||||
timesheet_id: timesheet_id,
|
timesheet_id: id,
|
||||||
date: date_only,
|
date: date_only,
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
|
|
@ -118,7 +116,7 @@ export class ExpensesCommandService extends BaseApprovalService<Expenses> {
|
||||||
const comment = assertAndTrimComment(payload.comment);
|
const comment = assertAndTrimComment(payload.comment);
|
||||||
const attachment = parseAttachmentId(payload.attachment);
|
const attachment = parseAttachmentId(payload.attachment);
|
||||||
|
|
||||||
const { id: bank_code_id, modifier } = await this.resolveBankCodeIdByType(tx, type);
|
const { id: bank_code_id, modifier } = await this.bankCodesResolver.findByType(type);
|
||||||
let amount = computeAmountDecimal(type, payload, modifier);
|
let amount = computeAmountDecimal(type, payload, modifier);
|
||||||
let mileage: number | null = null;
|
let mileage: number | null = null;
|
||||||
|
|
||||||
|
|
@ -139,11 +137,11 @@ export class ExpensesCommandService extends BaseApprovalService<Expenses> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attachment !== null) {
|
if (attachment !== null) {
|
||||||
const attachmentRow = await tx.attachments.findUnique({
|
const attachment_row = await tx.attachments.findUnique({
|
||||||
where: { id: attachment },
|
where: { id: attachment },
|
||||||
select: { status: true },
|
select: { status: true },
|
||||||
});
|
});
|
||||||
if (!attachmentRow || attachmentRow.status !== 'ACTIVE') {
|
if (!attachment_row || attachment_row.status !== 'ACTIVE') {
|
||||||
throw new BadRequestException('Attachment not found or inactive');
|
throw new BadRequestException('Attachment not found or inactive');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -167,7 +165,7 @@ export class ExpensesCommandService extends BaseApprovalService<Expenses> {
|
||||||
}) => {
|
}) => {
|
||||||
return tx.expenses.findFirst({
|
return tx.expenses.findFirst({
|
||||||
where: {
|
where: {
|
||||||
timesheet_id: timesheet_id,
|
timesheet_id: id,
|
||||||
date: date_only,
|
date: date_only,
|
||||||
bank_code_id: norm.bank_code_id,
|
bank_code_id: norm.bank_code_id,
|
||||||
amount: norm.amount,
|
amount: norm.amount,
|
||||||
|
|
@ -184,8 +182,8 @@ export class ExpensesCommandService extends BaseApprovalService<Expenses> {
|
||||||
// DELETE
|
// DELETE
|
||||||
//_____________________________________________________________________________________________
|
//_____________________________________________________________________________________________
|
||||||
if(old_expense && !new_expense) {
|
if(old_expense && !new_expense) {
|
||||||
const oldNorm = await normalizePayload(old_expense);
|
const old_norm = await normalizePayload(old_expense);
|
||||||
const existing = await findExactOld(oldNorm);
|
const existing = await findExactOld(old_norm);
|
||||||
if(!existing) {
|
if(!existing) {
|
||||||
throw new NotFoundException({
|
throw new NotFoundException({
|
||||||
error_code: 'EXPENSE_STALE',
|
error_code: 'EXPENSE_STALE',
|
||||||
|
|
@ -202,7 +200,7 @@ export class ExpensesCommandService extends BaseApprovalService<Expenses> {
|
||||||
const new_exp = await normalizePayload(new_expense);
|
const new_exp = await normalizePayload(new_expense);
|
||||||
await tx.expenses.create({
|
await tx.expenses.create({
|
||||||
data: {
|
data: {
|
||||||
timesheet_id: timesheet_id,
|
timesheet_id: id,
|
||||||
date: date_only,
|
date: date_only,
|
||||||
bank_code_id: new_exp.bank_code_id,
|
bank_code_id: new_exp.bank_code_id,
|
||||||
amount: new_exp.amount,
|
amount: new_exp.amount,
|
||||||
|
|
@ -218,8 +216,8 @@ export class ExpensesCommandService extends BaseApprovalService<Expenses> {
|
||||||
// UPDATE
|
// UPDATE
|
||||||
//_____________________________________________________________________________________________
|
//_____________________________________________________________________________________________
|
||||||
else if(old_expense && new_expense) {
|
else if(old_expense && new_expense) {
|
||||||
const oldNorm = await normalizePayload(old_expense);
|
const old_norm = await normalizePayload(old_expense);
|
||||||
const existing = await findExactOld(oldNorm);
|
const existing = await findExactOld(old_norm);
|
||||||
if(!existing) {
|
if(!existing) {
|
||||||
throw new NotFoundException({
|
throw new NotFoundException({
|
||||||
error_code: 'EXPENSE_STALE',
|
error_code: 'EXPENSE_STALE',
|
||||||
|
|
@ -249,22 +247,4 @@ export class ExpensesCommandService extends BaseApprovalService<Expenses> {
|
||||||
return { action, day };
|
return { action, day };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//_____________________________________________________________________________________________
|
|
||||||
// HELPERS
|
|
||||||
//_____________________________________________________________________________________________
|
|
||||||
|
|
||||||
private readonly resolveEmployeeIdByEmail = async (email: string): Promise<number> =>
|
|
||||||
this.employeesRepo.findIdByEmail(email);
|
|
||||||
|
|
||||||
private readonly ensureTimesheetForDate = async ( employee_id: number, date: Date
|
|
||||||
): Promise<number> => {
|
|
||||||
const { id } = await this.timesheetsRepo.ensureForDate(employee_id, date);
|
|
||||||
return id;
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly resolveBankCodeIdByType = async ( transaction: Prisma.TransactionClient, type: string
|
|
||||||
): Promise<{id: number; modifier: number}> =>
|
|
||||||
this.bankCodesRepo.findByType(type, transaction);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
import { DayExpensesDto, ExpenseDto } from "src/modules/timesheets/dtos/timesheet-period.dto";
|
import { DayExpensesDto, ExpenseDto } from "src/modules/timesheets/dtos/timesheet-period.dto";
|
||||||
import { EmployeesRepo } from "../repos/employee.repo";
|
|
||||||
import { round2, toUTCDateOnly } from "src/modules/timesheets/utils/timesheet.helpers";
|
import { round2, toUTCDateOnly } from "src/modules/timesheets/utils/timesheet.helpers";
|
||||||
import { EXPENSE_TYPES } from "src/modules/timesheets/types/timesheet.types";
|
import { EXPENSE_TYPES } from "src/modules/timesheets/types/timesheet.types";
|
||||||
|
import { EmployeeIdEmailResolver } from "src/modules/shared/utils/resolve-email-id.utils";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ExpensesQueryService {
|
export class ExpensesQueryService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
private readonly employeeRepo: EmployeesRepo,
|
private readonly employeeRepo: EmployeeIdEmailResolver,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,21 +7,16 @@ import { TimesheetsModule } from "../timesheets/timesheets.module";
|
||||||
import { TimesheetsCommandService } from "../timesheets/services/timesheets-command.service";
|
import { TimesheetsCommandService } from "../timesheets/services/timesheets-command.service";
|
||||||
import { ExpensesCommandService } from "../expenses/services/expenses-command.service";
|
import { ExpensesCommandService } from "../expenses/services/expenses-command.service";
|
||||||
import { ShiftsCommandService } from "../shifts/services/shifts-command.service";
|
import { ShiftsCommandService } from "../shifts/services/shifts-command.service";
|
||||||
import { BankCodesRepo } from "../expenses/repos/bank-codes.repo";
|
import { SharedModule } from "../shared/shared.module";
|
||||||
import { EmployeesRepo } from "../expenses/repos/employee.repo";
|
|
||||||
import { TimesheetsRepo } from "../expenses/repos/timesheets.repo";
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule, TimesheetsModule],
|
imports: [PrismaModule, TimesheetsModule, SharedModule],
|
||||||
providers: [
|
providers: [
|
||||||
PayPeriodsQueryService,
|
PayPeriodsQueryService,
|
||||||
PayPeriodsCommandService,
|
PayPeriodsCommandService,
|
||||||
TimesheetsCommandService,
|
TimesheetsCommandService,
|
||||||
ExpensesCommandService,
|
ExpensesCommandService,
|
||||||
ShiftsCommandService,
|
ShiftsCommandService,
|
||||||
BankCodesRepo,
|
|
||||||
TimesheetsRepo,
|
|
||||||
EmployeesRepo,
|
|
||||||
],
|
],
|
||||||
controllers: [PayPeriodsController],
|
controllers: [PayPeriodsController],
|
||||||
exports: [
|
exports: [
|
||||||
|
|
|
||||||
22
src/modules/shared/shared.module.ts
Normal file
22
src/modules/shared/shared.module.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
import { EmployeeIdEmailResolver } from "./utils/resolve-email-id.utils";
|
||||||
|
import { EmployeeTimesheetResolver } from "./utils/resolve-employee-timesheet.utils";
|
||||||
|
import { FullNameResolver } from "./utils/resolve-full-name.utils";
|
||||||
|
import { BankCodesResolver } from "./utils/resolve-bank-type-id.utils";
|
||||||
|
import { PrismaModule } from "src/prisma/prisma.module";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [PrismaModule],
|
||||||
|
providers: [
|
||||||
|
FullNameResolver,
|
||||||
|
EmployeeIdEmailResolver,
|
||||||
|
BankCodesResolver,
|
||||||
|
EmployeeTimesheetResolver,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
FullNameResolver,
|
||||||
|
EmployeeIdEmailResolver,
|
||||||
|
BankCodesResolver,
|
||||||
|
EmployeeTimesheetResolver,
|
||||||
|
],
|
||||||
|
}) export class SharedModule {}
|
||||||
|
|
@ -2,11 +2,10 @@ import { Injectable, NotFoundException } from "@nestjs/common";
|
||||||
import { Prisma, PrismaClient } from "@prisma/client";
|
import { Prisma, PrismaClient } from "@prisma/client";
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
|
||||||
|
|
||||||
type Tx = Prisma.TransactionClient | PrismaClient;
|
type Tx = Prisma.TransactionClient | PrismaClient;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BankCodesRepo {
|
export class BankCodesResolver {
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
//find id and modifier by type
|
//find id and modifier by type
|
||||||
|
|
@ -14,21 +13,11 @@ export class BankCodesRepo {
|
||||||
): Promise<{id:number; modifier: number }> => {
|
): Promise<{id:number; modifier: number }> => {
|
||||||
const db = client ?? this.prisma;
|
const db = client ?? this.prisma;
|
||||||
const bank = await db.bankCodes.findFirst({
|
const bank = await db.bankCodes.findFirst({
|
||||||
where: {
|
where: { type },
|
||||||
type,
|
select: { id: true, modifier: true },
|
||||||
},
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
modifier: true,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if(!bank) {
|
if(!bank) throw new NotFoundException(`Unknown bank code type: ${type}`);
|
||||||
throw new NotFoundException(`Unknown bank code type: ${type}`);
|
return { id: bank.id, modifier: bank.modifier };
|
||||||
}
|
|
||||||
return {
|
|
||||||
id: bank.id,
|
|
||||||
modifier: bank.modifier,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -2,31 +2,22 @@ import { Injectable, NotFoundException } from "@nestjs/common";
|
||||||
import { Prisma, PrismaClient } from "@prisma/client";
|
import { Prisma, PrismaClient } from "@prisma/client";
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
|
||||||
|
|
||||||
type Tx = Prisma.TransactionClient | PrismaClient;
|
type Tx = Prisma.TransactionClient | PrismaClient;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class EmployeesRepo {
|
export class EmployeeIdEmailResolver {
|
||||||
|
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
// find employee id by email
|
// find employee_id using email
|
||||||
readonly findIdByEmail = async ( email: string, client?: Tx
|
readonly findIdByEmail = async ( email: string, client?: Tx
|
||||||
): Promise<number> => {
|
): Promise<number> => {
|
||||||
const db = client ?? this.prisma;
|
const db = client ?? this.prisma;
|
||||||
const employee = await db.employees.findFirst({
|
const employee = await db.employees.findFirst({
|
||||||
where: {
|
where: { user: { email } },
|
||||||
user: {
|
select: { id: true },
|
||||||
email,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
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`);
|
|
||||||
}
|
|
||||||
return employee.id;
|
return employee.id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -7,7 +7,7 @@ import { PrismaService } from "src/prisma/prisma.service";
|
||||||
type Tx = Prisma.TransactionClient | PrismaClient;
|
type Tx = Prisma.TransactionClient | PrismaClient;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TimesheetsRepo {
|
export class EmployeeTimesheetResolver {
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
//find an existing timesheet linked to the employee
|
//find an existing timesheet linked to the employee
|
||||||
22
src/modules/shared/utils/resolve-full-name.utils.ts
Normal file
22
src/modules/shared/utils/resolve-full-name.utils.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||||
|
import { Prisma, PrismaClient } from "@prisma/client";
|
||||||
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
|
||||||
|
type Tx = Prisma.TransactionClient | PrismaClient;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class FullNameResolver {
|
||||||
|
constructor(private readonly prisma: PrismaService){}
|
||||||
|
|
||||||
|
readonly resolveFullName = async (employee_id: number, client?: Tx): Promise<string> =>{
|
||||||
|
const db = client ?? this.prisma;
|
||||||
|
const employee = await db.employees.findUnique({
|
||||||
|
where: { id: employee_id },
|
||||||
|
select: { user: { select: {first_name: true, last_name: true} } },
|
||||||
|
});
|
||||||
|
if(!employee) throw new NotFoundException(`Unknown user with name: ${employee_id}`)
|
||||||
|
|
||||||
|
const full_name = ( employee.user.first_name + " " + employee.user.last_name ) || " ";
|
||||||
|
return full_name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,7 +6,7 @@ import { ShiftsCommandService } from "../services/shifts-command.service";
|
||||||
import { ShiftsQueryService } from "../services/shifts-query.service";
|
import { ShiftsQueryService } from "../services/shifts-query.service";
|
||||||
import { GetShiftsOverviewDto } from "../dtos/get-shift-overview.dto";
|
import { GetShiftsOverviewDto } from "../dtos/get-shift-overview.dto";
|
||||||
import { UpsertShiftDto } from "../dtos/upsert-shift.dto";
|
import { UpsertShiftDto } from "../dtos/upsert-shift.dto";
|
||||||
import { OverviewRow } from "../types and interfaces/shifts-overview-row.interface";
|
import { OverviewRow } from "../types-and-interfaces/shifts-overview-row.interface";
|
||||||
|
|
||||||
@ApiTags('Shifts')
|
@ApiTags('Shifts')
|
||||||
@ApiBearerAuth('access-token')
|
@ApiBearerAuth('access-token')
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,11 @@ export function timeFromHHMMUTC(hhmm: string): Date {
|
||||||
return new Date(Date.UTC(1970,0,1,hour, min,0));
|
return new Date(Date.UTC(1970,0,1,hour, min,0));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function weekStartMondayUTC(date: Date): Date {
|
export function weekStartSundayUTC(d: Date): Date {
|
||||||
const d = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
|
|
||||||
const day = d.getUTCDay();
|
const day = d.getUTCDay();
|
||||||
const diff = (day + 6) % 7;
|
const start = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
|
||||||
d.setUTCDate(d.getUTCDate() - diff);
|
start.setUTCDate(start.getUTCDate()- day);
|
||||||
d.setUTCHours(0,0,0,0);
|
return start;
|
||||||
return d;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toDateOnlyUTC(input: string | Date): Date {
|
export function toDateOnlyUTC(input: string | Date): Date {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
import { BadRequestException, ConflictException, Injectable, NotFoundException, UnprocessableEntityException } from "@nestjs/common";
|
import { BadRequestException, ConflictException, Injectable, NotFoundException, UnprocessableEntityException } from "@nestjs/common";
|
||||||
import { formatHHmm, toDateOnlyUTC, weekStartMondayUTC } from "../helpers/shifts-date-time-helpers";
|
import { formatHHmm, toDateOnlyUTC, weekStartSundayUTC } from "../helpers/shifts-date-time-helpers";
|
||||||
import { normalizeShiftPayload, overlaps, resolveBankCodeByType } from "../utils/shifts.utils";
|
import { normalizeShiftPayload, overlaps } from "../utils/shifts.utils";
|
||||||
import { DayShiftResponse, UpsertAction } from "../types and interfaces/shifts-upsert.types";
|
import { DayShiftResponse, UpsertAction } from "../types-and-interfaces/shifts-upsert.types";
|
||||||
|
import { EmployeeIdEmailResolver } from "src/modules/shared/utils/resolve-email-id.utils";
|
||||||
|
import { BankCodesResolver } from "src/modules/shared/utils/resolve-bank-type-id.utils";
|
||||||
import { Prisma, Shifts } from "@prisma/client";
|
import { Prisma, Shifts } from "@prisma/client";
|
||||||
import { UpsertShiftDto } from "../dtos/upsert-shift.dto";
|
import { UpsertShiftDto } from "../dtos/upsert-shift.dto";
|
||||||
import { BaseApprovalService } from "src/common/shared/base-approval.service";
|
import { BaseApprovalService } from "src/common/shared/base-approval.service";
|
||||||
|
|
@ -9,7 +11,11 @@ import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ShiftsCommandService extends BaseApprovalService<Shifts> {
|
export class ShiftsCommandService extends BaseApprovalService<Shifts> {
|
||||||
constructor(prisma: PrismaService) { super(prisma); }
|
constructor(
|
||||||
|
prisma: PrismaService,
|
||||||
|
private readonly emailResolver: EmployeeIdEmailResolver,
|
||||||
|
private readonly bankTypeResolver: BankCodesResolver,
|
||||||
|
) { super(prisma); }
|
||||||
|
|
||||||
//_____________________________________________________________________________________________
|
//_____________________________________________________________________________________________
|
||||||
// APPROVAL AND DELEGATE METHODS
|
// APPROVAL AND DELEGATE METHODS
|
||||||
|
|
@ -40,88 +46,53 @@ export class ShiftsCommandService extends BaseApprovalService<Shifts> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const date_only = toDateOnlyUTC(date_string);
|
const date_only = toDateOnlyUTC(date_string);
|
||||||
|
const employee_id = await this.emailResolver.findIdByEmail(email);
|
||||||
|
|
||||||
//Resolve employee by email
|
return this.prisma.$transaction(async (tx) => {
|
||||||
const employee = await this.prisma.employees.findFirst({
|
const start_of_week = weekStartSundayUTC(date_only);
|
||||||
where: { user: {email } },
|
|
||||||
|
const timesheet = await tx.timesheets.upsert({
|
||||||
|
where: { employee_id_start_date: { employee_id, start_date: start_of_week } },
|
||||||
|
update: {},
|
||||||
|
create: { employee_id, start_date: start_of_week },
|
||||||
select: { id: true },
|
select: { id: true },
|
||||||
});
|
});
|
||||||
if(!employee) throw new NotFoundException(`Employee not found for email : ${ email }`);
|
|
||||||
|
|
||||||
//making sure a timesheet exist in selected week
|
//validation/sanitation
|
||||||
const start_of_week = weekStartMondayUTC(date_only);
|
//resolve bank_code_id using type
|
||||||
let timesheet = await this.prisma.timesheets.findFirst({
|
const old_norm_shift = old_shift ? await normalizeShiftPayload(old_shift) : undefined;
|
||||||
where: {
|
if (old_norm_shift && old_norm_shift.end_time.getTime() <= old_norm_shift.start_time.getTime()) {
|
||||||
employee_id: employee.id,
|
|
||||||
start_date: start_of_week
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
id: true
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if(!timesheet) {
|
|
||||||
timesheet = await this.prisma.timesheets.create({
|
|
||||||
data: {
|
|
||||||
employee_id: employee.id,
|
|
||||||
start_date: start_of_week
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
id: true
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//normalization of data to ensure a valid comparison between DB and payload
|
|
||||||
const old_norm = dto.old_shift
|
|
||||||
? normalizeShiftPayload(dto.old_shift)
|
|
||||||
: undefined;
|
|
||||||
const new_norm = dto.new_shift
|
|
||||||
? normalizeShiftPayload(dto.new_shift)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
if (old_norm && old_norm.end_time.getTime() <= old_norm.start_time.getTime()) {
|
|
||||||
throw new UnprocessableEntityException(' old_shift.end_time must be > old_shift.start_time');
|
throw new UnprocessableEntityException(' old_shift.end_time must be > old_shift.start_time');
|
||||||
}
|
}
|
||||||
if (new_norm && new_norm.end_time.getTime() <= new_norm.start_time.getTime()) {
|
const old_bank_code_id: number | undefined = old_norm_shift ? (await this.bankTypeResolver.findByType(old_norm_shift.type, tx))?.id : undefined;
|
||||||
|
|
||||||
|
|
||||||
|
const new_norm_shift = new_shift ? await normalizeShiftPayload(new_shift) : undefined;
|
||||||
|
if (new_norm_shift && new_norm_shift.end_time.getTime() <= new_norm_shift.start_time.getTime()) {
|
||||||
throw new UnprocessableEntityException(' new_shift.end_time must be > new_shift.start_time');
|
throw new UnprocessableEntityException(' new_shift.end_time must be > new_shift.start_time');
|
||||||
}
|
}
|
||||||
|
const new_bank_code_id: number | undefined = new_norm_shift ? (await this.bankTypeResolver.findByType(new_norm_shift.type, tx))?.id : undefined;
|
||||||
|
|
||||||
//Resolve bank_code_id with type
|
|
||||||
const old_bank_code_id = old_norm
|
|
||||||
? await resolveBankCodeByType(old_norm.type)
|
|
||||||
: undefined;
|
|
||||||
const new_bank_code_id = new_norm
|
|
||||||
? await resolveBankCodeByType(new_norm.type)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
//fetch all shifts in a single day
|
//fetch all shifts in a single day and verify possible overlaps
|
||||||
const day_shifts = await this.prisma.shifts.findMany({
|
const day_shifts = await tx.shifts.findMany({
|
||||||
where: {
|
where: { timesheet_id: timesheet.id, date: date_only },
|
||||||
timesheet_id: timesheet.id,
|
include: { bank_code: true },
|
||||||
date: date_only
|
orderBy: { start_time: 'asc'},
|
||||||
},
|
|
||||||
include: {
|
|
||||||
bank_code: true
|
|
||||||
},
|
|
||||||
orderBy: {
|
|
||||||
start_time: 'asc'
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await this.prisma.$transaction(async (transaction)=> {
|
|
||||||
let action: UpsertAction;
|
|
||||||
|
|
||||||
const findExactOldShift = async ()=> {
|
const findExactOldShift = async ()=> {
|
||||||
if(!old_norm || old_bank_code_id === undefined) return undefined;
|
if(!old_norm_shift || old_bank_code_id === undefined) return undefined;
|
||||||
const old_comment = old_norm.comment ?? null;
|
const old_comment = old_norm_shift.comment ?? null;
|
||||||
|
|
||||||
return transaction.shifts.findFirst({
|
return await tx.shifts.findFirst({
|
||||||
where: {
|
where: {
|
||||||
timesheet_id: timesheet.id,
|
timesheet_id: timesheet.id,
|
||||||
date: date_only,
|
date: date_only,
|
||||||
start_time: old_norm.start_time,
|
start_time: old_norm_shift.start_time,
|
||||||
end_time: old_norm.end_time,
|
end_time: old_norm_shift.end_time,
|
||||||
is_remote: old_norm.is_remote,
|
is_remote: old_norm_shift.is_remote,
|
||||||
comment: old_comment,
|
comment: old_comment,
|
||||||
bank_code_id: old_bank_code_id,
|
bank_code_id: old_bank_code_id,
|
||||||
},
|
},
|
||||||
|
|
@ -131,12 +102,12 @@ export class ShiftsCommandService extends BaseApprovalService<Shifts> {
|
||||||
|
|
||||||
//checks for overlaping shifts
|
//checks for overlaping shifts
|
||||||
const assertNoOverlap = (exclude_shift_id?: number)=> {
|
const assertNoOverlap = (exclude_shift_id?: number)=> {
|
||||||
if (!new_norm) return;
|
if (!new_norm_shift) return;
|
||||||
const overlap_with = day_shifts.filter((shift)=> {
|
const overlap_with = day_shifts.filter((shift)=> {
|
||||||
if(exclude_shift_id && shift.id === exclude_shift_id) return false;
|
if(exclude_shift_id && shift.id === exclude_shift_id) return false;
|
||||||
return overlaps(
|
return overlaps(
|
||||||
new_norm.start_time.getTime(),
|
new_norm_shift.start_time.getTime(),
|
||||||
new_norm.end_time.getTime(),
|
new_norm_shift.end_time.getTime(),
|
||||||
shift.start_time.getTime(),
|
shift.start_time.getTime(),
|
||||||
shift.end_time.getTime(),
|
shift.end_time.getTime(),
|
||||||
);
|
);
|
||||||
|
|
@ -148,18 +119,15 @@ export class ShiftsCommandService extends BaseApprovalService<Shifts> {
|
||||||
end_time: formatHHmm(shift.end_time),
|
end_time: formatHHmm(shift.end_time),
|
||||||
type: shift.bank_code?.type ?? 'UNKNOWN',
|
type: shift.bank_code?.type ?? 'UNKNOWN',
|
||||||
}));
|
}));
|
||||||
throw new ConflictException({
|
throw new ConflictException({ error_code: 'SHIFT_OVERLAP', message: 'New shift overlaps with existing shift(s)', conflicts});
|
||||||
error_code: 'SHIFT_OVERLAP',
|
|
||||||
message: 'New shift overlaps with existing shift(s)',
|
|
||||||
conflicts,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
let action: UpsertAction;
|
||||||
//_____________________________________________________________________________________________
|
//_____________________________________________________________________________________________
|
||||||
// DELETE
|
// DELETE
|
||||||
//_____________________________________________________________________________________________
|
//_____________________________________________________________________________________________
|
||||||
if ( old_shift && !new_shift ) {
|
if ( old_shift && !new_shift ) {
|
||||||
|
if (old_bank_code_id === undefined) throw new NotFoundException(`bank code not found for old_shift.type: ${old_norm_shift?.type ?? ''}`);
|
||||||
const existing = await findExactOldShift();
|
const existing = await findExactOldShift();
|
||||||
if(!existing) {
|
if(!existing) {
|
||||||
throw new NotFoundException({
|
throw new NotFoundException({
|
||||||
|
|
@ -167,22 +135,23 @@ export class ShiftsCommandService extends BaseApprovalService<Shifts> {
|
||||||
message: 'The shift was modified or deleted by someone else',
|
message: 'The shift was modified or deleted by someone else',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
await transaction.shifts.delete({ where: { id: existing.id } } );
|
await tx.shifts.delete({ where: { id: existing.id } } );
|
||||||
action = 'deleted';
|
action = 'deleted';
|
||||||
}
|
}
|
||||||
//_____________________________________________________________________________________________
|
//_____________________________________________________________________________________________
|
||||||
// CREATE
|
// CREATE
|
||||||
//_____________________________________________________________________________________________
|
//_____________________________________________________________________________________________
|
||||||
else if (!old_shift && new_shift) {
|
else if (!old_shift && new_shift) {
|
||||||
|
if (new_bank_code_id === undefined) throw new NotFoundException(`bank code not found for new_shift.type: ${new_norm_shift?.type ?? ''}`);
|
||||||
assertNoOverlap();
|
assertNoOverlap();
|
||||||
await transaction.shifts.create({
|
await tx.shifts.create({
|
||||||
data: {
|
data: {
|
||||||
timesheet_id: timesheet.id,
|
timesheet_id: timesheet.id,
|
||||||
date: date_only,
|
date: date_only,
|
||||||
start_time: new_norm!.start_time,
|
start_time: new_norm_shift!.start_time,
|
||||||
end_time: new_norm!.end_time,
|
end_time: new_norm_shift!.end_time,
|
||||||
is_remote: new_norm!.is_remote,
|
is_remote: new_norm_shift!.is_remote,
|
||||||
comment: new_norm!.comment ?? null,
|
comment: new_norm_shift!.comment ?? null,
|
||||||
bank_code_id: new_bank_code_id!,
|
bank_code_id: new_bank_code_id!,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -192,33 +161,29 @@ export class ShiftsCommandService extends BaseApprovalService<Shifts> {
|
||||||
// UPDATE
|
// UPDATE
|
||||||
//_____________________________________________________________________________________________
|
//_____________________________________________________________________________________________
|
||||||
else if (old_shift && new_shift){
|
else if (old_shift && new_shift){
|
||||||
|
if (old_bank_code_id === undefined) throw new NotFoundException(`bank code not found for old_shift.type: ${old_norm_shift?.type ?? ''}`);
|
||||||
|
if (new_bank_code_id === undefined) throw new NotFoundException(`bank code not found for new_shift.type: ${new_norm_shift?.type ?? ''}`);
|
||||||
const existing = await findExactOldShift();
|
const existing = await findExactOldShift();
|
||||||
if(!existing) {
|
if(!existing) throw new NotFoundException({ error_code: 'SHIFT_STALE', message: 'The shift was modified or deleted by someone else'});
|
||||||
throw new NotFoundException({
|
|
||||||
error_code: 'SHIFT_STALE',
|
|
||||||
message: 'The shift was modified or deleted by someone else',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
assertNoOverlap(existing.id);
|
assertNoOverlap(existing.id);
|
||||||
await transaction.shifts.update({
|
|
||||||
|
await tx.shifts.update({
|
||||||
where: {
|
where: {
|
||||||
id: existing.id
|
id: existing.id
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
start_time: new_norm!.start_time,
|
start_time: new_norm_shift!.start_time,
|
||||||
end_time: new_norm!.end_time,
|
end_time: new_norm_shift!.end_time,
|
||||||
is_remote: new_norm!.is_remote,
|
is_remote: new_norm_shift!.is_remote,
|
||||||
comment: new_norm!.comment ?? null,
|
comment: new_norm_shift!.comment ?? null,
|
||||||
bank_code_id: new_bank_code_id,
|
bank_code_id: new_bank_code_id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
action = 'updated';
|
action = 'updated';
|
||||||
} else {
|
} else throw new BadRequestException('At least one of old_shift or new_shift must be provided');
|
||||||
throw new BadRequestException('At least one of old_shift or new_shift must be provided');
|
|
||||||
}
|
|
||||||
|
|
||||||
//Reload the day (truth source)
|
//Reload the day (truth source)
|
||||||
const fresh_day = await transaction.shifts.findMany({
|
const fresh_day = await tx.shifts.findMany({
|
||||||
where: {
|
where: {
|
||||||
date: date_only,
|
date: date_only,
|
||||||
timesheet_id: timesheet.id,
|
timesheet_id: timesheet.id,
|
||||||
|
|
@ -242,6 +207,5 @@ export class ShiftsCommandService extends BaseApprovalService<Shifts> {
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2,7 +2,7 @@ import { Injectable, NotFoundException } from "@nestjs/common";
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
import { NotificationsService } from "src/modules/notifications/services/notifications.service";
|
import { NotificationsService } from "src/modules/notifications/services/notifications.service";
|
||||||
import { computeHours } from "src/common/utils/date-utils";
|
import { computeHours } from "src/common/utils/date-utils";
|
||||||
import { OverviewRow } from "../types and interfaces/shifts-overview-row.interface";
|
import { OverviewRow } from "../types-and-interfaces/shifts-overview-row.interface";
|
||||||
|
|
||||||
// const DAILY_LIMIT_HOURS = Number(process.env.DAILY_LIMIT_HOURS ?? 12);
|
// const DAILY_LIMIT_HOURS = Number(process.env.DAILY_LIMIT_HOURS ?? 12);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
import { BadRequestException, Body, Controller, Delete, Get, Param, ParseBoolPipe, ParseIntPipe, Patch, Post, Query } from '@nestjs/common';
|
import { BadRequestException, Body, Controller, Get, Param, ParseIntPipe, Post, Query } from '@nestjs/common';
|
||||||
import { TimesheetsQueryService } from '../services/timesheets-query.service';
|
import { TimesheetsQueryService } from '../services/timesheets-query.service';
|
||||||
import { CreateTimesheetDto, CreateWeekShiftsDto } from '../dtos/create-timesheet.dto';
|
import { CreateWeekShiftsDto } from '../dtos/create-timesheet.dto';
|
||||||
import { Timesheets } from '@prisma/client';
|
|
||||||
import { RolesAllowed } from "src/common/decorators/roles.decorators";
|
import { RolesAllowed } from "src/common/decorators/roles.decorators";
|
||||||
import { Roles as RoleEnum } from '.prisma/client';
|
import { Roles as RoleEnum } from '.prisma/client';
|
||||||
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||||
import { TimesheetsCommandService } from '../services/timesheets-command.service';
|
import { TimesheetsCommandService } from '../services/timesheets-command.service';
|
||||||
import { TimesheetPeriodDto } from '../dtos/timesheet-period.dto';
|
import { TimesheetDto, TimesheetPeriodDto } from '../dtos/timesheet-period.dto';
|
||||||
import { TimesheetDto } from '../dtos/overview-timesheet.dto';
|
|
||||||
|
|
||||||
@ApiTags('Timesheets')
|
@ApiTags('Timesheets')
|
||||||
@ApiBearerAuth('access-token')
|
@ApiBearerAuth('access-token')
|
||||||
|
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
export class TimesheetDto {
|
|
||||||
is_approved: boolean;
|
|
||||||
start_day: string;
|
|
||||||
end_day: string;
|
|
||||||
label: string;
|
|
||||||
shifts: ShiftsDto[];
|
|
||||||
expenses: ExpensesDto[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ShiftsDto {
|
|
||||||
bank_type: string;
|
|
||||||
date: string;
|
|
||||||
start_time: string;
|
|
||||||
end_time: string;
|
|
||||||
comment: string;
|
|
||||||
is_approved: boolean;
|
|
||||||
is_remote: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ExpensesDto {
|
|
||||||
bank_type: string;
|
|
||||||
date: string;
|
|
||||||
amount: number;
|
|
||||||
mileage: number;
|
|
||||||
comment: string;
|
|
||||||
supervisor_comment: string;
|
|
||||||
is_approved: boolean;
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +1,12 @@
|
||||||
|
export class TimesheetDto {
|
||||||
|
start_day: string;
|
||||||
|
end_day: string;
|
||||||
|
label: string;
|
||||||
|
shifts: ShiftDto[];
|
||||||
|
expenses: ExpenseDto[]
|
||||||
|
is_approved: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export class ShiftDto {
|
export class ShiftDto {
|
||||||
date: string;
|
date: string;
|
||||||
type: string;
|
type: string;
|
||||||
|
|
@ -31,7 +40,7 @@ export class DetailedShifts {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DayExpensesDto {
|
export class DayExpensesDto {
|
||||||
expenses: ExpenseDto[];
|
expenses: ExpenseDto[] = [];
|
||||||
total_mileage: number;
|
total_mileage: number;
|
||||||
total_expense: number;
|
total_expense: number;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
import { PartialType } from "@nestjs/swagger";
|
|
||||||
import { CreateTimesheetDto } from "./create-timesheet.dto";
|
|
||||||
|
|
||||||
export class UpdateTimesheetDto extends PartialType(CreateTimesheetDto) {}
|
|
||||||
|
|
@ -4,15 +4,21 @@ import { BaseApprovalService } from "src/common/shared/base-approval.service";
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
import { TimesheetsQueryService } from "./timesheets-query.service";
|
import { TimesheetsQueryService } from "./timesheets-query.service";
|
||||||
import { CreateTimesheetDto } from "../dtos/create-timesheet.dto";
|
import { CreateTimesheetDto } from "../dtos/create-timesheet.dto";
|
||||||
import { TimesheetDto } from "../dtos/overview-timesheet.dto";
|
|
||||||
import { getWeekEnd, getWeekStart } from "src/common/utils/date-utils";
|
import { getWeekEnd, getWeekStart } from "src/common/utils/date-utils";
|
||||||
import { parseISODate, parseHHmm } from "../utils/timesheet.helpers";
|
import { parseISODate, parseHHmm } from "../utils/timesheet.helpers";
|
||||||
|
import { TimesheetDto } from "../dtos/timesheet-period.dto";
|
||||||
|
import { EmployeeIdEmailResolver } from "src/modules/shared/utils/resolve-email-id.utils";
|
||||||
|
import { EmployeeTimesheetResolver } from "src/modules/shared/utils/resolve-employee-timesheet.utils";
|
||||||
|
import { BankCodesResolver } from "src/modules/shared/utils/resolve-bank-type-id.utils";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TimesheetsCommandService extends BaseApprovalService<Timesheets>{
|
export class TimesheetsCommandService extends BaseApprovalService<Timesheets>{
|
||||||
constructor(
|
constructor(
|
||||||
prisma: PrismaService,
|
prisma: PrismaService,
|
||||||
private readonly query: TimesheetsQueryService,
|
private readonly query: TimesheetsQueryService,
|
||||||
|
private readonly emailResolver: EmployeeIdEmailResolver,
|
||||||
|
private readonly timesheetResolver: EmployeeTimesheetResolver,
|
||||||
|
private readonly bankTypeResolver: BankCodesResolver,
|
||||||
) {super(prisma);}
|
) {super(prisma);}
|
||||||
//_____________________________________________________________________________________________
|
//_____________________________________________________________________________________________
|
||||||
// APPROVAL AND DELEGATE METHODS
|
// APPROVAL AND DELEGATE METHODS
|
||||||
|
|
@ -33,17 +39,14 @@ export class TimesheetsCommandService extends BaseApprovalService<Timesheets>{
|
||||||
|
|
||||||
async cascadeApprovalWithtx(transaction: Prisma.TransactionClient, timesheetId: number, isApproved: boolean): Promise<Timesheets> {
|
async cascadeApprovalWithtx(transaction: Prisma.TransactionClient, timesheetId: number, isApproved: boolean): Promise<Timesheets> {
|
||||||
const timesheet = await this.updateApprovalWithTransaction(transaction, timesheetId, isApproved);
|
const timesheet = await this.updateApprovalWithTransaction(transaction, timesheetId, isApproved);
|
||||||
|
|
||||||
await transaction.shifts.updateMany({
|
await transaction.shifts.updateMany({
|
||||||
where: { timesheet_id: timesheetId },
|
where: { timesheet_id: timesheetId },
|
||||||
data: { is_approved: isApproved },
|
data: { is_approved: isApproved },
|
||||||
});
|
});
|
||||||
|
|
||||||
await transaction.expenses.updateManyAndReturn({
|
await transaction.expenses.updateManyAndReturn({
|
||||||
where: { timesheet_id: timesheetId },
|
where: { timesheet_id: timesheetId },
|
||||||
data: { is_approved: isApproved },
|
data: { is_approved: isApproved },
|
||||||
});
|
});
|
||||||
|
|
||||||
return timesheet;
|
return timesheet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -56,20 +59,9 @@ export class TimesheetsCommandService extends BaseApprovalService<Timesheets>{
|
||||||
shifts: CreateTimesheetDto[],
|
shifts: CreateTimesheetDto[],
|
||||||
week_offset = 0,
|
week_offset = 0,
|
||||||
): Promise<TimesheetDto> {
|
): Promise<TimesheetDto> {
|
||||||
|
|
||||||
//match user's email with email
|
|
||||||
const user = await this.prisma.users.findUnique({
|
|
||||||
where: { email },
|
|
||||||
select: { id: true },
|
|
||||||
});
|
|
||||||
if(!user) throw new NotFoundException(`user with email ${email} not found`);
|
|
||||||
|
|
||||||
//fetchs employee matchint user's email
|
//fetchs employee matchint user's email
|
||||||
const employee = await this.prisma.employees.findFirst({
|
const employee_id = await this.emailResolver.findIdByEmail(email);
|
||||||
where: { user_id: user?.id },
|
if(!employee_id) throw new NotFoundException(`employee for ${ email } not found`);
|
||||||
select: { id: true },
|
|
||||||
});
|
|
||||||
if(!employee) throw new NotFoundException(`employee for ${ email } not found`);
|
|
||||||
|
|
||||||
//insure that the week starts on sunday and finishes on saturday
|
//insure that the week starts on sunday and finishes on saturday
|
||||||
const base = new Date();
|
const base = new Date();
|
||||||
|
|
@ -77,31 +69,15 @@ export class TimesheetsCommandService extends BaseApprovalService<Timesheets>{
|
||||||
const start_week = getWeekStart(base, 0);
|
const start_week = getWeekStart(base, 0);
|
||||||
const end_week = getWeekEnd(start_week);
|
const end_week = getWeekEnd(start_week);
|
||||||
|
|
||||||
const timesheet = await this.prisma.timesheets.upsert({
|
const timesheet = await this.timesheetResolver.ensureForDate(employee_id, base)
|
||||||
where: {
|
if(!timesheet) throw new NotFoundException(`no timesheet found for employe ${employee_id}`);
|
||||||
employee_id_start_date: {
|
|
||||||
employee_id: employee.id,
|
|
||||||
start_date: start_week,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
employee_id: employee.id,
|
|
||||||
start_date: start_week,
|
|
||||||
is_approved: false,
|
|
||||||
},
|
|
||||||
update: {},
|
|
||||||
select: { id: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
//validations and insertions
|
//validations and insertions
|
||||||
for(const shift of shifts) {
|
for(const shift of shifts) {
|
||||||
const date = parseISODate(shift.date);
|
const date = parseISODate(shift.date);
|
||||||
if (date < start_week || date > end_week) throw new BadRequestException(`date ${shift.date} not in current week`);
|
if (date < start_week || date > end_week) throw new BadRequestException(`date ${shift.date} not in current week`);
|
||||||
|
|
||||||
const bank_code = await this.prisma.bankCodes.findFirst({
|
const bank_code = await this.bankTypeResolver.findByType(shift.type)
|
||||||
where: { type: shift.type },
|
|
||||||
select: { id: true },
|
|
||||||
});
|
|
||||||
if(!bank_code) throw new BadRequestException(`Invalid bank_code type: ${shift.type}`);
|
if(!bank_code) throw new BadRequestException(`Invalid bank_code type: ${shift.type}`);
|
||||||
|
|
||||||
await this.prisma.shifts.create({
|
await this.prisma.shifts.create({
|
||||||
|
|
|
||||||
|
|
@ -2,42 +2,29 @@ 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 { TimesheetDto, TimesheetPeriodDto } from '../dtos/timesheet-period.dto';
|
||||||
import { TimesheetDto } from '../dtos/overview-timesheet.dto';
|
|
||||||
import { TimesheetPeriodDto } from '../dtos/timesheet-period.dto';
|
|
||||||
import { ShiftRow, ExpenseRow } from '../types/timesheet.types';
|
import { ShiftRow, ExpenseRow } from '../types/timesheet.types';
|
||||||
import { buildPeriod } from '../utils/timesheet.utils';
|
import { buildPeriod } from '../utils/timesheet.utils';
|
||||||
|
import { EmployeeIdEmailResolver } from 'src/modules/shared/utils/resolve-email-id.utils';
|
||||||
|
import { FullNameResolver } from 'src/modules/shared/utils/resolve-full-name.utils';
|
||||||
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TimesheetsQueryService {
|
export class TimesheetsQueryService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
// private readonly overtime: OvertimeService,
|
private readonly emailResolver: EmployeeIdEmailResolver,
|
||||||
|
private readonly fullNameResolver: FullNameResolver
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
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 using email
|
||||||
const employee = await this.prisma.employees.findFirst({
|
const employee_id = await this.emailResolver.findIdByEmail(email);
|
||||||
where: {
|
if(!employee_id) throw new NotFoundException(`employee with email : ${email} not found`);
|
||||||
user: { is: { email } }
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
user_id: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if(!employee) throw new NotFoundException(`no employee with email ${email} found`);
|
|
||||||
|
|
||||||
//gets the employee's full name
|
//finds the employee full name using employee_id
|
||||||
const user = await this.prisma.users.findFirst({
|
const full_name = await this.fullNameResolver.resolveFullName(employee_id);
|
||||||
where: { id: employee.user_id },
|
if(!full_name) throw new NotFoundException(`employee with id: ${employee_id} not found`)
|
||||||
select: {
|
|
||||||
first_name: true,
|
|
||||||
last_name: true,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const employee_full_name: string = ( user?.first_name + " " + user?.last_name ) || " ";
|
|
||||||
|
|
||||||
//finds the period
|
//finds the period
|
||||||
const period = await this.prisma.payPeriods.findFirst({
|
const period = await this.prisma.payPeriods.findFirst({
|
||||||
|
|
@ -57,7 +44,7 @@ export class TimesheetsQueryService {
|
||||||
|
|
||||||
const raw_shifts = await this.prisma.shifts.findMany({
|
const raw_shifts = await this.prisma.shifts.findMany({
|
||||||
where: {
|
where: {
|
||||||
timesheet: { is: { employee_id: employee.id } },
|
timesheet: { is: { employee_id: employee_id } },
|
||||||
date: { gte: from, lte: to },
|
date: { gte: from, lte: to },
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
|
|
@ -74,7 +61,7 @@ export class TimesheetsQueryService {
|
||||||
|
|
||||||
const raw_expenses = await this.prisma.expenses.findMany({
|
const raw_expenses = await this.prisma.expenses.findMany({
|
||||||
where: {
|
where: {
|
||||||
timesheet: { is: { employee_id: employee.id } },
|
timesheet: { is: { employee_id: employee_id } },
|
||||||
date: { gte: from, lte: to },
|
date: { gte: from, lte: to },
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
|
|
@ -115,24 +102,12 @@ export class TimesheetsQueryService {
|
||||||
supervisor_comment: expense.supervisor_comment ?? '',
|
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, full_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTimesheetByEmail(email: string, week_offset = 0): Promise<TimesheetDto> {
|
async getTimesheetByEmail(email: string, week_offset = 0): Promise<TimesheetDto> {
|
||||||
|
const employee_id = await this.emailResolver.findIdByEmail(email);
|
||||||
//fetch user related to email
|
if(!employee_id) throw new NotFoundException(`Employee with email: ${email} not found`);
|
||||||
const user = await this.prisma.users.findUnique({
|
|
||||||
where: { email },
|
|
||||||
select: { id: true },
|
|
||||||
});
|
|
||||||
if(!user) throw new NotFoundException(`user with email ${email} not found`);
|
|
||||||
|
|
||||||
//fetch employee_id matching the email
|
|
||||||
const employee = await this.prisma.employees.findFirst({
|
|
||||||
where: { user_id: user.id },
|
|
||||||
select: { id: true },
|
|
||||||
});
|
|
||||||
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();
|
||||||
|
|
@ -152,7 +127,7 @@ export class TimesheetsQueryService {
|
||||||
const timesheet = await this.prisma.timesheets.findUnique({
|
const timesheet = await this.prisma.timesheets.findUnique({
|
||||||
where: {
|
where: {
|
||||||
employee_id_start_date: {
|
employee_id_start_date: {
|
||||||
employee_id: employee.id,
|
employee_id: employee_id,
|
||||||
start_date: start_date_week,
|
start_date: start_date_week,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -182,7 +157,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 ?? '',
|
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),
|
||||||
end_time: toHHmm(shift_row.end_time),
|
end_time: toHHmm(shift_row.end_time),
|
||||||
|
|
@ -193,7 +168,7 @@ export class TimesheetsQueryService {
|
||||||
|
|
||||||
//maps all expenses of selected timsheet
|
//maps all expenses of selected timsheet
|
||||||
const expenses = timesheet.expense.map((exp) => ({
|
const expenses = timesheet.expense.map((exp) => ({
|
||||||
bank_type: exp.bank_code?.type ?? '',
|
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,
|
mileage: exp.mileage != null ? Number(exp.mileage) : 0,
|
||||||
|
|
@ -203,12 +178,12 @@ export class TimesheetsQueryService {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
is_approved: timesheet.is_approved,
|
|
||||||
start_day,
|
start_day,
|
||||||
end_day,
|
end_day,
|
||||||
label,
|
label,
|
||||||
shifts,
|
shifts,
|
||||||
expenses,
|
expenses,
|
||||||
|
is_approved: timesheet.is_approved,
|
||||||
} as TimesheetDto;
|
} as TimesheetDto;
|
||||||
}
|
}
|
||||||
//_____________________________________________________________________________________________
|
//_____________________________________________________________________________________________
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,11 @@ import { TimesheetsCommandService } from './services/timesheets-command.service'
|
||||||
import { ShiftsCommandService } from '../shifts/services/shifts-command.service';
|
import { ShiftsCommandService } from '../shifts/services/shifts-command.service';
|
||||||
import { ExpensesCommandService } from '../expenses/services/expenses-command.service';
|
import { ExpensesCommandService } from '../expenses/services/expenses-command.service';
|
||||||
import { BusinessLogicsModule } from 'src/modules/business-logics/business-logics.module';
|
import { BusinessLogicsModule } from 'src/modules/business-logics/business-logics.module';
|
||||||
import { BankCodesRepo } from '../expenses/repos/bank-codes.repo';
|
import { SharedModule } from '../shared/shared.module';
|
||||||
import { TimesheetsRepo } from '../expenses/repos/timesheets.repo';
|
|
||||||
import { EmployeesRepo } from '../expenses/repos/employee.repo';
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [BusinessLogicsModule],
|
imports: [BusinessLogicsModule, SharedModule],
|
||||||
controllers: [TimesheetsController],
|
controllers: [TimesheetsController],
|
||||||
providers: [
|
providers: [
|
||||||
TimesheetsQueryService,
|
TimesheetsQueryService,
|
||||||
|
|
@ -19,9 +17,7 @@ import { Module } from '@nestjs/common';
|
||||||
ShiftsCommandService,
|
ShiftsCommandService,
|
||||||
ExpensesCommandService,
|
ExpensesCommandService,
|
||||||
TimesheetArchiveService,
|
TimesheetArchiveService,
|
||||||
BankCodesRepo,
|
|
||||||
TimesheetsRepo,
|
|
||||||
EmployeesRepo,
|
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
TimesheetsQueryService,
|
TimesheetsQueryService,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user