refactor(shifts): refactor main upsert function to use shared utils and helpers

This commit is contained in:
Matthieu Haineault 2025-10-08 08:54:43 -04:00
parent cc310e286d
commit f6c5b2a73c
24 changed files with 312 additions and 438 deletions

View File

@ -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 } },

View File

@ -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,
], ],
}) })

View File

@ -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);
} }

View File

@ -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,
) {} ) {}

View File

@ -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: [

View 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 {}

View File

@ -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,
};
}; };
} }

View File

@ -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;
} }
} }

View File

@ -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

View 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;
}
}

View File

@ -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')

View File

@ -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 {

View File

@ -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;
} }
} }

View File

@ -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);

View File

@ -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')

View File

@ -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;
}

View File

@ -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;
} }

View File

@ -1,4 +0,0 @@
import { PartialType } from "@nestjs/swagger";
import { CreateTimesheetDto } from "./create-timesheet.dto";
export class UpdateTimesheetDto extends PartialType(CreateTimesheetDto) {}

View File

@ -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({

View File

@ -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;
} }
//_____________________________________________________________________________________________ //_____________________________________________________________________________________________

View File

@ -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,