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()
+ );
};