diff --git a/docs/swagger/swagger-spec.json b/docs/swagger/swagger-spec.json index d35159e..212a779 100644 --- a/docs/swagger/swagger-spec.json +++ b/docs/swagger/swagger-spec.json @@ -542,53 +542,6 @@ "Timesheets" ] }, - "patch": { - "operationId": "TimesheetsController_update", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "type": "number" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateTimesheetDto" - } - } - } - }, - "responses": { - "201": { - "description": "Timesheet updated", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateTimesheetDto" - } - } - } - }, - "400": { - "description": "Timesheet not found" - } - }, - "security": [ - { - "access-token": [] - } - ], - "summary": "Update timesheet", - "tags": [ - "Timesheets" - ] - }, "delete": { "operationId": "TimesheetsController_remove", "parameters": [ @@ -2408,48 +2361,7 @@ }, "CreateTimesheetDto": { "type": "object", - "properties": { - "id": { - "type": "number", - "example": 1, - "description": "timesheet`s unique ID (auto-generated)" - }, - "employee_id": { - "type": "number", - "example": 426433, - "description": "employee`s ID number of linked timsheet" - }, - "is_approved": { - "type": "boolean", - "example": true, - "description": "Timesheet`s status approval" - } - }, - "required": [ - "id", - "employee_id", - "is_approved" - ] - }, - "UpdateTimesheetDto": { - "type": "object", - "properties": { - "id": { - "type": "number", - "example": 1, - "description": "timesheet`s unique ID (auto-generated)" - }, - "employee_id": { - "type": "number", - "example": 426433, - "description": "employee`s ID number of linked timsheet" - }, - "is_approved": { - "type": "boolean", - "example": true, - "description": "Timesheet`s status approval" - } - } + "properties": {} }, "CreateExpenseDto": { "type": "object", diff --git a/src/modules/pay-periods/services/pay-periods-query.service.ts b/src/modules/pay-periods/services/pay-periods-query.service.ts index e6e18e6..c681d50 100644 --- a/src/modules/pay-periods/services/pay-periods-query.service.ts +++ b/src/modules/pay-periods/services/pay-periods-query.service.ts @@ -250,10 +250,10 @@ export class PayPeriodsQueryService { const amount = toMoney(expense.amount); record.expenses += amount; - const categorie = (expense.bank_code?.categorie || "").toUpperCase(); + const type = (expense.bank_code?.type || "").toUpperCase(); const rate = expense.bank_code?.modifier ?? 0; - if (categorie === "MILEAGE" && rate > 0) { - record.mileage += amount / rate; + if (type === "MILEAGE" && rate > 0) { + record.mileage += Math.round((amount / rate)/100)*100; } record.is_approved = record.is_approved && expense.timesheet.is_approved; } diff --git a/src/modules/timesheets/controllers/timesheets.controller.ts b/src/modules/timesheets/controllers/timesheets.controller.ts index 865f43d..2dff5b4 100644 --- a/src/modules/timesheets/controllers/timesheets.controller.ts +++ b/src/modules/timesheets/controllers/timesheets.controller.ts @@ -1,8 +1,7 @@ -import { BadRequestException, Body, Controller, Delete, Get, Param, ParseBoolPipe, ParseIntPipe, Patch, Query } from '@nestjs/common'; +import { BadRequestException, Body, Controller, Delete, Get, Param, ParseBoolPipe, ParseIntPipe, Patch, Post, Query } from '@nestjs/common'; import { TimesheetsQueryService } from '../services/timesheets-query.service'; -import { CreateTimesheetDto } from '../dtos/create-timesheet.dto'; +import { CreateTimesheetDto, CreateWeekShiftsDto } from '../dtos/create-timesheet.dto'; import { Timesheets } from '@prisma/client'; -import { UpdateTimesheetDto } from '../dtos/update-timesheet.dto'; import { RolesAllowed } from "src/common/decorators/roles.decorators"; import { Roles as RoleEnum } from '.prisma/client'; import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; @@ -20,15 +19,6 @@ export class TimesheetsController { private readonly timesheetsCommand: TimesheetsCommandService, ) {} - // @Post() - // //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR) - // @ApiOperation({ summary: 'Create timesheet' }) - // @ApiResponse({ status: 201, description: 'Timesheet created', type: CreateTimesheetDto }) - // @ApiResponse({ status: 400, description: 'Incomplete task or invalid data' }) - // create(@Body() dto: CreateTimesheetDto): Promise { - // return this.timesheetsQuery.create(dto); - // } - @Get() //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR) async getPeriodByQuery( @@ -49,6 +39,17 @@ export class TimesheetsController { return this.timesheetsQuery.getTimesheetByEmail(email, week_offset); } + @Post('shifts/:email') + async createTimesheetShifts( + @Param('email') email: string, + @Body() dto: CreateWeekShiftsDto, + @Query('offset') offset?: string, + ): Promise { + const week_offset = Number.isFinite(Number(offset)) ? Number(offset) : 0; + return this.timesheetsCommand.createWeekShiftsAndReturnOverview(email, dto.shifts, week_offset); + } + + @Get(':id') //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR) @ApiOperation({ summary: 'Find timesheet' }) @@ -58,18 +59,6 @@ export class TimesheetsController { return this.timesheetsQuery.findOne(id); } - @Patch(':id') - //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR) - @ApiOperation({ summary: 'Update timesheet' }) - @ApiResponse({ status: 201, description: 'Timesheet updated', type: CreateTimesheetDto }) - @ApiResponse({ status: 400, description: 'Timesheet not found' }) - update( - @Param('id', ParseIntPipe) id:number, - @Body() dto: UpdateTimesheetDto, - ): Promise { - return this.timesheetsQuery.update(id, dto); - } - @Delete(':id') // @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR) @ApiOperation({ summary: 'Delete timesheet' }) diff --git a/src/modules/timesheets/dtos/create-timesheet.dto.ts b/src/modules/timesheets/dtos/create-timesheet.dto.ts index 6a1ace2..2e1c62a 100644 --- a/src/modules/timesheets/dtos/create-timesheet.dto.ts +++ b/src/modules/timesheets/dtos/create-timesheet.dto.ts @@ -1,28 +1,33 @@ -import { ApiProperty } from "@nestjs/swagger"; import { Type } from "class-transformer"; -import { Allow, IsBoolean, IsInt, IsOptional } from "class-validator"; +import { IsArray, IsOptional, IsString, Length, Matches, ValidateNested } from "class-validator"; export class CreateTimesheetDto { - @ApiProperty({ - example: 1, - description: 'timesheet`s unique ID (auto-generated)', - }) - @Allow() - id?: number; - @ApiProperty({ - example: 426433, - description: 'employee`s ID number of linked timsheet', - }) - @Type(() => Number) - @IsInt() - employee_id: number; + @IsString() + @Matches(/^\d{4}-\d{2}-\d{2}$/) + date!: string; - @ApiProperty({ - example: true, - description: 'Timesheet`s status approval', - }) - @IsOptional() - @IsBoolean() - is_approved?: boolean; + @IsString() + @Length(1,64) + type!: string; + + @IsString() + @Matches(/^\d{2}:\d{2}$/) + start_time!: string; + + @IsString() + @Matches(/^\d{2}:\d{2}$/) + end_time!: string; + + @IsOptional() + @IsString() + @Length(0,512) + description?: string; +} + +export class CreateWeekShiftsDto { + @IsArray() + @ValidateNested({each:true}) + @Type(()=> CreateTimesheetDto) + shifts!: CreateTimesheetDto[]; } diff --git a/src/modules/timesheets/services/timesheets-command.service.ts b/src/modules/timesheets/services/timesheets-command.service.ts index abce079..5fdbec6 100644 --- a/src/modules/timesheets/services/timesheets-command.service.ts +++ b/src/modules/timesheets/services/timesheets-command.service.ts @@ -1,16 +1,24 @@ -import { Injectable } from "@nestjs/common"; +import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; import { Prisma, Timesheets } from "@prisma/client"; import { BaseApprovalService } from "src/common/shared/base-approval.service"; import { PrismaService } from "src/prisma/prisma.service"; +import { TimesheetsQueryService } from "./timesheets-query.service"; +import { CreateTimesheetDto } from "../dtos/create-timesheet.dto"; +import { TimesheetDto } from "../dtos/overview-timesheet.dto"; +import { getWeekEnd, getWeekStart } from "src/common/utils/date-utils"; @Injectable() export class TimesheetsCommandService extends BaseApprovalService{ - constructor(prisma: PrismaService) {super(prisma);} + constructor( + prisma: PrismaService, + private readonly query: TimesheetsQueryService, + ) {super(prisma);} protected get delegate() { return this.prisma.timesheets; } + protected delegateFor(transaction: Prisma.TransactionClient) { return transaction.timesheets; } @@ -37,4 +45,83 @@ export class TimesheetsCommandService extends BaseApprovalService{ return timesheet; } + + //create shifts within timesheet's week - employee overview functions + private parseISODate(iso: string): Date { + const [ y, m, d ] = iso.split('-').map(Number); + return new Date(y, (m ?? 1) - 1, d ?? 1); + } + + private parseHHmm(t: string): Date { + const [ hh, mm ] = t.split(':').map(Number); + return new Date(1970, 0, 1, hh || 0, mm || 0, 0, 0); + } + + async createWeekShiftsAndReturnOverview( + email:string, + shifts: CreateTimesheetDto[], + week_offset = 0, + ): Promise { + + //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 + const employee = await this.prisma.employees.findFirst({ + where: { user_id: user?.id }, + select: { id: true }, + }); + if(!employee) throw new NotFoundException(`employee for ${ email } not found`); + + //insure that the week starts on sunday and finishes on saturday + const base = new Date(); + base.setDate(base.getDate() + week_offset * 7); + const start_week = getWeekStart(base, 0); + const end_week = getWeekEnd(start_week); + + const timesheet = await this.prisma.timesheets.upsert({ + where: { + 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 + for(const shift of shifts) { + const date = this.parseISODate(shift.date); + 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({ + where: { type: shift.type }, + select: { id: true }, + }); + if(!bank_code) throw new BadRequestException(`Invalid bank_code type: ${shift.type}`); + + await this.prisma.shifts.create({ + data: { + timesheet_id: timesheet.id, + bank_code_id: bank_code.id, + date: date, + start_time: this.parseHHmm(shift.start_time), + end_time: this.parseHHmm(shift.end_time), + description: shift.description ?? null, + is_approved: false, + }, + }); + } + return this.query.getTimesheetByEmail(email, week_offset); + } } \ No newline at end of file diff --git a/src/modules/timesheets/services/timesheets-query.service.ts b/src/modules/timesheets/services/timesheets-query.service.ts index 697a739..b7cde71 100644 --- a/src/modules/timesheets/services/timesheets-query.service.ts +++ b/src/modules/timesheets/services/timesheets-query.service.ts @@ -8,6 +8,7 @@ import { TimesheetPeriodDto } from '../dtos/timesheet-period.dto'; import { buildPeriod, endOfDayUTC, toUTCDateOnly } from '../utils/timesheet.helpers'; import type { ShiftRow, ExpenseRow } from '../utils/timesheet.helpers'; import { TimesheetDto } from '../dtos/overview-timesheet.dto'; +import { CreateWeekShiftsDto } from '../dtos/create-timesheet.dto'; @Injectable() @@ -219,19 +220,20 @@ export class TimesheetsQueryService { return { ...timesheet, shift: detailedShifts, weeklyOvertimeHours }; } - async update(id: number, dto:UpdateTimesheetDto): Promise { - await this.findOne(id); - const { employee_id, is_approved } = dto; - return this.prisma.timesheets.update({ - where: { id }, - data: { - ...(employee_id !== undefined && { employee_id }), - ...(is_approved !== undefined && { is_approved }), - }, - include: { employee: { include: { user: true } }, - }, - }); - } + //deprecated + // async update(id: number, dto:UpdateTimesheetDto): Promise { + // await this.findOne(id); + // const { employee_id, is_approved } = dto; + // return this.prisma.timesheets.update({ + // where: { id }, + // data: { + // ...(employee_id !== undefined && { employee_id }), + // ...(is_approved !== undefined && { is_approved }), + // }, + // include: { employee: { include: { user: true } }, + // }, + // }); + // } async remove(id: number): Promise { await this.findOne(id);