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