From 91e7e1d854c66511d85fc48e66596e1f680ff34d Mon Sep 17 00:00:00 2001 From: Wayne Date: Thu, 30 May 2024 00:09:57 +0900 Subject: [PATCH] Add delete on time amendment and more validation --- src/app/api/timesheets/actions.ts | 28 +++++ src/app/api/timesheets/utils.ts | 112 ++++++++++-------- src/components/LeaveModal/LeaveModal.tsx | 8 +- src/components/LeaveTable/LeaveEditModal.tsx | 42 +++++-- src/components/LeaveTable/LeaveEntryTable.tsx | 56 ++++++--- .../LeaveTable/MobileLeaveEntry.tsx | 5 +- .../TimesheetAmendment/TimesheetAmendment.tsx | 85 ++++++++++++- .../TimesheetTable/EntryInputTable.tsx | 10 +- .../TimesheetTable/FastTimeEntryModal.tsx | 2 +- .../TimesheetTable/MobileTimesheetEntry.tsx | 4 +- .../TimesheetTable/TimesheetEditModal.tsx | 22 +++- 11 files changed, 278 insertions(+), 96 deletions(-) diff --git a/src/app/api/timesheets/actions.ts b/src/app/api/timesheets/actions.ts index 5d4ecb0..d362264 100644 --- a/src/app/api/timesheets/actions.ts +++ b/src/app/api/timesheets/actions.ts @@ -94,6 +94,34 @@ export const saveMemberLeave = async (data: { ); }; +export const deleteMemberEntry = async (data: { + staffId: number; + entryId: number; +}) => { + return serverFetchJson( + `${BASE_API_URL}/timesheets/deleteMemberEntry`, + { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }, + ); +}; + +export const deleteMemberLeave = async (data: { + staffId: number; + entryId: number; +}) => { + return serverFetchJson( + `${BASE_API_URL}/timesheets/deleteMemberLeave`, + { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }, + ); +}; + export const revalidateCacheAfterAmendment = () => { revalidatePath("/(main)/home"); }; diff --git a/src/app/api/timesheets/utils.ts b/src/app/api/timesheets/utils.ts index 673a03d..bbb13c2 100644 --- a/src/app/api/timesheets/utils.ts +++ b/src/app/api/timesheets/utils.ts @@ -28,7 +28,7 @@ export const validateTimeEntry = ( if (!entry.inputHours && !entry.otHours) { error[isHoliday ? "otHours" : "inputHours"] = "Required"; } else if (entry.inputHours && isHoliday) { - error.inputHours = "Cannot input normal hours for holidays"; + error.inputHours = "Cannot input normal hours on holidays"; } else if (entry.inputHours && entry.inputHours <= 0) { error.inputHours = "Input hours should be between 0 and {{DAILY_NORMAL_MAX_HOURS}}"; @@ -55,16 +55,31 @@ export const validateTimeEntry = ( return Object.keys(error).length > 0 ? error : undefined; }; -export const isValidLeaveEntry = (entry: Partial): string => { +export type LeaveEntryError = { + [field in keyof LeaveEntry]?: string; +}; + +export const validateLeaveEntry = ( + entry: Partial, + isHoliday: boolean, +): LeaveEntryError | undefined => { // Test for errrors - let error: keyof LeaveEntry | "" = ""; + const error: LeaveEntryError = {}; if (!entry.leaveTypeId) { - error = "leaveTypeId"; - } else if (!entry.inputHours || !(entry.inputHours >= 0)) { - error = "inputHours"; + error.leaveTypeId = "Required"; + } else if (entry.inputHours && isHoliday) { + error.inputHours = "Cannot input normal hours on holidays"; + } else if (!entry.inputHours) { + error.inputHours = "Required"; + } else if ( + entry.inputHours && + (entry.inputHours <= 0 || entry.inputHours > DAILY_NORMAL_MAX_HOURS) + ) { + error.inputHours = + "Input hours should be between 0 and {{DAILY_NORMAL_MAX_HOURS}}"; } - return error; + return Object.keys(error).length > 0 ? error : undefined; }; export const validateTimesheet = ( @@ -95,27 +110,10 @@ export const validateTimesheet = ( } // Check total hours - const leaves = leaveRecords[date]; - const leaveHours = - leaves?.reduce((acc, entry) => acc + entry.inputHours, 0) || 0; - - 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}}"; + const leaves = leaveRecords[date] || []; + const totalHourError = checkTotalHours(timeEntries, leaves); + if (totalHourError) { + errors[date] = totalHourError; } }); @@ -125,48 +123,64 @@ export const validateTimesheet = ( export const validateLeaveRecord = ( leaveRecords: RecordLeaveInput, timesheet: RecordTimesheetInput, + 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(leaveRecords).forEach((date) => { const leaves = leaveRecords[date]; // Check each leave entry for (const entry of leaves) { - const entryError = isValidLeaveEntry(entry); + const entryError = validateLeaveEntry(entry, holidays.has(date)); if (entryError) { errors[date] = "There are errors in the entries"; + return; } } // 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); - - 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}}"; + const totalHourError = checkTotalHours(timeEntries, leaves); + if (totalHourError) { + errors[date] = totalHourError; } }); return Object.keys(errors).length > 0 ? errors : undefined; }; +export const checkTotalHours = ( + timeEntries: TimeEntry[], + leaves: LeaveEntry[], +): string | undefined => { + const leaveHours = leaves.reduce((acc, entry) => acc + entry.inputHours, 0); + + 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) { + return "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 + ) { + return "The daily total hours cannot be more than {{TIMESHEET_DAILY_MAX_HOURS}}"; + } +}; + 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/LeaveModal/LeaveModal.tsx b/src/components/LeaveModal/LeaveModal.tsx index b22f8fc..e8f32d1 100644 --- a/src/components/LeaveModal/LeaveModal.tsx +++ b/src/components/LeaveModal/LeaveModal.tsx @@ -81,7 +81,11 @@ const LeaveModal: React.FC = ({ const onSubmit = useCallback>( async (data) => { - const errors = validateLeaveRecord(data, timesheetRecords); + const errors = validateLeaveRecord( + data, + timesheetRecords, + companyHolidays, + ); if (errors) { Object.keys(errors).forEach((date) => formProps.setError(date, { @@ -106,7 +110,7 @@ const LeaveModal: React.FC = ({ formProps.reset(newFormValues); onClose(); }, - [formProps, onClose, timesheetRecords, username], + [companyHolidays, formProps, onClose, timesheetRecords, username], ); const onCancel = useCallback(() => { diff --git a/src/components/LeaveTable/LeaveEditModal.tsx b/src/components/LeaveTable/LeaveEditModal.tsx index 524a268..05d1600 100644 --- a/src/components/LeaveTable/LeaveEditModal.tsx +++ b/src/components/LeaveTable/LeaveEditModal.tsx @@ -1,6 +1,6 @@ import { LeaveType } from "@/app/api/timesheets"; import { LeaveEntry } from "@/app/api/timesheets/actions"; -import { LEAVE_DAILY_MAX_HOURS } from "@/app/api/timesheets/utils"; +import { DAILY_NORMAL_MAX_HOURS } from "@/app/api/timesheets/utils"; import { shortDateFormatter } from "@/app/utils/formatUtil"; import { roundToNearestQuarter } from "@/app/utils/manhourUtils"; import { Check, Delete } from "@mui/icons-material"; @@ -24,7 +24,7 @@ import { useTranslation } from "react-i18next"; export interface Props extends Omit { onSave: (leaveEntry: LeaveEntry, recordDate?: string) => Promise; - onDelete?: () => void; + onDelete?: () => Promise; leaveTypes: LeaveType[]; defaultValues?: Partial; modalSx?: SxProps; @@ -59,7 +59,7 @@ const LeaveEditModal: React.FC = ({ t, i18n: { language }, } = useTranslation("home"); - const { register, control, reset, getValues, trigger, formState } = + const { register, control, reset, getValues, trigger, formState, setError } = useForm({ defaultValues: { leaveTypeId: leaveTypes[0].id, @@ -73,10 +73,16 @@ const LeaveEditModal: React.FC = ({ const saveHandler = useCallback(async () => { const valid = await trigger(); if (valid) { - await onSave(getValues(), recordDate); - reset({ id: Date.now() }); + try { + await onSave(getValues(), recordDate); + reset({ id: Date.now() }); + } catch (e) { + setError("root", { + message: e instanceof Error ? e.message : "Unknown error", + }); + } } - }, [getValues, onSave, recordDate, reset, trigger]); + }, [getValues, onSave, recordDate, reset, setError, trigger]); const closeHandler = useCallback>( (...args) => { @@ -121,12 +127,19 @@ const LeaveEditModal: React.FC = ({ fullWidth {...register("inputHours", { setValueAs: (value) => roundToNearestQuarter(parseFloat(value)), - validate: (value) => - (0 < value && value <= LEAVE_DAILY_MAX_HOURS) || - t( - "Input hours should be between 0 and {{LEAVE_DAILY_MAX_HOURS}}", - { LEAVE_DAILY_MAX_HOURS }, - ), + validate: (value) => { + if (isHoliday) { + return t("Cannot input normal hours on holidays"); + } + + return ( + (0 < value && value <= DAILY_NORMAL_MAX_HOURS) || + t( + "Input hours should be between 0 and {{DAILY_NORMAL_MAX_HOURS}}", + { DAILY_NORMAL_MAX_HOURS }, + ) + ); + }, })} error={Boolean(formState.errors.inputHours)} helperText={formState.errors.inputHours?.message} @@ -138,6 +151,11 @@ const LeaveEditModal: React.FC = ({ rows={2} {...register("remark")} /> + {formState.errors.root?.message && ( + + {t(formState.errors.root.message, { DAILY_NORMAL_MAX_HOURS })} + + )} {onDelete && (