| @@ -4,6 +4,8 @@ import { LeaveEntry, RecordTimeLeaveInput, TimeEntry } from "./actions"; | |||||
| import { convertDateArrayToString } from "@/app/utils/formatUtil"; | import { convertDateArrayToString } from "@/app/utils/formatUtil"; | ||||
| import compact from "lodash/compact"; | import compact from "lodash/compact"; | ||||
| import dayjs, { Dayjs } from "dayjs"; | import dayjs, { Dayjs } from "dayjs"; | ||||
| import isSameOrAfter from "dayjs/plugin/isSameOrAfter"; | |||||
| dayjs.extend(isSameOrAfter); | |||||
| export type TimeEntryError = { | export type TimeEntryError = { | ||||
| [field in keyof TimeEntry]?: string; | [field in keyof TimeEntry]?: string; | ||||
| @@ -84,6 +86,7 @@ export const validateTimeLeaveRecord = ( | |||||
| records: RecordTimeLeaveInput, | records: RecordTimeLeaveInput, | ||||
| companyHolidays: HolidaysResult[], | companyHolidays: HolidaysResult[], | ||||
| isFullTime?: boolean, | isFullTime?: boolean, | ||||
| joinDate?: Dayjs, | |||||
| ): { [date: string]: string } | undefined => { | ): { [date: string]: string } | undefined => { | ||||
| const errors: { [date: string]: string } = {}; | const errors: { [date: string]: string } = {}; | ||||
| @@ -122,6 +125,7 @@ export const validateTimeLeaveRecord = ( | |||||
| entries.filter((e) => e.type === "leaveEntry") as LeaveEntry[], | entries.filter((e) => e.type === "leaveEntry") as LeaveEntry[], | ||||
| isHoliday, | isHoliday, | ||||
| isFullTime, | isFullTime, | ||||
| joinDate, | |||||
| ); | ); | ||||
| if (totalHourError) { | if (totalHourError) { | ||||
| @@ -138,6 +142,7 @@ export const checkTotalHours = ( | |||||
| leaves: LeaveEntry[], | leaves: LeaveEntry[], | ||||
| isHoliday?: boolean, | isHoliday?: boolean, | ||||
| isFullTime?: boolean, | isFullTime?: boolean, | ||||
| joinDate?: Dayjs, | |||||
| ): string | undefined => { | ): string | undefined => { | ||||
| const leaveHours = leaves.reduce((acc, entry) => acc + entry.inputHours, 0); | const leaveHours = leaves.reduce((acc, entry) => acc + entry.inputHours, 0); | ||||
| @@ -149,19 +154,26 @@ export const checkTotalHours = ( | |||||
| return acc + (entry.otHours || 0); | return acc + (entry.otHours || 0); | ||||
| }, 0); | }, 0); | ||||
| if (totalInputHours + leaveHours > DAILY_NORMAL_MAX_HOURS) { | |||||
| const totalHours = totalInputHours + leaveHours; | |||||
| const isDayToCheckAfterJoinDate = | |||||
| joinDate && joinDate.isValid() ? dayJsObj.isSameOrAfter(joinDate) : true; | |||||
| if (!isDayToCheckAfterJoinDate && totalHours > 0) { | |||||
| return "Cannot input hours before join date."; | |||||
| } | |||||
| if (totalHours > 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."; | 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 ( | } else if ( | ||||
| isFullTime && | isFullTime && | ||||
| isDayToCheckAfterJoinDate && | |||||
| !isHoliday && | !isHoliday && | ||||
| !dayJsObj.isSame(dayjs(), "day") && | !dayJsObj.isSame(dayjs(), "day") && | ||||
| totalInputHours + leaveHours !== DAILY_NORMAL_MAX_HOURS | |||||
| totalHours !== DAILY_NORMAL_MAX_HOURS | |||||
| ) { | ) { | ||||
| return "The daily normal hours (timesheet hours + leave hours) for full-time staffs should be {{DAILY_NORMAL_MAX_HOURS}}."; | return "The daily normal hours (timesheet hours + leave hours) for full-time staffs should be {{DAILY_NORMAL_MAX_HOURS}}."; | ||||
| } else if ( | |||||
| totalInputHours + totalOtHours + leaveHours > | |||||
| TIMESHEET_DAILY_MAX_HOURS | |||||
| ) { | |||||
| } else if (totalHours + totalOtHours > TIMESHEET_DAILY_MAX_HOURS) { | |||||
| return "The daily total hours cannot be more than {{TIMESHEET_DAILY_MAX_HOURS}}"; | return "The daily total hours cannot be more than {{TIMESHEET_DAILY_MAX_HOURS}}"; | ||||
| } | } | ||||
| }; | }; | ||||
| @@ -24,7 +24,7 @@ import { | |||||
| } from "@/app/api/timesheets/actions"; | } from "@/app/api/timesheets/actions"; | ||||
| import { Props as LeaveEditModalProps } from "../LeaveTable/LeaveEditModal"; | import { Props as LeaveEditModalProps } from "../LeaveTable/LeaveEditModal"; | ||||
| import LeaveEditModal from "../LeaveTable/LeaveEditModal"; | import LeaveEditModal from "../LeaveTable/LeaveEditModal"; | ||||
| import dayjs from "dayjs"; | |||||
| import dayjs, { Dayjs } from "dayjs"; | |||||
| import { checkTotalHours } from "@/app/api/timesheets/utils"; | import { checkTotalHours } from "@/app/api/timesheets/utils"; | ||||
| import unionBy from "lodash/unionBy"; | import unionBy from "lodash/unionBy"; | ||||
| @@ -35,6 +35,7 @@ export interface Props { | |||||
| leaveRecords: RecordLeaveInput; | leaveRecords: RecordLeaveInput; | ||||
| timesheetRecords: RecordTimesheetInput; | timesheetRecords: RecordTimesheetInput; | ||||
| isFullTime: boolean; | isFullTime: boolean; | ||||
| joinDate: Dayjs; | |||||
| } | } | ||||
| interface EventClickArg { | interface EventClickArg { | ||||
| @@ -55,8 +56,12 @@ const LeaveCalendar: React.FC<Props> = ({ | |||||
| timesheetRecords, | timesheetRecords, | ||||
| leaveRecords, | leaveRecords, | ||||
| isFullTime, | isFullTime, | ||||
| joinDate, | |||||
| }) => { | }) => { | ||||
| const { t ,i18n: { language }} = useTranslation(["home", "common"]); | |||||
| const { | |||||
| t, | |||||
| i18n: { language }, | |||||
| } = useTranslation(["home", "common"]); | |||||
| const locale = language === "zh" ? "zh-tw" : "en"; | const locale = language === "zh" ? "zh-tw" : "en"; | ||||
| const theme = useTheme(); | const theme = useTheme(); | ||||
| @@ -236,11 +241,18 @@ const LeaveCalendar: React.FC<Props> = ({ | |||||
| leavesWithNewEntry, | leavesWithNewEntry, | ||||
| Boolean(isHoliday), | Boolean(isHoliday), | ||||
| isFullTime, | isFullTime, | ||||
| joinDate, | |||||
| ); | ); | ||||
| if (totalHourError) throw Error(totalHourError); | if (totalHourError) throw Error(totalHourError); | ||||
| }, | }, | ||||
| [companyHolidays, isFullTime, localLeaveRecords, timesheetRecords], | |||||
| [ | |||||
| companyHolidays, | |||||
| isFullTime, | |||||
| joinDate, | |||||
| localLeaveRecords, | |||||
| timesheetRecords, | |||||
| ], | |||||
| ); | ); | ||||
| const handleSaveLeave = useCallback( | const handleSaveLeave = useCallback( | ||||
| @@ -36,6 +36,7 @@ const LeaveModal: React.FC<Props> = ({ | |||||
| leaveRecords, | leaveRecords, | ||||
| timesheetRecords, | timesheetRecords, | ||||
| isFullTime, | isFullTime, | ||||
| joinDate, | |||||
| }) => { | }) => { | ||||
| const { t } = useTranslation("home"); | const { t } = useTranslation("home"); | ||||
| const isMobile = useIsMobile(); | const isMobile = useIsMobile(); | ||||
| @@ -43,6 +44,7 @@ const LeaveModal: React.FC<Props> = ({ | |||||
| const title = t("Record leave"); | const title = t("Record leave"); | ||||
| const content = ( | const content = ( | ||||
| <LeaveCalendar | <LeaveCalendar | ||||
| joinDate={joinDate} | |||||
| isFullTime={isFullTime} | isFullTime={isFullTime} | ||||
| leaveTypes={leaveTypes} | leaveTypes={leaveTypes} | ||||
| companyHolidays={companyHolidays} | companyHolidays={companyHolidays} | ||||
| @@ -20,7 +20,7 @@ import { | |||||
| RecordTimesheetInput, | RecordTimesheetInput, | ||||
| saveTimeLeave, | saveTimeLeave, | ||||
| } from "@/app/api/timesheets/actions"; | } from "@/app/api/timesheets/actions"; | ||||
| import dayjs from "dayjs"; | |||||
| import dayjs, { Dayjs } from "dayjs"; | |||||
| import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | ||||
| import { AssignedProject, ProjectWithTasks } from "@/app/api/projects"; | import { AssignedProject, ProjectWithTasks } from "@/app/api/projects"; | ||||
| import FullscreenModal from "../FullscreenModal"; | import FullscreenModal from "../FullscreenModal"; | ||||
| @@ -51,6 +51,7 @@ interface Props { | |||||
| fastEntryEnabled?: boolean; | fastEntryEnabled?: boolean; | ||||
| leaveTypes: LeaveType[]; | leaveTypes: LeaveType[]; | ||||
| isFullTime: boolean; | isFullTime: boolean; | ||||
| joinDate: Dayjs; | |||||
| miscTasks: Task[]; | miscTasks: Task[]; | ||||
| } | } | ||||
| @@ -77,6 +78,7 @@ const TimeLeaveModal: React.FC<Props> = ({ | |||||
| fastEntryEnabled, | fastEntryEnabled, | ||||
| leaveTypes, | leaveTypes, | ||||
| isFullTime, | isFullTime, | ||||
| joinDate, | |||||
| miscTasks, | miscTasks, | ||||
| }) => { | }) => { | ||||
| const { t } = useTranslation("home"); | const { t } = useTranslation("home"); | ||||
| @@ -113,7 +115,12 @@ const TimeLeaveModal: React.FC<Props> = ({ | |||||
| const onSubmit = useCallback<SubmitHandler<RecordTimeLeaveInput>>( | const onSubmit = useCallback<SubmitHandler<RecordTimeLeaveInput>>( | ||||
| async (data) => { | async (data) => { | ||||
| const errors = validateTimeLeaveRecord(data, companyHolidays, isFullTime); | |||||
| const errors = validateTimeLeaveRecord( | |||||
| data, | |||||
| companyHolidays, | |||||
| isFullTime, | |||||
| joinDate, | |||||
| ); | |||||
| if (errors) { | if (errors) { | ||||
| Object.keys(errors).forEach((date) => | Object.keys(errors).forEach((date) => | ||||
| formProps.setError(date, { | formProps.setError(date, { | ||||
| @@ -138,7 +145,7 @@ const TimeLeaveModal: React.FC<Props> = ({ | |||||
| formProps.reset(newFormValues); | formProps.reset(newFormValues); | ||||
| onClose(); | onClose(); | ||||
| }, | }, | ||||
| [companyHolidays, formProps, onClose, isFullTime], | |||||
| [companyHolidays, isFullTime, joinDate, formProps, onClose], | |||||
| ); | ); | ||||
| const onCancel = useCallback(() => { | const onCancel = useCallback(() => { | ||||
| @@ -26,6 +26,7 @@ import { TimesheetAmendmentModal } from "../TimesheetAmendment/TimesheetAmendmen | |||||
| import TimeLeaveModal from "../TimeLeaveModal/TimeLeaveModal"; | import TimeLeaveModal from "../TimeLeaveModal/TimeLeaveModal"; | ||||
| import LeaveModal from "../LeaveModal"; | import LeaveModal from "../LeaveModal"; | ||||
| import { Task } from "@/app/api/tasks"; | import { Task } from "@/app/api/tasks"; | ||||
| import dayjs from "dayjs"; | |||||
| export interface Props { | export interface Props { | ||||
| leaveTypes: LeaveType[]; | leaveTypes: LeaveType[]; | ||||
| @@ -40,6 +41,7 @@ export interface Props { | |||||
| maintainNormalStaffWorkspaceAbility: boolean; | maintainNormalStaffWorkspaceAbility: boolean; | ||||
| maintainManagementStaffWorkspaceAbility: boolean; | maintainManagementStaffWorkspaceAbility: boolean; | ||||
| isFullTime: boolean; | isFullTime: boolean; | ||||
| joinDate?: number | null; | |||||
| miscTasks: Task[]; | miscTasks: Task[]; | ||||
| } | } | ||||
| @@ -61,6 +63,7 @@ const UserWorkspacePage: React.FC<Props> = ({ | |||||
| maintainNormalStaffWorkspaceAbility, | maintainNormalStaffWorkspaceAbility, | ||||
| maintainManagementStaffWorkspaceAbility, | maintainManagementStaffWorkspaceAbility, | ||||
| isFullTime, | isFullTime, | ||||
| joinDate, | |||||
| miscTasks, | miscTasks, | ||||
| }) => { | }) => { | ||||
| const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); | const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); | ||||
| @@ -192,6 +195,7 @@ const UserWorkspacePage: React.FC<Props> = ({ | |||||
| timesheetRecords={defaultTimesheets} | timesheetRecords={defaultTimesheets} | ||||
| leaveRecords={defaultLeaveRecords} | leaveRecords={defaultLeaveRecords} | ||||
| isFullTime={isFullTime} | isFullTime={isFullTime} | ||||
| joinDate={dayjs(joinDate)} | |||||
| miscTasks={miscTasks} | miscTasks={miscTasks} | ||||
| /> | /> | ||||
| <LeaveModal | <LeaveModal | ||||
| @@ -203,6 +207,7 @@ const UserWorkspacePage: React.FC<Props> = ({ | |||||
| leaveRecords={defaultLeaveRecords} | leaveRecords={defaultLeaveRecords} | ||||
| timesheetRecords={defaultTimesheets} | timesheetRecords={defaultTimesheets} | ||||
| isFullTime={isFullTime} | isFullTime={isFullTime} | ||||
| joinDate={dayjs(joinDate)} | |||||
| /> | /> | ||||
| {assignedProjects.length > 0 ? ( | {assignedProjects.length > 0 ? ( | ||||
| <AssignedProjects | <AssignedProjects | ||||
| @@ -61,6 +61,7 @@ const UserWorkspaceWrapper: React.FC = async () => { | |||||
| return ( | return ( | ||||
| <UserWorkspacePage | <UserWorkspacePage | ||||
| joinDate={userStaff?.joinDate} | |||||
| isFullTime={isFullTime} | isFullTime={isFullTime} | ||||
| teamLeaves={teamLeaves} | teamLeaves={teamLeaves} | ||||
| teamTimesheets={teamTimesheets} | teamTimesheets={teamTimesheets} | ||||
| @@ -7,6 +7,10 @@ export interface SessionStaff { | |||||
| teamId: number; | teamId: number; | ||||
| isTeamLead: boolean; | isTeamLead: boolean; | ||||
| employType: string | null; | employType: string | null; | ||||
| /** | |||||
| * The join date in milliseconds since epoch | |||||
| */ | |||||
| joinDate: number | null; | |||||
| } | } | ||||
| export interface SessionWithTokens extends Session { | export interface SessionWithTokens extends Session { | ||||
| staff?: SessionStaff; | staff?: SessionStaff; | ||||