diff --git a/src/app/api/timesheets/utils.ts b/src/app/api/timesheets/utils.ts index 81b543d..673a03d 100644 --- a/src/app/api/timesheets/utils.ts +++ b/src/app/api/timesheets/utils.ts @@ -13,10 +13,6 @@ export type TimeEntryError = { [field in keyof TimeEntry]?: string; }; -interface TimeEntryValidationOptions { - skipTaskValidation?: boolean; -} - /** * @param entry - the time entry * @returns an object where the keys are the error fields and the values the error message, and undefined if there are no errors @@ -24,7 +20,6 @@ interface TimeEntryValidationOptions { export const validateTimeEntry = ( entry: Partial, isHoliday: boolean, - options: TimeEntryValidationOptions = {}, ): TimeEntryError | undefined => { // Test for errors const error: TimeEntryError = {}; @@ -46,12 +41,10 @@ export const validateTimeEntry = ( // If there is a project id, there should also be taskGroupId, taskId, inputHours if (entry.projectId) { - if (!options.skipTaskValidation) { - if (!entry.taskGroupId) { - error.taskGroupId = "Required"; - } else if (!entry.taskId) { - error.taskId = "Required"; - } + if (!entry.taskGroupId) { + error.taskGroupId = "Required"; + } else if (!entry.taskId) { + error.taskId = "Required"; } } else { if (!entry.remark) { @@ -78,7 +71,6 @@ export const validateTimesheet = ( timesheet: RecordTimesheetInput, leaveRecords: RecordLeaveInput, companyHolidays: HolidaysResult[], - options: TimeEntryValidationOptions = {}, ): { [date: string]: string } | undefined => { const errors: { [date: string]: string } = {}; @@ -94,7 +86,7 @@ export const validateTimesheet = ( // Check each entry for (const entry of timeEntries) { - const entryErrors = validateTimeEntry(entry, holidays.has(date), options); + const entryErrors = validateTimeEntry(entry, holidays.has(date)); if (entryErrors) { errors[date] = "There are errors in the entries"; @@ -107,7 +99,52 @@ export const validateTimesheet = ( const leaveHours = leaves?.reduce((acc, entry) => acc + entry.inputHours, 0) || 0; - const totalNormalHours = timeEntries.reduce((acc, entry) => { + const totalInputHours = timeEntries.reduce((acc, entry) => { + return acc + (entry.inputHours || 0); + }, 0); + + const totalOtHours = timeEntries.reduce((acc, entry) => { + return acc + (entry.otHours || 0); + }, 0); + + if (totalInputHours + leaveHours > DAILY_NORMAL_MAX_HOURS) { + errors[date] = + "The daily normal hours (timesheet hours + leave hours) cannot be more than {{DAILY_NORMAL_MAX_HOURS}}. Please use other hours for exceeding hours or decrease the leave hours."; + } else if ( + totalInputHours + 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 validateLeaveRecord = ( + leaveRecords: RecordLeaveInput, + timesheet: RecordTimesheetInput, +): { [date: string]: string } | undefined => { + const errors: { [date: string]: string } = {}; + + Object.keys(leaveRecords).forEach((date) => { + const leaves = leaveRecords[date]; + + // Check each leave entry + for (const entry of leaves) { + const entryError = isValidLeaveEntry(entry); + if (entryError) { + errors[date] = "There are errors in the entries"; + } + } + + // Check total hours + const timeEntries = timesheet[date] || []; + + const leaveHours = leaves.reduce((acc, entry) => acc + entry.inputHours, 0); + + const totalInputHours = timeEntries.reduce((acc, entry) => { return acc + (entry.inputHours || 0); }, 0); @@ -115,11 +152,11 @@ export const validateTimesheet = ( return acc + (entry.otHours || 0); }, 0); - if (totalNormalHours > DAILY_NORMAL_MAX_HOURS) { + if (totalInputHours + leaveHours > 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."; + "The daily normal hours (timesheet hours + leave hours) cannot be more than {{DAILY_NORMAL_MAX_HOURS}}. Please use other hours for exceeding hours or decrease the leave hours."; } else if ( - totalNormalHours + totalOtHours + leaveHours > + totalInputHours + totalOtHours + leaveHours > TIMESHEET_DAILY_MAX_HOURS ) { errors[date] = diff --git a/src/components/DateHoursTable/DateHoursList.tsx b/src/components/DateHoursTable/DateHoursList.tsx index d54e930..35f451c 100644 --- a/src/components/DateHoursTable/DateHoursList.tsx +++ b/src/components/DateHoursTable/DateHoursList.tsx @@ -18,7 +18,6 @@ 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"; import { HolidaysResult } from "@/app/api/holidays"; @@ -101,8 +100,7 @@ function DateHoursList({ const dailyTotal = leaveHours + timesheetHours; const normalHoursExceeded = - timesheetNormalHours > DAILY_NORMAL_MAX_HOURS; - const leaveExceeded = leaveHours > LEAVE_DAILY_MAX_HOURS; + timesheetNormalHours + leaveHours > DAILY_NORMAL_MAX_HOURS; const dailyTotalExceeded = dailyTotal > TIMESHEET_DAILY_MAX_HOURS; return ( @@ -148,11 +146,12 @@ function DateHoursList({ component="div" width="100%" variant="caption" - paddingInlineEnd="40%" > {t( - "The daily normal hours cannot be more than {{DAILY_NORMAL_MAX_HOURS}}. Please use other hours for exceeding hours.", + "The daily normal hours (timesheet hours + leave hours) cannot be more than {{DAILY_NORMAL_MAX_HOURS}} (timesheet hours: {{timesheetNormalHours}}, leave hours: {{leaveHours}}). Please use other hours for exceeding hours or decrease the leave hours.", { + timesheetNormalHours, + leaveHours, DAILY_NORMAL_MAX_HOURS, }, )} @@ -165,7 +164,7 @@ function DateHoursList({ justifyContent: "space-between", flexWrap: "wrap", alignItems: "baseline", - color: leaveExceeded ? "error.main" : undefined, + color: normalHoursExceeded ? "error.main" : undefined, }} > @@ -174,15 +173,20 @@ function DateHoursList({ {manhourFormatter.format(leaveHours)} - {leaveExceeded && ( + {normalHoursExceeded && ( - {t("Leave hours cannot be more than {{hours}}", { - hours: LEAVE_DAILY_MAX_HOURS, - })} + {t( + "The daily normal hours (timesheet hours + leave hours) cannot be more than {{DAILY_NORMAL_MAX_HOURS}} (timesheet hours: {{timesheetNormalHours}}, leave hours: {{leaveHours}}). Please use other hours for exceeding hours or decrease the leave hours.", + { + timesheetNormalHours, + leaveHours, + DAILY_NORMAL_MAX_HOURS, + }, + )} )} diff --git a/src/components/DateHoursTable/DateHoursTable.tsx b/src/components/DateHoursTable/DateHoursTable.tsx index 907b105..f6d18e2 100644 --- a/src/components/DateHoursTable/DateHoursTable.tsx +++ b/src/components/DateHoursTable/DateHoursTable.tsx @@ -22,7 +22,6 @@ import React, { 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"; import { HolidaysResult } from "@/app/api/holidays"; @@ -121,8 +120,8 @@ function DayRow({ const dailyTotal = leaveHours + timesheetHours; - const normalHoursExceeded = timesheetNormalHours > DAILY_NORMAL_MAX_HOURS; - const leaveExceeded = leaveHours > LEAVE_DAILY_MAX_HOURS; + const normalHoursExceeded = + timesheetNormalHours + leaveHours > DAILY_NORMAL_MAX_HOURS; const dailyTotalExceeded = dailyTotal > TIMESHEET_DAILY_MAX_HOURS; return ( @@ -158,8 +157,10 @@ function DayRow({ {normalHoursExceeded && ( ({ {/* Leave total */} {manhourFormatter.format(leaveHours)} - {leaveExceeded && ( + {normalHoursExceeded && ( diff --git a/src/components/LeaveModal/LeaveModal.tsx b/src/components/LeaveModal/LeaveModal.tsx index 62c6aa3..b22f8fc 100644 --- a/src/components/LeaveModal/LeaveModal.tsx +++ b/src/components/LeaveModal/LeaveModal.tsx @@ -26,6 +26,12 @@ import FullscreenModal from "../FullscreenModal"; import MobileLeaveTable from "../LeaveTable/MobileLeaveTable"; import useIsMobile from "@/app/utils/useIsMobile"; import { HolidaysResult } from "@/app/api/holidays"; +import { + DAILY_NORMAL_MAX_HOURS, + TIMESHEET_DAILY_MAX_HOURS, + validateLeaveRecord, +} from "@/app/api/timesheets/utils"; +import ErrorAlert from "../ErrorAlert"; interface Props { isOpen: boolean; @@ -75,6 +81,15 @@ const LeaveModal: React.FC = ({ const onSubmit = useCallback>( async (data) => { + const errors = validateLeaveRecord(data, timesheetRecords); + if (errors) { + Object.keys(errors).forEach((date) => + formProps.setError(date, { + message: errors[date], + }), + ); + return; + } const savedRecords = await saveLeave(data, username); const today = dayjs(); @@ -91,7 +106,7 @@ const LeaveModal: React.FC = ({ formProps.reset(newFormValues); onClose(); }, - [formProps, onClose, username], + [formProps, onClose, timesheetRecords, username], ); const onCancel = useCallback(() => { @@ -108,6 +123,20 @@ const LeaveModal: React.FC = ({ [onCancel], ); + const errorComponent = ( + { + const error = formProps.formState.errors[date]?.message; + return error + ? `${date}: ${t(error, { + TIMESHEET_DAILY_MAX_HOURS, + DAILY_NORMAL_MAX_HOURS, + })}` + : undefined; + })} + /> + ); + const matches = useIsMobile(); return ( @@ -135,6 +164,7 @@ const LeaveModal: React.FC = ({ timesheetRecords={timesheetRecords} /> + {errorComponent}