@@ -4,6 +4,7 @@ import UserWorkspacePage from "@/components/UserWorkspacePage"; | |||||
import { | import { | ||||
fetchLeaveTypes, | fetchLeaveTypes, | ||||
fetchLeaves, | fetchLeaves, | ||||
fetchTeamMemberTimesheets, | |||||
fetchTimesheets, | fetchTimesheets, | ||||
} from "@/app/api/timesheets"; | } from "@/app/api/timesheets"; | ||||
import { authOptions } from "@/config/authConfig"; | import { authOptions } from "@/config/authConfig"; | ||||
@@ -29,9 +30,10 @@ const Home: React.FC = async () => { | |||||
fetchLeaveTypes(); | fetchLeaveTypes(); | ||||
fetchProjectWithTasks(); | fetchProjectWithTasks(); | ||||
fetchHolidays(); | fetchHolidays(); | ||||
fetchTeamMemberTimesheets(username); | |||||
return ( | return ( | ||||
<I18nProvider namespaces={["home"]}> | |||||
<I18nProvider namespaces={["home", "common"]}> | |||||
<UserWorkspacePage username={username} /> | <UserWorkspacePage username={username} /> | ||||
</I18nProvider> | </I18nProvider> | ||||
); | ); | ||||
@@ -63,3 +63,14 @@ export const saveLeave = async (data: RecordLeaveInput, username: string) => { | |||||
return savedRecords; | 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; | name: string; | ||||
} | } | ||||
export type TeamTimeSheets = { | |||||
[memberId: number]: { | |||||
timeEntries: RecordTimesheetInput; | |||||
staffId: string; | |||||
name: string; | |||||
}; | |||||
}; | |||||
export const fetchTimesheets = cache(async (username: string) => { | export const fetchTimesheets = cache(async (username: string) => { | ||||
return serverFetchJson<RecordTimesheetInput>(`${BASE_API_URL}/timesheets`, { | return serverFetchJson<RecordTimesheetInput>(`${BASE_API_URL}/timesheets`, { | ||||
next: { tags: [`timesheets_${username}`] }, | next: { tags: [`timesheets_${username}`] }, | ||||
@@ -28,3 +36,12 @@ export const fetchLeaveTypes = cache(async () => { | |||||
next: { tags: ["leaveTypes"] }, | 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, | width: 300, | ||||
editable: true, | editable: true, | ||||
valueFormatter(params) { | 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"); | return project ? `${project.code} - ${project.name}` : t("None"); | ||||
}, | }, | ||||
renderEditCell(params: GridRenderEditCellParams<TimeEntryRow, number>) { | renderEditCell(params: GridRenderEditCellParams<TimeEntryRow, number>) { | ||||
@@ -295,7 +295,7 @@ const EntryInputTable: React.FC<Props> = ({ | |||||
: undefined; | : undefined; | ||||
const task = projectId | const task = projectId | ||||
? assignedProjects | |||||
? allProjects | |||||
.find((p) => p.id === projectId) | .find((p) => p.id === projectId) | ||||
?.tasks.find((t) => t.id === params.value) | ?.tasks.find((t) => t.id === params.value) | ||||
: undefined; | : undefined; | ||||
@@ -84,7 +84,7 @@ const MobileTimesheetEntry: React.FC<Props> = ({ | |||||
}, []); | }, []); | ||||
const onSaveEntry = useCallback( | const onSaveEntry = useCallback( | ||||
(entry: TimeEntry) => { | |||||
async (entry: TimeEntry) => { | |||||
const existingEntry = currentEntries.find((e) => e.id === entry.id); | const existingEntry = currentEntries.find((e) => e.id === entry.id); | ||||
if (existingEntry) { | if (existingEntry) { | ||||
setValue( | 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; | export default AutocompleteProjectSelect; |
@@ -23,11 +23,12 @@ import uniqBy from "lodash/uniqBy"; | |||||
import { roundToNearestQuarter } from "@/app/utils/manhourUtils"; | import { roundToNearestQuarter } from "@/app/utils/manhourUtils"; | ||||
export interface Props extends Omit<ModalProps, "children"> { | export interface Props extends Omit<ModalProps, "children"> { | ||||
onSave: (leaveEntry: TimeEntry) => void; | |||||
onSave: (timeEntry: TimeEntry) => Promise<void>; | |||||
onDelete?: () => void; | onDelete?: () => void; | ||||
defaultValues?: Partial<TimeEntry>; | defaultValues?: Partial<TimeEntry>; | ||||
allProjects: ProjectWithTasks[]; | allProjects: ProjectWithTasks[]; | ||||
assignedProjects: AssignedProject[]; | assignedProjects: AssignedProject[]; | ||||
modalSx?: SxProps; | |||||
} | } | ||||
const modalSx: SxProps = { | const modalSx: SxProps = { | ||||
@@ -50,6 +51,7 @@ const TimesheetEditModal: React.FC<Props> = ({ | |||||
defaultValues, | defaultValues, | ||||
allProjects, | allProjects, | ||||
assignedProjects, | assignedProjects, | ||||
modalSx: mSx, | |||||
}) => { | }) => { | ||||
const { t } = useTranslation("home"); | const { t } = useTranslation("home"); | ||||
@@ -110,7 +112,7 @@ const TimesheetEditModal: React.FC<Props> = ({ | |||||
return ( | return ( | ||||
<Modal open={open} onClose={closeHandler}> | <Modal open={open} onClose={closeHandler}> | ||||
<Paper sx={modalSx}> | |||||
<Paper sx={{ ...modalSx, ...mSx }}> | |||||
<FormControl fullWidth> | <FormControl fullWidth> | ||||
<InputLabel shrink>{t("Project Code and Name")}</InputLabel> | <InputLabel shrink>{t("Project Code and Name")}</InputLabel> | ||||
<Controller | <Controller | ||||
@@ -1,12 +1,16 @@ | |||||
"use client"; | "use client"; | ||||
import { useCallback, useState } from "react"; | |||||
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 { 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 AssignedProjects from "./AssignedProjects"; | ||||
import TimesheetModal from "../TimesheetModal"; | import TimesheetModal from "../TimesheetModal"; | ||||
import { AssignedProject, ProjectWithTasks } from "@/app/api/projects"; | import { AssignedProject, ProjectWithTasks } from "@/app/api/projects"; | ||||
@@ -15,10 +19,11 @@ import { | |||||
RecordTimesheetInput, | RecordTimesheetInput, | ||||
} from "@/app/api/timesheets/actions"; | } from "@/app/api/timesheets/actions"; | ||||
import LeaveModal from "../LeaveModal"; | 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 { CalendarIcon } from "@mui/x-date-pickers"; | ||||
import PastEntryCalendarModal from "../PastEntryCalendar/PastEntryCalendarModal"; | import PastEntryCalendarModal from "../PastEntryCalendar/PastEntryCalendarModal"; | ||||
import { HolidaysResult } from "@/app/api/holidays"; | import { HolidaysResult } from "@/app/api/holidays"; | ||||
import { TimesheetAmendmentModal } from "../TimesheetAmendment/TimesheetAmendmentModal"; | |||||
export interface Props { | export interface Props { | ||||
leaveTypes: LeaveType[]; | leaveTypes: LeaveType[]; | ||||
@@ -28,8 +33,14 @@ export interface Props { | |||||
defaultLeaveRecords: RecordLeaveInput; | defaultLeaveRecords: RecordLeaveInput; | ||||
defaultTimesheets: RecordTimesheetInput; | defaultTimesheets: RecordTimesheetInput; | ||||
holidays: HolidaysResult[]; | holidays: HolidaysResult[]; | ||||
teamTimesheets: TeamTimeSheets; | |||||
} | } | ||||
const menuItemSx: SxProps = { | |||||
gap: 1, | |||||
color: "neutral.700", | |||||
}; | |||||
const UserWorkspacePage: React.FC<Props> = ({ | const UserWorkspacePage: React.FC<Props> = ({ | ||||
leaveTypes, | leaveTypes, | ||||
allProjects, | allProjects, | ||||
@@ -38,13 +49,31 @@ const UserWorkspacePage: React.FC<Props> = ({ | |||||
defaultLeaveRecords, | defaultLeaveRecords, | ||||
defaultTimesheets, | defaultTimesheets, | ||||
holidays, | holidays, | ||||
teamTimesheets, | |||||
}) => { | }) => { | ||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); | |||||
const [isTimeheetModalVisible, setTimeheetModalVisible] = useState(false); | const [isTimeheetModalVisible, setTimeheetModalVisible] = useState(false); | ||||
const [isLeaveModalVisible, setLeaveModalVisible] = useState(false); | const [isLeaveModalVisible, setLeaveModalVisible] = useState(false); | ||||
const [isPastEventModalVisible, setPastEventModalVisible] = useState(false); | const [isPastEventModalVisible, setPastEventModalVisible] = useState(false); | ||||
const [isTimesheetAmendmentVisible, setisTimesheetAmendmentVisible] = | |||||
useState(false); | |||||
const { t } = useTranslation("home"); | 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(() => { | const handleAddTimesheetButtonClick = useCallback(() => { | ||||
setAnchorEl(null); | |||||
setTimeheetModalVisible(true); | setTimeheetModalVisible(true); | ||||
}, []); | }, []); | ||||
@@ -53,6 +82,7 @@ const UserWorkspacePage: React.FC<Props> = ({ | |||||
}, []); | }, []); | ||||
const handleAddLeaveButtonClick = useCallback(() => { | const handleAddLeaveButtonClick = useCallback(() => { | ||||
setAnchorEl(null); | |||||
setLeaveModalVisible(true); | setLeaveModalVisible(true); | ||||
}, []); | }, []); | ||||
@@ -61,6 +91,7 @@ const UserWorkspacePage: React.FC<Props> = ({ | |||||
}, []); | }, []); | ||||
const handlePastEventClick = useCallback(() => { | const handlePastEventClick = useCallback(() => { | ||||
setAnchorEl(null); | |||||
setPastEventModalVisible(true); | setPastEventModalVisible(true); | ||||
}, []); | }, []); | ||||
@@ -68,6 +99,15 @@ const UserWorkspacePage: React.FC<Props> = ({ | |||||
setPastEventModalVisible(false); | setPastEventModalVisible(false); | ||||
}, []); | }, []); | ||||
const handleAmendmentClick = useCallback(() => { | |||||
setAnchorEl(null); | |||||
setisTimesheetAmendmentVisible(true); | |||||
}, []); | |||||
const handleAmendmentClose = useCallback(() => { | |||||
setisTimesheetAmendmentVisible(false); | |||||
}, []); | |||||
return ( | return ( | ||||
<> | <> | ||||
<Stack | <Stack | ||||
@@ -79,24 +119,46 @@ const UserWorkspacePage: React.FC<Props> = ({ | |||||
<Typography variant="h4" marginInlineEnd={2}> | <Typography variant="h4" marginInlineEnd={2}> | ||||
{t("User Workspace")} | {t("User Workspace")} | ||||
</Typography> | </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> | </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 | <PastEntryCalendarModal | ||||
open={isPastEventModalVisible} | open={isPastEventModalVisible} | ||||
handleClose={handlePastEventClose} | handleClose={handlePastEventClose} | ||||
@@ -131,6 +193,15 @@ const UserWorkspacePage: React.FC<Props> = ({ | |||||
{t("You have no assigned projects!")} | {t("You have no assigned projects!")} | ||||
</Typography> | </Typography> | ||||
)} | )} | ||||
{showTimesheetAmendment && ( | |||||
<TimesheetAmendmentModal | |||||
allProjects={allProjects} | |||||
companyHolidays={holidays} | |||||
teamTimesheets={teamTimesheets} | |||||
open={isTimesheetAmendmentVisible} | |||||
onClose={handleAmendmentClose} | |||||
/> | |||||
)} | |||||
</> | </> | ||||
); | ); | ||||
}; | }; | ||||
@@ -6,6 +6,7 @@ import UserWorkspacePage from "./UserWorkspacePage"; | |||||
import { | import { | ||||
fetchLeaveTypes, | fetchLeaveTypes, | ||||
fetchLeaves, | fetchLeaves, | ||||
fetchTeamMemberTimesheets, | |||||
fetchTimesheets, | fetchTimesheets, | ||||
} from "@/app/api/timesheets"; | } from "@/app/api/timesheets"; | ||||
import { fetchHolidays } from "@/app/api/holidays"; | import { fetchHolidays } from "@/app/api/holidays"; | ||||
@@ -16,6 +17,7 @@ interface Props { | |||||
const UserWorkspaceWrapper: React.FC<Props> = async ({ username }) => { | const UserWorkspaceWrapper: React.FC<Props> = async ({ username }) => { | ||||
const [ | const [ | ||||
teamTimesheets, | |||||
assignedProjects, | assignedProjects, | ||||
allProjects, | allProjects, | ||||
timesheets, | timesheets, | ||||
@@ -23,6 +25,7 @@ const UserWorkspaceWrapper: React.FC<Props> = async ({ username }) => { | |||||
leaveTypes, | leaveTypes, | ||||
holidays, | holidays, | ||||
] = await Promise.all([ | ] = await Promise.all([ | ||||
fetchTeamMemberTimesheets(username), | |||||
fetchAssignedProjects(username), | fetchAssignedProjects(username), | ||||
fetchProjectWithTasks(), | fetchProjectWithTasks(), | ||||
fetchTimesheets(username), | fetchTimesheets(username), | ||||
@@ -33,6 +36,7 @@ const UserWorkspaceWrapper: React.FC<Props> = async ({ username }) => { | |||||
return ( | return ( | ||||
<UserWorkspacePage | <UserWorkspacePage | ||||
teamTimesheets={teamTimesheets} | |||||
allProjects={allProjects} | allProjects={allProjects} | ||||
assignedProjects={assignedProjects} | assignedProjects={assignedProjects} | ||||
username={username} | username={username} | ||||
@@ -1,6 +1,9 @@ | |||||
{ | { | ||||
"Grade {{grade}}": "Grade {{grade}}", | "Grade {{grade}}": "Grade {{grade}}", | ||||
"{{count}} hour_one": "{{count}} hour", | |||||
"{{count}} hour_other": "{{count}} hours", | |||||
"All": "All", | "All": "All", | ||||
"Petty Cash": "Petty Cash", | "Petty Cash": "Petty Cash", | ||||