@@ -4,6 +4,7 @@ import UserWorkspacePage from "@/components/UserWorkspacePage"; | |||
import { | |||
fetchLeaveTypes, | |||
fetchLeaves, | |||
fetchTeamMemberTimesheets, | |||
fetchTimesheets, | |||
} from "@/app/api/timesheets"; | |||
import { authOptions } from "@/config/authConfig"; | |||
@@ -29,9 +30,10 @@ const Home: React.FC = async () => { | |||
fetchLeaveTypes(); | |||
fetchProjectWithTasks(); | |||
fetchHolidays(); | |||
fetchTeamMemberTimesheets(username); | |||
return ( | |||
<I18nProvider namespaces={["home"]}> | |||
<I18nProvider namespaces={["home", "common"]}> | |||
<UserWorkspacePage username={username} /> | |||
</I18nProvider> | |||
); | |||
@@ -63,3 +63,14 @@ export const saveLeave = async (data: RecordLeaveInput, username: string) => { | |||
return savedRecords; | |||
}; | |||
export const saveMemberEntry = async (data: { | |||
staffId: number; | |||
entry: TimeEntry; | |||
}) => { | |||
return serverFetchJson<TimeEntry>(`${BASE_API_URL}/timesheets/saveMemberEntry`, { | |||
method: "POST", | |||
body: JSON.stringify(data), | |||
headers: { "Content-Type": "application/json" }, | |||
}); | |||
}; |
@@ -8,6 +8,14 @@ export interface LeaveType { | |||
name: string; | |||
} | |||
export type TeamTimeSheets = { | |||
[memberId: number]: { | |||
timeEntries: RecordTimesheetInput; | |||
staffId: string; | |||
name: string; | |||
}; | |||
}; | |||
export const fetchTimesheets = cache(async (username: string) => { | |||
return serverFetchJson<RecordTimesheetInput>(`${BASE_API_URL}/timesheets`, { | |||
next: { tags: [`timesheets_${username}`] }, | |||
@@ -28,3 +36,12 @@ export const fetchLeaveTypes = cache(async () => { | |||
next: { tags: ["leaveTypes"] }, | |||
}); | |||
}); | |||
export const fetchTeamMemberTimesheets = cache(async (username: string) => { | |||
return serverFetchJson<TeamTimeSheets>( | |||
`${BASE_API_URL}/timesheets/teamTimesheets`, | |||
{ | |||
next: { tags: [`team_timesheets_${username}`] }, | |||
}, | |||
); | |||
}); |
@@ -0,0 +1,61 @@ | |||
import FullCalendar from "@fullcalendar/react"; | |||
import { Box, useTheme } from "@mui/material"; | |||
import React from "react"; | |||
type Props = React.ComponentProps<typeof FullCalendar>; | |||
const StyledFullCalendar: React.FC<Props> = (props) => { | |||
const theme = useTheme(); | |||
return ( | |||
<Box | |||
sx={{ | |||
".fc": { | |||
fontFamily: theme.typography.fontFamily, | |||
".fc-toolbar-title": theme.typography.h6, | |||
".fc-button": { | |||
borderRadius: "12px", | |||
border: "none", | |||
fontWeight: theme.typography.button.fontWeight, | |||
fontSize: theme.typography.button.fontSize, | |||
}, | |||
".fc-button-primary": { | |||
backgroundColor: "primary.main", | |||
"&:disabled": { | |||
backgroundColor: "action.disabledBackground", | |||
color: "action.disabled", | |||
opacity: 1, | |||
}, | |||
"&:active:not(:disabled)": { | |||
backgroundColor: "primary.dark", | |||
}, | |||
}, | |||
".fc-prev-button": { marginInlineEnd: 1 }, | |||
".fc-prev-button, .fc-next-button": { | |||
background: "transparent", | |||
border: "none", | |||
color: "neutral.700", | |||
padding: 0, | |||
borderRadius: "50% !important", | |||
display: "inline-flex", | |||
alignItems: "center", | |||
justifyContent: "center", | |||
width: 40, | |||
height: 40, | |||
"&.fc-button:active": { | |||
color: "neutral.800", | |||
backgroundColor: "action.hover", | |||
}, | |||
"&:focus": { | |||
boxShadow: "none !important", | |||
}, | |||
}, | |||
}, | |||
}} | |||
> | |||
<FullCalendar {...props} /> | |||
</Box> | |||
); | |||
}; | |||
export default StyledFullCalendar; |
@@ -0,0 +1 @@ | |||
export { default } from "./StyledFullCalendar"; |
@@ -0,0 +1,191 @@ | |||
import React, { useCallback, useMemo, useState } from "react"; | |||
import { HolidaysResult } from "@/app/api/holidays"; | |||
import { TeamTimeSheets } from "@/app/api/timesheets"; | |||
import dayGridPlugin from "@fullcalendar/daygrid"; | |||
import { Autocomplete, Stack, TextField, useTheme } from "@mui/material"; | |||
import { useTranslation } from "react-i18next"; | |||
import transform from "lodash/transform"; | |||
import { getPublicHolidaysForNYears } from "@/app/utils/holidayUtils"; | |||
import { | |||
INPUT_DATE_FORMAT, | |||
convertDateArrayToString, | |||
} from "@/app/utils/formatUtil"; | |||
import StyledFullCalendar from "../StyledFullCalendar"; | |||
import { ProjectWithTasks } from "@/app/api/projects"; | |||
import { TimeEntry, saveMemberEntry } from "@/app/api/timesheets/actions"; | |||
import TimesheetEditModal, { | |||
Props as TimesheetEditModalProps, | |||
} from "../TimesheetTable/TimesheetEditModal"; | |||
export interface Props { | |||
teamTimesheets: TeamTimeSheets; | |||
companyHolidays: HolidaysResult[]; | |||
allProjects: ProjectWithTasks[]; | |||
} | |||
type MemberOption = TeamTimeSheets[0] & { id: string }; | |||
interface EventClickArg { | |||
event: { | |||
start: Date | null; | |||
extendedProps: { | |||
calendar?: string; | |||
entry?: TimeEntry; | |||
memberId?: string; | |||
}; | |||
}; | |||
} | |||
const TimesheetAmendment: React.FC<Props> = ({ | |||
teamTimesheets, | |||
companyHolidays, | |||
allProjects, | |||
}) => { | |||
const { t } = useTranslation(["home", "common"]); | |||
const theme = useTheme(); | |||
const projectMap = useMemo(() => { | |||
return allProjects.reduce<{ | |||
[id: ProjectWithTasks["id"]]: ProjectWithTasks; | |||
}>((acc, project) => { | |||
return { ...acc, [project.id]: project }; | |||
}, {}); | |||
}, [allProjects]); | |||
// member select | |||
const allMembers = useMemo(() => { | |||
return transform<TeamTimeSheets[0], MemberOption[]>( | |||
teamTimesheets, | |||
(acc, memberTimesheet, id) => { | |||
return acc.push({ | |||
...memberTimesheet, | |||
id, | |||
}); | |||
}, | |||
[], | |||
); | |||
}, [teamTimesheets]); | |||
const [selectedStaff, setSelectedStaff] = useState<MemberOption>( | |||
allMembers[0], | |||
); | |||
// edit modal related | |||
const [editModalProps, setEditModalProps] = useState< | |||
Partial<TimesheetEditModalProps> | |||
>({}); | |||
const [editModalOpen, setEditModalOpen] = useState(false); | |||
const openEditModal = useCallback((defaultValues?: TimeEntry) => { | |||
setEditModalProps({ | |||
defaultValues, | |||
}); | |||
setEditModalOpen(true); | |||
}, []); | |||
const closeEditModal = useCallback(() => { | |||
setEditModalOpen(false); | |||
}, []); | |||
// calendar related | |||
const holidays = useMemo(() => { | |||
return [ | |||
...getPublicHolidaysForNYears(2), | |||
...companyHolidays.map((h) => ({ | |||
title: h.name, | |||
date: convertDateArrayToString(h.date, INPUT_DATE_FORMAT), | |||
extendedProps: { | |||
calender: "holiday", | |||
}, | |||
})), | |||
].map((e) => ({ | |||
...e, | |||
backgroundColor: theme.palette.error.main, | |||
borderColor: theme.palette.error.main, | |||
})); | |||
}, [companyHolidays, theme.palette.error.main]); | |||
const timeEntries = useMemo( | |||
() => | |||
Object.keys(selectedStaff.timeEntries).flatMap((date, index) => { | |||
return selectedStaff.timeEntries[date].map((entry) => ({ | |||
id: `${date}-${index}-entry-${entry.id}`, | |||
date, | |||
title: `${t("{{count}} hour", { | |||
ns: "common", | |||
count: (entry.inputHours || 0) + (entry.otHours || 0), | |||
})} (${ | |||
entry.projectId | |||
? projectMap[entry.projectId].code | |||
: t("Non-billable task") | |||
})`, | |||
backgroundColor: theme.palette.info.main, | |||
borderColor: theme.palette.info.main, | |||
extendedProps: { | |||
calendar: "timeEntry", | |||
entry, | |||
memberId: selectedStaff.id, | |||
}, | |||
})); | |||
}), | |||
[projectMap, selectedStaff, t, theme], | |||
); | |||
const handleEventClick = useCallback( | |||
({ event }: EventClickArg) => { | |||
if ( | |||
event.extendedProps.calendar === "timeEntry" && | |||
event.extendedProps.entry | |||
) { | |||
openEditModal(event.extendedProps.entry); | |||
} | |||
}, | |||
[openEditModal], | |||
); | |||
const handleSave = useCallback( | |||
async (timeEntry: TimeEntry) => { | |||
// TODO: should be fine, but can handle parse error | |||
const intStaffId = parseInt(selectedStaff.id); | |||
await saveMemberEntry({ staffId: intStaffId, entry: timeEntry }); | |||
setEditModalOpen(false); | |||
}, | |||
[selectedStaff.id], | |||
); | |||
return ( | |||
<Stack spacing={2}> | |||
<Autocomplete | |||
sx={{ maxWidth: 400 }} | |||
noOptionsText={t("No team members")} | |||
value={selectedStaff} | |||
onChange={(_, value) => { | |||
if (value) setSelectedStaff(value); | |||
}} | |||
options={allMembers} | |||
isOptionEqualToValue={(option, value) => option.id === value.id} | |||
getOptionLabel={(option) => `${option.staffId} - ${option.name}`} | |||
renderInput={(params) => <TextField {...params} />} | |||
/> | |||
<StyledFullCalendar | |||
plugins={[dayGridPlugin]} | |||
initialView="dayGridMonth" | |||
buttonText={{ today: t("Today") }} | |||
events={[...holidays, ...timeEntries]} | |||
eventClick={handleEventClick} | |||
/> | |||
<TimesheetEditModal | |||
modalSx={{ maxWidth: 400 }} | |||
allProjects={allProjects} | |||
assignedProjects={[]} | |||
open={editModalOpen} | |||
onClose={closeEditModal} | |||
onSave={handleSave} | |||
{...editModalProps} | |||
/> | |||
</Stack> | |||
); | |||
}; | |||
export default TimesheetAmendment; |
@@ -0,0 +1,77 @@ | |||
import useIsMobile from "@/app/utils/useIsMobile"; | |||
import React from "react"; | |||
import FullscreenModal from "../FullscreenModal"; | |||
import { | |||
Box, | |||
Card, | |||
CardContent, | |||
Dialog, | |||
DialogContent, | |||
DialogTitle, | |||
Modal, | |||
SxProps, | |||
Typography, | |||
} from "@mui/material"; | |||
import { useTranslation } from "react-i18next"; | |||
import TimesheetAmendment, { | |||
Props as TimesheetAmendmentProps, | |||
} from "./TimesheetAmendment"; | |||
const modalSx: SxProps = { | |||
position: "absolute", | |||
top: "50%", | |||
left: "50%", | |||
transform: "translate(-50%, -50%)", | |||
width: { xs: "calc(100% - 2rem)", sm: "90%" }, | |||
maxHeight: "90%", | |||
maxWidth: 1400, | |||
}; | |||
interface Props extends TimesheetAmendmentProps { | |||
open: boolean; | |||
onClose: () => void; | |||
} | |||
export const TimesheetAmendmentModal: React.FC<Props> = ({ | |||
open, | |||
onClose, | |||
teamTimesheets, | |||
companyHolidays, | |||
allProjects, | |||
}) => { | |||
const { t } = useTranslation("home"); | |||
const isMobile = useIsMobile(); | |||
const title = t("Timesheet Amendment"); | |||
const content = ( | |||
<TimesheetAmendment | |||
companyHolidays={companyHolidays} | |||
teamTimesheets={teamTimesheets} | |||
allProjects={allProjects} | |||
/> | |||
); | |||
return isMobile ? ( | |||
<FullscreenModal open={open} onClose={onClose} closeModal={onClose}> | |||
<Box display="flex" flexDirection="column" gap={2} height="100%"> | |||
<Typography variant="h6" flex="none" padding={2}> | |||
{title} | |||
</Typography> | |||
<Box paddingInline={2}>{content}</Box> | |||
</Box> | |||
</FullscreenModal> | |||
) : ( | |||
<Modal open={open} onClose={onClose}> | |||
<Card sx={modalSx}> | |||
<CardContent> | |||
<Typography variant="overline" display="block" marginBlockEnd={1}> | |||
{title} | |||
</Typography> | |||
<Box maxHeight={900} overflow="scroll"> | |||
{content} | |||
</Box> | |||
</CardContent> | |||
</Card> | |||
</Modal> | |||
); | |||
}; |
@@ -0,0 +1 @@ | |||
export { default } from "./TimesheetAmendment"; |
@@ -215,7 +215,7 @@ const EntryInputTable: React.FC<Props> = ({ | |||
width: 300, | |||
editable: true, | |||
valueFormatter(params) { | |||
const project = assignedProjects.find((p) => p.id === params.value); | |||
const project = allProjects.find((p) => p.id === params.value); | |||
return project ? `${project.code} - ${project.name}` : t("None"); | |||
}, | |||
renderEditCell(params: GridRenderEditCellParams<TimeEntryRow, number>) { | |||
@@ -295,7 +295,7 @@ const EntryInputTable: React.FC<Props> = ({ | |||
: undefined; | |||
const task = projectId | |||
? assignedProjects | |||
? allProjects | |||
.find((p) => p.id === projectId) | |||
?.tasks.find((t) => t.id === params.value) | |||
: undefined; | |||
@@ -84,7 +84,7 @@ const MobileTimesheetEntry: React.FC<Props> = ({ | |||
}, []); | |||
const onSaveEntry = useCallback( | |||
(entry: TimeEntry) => { | |||
async (entry: TimeEntry) => { | |||
const existingEntry = currentEntries.find((e) => e.id === entry.id); | |||
if (existingEntry) { | |||
setValue( | |||
@@ -102,76 +102,4 @@ const AutocompleteProjectSelect: React.FC<Props> = ({ | |||
); | |||
}; | |||
// const ProjectSelect: React.FC<Props> = ({ | |||
// allProjects, | |||
// assignedProjects, | |||
// value, | |||
// onProjectSelect, | |||
// }) => { | |||
// const { t } = useTranslation("home"); | |||
// const nonAssignedProjects = useMemo(() => { | |||
// return differenceBy(allProjects, assignedProjects, "id"); | |||
// }, [allProjects, assignedProjects]); | |||
// const onChange = useCallback( | |||
// (event: SelectChangeEvent<number>) => { | |||
// const newValue = event.target.value; | |||
// onProjectSelect(newValue); | |||
// }, | |||
// [onProjectSelect], | |||
// ); | |||
// return ( | |||
// <Select | |||
// displayEmpty | |||
// value={value || ""} | |||
// onChange={onChange} | |||
// sx={{ width: "100%" }} | |||
// MenuProps={{ | |||
// slotProps: { | |||
// paper: { | |||
// sx: { maxHeight: 400 }, | |||
// }, | |||
// }, | |||
// anchorOrigin: { | |||
// vertical: "bottom", | |||
// horizontal: "left", | |||
// }, | |||
// transformOrigin: { | |||
// vertical: "top", | |||
// horizontal: "left", | |||
// }, | |||
// }} | |||
// > | |||
// <ListSubheader>{t("Non-billable")}</ListSubheader> | |||
// <MenuItem value={""}>{t("None")}</MenuItem> | |||
// {assignedProjects.length > 0 && [ | |||
// <ListSubheader key="assignedProjectsSubHeader"> | |||
// {t("Assigned Projects")} | |||
// </ListSubheader>, | |||
// ...assignedProjects.map((project) => ( | |||
// <MenuItem | |||
// key={project.id} | |||
// value={project.id} | |||
// sx={{ whiteSpace: "wrap" }} | |||
// >{`${project.code} - ${project.name}`}</MenuItem> | |||
// )), | |||
// ]} | |||
// {nonAssignedProjects.length > 0 && [ | |||
// <ListSubheader key="nonAssignedProjectsSubHeader"> | |||
// {t("Non-assigned Projects")} | |||
// </ListSubheader>, | |||
// ...nonAssignedProjects.map((project) => ( | |||
// <MenuItem | |||
// key={project.id} | |||
// value={project.id} | |||
// sx={{ whiteSpace: "wrap" }} | |||
// >{`${project.code} - ${project.name}`}</MenuItem> | |||
// )), | |||
// ]} | |||
// </Select> | |||
// ); | |||
// }; | |||
export default AutocompleteProjectSelect; |
@@ -23,11 +23,12 @@ import uniqBy from "lodash/uniqBy"; | |||
import { roundToNearestQuarter } from "@/app/utils/manhourUtils"; | |||
export interface Props extends Omit<ModalProps, "children"> { | |||
onSave: (leaveEntry: TimeEntry) => void; | |||
onSave: (timeEntry: TimeEntry) => Promise<void>; | |||
onDelete?: () => void; | |||
defaultValues?: Partial<TimeEntry>; | |||
allProjects: ProjectWithTasks[]; | |||
assignedProjects: AssignedProject[]; | |||
modalSx?: SxProps; | |||
} | |||
const modalSx: SxProps = { | |||
@@ -50,6 +51,7 @@ const TimesheetEditModal: React.FC<Props> = ({ | |||
defaultValues, | |||
allProjects, | |||
assignedProjects, | |||
modalSx: mSx, | |||
}) => { | |||
const { t } = useTranslation("home"); | |||
@@ -110,7 +112,7 @@ const TimesheetEditModal: React.FC<Props> = ({ | |||
return ( | |||
<Modal open={open} onClose={closeHandler}> | |||
<Paper sx={modalSx}> | |||
<Paper sx={{ ...modalSx, ...mSx }}> | |||
<FormControl fullWidth> | |||
<InputLabel shrink>{t("Project Code and Name")}</InputLabel> | |||
<Controller | |||
@@ -1,12 +1,16 @@ | |||
"use client"; | |||
import { useCallback, useState } from "react"; | |||
import React, { useCallback, useState } from "react"; | |||
import { useTranslation } from "react-i18next"; | |||
import Button from "@mui/material/Button"; | |||
import Stack from "@mui/material/Stack"; | |||
import { Add } from "@mui/icons-material"; | |||
import { Box, Typography } from "@mui/material"; | |||
import ButtonGroup from "@mui/material/ButtonGroup"; | |||
import { | |||
CalendarMonth, | |||
EditCalendar, | |||
Luggage, | |||
MoreTime, | |||
} from "@mui/icons-material"; | |||
import { Menu, MenuItem, SxProps, Typography } from "@mui/material"; | |||
import AssignedProjects from "./AssignedProjects"; | |||
import TimesheetModal from "../TimesheetModal"; | |||
import { AssignedProject, ProjectWithTasks } from "@/app/api/projects"; | |||
@@ -15,10 +19,11 @@ import { | |||
RecordTimesheetInput, | |||
} from "@/app/api/timesheets/actions"; | |||
import LeaveModal from "../LeaveModal"; | |||
import { LeaveType } from "@/app/api/timesheets"; | |||
import { LeaveType, TeamTimeSheets } from "@/app/api/timesheets"; | |||
import { CalendarIcon } from "@mui/x-date-pickers"; | |||
import PastEntryCalendarModal from "../PastEntryCalendar/PastEntryCalendarModal"; | |||
import { HolidaysResult } from "@/app/api/holidays"; | |||
import { TimesheetAmendmentModal } from "../TimesheetAmendment/TimesheetAmendmentModal"; | |||
export interface Props { | |||
leaveTypes: LeaveType[]; | |||
@@ -28,8 +33,14 @@ export interface Props { | |||
defaultLeaveRecords: RecordLeaveInput; | |||
defaultTimesheets: RecordTimesheetInput; | |||
holidays: HolidaysResult[]; | |||
teamTimesheets: TeamTimeSheets; | |||
} | |||
const menuItemSx: SxProps = { | |||
gap: 1, | |||
color: "neutral.700", | |||
}; | |||
const UserWorkspacePage: React.FC<Props> = ({ | |||
leaveTypes, | |||
allProjects, | |||
@@ -38,13 +49,31 @@ const UserWorkspacePage: React.FC<Props> = ({ | |||
defaultLeaveRecords, | |||
defaultTimesheets, | |||
holidays, | |||
teamTimesheets, | |||
}) => { | |||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); | |||
const [isTimeheetModalVisible, setTimeheetModalVisible] = useState(false); | |||
const [isLeaveModalVisible, setLeaveModalVisible] = useState(false); | |||
const [isPastEventModalVisible, setPastEventModalVisible] = useState(false); | |||
const [isTimesheetAmendmentVisible, setisTimesheetAmendmentVisible] = | |||
useState(false); | |||
const { t } = useTranslation("home"); | |||
const showTimesheetAmendment = Object.keys(teamTimesheets).length > 0; | |||
const handleOpenActionMenu = useCallback< | |||
React.MouseEventHandler<HTMLButtonElement> | |||
>((event) => { | |||
setAnchorEl(event.currentTarget); | |||
}, []); | |||
const handleCloseActionMenu = useCallback(() => { | |||
setAnchorEl(null); | |||
}, []); | |||
const handleAddTimesheetButtonClick = useCallback(() => { | |||
setAnchorEl(null); | |||
setTimeheetModalVisible(true); | |||
}, []); | |||
@@ -53,6 +82,7 @@ const UserWorkspacePage: React.FC<Props> = ({ | |||
}, []); | |||
const handleAddLeaveButtonClick = useCallback(() => { | |||
setAnchorEl(null); | |||
setLeaveModalVisible(true); | |||
}, []); | |||
@@ -61,6 +91,7 @@ const UserWorkspacePage: React.FC<Props> = ({ | |||
}, []); | |||
const handlePastEventClick = useCallback(() => { | |||
setAnchorEl(null); | |||
setPastEventModalVisible(true); | |||
}, []); | |||
@@ -68,6 +99,15 @@ const UserWorkspacePage: React.FC<Props> = ({ | |||
setPastEventModalVisible(false); | |||
}, []); | |||
const handleAmendmentClick = useCallback(() => { | |||
setAnchorEl(null); | |||
setisTimesheetAmendmentVisible(true); | |||
}, []); | |||
const handleAmendmentClose = useCallback(() => { | |||
setisTimesheetAmendmentVisible(false); | |||
}, []); | |||
return ( | |||
<> | |||
<Stack | |||
@@ -79,24 +119,46 @@ const UserWorkspacePage: React.FC<Props> = ({ | |||
<Typography variant="h4" marginInlineEnd={2}> | |||
{t("User Workspace")} | |||
</Typography> | |||
<Box display="flex" flexWrap="wrap" gap={2}> | |||
<Button | |||
startIcon={<CalendarIcon />} | |||
variant="outlined" | |||
onClick={handlePastEventClick} | |||
> | |||
{t("View Past Entries")} | |||
</Button> | |||
<ButtonGroup variant="contained"> | |||
<Button startIcon={<Add />} onClick={handleAddTimesheetButtonClick}> | |||
{t("Enter Time")} | |||
</Button> | |||
<Button startIcon={<Add />} onClick={handleAddLeaveButtonClick}> | |||
{t("Record Leave")} | |||
</Button> | |||
</ButtonGroup> | |||
</Box> | |||
<Button | |||
startIcon={<CalendarIcon />} | |||
variant="contained" | |||
onClick={handleOpenActionMenu} | |||
> | |||
{t("Timesheet Actions")} | |||
</Button> | |||
</Stack> | |||
<Menu | |||
anchorEl={anchorEl} | |||
open={Boolean(anchorEl)} | |||
onClose={handleCloseActionMenu} | |||
anchorOrigin={{ | |||
vertical: "bottom", | |||
horizontal: "right", | |||
}} | |||
transformOrigin={{ | |||
vertical: "top", | |||
horizontal: "right", | |||
}} | |||
> | |||
<MenuItem onClick={handleAddTimesheetButtonClick} sx={menuItemSx}> | |||
<MoreTime /> | |||
{t("Enter Time")} | |||
</MenuItem> | |||
<MenuItem onClick={handleAddLeaveButtonClick} sx={menuItemSx}> | |||
<Luggage /> | |||
{t("Record Leave")} | |||
</MenuItem> | |||
<MenuItem onClick={handlePastEventClick} sx={menuItemSx}> | |||
<CalendarMonth /> | |||
{t("View Past Entries")} | |||
</MenuItem> | |||
{showTimesheetAmendment && ( | |||
<MenuItem onClick={handleAmendmentClick} sx={menuItemSx}> | |||
<EditCalendar /> | |||
{t("Timesheet Amendment")} | |||
</MenuItem> | |||
)} | |||
</Menu> | |||
<PastEntryCalendarModal | |||
open={isPastEventModalVisible} | |||
handleClose={handlePastEventClose} | |||
@@ -131,6 +193,15 @@ const UserWorkspacePage: React.FC<Props> = ({ | |||
{t("You have no assigned projects!")} | |||
</Typography> | |||
)} | |||
{showTimesheetAmendment && ( | |||
<TimesheetAmendmentModal | |||
allProjects={allProjects} | |||
companyHolidays={holidays} | |||
teamTimesheets={teamTimesheets} | |||
open={isTimesheetAmendmentVisible} | |||
onClose={handleAmendmentClose} | |||
/> | |||
)} | |||
</> | |||
); | |||
}; | |||
@@ -6,6 +6,7 @@ import UserWorkspacePage from "./UserWorkspacePage"; | |||
import { | |||
fetchLeaveTypes, | |||
fetchLeaves, | |||
fetchTeamMemberTimesheets, | |||
fetchTimesheets, | |||
} from "@/app/api/timesheets"; | |||
import { fetchHolidays } from "@/app/api/holidays"; | |||
@@ -16,6 +17,7 @@ interface Props { | |||
const UserWorkspaceWrapper: React.FC<Props> = async ({ username }) => { | |||
const [ | |||
teamTimesheets, | |||
assignedProjects, | |||
allProjects, | |||
timesheets, | |||
@@ -23,6 +25,7 @@ const UserWorkspaceWrapper: React.FC<Props> = async ({ username }) => { | |||
leaveTypes, | |||
holidays, | |||
] = await Promise.all([ | |||
fetchTeamMemberTimesheets(username), | |||
fetchAssignedProjects(username), | |||
fetchProjectWithTasks(), | |||
fetchTimesheets(username), | |||
@@ -33,6 +36,7 @@ const UserWorkspaceWrapper: React.FC<Props> = async ({ username }) => { | |||
return ( | |||
<UserWorkspacePage | |||
teamTimesheets={teamTimesheets} | |||
allProjects={allProjects} | |||
assignedProjects={assignedProjects} | |||
username={username} | |||
@@ -1,6 +1,9 @@ | |||
{ | |||
"Grade {{grade}}": "Grade {{grade}}", | |||
"{{count}} hour_one": "{{count}} hour", | |||
"{{count}} hour_other": "{{count}} hours", | |||
"All": "All", | |||
"Petty Cash": "Petty Cash", | |||