diff --git a/src/frontend/src/pages/HomePage/AdminHomePage.tsx b/src/frontend/src/pages/HomePage/AdminHomePage.tsx index 39f44432d7..4dc62858e0 100644 --- a/src/frontend/src/pages/HomePage/AdminHomePage.tsx +++ b/src/frontend/src/pages/HomePage/AdminHomePage.tsx @@ -3,12 +3,13 @@ * See the LICENSE file in the repository root folder for details. */ -import { Grid, Typography } from '@mui/material'; +import { Typography, Grid } from '@mui/material'; import { useSingleUserSettings } from '../../hooks/users.hooks'; import LoadingIndicator from '../../components/LoadingIndicator'; import ErrorPage from '../ErrorPage'; import PageLayout, { PAGE_GRID_HEIGHT } from '../../components/PageLayout'; import { AuthenticatedUser } from 'shared'; +import WorkPackagesSelectionView from './components/WorkPackagesSelectionView'; import ChangeRequestsToReview from './components/ChangeRequestsToReview'; interface AdminHomePageProps { @@ -26,10 +27,13 @@ const AdminHomePage = ({ user }: AdminHomePageProps) => { Welcome, {user.firstName}! - - + + + + + ); diff --git a/src/frontend/src/pages/HomePage/components/ChangeRequestsToReview.tsx b/src/frontend/src/pages/HomePage/components/ChangeRequestsToReview.tsx index a003cf636b..e7161db966 100644 --- a/src/frontend/src/pages/HomePage/components/ChangeRequestsToReview.tsx +++ b/src/frontend/src/pages/HomePage/components/ChangeRequestsToReview.tsx @@ -19,7 +19,7 @@ const NoChangeRequestsToReview: React.FC = () => { } heading={`You're all caught up!`} - message={'Uou have no unreviewed changre requests!'} + message={'You have no unreviewed changre requests!'} /> ); }; diff --git a/src/frontend/src/pages/HomePage/components/WorkPackageSelect.tsx b/src/frontend/src/pages/HomePage/components/WorkPackageSelect.tsx new file mode 100644 index 0000000000..996a28cb0c --- /dev/null +++ b/src/frontend/src/pages/HomePage/components/WorkPackageSelect.tsx @@ -0,0 +1,91 @@ +import { Box, Card, Typography, useTheme } from '@mui/material'; +import { useEffect, useRef, useState } from 'react'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; + +interface CustomSelectProps { + options: string[]; + onSelect: (selectedOption: number) => void; + selected?: number; +} + +const WorkPackageSelect: React.FC = ({ options, onSelect, selected = 0 }) => { + const theme = useTheme(); + const [isOpen, setIsOpen] = useState(false); + const dropdownRef = useRef(null); + + const handleSelect = (option: string) => { + setIsOpen(false); + onSelect(options.indexOf(option)); + }; + + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + }; + + useEffect(() => { + if (isOpen) { + document.addEventListener('mousedown', handleClickOutside); + } else { + document.removeEventListener('mousedown', handleClickOutside); + } + }, [isOpen]); + + return ( + + setIsOpen(!isOpen)} + variant="h5" + sx={{ paddingX: 2, paddingY: 1, display: 'inline-block', cursor: 'pointer' }} + > + + {options[selected]} + + {isOpen && ( + setIsOpen(!isOpen)} sx={{ position: 'absolute', top: '-40%', cursor: 'pointer' }}> + + + {options[selected]} + + {options + .filter((option) => option !== options.at(selected)) + .map((option) => ( + { + handleSelect(option); + }} + sx={{ + cursor: 'pointer', + paddingX: 2, + paddingY: 1, + backgroundColor: theme.palette.background.paper, + position: 'relative', + '&:hover': { + backgroundColor: theme.palette.action.hover + } + }} + variant="h5" + > + {option} + + ))} + + + )} + + ); +}; + +export default WorkPackageSelect; diff --git a/src/frontend/src/pages/HomePage/components/WorkPackagesSelectionView.tsx b/src/frontend/src/pages/HomePage/components/WorkPackagesSelectionView.tsx new file mode 100644 index 0000000000..2b05907c4d --- /dev/null +++ b/src/frontend/src/pages/HomePage/components/WorkPackagesSelectionView.tsx @@ -0,0 +1,129 @@ +import { WorkPackage } from 'shared'; +import { Box, Card, CardContent, useTheme } from '@mui/material'; +import { + getInProgressWorkPackages, + getOverdueWorkPackages, + getUpcomingWorkPackages +} from '../../../utils/work-package.utils'; +import { useCurrentUser } from '../../../hooks/users.hooks'; +import WorkPackageCard from './WorkPackageCard'; +import WorkPackageSelect from './WorkPackageSelect'; +import React, { useState } from 'react'; +import EmptyPageBlockDisplay from './EmptyPageBlockDisplay'; +import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined'; + +const NoWorkPackages: React.FC = () => { + return ( + } + heading={`You're all set!`} + message={'You have no pending work packages of this type!'} + /> + ); +}; + +const WorkPackagesSelectionView: React.FC = () => { + const user = useCurrentUser(); + const theme = useTheme(); + + const teamsAsHead = user.teamsAsHead ?? []; + const teamsAsLead = user.teamsAsLead ?? []; + const teamsAsLeadership = [...teamsAsHead, ...teamsAsLead]; + + const relevantWPs = teamsAsLeadership.map((team) => team.projects.map((project) => project.workPackages)).flat(2); + + const upcomingWPs: WorkPackage[] = getUpcomingWorkPackages(relevantWPs); + const inProgressWPs: WorkPackage[] = getInProgressWorkPackages(relevantWPs); + const overdueWPs: WorkPackage[] = getOverdueWorkPackages(relevantWPs); + + // options for selection + const workPackageOptions: [string, WorkPackage[]][] = [ + [`Upcoming Work Packages (${upcomingWPs.length})`, upcomingWPs], + [`In Progress Work Packages (${inProgressWPs.length})`, inProgressWPs], + [`Overdue Work Packages (${overdueWPs.length})`, overdueWPs] + ]; + + let defaultFirstDisplay = 2; + if (workPackageOptions[2][1].length === 0) { + defaultFirstDisplay = 1; + if (workPackageOptions[1][1].length === 0) { + defaultFirstDisplay = 0; + } + } + + const [currentDisplayedWPs, setCurrentDisplayedWPs] = useState(defaultFirstDisplay); + + // destructuring tuple to get wps of selected option + const [, currentWps] = workPackageOptions[currentDisplayedWPs]; + + const WorkPackagesDisplay = (workPackages: WorkPackage[]) => ( + + {workPackages.map((wp) => ( + + ))} + + ); + + return ( + + + wp[0])} + onSelect={setCurrentDisplayedWPs} + selected={currentDisplayedWPs} + /> + + {currentWps.length === 0 ? : WorkPackagesDisplay(currentWps)} + + + + ); +}; + +export default WorkPackagesSelectionView; diff --git a/src/frontend/src/utils/work-package.utils.ts b/src/frontend/src/utils/work-package.utils.ts index 8a631082f5..5e39ad6b7d 100644 --- a/src/frontend/src/utils/work-package.utils.ts +++ b/src/frontend/src/utils/work-package.utils.ts @@ -1,4 +1,4 @@ -import { WbsElement, WbsElementStatus, wbsPipe, WorkPackage } from 'shared'; +import { addWeeksToDate, WbsElement, WbsElementStatus, wbsPipe, WorkPackage } from 'shared'; import { WPFormType } from './form'; export const getTitleFromFormType = (formType: WPFormType, wbsElement: WbsElement): string => { @@ -15,8 +15,34 @@ export const getTitleFromFormType = (formType: WPFormType, wbsElement: WbsElemen /** * Given a list of work packages, return the work packages that are overdue. * @param wpList a list of work packages. - * @returns a list of work packages that are overdue. + * @returns a sub-list of work packages that are not complete, and have end dates before the current date. */ export const getOverdueWorkPackages = (wpList: WorkPackage[]): WorkPackage[] => { - return wpList.filter((wp) => wp.status !== WbsElementStatus.Complete && wp.endDate < new Date()); + return wpList.filter((wp) => wp.status !== WbsElementStatus.Complete && new Date(wp.endDate) <= new Date()); +}; + +/** + * Given a list of work packages, return the work packages that are upcoming. + * @param wpList a list of work packages. + * @returns a sub-list of work packages that are active and have a start date within the next 2 weeks. + */ +export const getUpcomingWorkPackages = (wpList: WorkPackage[]): WorkPackage[] => { + return wpList.filter( + (wp) => + wp.status !== WbsElementStatus.Complete && + new Date(wp.startDate) <= addWeeksToDate(new Date(), 2) && + new Date(wp.startDate) >= new Date() + ); +}; + +/** + * Given a list of work packages, return the work packages that are in progress. + * @param wpList a list of work packages. + * @returns a sub-list of work packages that are active, have a start date in the past, and an end date in the future. + */ +export const getInProgressWorkPackages = (wpList: WorkPackage[]): WorkPackage[] => { + return wpList.filter( + (wp) => + wp.status === WbsElementStatus.Active && new Date(wp.endDate) >= new Date() && new Date(wp.startDate) <= new Date() + ); };