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