refactor(timesheets): deep refactor of the timesheet module and small corrections of the shift module.
This commit is contained in:
parent
b7ad300a6e
commit
11f6cf2049
|
|
@ -1,5 +1,5 @@
|
||||||
import { BadRequestException, Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, Query } from "@nestjs/common";
|
import { BadRequestException, Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, Query } from "@nestjs/common";
|
||||||
import { CreateResult, DeleteResult, ShiftsUpsertService, ShiftWithOvertimeDto, UpdateResult } from "../services/shifts-upsert.service";
|
import { CreateResult, ShiftsUpsertService, UpdateResult } from "../services/shifts-upsert.service";
|
||||||
import { updateShiftDto } from "../dtos/update-shift.dto";
|
import { updateShiftDto } from "../dtos/update-shift.dto";
|
||||||
import { ShiftDto } from "../dtos/shift.dto";
|
import { ShiftDto } from "../dtos/shift.dto";
|
||||||
import { ShiftsGetService } from "../services/shifts-get.service";
|
import { ShiftsGetService } from "../services/shifts-get.service";
|
||||||
|
|
@ -12,10 +12,10 @@ export class ShiftController {
|
||||||
private readonly get_service: ShiftsGetService
|
private readonly get_service: ShiftsGetService
|
||||||
){}
|
){}
|
||||||
|
|
||||||
@Get("shifts")
|
@Get()
|
||||||
async getShiftsByIds(
|
async getShiftsByIds(
|
||||||
@Query("shift_ids") shift_ids: string) {
|
@Query("shift_ids") shift_ids: string) {
|
||||||
const parsed = shift_ids.split(", ").map(value => Number(value)).filter(Number.isFinite);
|
const parsed = shift_ids.split(/,\s*/).map(value => Number(value)).filter(Number.isFinite);
|
||||||
return this.get_service.getShiftByShiftId(parsed);
|
return this.get_service.getShiftByShiftId(parsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -42,15 +42,8 @@ export class ShiftController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':shift_id')
|
@Delete(':shift_id')
|
||||||
removeBatch(
|
remove(@Param('shift_id') shift_id: number ) {
|
||||||
@Body() body: { ids: number[] }): Promise<DeleteResult[]> {
|
return this.upsert_service.deleteShift(shift_id);
|
||||||
const ids = Array.isArray(body?.ids)
|
|
||||||
? body.ids.filter((value) => Number.isFinite(value))
|
|
||||||
: [];
|
|
||||||
if( ids.length === 0) {
|
|
||||||
throw new BadRequestException('Body is missing or invalid (delete shifts)');
|
|
||||||
}
|
|
||||||
return this.upsert_service.deleteShifts(ids);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -39,11 +39,6 @@ export class ShiftsUpsertService {
|
||||||
//checks for overlaping shifts
|
//checks for overlaping shifts
|
||||||
//create new shifts
|
//create new shifts
|
||||||
//calculate overtime
|
//calculate overtime
|
||||||
async createShift(timesheet_id: number, dto: ShiftDto): Promise<ShiftWithOvertimeDto> {
|
|
||||||
const [result] = await this.createShifts(timesheet_id, [dto]);
|
|
||||||
if (!result.ok) throw result.error;
|
|
||||||
return result.data;
|
|
||||||
}
|
|
||||||
async createShifts(timesheet_id: number, dtos: ShiftDto[]): Promise<CreateResult[]> {
|
async createShifts(timesheet_id: number, dtos: ShiftDto[]): Promise<CreateResult[]> {
|
||||||
if (!Array.isArray(dtos) || dtos.length === 0) return [];
|
if (!Array.isArray(dtos) || dtos.length === 0) return [];
|
||||||
|
|
||||||
|
|
@ -179,13 +174,6 @@ export class ShiftsUpsertService {
|
||||||
// update shifts in DB
|
// update shifts in DB
|
||||||
// recalculate overtime after update
|
// recalculate overtime after update
|
||||||
// return an updated version to display
|
// return an updated version to display
|
||||||
|
|
||||||
async updateShift(shift_id: number, dto: updateShiftDto): Promise<ShiftWithOvertimeDto> {
|
|
||||||
const [results] = await this.updateShifts([{ id: shift_id, dto }]);
|
|
||||||
if (!results.ok) throw results.error;
|
|
||||||
return results.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateShifts(updates: UpdatePayload[]): Promise<UpdateResult[]> {
|
async updateShifts(updates: UpdatePayload[]): Promise<UpdateResult[]> {
|
||||||
if (!Array.isArray(updates) || updates.length === 0) return [];
|
if (!Array.isArray(updates) || updates.length === 0) return [];
|
||||||
|
|
||||||
|
|
@ -345,43 +333,24 @@ export class ShiftsUpsertService {
|
||||||
//_________________________________________________________________
|
//_________________________________________________________________
|
||||||
// DELETE
|
// DELETE
|
||||||
//_________________________________________________________________
|
//_________________________________________________________________
|
||||||
//finds shifts using shit_ids
|
//finds shift using shit_ids
|
||||||
//recalc overtime shifts after delete
|
//recalc overtime shifts after delete
|
||||||
//blocs deletion if approved
|
//blocs deletion if approved
|
||||||
async deleteShift(shift_id: number) {
|
async deleteShift(shift_id: number) {
|
||||||
const [result] = await this.deleteShifts([shift_id]);
|
return await this.prisma.$transaction(async (tx) =>{
|
||||||
if (!result.ok) throw result.error;
|
const shift = await tx.shifts.findUnique({
|
||||||
return { success: true, overtime: result.overtime };
|
where: { id: shift_id },
|
||||||
}
|
select: { id: true, date: true, timesheet_id: true },
|
||||||
async deleteShifts(shift_ids: number[]): Promise<DeleteResult[]> {
|
|
||||||
if (!Array.isArray(shift_ids) || shift_ids.length === 0) return [];
|
|
||||||
|
|
||||||
return this.prisma.$transaction(async (tx) => {
|
|
||||||
const rows = await tx.shifts.findMany({
|
|
||||||
where: { id: { in: shift_ids } },
|
|
||||||
select: { id: true, date: true, timesheet_id: true, is_approved: true },
|
|
||||||
});
|
});
|
||||||
const byId = new Map(rows.map(row => [row.id, row]));
|
if(!shift) throw new NotFoundException(`Shift with id #${shift_id} not found`);
|
||||||
|
|
||||||
for (const id of shift_ids) {
|
await tx.shifts.delete({ where: { id: shift_id } });
|
||||||
const row = byId.get(id);
|
|
||||||
if (!row) {
|
|
||||||
return shift_ids.map(existing_shift_id =>
|
|
||||||
existing_shift_id === id
|
|
||||||
? ({ ok: false, id: existing_shift_id, error: new NotFoundException(`Shift with id #${existing_shift_id} not found`) } as DeleteResult)
|
|
||||||
: ({ ok: false, id: existing_shift_id, error: new BadRequestException('Batch aborted due to missing shift') })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const results: DeleteResult[] = [];
|
const summary = await this.overtime.getWeekOvertimeSummary( shift.timesheet_id, shift.date, tx);
|
||||||
for (const id of shift_ids) {
|
return {
|
||||||
const row = byId.get(id)!;
|
success: true,
|
||||||
await tx.shifts.delete({ where: { id } });
|
overtime: summary
|
||||||
const summary = await this.overtime.getWeekOvertimeSummary(row.timesheet_id, row.date, tx);
|
};
|
||||||
results.push({ ok: true, id, overtime: summary });
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,16 @@
|
||||||
import { Controller} from "@nestjs/common";
|
import { GetTimesheetsOverviewService } from "../services/timesheet-get-overview.service";
|
||||||
|
import { Controller, Get, Query} from "@nestjs/common";
|
||||||
|
|
||||||
@Controller('timesheet')
|
@Controller('timesheets')
|
||||||
export class TimesheetController {
|
export class TimesheetController {
|
||||||
constructor(){}
|
constructor(private readonly timesheetOverview: GetTimesheetsOverviewService){}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
async getTimesheetByIds(
|
||||||
|
@Query('timesheet_ids') timesheet_ids: string ) {
|
||||||
|
const parsed = timesheet_ids.split(/,\s*/).map(value => Number(value)).filter(Number.isFinite);
|
||||||
|
return this.timesheetOverview.getTimesheetsByIds(parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,3 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export class Timesheets {
|
export class Timesheets {
|
||||||
employee_fullname: string;
|
employee_fullname: string;
|
||||||
timesheets: Timesheet[];
|
timesheets: Timesheet[];
|
||||||
|
|
@ -35,7 +30,7 @@ export class TotalHours {
|
||||||
}
|
}
|
||||||
export class TotalExpenses {
|
export class TotalExpenses {
|
||||||
expenses: number;
|
expenses: number;
|
||||||
perd_diem: number;
|
per_diem: number;
|
||||||
on_call: number;
|
on_call: number;
|
||||||
mileage: number;
|
mileage: number;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
export const toDateFromString = ( date: Date | string):Date => {
|
||||||
|
const d = new Date(date);
|
||||||
|
return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sevenDaysFrom = (date: Date | string): Date[] => {
|
||||||
|
return Array.from({length: 7 }, (_,i) => {
|
||||||
|
const d = new Date(date);
|
||||||
|
d.setUTCDate(d.getUTCDate() + i );
|
||||||
|
return d;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export const toStringFromDate = (date: Date | string): string => {
|
||||||
|
const d = toDateFromString(date);
|
||||||
|
const year = d.getUTCFullYear();
|
||||||
|
const month = String(d.getUTCMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(d.getUTCDate()).padStart(2, '0');
|
||||||
|
return `${year}-${month}-${d}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const toHHmmFromDate = (input: Date | string): string => {
|
||||||
|
const date = new Date(input);
|
||||||
|
const hh = String(date.getUTCHours()).padStart(2, '0');
|
||||||
|
const mm = String(date.getUTCMinutes()).padStart(2, '0');
|
||||||
|
return `${hh}:${mm}`;
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,192 @@
|
||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
import { sevenDaysFrom, toDateFromString, toHHmmFromDate, toStringFromDate } from "../helpers/timesheets-date-time-helpers";
|
||||||
|
|
||||||
|
type TotalHours = {
|
||||||
|
regular: number;
|
||||||
|
evening: number;
|
||||||
|
emergency: number;
|
||||||
|
overtime: number;
|
||||||
|
vacation: number;
|
||||||
|
holiday: number;
|
||||||
|
sick: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TotalExpenses = {
|
||||||
|
expenses: number;
|
||||||
|
per_diem: number;
|
||||||
|
on_call: number;
|
||||||
|
mileage: number;
|
||||||
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GetTimesheetsOverviewService {
|
export class GetTimesheetsOverviewService {
|
||||||
constructor(private readonly prisma: PrismaService){}
|
constructor(private readonly prisma: PrismaService) { }
|
||||||
|
|
||||||
|
async getTimesheetsByIds(timesheet_ids: number[]) {
|
||||||
|
if (!Array.isArray(timesheet_ids) || timesheet_ids.length === 0) throw new NotFoundException(`Timesheet_ids are missing`);
|
||||||
|
|
||||||
|
//fetch all needed data using timesheet ids
|
||||||
|
const rows = await this.prisma.timesheets.findMany({
|
||||||
|
where: { id: { in: timesheet_ids } },
|
||||||
|
include: {
|
||||||
|
employee: { include: { user: true } },
|
||||||
|
shift: { include: { bank_code: true } },
|
||||||
|
expense: { include: { bank_code: true, attachment_record: true } },
|
||||||
|
},
|
||||||
|
orderBy: { start_date: 'asc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (rows.length === 0) throw new NotFoundException('Timesheet(s) not found');
|
||||||
|
|
||||||
|
//build full name
|
||||||
|
const user = rows[0].employee.user;
|
||||||
|
const employee_fullname = `${user.first_name} ${user.last_name}`.trim();
|
||||||
|
|
||||||
|
const timesheets = rows.map((timesheet) => this.mapOneTimesheet(timesheet));
|
||||||
|
return { employee_fullname, timesheets };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------------
|
||||||
|
// MAPPERS & HELPERS
|
||||||
|
//-----------------------------------------------------------------------------------
|
||||||
|
private mapOneTimesheet(timesheet: any) {
|
||||||
|
//converts string to UTC date format
|
||||||
|
const start = toDateFromString(timesheet.start_date);
|
||||||
|
const day_dates = sevenDaysFrom(start);
|
||||||
|
|
||||||
|
//map of shifts by days
|
||||||
|
const shifts_by_date = new Map<string, any[]>();
|
||||||
|
for (const shift of timesheet.shift) {
|
||||||
|
const date = toStringFromDate(shift.date);
|
||||||
|
const arr = shifts_by_date.get(date) ?? [];
|
||||||
|
arr.push(shift);
|
||||||
|
shifts_by_date.set(date, arr);
|
||||||
|
}
|
||||||
|
//map of expenses by days
|
||||||
|
const expenses_by_date = new Map<string, any[]>();
|
||||||
|
for (const expense of timesheet.expense) {
|
||||||
|
const date = toStringFromDate(expense.date);
|
||||||
|
const arr = expenses_by_date.get(date) ?? [];
|
||||||
|
arr.push(expense);
|
||||||
|
expenses_by_date.set(date, arr);
|
||||||
|
}
|
||||||
|
//weekly totals
|
||||||
|
const weekly_hours: TotalHours[] = [emptyHours()];
|
||||||
|
const weekly_expenses: TotalExpenses[] = [emptyExpenses()];
|
||||||
|
|
||||||
|
//map of days
|
||||||
|
const days = day_dates.map((date) => {
|
||||||
|
const date_iso = toStringFromDate(date);
|
||||||
|
const shifts_source = shifts_by_date.get(date_iso) ?? [];
|
||||||
|
const expenses_source = expenses_by_date.get(date_iso) ?? [];
|
||||||
|
//inner map of shifts
|
||||||
|
const shifts = shifts_source.map((shift) => ({
|
||||||
|
date: toStringFromDate(shift.date),
|
||||||
|
start_time: toHHmmFromDate(shift.start_time),
|
||||||
|
end_time: toHHmmFromDate(shift.end_time),
|
||||||
|
type: shift.bank_code?.type ?? '',
|
||||||
|
is_remote: shift.is_remote ?? false,
|
||||||
|
is_approved: shift.is_approved ?? false,
|
||||||
|
shift_id: shift.id ?? null,
|
||||||
|
comment: shift.comment ?? null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
//inner map of expenses
|
||||||
|
const expenses = expenses_source.map((expense) => ({
|
||||||
|
date: toStringFromDate(expense.date),
|
||||||
|
amount: expense.amount ? Number(expense.amount) : undefined,
|
||||||
|
mileage: expense.mileage ? Number(expense.mileage) : undefined,
|
||||||
|
expense_id: expense.id ?? null,
|
||||||
|
attachment: expense.attachment_record ? String(expense.attachment_record.id) : undefined,
|
||||||
|
is_approved: expense.is_approved ?? false,
|
||||||
|
comment: expense.comment ?? '',
|
||||||
|
supervisor_comment: expense.supervisor_comment,
|
||||||
|
}));
|
||||||
|
|
||||||
|
//daily totals
|
||||||
|
const daily_hours = [emptyHours()];
|
||||||
|
const daily_expenses = [emptyExpenses()];
|
||||||
|
|
||||||
|
//totals by shift types
|
||||||
|
for (const shift of shifts_source) {
|
||||||
|
const hours = diffOfHours(shift.start_time, shift.end_time);
|
||||||
|
const subgroup = hoursSubGroupFromBankCode(shift.bank_code);
|
||||||
|
daily_hours[0][subgroup] += hours;
|
||||||
|
weekly_hours[0][subgroup] += hours;
|
||||||
|
}
|
||||||
|
|
||||||
|
//totals by expense types
|
||||||
|
for (const expense of expenses_source) {
|
||||||
|
const subgroup = expenseSubgroupFromBankCode(expense.bank_code);
|
||||||
|
if (subgroup === 'mileage') {
|
||||||
|
const mileage = num(expense.mileage);
|
||||||
|
daily_expenses[0].mileage += mileage;
|
||||||
|
weekly_expenses[0].mileage += mileage;
|
||||||
|
} else if (subgroup === 'per_diem') {
|
||||||
|
const amount = num(expense.amount);
|
||||||
|
daily_expenses[0].per_diem += amount;
|
||||||
|
weekly_expenses[0].per_diem += amount;
|
||||||
|
} else if (subgroup === 'on_call') {
|
||||||
|
const amount = num(expense.amount);
|
||||||
|
daily_expenses[0].on_call += amount;
|
||||||
|
weekly_expenses[0].on_call += amount;
|
||||||
|
} else {
|
||||||
|
const amount = num(expense.amount);
|
||||||
|
daily_expenses[0].expenses += amount;
|
||||||
|
weekly_expenses[0].expenses += amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
date: date_iso,
|
||||||
|
shifts,
|
||||||
|
expenses,
|
||||||
|
daily_hours,
|
||||||
|
daily_expenses,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
timesheet_id: timesheet.id,
|
||||||
|
is_approved: timesheet.is_approved ?? false,
|
||||||
|
days,
|
||||||
|
weekly_hours,
|
||||||
|
weekly_expenses,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const emptyHours = (): TotalHours => {
|
||||||
|
return { regular: 0, evening: 0, emergency: 0, overtime: 0, vacation: 0, holiday: 0, sick: 0 };
|
||||||
|
}
|
||||||
|
const emptyExpenses = (): TotalExpenses => {
|
||||||
|
return { expenses: 0, per_diem: 0, on_call: 0, mileage: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const diffOfHours = (a: Date, b: Date): number => {
|
||||||
|
const ms = new Date(b).getTime() - new Date(a).getTime();
|
||||||
|
return Math.max(0, Math.round((ms / 36e5) * 1000) / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
const num = (value: any): number => {
|
||||||
|
return value ? Number(value) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hoursSubGroupFromBankCode = (bank_code: any): keyof TotalHours => {
|
||||||
|
const type = bank_code.type;
|
||||||
|
if (type.includes('EVENING')) return 'evening';
|
||||||
|
if (type.includes('EMERGENCY')) return 'emergency';
|
||||||
|
if (type.includes('OVERTIME')) return 'overtime';
|
||||||
|
if (type.includes('VACATION')) return 'vacation';
|
||||||
|
if (type.includes('HOLIDAY')) return 'holiday';
|
||||||
|
if (type.includes('SICK')) return 'sick';
|
||||||
|
return 'regular'
|
||||||
|
}
|
||||||
|
|
||||||
|
const expenseSubgroupFromBankCode = (bank_code: any): keyof TotalExpenses => {
|
||||||
|
const type = bank_code.type;
|
||||||
|
if (type.includes('MILEAGE')) return 'mileage';
|
||||||
|
if (type.includes('PER_DIEM')) return 'per_diem';
|
||||||
|
if (type.includes('ON_CALL')) return 'on_call';
|
||||||
|
return 'expenses';
|
||||||
}
|
}
|
||||||
|
|
@ -1,32 +1,24 @@
|
||||||
import { TimesheetsController } from './~misc_deprecated-files/timesheets.controller';
|
import { GetTimesheetsOverviewService } from './services/timesheet-get-overview.service';
|
||||||
import { TimesheetsQueryService } from './~misc_deprecated-files/timesheets-query.service';
|
|
||||||
import { TimesheetArchiveService } from './services/timesheet-archive.service';
|
import { TimesheetArchiveService } from './services/timesheet-archive.service';
|
||||||
import { TimesheetsCommandService } from './~misc_deprecated-files/timesheets-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 { TimesheetController } from './controllers/timesheet.controller';
|
||||||
import { SharedModule } from '../shared/shared.module';
|
import { SharedModule } from '../shared/shared.module';
|
||||||
|
import { ShiftsModule } from '../shifts/shifts.module';
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { ShiftsGetService } from '../shifts/services/shifts-get.service';
|
|
||||||
import { TimesheetSelectorsService } from './~misc_deprecated-files/utils-helpers-others/timesheet.selectors';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
BusinessLogicsModule,
|
BusinessLogicsModule,
|
||||||
SharedModule,
|
SharedModule,
|
||||||
ShiftsGetService,
|
ShiftsModule,
|
||||||
],
|
],
|
||||||
controllers: [TimesheetsController],
|
controllers: [TimesheetController],
|
||||||
providers: [
|
providers: [
|
||||||
TimesheetsQueryService,
|
|
||||||
TimesheetsCommandService,
|
|
||||||
ExpensesCommandService,
|
|
||||||
TimesheetArchiveService,
|
TimesheetArchiveService,
|
||||||
TimesheetSelectorsService,
|
GetTimesheetsOverviewService,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
TimesheetsQueryService,
|
|
||||||
TimesheetArchiveService,
|
|
||||||
TimesheetsCommandService
|
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class TimesheetsModule {}
|
export class TimesheetsModule {}
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ import { RolesAllowed } from "src/common/decorators/roles.decorators";
|
||||||
import { Roles as RoleEnum } from '.prisma/client';
|
import { Roles as RoleEnum } from '.prisma/client';
|
||||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||||
import { TimesheetsCommandService } from './timesheets-command.service';
|
import { TimesheetsCommandService } from './timesheets-command.service';
|
||||||
import { TimesheetMap } from './utils-helpers-others/timesheet.types';
|
|
||||||
import { TimesheetPeriodDto } from './timesheet-period.dto';
|
import { TimesheetPeriodDto } from './timesheet-period.dto';
|
||||||
|
import { TimesheetMap } from './timesheet.types';
|
||||||
|
|
||||||
|
|
||||||
@ApiTags('Timesheets')
|
@ApiTags('Timesheets')
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user