Skip to content

Commit

Permalink
Merge pull request #2888 from Northeastern-Electric-Racing/#2809-Work…
Browse files Browse the repository at this point in the history
…package-Selection-View

#2809 Workpackage Selection View
  • Loading branch information
walker-sean authored Dec 8, 2024
2 parents 5cfe9d8 + 606be36 commit 86e033f
Show file tree
Hide file tree
Showing 5 changed files with 257 additions and 7 deletions.
10 changes: 7 additions & 3 deletions src/frontend/src/pages/HomePage/AdminHomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -26,10 +27,13 @@ const AdminHomePage = ({ user }: AdminHomePageProps) => {
<Typography variant="h3" marginLeft="auto" sx={{ marginTop: 2, textAlign: 'center', pt: 3, padding: 0 }}>
Welcome, {user.firstName}!
</Typography>
<Grid container height={`${PAGE_GRID_HEIGHT}vh`} mt={2}>
<Grid item xs={12} md={12} height={`40%`}>
<Grid container height={`${PAGE_GRID_HEIGHT}vh`} mt={1} spacing={2}>
<Grid item xs={12} md={12} height={`44%`}>
<ChangeRequestsToReview user={user} />
</Grid>
<Grid item xs={10} md={7} height="56%">
<WorkPackagesSelectionView />
</Grid>
</Grid>
</PageLayout>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const NoChangeRequestsToReview: React.FC = () => {
<EmptyPageBlockDisplay
icon={<CheckCircleOutlineOutlinedIcon sx={{ fontSize: 70 }} />}
heading={`You're all caught up!`}
message={'Uou have no unreviewed changre requests!'}
message={'You have no unreviewed changre requests!'}
/>
);
};
Expand Down
91 changes: 91 additions & 0 deletions src/frontend/src/pages/HomePage/components/WorkPackageSelect.tsx
Original file line number Diff line number Diff line change
@@ -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<CustomSelectProps> = ({ options, onSelect, selected = 0 }) => {
const theme = useTheme();
const [isOpen, setIsOpen] = useState<boolean>(false);
const dropdownRef = useRef<HTMLDivElement>(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 (
<Box ref={dropdownRef} sx={{ position: 'relative' }}>
<Typography
onClick={() => setIsOpen(!isOpen)}
variant="h5"
sx={{ paddingX: 2, paddingY: 1, display: 'inline-block', cursor: 'pointer' }}
>
<ExpandMoreIcon sx={{ ml: -1, paddingRight: 0.5 }} />
{options[selected]}
</Typography>
{isOpen && (
<Box onClick={() => setIsOpen(!isOpen)} sx={{ position: 'absolute', top: '-40%', cursor: 'pointer' }}>
<Card sx={{ my: 2, background: theme.palette.background.paper }} variant="outlined">
<Typography
sx={{
paddingX: 2,
paddingY: 1,
position: 'relative',
'&:hover': {
backgroundColor: theme.palette.action.hover
}
}}
variant="h5"
>
{options[selected]}
</Typography>
{options
.filter((option) => option !== options.at(selected))
.map((option) => (
<Typography
key={option}
onClick={() => {
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}
</Typography>
))}
</Card>
</Box>
)}
</Box>
);
};

export default WorkPackageSelect;
Original file line number Diff line number Diff line change
@@ -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 (
<EmptyPageBlockDisplay
icon={<CheckCircleOutlineOutlinedIcon sx={{ fontSize: 70 }} />}
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<number>(defaultFirstDisplay);

// destructuring tuple to get wps of selected option
const [, currentWps] = workPackageOptions[currentDisplayedWPs];

const WorkPackagesDisplay = (workPackages: WorkPackage[]) => (
<Box
sx={{
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap',
overflow: 'auto',
width: '100%',
gap: 2
}}
>
{workPackages.map((wp) => (
<WorkPackageCard wp={wp} />
))}
</Box>
);

return (
<Card
sx={{
height: '100%',
background: theme.palette.background.paper
}}
variant="outlined"
>
<CardContent
sx={{
display: 'flex',
flexDirection: 'column',
height: '100%',
flexWrap: 'nowrap'
}}
>
<WorkPackageSelect
options={workPackageOptions.map((wp) => wp[0])}
onSelect={setCurrentDisplayedWPs}
selected={currentDisplayedWPs}
/>
<Box
sx={{
mt: 2,
display: 'flex',
flex: 1,
flexDirection: 'column',
gap: 2,
overflowX: 'hidden',
overflowY: 'auto',
'&::-webkit-scrollbar': {
width: '20px'
},
'&::-webkit-scrollbar-track': {
backgroundColor: 'transparent'
},
'&::-webkit-scrollbar-thumb': {
backgroundColor: theme.palette.primary.main,
borderRadius: '20px',
border: '6px solid transparent',
backgroundClip: 'content-box'
},
scrollbarWidth: 'auto',
scrollbarColor: `${theme.palette.primary.main} transparent`
}}
>
{currentWps.length === 0 ? <NoWorkPackages /> : WorkPackagesDisplay(currentWps)}
</Box>
</CardContent>
</Card>
);
};

export default WorkPackagesSelectionView;
32 changes: 29 additions & 3 deletions src/frontend/src/utils/work-package.utils.ts
Original file line number Diff line number Diff line change
@@ -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 => {
Expand All @@ -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()
);
};

0 comments on commit 86e033f

Please sign in to comment.