diff --git a/docs/swagger/swagger-spec.json b/docs/swagger/swagger-spec.json index 718184d..de9670c 100644 --- a/docs/swagger/swagger-spec.json +++ b/docs/swagger/swagger-spec.json @@ -57,6 +57,20 @@ ] } }, + "/auth/me": { + "get": { + "operationId": "AuthController_getProfile", + "parameters": [], + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "Auth" + ] + } + }, "/employees/employee-list": { "get": { "operationId": "EmployeesController_findListEmployees", @@ -153,8 +167,6 @@ ] } }, -<<<<<<< HEAD -======= "/employees/profile/{email}": { "get": { "operationId": "EmployeesController_findOneProfile", @@ -195,381 +207,6 @@ ] } }, - "/timesheets": { - "get": { - "operationId": "TimesheetsController_getPeriodByQuery", - "parameters": [ - { - "name": "year", - "required": true, - "in": "query", - "schema": { - "type": "number" - } - }, - { - "name": "period_no", - "required": true, - "in": "query", - "schema": { - "type": "number" - } - }, - { - "name": "email", - "required": true, - "in": "query", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "" - } - }, - "security": [ - { - "access-token": [] - } - ], - "tags": [ - "Timesheets" - ] - } - }, - "/timesheets/{email}": { - "get": { - "operationId": "TimesheetsController_getByEmail", - "parameters": [ - { - "name": "email", - "required": true, - "in": "path", - "schema": { - "type": "string" - } - }, - { - "name": "offset", - "required": true, - "in": "query", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "" - } - }, - "security": [ - { - "access-token": [] - } - ], - "tags": [ - "Timesheets" - ] - } - }, - "/timesheets/shifts/{email}": { - "post": { - "operationId": "TimesheetsController_createTimesheetShifts", - "parameters": [ - { - "name": "email", - "required": true, - "in": "path", - "schema": { - "type": "string" - } - }, - { - "name": "offset", - "required": true, - "in": "query", - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateWeekShiftsDto" - } - } - } - }, - "responses": { - "201": { - "description": "" - } - }, - "security": [ - { - "access-token": [] - } - ], - "tags": [ - "Timesheets" - ] - } - }, - "/Expenses/upsert/{email}/{date}": { - "put": { - "operationId": "ExpensesController_upsert_by_date", - "parameters": [ - { - "name": "email", - "required": true, - "in": "path", - "schema": { - "type": "string" - } - }, - { - "name": "date", - "required": true, - "in": "path", - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpsertExpenseDto" - } - } - } - }, - "responses": { - "200": { - "description": "" - } - }, - "security": [ - { - "access-token": [] - } - ], - "tags": [ - "Expenses" - ] - } - }, - "/Expenses/list/{email}/{year}/{period_no}": { - "get": { - "operationId": "ExpensesController_findExpenseListByPayPeriodAndEmail", - "parameters": [ - { - "name": "email", - "required": true, - "in": "path", - "schema": { - "type": "string" - } - }, - { - "name": "year", - "required": true, - "in": "path", - "schema": { - "type": "number" - } - }, - { - "name": "period_no", - "required": true, - "in": "path", - "schema": { - "type": "number" - } - } - ], - "responses": { - "200": { - "description": "" - } - }, - "security": [ - { - "access-token": [] - } - ], - "tags": [ - "Expenses" - ] - } - }, - "/shifts/upsert/{email}": { - "put": { - "operationId": "ShiftsController_upsert_by_date", - "parameters": [ - { - "name": "email", - "required": true, - "in": "path", - "schema": { - "type": "string" - } - }, - { - "name": "action", - "required": true, - "in": "query", - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpsertShiftDto" - } - } - } - }, - "responses": { - "200": { - "description": "" - } - }, - "security": [ - { - "access-token": [] - } - ], - "tags": [ - "Shifts" - ] - } - }, - "/shifts/delete/{email}/{date}": { - "delete": { - "operationId": "ShiftsController_remove", - "parameters": [ - { - "name": "email", - "required": true, - "in": "path", - "schema": { - "type": "string" - } - }, - { - "name": "date", - "required": true, - "in": "path", - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpsertShiftDto" - } - } - } - }, - "responses": { - "200": { - "description": "" - } - }, - "security": [ - { - "access-token": [] - } - ], - "tags": [ - "Shifts" - ] - } - }, - "/shifts/approval/{id}": { - "patch": { - "operationId": "ShiftsController_approve", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "type": "number" - } - } - ], - "responses": { - "200": { - "description": "" - } - }, - "security": [ - { - "access-token": [] - } - ], - "tags": [ - "Shifts" - ] - } - }, - "/shifts/summary": { - "get": { - "operationId": "ShiftsController_getSummary", - "parameters": [], - "responses": { - "200": { - "description": "" - } - }, - "security": [ - { - "access-token": [] - } - ], - "tags": [ - "Shifts" - ] - } - }, - "/shifts/export.csv": { - "get": { - "operationId": "ShiftsController_exportCsv", - "parameters": [], - "responses": { - "200": { - "description": "" - } - }, - "security": [ - { - "access-token": [] - } - ], - "tags": [ - "Shifts" - ] - } - }, ->>>>>>> 88f7c0cb0e3824f6faed91058a7d55a9cca048a7 "/notifications/summary": { "get": { "operationId": "NotificationsController_summary", @@ -598,80 +235,6 @@ ] } }, -<<<<<<< HEAD -======= - "/leave-requests/upsert": { - "post": { - "operationId": "LeaveRequestController_upsertLeaveRequest", - "parameters": [], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpsertLeaveRequestDto" - } - } - } - }, - "responses": { - "201": { - "description": "" - } - }, - "security": [ - { - "access-token": [] - } - ], - "tags": [ - "Leave Requests" - ] - } - }, - "/auth/v1/login": { - "get": { - "operationId": "AuthController_login", - "parameters": [], - "responses": { - "200": { - "description": "" - } - }, - "tags": [ - "Auth" - ] - } - }, - "/auth/callback": { - "get": { - "operationId": "AuthController_loginCallback", - "parameters": [], - "responses": { - "200": { - "description": "" - } - }, - "tags": [ - "Auth" - ] - } - }, - "/auth/me": { - "get": { - "operationId": "AuthController_getProfile", - "parameters": [], - "responses": { - "200": { - "description": "" - } - }, - "tags": [ - "Auth" - ] - } - }, ->>>>>>> 88f7c0cb0e3824f6faed91058a7d55a9cca048a7 "/oauth-sessions": { "post": { "operationId": "OauthSessionsController_create", @@ -1327,29 +890,10 @@ "first_work_day" ] }, -<<<<<<< HEAD -======= "EmployeeProfileItemDto": { "type": "object", "properties": {} }, - "CreateWeekShiftsDto": { - "type": "object", - "properties": {} - }, - "UpsertExpenseDto": { - "type": "object", - "properties": {} - }, - "UpsertShiftDto": { - "type": "object", - "properties": {} - }, - "UpsertLeaveRequestDto": { - "type": "object", - "properties": {} - }, ->>>>>>> 88f7c0cb0e3824f6faed91058a7d55a9cca048a7 "CreateOauthSessionDto": { "type": "object", "properties": { diff --git a/src/modules/expenses/controllers/expense.controller.ts b/src/modules/expenses/controllers/expense.controller.ts new file mode 100644 index 0000000..2359b7a --- /dev/null +++ b/src/modules/expenses/controllers/expense.controller.ts @@ -0,0 +1,10 @@ +import { Controller } from "@nestjs/common"; +import { PrismaService } from "src/prisma/prisma.service"; + + +@Controller('expense') +export class ExpenseController { + constructor(private readonly prisma: PrismaService){} + + +} \ No newline at end of file diff --git a/src/modules/expenses/dtos/create-expense.dto.ts b/src/modules/expenses/dtos/create-expense.dto.ts deleted file mode 100644 index d0e4863..0000000 --- a/src/modules/expenses/dtos/create-expense.dto.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { Type } from "class-transformer"; -import { Allow, IsBoolean, IsDate, IsDateString, IsInt, IsNumber, IsOptional, IsString } from "class-validator"; - -export class CreateExpenseDto { - @ApiProperty({ - example: 1, - description: 'Unique ID of the expense (auto-generated)', - }) - @Allow() - id?: number; - - @ApiProperty({ - example: 101, - description: 'ID number for a set timesheet', - }) - @Type(()=> Number) - @IsInt() - timesheet_id: number; - - @ApiProperty({ - example: 7, - description: 'ID number of an bank code (link with bank-codes)', - }) - @Type(() => Number) - @IsInt() - bank_code_id: number; - - @ApiProperty({ - example: '3018-10-20T00:00:00.000Z', - description: 'Date where the expense was made', - }) - @IsDateString() - date: string; - - @ApiProperty({ - example: 17.82, - description: 'amount in $ for a refund', - }) - @Type(() => Number) - @IsNumber() - amount: number; - - @ApiProperty({ - example:'Spent for mileage between A and B', - description:'explain`s why the expense was made' - }) - @IsString() - comment: string; - - @ApiProperty({ - example: 'DENIED, APPROUVED, PENDING, etc...', - description: 'validation status', - }) - @IsOptional() - @IsBoolean() - is_approved?: boolean; - - @ApiProperty({ - example:'Asked X to go there as an emergency response', - description:'Supervisro`s justification for the spending of an employee' - }) - @IsString() - @IsOptional() - supervisor_comment?: string; -} diff --git a/src/modules/expenses/dtos/expense.dto.ts b/src/modules/expenses/dtos/expense.dto.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/expenses/dtos/get-expense.dto.ts b/src/modules/expenses/dtos/get-expense.dto.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/expenses/dtos/search-expense.dto.ts b/src/modules/expenses/dtos/search-expense.dto.ts deleted file mode 100644 index 3eb7758..0000000 --- a/src/modules/expenses/dtos/search-expense.dto.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Type } from "class-transformer"; -import { IsDateString, IsInt, IsOptional, IsString } from "class-validator"; - -export class SearchExpensesDto { - @IsOptional() - @Type(()=> Number) - @IsInt() - timesheet_id?: number; - - @IsOptional() - @Type(()=> Number) - @IsInt() - bank_code_id?: number; - - @IsOptional() - @IsString() - comment_contains?: string; - - @IsOptional() - @IsDateString() - start_date: string; - - @IsOptional() - @IsDateString() - end_date: string; -} \ No newline at end of file diff --git a/src/modules/expenses/dtos/update-expense.dto.ts b/src/modules/expenses/dtos/update-expense.dto.ts index 1b2426f..e69de29 100644 --- a/src/modules/expenses/dtos/update-expense.dto.ts +++ b/src/modules/expenses/dtos/update-expense.dto.ts @@ -1,4 +0,0 @@ -import { PartialType } from "@nestjs/swagger"; -import { CreateExpenseDto } from "./create-expense.dto"; - -export class UpdateExpenseDto extends PartialType(CreateExpenseDto) {} \ No newline at end of file diff --git a/src/modules/expenses/dtos/upsert-expense.dto.ts b/src/modules/expenses/dtos/upsert-expense.dto.ts deleted file mode 100644 index 5bea2c3..0000000 --- a/src/modules/expenses/dtos/upsert-expense.dto.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Transform, Type } from "class-transformer"; -import { - IsNumber, - IsOptional, - IsString, - Matches, - MaxLength, - Min, - ValidateIf, - ValidateNested -} from "class-validator"; - -export class ExpensePayloadDto { - @IsString() - type!: string; - - @ValidateIf(o => (o.type ?? '').toUpperCase() !== 'MILEAGE') - @IsNumber() - @Min(0) - amount?: number; - - @ValidateIf(o => (o.type ?? '').toUpperCase() === 'MILEAGE') - @IsNumber() - @Min(0) - mileage?: number; - - @IsString() - @MaxLength(280) - @Transform(({ value }) => (typeof value === 'string' ? value.trim() : value)) - comment!: string; - - @IsOptional() - @Transform(({ value }) => { - if (value === null || value === undefined || value === '') return undefined; - if (typeof value === 'number') return value.toString(); - if (typeof value === 'string') { - const trimmed = value.trim(); - return trimmed.length ? trimmed : undefined; - } - return undefined; - }) - @IsString() - @Matches(/^\d+$/) - @MaxLength(255) - attachment?: string; -} - - -export class UpsertExpenseDto { - @IsOptional() - @ValidateNested() - @Type(()=> ExpensePayloadDto) - old_expense?: ExpensePayloadDto; - - @IsOptional() - @ValidateNested() - @Type(()=> ExpensePayloadDto) - new_expense?: ExpensePayloadDto; -} \ No newline at end of file diff --git a/src/modules/expenses/types and interfaces/expenses.types.interfaces.ts b/src/modules/expenses/types and interfaces/expenses.types.interfaces.ts deleted file mode 100644 index 5f592a5..0000000 --- a/src/modules/expenses/types and interfaces/expenses.types.interfaces.ts +++ /dev/null @@ -1,14 +0,0 @@ -export type UpsertAction = 'create' | 'update' | 'delete'; - -export interface ExpenseResponse { - date: string; - type: string; - amount: number; - comment: string; - is_approved: boolean; -}; - -export type UpsertExpenseResult = { - action: UpsertAction; - day: ExpenseResponse[] -}; \ No newline at end of file diff --git a/src/modules/expenses/utils/expenses.utils.ts b/src/modules/expenses/utils/expenses.utils.ts deleted file mode 100644 index 6959bde..0000000 --- a/src/modules/expenses/utils/expenses.utils.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { BadRequestException } from "@nestjs/common"; -import { ExpenseResponse } from "../types and interfaces/expenses.types.interfaces"; -import { Prisma } from "@prisma/client"; - -//uppercase and trim for validation -export function normalizeType(type: string): string { - return (type ?? '').trim().toUpperCase(); -}; - -//required comment after trim -export function assertAndTrimComment(comment: string): string { - const cmt = (comment ?? '').trim(); - if(cmt.length === 0) { - throw new BadRequestException('A comment is required'); - } - return cmt; -}; - -//rounding $ to 2 decimals -export function roundMoney2(num: number): number { - return Math.round((num + Number.EPSILON) * 100)/ 100; -}; - -export function computeMileageAmount(km: number, modifier: number): number { - if(km < 0) throw new BadRequestException('mileage must be positive'); - if(modifier < 0) throw new BadRequestException('modifier must be positive'); - return roundMoney2(km * modifier); -}; - -//compat. types with Prisma.Decimal. work around Prisma import in utils. -export type DecimalLike = - | number - | string - | { toNumber?: () => number } - | { toString?: () => string }; - - -//safe conversion to number -export function toNumberSafe(value: DecimalLike): number { - if(typeof value === 'number') return value; - if(value && typeof (value as any).toNumber === 'function') return (value as any).toNumber(); - return Number( - typeof (value as any)?.toString === 'function' - ? (value as any).toString() - : value, - ); -} - -export const parseAttachmentId = (value: unknown): number | null => { - if (value == null) { - return null; - } - - if (typeof value === 'number') { - if (!Number.isInteger(value) || value <= 0) { - throw new BadRequestException('Invalid attachment id'); - } - return value; - } - - if (typeof value === 'string') { - - const trimmed = value.trim(); - if (!trimmed.length) return null; - if (!/^\d+$/.test(trimmed)) throw new BadRequestException('Invalid attachment id'); - - const parsed = Number(trimmed); - if (parsed <= 0) throw new BadRequestException('Invalid attachment id'); - - return parsed; - } - throw new BadRequestException('Invalid attachment id'); -}; - - -//map of a row for DayExpenseResponse -export function mapDbExpenseToDayResponse(row: { - date: Date; - amount: Prisma.Decimal | number | string | null; - mileage?: Prisma.Decimal | number | string | null; - comment: string; - is_approved: boolean; - bank_code?: { type?: string | null } | null; -}): ExpenseResponse { - const yyyyMmDd = row.date.toISOString().slice(0,10); - const toNum = (value: any)=> (value == null ? 0 : Number(value)); - return { - date: yyyyMmDd, - type: normalizeType(row.bank_code?.type ?? 'UNKNOWN'), - amount: toNum(row.amount), - comment: row.comment, - is_approved: row.is_approved, - ...(row.mileage !== null ? { mileage: toNum(row.mileage) }: {}), - }; -} - - export const computeAmountDecimal = ( - type: string, - payload: { - amount?: number; - mileage?: number; - }, - modifier: number, - ): Prisma.Decimal => { - if(type === 'MILEAGE') { - const km = payload.mileage ?? 0; - const amountNumber = computeMileageAmount(km, modifier); - return new Prisma.Decimal(amountNumber); - } - return new Prisma.Decimal(payload.amount!); - }; diff --git a/src/modules/expenses/~misc_deprecated-files/create-expense.dto.ts b/src/modules/expenses/~misc_deprecated-files/create-expense.dto.ts new file mode 100644 index 0000000..88ee933 --- /dev/null +++ b/src/modules/expenses/~misc_deprecated-files/create-expense.dto.ts @@ -0,0 +1,66 @@ +// import { ApiProperty } from "@nestjs/swagger"; +// import { Type } from "class-transformer"; +// import { Allow, IsBoolean, IsDate, IsDateString, IsInt, IsNumber, IsOptional, IsString } from "class-validator"; + +// export class CreateExpenseDto { +// @ApiProperty({ +// example: 1, +// description: 'Unique ID of the expense (auto-generated)', +// }) +// @Allow() +// id?: number; + +// @ApiProperty({ +// example: 101, +// description: 'ID number for a set timesheet', +// }) +// @Type(()=> Number) +// @IsInt() +// timesheet_id: number; + +// @ApiProperty({ +// example: 7, +// description: 'ID number of an bank code (link with bank-codes)', +// }) +// @Type(() => Number) +// @IsInt() +// bank_code_id: number; + +// @ApiProperty({ +// example: '3018-10-20T00:00:00.000Z', +// description: 'Date where the expense was made', +// }) +// @IsDateString() +// date: string; + +// @ApiProperty({ +// example: 17.82, +// description: 'amount in $ for a refund', +// }) +// @Type(() => Number) +// @IsNumber() +// amount: number; + +// @ApiProperty({ +// example:'Spent for mileage between A and B', +// description:'explain`s why the expense was made' +// }) +// @IsString() +// comment: string; + +// @ApiProperty({ +// example: 'DENIED, APPROUVED, PENDING, etc...', +// description: 'validation status', +// }) +// @IsOptional() +// @IsBoolean() +// is_approved?: boolean; + +// @ApiProperty({ +// example:'Asked X to go there as an emergency response', +// description:'Supervisro`s justification for the spending of an employee' +// }) +// @IsString() +// @IsOptional() +// supervisor_comment?: string; +// } diff --git a/src/modules/expenses/services/expenses-command.service.ts b/src/modules/expenses/~misc_deprecated-files/expenses-command.service.ts similarity index 100% rename from src/modules/expenses/services/expenses-command.service.ts rename to src/modules/expenses/~misc_deprecated-files/expenses-command.service.ts diff --git a/src/modules/expenses/services/expenses-query.service.ts b/src/modules/expenses/~misc_deprecated-files/expenses-query.service.ts similarity index 100% rename from src/modules/expenses/services/expenses-query.service.ts rename to src/modules/expenses/~misc_deprecated-files/expenses-query.service.ts diff --git a/src/modules/expenses/controllers/expenses.controller.ts b/src/modules/expenses/~misc_deprecated-files/expenses.controller.ts similarity index 100% rename from src/modules/expenses/controllers/expenses.controller.ts rename to src/modules/expenses/~misc_deprecated-files/expenses.controller.ts diff --git a/src/modules/expenses/~misc_deprecated-files/expenses.types.interfaces.ts b/src/modules/expenses/~misc_deprecated-files/expenses.types.interfaces.ts new file mode 100644 index 0000000..565d461 --- /dev/null +++ b/src/modules/expenses/~misc_deprecated-files/expenses.types.interfaces.ts @@ -0,0 +1,14 @@ +// export type UpsertAction = 'create' | 'update' | 'delete'; + +// export interface ExpenseResponse { +// date: string; +// type: string; +// amount: number; +// comment: string; +// is_approved: boolean; +// }; + +// export type UpsertExpenseResult = { +// action: UpsertAction; +// day: ExpenseResponse[] +// }; \ No newline at end of file diff --git a/src/modules/expenses/~misc_deprecated-files/expenses.utils.ts b/src/modules/expenses/~misc_deprecated-files/expenses.utils.ts new file mode 100644 index 0000000..c74aad4 --- /dev/null +++ b/src/modules/expenses/~misc_deprecated-files/expenses.utils.ts @@ -0,0 +1,111 @@ +// import { BadRequestException } from "@nestjs/common"; +// import { ExpenseResponse } from "../types and interfaces/expenses.types.interfaces"; +// import { Prisma } from "@prisma/client"; + +// //uppercase and trim for validation +// export function normalizeType(type: string): string { +// return (type ?? '').trim().toUpperCase(); +// }; + +// //required comment after trim +// export function assertAndTrimComment(comment: string): string { +// const cmt = (comment ?? '').trim(); +// if(cmt.length === 0) { +// throw new BadRequestException('A comment is required'); +// } +// return cmt; +// }; + +// //rounding $ to 2 decimals +// export function roundMoney2(num: number): number { +// return Math.round((num + Number.EPSILON) * 100)/ 100; +// }; + +// export function computeMileageAmount(km: number, modifier: number): number { +// if(km < 0) throw new BadRequestException('mileage must be positive'); +// if(modifier < 0) throw new BadRequestException('modifier must be positive'); +// return roundMoney2(km * modifier); +// }; + +// //compat. types with Prisma.Decimal. work around Prisma import in utils. +// export type DecimalLike = +// | number +// | string +// | { toNumber?: () => number } +// | { toString?: () => string }; + + +// //safe conversion to number +// export function toNumberSafe(value: DecimalLike): number { +// if(typeof value === 'number') return value; +// if(value && typeof (value as any).toNumber === 'function') return (value as any).toNumber(); +// return Number( +// typeof (value as any)?.toString === 'function' +// ? (value as any).toString() +// : value, +// ); +// } + +// export const parseAttachmentId = (value: unknown): number | null => { +// if (value == null) { +// return null; +// } + +// if (typeof value === 'number') { +// if (!Number.isInteger(value) || value <= 0) { +// throw new BadRequestException('Invalid attachment id'); +// } +// return value; +// } + +// if (typeof value === 'string') { + +// const trimmed = value.trim(); +// if (!trimmed.length) return null; +// if (!/^\d+$/.test(trimmed)) throw new BadRequestException('Invalid attachment id'); + +// const parsed = Number(trimmed); +// if (parsed <= 0) throw new BadRequestException('Invalid attachment id'); + +// return parsed; +// } +// throw new BadRequestException('Invalid attachment id'); +// }; + + +// //map of a row for DayExpenseResponse +// export function mapDbExpenseToDayResponse(row: { +// date: Date; +// amount: Prisma.Decimal | number | string | null; +// mileage?: Prisma.Decimal | number | string | null; +// comment: string; +// is_approved: boolean; +// bank_code?: { type?: string | null } | null; +// }): ExpenseResponse { +// const yyyyMmDd = row.date.toISOString().slice(0,10); +// const toNum = (value: any)=> (value == null ? 0 : Number(value)); +// return { +// date: yyyyMmDd, +// type: normalizeType(row.bank_code?.type ?? 'UNKNOWN'), +// amount: toNum(row.amount), +// comment: row.comment, +// is_approved: row.is_approved, +// ...(row.mileage !== null ? { mileage: toNum(row.mileage) }: {}), +// }; +// } + +// export const computeAmountDecimal = ( +// type: string, +// payload: { +// amount?: number; +// mileage?: number; +// }, +// modifier: number, +// ): Prisma.Decimal => { +// if(type === 'MILEAGE') { +// const km = payload.mileage ?? 0; +// const amountNumber = computeMileageAmount(km, modifier); +// return new Prisma.Decimal(amountNumber); +// } +// return new Prisma.Decimal(payload.amount!); +// }; diff --git a/src/modules/expenses/~misc_deprecated-files/search-expense.dto.ts b/src/modules/expenses/~misc_deprecated-files/search-expense.dto.ts new file mode 100644 index 0000000..665faac --- /dev/null +++ b/src/modules/expenses/~misc_deprecated-files/search-expense.dto.ts @@ -0,0 +1,26 @@ +// import { Type } from "class-transformer"; +// import { IsDateString, IsInt, IsOptional, IsString } from "class-validator"; + +// export class SearchExpensesDto { +// @IsOptional() +// @Type(()=> Number) +// @IsInt() +// timesheet_id?: number; + +// @IsOptional() +// @Type(()=> Number) +// @IsInt() +// bank_code_id?: number; + +// @IsOptional() +// @IsString() +// comment_contains?: string; + +// @IsOptional() +// @IsDateString() +// start_date: string; + +// @IsOptional() +// @IsDateString() +// end_date: string; +// } \ No newline at end of file diff --git a/src/modules/expenses/~misc_deprecated-files/update-expense.dto.ts b/src/modules/expenses/~misc_deprecated-files/update-expense.dto.ts new file mode 100644 index 0000000..c9f1ef6 --- /dev/null +++ b/src/modules/expenses/~misc_deprecated-files/update-expense.dto.ts @@ -0,0 +1,4 @@ +// import { PartialType } from "@nestjs/swagger"; +// import { CreateExpenseDto } from "./create-expense.dto"; + +// export class UpdateExpenseDto extends PartialType(CreateExpenseDto) {} \ No newline at end of file diff --git a/src/modules/expenses/~misc_deprecated-files/upsert-expense.dto.ts b/src/modules/expenses/~misc_deprecated-files/upsert-expense.dto.ts new file mode 100644 index 0000000..3991aed --- /dev/null +++ b/src/modules/expenses/~misc_deprecated-files/upsert-expense.dto.ts @@ -0,0 +1,59 @@ +// import { Transform, Type } from "class-transformer"; +// import { +// IsNumber, +// IsOptional, +// IsString, +// Matches, +// MaxLength, +// Min, +// ValidateIf, +// ValidateNested +// } from "class-validator"; + +// export class ExpensePayloadDto { +// @IsString() +// type!: string; + +// @ValidateIf(o => (o.type ?? '').toUpperCase() !== 'MILEAGE') +// @IsNumber() +// @Min(0) +// amount?: number; + +// @ValidateIf(o => (o.type ?? '').toUpperCase() === 'MILEAGE') +// @IsNumber() +// @Min(0) +// mileage?: number; + +// @IsString() +// @MaxLength(280) +// @Transform(({ value }) => (typeof value === 'string' ? value.trim() : value)) +// comment!: string; + +// @IsOptional() +// @Transform(({ value }) => { +// if (value === null || value === undefined || value === '') return undefined; +// if (typeof value === 'number') return value.toString(); +// if (typeof value === 'string') { +// const trimmed = value.trim(); +// return trimmed.length ? trimmed : undefined; +// } +// return undefined; +// }) +// @IsString() +// @Matches(/^\d+$/) +// @MaxLength(255) +// attachment?: string; +// } + + +// export class UpsertExpenseDto { +// @IsOptional() +// @ValidateNested() +// @Type(()=> ExpensePayloadDto) +// old_expense?: ExpensePayloadDto; + +// @IsOptional() +// @ValidateNested() +// @Type(()=> ExpensePayloadDto) +// new_expense?: ExpensePayloadDto; +// } \ No newline at end of file diff --git a/src/modules/timesheets/controllers/timesheet.controller.ts b/src/modules/timesheets/controllers/timesheet.controller.ts index 83f28c0..1efb2fe 100644 --- a/src/modules/timesheets/controllers/timesheet.controller.ts +++ b/src/modules/timesheets/controllers/timesheet.controller.ts @@ -1,17 +1,26 @@ +import { EmailToIdResolver } from "src/modules/shared/utils/resolve-email-id.utils"; import { GetTimesheetsOverviewService } from "../services/timesheet-get-overview.service"; -import { Controller, Get, Query} from "@nestjs/common"; +import { BadRequestException, Controller, Get, Query} from "@nestjs/common"; @Controller('timesheets') export class TimesheetController { - constructor(private readonly timesheetOverview: GetTimesheetsOverviewService){} + constructor( + private readonly timesheetOverview: GetTimesheetsOverviewService, + private readonly emailResolver: EmailToIdResolver, + ){} @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); + @Query('employee_email') employee_email: string, + @Query('year') year: string, + @Query('period_number') period_number: string, + ) { + if (!employee_email || !year || !period_number) { + throw new BadRequestException('Query params "employee_email", "year" and eriod_number" are required.'); + } + const employee_id = await this.emailResolver.findIdByEmail(employee_email); + const pay_year = Number(year); + const period_num = Number(period_number); + return this.timesheetOverview.getTimesheetsForEmployeeByPeriod(employee_id, pay_year, period_num); } - - - -} \ No newline at end of file +} diff --git a/src/modules/timesheets/services/timesheet-get-overview.service.ts b/src/modules/timesheets/services/timesheet-get-overview.service.ts index 8cf22a8..9697c44 100644 --- a/src/modules/timesheets/services/timesheet-get-overview.service.ts +++ b/src/modules/timesheets/services/timesheet-get-overview.service.ts @@ -1,6 +1,6 @@ +import { sevenDaysFrom, toDateFromString, toHHmmFromDate, toStringFromDate } from "../helpers/timesheets-date-time-helpers"; 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; @@ -23,26 +23,31 @@ type TotalExpenses = { 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`); + async getTimesheetsForEmployeeByPeriod(employee_id: number, pay_year: number, pay_period_no: number) { + //find period using year and period_no + const period = await this.prisma.payPeriods.findFirst({ + where: { pay_year, pay_period_no }, + }); + if (!period) throw new NotFoundException(`Pay period ${pay_year}-${pay_period_no} not found`); - //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' }, + //loads the timesheets related to the fetched pay-period + const rows = await this.loadTimesheets({ + employee_id, + start_date: { gte: period.period_start, lte: period.period_end }, }); - if (rows.length === 0) throw new NotFoundException('Timesheet(s) not found'); - - //build full name - const user = rows[0].employee.user; + //find user infos using the employee_id + const employee = await this.prisma.employees.findUnique({ + where: { id: employee_id }, + include: { user: true }, + }); + if (!employee) throw new NotFoundException(`Employee #${employee_id} not found`); + + //builds employee full name + const user = employee.user; const employee_fullname = `${user.first_name} ${user.last_name}`.trim(); + //maps all timesheet's infos const timesheets = rows.map((timesheet) => this.mapOneTimesheet(timesheet)); return { employee_fullname, timesheets }; } @@ -51,16 +56,29 @@ export class GetTimesheetsOverviewService { //----------------------------------------------------------------------------------- // MAPPERS & HELPERS //----------------------------------------------------------------------------------- + //fetch timesheet's infos + private async loadTimesheets(where: any) { + return this.prisma.timesheets.findMany({ + where, + include: { + employee: { include: { user: true } }, + shift: { include: { bank_code: true } }, + expense: { include: { bank_code: true, attachment_record: true } }, + }, + orderBy: { start_date: 'asc' }, + }); + } + private mapOneTimesheet(timesheet: any) { //converts string to UTC date format - const start = toDateFromString(timesheet.start_date); + const start = toDateFromString(timesheet.start_date); const day_dates = sevenDaysFrom(start); //map of shifts by days const shifts_by_date = new Map(); for (const shift of timesheet.shift) { const date = toStringFromDate(shift.date); - const arr = shifts_by_date.get(date) ?? []; + const arr = shifts_by_date.get(date) ?? []; arr.push(shift); shifts_by_date.set(date, arr); } @@ -68,40 +86,40 @@ export class GetTimesheetsOverviewService { const expenses_by_date = new Map(); for (const expense of timesheet.expense) { const date = toStringFromDate(expense.date); - const arr = expenses_by_date.get(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_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 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, + 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, + 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, + 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 ?? '', + comment: expense.comment ?? '', supervisor_comment: expense.supervisor_comment, })); @@ -113,7 +131,7 @@ export class GetTimesheetsOverviewService { 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; + daily_hours[0][subgroup] += hours; weekly_hours[0][subgroup] += hours; } @@ -148,7 +166,7 @@ export class GetTimesheetsOverviewService { }); return { timesheet_id: timesheet.id, - is_approved: timesheet.is_approved ?? false, + is_approved: timesheet.is_approved ?? false, days, weekly_hours, weekly_expenses, @@ -156,37 +174,36 @@ export class GetTimesheetsOverviewService { } } -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 }; -} +//filled array with default values +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 } }; +//calculate the differences of hours 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; -} +//validate numeric values +const num = (value: any): number => { return value ? Number(value) : 0 }; +// shift's subgroup types const hoursSubGroupFromBankCode = (bank_code: any): keyof TotalHours => { const type = bank_code.type; - if (type.includes('EVENING')) return 'evening'; + 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'; + 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' } +// expense's subgroup types const expenseSubgroupFromBankCode = (bank_code: any): keyof TotalExpenses => { const type = bank_code.type; - if (type.includes('MILEAGE')) return 'mileage'; + if (type.includes('MILEAGE')) return 'mileage'; if (type.includes('PER_DIEM')) return 'per_diem'; - if (type.includes('ON_CALL')) return 'on_call'; + if (type.includes('ON_CALL')) return 'on_call'; return 'expenses'; } diff --git a/src/modules/timesheets/timesheets.module.ts b/src/modules/timesheets/timesheets.module.ts index 32d2c52..be44043 100644 --- a/src/modules/timesheets/timesheets.module.ts +++ b/src/modules/timesheets/timesheets.module.ts @@ -16,9 +16,8 @@ import { Module } from '@nestjs/common'; providers: [ TimesheetArchiveService, GetTimesheetsOverviewService, + SharedModule, ], - exports: [ - - ], + exports: [], }) export class TimesheetsModule {}