diff --git a/src/backend/src/prisma/seed.ts b/src/backend/src/prisma/seed.ts index 4b5cce81c5..83ad8e1355 100644 --- a/src/backend/src/prisma/seed.ts +++ b/src/backend/src/prisma/seed.ts @@ -561,6 +561,96 @@ const performSeed: () => Promise = async () => { katara.userId ); + /** Project 7 */ + const { projectWbsNumber: project7WbsNumber, projectId: project7Id } = await seedProject( + lexLuther, + changeRequest1.crId, + 1, + 'Laser Cannon Prototype', + 'Develop a prototype of a laser cannon for the Justice League', + [justiceLeague.teamId], + zatanna, + 500, + ['T2.1.1', 'T5.5.2'], + ['Increase accuracy by 20% from 80% to 100%'], + ['Capable of penetrating reinforced steel'], + ['Must be mounted on the roof of the Batmobile'], + [ + { + linkId: '-1', + url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', + linkTypeName: 'Confluence' + }, + { + linkId: '-1', + url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', + linkTypeName: 'Bill of Materials' + } + ], + zatanna.userId, + lexLuther.userId + ); + + /** Project 8 */ + const { projectWbsNumber: project8WbsNumber, projectId: project8Id } = await seedProject( + ryanGiggs, + changeRequest1.crId, + 1, + 'Stadium Renovation', + `Renovate the team's stadium to improve fan experience`, + [ravens.teamId], + mikeMacdonald, + 1000000, + ['T9.7.3'], + ['Install new seating with better sightlines'], + ['Upgrade concession stands with more variety'], + ['Implement a state-of-the-art sound system'], + [ + { + linkId: '-1', + url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', + linkTypeName: 'Confluence' + }, + { + linkId: '-1', + url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', + linkTypeName: 'Bill of Materials' + } + ], + mikeMacdonald.userId, + ryanGiggs.userId + ); + + /** Project 9 */ + const { projectWbsNumber: project9WbsNumber, projectId: project9Id } = await seedProject( + glen, + changeRequest1.crId, + 1, + 'Community Outreach Program', + 'Initiate a community outreach program to engage with local schools', + [slackBotTeam.teamId], + june, + 5000, + ['T11.2.5', 'T13.8.1'], + ['Increase participation by 50% from 100 to 150 students'], + ['Expand program to include after-school tutoring'], + ['Establish partnerships with local businesses for sponsorship'], + [ + { + linkId: '-1', + url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', + linkTypeName: 'Confluence' + }, + { + linkId: '-1', + url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', + linkTypeName: 'Bill of Materials' + } + ], + june.userId, + glen.userId + ); + /** * Change Requests for Creating Work Packages */ @@ -690,6 +780,131 @@ const performSeed: () => Promise = async () => { // approve the change request await ChangeRequestsService.reviewChangeRequest(batman, changeRequestProject6Id, 'LGTM', true, proposedSolution6Id); + const changeRequestProject7 = await ChangeRequestsService.createStandardChangeRequest( + cyborg, + project7WbsNumber.carNumber, + project7WbsNumber.projectNumber, + project7WbsNumber.workPackageNumber, + CR_Type.OTHER, + 'Initial Change Request', + [ + { + type: Scope_CR_Why_Type.INITIALIZATION, + explain: 'need this to initialize work packages' + } + ], + [ + { + budgetImpact: 0, + description: 'Initializing seed data', + timelineImpact: 0, + scopeImpact: 'no scope impact' + } + ], + null, + null + ); + + const changeRequestProject7Id = changeRequestProject7.crId; + + // make a proposed solution for it + const proposedSolution7 = await ChangeRequestsService.addProposedSolution( + cyborg, + changeRequestProject7Id, + 0, + 'Initializing seed data', + 0, + 'no scope impact' + ); + + const proposedSolution7Id = proposedSolution7.id; + + // approve the change request + await ChangeRequestsService.reviewChangeRequest(batman, changeRequestProject7Id, 'LGTM', true, proposedSolution7Id); + + const changeRequestProject8 = await ChangeRequestsService.createStandardChangeRequest( + cyborg, + project8WbsNumber.carNumber, + project8WbsNumber.projectNumber, + project8WbsNumber.workPackageNumber, + CR_Type.OTHER, + 'Initial Change Request', + [ + { + type: Scope_CR_Why_Type.INITIALIZATION, + explain: 'need this to initialize work packages' + } + ], + [ + { + budgetImpact: 0, + description: 'Initializing seed data', + timelineImpact: 0, + scopeImpact: 'no scope impact' + } + ], + null, + null + ); + + const changeRequestProject8Id = changeRequestProject8.crId; + + // make a proposed solution for it + const proposedSolution8 = await ChangeRequestsService.addProposedSolution( + cyborg, + changeRequestProject8Id, + 0, + 'Initializing seed data', + 0, + 'no scope impact' + ); + + const proposedSolution8Id = proposedSolution8.id; + + // approve the change request + await ChangeRequestsService.reviewChangeRequest(batman, changeRequestProject8Id, 'LGTM', true, proposedSolution8Id); + + const changeRequestProject9 = await ChangeRequestsService.createStandardChangeRequest( + cyborg, + project9WbsNumber.carNumber, + project9WbsNumber.projectNumber, + project9WbsNumber.workPackageNumber, + CR_Type.OTHER, + 'Initial Change Request', + [ + { + type: Scope_CR_Why_Type.INITIALIZATION, + explain: 'need this to initialize work packages' + } + ], + [ + { + budgetImpact: 0, + description: 'Initializing seed data', + timelineImpact: 0, + scopeImpact: 'no scope impact' + } + ], + null, + null + ); + + const changeRequestProject9Id = changeRequestProject9.crId; + + // make a proposed solution for it + const proposedSolution9 = await ChangeRequestsService.addProposedSolution( + cyborg, + changeRequestProject9Id, + 0, + 'Initializing seed data', + 0, + 'no scope impact' + ); + + const proposedSolution9Id = proposedSolution9.id; + + // approve the change request + await ChangeRequestsService.reviewChangeRequest(batman, changeRequestProject9Id, 'LGTM', true, proposedSolution9Id); /** * Work Packages */ @@ -916,6 +1131,162 @@ const performSeed: () => Promise = async () => { await ChangeRequestsService.reviewChangeRequest(joeShmoe, workPackage7ActivationCrId, 'LFG', true, null); + /** Work Packages for Project 7 */ + /** Work Package 1 */ + const { workPackageWbsNumber: project3WP1WbsNumber, workPackage: project3WP1 } = await seedWorkPackage( + lexLuther, + 'Design Laser Canon', + changeRequestProject7Id, + WorkPackageStage.Design, + '01/01/2023', + 3, + [], + [ + 'Define the specifications and requirements for the laser cannon prototype', + 'Research and evaluate existing laser technologies and components', + 'Design the conceptual framework and architecture of the prototype' + ], + ['Conceptual design and specifications document for the laser cannon prototype'], + zatanna, + WbsElementStatus.Active, + zatanna.userId, + lexLuther.userId + ); + + const project3WP1ActivationCrId = await ChangeRequestsService.createActivationChangeRequest( + lexLuther, + project3WP1.wbsElement.carNumber, + project3WP1.wbsElement.projectNumber, + project3WP1.wbsElement.workPackageNumber, + CR_Type.ACTIVATION, + project3WP1.project.wbsElement.leadId!, + project3WP1.project.wbsElement.managerId!, + new Date('2024-03-25T04:00:00.000Z'), + true + ); + + await ChangeRequestsService.reviewChangeRequest(joeShmoe, project3WP1ActivationCrId, 'Approved!', true, null); + + /** Work Package 2 */ + const { workPackageWbsNumber: project3WP2WbsNumber, workPackage: project3WP2 } = await seedWorkPackage( + lexLuther, + 'Laser Canon Research', + changeRequestProject7Id, + WorkPackageStage.Research, + '01/22/2023', + 5, + [], + [ + 'Research and select appropriate materials for the construction of the laser cannon', + 'Design and fabricate the structural components of the prototype', + 'Test and validate the structural integrity of the prototype' + ], + ['Prototype design and materials report'], + zatanna, + WbsElementStatus.Active, + zatanna.userId, + lexLuther.userId + ); + + /** Work Package 3 */ + const { workPackageWbsNumber: project3WP3WbsNumber, workPackage: project3WP3 } = await seedWorkPackage( + lexLuther, + 'Laser Canon Testing', + changeRequestProject7Id, + WorkPackageStage.Testing, + '02/15/2023', + 3, + [], + [ + 'Construct and integrate the electronic components into the prototype', + 'Perform functionality tests and evaluate the performance of the prototype', + 'Generate a comprehensive test report and make necessary adjustments' + ], + ['Prototype integration and testing report'], + zatanna, + WbsElementStatus.Active, + zatanna.userId, + lexLuther.userId + ); + + /** Work Packages for Project 8 */ + /** Work Package 1 */ + const { workPackageWbsNumber: project4WP1WbsNumber, workPackage: project4WP1 } = await seedWorkPackage( + ryanGiggs, + 'Stadium Research', + changeRequestProject8Id, + WorkPackageStage.Research, + '02/01/2023', + 5, + [], + [ + 'Conduct a site survey and assessment of the current stadium infrastructure', + 'Develop a detailed project plan including timelines, resource allocation, and budget estimates', + 'Obtain necessary permits and regulatory approvals' + ], + ['Comprehensive project plan and timeline'], + mikeMacdonald, + WbsElementStatus.Active, + mikeMacdonald.userId, + ryanGiggs.userId + ); + + const project4WP1ActivationCrId = await ChangeRequestsService.createActivationChangeRequest( + ryanGiggs, + project4WP1.wbsElement.carNumber, + project4WP1.wbsElement.projectNumber, + project4WP1.wbsElement.workPackageNumber, + CR_Type.ACTIVATION, + project4WP1.project.wbsElement.leadId!, + project4WP1.project.wbsElement.managerId!, + new Date('2023-08-21T04:00:00.000Z'), + true + ); + + await ChangeRequestsService.reviewChangeRequest(joeShmoe, project4WP1ActivationCrId, 'Approved!', true, null); + + /** Work Package 2 */ + const { workPackageWbsNumber: project4WP2WbsNumber, workPackage: project4WP2 } = await seedWorkPackage( + ryanGiggs, + 'Stadium Install', + changeRequestProject8Id, + WorkPackageStage.Install, + '03/01/2023', + 8, + [], + [ + 'Demolish and remove existing seating and infrastructure', + 'Construct and install new seating structures according to specifications', + 'Install amenities such as restrooms, concession stands, and VIP areas' + ], + ['Completed construction of stadium infrastructure'], + mikeMacdonald, + WbsElementStatus.Active, + mikeMacdonald.userId, + ryanGiggs.userId + ); + + /** Work Package 3 */ + const { workPackageWbsNumber: project4WP3WbsNumber, workPackage: project4WP3 } = await seedWorkPackage( + ryanGiggs, + 'Stadium Testing', + changeRequestProject8Id, + WorkPackageStage.Testing, + '06/01/2023', + 3, + [], + [ + 'Perform thorough testing of all stadium systems including lighting, sound, and safety equipment', + 'Simulate crowd scenarios to evaluate traffic flow and security measures', + 'Address any deficiencies and make necessary adjustments' + ], + ['Stadium testing and commissioning report'], + mikeMacdonald, + WbsElementStatus.Active, + mikeMacdonald.userId, + ryanGiggs.userId + ); + /** * Change Requests */ diff --git a/src/frontend/src/app/AppAuthenticated.tsx b/src/frontend/src/app/AppAuthenticated.tsx index 00fe292b22..b3829b9928 100644 --- a/src/frontend/src/app/AppAuthenticated.tsx +++ b/src/frontend/src/app/AppAuthenticated.tsx @@ -23,12 +23,11 @@ import SetUserPreferences from '../pages/HomePage/SetUserPreferences'; import Finance from '../pages/FinancePage/Finance'; import Sidebar from '../layouts/Sidebar/Sidebar'; import { Box } from '@mui/system'; -import { Container, Typography } from '@mui/material'; +import { Container } from '@mui/material'; import ErrorPage from '../pages/ErrorPage'; -import { Role, RoleEnum, isGuest } from 'shared'; +import { Role, isGuest } from 'shared'; import Calendar from '../pages/CalendarPage/Calendar'; import { useState } from 'react'; -import PageBlock from '../layouts/PageBlock'; interface AppAuthenticatedProps { userId: number; @@ -50,14 +49,6 @@ const AppAuthenticated: React.FC = ({ userId, userRole }) } } - const Maintainenance = () => { - return ( - - This Page is Currently Undergoing Maintainenance - - ); - }; - return userSettingsData.slackId || isGuest(userRole) ? ( @@ -67,7 +58,7 @@ const AppAuthenticated: React.FC = ({ userId, userRole }) - + diff --git a/src/frontend/src/pages/GanttPage/GanttChart.tsx b/src/frontend/src/pages/GanttPage/GanttChart.tsx index 1d10370f7c..2ec8c33963 100644 --- a/src/frontend/src/pages/GanttPage/GanttChart.tsx +++ b/src/frontend/src/pages/GanttPage/GanttChart.tsx @@ -1,18 +1,12 @@ -import { Box, Button, Chip, IconButton, Typography, useTheme } from '@mui/material'; +import { Box } from '@mui/material'; import { EventChange, GanttTask, RequestEventChange } from '../../utils/gantt.utils'; -import { Edit } from '@mui/icons-material'; -import GanttChartSection from './GanttChartSection'; -import { NERButton } from '../../components/NERButton'; -import { useHistory } from 'react-router-dom'; -import { routes } from '../../utils/routes'; +import GanttChartTeamSection from './GanttChartTeamSection'; interface GanttChartProps { startDate: Date; endDate: Date; teamsList: string[]; teamNameToGanttTasksMap: Map; - chartEditingState: Array<{ teamName: string; editing: boolean }>; - setChartEditingState: (array: Array<{ teamName: string; editing: boolean }>) => void; saveChanges: (eventChanges: EventChange[]) => void; showWorkPackagesMap: Map; setShowWorkPackagesMap: React.Dispatch>>; @@ -24,107 +18,32 @@ const GanttChart = ({ endDate, teamsList, teamNameToGanttTasksMap, - chartEditingState, - setChartEditingState, saveChanges, showWorkPackagesMap, setShowWorkPackagesMap, highlightedChange }: GanttChartProps) => { - const theme = useTheme(); - const history = useHistory(); - return ( - <> - - history.push(routes.PROJECTS_NEW)}> - Create Project - - - - {teamsList.map((teamName: string) => { - const tasks = teamNameToGanttTasksMap.get(teamName); - - if (!chartEditingState.map((entry) => entry.teamName).includes(teamName)) { - setChartEditingState([...chartEditingState, { teamName, editing: false }]); - } - - const isEditMode = chartEditingState.find((entry) => entry.teamName === teamName)?.editing || false; - - const handleEdit = () => { - const index = chartEditingState.findIndex((entry) => entry.teamName === teamName); - if (index !== -1) { - chartEditingState[index] = { teamName, editing: !isEditMode }; - } - - if (!isEditMode) { - const projects = tasks ? tasks.filter((event) => !event.project) : []; - projects.forEach((project) => { - setShowWorkPackagesMap((prev) => new Map(prev.set(project.id, true))); - }); - } - - setChartEditingState([...chartEditingState]); - }; - - if (!tasks) return <>; - - // Sorting the work packages of each project based on their start date - tasks.forEach((task) => { - task.children.sort((a, b) => a.start.getTime() - b.start.getTime()); - }); - return ( - - - - {teamName} - - - {isEditMode ? ( - - ) : ( - - - - )} - - - - - - ); - })} - - + + {teamsList.map((teamName: string) => { + const projectTasks = teamNameToGanttTasksMap.get(teamName); + + return projectTasks ? ( + + ) : ( + <> + ); + })} + ); }; diff --git a/src/frontend/src/pages/GanttPage/GanttChartComponents/GanttChartFilters.tsx b/src/frontend/src/pages/GanttPage/GanttChartComponents/GanttChartFilters.tsx index 9e155ac79b..4b23f2fb61 100644 --- a/src/frontend/src/pages/GanttPage/GanttChartComponents/GanttChartFilters.tsx +++ b/src/frontend/src/pages/GanttPage/GanttChartComponents/GanttChartFilters.tsx @@ -3,7 +3,7 @@ * See the LICENSE file in the repository root folder for details. */ -import { Button, Checkbox, Chip, Grid, Typography, useTheme } from '@mui/material'; +import { Box, Checkbox, Chip, IconButton, Typography, useTheme } from '@mui/material'; import UnfoldMoreIcon from '@mui/icons-material/UnfoldMore'; import UnfoldLessIcon from '@mui/icons-material/UnfoldLess'; import RestartAltIcon from '@mui/icons-material/RestartAlt'; @@ -53,12 +53,11 @@ const FilterRow = ({ checkedMap[button.filterLabel] = button.defaultChecked; }); return ( - + {label} - - + {buttons.map((button) => ( ))} - - + + ); }; @@ -101,49 +100,59 @@ const GanttChartFilters = ({ }: GanttChartFiltersProps) => { const FilterButtons = () => { return ( - - - - - - - - - - - + + + + + + Expand + + + + + + Collapse + + + + + + Reset + + + + Overdue + + ); }; return ( - - - - - - - - - - - + + + + + + ); }; diff --git a/src/frontend/src/pages/GanttPage/GanttChartComponents/GanttChartFiltersButton.tsx b/src/frontend/src/pages/GanttPage/GanttChartComponents/GanttChartFiltersButton.tsx index f70c182155..68d3d31439 100644 --- a/src/frontend/src/pages/GanttPage/GanttChartComponents/GanttChartFiltersButton.tsx +++ b/src/frontend/src/pages/GanttPage/GanttChartComponents/GanttChartFiltersButton.tsx @@ -50,14 +50,14 @@ const GanttChartFiltersButton = ({ anchorEl={anchorFilterEl} onClose={handleFilterClose} anchorOrigin={{ - vertical: 'bottom', + vertical: 'top', horizontal: 'right' }} transformOrigin={{ vertical: 'top', horizontal: 'right' }} - sx={{ dispaly: 'flex', justifyContent: 'center', alignItems: 'center' }} + sx={{ maxWidth: '100rem' }} > void; - isEditMode: boolean; - onWorkPackageToggle?: () => void; - showWorkPackages?: boolean; - highlightedChange?: RequestEventChange; -} & ComponentProps<'div'>) => { - const theme = useTheme(); - const id = useId() || 'id'; // id for creating event changes - const history = useHistory(); - const [isResizing, setIsResizing] = useState(false); - const [startX, setStartX] = useState(null); - const [showDropPoints, setShowDropPoints] = useState(false); - const [initialWidth, setInitialWidth] = useState(0); // original width of the component, should not change on resize - const [width, setWidth] = useState(0); // current width of component, will change on resize - const [measureRef, bounds] = useMeasure(); - const [showPopup, setShowPopup] = useState(false); - const [cursorX, setCursorX] = useState(0); - const [cursorY, setCursorY] = useState(0); - const isProject = !event.project; - - const getStartCol = (event: GanttTaskData) => { - const startCol = days.findIndex((day) => dateToString(day) === dateToString(event.start)) + 1; - return startCol; - }; - - // if the end date doesn't exist within the timeframe, have it span to the end - const getEndCol = (event: GanttTaskData) => { - const endCol = - days.findIndex((day) => dateToString(day) === dateToString(event.end)) === -1 - ? days.length + 1 - : days.findIndex((day) => dateToString(day) === dateToString(event.end)) + 2; - return endCol; - }; - - const lengthInDays = differenceInDays(event.end, event.start); - - // used to make sure that any changes to the start and end dates are made in multiples of 7 - const roundToMultipleOf7 = (num: number) => { - return Math.round(num / 7) * 7; - }; - - useEffect(() => { - if (bounds.width !== 0 && width === 0) { - setInitialWidth(bounds.width); - setWidth(bounds.width); - } - }, [bounds, width]); - - const handleMouseDown = (e: MouseEvent) => { - setIsResizing(true); - setStartX(e.clientX); - }; - - const handleMouseMove = (e: MouseEvent) => { - if (isResizing) { - const currentX = e.clientX; - const deltaX = currentX - startX!; - setWidth(Math.max(100, width + deltaX)); - setStartX(currentX); - } - }; - - const handleMouseUp = () => { - if (isResizing) { - setIsResizing(false); - // Use change in width to calculate new length - const newEventLengthInDays = roundToMultipleOf7(Math.round((lengthInDays / initialWidth) * width)); - const newEndDate = addDays(event.start, newEventLengthInDays); - createChange({ id, eventId: event.id, type: 'change-end-date', originalEnd: event.end, newEnd: newEndDate }); - } - }; - - const onDragStart = (e: DragEvent) => { - setShowDropPoints(true); - }; - const onDragEnd = (e: DragEvent) => { - setShowDropPoints(false); - }; - - const onDragOver = (e: DragEvent) => { - e.preventDefault(); - }; - const onDrop = (day: Date) => { - const days = roundToMultipleOf7(differenceInDays(day, event.start)); - createChange({ id, eventId: event.id, type: 'shift-by-days', days }); - }; - - const handleOnMouseOver = (e: React.MouseEvent) => { - if (!isEditMode) { - setCursorX(e.clientX); - setCursorY(e.clientY); - setShowPopup(true); - } - }; - - const handleOnMouseLeave = () => { - setShowPopup(false); - }; - - return isEditMode ? ( -
- - {/* Drop areas */} - {showDropPoints && - days.map((day, index) => ( - onDrop(day)} - sx={{ - borderRadius: '0.25rem', - height: '2rem', - minWidth: GANTT_CHART_CELL_SIZE, - maxWidth: GANTT_CHART_CELL_SIZE, - backgroundColor: `color-mix(in srgb, ${theme.palette.background.default}, transparent 75%);` - }} - /> - ))} - - -
- - - - - {event.name} - - - - - -
-
-
- ) : ( - - -
history.push(`${`${routes.PROJECTS}/${event.id}`}`)} - > - - - -
-
history.push(`${`${routes.PROJECTS}/${event.id}`}`) : undefined} - > - {isProject && ( - - {showWorkPackages ? : } - - )} - - {event.name} - -
- {event.children.map((child) => { - return ( -
history.push(`${`${routes.PROJECTS}/${event.id}`}`)} - /> - ); - })} - {highlightedChange && ( -
dateToString(day) === dateToString(highlightedChange.newStart)) + 1, - gridColumnEnd: - days.findIndex((day) => dateToString(day) === dateToString(highlightedChange.newEnd)) === -1 - ? days.length + 1 - : days.findIndex((day) => dateToString(day) === dateToString(highlightedChange.newEnd)) + 2, - height: '2rem', - border: `1px solid ${theme.palette.text.primary}`, - borderRadius: '0.25rem', - backgroundColor: '#ef4345', - cursor: 'pointer', - gridRow: 1, - zIndex: 6 - }} - > - - {highlightedChange.name} - -
- )} - - {showPopup && ( - - )} - - ); -}; - -export default GanttTaskBar; diff --git a/src/frontend/src/pages/GanttPage/GanttChartComponents/GanttTaskBar/GanttTaskBar.tsx b/src/frontend/src/pages/GanttPage/GanttChartComponents/GanttTaskBar/GanttTaskBar.tsx new file mode 100644 index 0000000000..61ab3085ea --- /dev/null +++ b/src/frontend/src/pages/GanttPage/GanttChartComponents/GanttTaskBar/GanttTaskBar.tsx @@ -0,0 +1,77 @@ +/* + * This file is part of NER's FinishLine and licensed under GNU AGPLv3. + * See the LICENSE file in the repository root folder for details. + */ + +import { EventChange, GanttTaskData, RequestEventChange } from '../../../../utils/gantt.utils'; +import { dateToString, getMonday } from '../../../../utils/datetime.utils'; +import GanttTaskBarEdit from './GanttTaskBarEdit'; +import GanttTaskBarView from './GanttTaskBarView'; + +const GanttTaskBar = ({ + days, + event, + createChange, + isEditMode, + handleOnMouseOver, + onWorkPackageToggle, + handleOnMouseLeave, + showWorkPackages = false, + highlightedChange +}: { + days: Date[]; + event: GanttTaskData; + createChange: (change: EventChange) => void; + isEditMode: boolean; + handleOnMouseOver: (e: React.MouseEvent, event: GanttTaskData) => void; + handleOnMouseLeave: () => void; + onWorkPackageToggle?: () => void; + showWorkPackages?: boolean; + highlightedChange?: RequestEventChange; +}) => { + const isProject = !event.project; + + const getStartCol = (event: GanttTaskData) => { + const startCol = days.findIndex((day) => dateToString(day) === dateToString(getMonday(event.start))) + 1; + return startCol; + }; + + // if the end date doesn't exist within the timeframe, have it span to the end + const getEndCol = (event: GanttTaskData) => { + const endCol = + days.findIndex((day) => dateToString(day) === dateToString(getMonday(event.end))) === -1 + ? days.length + 1 + : days.findIndex((day) => dateToString(day) === dateToString(getMonday(event.end))) + 2; + return endCol; + }; + + const onMouseOver = (e: React.MouseEvent) => { + handleOnMouseOver(e, event); + }; + + return isEditMode ? ( + + ) : ( + + ); +}; + +export default GanttTaskBar; diff --git a/src/frontend/src/pages/GanttPage/GanttChartComponents/GanttTaskBar/GanttTaskBarEdit.tsx b/src/frontend/src/pages/GanttPage/GanttChartComponents/GanttTaskBar/GanttTaskBarEdit.tsx new file mode 100644 index 0000000000..3a10fe2571 --- /dev/null +++ b/src/frontend/src/pages/GanttPage/GanttChartComponents/GanttTaskBar/GanttTaskBarEdit.tsx @@ -0,0 +1,197 @@ +import { Box, Typography, useTheme } from '@mui/material'; +import { EventChange, GANTT_CHART_CELL_SIZE, GANTT_CHART_GAP_SIZE, GanttTaskData } from '../../../../utils/gantt.utils'; +import { addDays, differenceInDays } from 'date-fns'; +import { DragEvent, MouseEvent, useEffect, useState } from 'react'; +import useId from '@mui/material/utils/useId'; +import useMeasure from 'react-use-measure'; + +const GanttTaskBarEdit = ({ + days, + event, + createChange, + getStartCol, + getEndCol, + isProject +}: { + days: Date[]; + event: GanttTaskData; + createChange: (change: EventChange) => void; + getStartCol: (event: GanttTaskData) => number; + getEndCol: (event: GanttTaskData) => number; + isProject: boolean; +}) => { + const theme = useTheme(); + const id = useId() || 'id'; // id for creating event changes + const [startX, setStartX] = useState(null); + const [showDropPoints, setShowDropPoints] = useState(false); + const [isResizing, setIsResizing] = useState(false); + const [initialWidth, setInitialWidth] = useState(0); // original width of the component, should not change on resize + const [width, setWidth] = useState(0); // current width of component, will change on resize + const [measureRef, bounds] = useMeasure(); + + const lengthInDays = differenceInDays(event.end, event.start); + + // used to make sure that any changes to the start and end dates are made in multiples of 7 + const roundToMultipleOf7 = (num: number) => { + return Math.round(num / 7) * 7; + }; + + const handleMouseDown = (e: MouseEvent) => { + setIsResizing(true); + setStartX(e.clientX); + }; + + const handleMouseMove = (e: MouseEvent) => { + if (isResizing) { + const currentX = e.clientX; + const deltaX = currentX - startX!; + setWidth(Math.max(100, width + deltaX)); + setStartX(currentX); + } + }; + + const handleMouseUp = () => { + if (isResizing) { + setIsResizing(false); + // Use change in width to calculate new length + const newEventLengthInDays = roundToMultipleOf7(Math.round((lengthInDays / initialWidth) * width)); + const newEndDate = addDays(event.start, newEventLengthInDays); + createChange({ id, eventId: event.id, type: 'change-end-date', originalEnd: event.end, newEnd: newEndDate }); + } + }; + + const onDragStart = (e: DragEvent) => { + setShowDropPoints(true); + }; + const onDragEnd = (e: DragEvent) => { + setShowDropPoints(false); + }; + + const onDragOver = (e: DragEvent) => { + e.preventDefault(); + }; + const onDrop = (day: Date) => { + const days = roundToMultipleOf7(differenceInDays(day, event.start)); + createChange({ id, eventId: event.id, type: 'shift-by-days', days }); + }; + + useEffect(() => { + if (bounds.width !== 0 && width === 0) { + setInitialWidth(bounds.width); + setWidth(bounds.width); + } + }, [bounds, width]); + + return ( +
+ + {/* Drop areas */} + {showDropPoints && + days.map((day, index) => ( + onDrop(day)} + sx={{ + borderRadius: '0.25rem', + height: '2rem', + minWidth: GANTT_CHART_CELL_SIZE, + maxWidth: GANTT_CHART_CELL_SIZE, + backgroundColor: `color-mix(in srgb, ${theme.palette.background.default}, transparent 75%);` + }} + /> + ))} + + +
+ + + + + {event.name} + + + + + +
+
+
+ ); +}; + +export default GanttTaskBarEdit; diff --git a/src/frontend/src/pages/GanttPage/GanttChartComponents/GanttTaskBar/GanttTaskBarView.tsx b/src/frontend/src/pages/GanttPage/GanttChartComponents/GanttTaskBar/GanttTaskBarView.tsx new file mode 100644 index 0000000000..84ccaec151 --- /dev/null +++ b/src/frontend/src/pages/GanttPage/GanttChartComponents/GanttTaskBar/GanttTaskBarView.tsx @@ -0,0 +1,184 @@ +import { + GANTT_CHART_GAP_SIZE, + GANTT_CHART_CELL_SIZE, + GanttTaskData, + RequestEventChange +} from '../../../../utils/gantt.utils'; +import { Box, IconButton, Typography, useTheme } from '@mui/material'; +import { grey } from '@mui/material/colors'; +import { dateToString } from '../../../../utils/datetime.utils'; +import { routes } from '../../../../utils/routes'; +import { useHistory } from 'react-router-dom'; +import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; +import ArrowRightIcon from '@mui/icons-material/ArrowRight'; + +const GanttTaskBarView = ({ + days, + event, + getStartCol, + getEndCol, + isProject, + handleOnMouseOver, + handleOnMouseLeave, + onWorkPackageToggle, + showWorkPackages, + highlightedChange +}: { + days: Date[]; + event: GanttTaskData; + getStartCol: (event: GanttTaskData) => number; + getEndCol: (event: GanttTaskData) => number; + isProject: boolean; + handleOnMouseOver: (e: React.MouseEvent) => void; + handleOnMouseLeave: () => void; + onWorkPackageToggle?: () => void; + showWorkPackages?: boolean; + highlightedChange?: RequestEventChange; +}) => { + const theme = useTheme(); + const history = useHistory(); + + return ( + + +
history.push(`${`${routes.PROJECTS}/${event.id}`}`)} + > + + + +
+
history.push(`${`${routes.PROJECTS}/${event.id}`}`) : undefined} + > + {isProject && ( + + {showWorkPackages ? : } + + )} + + {event.name} + +
+ {event.workPackages.map((child) => { + return ( +
history.push(`${`${routes.PROJECTS}/${event.id}`}`)} + /> + ); + })} + {highlightedChange && ( +
dateToString(day) === dateToString(highlightedChange.newStart)) + 1, + gridColumnEnd: + days.findIndex((day) => dateToString(day) === dateToString(highlightedChange.newEnd)) === -1 + ? days.length + 1 + : days.findIndex((day) => dateToString(day) === dateToString(highlightedChange.newEnd)) + 2, + height: '2rem', + border: `1px solid ${theme.palette.text.primary}`, + borderRadius: '0.25rem', + backgroundColor: '#ef4345', + cursor: 'pointer', + gridRow: 1, + zIndex: 6 + }} + > + + {highlightedChange.name} + +
+ )} + + + ); +}; + +export default GanttTaskBarView; diff --git a/src/frontend/src/pages/GanttPage/GanttChartPage.tsx b/src/frontend/src/pages/GanttPage/GanttChartPage.tsx index e7f4676fec..02907fcafa 100644 --- a/src/frontend/src/pages/GanttPage/GanttChartPage.tsx +++ b/src/frontend/src/pages/GanttPage/GanttChartPage.tsx @@ -38,6 +38,10 @@ import { GanttRequestChangeModal } from './GanttChartComponents/GanttRequestChan const GanttChartPage: FC = () => { const query = useQuery(); const history = useHistory(); + const ganttParams = localStorage.getItem('ganttURL'); + if (ganttParams && history.location.search !== ganttParams) { + history.push(`${history.location.pathname + ganttParams}`); + } const { isLoading: projectsIsLoading, isError: projectsIsError, data: projects, error: projectsError } = useAllProjects(); const { isLoading: teamTypesIsLoading, @@ -46,12 +50,6 @@ const GanttChartPage: FC = () => { error: teamTypesError } = useAllTeamTypes(); const { isLoading: teamsIsLoading, isError: teamsIsError, data: teams, error: teamsError } = useAllTeams(); - const [chartEditingState, setChartEditingState] = React.useState< - Array<{ - teamName: string; - editing: boolean; - }> - >([]); const [searchText, setSearchText] = useState(''); const [ganttTaskChanges, setGanttTaskChanges] = useState([]); const [showWorkPackagesMap, setShowWorkPackagesMap] = useState>(new Map()); @@ -76,7 +74,7 @@ const GanttChartPage: FC = () => { const sortedProjects = filteredProjects.sort( (a, b) => (a.startDate || new Date()).getTime() - (b.startDate || new Date()).getTime() ); - const ganttTasks = sortedProjects.flatMap((project) => transformProjectToGanttTask(project)); + const ganttProjectTasks = sortedProjects.flatMap((project) => transformProjectToGanttTask(project)); if (projectsIsLoading || teamTypesIsLoading || teamsIsLoading || !teams || !projects || !teamTypes) return ; @@ -161,6 +159,7 @@ const GanttChartPage: FC = () => { const resetHandler = () => { history.push(routes.GANTT); + localStorage.removeItem('ganttURL'); showWorkPackagesMap.clear(); }; @@ -168,17 +167,25 @@ const GanttChartPage: FC = () => { const teamNameToGanttTasksMap = new Map(); - ganttTasks.forEach((ganttTask) => { + ganttProjectTasks.forEach((ganttTask) => { const tasks: GanttTask[] = teamNameToGanttTasksMap.get(ganttTask.teamName) || []; tasks.push(ganttTask); teamNameToGanttTasksMap.set(ganttTask.teamName, tasks); }); + const allGanttTasks = ganttProjectTasks + .flatMap((projectTask) => + projectTask.workPackages.map((wp) => { + return { ...wp, teamName: projectTask.teamName }; + }) + ) + .concat(ganttProjectTasks); + // find the earliest start date and subtract 2 weeks to use as the first date on calendar const startDate = - ganttTasks.length !== 0 + allGanttTasks.length !== 0 ? sub( - ganttTasks + allGanttTasks .map((task) => task.start) .reduce((previous, current) => { return previous < current ? previous : current; @@ -189,9 +196,9 @@ const GanttChartPage: FC = () => { // find the latest end date and add 6 months to use as the last date on calendar const endDate = - ganttTasks.length !== 0 + allGanttTasks.length !== 0 ? add( - ganttTasks + allGanttTasks .map((task) => task.end) .reduce((previous, current) => { return previous > current ? previous : current; @@ -204,7 +211,8 @@ const GanttChartPage: FC = () => { const sortedTeamList: string[] = teamList.sort(sortTeamNames); const saveChanges = (eventChanges: EventChange[]) => { - const updatedGanttTasks = aggregateGanttChanges(eventChanges, ganttTasks); + //get wps out of each project + const updatedGanttTasks = aggregateGanttChanges(eventChanges, allGanttTasks); setGanttTaskChanges(updatedGanttTasks); }; @@ -213,13 +221,13 @@ const GanttChartPage: FC = () => { }; const collapseHandler = () => { - ganttTasks.forEach((task) => { + ganttProjectTasks.forEach((task) => { setShowWorkPackagesMap((prev) => new Map(prev.set(task.id, false))); }); }; const expandHandler = () => { - ganttTasks.forEach((task) => { + ganttProjectTasks.forEach((task) => { setShowWorkPackagesMap((prev) => new Map(prev.set(task.id, true))); }); }; @@ -263,8 +271,6 @@ const GanttChartPage: FC = () => { endDate={endDate} teamsList={sortedTeamList} teamNameToGanttTasksMap={teamNameToGanttTasksMap} - chartEditingState={chartEditingState} - setChartEditingState={setChartEditingState} saveChanges={saveChanges} showWorkPackagesMap={showWorkPackagesMap} setShowWorkPackagesMap={setShowWorkPackagesMap} diff --git a/src/frontend/src/pages/GanttPage/GanttChartSection.tsx b/src/frontend/src/pages/GanttPage/GanttChartSection.tsx index 05393b03a5..7b95c52569 100644 --- a/src/frontend/src/pages/GanttPage/GanttChartSection.tsx +++ b/src/frontend/src/pages/GanttPage/GanttChartSection.tsx @@ -4,17 +4,18 @@ */ import { eachDayOfInterval, isMonday } from 'date-fns'; -import { applyChangesToEvents, EventChange, GanttTaskData, RequestEventChange } from '../../utils/gantt.utils'; +import { EventChange, GanttTaskData, RequestEventChange } from '../../utils/gantt.utils'; import { Box, Typography, Collapse } from '@mui/material'; -import GanttTaskBar from './GanttChartComponents/GanttTaskBar'; -import { useEffect, useState } from 'react'; +import GanttTaskBar from './GanttChartComponents/GanttTaskBar/GanttTaskBar'; +import { useState } from 'react'; +import GanttToolTip from './GanttChartComponents/GanttToolTip'; interface GanttChartSectionProps { start: Date; end: Date; - tasks: GanttTaskData[]; + projects: GanttTaskData[]; isEditMode: boolean; - saveChanges: (eventChanges: EventChange[]) => void; + createChange: (change: EventChange) => void; showWorkPackagesMap: Map; setShowWorkPackagesMap: React.Dispatch>>; highlightedChange?: RequestEventChange; @@ -23,40 +24,37 @@ interface GanttChartSectionProps { const GanttChartSection = ({ start, end, - tasks, + projects, isEditMode, - saveChanges, + createChange, showWorkPackagesMap, setShowWorkPackagesMap, highlightedChange }: GanttChartSectionProps) => { const days = eachDayOfInterval({ start, end }).filter((day) => isMonday(day)); - const [eventChanges, setEventChanges] = useState([]); + const [currentTask, setCurrentTask] = useState(undefined); + const [cursorX, setCursorX] = useState(0); + const [cursorY, setCursorY] = useState(0); - const createChange = (change: EventChange) => { - setEventChanges([...eventChanges, change]); - }; - - useEffect(() => { - // only try to save changes when we're going from non-editing to editing mode + const handleOnMouseOver = (e: React.MouseEvent, event: GanttTaskData) => { if (!isEditMode) { - saveChanges(eventChanges); - setEventChanges([]); // reset the changes after sending them + setCursorX(e.clientX); + setCursorY(e.clientY); + setCurrentTask(event); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isEditMode]); + }; - const displayEvents = applyChangesToEvents(tasks, eventChanges); - const projects = displayEvents.filter((event) => !event.project); + const handleOnMouseLeave = () => { + setCurrentTask(undefined); + }; const toggleWorkPackages = (projectTask: GanttTaskData) => { setShowWorkPackagesMap((prev) => new Map(prev.set(projectTask.id, !prev.get(projectTask.id)))); }; - return tasks.length > 0 ? ( + return projects.length > 0 ? ( - {/* Data display: reset list of events every time eventChanges list changes using key */} - + {projects.map((project) => { return ( <> @@ -67,20 +65,23 @@ const GanttChartSection = ({ event={project} isEditMode={isEditMode} createChange={createChange} + handleOnMouseOver={handleOnMouseOver} + handleOnMouseLeave={handleOnMouseLeave} onWorkPackageToggle={() => toggleWorkPackages(project)} showWorkPackages={showWorkPackagesMap.get(project.id)} /> - {project.children.map((workPackage) => { - const displayWorkPackage = displayEvents.find((event) => event.id === workPackage.id); + {project.workPackages.map((workPackage) => { return ( + {currentTask && ( + + )} ) : ( No items to display diff --git a/src/frontend/src/pages/GanttPage/GanttChartTeamSection.tsx b/src/frontend/src/pages/GanttPage/GanttChartTeamSection.tsx new file mode 100644 index 0000000000..ce03916967 --- /dev/null +++ b/src/frontend/src/pages/GanttPage/GanttChartTeamSection.tsx @@ -0,0 +1,108 @@ +import { Edit } from '@mui/icons-material'; +import { Box, Chip, IconButton, Typography, useTheme } from '@mui/material'; +import GanttChartSection from './GanttChartSection'; +import { EventChange, GanttTask, RequestEventChange, applyChangesToEvents } from '../../utils/gantt.utils'; +import { useState } from 'react'; + +interface GanttChartTeamSectionProps { + startDate: Date; + endDate: Date; + saveChanges: (eventChanges: EventChange[]) => void; + showWorkPackagesMap: Map; + setShowWorkPackagesMap: React.Dispatch>>; + teamName: string; + projectTasks: GanttTask[]; + highlightedChange?: RequestEventChange; +} + +const GanttChartTeamSection = ({ + startDate, + endDate, + saveChanges, + showWorkPackagesMap, + setShowWorkPackagesMap, + teamName, + projectTasks, + highlightedChange +}: GanttChartTeamSectionProps) => { + const theme = useTheme(); + const [eventChanges, setEventChanges] = useState([]); + const [isEditMode, setIsEditMode] = useState(false); + + const createChange = (change: EventChange) => { + setEventChanges([...eventChanges, change]); + }; + + const handleSave = () => { + saveChanges(eventChanges); + setEventChanges([]); + setIsEditMode(false); + }; + + const handleEdit = () => { + projectTasks.forEach((project) => { + setShowWorkPackagesMap((prev) => new Map(prev.set(project.id, true))); + }); + + setIsEditMode(true); + }; + + // Sorting the work packages of each project based on their start date + projectTasks.forEach((task) => { + task.workPackages.sort((a, b) => a.start.getTime() - b.start.getTime()); + }); + + const displayedProjects = isEditMode ? applyChangesToEvents(projectTasks, eventChanges) : projectTasks; + + return ( + + + + {teamName} + + + {isEditMode ? ( + + ) : ( + + + + )} + + + + + + ); +}; + +export default GanttChartTeamSection; diff --git a/src/frontend/src/utils/gantt.utils.ts b/src/frontend/src/utils/gantt.utils.ts index f46fc6e2a4..3a0b352a18 100644 --- a/src/frontend/src/utils/gantt.utils.ts +++ b/src/frontend/src/utils/gantt.utils.ts @@ -20,22 +20,13 @@ export interface GanttTaskData { name: string; start: Date; end: Date; - /** - * From 0 to 100 - */ - progress: number; - children: GanttTaskData[]; + workPackages: GanttTaskData[]; styles?: { color?: string; backgroundColor?: string; backgroundSelectedColor?: string; - progressColor?: string; - progressSelectedColor?: string; }; - isDisabled?: boolean; project?: string; - dependencies?: string[]; - displayOrder?: number; onClick?: () => void; projectLead?: User; projectManager?: User; @@ -58,9 +49,13 @@ export type RequestEventChange = { duration: number; }; -export const applyChangeToEvent = (event: GanttTaskData, eventChanges: EventChange[]) => { +export const applyChangeToEvent = (event: GanttTaskData, eventChanges: EventChange[]): GanttTaskData => { + const workPackages = event.workPackages && event.workPackages.map((wpEvent) => applyChangeToEvent(wpEvent, eventChanges)); + + const currentEventChanges = eventChanges.filter((ec) => ec.eventId === event.id); + const changedEvent = { ...event }; - for (const eventChange of eventChanges) { + for (const eventChange of currentEventChanges) { switch (eventChange.type) { case 'change-end-date': { changedEvent.end = eventChange.newEnd; @@ -73,13 +68,12 @@ export const applyChangeToEvent = (event: GanttTaskData, eventChanges: EventChan } } } - return changedEvent; + return { ...changedEvent, workPackages }; }; -export const applyChangesToEvents = (events: GanttTaskData[], eventChanges: EventChange[]) => { +export const applyChangesToEvents = (events: GanttTaskData[], eventChanges: EventChange[]): GanttTaskData[] => { return events.map((event) => { - const changes = eventChanges.filter((ec) => ec.eventId === event.id); - return applyChangeToEvent(event, changes); + return applyChangeToEvent(event, eventChanges); }); }; @@ -137,13 +131,16 @@ export const buildGanttSearchParams = (ganttFilters: GanttFilters): string => { return `&team=${name}`; }; - return ( + const newParams = '?' + ganttFilters.showCars.map((car) => carFormat(car.toString())).join('') + ganttFilters.showTeamTypes.map(teamTypeFormat).join('') + ganttFilters.showTeams.map(teamFormat).join('') + - `&overdue=${ganttFilters.showOnlyOverdue}` - ); + `&overdue=${ganttFilters.showOnlyOverdue}`; + + localStorage.setItem('ganttURL', newParams); + + return newParams; }; export const transformWorkPackageToGanttTask = (workPackage: WorkPackage, teamName: string): GanttTask => { @@ -152,11 +149,10 @@ export const transformWorkPackageToGanttTask = (workPackage: WorkPackage, teamNa name: wbsPipe(workPackage.wbsNum) + ' ' + workPackage.name, start: workPackage.startDate, end: workPackage.endDate, - progress: 100, project: projectWbsPipe(workPackage.wbsNum), type: 'task', teamName, - children: [], + workPackages: [], styles: { color: GanttWorkPackageTextColorPipe(workPackage.stage), backgroundColor: GanttWorkPackageStageColorPipe(workPackage.stage, workPackage.status) @@ -181,20 +177,15 @@ export const transformProjectToGanttTask = (project: Project): GanttTask[] => { name: wbsPipe(project.wbsNum) + ' - ' + project.name, start: project.startDate || new Date(), end: project.endDate || new Date(), - progress: 100, type: 'project', teamName, - children: project.workPackages.map((wp) => transformWorkPackageToGanttTask(wp, teamName)), + workPackages: project.workPackages.map((wp) => transformWorkPackageToGanttTask(wp, teamName)), onClick: () => { window.open(`/projects/${wbsPipe(project.wbsNum)}`, '_blank'); } }; - const workPackageTasks = project.workPackages - .sort((a, b) => a.startDate.getTime() - b.startDate.getTime()) - .map((workPackage) => transformWorkPackageToGanttTask(workPackage, teamName)); - - return [projectTask, ...workPackageTasks]; + return [projectTask]; }; /**