diff --git a/src/backend/src/controllers/statistics.controllers.ts b/src/backend/src/controllers/statistics.controllers.ts index 39d2c3946a..c68c5fece2 100644 --- a/src/backend/src/controllers/statistics.controllers.ts +++ b/src/backend/src/controllers/statistics.controllers.ts @@ -139,4 +139,34 @@ export default class StatisticsController { next(error); } } + + static async removeGraphFromGraphCollection(req: Request, res: Response, next: NextFunction) { + try { + const { graphCollectionId, graphId } = req.params; + console.log('test'); + const message: { message: string } = await StatisticsService.removeGraphFromCollection( + req.currentUser, + graphCollectionId, + graphId, + req.organization + ); + res.status(200).json(message); + } catch (error: unknown) { + next(error); + } + } + + static async deleteGraphCollection(req: Request, res: Response, next: NextFunction) { + try { + const { graphCollectionId } = req.params; + const message: { message: string } = await StatisticsService.deleteGraphCollection( + req.currentUser, + graphCollectionId, + req.organization + ); + res.status(200).json(message); + } catch (error: unknown) { + next(error); + } + } } diff --git a/src/backend/src/routes/statistics.routes.ts b/src/backend/src/routes/statistics.routes.ts index 890db4a91e..57b3eb375c 100644 --- a/src/backend/src/routes/statistics.routes.ts +++ b/src/backend/src/routes/statistics.routes.ts @@ -74,4 +74,11 @@ statisticsRouter.post( StatisticsController.editGraphCollection ); +statisticsRouter.post( + '/graph-collections/:graphCollectionId/remove/:graphId', + StatisticsController.removeGraphFromGraphCollection +); + +statisticsRouter.delete('/graph-collections/:graphCollectionId/delete', StatisticsController.deleteGraphCollection); + export default statisticsRouter; diff --git a/src/backend/src/services/statistics.services.ts b/src/backend/src/services/statistics.services.ts index c8fea4494b..1c073f53b2 100644 --- a/src/backend/src/services/statistics.services.ts +++ b/src/backend/src/services/statistics.services.ts @@ -316,6 +316,15 @@ export default class StatisticsService { ); } + /** + * Creates a graph collection in the database + * + * @param user The user who is creating the graph collection + * @param title The title of the graph collection that is being created + * @param specialPermissions Any special permissions related to the graph collection + * @param organization The organization the collection is in + * @returns The created graph collection + */ static async createGraphCollection( user: User, title: string, @@ -391,6 +400,16 @@ export default class StatisticsService { ); } + /** + * Edits the given graph collection with the updated values + * + * @param user The user who is editing the graph collection + * @param graphCollectionId The id of the collection that is being edited + * @param title The new title of the collection + * @param specialPermission The new permissions of the collection + * @param organization The organization that the user is currently in + * @returns The updated Graph collection + */ static async editGraphCollection( user: User, graphCollectionId: string, @@ -442,4 +461,99 @@ export default class StatisticsService { ) ); } + + /** + * Removes a graph from the given graph collection + * + * @param user The user who is removing the graph + * @param graphCollectionId The collection that the graph will be removed from + * @param graphId The graph that is being removed + * @param organization The organization the user is currently in + */ + static async removeGraphFromCollection( + user: User, + graphCollectionId: string, + graphId: string, + organization: Organization + ): Promise<{ message: string }> { + if (!(await userHasPermissionNew(user.userId, organization.organizationId, [Permission.EDIT_GRAPH_COLLECTION]))) { + throw new AccessDeniedException('You do not have permission to edit graph collections'); + } + + const graph = await prisma.graph.findUnique({ + where: { id: graphId, organizationId: organization.organizationId } + }); + + if (!graph) { + throw new NotFoundException('Graph', graphId); + } + if (graph.dateDeleted) { + throw new DeletedException('Graph', graphId); + } + + const collection = await prisma.graph_Collection.findUnique({ + where: { id: graphCollectionId, organizationId: organization.organizationId } + }); + + if (!collection) { + throw new NotFoundException('Graph Collection', graphCollectionId); + } + if (collection.dateDeleted) { + throw new DeletedException('Graph Collection', graphCollectionId); + } + + console.log('test'); + + await prisma.graph.update({ + where: { id: graphId }, + data: { + graphCollectionId: null + }, + ...getGraphQueryArgs(organization.organizationId) + }); + + return { message: 'Graph unlinked' }; + } + + /** + * Deletes a graph collection + * + * @param user The user who is deleting the graph collection + * @param graphCollectionId The collection to be deleted + * @param organization The organization the user is currently in + */ + static async deleteGraphCollection( + user: User, + graphCollectionId: string, + organization: Organization + ): Promise<{ message: string }> { + if (!(await userHasPermissionNew(user.userId, organization.organizationId, [Permission.DELETE_GRAPH_COLLECTION]))) { + throw new AccessDeniedException('You do not have permission to edit graph collections'); + } + + const collection = await prisma.graph_Collection.findUnique({ + where: { id: graphCollectionId, organizationId: organization.organizationId } + }); + + if (!collection) { + throw new NotFoundException('Graph Collection', graphCollectionId); + } + if (collection.dateDeleted) { + throw new DeletedException('Graph Collection', graphCollectionId); + } + + await prisma.graph_Collection.update({ + where: { id: graphCollectionId }, + data: { + dateDeleted: new Date(), + userDeleted: { + connect: { + userId: user.userId + } + } + } + }); + + return { message: 'Graph Deleted' }; + } } diff --git a/src/frontend/src/apis/statistics.api.ts b/src/frontend/src/apis/statistics.api.ts index b6f64acf55..9e18d0518a 100644 --- a/src/frontend/src/apis/statistics.api.ts +++ b/src/frontend/src/apis/statistics.api.ts @@ -44,3 +44,11 @@ export const updateGraphCollection = (id: string, payload: GraphCollectionFormIn transformResponse: (data) => graphCollectionTransformer(JSON.parse(data)) }); }; + +export const deleteGraphCollection = (id: string) => { + return axios.delete<{ message: string }>(apiUrls.deleteGraphCollection(id)); +}; + +export const removeGraphFromCollection = (collectionId: string, graphId: string) => { + return axios.post<{ message: string }>(apiUrls.removeGraphFromGraphCollection(collectionId, graphId)); +}; diff --git a/src/frontend/src/components/GraphCollectionCard.tsx b/src/frontend/src/components/GraphCollectionCard.tsx index c245545d89..979c8aec51 100644 --- a/src/frontend/src/components/GraphCollectionCard.tsx +++ b/src/frontend/src/components/GraphCollectionCard.tsx @@ -1,17 +1,43 @@ -import { Card, CardContent, Grid, Link, Typography } from '@mui/material'; +import { Card, CardContent, Grid, IconButton, Link, Typography } from '@mui/material'; import { GraphCollection } from 'shared'; import { datePipe, displayEnum, fullNamePipe } from '../utils/pipes'; import { Link as RouterLink } from 'react-router-dom'; -import { Construction } from '@mui/icons-material'; +import { Construction, Delete } from '@mui/icons-material'; import { DateRangeIcon } from '@mui/x-date-pickers'; +import LoadingIndicator from './LoadingIndicator'; +import { useDeleteGraphCollection } from '../hooks/statistics.hooks'; +import { useToast } from '../hooks/toasts.hooks'; interface GraphCollectionCardProps { graphCollection: GraphCollection; } const GraphCollectionCard = ({ graphCollection }: GraphCollectionCardProps) => { + const { isLoading: removeGraphCollectionIsLoading, mutateAsync: removeGraphCollection } = useDeleteGraphCollection( + graphCollection.id + ); + const toast = useToast(); + + if (removeGraphCollectionIsLoading) { + return <LoadingIndicator />; + } + + const onDeletePressed = async () => { + try { + await removeGraphCollection(); + toast.success('Successfully deleted collection'); + } catch (error) { + if (error instanceof Error) { + toast.error('Failed to delete collection' + error.message); + } + } + }; + return ( - <Card sx={{ width: '100%', borderRadius: 5 }}> + <Card sx={{ width: '100%', borderRadius: 5, position: 'relative' }}> + <IconButton sx={{ position: 'absolute', top: 5, right: 5 }} onClick={onDeletePressed}> + <Delete /> + </IconButton> <CardContent> <Grid container spacing={1}> <Grid item xs={12}> diff --git a/src/frontend/src/hooks/statistics.hooks.ts b/src/frontend/src/hooks/statistics.hooks.ts index 91e3159a14..f6b261c5bf 100644 --- a/src/frontend/src/hooks/statistics.hooks.ts +++ b/src/frontend/src/hooks/statistics.hooks.ts @@ -3,9 +3,11 @@ import { CreateGraphArgs, Graph, GraphCollection, GraphCollectionFormInput } fro import { createGraph, createGraphCollection, + deleteGraphCollection, getAllGraphCollections, getSingleGraph, getSingleGraphCollection, + removeGraphFromCollection, updateGraph, updateGraphCollection } from '../apis/statistics.api'; @@ -118,3 +120,44 @@ export const useUpdateGraphCollection = (id: string) => { } ); }; + +/** + * Custom react hook to remove a graph from a graph collection + * + * @param collectionId The id of the graph collection to update + * @param graphId The id of the graph to remove from the collection + * @returns Mutation function to remove the graph from the collection + */ +export const useRemoveGraphFromCollection = (collectionId: string, graphId: string) => { + const queryClient = useQueryClient(); + return useMutation<{ message: string }, Error>( + [], + async () => { + const { data } = await removeGraphFromCollection(collectionId, graphId); + return data; + }, + { + onSuccess: () => queryClient.invalidateQueries(['graph-collections', collectionId]) + } + ); +}; + +/** + * Custom react hook to delete a graph collection + * + * @param id The id of the graph collection to delete + * @returns Mutation function to delete the graph collection with the given id + */ +export const useDeleteGraphCollection = (id: string) => { + const queryClient = useQueryClient(); + return useMutation<{ message: string }, Error>( + [], + async () => { + const { data } = await deleteGraphCollection(id); + return data; + }, + { + onSuccess: () => queryClient.invalidateQueries(['graph-collections']) + } + ); +}; diff --git a/src/frontend/src/pages/StatisticsPage/GraphView/GraphView.tsx b/src/frontend/src/pages/StatisticsPage/GraphView/GraphView.tsx index ddb6675b88..95073e4256 100644 --- a/src/frontend/src/pages/StatisticsPage/GraphView/GraphView.tsx +++ b/src/frontend/src/pages/StatisticsPage/GraphView/GraphView.tsx @@ -2,12 +2,14 @@ import { Box, IconButton, Typography } from '@mui/material'; import { Graph, GraphDisplayType } from 'shared'; import GraphBarChartView from './GraphBarChartView'; import GraphPieChartView from './GraphPieChartView'; -import { Edit } from '@mui/icons-material'; +import { Delete, Edit } from '@mui/icons-material'; import { useHistory, useParams } from 'react-router-dom'; import { datePipe } from '../../../utils/pipes'; import { useGetCarsByIds } from '../../../hooks/cars.hooks'; import ErrorPage from '../../ErrorPage'; import LoadingIndicator from '../../../components/LoadingIndicator'; +import { useRemoveGraphFromCollection } from '../../../hooks/statistics.hooks'; +import { useToast } from '../../../hooks/toasts.hooks'; interface GraphViewProps { graph: Graph; @@ -18,15 +20,31 @@ const GraphView = ({ graph, height = 500 }: GraphViewProps) => { const history = useHistory(); const { graphCollectionId } = useParams<{ graphCollectionId: string }>(); const { isLoading, data: cars, error, isError } = useGetCarsByIds(new Set(graph.carIds)); + const { isLoading: removeGraphIsLoading, mutateAsync: removeGraph } = useRemoveGraphFromCollection( + graphCollectionId, + graph.graphId + ); + const toast = useToast(); if (isError) { return <ErrorPage error={error} />; } - if (isLoading || !cars) { + if (isLoading || !cars || removeGraphIsLoading) { return <LoadingIndicator />; } + const onRemovePressed = async () => { + try { + await removeGraph(); + toast.success('Successfully removed graph'); + } catch (error) { + if (error instanceof Error) { + toast.error('Failed to remove graph: ' + error.message); + } + } + }; + const Graph = () => { switch (graph.graphDisplayType) { case GraphDisplayType.BAR: @@ -50,6 +68,9 @@ const GraphView = ({ graph, height = 500 }: GraphViewProps) => { > <Edit /> </IconButton> + <IconButton sx={{ height: 40 }} onClick={onRemovePressed}> + <Delete /> + </IconButton> </Box> <Typography textAlign={'center'} fontWeight={'regular'} fontSize={20} variant="h6" noWrap> {!graph.startDate && !graph.endDate diff --git a/src/frontend/src/utils/task.utils.ts b/src/frontend/src/utils/task.utils.ts index f39b4a7fcc..2f2f20500b 100644 --- a/src/frontend/src/utils/task.utils.ts +++ b/src/frontend/src/utils/task.utils.ts @@ -81,6 +81,8 @@ export const taskPriorityColor = (task: Task) => { }; export const getOverdueTasks = (tasks: Task[]) => { - const overdueTasks = new Set(tasks.filter((task) => (task.deadline ? daysOverdue(new Date(task.deadline)) : 0) > 0)); + const overdueTasks = new Set( + tasks.filter((task) => task.status !== TaskStatus.DONE && (task.deadline ? daysOverdue(new Date(task.deadline)) : 0) > 0) + ); return [...overdueTasks]; }; diff --git a/src/frontend/src/utils/urls.ts b/src/frontend/src/utils/urls.ts index 87facb2d31..5b15052cca 100644 --- a/src/frontend/src/utils/urls.ts +++ b/src/frontend/src/utils/urls.ts @@ -237,6 +237,9 @@ const createGraphCollection = () => `${graphCollections()}/create`; const getGraphById = (id: string) => `${statistics()}/graph/${id}`; const updateGraph = (id: string) => `${getGraphById(id)}/edit`; const updateGraphCollection = (id: string) => `${graphCollectionById(id)}/edit`; +const removeGraphFromGraphCollection = (graphCollectionId: string, graphId: string) => + `${graphCollectionById(graphCollectionId)}/remove/${graphId}`; +const deleteGraphCollection = (id: string) => `${graphCollectionById(id)}/delete`; /**************** Other Endpoints ****************/ const version = () => `https://api.github.com/repos/Northeastern-Electric-Racing/FinishLine/releases/latest`; @@ -425,6 +428,8 @@ export const apiUrls = { getGraphById, updateGraph, updateGraphCollection, + removeGraphFromGraphCollection, + deleteGraphCollection, onboarding, allChecklists,