feat(Result): ajusted return values to match Result pattern.

This commit is contained in:
Matthieu Haineault 2025-11-12 09:16:37 -05:00
parent a8d53ab0aa
commit 1d9eaeab30
20 changed files with 205 additions and 214 deletions

0
npm
View File

View File

@ -2,11 +2,11 @@ export type Result<T, E> =
| { success: true; data: T } | { success: true; data: T }
| { success: false; error: E }; | { success: false; error: E };
const success = <T>(data: T): Result<T, never> => { // const success = <T>(data: T): Result<T, never> => {
return { success: true, data }; // return { success: true, data };
} // }
const failure = <E>(error: E): Result<never, E> => { // const failure = <E>(error: E): Result<never, E> => {
return { success: false, error }; // return { success: false, error };
} // }

View File

@ -1,10 +1,28 @@
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { Prisma, PrismaClient } from '@prisma/client';
import { getWeekStart, getWeekEnd, computeHours } from 'src/common/utils/date-utils'; import { getWeekStart, getWeekEnd, computeHours } from 'src/common/utils/date-utils';
import { PrismaService } from 'src/prisma/prisma.service'; import { PrismaService } from 'src/prisma/prisma.service';
import { DAILY_LIMIT_HOURS, WEEKLY_LIMIT_HOURS } from 'src/time-and-attendance/utils/constants.utils'; import { DAILY_LIMIT_HOURS, WEEKLY_LIMIT_HOURS } from 'src/time-and-attendance/utils/constants.utils';
import { Tx, WeekOvertimeSummary } from 'src/time-and-attendance/utils/type.utils';
type Tx = Prisma.TransactionClient | PrismaClient;
type WeekOvertimeSummary = {
week_start:string;
week_end: string;
week_total_hours: number;
weekly_overtime: number;
daily_overtime_kept: number;
total_overtime: number;
breakdown: Array<{
date:string;
day_hours: number;
day_overtime: number;
daily_kept: number;
running_total_before: number;
}>;
};
@Injectable() @Injectable()
export class OvertimeService { export class OvertimeService {

View File

@ -1,5 +1,4 @@
import { Controller, Post, Param, Body, Patch, Delete, Req, UnauthorizedException } from "@nestjs/common"; import { Controller, Post, Param, Body, Patch, Delete, Req, UnauthorizedException } from "@nestjs/common";
import { CreateExpenseResult } from "src/time-and-attendance/utils/type.utils";
import { ExpenseUpsertService } from "src/time-and-attendance/expenses/services/expense-upsert.service"; import { ExpenseUpsertService } from "src/time-and-attendance/expenses/services/expense-upsert.service";
import { ExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-create.dto"; import { ExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-create.dto";
import { RolesAllowed } from "src/common/decorators/roles.decorators"; import { RolesAllowed } from "src/common/decorators/roles.decorators";

View File

@ -1,6 +1,6 @@
import { toDateFromString, toStringFromDate, weekStartSunday } from "src/time-and-attendance/utils/date-time.utils"; import { toDateFromString, toStringFromDate, weekStartSunday } from "src/time-and-attendance/utils/date-time.utils";
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
import { NormalizedExpense } from "src/time-and-attendance/utils/type.utils"; // import { NormalizedExpense } from "src/time-and-attendance/utils/type.utils";
import { expense_select } from "src/time-and-attendance/utils/selects.utils"; import { expense_select } from "src/time-and-attendance/utils/selects.utils";
import { PrismaService } from "src/prisma/prisma.service"; import { PrismaService } from "src/prisma/prisma.service";
import { GetExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-get.dto"; import { GetExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-get.dto";
@ -8,6 +8,7 @@ import { ExpenseEntity } from "src/time-and-attendance/expenses/dtos/expense-ent
import { ExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-create.dto"; import { ExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-create.dto";
import { Injectable } from "@nestjs/common"; import { Injectable } from "@nestjs/common";
import { Result } from "src/common/errors/result-error.factory"; import { Result } from "src/common/errors/result-error.factory";
import { NormalizedExpense } from "src/time-and-attendance/utils/type.utils";
@Injectable() @Injectable()

View File

@ -1,6 +1,8 @@
import { LeaveRequestViewDto } from "../dtos/leave-request-view.dto"; import { LeaveRequestViewDto } from "../dtos/leave-request-view.dto";
import { LeaveRequestRow } from "src/time-and-attendance/utils/type.utils";
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils";
type LeaveRequestRow = Prisma.LeaveRequestsGetPayload<{ select: typeof leaveRequestsSelect}>;
const toNum = (value?: Prisma.Decimal | null) => const toNum = (value?: Prisma.Decimal | null) =>
value !== null && value !== undefined ? Number(value) : undefined; value !== null && value !== undefined ? Number(value) : undefined;

View File

@ -1,9 +1,11 @@
import { Prisma } from "@prisma/client";
import { LeaveRequestViewDto } from "src/time-and-attendance/leave-requests/dtos/leave-request-view.dto"; import { LeaveRequestViewDto } from "src/time-and-attendance/leave-requests/dtos/leave-request-view.dto";
import { mapArchiveRowToView } from "src/time-and-attendance/leave-requests/mappers/leave-requests-archive.mapper"; import { mapArchiveRowToView } from "src/time-and-attendance/leave-requests/mappers/leave-requests-archive.mapper";
import { mapRowToView } from "src/time-and-attendance/leave-requests/mappers/leave-requests.mapper"; import { mapRowToView } from "src/time-and-attendance/leave-requests/mappers/leave-requests.mapper";
import { LeaveRequestArchiveRow } from "src/time-and-attendance/leave-requests/utils/leave-requests-archive.select"; import { LeaveRequestArchiveRow } from "src/time-and-attendance/leave-requests/utils/leave-requests-archive.select";
import { LeaveRequestRow } from "src/time-and-attendance/utils/type.utils"; import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils";
export type LeaveRequestRow = Prisma.LeaveRequestsGetPayload<{ select: typeof leaveRequestsSelect}>;
/** Active (table leave_requests) : proxy to base mapper */ /** Active (table leave_requests) : proxy to base mapper */
export function mapRowToViewWithDays(row: LeaveRequestRow): LeaveRequestViewDto { export function mapRowToViewWithDays(row: LeaveRequestRow): LeaveRequestViewDto {

View File

@ -1,7 +1,6 @@
import { PresetResponse, ShiftResponse } from "src/time-and-attendance/utils/type.utils"; import { PresetResponse, ShiftResponse } from "src/time-and-attendance/utils/type.utils";
import { PrismaService } from "src/prisma/prisma.service"; import { PrismaService } from "src/prisma/prisma.service";
import { Injectable } from "@nestjs/common"; import { Injectable } from "@nestjs/common";
import { Prisma } from "@prisma/client";
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
import { Result } from "src/common/errors/result-error.factory"; import { Result } from "src/common/errors/result-error.factory";

View File

@ -1,5 +1,4 @@
import { Injectable, BadRequestException, NotFoundException, ConflictException } from "@nestjs/common"; import { Injectable, BadRequestException, NotFoundException, ConflictException } from "@nestjs/common";
import { CreatePresetResult, DeletePresetResult, UpdatePresetResult } from "src/time-and-attendance/utils/type.utils";
import { Prisma, Weekday } from "@prisma/client"; import { Prisma, Weekday } from "@prisma/client";
import { toDateFromString, toHHmmFromDate } from "src/time-and-attendance/utils/date-time.utils"; import { toDateFromString, toHHmmFromDate } from "src/time-and-attendance/utils/date-time.utils";
import { PrismaService } from "src/prisma/prisma.service"; import { PrismaService } from "src/prisma/prisma.service";
@ -21,7 +20,7 @@ export class SchedulePresetsUpsertService {
async createPreset(email: string, dto: SchedulePresetsDto): Promise<Result<SchedulePresetsDto, string>> { async createPreset(email: string, dto: SchedulePresetsDto): Promise<Result<SchedulePresetsDto, string>> {
try { try {
const shifts_data = await this.normalizePresetShifts(dto); const shifts_data = await this.normalizePresetShifts(dto);
if (!shifts_data) return { success: false, error: `Employee with email: ${email} or dto not found` }; if (!shifts_data.success) return { success: false, error: `Employee with email: ${email} or dto not found` };
const employee_id = await this.emailResolver.findIdByEmail(email); const employee_id = await this.emailResolver.findIdByEmail(email);
if (!employee_id.success) return { success: false, error: employee_id.error }; if (!employee_id.success) return { success: false, error: employee_id.error };
@ -38,7 +37,7 @@ export class SchedulePresetsUpsertService {
employee_id: employee_id.data, employee_id: employee_id.data,
name: dto.name, name: dto.name,
is_default: !!dto.is_default, is_default: !!dto.is_default,
shifts: { create: shifts_data }, shifts: { create: shifts_data.data },
}, },
}); });
return { success: true, data: created } return { success: true, data: created }
@ -66,6 +65,7 @@ export class SchedulePresetsUpsertService {
if (!existing) return { success: false, error: `Preset "${dto.name}" not found` }; if (!existing) return { success: false, error: `Preset "${dto.name}" not found` };
const shifts_data = await this.normalizePresetShifts(dto); const shifts_data = await this.normalizePresetShifts(dto);
if(!shifts_data.success) return { success: false, error: 'An error occured during normalization'}
await this.prisma.$transaction(async (tx) => { await this.prisma.$transaction(async (tx) => {
if (typeof dto.is_default === 'boolean') { if (typeof dto.is_default === 'boolean') {
@ -87,33 +87,34 @@ export class SchedulePresetsUpsertService {
}, },
}); });
} }
if (shifts_data.length <= 0) return { success: false, error: 'Preset shifts to update not found' }; if (shifts_data.data.length <= 0) return { success: false, error: 'Preset shifts to update not found' };
await tx.schedulePresetShifts.deleteMany({ where: { preset_id: existing.id } }); await tx.schedulePresetShifts.deleteMany({ where: { preset_id: existing.id } });
try { // try {
const create_many_data: Prisma.SchedulePresetShiftsCreateManyInput[] = // const create_many_data: Result<Prisma.SchedulePresetShiftsCreateManyInput[], string> =
shifts_data.map((shift) => { // shifts_data.data.map((shift) => {
if (!shift.bank_code || !('connect' in shift.bank_code) || typeof shift.bank_code.connect?.id !== 'number') { // if (!shift.bank_code || !('connect' in shift.bank_code) || typeof shift.bank_code.connect?.id !== 'number') {
throw new NotFoundException(`Bank code is required for updates( ${shift.week_day}, ${shift.sort_order})`); // return { success: false, error: `Bank code is required for updates( ${shift.week_day}, ${shift.sort_order})`}
} // }
const bank_code_id = shift.bank_code.connect.id; // const bank_code_id = shift.bank_code.connect.id;
return { // return {
preset_id: existing.id, // preset_id: existing.id,
week_day: shift.week_day, // week_day: shift.week_day,
sort_order: shift.sort_order, // sort_order: shift.sort_order,
start_time: shift.start_time, // start_time: shift.start_time,
end_time: shift.end_time, // end_time: shift.end_time,
is_remote: shift.is_remote ?? false, // is_remote: shift.is_remote ?? false,
bank_code_id: bank_code_id, // bank_code_id: bank_code_id,
}; // };
}); // });
await tx.schedulePresetShifts.createMany({ data: create_many_data }); // if(!create_many_data.success) return { success: false, error: 'Invalid data'}
// await tx.schedulePresetShifts.createMany({ data: create_many_data.data });
return { success: true, data: create_many_data } // return { success: true, data: create_many_data }
} catch (error) { // } catch (error) {
return { success: false, error: 'An error occured. Invalid data detected. ' }; // return { success: false, error: 'An error occured. Invalid data detected. ' };
} // }
}); });
const saved = await this.prisma.schedulePresets.findUnique({ const saved = await this.prisma.schedulePresets.findUnique({
@ -175,15 +176,16 @@ export class SchedulePresetsUpsertService {
//resolve bank_code_id using type and convert hours to TIME and valid shifts end/start //resolve bank_code_id using type and convert hours to TIME and valid shifts end/start
private async normalizePresetShifts( private async normalizePresetShifts(
dto: SchedulePresetsDto dto: SchedulePresetsDto
): Promise<Prisma.SchedulePresetShiftsCreateWithoutPresetInput[]> { ): Promise<Result<Prisma.SchedulePresetShiftsCreateWithoutPresetInput[], string>> {
if (!dto.preset_shifts?.length) throw new NotFoundException(`Empty or preset shifts not found`); if (!dto.preset_shifts?.length) throw new NotFoundException(`Empty or preset shifts not found`);
const types = Array.from(new Set(dto.preset_shifts.map((shift) => shift.type))); const types = Array.from(new Set(dto.preset_shifts.map((shift) => shift.type)));
const bank_code_set = new Map<string, number>(); const bank_code_set = new Map<string, number>();
for (const type of types) { for (const type of types) {
const { id } = await this.typeResolver.findIdAndModifierByType(type); const bank_code = await this.typeResolver.findIdAndModifierByType(type);
bank_code_set.set(type, id) if (!bank_code.success) return { success: false, error: 'Bank_code not found' }
bank_code_set.set(type, bank_code.data.id);
} }
const pair_set = new Set<string>(); const pair_set = new Set<string>();
@ -216,6 +218,6 @@ export class SchedulePresetsUpsertService {
is_remote: !!shift.is_remote, is_remote: !!shift.is_remote,
}; };
}); });
return items; return { success: true, data: items };
} }
} }

View File

@ -1,6 +1,5 @@
import { Body, Controller, Delete, Param, Patch, Post, Req } from "@nestjs/common"; import { Body, Controller, Delete, Param, Patch, Post, Req } from "@nestjs/common";
import { ShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-create.dto"; import { ShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-create.dto";
import { CreateShiftResult, UpdateShiftResult } from "src/time-and-attendance/utils/type.utils";
import { RolesAllowed } from "src/common/decorators/roles.decorators"; import { RolesAllowed } from "src/common/decorators/roles.decorators";
import { GLOBAL_CONTROLLER_ROLES } from "src/common/shared/role-groupes"; import { GLOBAL_CONTROLLER_ROLES } from "src/common/shared/role-groupes";
import { Result } from "src/common/errors/result-error.factory"; import { Result } from "src/common/errors/result-error.factory";

View File

@ -63,12 +63,13 @@ export class ShiftsCreateService {
try { try {
//transform string format to date and HHmm //transform string format to date and HHmm
const normed_shift = await this.normalizeShiftDto(dto); const normed_shift = await this.normalizeShiftDto(dto);
if (normed_shift.end_time <= normed_shift.start_time) return { if(!normed_shift.success) return { success: false, error: 'An error occured during normalization' }
if (normed_shift.data.end_time <= normed_shift.data.start_time) return {
success: false, success: false,
error: `INVALID_SHIFT - ` error: `INVALID_SHIFT - `
+ `start_time: ${toStringFromHHmm(normed_shift.start_time)},` + `start_time: ${toStringFromHHmm(normed_shift.data.start_time)},`
+ `end_time: ${toStringFromHHmm(normed_shift.end_time)},` + `end_time: ${toStringFromHHmm(normed_shift.data.end_time)},`
+ `date: ${toStringFromDate(normed_shift.date)}.` + `date: ${toStringFromDate(normed_shift.data.date)}.`
} }
//fetch the right timesheet //fetch the right timesheet
const timesheet = await this.prisma.timesheets.findUnique({ const timesheet = await this.prisma.timesheets.findUnique({
@ -78,16 +79,17 @@ export class ShiftsCreateService {
if (!timesheet) return { if (!timesheet) return {
success: false, success: false,
error: `INVALID_TIMESHEET -` error: `INVALID_TIMESHEET -`
+ `start_time: ${toStringFromHHmm(normed_shift.start_time)},` + `start_time: ${toStringFromHHmm(normed_shift.data.start_time)},`
+ `end_time: ${toStringFromHHmm(normed_shift.end_time)},` + `end_time: ${toStringFromHHmm(normed_shift.data.end_time)},`
+ `date: ${toStringFromDate(normed_shift.date)}.` + `date: ${toStringFromDate(normed_shift.data.date)}.`
} }
//finds bank_code_id using the type //finds bank_code_id using the type
const bank_code = await this.typeResolver.findBankCodeIDByType(dto.type); const bank_code_id = await this.typeResolver.findBankCodeIDByType(dto.type);
if (!bank_code_id.success) return { success: false, error: bank_code_id.error };
//fetchs existing shifts from DB to check for overlaps //fetchs existing shifts from DB to check for overlaps
const existing_shifts = await this.prisma.shifts.findMany({ const existing_shifts = await this.prisma.shifts.findMany({
where: { timesheet_id: timesheet.id, date: normed_shift.date }, where: { timesheet_id: timesheet.id, date: normed_shift.data.date },
select: { id: true, date: true, start_time: true, end_time: true }, select: { id: true, date: true, start_time: true, end_time: true },
}); });
for (const existing of existing_shifts) { for (const existing of existing_shifts) {
@ -96,16 +98,16 @@ export class ShiftsCreateService {
const existing_date = await toDateFromString(existing.date); const existing_date = await toDateFromString(existing.date);
const has_overlap = overlaps( const has_overlap = overlaps(
{ start: normed_shift.start_time, end: normed_shift.end_time, date: normed_shift.date }, { start: normed_shift.data.start_time, end: normed_shift.data.end_time, date: normed_shift.data.date },
{ start: existing_start, end: existing_end, date: existing_date }, { start: existing_start, end: existing_end, date: existing_date },
); );
if (has_overlap) { if (has_overlap) {
return { return {
success: false, success: false,
error: `SHIFT_OVERLAP` error: `SHIFT_OVERLAP`
+ `new shift: ${toStringFromHHmm(normed_shift.start_time)}${toStringFromHHmm(normed_shift.end_time)} ` + `new shift: ${toStringFromHHmm(normed_shift.data.start_time)}${toStringFromHHmm(normed_shift.data.end_time)} `
+ `existing shift: ${toStringFromHHmm(existing.start_time)}${toStringFromHHmm(existing.end_time)} ` + `existing shift: ${toStringFromHHmm(existing.start_time)}${toStringFromHHmm(existing.end_time)} `
+ `date: ${toStringFromDate(normed_shift.date)})`, + `date: ${toStringFromDate(normed_shift.data.date)})`,
} }
} }
} }
@ -114,10 +116,10 @@ export class ShiftsCreateService {
const created_shift = await this.prisma.shifts.create({ const created_shift = await this.prisma.shifts.create({
data: { data: {
timesheet_id: timesheet.id, timesheet_id: timesheet.id,
bank_code_id: bank_code.id, bank_code_id: bank_code_id.data,
date: normed_shift.date, date: normed_shift.data.date,
start_time: normed_shift.start_time, start_time: normed_shift.data.start_time,
end_time: normed_shift.end_time, end_time: normed_shift.data.end_time,
is_approved: dto.is_approved, is_approved: dto.is_approved,
is_remote: dto.is_remote, is_remote: dto.is_remote,
comment: dto.comment ?? '', comment: dto.comment ?? '',
@ -145,11 +147,14 @@ export class ShiftsCreateService {
// LOCAL HELPERS // LOCAL HELPERS
//_________________________________________________________________ //_________________________________________________________________
//converts all string hours and date to Date and HHmm formats //converts all string hours and date to Date and HHmm formats
private normalizeShiftDto = async (dto: ShiftDto): Promise<Normalized> => { private normalizeShiftDto = async (dto: ShiftDto): Promise<Result<Normalized, string>> => {
const { id: bank_code_id } = await this.typeResolver.findBankCodeIDByType(dto.type); const bank_code_id = await this.typeResolver.findBankCodeIDByType(dto.type);
if(!bank_code_id.success) return { success: false, error: 'Bank_code not found'}
const date = toDateFromString(dto.date); const date = toDateFromString(dto.date);
const start_time = toHHmmFromString(dto.start_time); const start_time = toHHmmFromString(dto.start_time);
const end_time = toHHmmFromString(dto.end_time); const end_time = toHHmmFromString(dto.end_time);
return { date, start_time, end_time, bank_code_id: bank_code_id };
return { success: true, data: {date, start_time, end_time, bank_code_id: bank_code_id.data} };
} }
} }

View File

@ -1,14 +1,10 @@
import { BadRequestException, NotFoundException, ConflictException } from "@nestjs/common";
import { Result } from "src/common/errors/result-error.factory"; import { Result } from "src/common/errors/result-error.factory";
import { PrismaService } from "src/prisma/prisma.service"; import { PrismaService } from "src/prisma/prisma.service";
import { ShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-create.dto"; import { ShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-create.dto";
import { ShiftEntity } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-entity.dto"; import { toDateFromString, toHHmmFromString, toStringFromHHmm, toStringFromDate, overlaps } from "src/time-and-attendance/utils/date-time.utils";
import { GetShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-get.dto";
import { ShiftsCreateService } from "src/time-and-attendance/time-tracker/shifts/services/shifts-create.service";
import { toDateFromString, toHHmmFromString, overlaps, toStringFromHHmm, toStringFromDate } from "src/time-and-attendance/utils/date-time.utils";
import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils"; import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils";
import { shift_select } from "src/time-and-attendance/utils/selects.utils"; import { shift_select } from "src/time-and-attendance/utils/selects.utils";
import { UpdateShiftResult, Normalized } from "src/time-and-attendance/utils/type.utils"; import { Normalized } from "src/time-and-attendance/utils/type.utils";
export class ShiftsUpdateDeleteService { export class ShiftsUpdateDeleteService {
constructor( constructor(
@ -22,7 +18,7 @@ export class ShiftsUpdateDeleteService {
if (!Array.isArray(shifts) || shifts.length === 0) return { success: false, error: 'No data received' }; if (!Array.isArray(shifts) || shifts.length === 0) return { success: false, error: 'No data received' };
//calls the update functions and await the return of successfull result or not //calls the update functions and await the return of successfull result or not
const results = await Promise.allSettled(shifts.map(shift => this.updateShifts(shift))); const results = await Promise.allSettled(shifts.map(shift => this.updateShift(shift)));
//return arrays of updated shifts or errors //return arrays of updated shifts or errors
const updated_shifts: ShiftDto[] = []; const updated_shifts: ShiftDto[] = [];
@ -52,7 +48,7 @@ export class ShiftsUpdateDeleteService {
//_________________________________________________________________ //_________________________________________________________________
// UPDATE // UPDATE
//_________________________________________________________________ //_________________________________________________________________
async updateShifts(dto: ShiftDto): Promise<Result<ShiftDto, string>> { async updateShift(dto: ShiftDto): Promise<Result<ShiftDto, string>> {
try { try {
//finds original shift //finds original shift
const original = await this.prisma.shifts.findFirst({ const original = await this.prisma.shifts.findFirst({
@ -63,32 +59,36 @@ export class ShiftsUpdateDeleteService {
//transform string format to date and HHmm //transform string format to date and HHmm
const normed_shift = await this.normalizeShiftDto(dto); const normed_shift = await this.normalizeShiftDto(dto);
if (normed_shift.end_time <= normed_shift.start_time) return { if (!normed_shift.success) return { success: false, error: 'An error occured during normalization' }
if (normed_shift.data.end_time <= normed_shift.data.start_time) return {
success: false, success: false,
error: `INVALID_SHIFT - ` error: `INVALID_SHIFT - `
+ `start_time: ${toStringFromHHmm(normed_shift.start_time)},` + `start_time: ${toStringFromHHmm(normed_shift.data.start_time)},`
+ `end_time: ${toStringFromHHmm(normed_shift.end_time)},` + `end_time: ${toStringFromHHmm(normed_shift.data.end_time)},`
+ `date: ${toStringFromDate(normed_shift.date)}.` + `date: ${toStringFromDate(normed_shift.data.date)}.`
}; };
const overlap_check = await this.overlapChecker(normed_shift.data);
if(!overlap_check.success) return { success: false, error: 'Invalid shift, overlaps with existing shifts'}
//finds bank_code_id using the type //finds bank_code_id using the type
const bank_code = await this.typeResolver.findBankCodeIDByType(dto.type); const bank_code = await this.typeResolver.findBankCodeIDByType(dto.type);
if (!bank_code.success) return { success: false, error: 'No bank_code_id found' };
//updates sent to DB //updates sent to DB
const updated = await this.prisma.shifts.update({ const updated = await this.prisma.shifts.update({
where: { id: original.id }, where: { id: original.id },
data: { data: {
date: normed_shift.date, date: normed_shift.data.date,
start_time: normed_shift.start_time, start_time: normed_shift.data.start_time,
end_time: normed_shift.end_time, end_time: normed_shift.data.end_time,
bank_code_id: bank_code.id, bank_code_id: bank_code.data,
comment: dto.comment, comment: dto.comment,
is_approved: dto.is_approved, is_approved: dto.is_approved,
is_remote: dto.is_remote, is_remote: dto.is_remote,
}, },
select: shift_select, select: shift_select,
}); });
if(!updated) return {success: false, error: ' An error occured during update, Invalid Datas'}; if (!updated) return { success: false, error: ' An error occured during update, Invalid Datas' };
// builds an object to return for display in the frontend // builds an object to return for display in the frontend
const shift: ShiftDto = { const shift: ShiftDto = {
@ -134,11 +134,42 @@ export class ShiftsUpdateDeleteService {
//_________________________________________________________________ //_________________________________________________________________
// helpers // helpers
//_________________________________________________________________ //_________________________________________________________________
private normalizeShiftDto = async (dto: ShiftDto): Promise<Normalized> => { private normalizeShiftDto = async (dto: ShiftDto): Promise<Result<Normalized, string>> => {
const { id: bank_code_id } = await this.typeResolver.findBankCodeIDByType(dto.type); const bank_code_id = await this.typeResolver.findBankCodeIDByType(dto.type);
if (!bank_code_id.success) return { success: false, error: 'Bank_code not found' }
const date = toDateFromString(dto.date); const date = toDateFromString(dto.date);
const start_time = toHHmmFromString(dto.start_time); const start_time = toHHmmFromString(dto.start_time);
const end_time = toHHmmFromString(dto.end_time); const end_time = toHHmmFromString(dto.end_time);
return { date, start_time, end_time, bank_code_id: bank_code_id };
return { success: true, data: { date, start_time, end_time, bank_code_id: bank_code_id.data } };
}
private overlapChecker = async (dto: Normalized): Promise<Result<void, string>> => {
const existing_shifts = await this.prisma.shifts.findMany({
where: { date: dto.date },
select: { id: true, date: true, start_time: true, end_time: true },
});
for (const existing of existing_shifts) {
const existing_start = toDateFromString(existing.start_time);
const existing_end = toDateFromString(existing.end_time);
const existing_date = toDateFromString(existing.date);
const has_overlap = overlaps(
{ start: dto.start_time, end: dto.end_time, date: dto.date },
{ start: existing_start, end: existing_end, date: existing_date },
);
if (has_overlap) {
return {
success: false,
error: `SHIFT_OVERLAP`
+ `new shift: ${toStringFromHHmm(dto.start_time)}${toStringFromHHmm(dto.end_time)} `
+ `existing shift: ${toStringFromHHmm(existing.start_time)}${toStringFromHHmm(existing.end_time)} `
+ `date: ${toStringFromDate(dto.date)})`,
}
}
}
return { success: true, data: undefined }
} }
} }

View File

@ -1,12 +1,28 @@
import { sevenDaysFrom, toStringFromDate, toHHmmFromDate, toDateFromString } from "src/time-and-attendance/utils/date-time.utils"; import { sevenDaysFrom, toStringFromDate, toHHmmFromDate, toDateFromString } from "src/time-and-attendance/utils/date-time.utils";
import { NUMBER_OF_TIMESHEETS_TO_RETURN } from "src/time-and-attendance/utils/constants.utils"; import { NUMBER_OF_TIMESHEETS_TO_RETURN } from "src/time-and-attendance/utils/constants.utils";
import { Injectable, NotFoundException } from "@nestjs/common"; import { Injectable } from "@nestjs/common";
import { TotalExpenses, TotalHours } from "src/time-and-attendance/utils/type.utils";
import { PrismaService } from "src/prisma/prisma.service"; import { PrismaService } from "src/prisma/prisma.service";
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
import { Timesheets } from "src/time-and-attendance/time-tracker/timesheets/dtos/timesheet.dto"; import { Timesheets } from "src/time-and-attendance/time-tracker/timesheets/dtos/timesheet.dto";
import { Result } from "src/common/errors/result-error.factory"; import { Result } from "src/common/errors/result-error.factory";
export type TotalHours = {
regular: number;
evening: number;
emergency: number;
overtime: number;
vacation: number;
holiday: number;
sick: number;
};
export type TotalExpenses = {
expenses: number;
per_diem: number;
on_call: number;
mileage: number;
};
@Injectable() @Injectable()
export class GetTimesheetsOverviewService { export class GetTimesheetsOverviewService {
constructor( constructor(
@ -61,14 +77,14 @@ export class GetTimesheetsOverviewService {
const employee_fullname = `${user.first_name} ${user.last_name}`.trim(); const employee_fullname = `${user.first_name} ${user.last_name}`.trim();
//maps all timesheet's infos //maps all timesheet's infos
const timesheets = rows.map((timesheet) => this.mapOneTimesheet(timesheet)); const timesheets = await Promise.all(rows.map((timesheet) => this.mapOneTimesheet(timesheet)));
return { success: true, data: { employee_fullname, timesheets } };
return { success: true, data:{ employee_fullname, timesheets} };
} catch (error) { } catch (error) {
return { success: false, error} return { success: false, error}
} }
} }
//----------------------------------------------------------------------------------- //-----------------------------------------------------------------------------------
// MAPPERS & HELPERS // MAPPERS & HELPERS
//----------------------------------------------------------------------------------- //-----------------------------------------------------------------------------------
@ -111,11 +127,14 @@ export class GetTimesheetsOverviewService {
const weekly_hours: TotalHours[] = [emptyHours()]; const weekly_hours: TotalHours[] = [emptyHours()];
const weekly_expenses: TotalExpenses[] = [emptyExpenses()]; const weekly_expenses: TotalExpenses[] = [emptyExpenses()];
//map of days //map of days
const days = day_dates.map((date) => { const days = day_dates.map((date) => {
const date_iso = toStringFromDate(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) ?? []; const expenses_source = expenses_by_date.get(date_iso) ?? [];
//inner map of shifts //inner map of shifts
const shifts = shifts_source.map((shift) => ({ const shifts = shifts_source.map((shift) => ({
timesheet_id: shift.timesheet_id, timesheet_id: shift.timesheet_id,
@ -139,6 +158,7 @@ export class GetTimesheetsOverviewService {
is_approved: expense.is_approved ?? false, is_approved: expense.is_approved ?? false,
comment: expense.comment ?? '', comment: expense.comment ?? '',
supervisor_comment: expense.supervisor_comment, supervisor_comment: expense.supervisor_comment,
type: expense.type,
})); }));
//daily totals //daily totals

View File

@ -1,44 +1,47 @@
import { Injectable, NotFoundException } from "@nestjs/common"; import { Injectable, NotFoundException } from "@nestjs/common";
import { Prisma, PrismaClient } from "@prisma/client"; import { Prisma, PrismaClient } from "@prisma/client";
import { Result } from "src/common/errors/result-error.factory";
import { PrismaService } from "src/prisma/prisma.service"; import { PrismaService } from "src/prisma/prisma.service";
type Tx = Prisma.TransactionClient | PrismaClient; type Tx = Prisma.TransactionClient | PrismaClient;
@Injectable() @Injectable()
export class BankCodesResolver { export class BankCodesResolver {
constructor(private readonly prisma: PrismaService) {} constructor(private readonly prisma: PrismaService) { }
//find id and modifier by type //find id and modifier by type
readonly findIdAndModifierByType = async ( type: string, client?: Tx readonly findIdAndModifierByType = async (type: string, client?: Tx
): Promise<{id:number; modifier: number }> => { ): Promise<Result<{ id: number; modifier: number }, string>> => {
const db = client ?? this.prisma; const db = client ?? this.prisma;
const bank = await db.bankCodes.findFirst({ const bank = await db.bankCodes.findFirst({
where: { type }, where: { type },
select: { id: true, modifier: true }, select: { id: true, modifier: true },
}); });
if (!bank) return { success: false, error: `Unknown bank code type: ${type}` };
if(!bank) throw new NotFoundException(`Unknown bank code type: ${type}`); return { success: true, data: { id: bank.id, modifier: bank.modifier } };
return { id: bank.id, modifier: bank.modifier };
}; };
//finds only id by type //finds only id by type
readonly findBankCodeIDByType = async (type: string, client?: Tx) => { readonly findBankCodeIDByType = async (type: string, client?: Tx): Promise<Result<number, string>> => {
const db = client ?? this.prisma; const db = client ?? this.prisma;
const bank_code_id = await db.bankCodes.findFirst({ const bank_code = await db.bankCodes.findFirst({
where: { type }, where: { type },
select: {id: true}, select: { id: true },
}); });
if(!bank_code_id) throw new NotFoundException(`Unkown bank type: ${type}`); if (!bank_code) return { success: false, error:`Unkown bank type: ${type}`};
return bank_code_id;
return { success: true, data: bank_code.id};
} }
readonly findTypeByBankCodeId = async (bank_code_id: number, client?: Tx) => { readonly findTypeByBankCodeId = async (bank_code_id: number, client?: Tx): Promise<Result<string, string>> => {
const db = client ?? this.prisma; const db = client ?? this.prisma;
const type = await db.bankCodes.findFirst({ const bank_code = await db.bankCodes.findFirst({
where: { id: bank_code_id }, where: { id: bank_code_id },
select: { type: true }, select: { type: true },
}); });
if(!type) throw new NotFoundException(`Type with id : ${bank_code_id} not found`); if (!bank_code) return {success: false, error: `Type with id : ${bank_code_id} not found` }
return type;
return {success: true, data: bank_code.type};
} }
} }

View File

@ -1,5 +1,6 @@
import { Injectable, NotFoundException } from "@nestjs/common"; import { Injectable } from "@nestjs/common";
import { Prisma, PrismaClient } from "@prisma/client"; import { Prisma, PrismaClient } from "@prisma/client";
import { Result } from "src/common/errors/result-error.factory";
import { PrismaService } from "src/prisma/prisma.service"; import { PrismaService } from "src/prisma/prisma.service";
type Tx = Prisma.TransactionClient | PrismaClient; type Tx = Prisma.TransactionClient | PrismaClient;
@ -8,15 +9,15 @@ type Tx = Prisma.TransactionClient | PrismaClient;
export class FullNameResolver { export class FullNameResolver {
constructor(private readonly prisma: PrismaService){} constructor(private readonly prisma: PrismaService){}
readonly resolveFullName = async (employee_id: number, client?: Tx): Promise<string> =>{ readonly resolveFullName = async (employee_id: number, client?: Tx): Promise<Result<string, string>> =>{
const db = client ?? this.prisma; const db = client ?? this.prisma;
const employee = await db.employees.findUnique({ const employee = await db.employees.findUnique({
where: { id: employee_id }, where: { id: employee_id },
select: { user: { select: {first_name: true, last_name: true} } }, select: { user: { select: {first_name: true, last_name: true} } },
}); });
if(!employee) throw new NotFoundException(`Unknown user with name: ${employee_id}`) if(!employee) return { success: false, error: `Unknown user with id ${employee_id}`}
const full_name = ( employee.user.first_name + " " + employee.user.last_name ) || " "; const full_name = ( employee.user.first_name + " " + employee.user.last_name ) || " ";
return full_name; return {success: true, data: full_name };
} }
} }

View File

@ -1,14 +1,23 @@
import { Prisma, PrismaClient } from "@prisma/client"; import { Prisma, PrismaClient } from "@prisma/client";
import { NotFoundException } from "@nestjs/common";
import { PrismaService } from "src/prisma/prisma.service"; import { PrismaService } from "src/prisma/prisma.service";
import { ShiftKey } from "src/time-and-attendance/utils/type.utils"; import { Result } from "src/common/errors/result-error.factory";
type Tx = Prisma.TransactionClient | PrismaClient; type Tx = Prisma.TransactionClient | PrismaClient;
interface ShiftKey {
timesheet_id: number;
date: Date;
start_time: Date;
end_time: Date;
bank_code_id: number;
is_remote: boolean;
comment?: string | null;
}
export class ShiftIdResolver { export class ShiftIdResolver {
constructor(private readonly prisma: PrismaService) {} constructor(private readonly prisma: PrismaService) {}
readonly findShiftIdByData = async ( key: ShiftKey, client?: Tx ): Promise<{id:number}> => { readonly findShiftIdByData = async ( key: ShiftKey, client?: Tx ): Promise<Result<number, string>> => {
const db = client ?? this.prisma; const db = client ?? this.prisma;
const shift = await db.shifts.findFirst({ const shift = await db.shifts.findFirst({
where: { where: {
@ -23,7 +32,8 @@ export class ShiftIdResolver {
select: { id: true }, select: { id: true },
}); });
if(!shift) throw new NotFoundException(`shift not found`); if(!shift) return { success: false, error: `shift not found`}
return { id: shift.id };
return { success: true, data: shift.id };
}; };
} }

View File

@ -51,36 +51,6 @@ export const leaveRequestsSelect = {
}, },
} satisfies Prisma.LeaveRequestsSelect; } satisfies Prisma.LeaveRequestsSelect;
export const EXPENSE_SELECT = {
date: true,
amount: true,
mileage: true,
comment: true,
is_approved: true,
supervisor_comment: true,
bank_code: { select: { type: true } },
} as const;
export const EXPENSE_ASC_ORDER = { date: 'asc' as const };
export const PAY_PERIOD_SELECT = {
period_start: true,
period_end: true,
} as const;
export const SHIFT_SELECT = {
date: true,
start_time: true,
end_time: true,
comment: true,
is_approved: true,
is_remote: true,
bank_code: {select: { type: true } },
} as const;
export const SHIFT_ASC_ORDER = [{date: 'asc' as const}, {start_time: 'asc' as const}];
export const timesheet_select = { export const timesheet_select = {
id: true, id: true,
employee_id: true, employee_id: true,

View File

@ -1,47 +1,9 @@
import { Prisma, PrismaClient } from "@prisma/client";
import { GetExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-get.dto";
import { SchedulePresetsDto } from "src/time-and-attendance/time-tracker/schedule-presets/dtos/create-schedule-presets.dto";
import { GetShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-get.dto";
import { ShiftEntity } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-entity.dto";
import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils";
export type TotalHours = {
regular: number;
evening: number;
emergency: number;
overtime: number;
vacation: number;
holiday: number;
sick: number;
};
export type TotalExpenses = {
expenses: number;
per_diem: number;
on_call: number;
mileage: number;
};
export type Normalized = { export type Normalized = {
date: Date; date: Date;
start_time: Date; start_time: Date;
end_time: Date; end_time: Date;
bank_code_id: number; bank_code_id: number;
}; };
export type CreateShiftResult = { ok: true; data: GetShiftDto } | { ok: false; error: any };
export type UpdateShiftResult = { ok: true; id: number; data: GetShiftDto } | { ok: false; id: number; error: any };
export type NormedOk = {
index: number;
dto: ShiftEntity;
normed: Normalized;
timesheet_id: number;
};
export type DeletePresetResult = { ok: true; id: number; } | { ok: false; id: number; error: any };
export type CreatePresetResult = { ok: true; } | { ok: false; error: any };
export type UpdatePresetResult = { ok: true; id: number; data: SchedulePresetsDto } | { ok: false; id: number; error: any };
export type NormalizedExpense = { export type NormalizedExpense = {
date: Date; date: Date;
@ -51,9 +13,6 @@ export type NormalizedExpense = {
parsed_mileage?: number; parsed_mileage?: number;
parsed_attachment?: number; parsed_attachment?: number;
}; };
export type CreateExpenseResult = { ok: true; data: GetExpenseDto } | { ok: false; error: any };
export type DeleteExpenseResult = { ok: true; id: number; } | { ok: false; id: number; error: any };
export type ShiftResponse = { export type ShiftResponse = {
week_day: string; week_day: string;
@ -76,33 +35,3 @@ export type ApplyResult = {
created: number; created: number;
skipped: number; skipped: number;
} }
export type LeaveRequestRow = Prisma.LeaveRequestsGetPayload<{ select: typeof leaveRequestsSelect}>;
export type Tx = Prisma.TransactionClient | PrismaClient;
export type WeekOvertimeSummary = {
week_start:string;
week_end: string;
week_total_hours: number;
weekly_overtime: number;
daily_overtime_kept: number;
total_overtime: number;
breakdown: Array<{
date:string;
day_hours: number;
day_overtime: number;
daily_kept: number;
running_total_before: number;
}>;
};
export interface ShiftKey {
timesheet_id: number;
date: Date;
start_time: Date;
end_time: Date;
bank_code_id: number;
is_remote: boolean;
comment?: string | null;
}

View File

0
tsx
View File