feat(timesheet): added Post function to create a new shifts inside a timesheet
This commit is contained in:
parent
5063c1dfec
commit
4f7563ce9b
|
|
@ -542,53 +542,6 @@
|
||||||
"Timesheets"
|
"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": {
|
"delete": {
|
||||||
"operationId": "TimesheetsController_remove",
|
"operationId": "TimesheetsController_remove",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
|
|
@ -2408,48 +2361,7 @@
|
||||||
},
|
},
|
||||||
"CreateTimesheetDto": {
|
"CreateTimesheetDto": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"CreateExpenseDto": {
|
"CreateExpenseDto": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
|
|
||||||
|
|
@ -250,10 +250,10 @@ export class PayPeriodsQueryService {
|
||||||
const amount = toMoney(expense.amount);
|
const amount = toMoney(expense.amount);
|
||||||
record.expenses += 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;
|
const rate = expense.bank_code?.modifier ?? 0;
|
||||||
if (categorie === "MILEAGE" && rate > 0) {
|
if (type === "MILEAGE" && rate > 0) {
|
||||||
record.mileage += amount / rate;
|
record.mileage += Math.round((amount / rate)/100)*100;
|
||||||
}
|
}
|
||||||
record.is_approved = record.is_approved && expense.timesheet.is_approved;
|
record.is_approved = record.is_approved && expense.timesheet.is_approved;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 { 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 { Timesheets } from '@prisma/client';
|
||||||
import { UpdateTimesheetDto } from '../dtos/update-timesheet.dto';
|
|
||||||
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, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||||
|
|
@ -20,15 +19,6 @@ export class TimesheetsController {
|
||||||
private readonly timesheetsCommand: TimesheetsCommandService,
|
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<Timesheets> {
|
|
||||||
// return this.timesheetsQuery.create(dto);
|
|
||||||
// }
|
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||||
async getPeriodByQuery(
|
async getPeriodByQuery(
|
||||||
|
|
@ -49,6 +39,17 @@ export class TimesheetsController {
|
||||||
return this.timesheetsQuery.getTimesheetByEmail(email, week_offset);
|
return this.timesheetsQuery.getTimesheetByEmail(email, week_offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('shifts/:email')
|
||||||
|
async createTimesheetShifts(
|
||||||
|
@Param('email') email: string,
|
||||||
|
@Body() dto: CreateWeekShiftsDto,
|
||||||
|
@Query('offset') offset?: string,
|
||||||
|
): Promise<TimesheetDto> {
|
||||||
|
const week_offset = Number.isFinite(Number(offset)) ? Number(offset) : 0;
|
||||||
|
return this.timesheetsCommand.createWeekShiftsAndReturnOverview(email, dto.shifts, week_offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||||
@ApiOperation({ summary: 'Find timesheet' })
|
@ApiOperation({ summary: 'Find timesheet' })
|
||||||
|
|
@ -58,18 +59,6 @@ export class TimesheetsController {
|
||||||
return this.timesheetsQuery.findOne(id);
|
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<Timesheets> {
|
|
||||||
return this.timesheetsQuery.update(id, dto);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
// @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
// @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||||
@ApiOperation({ summary: 'Delete timesheet' })
|
@ApiOperation({ summary: 'Delete timesheet' })
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,33 @@
|
||||||
import { ApiProperty } from "@nestjs/swagger";
|
|
||||||
import { Type } from "class-transformer";
|
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 {
|
export class CreateTimesheetDto {
|
||||||
@ApiProperty({
|
|
||||||
example: 1,
|
|
||||||
description: 'timesheet`s unique ID (auto-generated)',
|
|
||||||
})
|
|
||||||
@Allow()
|
|
||||||
id?: number;
|
|
||||||
|
|
||||||
@ApiProperty({
|
@IsString()
|
||||||
example: 426433,
|
@Matches(/^\d{4}-\d{2}-\d{2}$/)
|
||||||
description: 'employee`s ID number of linked timsheet',
|
date!: string;
|
||||||
})
|
|
||||||
@Type(() => Number)
|
|
||||||
@IsInt()
|
|
||||||
employee_id: number;
|
|
||||||
|
|
||||||
@ApiProperty({
|
@IsString()
|
||||||
example: true,
|
@Length(1,64)
|
||||||
description: 'Timesheet`s status approval',
|
type!: string;
|
||||||
})
|
|
||||||
@IsOptional()
|
@IsString()
|
||||||
@IsBoolean()
|
@Matches(/^\d{2}:\d{2}$/)
|
||||||
is_approved?: boolean;
|
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[];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,24 @@
|
||||||
|
|
||||||
import { Injectable } from "@nestjs/common";
|
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
|
||||||
import { Prisma, Timesheets } from "@prisma/client";
|
import { Prisma, Timesheets } from "@prisma/client";
|
||||||
import { BaseApprovalService } from "src/common/shared/base-approval.service";
|
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 { CreateTimesheetDto } from "../dtos/create-timesheet.dto";
|
||||||
|
import { TimesheetDto } from "../dtos/overview-timesheet.dto";
|
||||||
|
import { getWeekEnd, getWeekStart } from "src/common/utils/date-utils";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TimesheetsCommandService extends BaseApprovalService<Timesheets>{
|
export class TimesheetsCommandService extends BaseApprovalService<Timesheets>{
|
||||||
constructor(prisma: PrismaService) {super(prisma);}
|
constructor(
|
||||||
|
prisma: PrismaService,
|
||||||
|
private readonly query: TimesheetsQueryService,
|
||||||
|
) {super(prisma);}
|
||||||
|
|
||||||
protected get delegate() {
|
protected get delegate() {
|
||||||
return this.prisma.timesheets;
|
return this.prisma.timesheets;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected delegateFor(transaction: Prisma.TransactionClient) {
|
protected delegateFor(transaction: Prisma.TransactionClient) {
|
||||||
return transaction.timesheets;
|
return transaction.timesheets;
|
||||||
}
|
}
|
||||||
|
|
@ -37,4 +45,83 @@ export class TimesheetsCommandService extends BaseApprovalService<Timesheets>{
|
||||||
return timesheet;
|
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<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
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -8,6 +8,7 @@ import { TimesheetPeriodDto } from '../dtos/timesheet-period.dto';
|
||||||
import { buildPeriod, endOfDayUTC, toUTCDateOnly } from '../utils/timesheet.helpers';
|
import { buildPeriod, endOfDayUTC, toUTCDateOnly } from '../utils/timesheet.helpers';
|
||||||
import type { ShiftRow, ExpenseRow } from '../utils/timesheet.helpers';
|
import type { ShiftRow, ExpenseRow } from '../utils/timesheet.helpers';
|
||||||
import { TimesheetDto } from '../dtos/overview-timesheet.dto';
|
import { TimesheetDto } from '../dtos/overview-timesheet.dto';
|
||||||
|
import { CreateWeekShiftsDto } from '../dtos/create-timesheet.dto';
|
||||||
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|
@ -219,19 +220,20 @@ export class TimesheetsQueryService {
|
||||||
return { ...timesheet, shift: detailedShifts, weeklyOvertimeHours };
|
return { ...timesheet, shift: detailedShifts, weeklyOvertimeHours };
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(id: number, dto:UpdateTimesheetDto): Promise<Timesheets> {
|
//deprecated
|
||||||
await this.findOne(id);
|
// async update(id: number, dto:UpdateTimesheetDto): Promise<Timesheets> {
|
||||||
const { employee_id, is_approved } = dto;
|
// await this.findOne(id);
|
||||||
return this.prisma.timesheets.update({
|
// const { employee_id, is_approved } = dto;
|
||||||
where: { id },
|
// return this.prisma.timesheets.update({
|
||||||
data: {
|
// where: { id },
|
||||||
...(employee_id !== undefined && { employee_id }),
|
// data: {
|
||||||
...(is_approved !== undefined && { is_approved }),
|
// ...(employee_id !== undefined && { employee_id }),
|
||||||
},
|
// ...(is_approved !== undefined && { is_approved }),
|
||||||
include: { employee: { include: { user: true } },
|
// },
|
||||||
},
|
// include: { employee: { include: { user: true } },
|
||||||
});
|
// },
|
||||||
}
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
async remove(id: number): Promise<Timesheets> {
|
async remove(id: number): Promise<Timesheets> {
|
||||||
await this.findOne(id);
|
await this.findOne(id);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user