diff --git a/docs/swagger/swagger-spec.json b/docs/swagger/swagger-spec.json index 9d03aa2..d3afcc6 100644 --- a/docs/swagger/swagger-spec.json +++ b/docs/swagger/swagger-spec.json @@ -464,24 +464,36 @@ ] }, "get": { - "operationId": "TimesheetsController_findAll", - "parameters": [], - "responses": { - "201": { - "description": "List of timesheet found", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CreateTimesheetDto" - } - } - } + "operationId": "TimesheetsController_getPeriodByQuery", + "parameters": [ + { + "name": "year", + "required": true, + "in": "query", + "schema": { + "type": "number" } }, - "400": { - "description": "List of timesheets not found" + { + "name": "period_no", + "required": true, + "in": "query", + "schema": { + "type": "number" + } + }, + { + "name": "email", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "" } }, "security": [ @@ -489,7 +501,6 @@ "access-token": [] } ], - "summary": "Find all timesheets", "tags": [ "Timesheets" ] @@ -618,7 +629,7 @@ ] } }, - "/timesheets/{id}/approval": { + "/timesheets/approval/{id}": { "patch": { "operationId": "TimesheetsController_approve", "parameters": [ @@ -1206,7 +1217,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/CreateLeaveRequestsDto" + "$ref": "#/components/schemas/LeaveRequestViewDto" } } } @@ -1246,7 +1257,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateLeaveRequestsDto" + "$ref": "#/components/schemas/LeaveRequestViewDto" } } } @@ -1293,7 +1304,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateLeaveRequestsDto" + "$ref": "#/components/schemas/LeaveRequestViewDto" } } } @@ -1525,29 +1536,6 @@ ] } }, - "/exports/csv": { - "get": { - "operationId": "CsvExportController_exportCsv", - "parameters": [ - { - "name": "period", - "required": true, - "in": "query", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "" - } - }, - "tags": [ - "CsvExport" - ] - } - }, "/customers": { "post": { "operationId": "CustomersController_create", @@ -2293,7 +2281,7 @@ "description": "Employee`s email" }, "phone_number": { - "type": "number", + "type": "string", "example": "82538437464", "description": "Employee`s phone number" }, @@ -2378,7 +2366,7 @@ "description": "Employee`s email" }, "phone_number": { - "type": "number", + "type": "string", "example": "82538437464", "description": "Employee`s phone number" }, @@ -2646,16 +2634,6 @@ "CreateLeaveRequestsDto": { "type": "object", "properties": { - "id": { - "type": "number", - "example": 1, - "description": "Leave request`s unique id(auto-incremented)" - }, - "employee_id": { - "type": "number", - "example": "4655867", - "description": "Employee`s id" - }, "bank_code_id": { "type": "number", "example": 7, @@ -2667,13 +2645,11 @@ "description": "type of leave request for an accounting perception" }, "start_date_time": { - "format": "date-time", "type": "string", "example": "22/06/2463", "description": "Leave request`s start date" }, "end_date_time": { - "format": "date-time", "type": "string", "example": "25/03/3019", "description": "Leave request`s end date" @@ -2690,8 +2666,6 @@ } }, "required": [ - "id", - "employee_id", "bank_code_id", "leave_type", "start_date_time", @@ -2700,19 +2674,13 @@ "approval_status" ] }, + "LeaveRequestViewDto": { + "type": "object", + "properties": {} + }, "UpdateLeaveRequestsDto": { "type": "object", "properties": { - "id": { - "type": "number", - "example": 1, - "description": "Leave request`s unique id(auto-incremented)" - }, - "employee_id": { - "type": "number", - "example": "4655867", - "description": "Employee`s id" - }, "bank_code_id": { "type": "number", "example": 7, @@ -2724,13 +2692,11 @@ "description": "type of leave request for an accounting perception" }, "start_date_time": { - "format": "date-time", "type": "string", "example": "22/06/2463", "description": "Leave request`s start date" }, "end_date_time": { - "format": "date-time", "type": "string", "example": "25/03/3019", "description": "Leave request`s end date" @@ -2845,7 +2811,7 @@ "description": "Customer`s email" }, "phone_number": { - "type": "number", + "type": "string", "example": "8436637464", "description": "Customer`s phone number" }, @@ -2898,7 +2864,7 @@ "description": "Customer`s email" }, "phone_number": { - "type": "number", + "type": "string", "example": "8436637464", "description": "Customer`s phone number" }, diff --git a/prisma/mock-seeds-scripts/02-users.ts b/prisma/mock-seeds-scripts/02-users.ts index 5078b3e..442678e 100644 --- a/prisma/mock-seeds-scripts/02-users.ts +++ b/prisma/mock-seeds-scripts/02-users.ts @@ -1,7 +1,7 @@ import { PrismaClient, Roles } from '@prisma/client'; const prisma = new PrismaClient(); -const BASE_PHONE = 1_100_000_000; // < 2_147_483_647 +const BASE_PHONE = '1_100_000_000'; // < 2_147_483_647 function emailFor(i: number) { return `user${i + 1}@example.test`; @@ -16,7 +16,7 @@ async function main() { first_name: string; last_name: string; email: string; - phone_number: number; + phone_number: string; residence?: string | null; role: Roles; }[] = []; diff --git a/src/app.module.ts b/src/app.module.ts index c1a20e8..7a4aadf 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -5,7 +5,7 @@ import { ArchivalModule } from './modules/archival/archival.module'; import { AuthenticationModule } from './modules/authentication/auth.module'; import { BankCodesModule } from './modules/bank-codes/bank-codes.module'; import { BusinessLogicsModule } from './modules/business-logics/business-logics.module'; -import { CsvExportModule } from './modules/exports/csv-exports.module'; +// import { CsvExportModule } from './modules/exports/csv-exports.module'; import { CustomersModule } from './modules/customers/customers.module'; import { EmployeesModule } from './modules/employees/employees.module'; import { ExpensesModule } from './modules/expenses/expenses.module'; @@ -30,7 +30,7 @@ import { ConfigModule } from '@nestjs/config'; BankCodesModule, BusinessLogicsModule, ConfigModule.forRoot({isGlobal: true}), - CsvExportModule, + // CsvExportModule, CustomersModule, EmployeesModule, ExpensesModule, diff --git a/src/modules/customers/dtos/create-customer.dto.ts b/src/modules/customers/dtos/create-customer.dto.ts index bc35918..398da0e 100644 --- a/src/modules/customers/dtos/create-customer.dto.ts +++ b/src/modules/customers/dtos/create-customer.dto.ts @@ -55,10 +55,10 @@ export class CreateCustomerDto { example: '8436637464', description: 'Customer`s phone number', }) - @Type(() => Number) + @IsString() @IsInt() @IsPositive() - phone_number: number; + phone_number: string; @ApiProperty({ example: '1 Ringbearer`s way, Mount Doom city, ME, T1R 1N6 ', diff --git a/src/modules/employees/dtos/create-employee.dto.ts b/src/modules/employees/dtos/create-employee.dto.ts index bfdc973..89279ef 100644 --- a/src/modules/employees/dtos/create-employee.dto.ts +++ b/src/modules/employees/dtos/create-employee.dto.ts @@ -62,10 +62,8 @@ export class CreateEmployeeDto { example: '82538437464', description: 'Employee`s phone number', }) - @Type(() => Number) - @IsInt() - @IsPositive() - phone_number: number; + @IsString() + phone_number: string; @ApiProperty({ example: '1 Bagshot Row, Hobbiton, The Shire, Middle-earth', diff --git a/src/modules/employees/dtos/profil-employee.dto.ts b/src/modules/employees/dtos/profil-employee.dto.ts index d790558..c6836cf 100644 --- a/src/modules/employees/dtos/profil-employee.dto.ts +++ b/src/modules/employees/dtos/profil-employee.dto.ts @@ -6,7 +6,7 @@ export class EmployeeProfileItemDto { company_name: number | null; job_title: string | null; email: string | null; - phone_number: number; + phone_number: string; first_work_day: string; last_work_day?: string | null; residence: string | null; diff --git a/src/modules/employees/dtos/update-employee.dto.ts b/src/modules/employees/dtos/update-employee.dto.ts index 517c48f..334a01a 100644 --- a/src/modules/employees/dtos/update-employee.dto.ts +++ b/src/modules/employees/dtos/update-employee.dto.ts @@ -17,6 +17,6 @@ export class UpdateEmployeeDto extends PartialType(CreateEmployeeDto) { @IsOptional() supervisor_id?: number; - @Max(2147483647) - phone_number: number; + @IsOptional() + phone_number: string; } diff --git a/src/modules/exports/controllers/csv-exports.controller.ts b/src/modules/exports/controllers/csv-exports.controller.ts index 7346bcb..c3363f9 100644 --- a/src/modules/exports/controllers/csv-exports.controller.ts +++ b/src/modules/exports/controllers/csv-exports.controller.ts @@ -2,7 +2,7 @@ import { Controller, Get, Header, Query, UseGuards } from "@nestjs/common"; import { RolesGuard } from "src/common/guards/roles.guard"; import { Roles as RoleEnum } from '.prisma/client'; import { CsvExportService } from "../services/csv-exports.service"; -import { ExportCompany, ExportCsvOptionsDto, ExportType } from "../dtos/export-csv-options.dto"; +// import { ExportCompany, ExportCsvOptionsDto, ExportType } from "../dtos/export-csv-options.dto"; import { RolesAllowed } from "src/common/decorators/roles.decorators"; @@ -11,34 +11,34 @@ import { RolesAllowed } from "src/common/decorators/roles.decorators"; export class CsvExportController { constructor(private readonly csvService: CsvExportService) {} - @Get('csv/:year/:period_no') - @Header('Content-Type', 'text/csv; charset=utf-8') - @Header('Content-Disposition', 'attachment; filename="export.csv"') - //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.ACCOUNTING, RoleEnum.HR) - async exportCsv(@Query() options: ExportCsvOptionsDto, - @Query('period') periodId: string ): Promise { - //modify to accept year and period_number - //sets default values - const companies = options.companies && options.companies.length ? options.companies : - [ ExportCompany.TARGO, ExportCompany.SOLUCOM]; - const types = options.type && options.type.length ? options.type : - Object.values(ExportType); + // @Get('csv/:year/:period_no') + // @Header('Content-Type', 'text/csv; charset=utf-8') + // @Header('Content-Disposition', 'attachment; filename="export.csv"') + // //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.ACCOUNTING, RoleEnum.HR) + // async exportCsv(@Query() options: ExportCsvOptionsDto, + // @Query('period') periodId: string ): Promise { + // //modify to accept year and period_number + // //sets default values + // const companies = options.companies && options.companies.length ? options.companies : + // [ ExportCompany.TARGO, ExportCompany.SOLUCOM]; + // const types = options.type && options.type.length ? options.type : + // Object.values(ExportType); - //collects all - const all = await this.csvService.collectTransaction(Number(periodId), companies); + // //collects all + // const all = await this.csvService.collectTransaction(Number(periodId), companies); - //filters by type - const filtered = all.filter(row => { - switch (row.bank_code.toLocaleLowerCase()) { - case 'holiday' : return types.includes(ExportType.HOLIDAY); - case 'vacation' : return types.includes(ExportType.VACATION); - case 'expenses' : return types.includes(ExportType.EXPENSES); - default : return types.includes(ExportType.SHIFTS); - } - }); + // //filters by type + // const filtered = all.filter(row => { + // switch (row.bank_code.toLocaleLowerCase()) { + // case 'holiday' : return types.includes(ExportType.HOLIDAY); + // case 'vacation' : return types.includes(ExportType.VACATION); + // case 'expenses' : return types.includes(ExportType.EXPENSES); + // default : return types.includes(ExportType.SHIFTS); + // } + // }); - //generating the csv file - return this.csvService.generateCsv(filtered); - } + // //generating the csv file + // return this.csvService.generateCsv(filtered); + // } } \ No newline at end of file diff --git a/src/modules/exports/services/csv-exports.service.ts b/src/modules/exports/services/csv-exports.service.ts index 00157db..d8bd73e 100644 --- a/src/modules/exports/services/csv-exports.service.ts +++ b/src/modules/exports/services/csv-exports.service.ts @@ -31,38 +31,38 @@ type Filters = { export class CsvExportService { constructor(private readonly prisma: PrismaService) {} - async collectTransaction( - year: number, - period_no: number, - filters: Filters, - approved: boolean = true - ): Promise { + // async collectTransaction( + // year: number, + // period_no: number, + // filters: Filters, + // approved: boolean = true + // ): Promise { //fetch period - const period = await this.prisma.payPeriods.findFirst({ - where: { pay_year: year, pay_period_no: period_no }, - select: { period_start: true, period_end: true }, - }); - if(!period) throw new NotFoundException(`Pay period ${ year }-${ period_no } not found`); + // const period = await this.prisma.payPeriods.findFirst({ + // where: { pay_year: year, pay_period_no: period_no }, + // select: { period_start: true, period_end: true }, + // }); + // if(!period) throw new NotFoundException(`Pay period ${ year }-${ period_no } not found`); - const start = period.period_start; - const end = period.period_end; + // const start = period.period_start; + // const end = period.period_end; - //fetch company codes from .env - const comapany_codes = this.resolveCompanyCodes(filters.companies); - if(comapany_codes.length === 0) throw new BadRequestException('No company selected'); + // //fetch company codes from .env + // const comapany_codes = this.resolveCompanyCodes(filters.companies); + // if(comapany_codes.length === 0) throw new BadRequestException('No company selected'); - //Flag types - const { shifts: want_shifts, expenses: want_expense, holiday: want_holiday, vacation: want_vacation } = filters.types; - if(!want_shifts && !want_expense && !want_holiday && !want_vacation) { - throw new BadRequestException(' No export type selected '); - } + // //Flag types + // const { shifts: want_shifts, expenses: want_expense, holiday: want_holiday, vacation: want_vacation } = filters.types; + // if(!want_shifts && !want_expense && !want_holiday && !want_vacation) { + // throw new BadRequestException(' No export type selected '); + // } - const approved_filter = filters.approved? { is_approved: true } : {}; + // const approved_filter = filters.approved? { is_approved: true } : {}; - //Prisma queries - const [shifts, expenses] = await Promise.all([ - want_shifts || want_expense || want_holiday || want_vacation - ]) + // //Prisma queries + // const [shifts, expenses] = await Promise.all([ + // want_shifts || want_expense || want_holiday || want_vacation + // ]) @@ -198,16 +198,16 @@ export class CsvExportService { // } //Final Mapping and sorts - return rows.sort((a,b) => { - if(a.external_payroll_id !== b.external_payroll_id) { - return a.external_payroll_id - b.external_payroll_id; - } - if(a.bank_code !== b.bank_code) { - return a.bank_code.localeCompare(b.bank_code); - } - return a.week_number - b.week_number; - }); - } + // return rows.sort((a,b) => { + // if(a.external_payroll_id !== b.external_payroll_id) { + // return a.external_payroll_id - b.external_payroll_id; + // } + // if(a.bank_code !== b.bank_code) { + // return a.bank_code.localeCompare(b.bank_code); + // } + // return a.week_number - b.week_number; + // }); + // } resolveCompanyCodes(companies: { targo: boolean; solucom: boolean; }) { throw new Error("Method not implemented."); } diff --git a/src/modules/timesheets/controllers/timesheets.controller.ts b/src/modules/timesheets/controllers/timesheets.controller.ts index a5519ef..7280c3c 100644 --- a/src/modules/timesheets/controllers/timesheets.controller.ts +++ b/src/modules/timesheets/controllers/timesheets.controller.ts @@ -36,8 +36,8 @@ export class TimesheetsController { @Query('period_no', ParseIntPipe ) period_no: number, @Query('email') email?: string ): Promise { - if(!email || !email.trim()) throw new BadRequestException('Query param "email" is mandatory for this route.'); - return this.timesheetsQuery.findAll(year, period_no, email.trim()); + if(!email || !(email = email.trim())) throw new BadRequestException('Query param "email" is mandatory for this route.'); + return this.timesheetsQuery.findAll(year, period_no, email); } @Get(':id') @@ -70,7 +70,7 @@ export class TimesheetsController { return this.timesheetsQuery.remove(id); } - @Patch(':id/approval') + @Patch('approval/:id') //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR) async approve(@Param('id', ParseIntPipe) id: number, @Body('is_approved', ParseBoolPipe) isApproved: boolean) { return this.timesheetsCommand.updateApproval(id, isApproved); diff --git a/src/modules/timesheets/dtos/timesheet-period.dto.ts b/src/modules/timesheets/dtos/timesheet-period.dto.ts index c15b3a5..e383b08 100644 --- a/src/modules/timesheets/dtos/timesheet-period.dto.ts +++ b/src/modules/timesheets/dtos/timesheet-period.dto.ts @@ -27,13 +27,13 @@ export class DayExpensesDto { export class WeekDto { is_approved: boolean; shifts: { - sun: DayShiftsDto; - mon: DayShiftsDto; - tue: DayShiftsDto; - wed: DayShiftsDto; - thu: DayShiftsDto; - fri: DayShiftsDto; - sat: DayShiftsDto; + sun: DetailedShifts; + mon: DetailedShifts; + tue: DetailedShifts; + wed: DetailedShifts; + thu: DetailedShifts; + fri: DetailedShifts; + sat: DetailedShifts; } expenses: { sun: DayExpensesDto; diff --git a/src/modules/timesheets/services/timesheets-query.service.ts b/src/modules/timesheets/services/timesheets-query.service.ts index 0fe08ee..cb6f67f 100644 --- a/src/modules/timesheets/services/timesheets-query.service.ts +++ b/src/modules/timesheets/services/timesheets-query.service.ts @@ -5,14 +5,10 @@ import { Timesheets, TimesheetsArchive } from '@prisma/client'; import { UpdateTimesheetDto } from '../dtos/update-timesheet.dto'; import { OvertimeService } from 'src/modules/business-logics/services/overtime.service'; import { computeHours } from 'src/common/utils/date-utils'; -import { buildPrismaWhere } from 'src/common/shared/build-prisma-where'; -import { SearchTimesheetDto } from '../dtos/search-timesheet.dto'; import { TimesheetPeriodDto } from '../dtos/timesheet-period.dto'; import { buildPeriod, endOfDayUTC, toUTCDateOnly } from '../utils/timesheet.helpers'; +import type { ShiftRow, ExpenseRow } from '../utils/timesheet.helpers'; -// deprecated (used with old findAll) const ROUND_TO = 5; -type ShiftRow = { date: Date; start_time: Date; end_time: Date; is_approved?: boolean }; -type ExpenseRow = { date: Date; amount: number; type: string; is_approved?: boolean }; @Injectable() export class TimesheetsQueryService { @@ -21,8 +17,6 @@ export class TimesheetsQueryService { private readonly overtime: OvertimeService, ) {} - - async create(dto : CreateTimesheetDto): Promise { const { employee_id, is_approved } = dto; return this.prisma.timesheets.create({ @@ -74,7 +68,7 @@ export class TimesheetsQueryService { }), ]); - //Shift data mapping + // data mapping const shifts: ShiftRow[] = raw_shifts.map(shift => ({ date: shift.date, start_time: shift.start_time, diff --git a/src/modules/timesheets/utils/timesheet.helpers.ts b/src/modules/timesheets/utils/timesheet.helpers.ts index 6ad14af..7e32901 100644 --- a/src/modules/timesheets/utils/timesheet.helpers.ts +++ b/src/modules/timesheets/utils/timesheet.helpers.ts @@ -1,12 +1,12 @@ -import { DayExpensesDto, DayShiftsDto, ShiftDto, TimesheetPeriodDto, WeekDto } from "../dtos/timesheet-period.dto"; +import { DayExpensesDto, DayShiftsDto, DetailedShifts, ShiftDto, TimesheetPeriodDto, WeekDto } from "../dtos/timesheet-period.dto"; //makes the strings indexes for arrays export const DAY_KEYS = ['sun','mon','tue','wed','thu','fri','sat'] as const; -export type DayKey = 'sun'|'mon'|'tue'|'wed'|'thu'|'fri'|'sat'; +export type DayKey = typeof DAY_KEYS[number]; //DB line types -type ShiftRow = { date: Date; start_time: Date; end_time: Date; is_approved?: boolean }; -type ExpenseRow = { date: Date; amount: number; type: string; is_approved?: boolean }; +export type ShiftRow = { date: Date; start_time: Date; end_time: Date; is_approved?: boolean }; +export type ExpenseRow = { date: Date; amount: number; type: string; is_approved?: boolean }; export function dayKeyFromDate(date: Date, useUTC = true): DayKey { const index = useUTC ? date.getUTCDay() : date.getDay(); // 0=Sunday..6=Saturday @@ -14,6 +14,7 @@ export function dayKeyFromDate(date: Date, useUTC = true): DayKey { } const MS_PER_DAY = 86_400_000; +const MS_PER_HOUR = 3_600_000; export function toUTCDateOnly(date: Date | string): Date { const d = new Date(date); @@ -34,12 +35,6 @@ export function isBetweenUTC(date: Date, start: Date, end_inclusive: Date): bool return time >= start.getTime() && time <= end_inclusive.getTime(); } -export function dayIndex(week_start: Date, date: Date): 1|2|3|4|5|6|7 { - const diff = Math.floor((toUTCDateOnly(date).getTime() - toUTCDateOnly(week_start).getTime())/ MS_PER_DAY); - const index = Math.min(6, Math.max(0, diff)) + 1; - return index as 1|2|3|4|5|6|7; -} - export function toTimeString(date: Date): string { const hours = String(date.getUTCHours()).padStart(2,'0'); const minutes = String(date.getUTCMinutes()).padStart(2,'0'); @@ -50,21 +45,33 @@ export function round2(num: number) { return Math.round(num * 100) / 100; } -export function makeEmptyDayShifts(): DayShiftsDto { return []; } +function shortDate(date:Date): string { + const mm = String(date.getUTCMonth()+1).padStart(2,'0'); + const dd = String(date.getUTCDate()).padStart(2,'0'); + return `${mm}/${dd}`; +} + +// export function makeEmptyDayShifts(): DayShiftsDto { return []; } export function makeEmptyDayExpenses(): DayExpensesDto { return {cash: [], km: []}; } -export function makeEmptyWeek(): WeekDto { +export function makeEmptyWeek(week_start: Date): WeekDto { + const make_empty_shifts = (offset: number): DetailedShifts => ({ + shifts: [], + total_hours: 0, + short_date: shortDate(addDays(week_start, offset)), + break_durations: undefined, + }); return { is_approved: true, shifts: { - sun: makeEmptyDayShifts(), - mon: makeEmptyDayShifts(), - tue: makeEmptyDayShifts(), - wed: makeEmptyDayShifts(), - thu: makeEmptyDayShifts(), - fri: makeEmptyDayShifts(), - sat: makeEmptyDayShifts(), + sun: make_empty_shifts(0), + mon: make_empty_shifts(1), + tue: make_empty_shifts(2), + wed: make_empty_shifts(3), + thu: make_empty_shifts(4), + fri: make_empty_shifts(5), + sat: make_empty_shifts(6), }, expenses: { sun: makeEmptyDayExpenses(), @@ -79,7 +86,7 @@ export function makeEmptyWeek(): WeekDto { } export function makeEmptyPeriod(): TimesheetPeriodDto { - return { week1: makeEmptyWeek(), week2: makeEmptyWeek() }; + return { week1: makeEmptyWeek(new Date()), week2: makeEmptyWeek(new Date()) }; } //needs ajusting according to DB's data for expenses types @@ -89,16 +96,27 @@ export function normalizeExpenseBucket(db_type: string): 'km' | 'cash' { return 'cash'; } -export function buildWeek( week_start: Date, week_end: Date, shifts: ShiftRow[], expenses: ExpenseRow[]): WeekDto { - const week = makeEmptyWeek(); +export function buildWeek( + week_start: Date, + week_end: Date, + shifts: ShiftRow[], + expenses: ExpenseRow[], + ): WeekDto { + const week = makeEmptyWeek(week_start); let all_approved = true; + //array of shifts per day ( to check for break_gaps and calculate daily total hours ) + const dayTimes: Record> = { + sun: [], mon: [], tue: [], wed: [],thu: [], fri: [], sat: [], + }; + + //Shifts mapped and filtered by dates const week_shifts = shifts.filter(shift => isBetweenUTC(shift.date, week_start, week_end)); for (const shift of week_shifts) { const key = dayKeyFromDate(shift.date, true); - week.shifts[key].push({ - shifts: [], + dayTimes[key].push({start: shift.start_time, end:shift.end_time }); + week.shifts[key].shifts.push({ start: toTimeString(shift.start_time), end : toTimeString(shift.end_time), is_approved: shift.is_approved ?? true, @@ -118,11 +136,38 @@ export function buildWeek( week_start: Date, week_end: Date, shifts: ShiftRow[], }); all_approved = all_approved && (expense.is_approved ?? true); } + + for (const key of DAY_KEYS) { + //sorts shifts in chronological order + const times = dayTimes[key].sort((a,b) => a.start.getTime() - b.start.getTime()); + + //daily total hours + const total = times.reduce((sum, time) => { + const duration = (time.end.getTime() - time.start.getTime()) / MS_PER_HOUR; + return sum + Math.max(0, duration); + }, 0); + week.shifts[key].total_hours = round2(total); + + //break_duration + if (times.length >= 2) { + let break_gaps = 0; + for (let i = 1; i < times.length; i++) { + const gap = (times[i].start.getTime() - times[i-1].end.getTime()) / MS_PER_HOUR; + if(gap > 0) break_gaps += gap; + } + if(break_gaps > 0) week.shifts[key].break_durations = round2(break_gaps); + } + } week.is_approved = all_approved; return week; } -export function buildPeriod( period_start: Date, period_end: Date, shifts: ShiftRow[], expenses: ExpenseRow[]): TimesheetPeriodDto { +export function buildPeriod( + period_start: Date, + period_end: Date, + shifts: ShiftRow[], + expenses: ExpenseRow[] +): TimesheetPeriodDto { const week1_start = toUTCDateOnly(period_start); const week1_end = endOfDayUTC(addDays(week1_start, 6)); const week2_start = toUTCDateOnly(addDays(week1_start, 7));