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 { 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 { ShiftDto } from "../dtos/shift.dto";
|
||||
import { ShiftsGetService } from "../services/shifts-get.service";
|
||||
|
|
@ -12,10 +12,10 @@ export class ShiftController {
|
|||
private readonly get_service: ShiftsGetService
|
||||
){}
|
||||
|
||||
@Get("shifts")
|
||||
@Get()
|
||||
async getShiftsByIds(
|
||||
@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);
|
||||
}
|
||||
|
||||
|
|
@ -42,15 +42,8 @@ export class ShiftController {
|
|||
}
|
||||
|
||||
@Delete(':shift_id')
|
||||
removeBatch(
|
||||
@Body() body: { ids: number[] }): Promise<DeleteResult[]> {
|
||||
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);
|
||||
remove(@Param('shift_id') shift_id: number ) {
|
||||
return this.upsert_service.deleteShift(shift_id);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -39,11 +39,6 @@ export class ShiftsUpsertService {
|
|||
//checks for overlaping shifts
|
||||
//create new shifts
|
||||
//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[]> {
|
||||
if (!Array.isArray(dtos) || dtos.length === 0) return [];
|
||||
|
||||
|
|
@ -179,13 +174,6 @@ export class ShiftsUpsertService {
|
|||
// update shifts in DB
|
||||
// recalculate overtime after update
|
||||
// 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[]> {
|
||||
if (!Array.isArray(updates) || updates.length === 0) return [];
|
||||
|
||||
|
|
@ -345,43 +333,24 @@ export class ShiftsUpsertService {
|
|||
//_________________________________________________________________
|
||||
// DELETE
|
||||
//_________________________________________________________________
|
||||
//finds shifts using shit_ids
|
||||
//finds shift using shit_ids
|
||||
//recalc overtime shifts after delete
|
||||
//blocs deletion if approved
|
||||
async deleteShift(shift_id: number) {
|
||||
const [result] = await this.deleteShifts([shift_id]);
|
||||
if (!result.ok) throw result.error;
|
||||
return { success: true, overtime: result.overtime };
|
||||
}
|
||||
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 },
|
||||
return await this.prisma.$transaction(async (tx) =>{
|
||||
const shift = await tx.shifts.findUnique({
|
||||
where: { id: shift_id },
|
||||
select: { id: true, date: true, timesheet_id: 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) {
|
||||
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') })
|
||||
);
|
||||
}
|
||||
}
|
||||
await tx.shifts.delete({ where: { id: shift_id } });
|
||||
|
||||
const results: DeleteResult[] = [];
|
||||
for (const id of shift_ids) {
|
||||
const row = byId.get(id)!;
|
||||
await tx.shifts.delete({ where: { id } });
|
||||
const summary = await this.overtime.getWeekOvertimeSummary(row.timesheet_id, row.date, tx);
|
||||
results.push({ ok: true, id, overtime: summary });
|
||||
}
|
||||
return results;
|
||||
const summary = await this.overtime.getWeekOvertimeSummary( shift.timesheet_id, shift.date, tx);
|
||||
return {
|
||||
success: true,
|
||||
overtime: summary
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
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 {
|
||||
employee_fullname: string;
|
||||
timesheets: Timesheet[];
|
||||
|
|
@ -35,7 +30,7 @@ export class TotalHours {
|
|||
}
|
||||
export class TotalExpenses {
|
||||
expenses: number;
|
||||
perd_diem: number;
|
||||
per_diem: number;
|
||||
on_call: 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 { 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()
|
||||
export class GetTimesheetsOverviewService {
|
||||
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 { TimesheetsQueryService } from './~misc_deprecated-files/timesheets-query.service';
|
||||
import { GetTimesheetsOverviewService } from './services/timesheet-get-overview.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 { TimesheetController } from './controllers/timesheet.controller';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { ShiftsModule } from '../shifts/shifts.module';
|
||||
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({
|
||||
imports: [
|
||||
BusinessLogicsModule,
|
||||
SharedModule,
|
||||
ShiftsGetService,
|
||||
ShiftsModule,
|
||||
],
|
||||
controllers: [TimesheetsController],
|
||||
controllers: [TimesheetController],
|
||||
providers: [
|
||||
TimesheetsQueryService,
|
||||
TimesheetsCommandService,
|
||||
ExpensesCommandService,
|
||||
TimesheetArchiveService,
|
||||
TimesheetSelectorsService,
|
||||
GetTimesheetsOverviewService,
|
||||
],
|
||||
exports: [
|
||||
TimesheetsQueryService,
|
||||
TimesheetArchiveService,
|
||||
TimesheetsCommandService
|
||||
|
||||
],
|
||||
})
|
||||
export class TimesheetsModule {}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import { RolesAllowed } from "src/common/decorators/roles.decorators";
|
|||
import { Roles as RoleEnum } from '.prisma/client';
|
||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||
import { TimesheetsCommandService } from './timesheets-command.service';
|
||||
import { TimesheetMap } from './utils-helpers-others/timesheet.types';
|
||||
import { TimesheetPeriodDto } from './timesheet-period.dto';
|
||||
import { TimesheetMap } from './timesheet.types';
|
||||
|
||||
|
||||
@ApiTags('Timesheets')
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user