@@ -13,6 +13,7 @@ export type TeamTimeSheets = { | |||||
timeEntries: RecordTimesheetInput; | timeEntries: RecordTimesheetInput; | ||||
staffId: string; | staffId: string; | ||||
name: string; | name: string; | ||||
employType: string | null; | |||||
}; | }; | ||||
}; | }; | ||||
@@ -21,6 +22,7 @@ export type TeamLeaves = { | |||||
leaveEntries: RecordLeaveInput; | leaveEntries: RecordLeaveInput; | ||||
staffId: string; | staffId: string; | ||||
name: string; | name: string; | ||||
employType: string | null; | |||||
}; | }; | ||||
}; | }; | ||||
@@ -3,6 +3,7 @@ import { HolidaysResult } from "../holidays"; | |||||
import { LeaveEntry, RecordTimeLeaveInput, TimeEntry } from "./actions"; | 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 from "dayjs"; | |||||
export type TimeEntryError = { | export type TimeEntryError = { | ||||
[field in keyof TimeEntry]?: string; | [field in keyof TimeEntry]?: string; | ||||
@@ -80,6 +81,7 @@ export const validateLeaveEntry = ( | |||||
export const validateTimeLeaveRecord = ( | export const validateTimeLeaveRecord = ( | ||||
records: RecordTimeLeaveInput, | records: RecordTimeLeaveInput, | ||||
companyHolidays: HolidaysResult[], | companyHolidays: HolidaysResult[], | ||||
isFullTime?: boolean, | |||||
): { [date: string]: string } | undefined => { | ): { [date: string]: string } | undefined => { | ||||
const errors: { [date: string]: string } = {}; | const errors: { [date: string]: string } = {}; | ||||
@@ -91,14 +93,18 @@ export const validateTimeLeaveRecord = ( | |||||
); | ); | ||||
Object.keys(records).forEach((date) => { | Object.keys(records).forEach((date) => { | ||||
const dayJsObj = dayjs(date); | |||||
const isHoliday = | |||||
holidays.has(date) || dayJsObj.day() === 0 || dayJsObj.day() === 6; | |||||
const entries = records[date]; | const entries = records[date]; | ||||
// Check each entry | // Check each entry | ||||
for (const entry of entries) { | for (const entry of entries) { | ||||
let entryError; | let entryError; | ||||
if (entry.type === "leaveEntry") { | if (entry.type === "leaveEntry") { | ||||
entryError = validateLeaveEntry(entry, holidays.has(date)); | |||||
entryError = validateLeaveEntry(entry, isHoliday); | |||||
} else { | } else { | ||||
entryError = validateTimeEntry(entry, holidays.has(date)); | |||||
entryError = validateTimeEntry(entry, isHoliday); | |||||
} | } | ||||
if (entryError) { | if (entryError) { | ||||
@@ -111,6 +117,8 @@ export const validateTimeLeaveRecord = ( | |||||
const totalHourError = checkTotalHours( | const totalHourError = checkTotalHours( | ||||
entries.filter((e) => e.type === "timeEntry") as TimeEntry[], | entries.filter((e) => e.type === "timeEntry") as TimeEntry[], | ||||
entries.filter((e) => e.type === "leaveEntry") as LeaveEntry[], | entries.filter((e) => e.type === "leaveEntry") as LeaveEntry[], | ||||
isHoliday, | |||||
isFullTime, | |||||
); | ); | ||||
if (totalHourError) { | if (totalHourError) { | ||||
@@ -124,6 +132,8 @@ export const validateTimeLeaveRecord = ( | |||||
export const checkTotalHours = ( | export const checkTotalHours = ( | ||||
timeEntries: TimeEntry[], | timeEntries: TimeEntry[], | ||||
leaves: LeaveEntry[], | leaves: LeaveEntry[], | ||||
isHoliday?: boolean, | |||||
isFullTime?: boolean, | |||||
): string | undefined => { | ): string | undefined => { | ||||
const leaveHours = leaves.reduce((acc, entry) => acc + entry.inputHours, 0); | const leaveHours = leaves.reduce((acc, entry) => acc + entry.inputHours, 0); | ||||
@@ -137,6 +147,12 @@ export const checkTotalHours = ( | |||||
if (totalInputHours + leaveHours > DAILY_NORMAL_MAX_HOURS) { | 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."; | 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 && | |||||
!isHoliday && | |||||
totalInputHours + leaveHours !== 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 ( | } else if ( | ||||
totalInputHours + totalOtHours + leaveHours > | totalInputHours + totalOtHours + leaveHours > | ||||
TIMESHEET_DAILY_MAX_HOURS | TIMESHEET_DAILY_MAX_HOURS | ||||
@@ -34,6 +34,7 @@ export interface Props { | |||||
allProjects: ProjectWithTasks[]; | allProjects: ProjectWithTasks[]; | ||||
leaveRecords: RecordLeaveInput; | leaveRecords: RecordLeaveInput; | ||||
timesheetRecords: RecordTimesheetInput; | timesheetRecords: RecordTimesheetInput; | ||||
isFullTime: boolean; | |||||
} | } | ||||
interface EventClickArg { | interface EventClickArg { | ||||
@@ -53,6 +54,7 @@ const LeaveCalendar: React.FC<Props> = ({ | |||||
leaveTypes, | leaveTypes, | ||||
timesheetRecords, | timesheetRecords, | ||||
leaveRecords, | leaveRecords, | ||||
isFullTime, | |||||
}) => { | }) => { | ||||
const { t } = useTranslation(["home", "common"]); | const { t } = useTranslation(["home", "common"]); | ||||
@@ -215,6 +217,10 @@ const LeaveCalendar: React.FC<Props> = ({ | |||||
if (!date) { | if (!date) { | ||||
throw Error("Invalid date"); | throw Error("Invalid date"); | ||||
} | } | ||||
const dayJsObj = dayjs(date); | |||||
const holiday = getHolidayForDate(date, companyHolidays); | |||||
const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6; | |||||
const leaves = localLeaveRecords[date] || []; | const leaves = localLeaveRecords[date] || []; | ||||
const timesheets = timesheetRecords[date] || []; | const timesheets = timesheetRecords[date] || []; | ||||
@@ -224,11 +230,16 @@ const LeaveCalendar: React.FC<Props> = ({ | |||||
"id", | "id", | ||||
); | ); | ||||
const totalHourError = checkTotalHours(timesheets, leavesWithNewEntry); | |||||
const totalHourError = checkTotalHours( | |||||
timesheets, | |||||
leavesWithNewEntry, | |||||
Boolean(isHoliday), | |||||
isFullTime, | |||||
); | |||||
if (totalHourError) throw Error(totalHourError); | if (totalHourError) throw Error(totalHourError); | ||||
}, | }, | ||||
[localLeaveRecords, timesheetRecords], | |||||
[companyHolidays, isFullTime, localLeaveRecords, timesheetRecords], | |||||
); | ); | ||||
const handleSaveLeave = useCallback( | const handleSaveLeave = useCallback( | ||||
@@ -35,6 +35,7 @@ const LeaveModal: React.FC<Props> = ({ | |||||
allProjects, | allProjects, | ||||
leaveRecords, | leaveRecords, | ||||
timesheetRecords, | timesheetRecords, | ||||
isFullTime, | |||||
}) => { | }) => { | ||||
const { t } = useTranslation("home"); | const { t } = useTranslation("home"); | ||||
const isMobile = useIsMobile(); | const isMobile = useIsMobile(); | ||||
@@ -42,6 +43,7 @@ const LeaveModal: React.FC<Props> = ({ | |||||
const title = t("Record leave"); | const title = t("Record leave"); | ||||
const content = ( | const content = ( | ||||
<LeaveCalendar | <LeaveCalendar | ||||
isFullTime={isFullTime} | |||||
leaveTypes={leaveTypes} | leaveTypes={leaveTypes} | ||||
companyHolidays={companyHolidays} | companyHolidays={companyHolidays} | ||||
allProjects={allProjects} | allProjects={allProjects} | ||||
@@ -1,5 +1,3 @@ | |||||
import { Grid } from "@mui/material"; | |||||
interface Props { | interface Props { | ||||
width?: number; | width?: number; | ||||
height?: number; | height?: number; | ||||
@@ -16,7 +14,7 @@ const Logo: React.FC<Props> = ({ width, height }) => { | |||||
<g clipPath="url(#a)"> | <g clipPath="url(#a)"> | ||||
<path id="logo" | <path id="logo" | ||||
fill="#89ba17" stroke="#89ba17" stroke-width="1" | |||||
fill="#89ba17" stroke="#89ba17" strokeWidth="1" | |||||
d="M 98.00,125.00 | d="M 98.00,125.00 | ||||
C 92.11,126.67 84.23,126.00 78.00,126.00 | C 92.11,126.67 84.23,126.00 78.00,126.00 | ||||
68.19,126.00 48.68,126.75 40.00,125.00 | 68.19,126.00 48.68,126.75 40.00,125.00 | ||||
@@ -66,7 +64,7 @@ const Logo: React.FC<Props> = ({ width, height }) => { | |||||
41.00,156.00 39.00,156.00 39.00,156.00 Z" /> | 41.00,156.00 39.00,156.00 39.00,156.00 Z" /> | ||||
<path id="word" | <path id="word" | ||||
fill="#111927" stroke="#111927" stroke-width="1" | |||||
fill="#111927" stroke="#111927" strokeWidth="1" | |||||
d="M 273.00,64.00 | d="M 273.00,64.00 | ||||
C 273.00,64.00 279.96,66.35 279.96,66.35 | C 273.00,64.00 279.96,66.35 279.96,66.35 | ||||
283.26,67.45 289.15,67.63 290.83,63.79 | 283.26,67.45 289.15,67.63 290.83,63.79 | ||||
@@ -49,6 +49,7 @@ interface Props { | |||||
companyHolidays: HolidaysResult[]; | companyHolidays: HolidaysResult[]; | ||||
fastEntryEnabled?: boolean; | fastEntryEnabled?: boolean; | ||||
leaveTypes: LeaveType[]; | leaveTypes: LeaveType[]; | ||||
isFullTime: boolean; | |||||
} | } | ||||
const modalSx: SxProps = { | const modalSx: SxProps = { | ||||
@@ -71,6 +72,7 @@ const TimeLeaveModal: React.FC<Props> = ({ | |||||
companyHolidays, | companyHolidays, | ||||
fastEntryEnabled, | fastEntryEnabled, | ||||
leaveTypes, | leaveTypes, | ||||
isFullTime | |||||
}) => { | }) => { | ||||
const { t } = useTranslation("home"); | const { t } = useTranslation("home"); | ||||
@@ -106,7 +108,7 @@ const TimeLeaveModal: React.FC<Props> = ({ | |||||
const onSubmit = useCallback<SubmitHandler<RecordTimeLeaveInput>>( | const onSubmit = useCallback<SubmitHandler<RecordTimeLeaveInput>>( | ||||
async (data) => { | async (data) => { | ||||
const errors = validateTimeLeaveRecord(data, companyHolidays); | |||||
const errors = validateTimeLeaveRecord(data, companyHolidays, isFullTime); | |||||
if (errors) { | if (errors) { | ||||
Object.keys(errors).forEach((date) => | Object.keys(errors).forEach((date) => | ||||
formProps.setError(date, { | formProps.setError(date, { | ||||
@@ -131,7 +133,7 @@ const TimeLeaveModal: React.FC<Props> = ({ | |||||
formProps.reset(newFormValues); | formProps.reset(newFormValues); | ||||
onClose(); | onClose(); | ||||
}, | }, | ||||
[companyHolidays, formProps, onClose], | |||||
[companyHolidays, formProps, onClose, isFullTime], | |||||
); | ); | ||||
const onCancel = useCallback(() => { | const onCancel = useCallback(() => { | ||||
@@ -347,6 +347,10 @@ const TimesheetAmendment: React.FC<Props> = ({ | |||||
if (!date) { | if (!date) { | ||||
throw Error("Invalid date"); | throw Error("Invalid date"); | ||||
} | } | ||||
const dayJsObj = dayjs(date); | |||||
const holiday = getHolidayForDate(date, companyHolidays); | |||||
const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6; | |||||
const intStaffId = parseInt(selectedStaff.id); | const intStaffId = parseInt(selectedStaff.id); | ||||
const leaves = localTeamLeaves[intStaffId].leaveEntries[date] || []; | const leaves = localTeamLeaves[intStaffId].leaveEntries[date] || []; | ||||
const timesheets = | const timesheets = | ||||
@@ -360,7 +364,11 @@ const TimesheetAmendment: React.FC<Props> = ({ | |||||
leaves, | leaves, | ||||
"id", | "id", | ||||
); | ); | ||||
totalHourError = checkTotalHours(timesheets, leavesWithNewEntry); | |||||
totalHourError = checkTotalHours( | |||||
timesheets, | |||||
leavesWithNewEntry, | |||||
Boolean(isHoliday), | |||||
); | |||||
} else { | } else { | ||||
// newEntry is a timesheet entry | // newEntry is a timesheet entry | ||||
const timesheetsWithNewEntry = unionBy( | const timesheetsWithNewEntry = unionBy( | ||||
@@ -368,11 +376,15 @@ const TimesheetAmendment: React.FC<Props> = ({ | |||||
timesheets, | timesheets, | ||||
"id", | "id", | ||||
); | ); | ||||
totalHourError = checkTotalHours(timesheetsWithNewEntry, leaves); | |||||
totalHourError = checkTotalHours( | |||||
timesheetsWithNewEntry, | |||||
leaves, | |||||
Boolean(isHoliday), | |||||
); | |||||
} | } | ||||
if (totalHourError) throw Error(totalHourError); | if (totalHourError) throw Error(totalHourError); | ||||
}, | }, | ||||
[localTeamLeaves, localTeamTimesheets, selectedStaff.id], | |||||
[localTeamLeaves, localTeamTimesheets, selectedStaff, companyHolidays], | |||||
); | ); | ||||
const handleSave = useCallback( | const handleSave = useCallback( | ||||
@@ -4,7 +4,12 @@ import React, { useCallback, useState } from "react"; | |||||
import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
import Button from "@mui/material/Button"; | import Button from "@mui/material/Button"; | ||||
import Stack from "@mui/material/Stack"; | import Stack from "@mui/material/Stack"; | ||||
import { CalendarMonth, EditCalendar, Luggage, MoreTime } from "@mui/icons-material"; | |||||
import { | |||||
CalendarMonth, | |||||
EditCalendar, | |||||
Luggage, | |||||
MoreTime, | |||||
} from "@mui/icons-material"; | |||||
import { Menu, MenuItem, SxProps, Typography } from "@mui/material"; | import { Menu, MenuItem, SxProps, Typography } from "@mui/material"; | ||||
import AssignedProjects from "./AssignedProjects"; | import AssignedProjects from "./AssignedProjects"; | ||||
import { AssignedProject, ProjectWithTasks } from "@/app/api/projects"; | import { AssignedProject, ProjectWithTasks } from "@/app/api/projects"; | ||||
@@ -33,6 +38,7 @@ export interface Props { | |||||
fastEntryEnabled: boolean; | fastEntryEnabled: boolean; | ||||
maintainNormalStaffWorkspaceAbility: boolean; | maintainNormalStaffWorkspaceAbility: boolean; | ||||
maintainManagementStaffWorkspaceAbility: boolean; | maintainManagementStaffWorkspaceAbility: boolean; | ||||
isFullTime: boolean; | |||||
} | } | ||||
const menuItemSx: SxProps = { | const menuItemSx: SxProps = { | ||||
@@ -52,6 +58,7 @@ const UserWorkspacePage: React.FC<Props> = ({ | |||||
fastEntryEnabled, | fastEntryEnabled, | ||||
maintainNormalStaffWorkspaceAbility, | maintainNormalStaffWorkspaceAbility, | ||||
maintainManagementStaffWorkspaceAbility, | maintainManagementStaffWorkspaceAbility, | ||||
isFullTime, | |||||
}) => { | }) => { | ||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); | const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); | ||||
@@ -181,6 +188,7 @@ const UserWorkspacePage: React.FC<Props> = ({ | |||||
assignedProjects={assignedProjects} | assignedProjects={assignedProjects} | ||||
timesheetRecords={defaultTimesheets} | timesheetRecords={defaultTimesheets} | ||||
leaveRecords={defaultLeaveRecords} | leaveRecords={defaultLeaveRecords} | ||||
isFullTime={isFullTime} | |||||
/> | /> | ||||
<LeaveModal | <LeaveModal | ||||
open={isLeaveCalendarVisible} | open={isLeaveCalendarVisible} | ||||
@@ -190,6 +198,7 @@ const UserWorkspacePage: React.FC<Props> = ({ | |||||
allProjects={allProjects} | allProjects={allProjects} | ||||
leaveRecords={defaultLeaveRecords} | leaveRecords={defaultLeaveRecords} | ||||
timesheetRecords={defaultTimesheets} | timesheetRecords={defaultTimesheets} | ||||
isFullTime={isFullTime} | |||||
/> | /> | ||||
{assignedProjects.length > 0 ? ( | {assignedProjects.length > 0 ? ( | ||||
<AssignedProjects | <AssignedProjects | ||||
@@ -11,8 +11,12 @@ import { | |||||
fetchTimesheets, | fetchTimesheets, | ||||
} from "@/app/api/timesheets"; | } from "@/app/api/timesheets"; | ||||
import { fetchHolidays } from "@/app/api/holidays"; | import { fetchHolidays } from "@/app/api/holidays"; | ||||
import { getUserAbilities } from "@/app/utils/commonUtil"; | |||||
import { MAINTAIN_TIMESHEET_FAST_TIME_ENTRY, MAINTAIN_NORMAL_STAFF_WORKSPACE, MAINTAIN_MANAGEMENT_STAFF_WORKSPACE } from "@/middleware"; | |||||
import { getUserAbilities, getUserStaff } from "@/app/utils/commonUtil"; | |||||
import { | |||||
MAINTAIN_TIMESHEET_FAST_TIME_ENTRY, | |||||
MAINTAIN_NORMAL_STAFF_WORKSPACE, | |||||
MAINTAIN_MANAGEMENT_STAFF_WORKSPACE, | |||||
} from "@/middleware"; | |||||
const UserWorkspaceWrapper: React.FC = async () => { | const UserWorkspaceWrapper: React.FC = async () => { | ||||
const [ | const [ | ||||
@@ -25,6 +29,7 @@ const UserWorkspaceWrapper: React.FC = async () => { | |||||
leaveTypes, | leaveTypes, | ||||
holidays, | holidays, | ||||
abilities, | abilities, | ||||
userStaff, | |||||
] = await Promise.all([ | ] = await Promise.all([ | ||||
fetchTeamMemberLeaves(), | fetchTeamMemberLeaves(), | ||||
fetchTeamMemberTimesheets(), | fetchTeamMemberTimesheets(), | ||||
@@ -34,15 +39,24 @@ const UserWorkspaceWrapper: React.FC = async () => { | |||||
fetchLeaves(), | fetchLeaves(), | ||||
fetchLeaveTypes(), | fetchLeaveTypes(), | ||||
fetchHolidays(), | fetchHolidays(), | ||||
getUserAbilities() | |||||
getUserAbilities(), | |||||
getUserStaff(), | |||||
]); | ]); | ||||
const fastEntryEnabled = abilities.includes(MAINTAIN_TIMESHEET_FAST_TIME_ENTRY) | |||||
const maintainNormalStaffWorkspaceAbility = abilities.includes(MAINTAIN_NORMAL_STAFF_WORKSPACE) | |||||
const maintainManagementStaffWorkspaceAbility = abilities.includes(MAINTAIN_MANAGEMENT_STAFF_WORKSPACE) | |||||
const fastEntryEnabled = abilities.includes( | |||||
MAINTAIN_TIMESHEET_FAST_TIME_ENTRY, | |||||
); | |||||
const maintainNormalStaffWorkspaceAbility = abilities.includes( | |||||
MAINTAIN_NORMAL_STAFF_WORKSPACE, | |||||
); | |||||
const maintainManagementStaffWorkspaceAbility = abilities.includes( | |||||
MAINTAIN_MANAGEMENT_STAFF_WORKSPACE, | |||||
); | |||||
const isFullTime = userStaff?.employType === "FT"; | |||||
return ( | return ( | ||||
<UserWorkspacePage | <UserWorkspacePage | ||||
isFullTime={isFullTime} | |||||
teamLeaves={teamLeaves} | teamLeaves={teamLeaves} | ||||
teamTimesheets={teamTimesheets} | teamTimesheets={teamTimesheets} | ||||
allProjects={allProjects} | allProjects={allProjects} | ||||
@@ -54,7 +68,9 @@ const UserWorkspaceWrapper: React.FC = async () => { | |||||
// Change to access check | // Change to access check | ||||
fastEntryEnabled={fastEntryEnabled} | fastEntryEnabled={fastEntryEnabled} | ||||
maintainNormalStaffWorkspaceAbility={maintainNormalStaffWorkspaceAbility} | maintainNormalStaffWorkspaceAbility={maintainNormalStaffWorkspaceAbility} | ||||
maintainManagementStaffWorkspaceAbility={maintainManagementStaffWorkspaceAbility} | |||||
maintainManagementStaffWorkspaceAbility={ | |||||
maintainManagementStaffWorkspaceAbility | |||||
} | |||||
/> | /> | ||||
); | ); | ||||
}; | }; | ||||
@@ -6,10 +6,11 @@ export interface SessionStaff { | |||||
id: number; | id: number; | ||||
teamId: number; | teamId: number; | ||||
isTeamLead: boolean; | isTeamLead: boolean; | ||||
employType: string | null; | |||||
} | } | ||||
export interface SessionWithTokens extends Session { | export interface SessionWithTokens extends Session { | ||||
staff?: SessionStaff; | staff?: SessionStaff; | ||||
role?: String; | |||||
role?: string; | |||||
abilities?: string[]; | abilities?: string[]; | ||||
accessToken?: string; | accessToken?: string; | ||||
refreshToken?: string; | refreshToken?: string; | ||||
@@ -60,14 +61,14 @@ export const authOptions: AuthOptions = { | |||||
session({ session, token }) { | session({ session, token }) { | ||||
const sessionWithToken: SessionWithTokens = { | const sessionWithToken: SessionWithTokens = { | ||||
...session, | ...session, | ||||
role: token.role as String, | |||||
role: token.role as string, | |||||
// Add the data from the token to the session | // Add the data from the token to the session | ||||
abilities: (token.abilities as ability[]).map( | abilities: (token.abilities as ability[]).map( | ||||
(item: ability) => item.actionSubjectCombo, | (item: ability) => item.actionSubjectCombo, | ||||
) as string[], | ) as string[], | ||||
accessToken: token.accessToken as string | undefined, | accessToken: token.accessToken as string | undefined, | ||||
refreshToken: token.refreshToken as string | undefined, | refreshToken: token.refreshToken as string | undefined, | ||||
staff: token.staff as SessionStaff | |||||
staff: token.staff as SessionStaff, | |||||
}; | }; | ||||
// console.log(sessionWithToken) | // console.log(sessionWithToken) | ||||
return sessionWithToken; | return sessionWithToken; | ||||