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