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