@@ -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; | ||||