diff --git a/README.md b/README.md index eb16bb8e..9ce5edc0 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,6 @@

- -
Table of Contents @@ -35,24 +33,24 @@
+ +## About the Project - -## About The Project -This project aims to develop a volunteer management system for Lagos Food Bank Initiative. The system will allow volunteers to sign up for shifts, and for LFBI to manage volunteers and hours. +This project aims to develop a volunteer management system for Lagos Food Bank Initiative. The system will allow volunteers to sign up for shifts, and for LFBI to manage volunteers and hours. ### Built With -* [![Next][Next.js]][Next-url] -* [![React][React.js]][React-url] -* [![Express][Express.js]][Express-url] -* [![Postresql][Prisma.io]][Prisma-url] +- [![Next][Next.js]][Next-url] +- [![React][React.js]][React-url] +- [![Express][Express.js]][Express-url] +- [![Postresql][Prisma.io]][Prisma-url] -## Getting Started +## Getting Started -> Folder structure +> Folder structure . ├── frontend # Next.js client @@ -60,9 +58,10 @@ This project aims to develop a volunteer management system for Lagos Food Bank I └── README.md ### Prerequisites -* Nodejs -* PostreSQL -* Docker + +- Nodejs +- PostreSQL +- Docker ### Installation @@ -85,9 +84,60 @@ This project aims to develop a volunteer management system for Lagos Food Bank I > Note: See individual project files for more information on how to build and deploy the project. +## Contributors + +### Spring 2024 + +- Leads + - Akinfolami Akin-Alamu + - Jason Zheng +- Developers + - Arushi Aggarwal + - Owen Chen + - Hubert He + - Trung-Nghia Le + - Brandon Lerit + - Diego Marques + - Tanvi Mavani + - David Valarezo + +### Fall 2023 + +- Leads + - Akinfolami Akin-Alamu + - Jason Zheng +- Developers + - Sneha Rajaraman + - Daniel Thorne + - Louis Valencia + - Sophie Wang + - Yichen Yao + - Hannah Zhang +- Designers + - Ella Keen Allee + - Bella Besuud + - Mika Labadan + +### Spring 2023 + +- Leads + - Akinfolami Akin-Alamu + - Jason Zheng +- Developers + - Jiayi Bai + - Daniel Botros + - Sneha Rajaraman + - Sophie Wang + - Yichen Yao + - Hannah Zhang +- Designers + - Bella Besuud + - Mika Labadan + - Julia Papp + [Next.js]: https://img.shields.io/badge/next.js-000000?style=for-the-badge&logo=nextdotjs&logoColor=white [Next-url]: https://nextjs.org/ [React.js]: https://img.shields.io/badge/React-20232A?style=for-the-badge&logo=react&logoColor=61DAFB @@ -96,4 +146,3 @@ This project aims to develop a volunteer management system for Lagos Food Bank I [Express.js]: https://img.shields.io/badge/express.js-%23404d59.svg?style=for-the-badge&logo=express&logoColor=%2361DAFB [Express-url]: https://expressjs.com/ [Prisma-url]: https://www.prisma.io/ - diff --git a/backend/src/events/controllers.ts b/backend/src/events/controllers.ts index f28729a2..fd89ea90 100644 --- a/backend/src/events/controllers.ts +++ b/backend/src/events/controllers.ts @@ -368,11 +368,14 @@ const addAttendee = async (eventID: string, userID: string) => { eventLocation, textBody ); - await sendEmail( - userEmail, - "Your registration was successful.", - updatedHtml - ); + const userPreferences = await userController.getUserPreferences(userID); + if (userPreferences?.preferences?.sendEmailNotification === true) { + await sendEmail( + userEmail, + "Your registration was successful.", + updatedHtml + ); + } } if (eventIsInThePast) { return Promise.reject("Event is past, cannot enroll new user"); @@ -425,11 +428,14 @@ const deleteAttendee = async ( eventLocation, textBody ); - await sendEmail( - userEmail, - "Your event cancellation was successful.", - updatedHtml - ); + const userPreferences = await userController.getUserPreferences(userID); + if (userPreferences?.preferences?.sendEmailNotification === true) { + await sendEmail( + userEmail, + "Your event cancellation was successful.", + updatedHtml + ); + } } // update db @@ -511,11 +517,14 @@ const confirmUser = async (eventID: string, userID: string) => { eventLocation, textBody ); - await sendEmail( - userEmail, - "Your attendance has been confirmed", - updatedHtml - ); + const userPreferences = await userController.getUserPreferences(userID); + if (userPreferences?.preferences?.sendEmailNotification === true) { + await sendEmail( + userEmail, + "Your attendance has been confirmed", + updatedHtml + ); + } } return await prisma.eventEnrollment.update({ diff --git a/backend/src/users/controllers.ts b/backend/src/users/controllers.ts index 0bb21384..cf8f7ff0 100644 --- a/backend/src/users/controllers.ts +++ b/backend/src/users/controllers.ts @@ -231,6 +231,7 @@ const getUsers = async ( }, include: { profile: true, + preferences: true, events: eventId ? { where: { @@ -605,20 +606,23 @@ const editRole = async (userId: string, role: string) => { var textBodyVS = "Your role has changed from volunteer to supervisor."; if (process.env.NODE_ENV != "test") { - if (prevUserRole === "SUPERVISOR" && role === "ADMIN") { - const updatedHtml = replaceUserInputs( - stringUserUpdate, - userName, - textBodySA - ); - await sendEmail(userEmail, "Your email subject", updatedHtml); - } else if (prevUserRole === "VOLUNTEER" && role === "SUPERVISOR") { - const updatedHtml = replaceUserInputs( - stringUserUpdate, - userName, - textBodyVS - ); - await sendEmail(userEmail, "Your role has changed.", updatedHtml); + const userPreferences = await userController.getUserPreferences(userId); + if (userPreferences?.preferences?.sendEmailNotification === true) { + if (prevUserRole === "SUPERVISOR" && role === "ADMIN") { + const updatedHtml = replaceUserInputs( + stringUserUpdate, + userName, + textBodySA + ); + await sendEmail(userEmail, "Your email subject", updatedHtml); + } else if (prevUserRole === "VOLUNTEER" && role === "SUPERVISOR") { + const updatedHtml = replaceUserInputs( + stringUserUpdate, + userName, + textBodyVS + ); + await sendEmail(userEmail, "Your role has changed.", updatedHtml); + } } } return prisma.user.update({ 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 1e8f2b30..6743ec80 100644 --- a/frontend/src/components/organisms/EventForm.tsx +++ b/frontend/src/components/organisms/EventForm.tsx @@ -27,6 +27,8 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import Dropzone from "../atoms/Dropzone"; import Alert from "../atoms/Alert"; import EditorComp from "@/components/atoms/Editor"; +import Modal from "../molecules/Modal"; +import Alert from "../atoms/Alert"; interface EventFormProps { eventId?: string | string[] | undefined; @@ -46,6 +48,7 @@ type FormValues = { startTime: Date; endTime: Date; mode: string; + status: string; }; /** An EventForm page */ @@ -87,6 +90,10 @@ const EventForm = ({ setStatus(status); }; + type modalBodyProps = { + handleClose: () => void; + }; + /** React hook form */ const { register, @@ -117,6 +124,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, @@ -211,6 +223,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 { @@ -236,9 +267,49 @@ const EventForm = ({ const disableEditEvent = eventDetails ? new Date(eventDetails?.startDate) < currentDate : false; + /** 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 */}
-
@@ -453,7 +537,11 @@ const EventForm = ({ diff --git a/frontend/src/components/organisms/ProfileForm.tsx b/frontend/src/components/organisms/ProfileForm.tsx index a35ecdd3..d4b7ef9a 100644 --- a/frontend/src/components/organisms/ProfileForm.tsx +++ b/frontend/src/components/organisms/ProfileForm.tsx @@ -10,6 +10,7 @@ import { api } from "@/utils/api"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { updatePassword } from "firebase/auth"; import { User } from "firebase/auth"; +import { Controller } from "react-hook-form"; type FormValues = { email: string; @@ -34,6 +35,7 @@ type formData = { verified?: boolean; disciplinaryNotices?: number; imageUrl?: string; + sendEmailNotification: boolean; }; interface ProfileFormProps { @@ -76,6 +78,7 @@ const ProfileForm = ({ userDetails }: ProfileFormProps) => { /** React hook form */ const { register, + control, handleSubmit, watch, reset, @@ -89,18 +92,12 @@ const ProfileForm = ({ userDetails }: ProfileFormProps) => { oldPassword: "", newPassword: "", confirmNewPassword: "", - emailNotifications: false, + emailNotifications: userDetails.sendEmailNotification, }, }); /** Handles checkbox */ - // TODO: Implement this - const [checked, setChecked] = useState(false); - const handleCheckbox = () => { - setChecked((checked) => !checked); - }; - /** Tanstack query mutation to reauthenticate the user session */ const ReAuthenticateUserSession = useMutation({ mutationFn: async (data: any) => { @@ -140,12 +137,26 @@ const ProfileForm = ({ userDetails }: ProfileFormProps) => { retry: false, }); + /** Tanstack query mutation to update the user profile */ + const updatePreferencesInDB = useMutation({ + mutationFn: async (emailNotifications: boolean) => { + return api.put(`/users/${userDetails.id}/preferences`, { + sendEmailNotification: emailNotifications, + }); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["profile"] }); + }, + retry: false, + }); + /** Handles form submit */ const handleChanges: SubmitHandler = async (data) => { try { await ReAuthenticateUserSession.mutateAsync(data); await updateUserPasswordInFirebase.mutateAsync(data); await updateProfileInDB.mutateAsync(data); + await updatePreferencesInDB.mutateAsync(data.emailNotifications); setSuccessNotificationOpen(true); } catch (error: any) { setErrorNotificationOpen(true); @@ -254,10 +265,16 @@ const ProfileForm = ({ userDetails }: ProfileFormProps) => { }, })} /> - ( + + )} />
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/pages/profile.tsx b/frontend/src/pages/profile.tsx index a6635312..ed9fabe2 100644 --- a/frontend/src/pages/profile.tsx +++ b/frontend/src/pages/profile.tsx @@ -20,8 +20,8 @@ const Profile = () => { const { data, isLoading, isError } = useQuery({ queryKey: ["profile", user?.email], queryFn: async () => { - const { data } = await api.get(`/users/search/?email=${user?.email}`); - return data["data"][0]; + const { data } = await api.get(`/users?email=${user?.email}`); + return data["data"]["result"][0]; }, }); @@ -102,6 +102,7 @@ const Profile = () => { diff --git a/frontend/src/utils/helpers.ts b/frontend/src/utils/helpers.ts index 82056a2f..879103c1 100644 --- a/frontend/src/utils/helpers.ts +++ b/frontend/src/utils/helpers.ts @@ -17,8 +17,8 @@ import { * @returns the userid */ export const fetchUserIdFromDatabase = async (email: string) => { - const { data } = await api.get(`/users/search/?email=${email}`); - return data["data"][0]["id"]; + const { data } = await api.get(`/users?email=${email}`); + return data["data"]["result"][0]["id"]; }; /** 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; };