From 9d442dd0b8f449af527f044958c83f5140640963 Mon Sep 17 00:00:00 2001 From: dvala041 <86671843+dvala041@users.noreply.github.com> Date: Sat, 8 Jun 2024 18:50:29 -0400 Subject: [PATCH] Allow supervisors to cancel events (#245) * Added Tanstack Mutation for canceling events and modal * In EventCardNew.tsx we added functionality to display if an event is canceled in red text. Also added a event status column to past events in ViewEvents.tsx * feat: Add functionality to display canceled events and event status column * Add check for past events when disabling buttons * Format * FIX: Cancel event button is grayed out even if event is upcoming and is not canceled. * Add visual alert to EventForm * Remove disablePastEvent functionality * Fix modal * a --------- Co-authored-by: Akinfolami Akin-Alamu Co-authored-by: Jason Zheng --- frontend/src/components/atoms/Alert.tsx | 6 +- frontend/src/components/atoms/Button.tsx | 5 +- .../src/components/organisms/EventCardNew.tsx | 14 ++- .../src/components/organisms/EventForm.tsx | 89 ++++++++++++++++++- .../src/components/organisms/ViewEvents.tsx | 45 ++++++++++ frontend/src/pages/events/[eventid]/edit.tsx | 2 + frontend/src/utils/types.ts | 1 + 7 files changed, 155 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/atoms/Alert.tsx b/frontend/src/components/atoms/Alert.tsx index 330499f9..cf66905e 100644 --- a/frontend/src/components/atoms/Alert.tsx +++ b/frontend/src/components/atoms/Alert.tsx @@ -2,10 +2,11 @@ import React, { ReactNode, forwardRef, Ref } from "react"; import MuiAlert from "@mui/material/Alert"; import CheckCircleIcon from "@mui/icons-material/CheckCircle"; import CancelIcon from "@mui/icons-material/Cancel"; +import ErrorIcon from "@mui/icons-material/Error"; interface AlertProps { children: ReactNode; - variety: "success" | "error"; + variety: "success" | "error" | "warning"; onClose: () => void; [key: string]: any; } @@ -28,6 +29,9 @@ const Alert = forwardRef( bgcolor = "error.light"; icon = ; break; + case "warning": + bgcolor = "warning.light"; + icon = ; } return ( diff --git a/frontend/src/components/atoms/Button.tsx b/frontend/src/components/atoms/Button.tsx index fa29313f..34b8cc20 100644 --- a/frontend/src/components/atoms/Button.tsx +++ b/frontend/src/components/atoms/Button.tsx @@ -5,7 +5,7 @@ import CircularProgress from "@mui/material/CircularProgress"; interface ButtonProps { children: ReactNode; icon?: ReactNode; - variety?: "primary" | "secondary" | "tertiary" | "error"; + variety?: "primary" | "secondary" | "tertiary" | "error" | "mainError"; size?: "small" | "medium"; loading?: boolean; [key: string]: any; @@ -53,6 +53,9 @@ const Button = ({ variant = "outlined"; color = "error"; break; + case "mainError": + variant = "contained"; + color = "error"; } return ( diff --git a/frontend/src/components/organisms/EventCardNew.tsx b/frontend/src/components/organisms/EventCardNew.tsx index e8eda813..62562b0f 100644 --- a/frontend/src/components/organisms/EventCardNew.tsx +++ b/frontend/src/components/organisms/EventCardNew.tsx @@ -8,6 +8,7 @@ import { ViewEventsEvent } from "@/utils/types"; import { format } from "date-fns"; import Link from "next/link"; import { displayDateInfo } from "@/utils/helpers"; +import Chip from "../atoms/Chip"; interface EventCardNewProps { event: ViewEventsEvent; @@ -18,7 +19,12 @@ const EventCardContent = ({ event }: EventCardNewProps) => { const formattedEndTime = format(new Date(event.endDate), "hh:mm a"); const timeRange = `${formattedStartTime} - ${formattedEndTime}`; const date = new Date(event.startDate); - const dateInfo = displayDateInfo(date); + const dateInfo = + event.status === "CANCELED" ? ( + + ) : ( + displayDateInfo(date) + ); const url = event.role === "Supervisor" ? `/events/${event.id}/attendees` @@ -29,7 +35,11 @@ const EventCardContent = ({ event }: EventCardNewProps) => { return (
-
+
}> {dateInfo} diff --git a/frontend/src/components/organisms/EventForm.tsx b/frontend/src/components/organisms/EventForm.tsx index 598c23b0..4f3325f7 100644 --- a/frontend/src/components/organisms/EventForm.tsx +++ b/frontend/src/components/organisms/EventForm.tsx @@ -26,6 +26,8 @@ import { api } from "@/utils/api"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import Dropzone from "../atoms/Dropzone"; import EditorComp from "@/components/atoms/Editor"; +import Modal from "../molecules/Modal"; +import Alert from "../atoms/Alert"; interface EventFormProps { eventId?: string | string[] | undefined; @@ -45,6 +47,7 @@ type FormValues = { startTime: Date; endTime: Date; mode: string; + status: string; }; /** An EventForm page */ @@ -86,6 +89,10 @@ const EventForm = ({ setStatus(status); }; + type modalBodyProps = { + handleClose: () => void; + }; + /** React hook form */ const { register, @@ -116,6 +123,11 @@ const EventForm = ({ /** Handles form errors for time and date validation */ const [errorMessage, setErrorMessage] = useState(null); + /** Handles for cancelling an event */ + const [open, setOpen] = useState(false); + const handleOpen = () => setOpen(true); + const handleClose = () => setOpen(false); + /** Tanstack mutation for creating a new event */ const { mutateAsync: handleCreateNewEvent, @@ -210,6 +222,25 @@ const EventForm = ({ }, }); + /** Tanstack mutation for canceling an event */ + const { mutateAsync: handleCancelEventAsync, isPending: cancelEventPending } = + useMutation({ + mutationFn: async () => { + const { response } = await api.patch(`/events/${eventId}/status`, { + status: "CANCELED", + }); + return response; + }, + retry: false, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: ["event", eventId], + }); + localStorage.setItem("eventCanceled", "true"); + router.push("/events/view"); + }, + }); + /** Helper for handling creating events */ const handleCreateEvent: SubmitHandler = async (data) => { try { @@ -230,8 +261,49 @@ const EventForm = ({ } }; + /** Helper for handling canceling events */ + const handleCancelEvent = async () => { + try { + await handleCancelEventAsync(); + } catch (error) { + setErrorNotificationOpen(true); + setErrorMessage("We were unable to cancel this event. Please try again"); + } + }; + + /** Confirmation modal for canceling an event */ + const ModalBody = ({ handleClose }: modalBodyProps) => { + return ( +
+

+ Are you sure you want to cancel this event? +

+
+
+ +
+
+ +
+
+
+ ); + }; + + // Check if this event has been canceled + const thisEventHasBeenCanceled = eventDetails?.status === "CANCELED"; + return ( <> + } + /> {/* Error component */} Error: {errorMessage} + {thisEventHasBeenCanceled && ( +
+ This event has been canceled. +
+ )}
Go back
- {/* TODO: Add functionality */}
- +
diff --git a/frontend/src/components/organisms/ViewEvents.tsx b/frontend/src/components/organisms/ViewEvents.tsx index 01a28440..fa9ea00b 100644 --- a/frontend/src/components/organisms/ViewEvents.tsx +++ b/frontend/src/components/organisms/ViewEvents.tsx @@ -63,6 +63,7 @@ const UpcomingEvents = () => { endDate: event["endDate"], role: "Supervisor", hours: eventHours(event["startDate"], event["endDate"]), + status: event["status"], imageURL: event["imageURL"], }; } @@ -79,6 +80,7 @@ const UpcomingEvents = () => { role: "Volunteer", hours: eventHours(event["startDate"], event["endDate"]), imageURL: event["imageURL"], + status: event["status"], }; } ) || []; @@ -213,6 +215,7 @@ const PastEvents = () => { endDate: event["endDate"], role: "Supervisor", hours: eventHours(event["endDate"], event["startDate"]), + status: event["status"], }); }); @@ -225,6 +228,7 @@ const PastEvents = () => { endDate: event["endDate"], role: "Volunteer", hours: eventHours(event["endDate"], event["startDate"]), + status: event["status"], }); }); @@ -311,6 +315,19 @@ const PastEvents = () => { renderHeader: (params) => (
{params.colDef.headerName}
), + renderCell: (params) => ( +
+ {params.row.name} + {params.row.status == "CANCELED" && ( + + )} +
+ ), }, { field: "startDate", @@ -373,6 +390,19 @@ const PastEvents = () => { renderHeader: (params) => (
{params.colDef.headerName}
), + renderCell: (params) => ( +
+ {params.row.name} + {params.row.status == "CANCELED" && ( + + )} +
+ ), }, { field: "startDate", @@ -519,6 +549,7 @@ const ViewEvents = () => { const [isEventCreated, setIsEventCreated] = useState(false); const [isEventEdited, setIsEventEdited] = useState(false); + const [isEventCanceled, setIsEventCanceled] = useState(false); useEffect(() => { const isEventCreated = localStorage.getItem("eventCreated"); @@ -530,6 +561,10 @@ const ViewEvents = () => { setIsEventEdited(true); localStorage.removeItem("eventEdited"); } + if (localStorage.getItem("eventCanceled")) { + setIsEventCanceled(true); + localStorage.removeItem("eventCanceled"); + } }, []); return ( @@ -551,6 +586,16 @@ const ViewEvents = () => { > Your event has been successfully updated! + + {/* Event canceled success notification */} + setIsEventCanceled(false)} + > + Your event has been successfully canceled! + + My Events
} diff --git a/frontend/src/pages/events/[eventid]/edit.tsx b/frontend/src/pages/events/[eventid]/edit.tsx index 6b469c8c..901ddb17 100644 --- a/frontend/src/pages/events/[eventid]/edit.tsx +++ b/frontend/src/pages/events/[eventid]/edit.tsx @@ -20,6 +20,7 @@ type eventData = { startTime: Date; endTime: Date; mode: string; + status: string; }; /** An EditEvent page */ @@ -48,6 +49,7 @@ const EditEvent = () => { startTime: data?.startDate, endTime: data?.endDate, mode: data?.mode, + status: data?.status, }; /** Loading screen */ diff --git a/frontend/src/utils/types.ts b/frontend/src/utils/types.ts index 49a33915..ce00952e 100644 --- a/frontend/src/utils/types.ts +++ b/frontend/src/utils/types.ts @@ -11,6 +11,7 @@ export type ViewEventsEvent = { ownerId?: string; description?: string; capacity?: number; + status?: EventStatus; imageURL?: string; };