diff --git a/src/app/api/timesheets/utils.ts b/src/app/api/timesheets/utils.ts index 22d31ce..7a6dad3 100644 --- a/src/app/api/timesheets/utils.ts +++ b/src/app/api/timesheets/utils.ts @@ -1,4 +1,13 @@ -import { LeaveEntry, TimeEntry } from "./actions"; +import { getPublicHolidaysForNYears } from "@/app/utils/holidayUtils"; +import { HolidaysResult } from "../holidays"; +import { + LeaveEntry, + RecordLeaveInput, + RecordTimesheetInput, + TimeEntry, +} from "./actions"; +import { convertDateArrayToString } from "@/app/utils/formatUtil"; +import compact from "lodash/compact"; export type TimeEntryError = { [field in keyof TimeEntry]?: string; @@ -6,7 +15,7 @@ export type TimeEntryError = { /** * @param entry - the time entry - * @returns the field where there is an error, or an empty string if there is none + * @returns an object where the keys are the error fields and the values the error message, and undefined if there are no errors */ export const validateTimeEntry = ( entry: Partial, @@ -58,6 +67,61 @@ export const isValidLeaveEntry = (entry: Partial): string => { return error; }; +export const validateTimesheet = ( + timesheet: RecordTimesheetInput, + leaveRecords: RecordLeaveInput, + companyHolidays: HolidaysResult[], +): { [date: string]: string } | undefined => { + const errors: { [date: string]: string } = {}; + + const holidays = new Set( + compact([ + ...getPublicHolidaysForNYears(2).map((h) => h.date), + ...companyHolidays.map((h) => convertDateArrayToString(h.date)), + ]), + ); + + Object.keys(timesheet).forEach((date) => { + const timeEntries = timesheet[date]; + + // Check each entry + for (const entry of timeEntries) { + const entryErrors = validateTimeEntry(entry, holidays.has(date)); + + if (entryErrors) { + errors[date] = "There are errors in the entries"; + return; + } + } + + // Check total hours + const leaves = leaveRecords[date]; + const leaveHours = + leaves?.reduce((acc, entry) => acc + entry.inputHours, 0) || 0; + + const totalNormalHours = timeEntries.reduce((acc, entry) => { + return acc + (entry.inputHours || 0); + }, 0); + + const totalOtHours = timeEntries.reduce((acc, entry) => { + return acc + (entry.otHours || 0); + }, 0); + + if (totalNormalHours > DAILY_NORMAL_MAX_HOURS) { + errors[date] = + "The daily normal hours cannot be more than {{DAILY_NORMAL_MAX_HOURS}}. Please use other hours for exceeding hours."; + } else if ( + totalNormalHours + totalOtHours + leaveHours > + TIMESHEET_DAILY_MAX_HOURS + ) { + errors[date] = + "The daily total hours cannot be more than {{TIMESHEET_DAILY_MAX_HOURS}}"; + } + }); + + return Object.keys(errors).length > 0 ? errors : undefined; +}; + export const DAILY_NORMAL_MAX_HOURS = 8; export const LEAVE_DAILY_MAX_HOURS = 8; export const TIMESHEET_DAILY_MAX_HOURS = 20; diff --git a/src/components/DateHoursTable/DateHoursList.tsx b/src/components/DateHoursTable/DateHoursList.tsx index 685aef9..d54e930 100644 --- a/src/components/DateHoursTable/DateHoursList.tsx +++ b/src/components/DateHoursTable/DateHoursList.tsx @@ -17,6 +17,7 @@ import dayjs from "dayjs"; import React, { useCallback, useState } from "react"; import { useTranslation } from "react-i18next"; import { + DAILY_NORMAL_MAX_HOURS, LEAVE_DAILY_MAX_HOURS, TIMESHEET_DAILY_MAX_HOURS, } from "@/app/api/timesheets/utils"; @@ -32,6 +33,7 @@ interface Props { EntryComponentProps & { date: string } >; entryComponentProps: EntryComponentProps; + errorComponent?: React.ReactNode; } function DateHoursList({ @@ -41,6 +43,7 @@ function DateHoursList({ EntryComponent, entryComponentProps, companyHolidays, + errorComponent, }: Props) { const { t, @@ -83,15 +86,22 @@ function DateHoursList({ leaves?.reduce((acc, entry) => acc + entry.inputHours, 0) || 0; const timesheet = timesheetEntries[day]; - const timesheetHours = + const timesheetNormalHours = timesheet?.reduce( - (acc, entry) => - acc + (entry.inputHours || 0) + (entry.otHours || 0), + (acc, entry) => acc + (entry.inputHours || 0), 0, ) || 0; + const timesheetOtHours = + timesheet?.reduce( + (acc, entry) => acc + (entry.otHours || 0), + 0, + ) || 0; + const timesheetHours = timesheetNormalHours + timesheetOtHours; const dailyTotal = leaveHours + timesheetHours; + const normalHoursExceeded = + timesheetNormalHours > DAILY_NORMAL_MAX_HOURS; const leaveExceeded = leaveHours > LEAVE_DAILY_MAX_HOURS; const dailyTotalExceeded = dailyTotal > TIMESHEET_DAILY_MAX_HOURS; @@ -122,7 +132,9 @@ function DateHoursList({ sx={{ display: "flex", justifyContent: "space-between", + flexWrap: "wrap", alignItems: "baseline", + color: normalHoursExceeded ? "error.main" : undefined, }} > @@ -131,6 +143,21 @@ function DateHoursList({ {manhourFormatter.format(timesheetHours)} + {normalHoursExceeded && ( + + {t( + "The daily normal hours cannot be more than {{DAILY_NORMAL_MAX_HOURS}}. Please use other hours for exceeding hours.", + { + DAILY_NORMAL_MAX_HOURS, + }, + )} + + )} ({ variant="caption" > {t( - "The daily total hours cannot be more than {{hours}}", + "The daily total hours cannot be more than {{TIMESHEET_DAILY_MAX_HOURS}}", { - hours: TIMESHEET_DAILY_MAX_HOURS, + TIMESHEET_DAILY_MAX_HOURS, }, )} @@ -198,6 +225,7 @@ function DateHoursList({ })} )} + {errorComponent} {isDateSelected ? (