| @@ -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", | |||