From 03124d39f630fe0dd9ff0de9e07d4db0fa909b35 Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Mon, 18 Nov 2024 17:06:43 -0500 Subject: [PATCH 01/41] #2998-set up get unread notifications endpoint --- .../src/controllers/users.controllers.ts | 12 +++++ .../notifications.query-args.ts | 11 ++++ .../migration.sql | 51 +++++++++++++++++++ src/backend/src/prisma/schema.prisma | 2 +- src/backend/src/routes/users.routes.ts | 1 + src/backend/src/services/users.services.ts | 12 +++++ .../transformers/notification.transformer.ts | 12 +++++ src/shared/index.ts | 1 + src/shared/src/types/notification.types.ts | 4 ++ 9 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 src/backend/src/prisma-query-args/notifications.query-args.ts create mode 100644 src/backend/src/prisma/migrations/20241118211532_updated_notifications/migration.sql create mode 100644 src/backend/src/transformers/notification.transformer.ts create mode 100644 src/shared/src/types/notification.types.ts diff --git a/src/backend/src/controllers/users.controllers.ts b/src/backend/src/controllers/users.controllers.ts index 5cfa740368..896648bd4c 100644 --- a/src/backend/src/controllers/users.controllers.ts +++ b/src/backend/src/controllers/users.controllers.ts @@ -191,4 +191,16 @@ export default class UsersController { return next(error); } } + + static async getUserUnreadNotifications(req: Request, res: Response, next: NextFunction) { + try { + const { userId } = req.params; + const { organization } = req; + + const unreadNotifications = await UsersService.getUserUnreadNotifications(userId, organization); + return res.status(200).json(unreadNotifications); + } catch (error: unknown) { + return next(error); + } + } } diff --git a/src/backend/src/prisma-query-args/notifications.query-args.ts b/src/backend/src/prisma-query-args/notifications.query-args.ts new file mode 100644 index 0000000000..9081f717cc --- /dev/null +++ b/src/backend/src/prisma-query-args/notifications.query-args.ts @@ -0,0 +1,11 @@ +import { Prisma } from '@prisma/client'; +import { getUserQueryArgs } from './user.query-args'; + +export type NotificationQueryArgs = ReturnType; + +export const getNotificationQueryArgs = (organizationId: string) => + Prisma.validator()({ + include: { + usersReceived: getUserQueryArgs(organizationId) + } + }); diff --git a/src/backend/src/prisma/migrations/20241118211532_updated_notifications/migration.sql b/src/backend/src/prisma/migrations/20241118211532_updated_notifications/migration.sql new file mode 100644 index 0000000000..192243b385 --- /dev/null +++ b/src/backend/src/prisma/migrations/20241118211532_updated_notifications/migration.sql @@ -0,0 +1,51 @@ +/* + Warnings: + + - You are about to drop the column `userId` on the `Notification` table. All the data in the column will be lost. + - Added the required column `text` to the `Announcement` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropForeignKey +ALTER TABLE "Notification" DROP CONSTRAINT "Notification_userId_fkey"; + +-- AlterTable +ALTER TABLE "Announcement" ADD COLUMN "text" TEXT NOT NULL; + +-- AlterTable +ALTER TABLE "Notification" DROP COLUMN "userId"; + +-- CreateTable +CREATE TABLE "_ReceivedAnnouncements" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL +); + +-- CreateTable +CREATE TABLE "_NotificationToUser" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "_ReceivedAnnouncements_AB_unique" ON "_ReceivedAnnouncements"("A", "B"); + +-- CreateIndex +CREATE INDEX "_ReceivedAnnouncements_B_index" ON "_ReceivedAnnouncements"("B"); + +-- CreateIndex +CREATE UNIQUE INDEX "_NotificationToUser_AB_unique" ON "_NotificationToUser"("A", "B"); + +-- CreateIndex +CREATE INDEX "_NotificationToUser_B_index" ON "_NotificationToUser"("B"); + +-- AddForeignKey +ALTER TABLE "_ReceivedAnnouncements" ADD CONSTRAINT "_ReceivedAnnouncements_A_fkey" FOREIGN KEY ("A") REFERENCES "Announcement"("announcementId") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_ReceivedAnnouncements" ADD CONSTRAINT "_ReceivedAnnouncements_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("userId") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_NotificationToUser" ADD CONSTRAINT "_NotificationToUser_A_fkey" FOREIGN KEY ("A") REFERENCES "Notification"("notificationId") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_NotificationToUser" ADD CONSTRAINT "_NotificationToUser_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("userId") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/src/backend/src/prisma/schema.prisma b/src/backend/src/prisma/schema.prisma index 112f7a6936..7023bbea12 100644 --- a/src/backend/src/prisma/schema.prisma +++ b/src/backend/src/prisma/schema.prisma @@ -944,5 +944,5 @@ model Notification { notificationId String @id @default(uuid()) text String iconName String - users User[] + usersReceived User[] } diff --git a/src/backend/src/routes/users.routes.ts b/src/backend/src/routes/users.routes.ts index 2f95201f6f..34ae1a0136 100644 --- a/src/backend/src/routes/users.routes.ts +++ b/src/backend/src/routes/users.routes.ts @@ -54,5 +54,6 @@ userRouter.post( validateInputs, UsersController.getManyUserTasks ); +userRouter.get('/:userId/notifications', UsersController.getUserUnreadNotifications); export default userRouter; diff --git a/src/backend/src/services/users.services.ts b/src/backend/src/services/users.services.ts index 4c7d2b4007..0b8e06b096 100644 --- a/src/backend/src/services/users.services.ts +++ b/src/backend/src/services/users.services.ts @@ -38,6 +38,8 @@ import { getAuthUserQueryArgs } from '../prisma-query-args/auth-user.query-args' import authenticatedUserTransformer from '../transformers/auth-user.transformer'; import { getTaskQueryArgs } from '../prisma-query-args/tasks.query-args'; import taskTransformer from '../transformers/tasks.transformer'; +import { getNotificationQueryArgs } from '../prisma-query-args/notifications.query-args'; +import notificationTransformer from '../transformers/notification.transformer'; export default class UsersService { /** @@ -571,4 +573,14 @@ export default class UsersService { const resolvedTasks = await Promise.all(tasksPromises); return resolvedTasks.flat(); } + + static async getUserUnreadNotifications(userId: string, organization: Organization) { + const requestedUser = await prisma.user.findUnique({ + where: { userId }, + include: { unreadNotifications: getNotificationQueryArgs(organization.organizationId) } + }); + if (!requestedUser) throw new NotFoundException('User', userId); + + return requestedUser.unreadNotifications.map(notificationTransformer); + } } diff --git a/src/backend/src/transformers/notification.transformer.ts b/src/backend/src/transformers/notification.transformer.ts new file mode 100644 index 0000000000..24e4347b20 --- /dev/null +++ b/src/backend/src/transformers/notification.transformer.ts @@ -0,0 +1,12 @@ +import { Prisma } from '@prisma/client'; +import { NotificationQueryArgs } from '../prisma-query-args/notifications.query-args'; +import { Notification } from 'shared'; + +const notificationTransformer = (notification: Prisma.NotificationGetPayload): Notification => { + return { + text: notification.text, + iconName: notification.iconName + }; +}; + +export default notificationTransformer; diff --git a/src/shared/index.ts b/src/shared/index.ts index 1d3f6c399f..763ce5e09e 100644 --- a/src/shared/index.ts +++ b/src/shared/index.ts @@ -11,6 +11,7 @@ export * from './src/types/team-types'; export * from './src/types/task-types'; export * from './src/types/reimbursement-requests-types'; export * from './src/types/design-review-types'; +export * from './src/types/notification.types'; export * from './src/validate-wbs'; export * from './src/date-utils'; diff --git a/src/shared/src/types/notification.types.ts b/src/shared/src/types/notification.types.ts new file mode 100644 index 0000000000..2d7260d35f --- /dev/null +++ b/src/shared/src/types/notification.types.ts @@ -0,0 +1,4 @@ +export interface Notification { + text: String; + iconName: String; +} From 6ae13e3dafc44cd553a59a524992146eb54a888a Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Tue, 19 Nov 2024 10:05:43 -0500 Subject: [PATCH 02/41] #2998-wrote tests --- src/backend/tests/unmocked/users.test.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/backend/tests/unmocked/users.test.ts b/src/backend/tests/unmocked/users.test.ts index b26a941bd5..80e5c13b2d 100644 --- a/src/backend/tests/unmocked/users.test.ts +++ b/src/backend/tests/unmocked/users.test.ts @@ -72,4 +72,24 @@ describe('User Tests', () => { expect(batmanWithNotifications?.unreadNotifications[1].text).toBe('test2'); }); }); + + describe('Get Notifications', () => { + it('fails on invalid user id', async () => { + await expect(async () => await UsersService.getUserUnreadNotifications('1', organization)).rejects.toThrow( + new NotFoundException('User', '1') + ); + }); + + it('Succeeds and gets user notifications', async () => { + const testBatman = await createTestUser(batmanAppAdmin, orgId); + await UsersService.sendNotification(testBatman.userId, 'test1', 'test1'); + await UsersService.sendNotification(testBatman.userId, 'test2', 'test2'); + + const notifications = await UsersService.getUserUnreadNotifications(testBatman.userId, organization); + + expect(notifications).toHaveLength(2); + expect(notifications[0].text).toBe('test1'); + expect(notifications[1].text).toBe('test2'); + }); + }); }); From 6875955c608bea1b2dbb77d8800398235e75c423 Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Tue, 19 Nov 2024 11:47:30 -0500 Subject: [PATCH 03/41] #3000-set up hooks --- .../transformers/notifications.transformers.ts | 13 +++++++++++++ src/frontend/src/apis/users.api.ts | 18 ++++++++++++++++++ src/frontend/src/hooks/users.hooks.ts | 18 ++++++++++++++++-- src/frontend/src/utils/urls.ts | 4 ++++ 4 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 src/frontend/src/apis/transformers/notifications.transformers.ts diff --git a/src/frontend/src/apis/transformers/notifications.transformers.ts b/src/frontend/src/apis/transformers/notifications.transformers.ts new file mode 100644 index 0000000000..b429b61efc --- /dev/null +++ b/src/frontend/src/apis/transformers/notifications.transformers.ts @@ -0,0 +1,13 @@ +import { Notification } from 'shared'; + +/** + * Transforms a notification + * + * @param notification Incoming task object supplied by the HTTP response. + * @returns Properly transformed notification object. + */ +export const notificationTransformer = (notification: Notification): Notification => { + return { + ...notification + }; +}; diff --git a/src/frontend/src/apis/users.api.ts b/src/frontend/src/apis/users.api.ts index afa5ea00f6..4ad76fe84c 100644 --- a/src/frontend/src/apis/users.api.ts +++ b/src/frontend/src/apis/users.api.ts @@ -5,6 +5,7 @@ import axios from '../utils/axios'; import { + Notification, Project, SetUserScheduleSettingsPayload, Task, @@ -23,6 +24,7 @@ import { import { AuthenticatedUser, UserSettings } from 'shared'; import { projectTransformer } from './transformers/projects.transformers'; import { taskTransformer } from './transformers/tasks.transformers'; +import notificationTransformer from '../../../backend/src/transformers/notification.transformer'; /** * Fetches all users. @@ -159,3 +161,19 @@ export const getManyUserTasks = (userIds: string[]) => { } ); }; + +/* + * Sends a notification to the user with the given id + */ +export const sendNotification = (id: string, notification: Notification) => { + return axios.post(apiUrls.userSendNotifications(id), notification); +}; + +/* + * Gets all unread notifications of the user with the given id + */ +export const getNotifications = (id: string) => { + return axios.get(apiUrls.userNotifications(id), { + transformResponse: (data) => notificationTransformer(JSON.parse(data)) + }); +}; diff --git a/src/frontend/src/hooks/users.hooks.ts b/src/frontend/src/hooks/users.hooks.ts index 96b659c1f1..e2278ee136 100644 --- a/src/frontend/src/hooks/users.hooks.ts +++ b/src/frontend/src/hooks/users.hooks.ts @@ -19,7 +19,8 @@ import { getUserScheduleSettings, updateUserScheduleSettings, getUserTasks, - getManyUserTasks + getManyUserTasks, + getNotifications } from '../apis/users.api'; import { User, @@ -31,7 +32,8 @@ import { UserScheduleSettings, UserWithScheduleSettings, SetUserScheduleSettingsPayload, - Task + Task, + Notification } from 'shared'; import { useAuth } from './auth.hooks'; import { useContext } from 'react'; @@ -260,3 +262,15 @@ export const useManyUserTasks = (userIds: string[]) => { return data; }); }; + +/** + * Curstom react hook to get all unread notifications from a user + * @param userId id of user to get unread notifications from + * @returns + */ +export const useUserNotifications = (userId: string) => { + return useQuery(['users', userId, 'notifications'], async () => { + const { data } = await getNotifications(userId); + return data; + }); +}; diff --git a/src/frontend/src/utils/urls.ts b/src/frontend/src/utils/urls.ts index 1b00a9e46b..cffa6050ac 100644 --- a/src/frontend/src/utils/urls.ts +++ b/src/frontend/src/utils/urls.ts @@ -26,6 +26,8 @@ const userScheduleSettings = (id: string) => `${usersById(id)}/schedule-settings const userScheduleSettingsSet = () => `${users()}/schedule-settings/set`; const userTasks = (id: string) => `${usersById(id)}/tasks`; const manyUserTasks = () => `${users()}/tasks/get-many`; +const userSendNotifications = (id: string) => `${usersById(id)}/notifications/send`; +const userNotifications = (id: string) => `${usersById(id)}/notifications`; /**************** Projects Endpoints ****************/ const projects = () => `${API_URL}/projects`; @@ -211,6 +213,8 @@ export const apiUrls = { userScheduleSettingsSet, userTasks, manyUserTasks, + userSendNotifications, + userNotifications, projects, allProjects, From fc44a6c70879c1e646989e228aedfe2756676f6f Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Tue, 19 Nov 2024 20:53:47 -0500 Subject: [PATCH 04/41] #3000-created notification card --- src/backend/src/prisma/seed.ts | 3 + src/frontend/src/apis/users.api.ts | 2 +- .../src/components/NotificationCard.tsx | 59 +++++++++++++++++++ .../src/pages/HomePage/AdminHomePage.tsx | 16 ++++- 4 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 src/frontend/src/components/NotificationCard.tsx diff --git a/src/backend/src/prisma/seed.ts b/src/backend/src/prisma/seed.ts index a30e502baf..451cb37462 100644 --- a/src/backend/src/prisma/seed.ts +++ b/src/backend/src/prisma/seed.ts @@ -1893,6 +1893,9 @@ const performSeed: () => Promise = async () => { await RecruitmentServices.createFaq(batman, 'When was FinishLine created?', 'FinishLine was created in 2019', ner); await RecruitmentServices.createFaq(batman, 'How many developers are working on FinishLine?', '178 as of 2024', ner); + + await UsersService.sendNotification(thomasEmrax.userId, 'testing notifications', 'star'); + await UsersService.sendNotification(thomasEmrax.userId, 'testing notifications #2', 'star'); }; performSeed() diff --git a/src/frontend/src/apis/users.api.ts b/src/frontend/src/apis/users.api.ts index 4ad76fe84c..3bcc945281 100644 --- a/src/frontend/src/apis/users.api.ts +++ b/src/frontend/src/apis/users.api.ts @@ -174,6 +174,6 @@ export const sendNotification = (id: string, notification: Notification) => { */ export const getNotifications = (id: string) => { return axios.get(apiUrls.userNotifications(id), { - transformResponse: (data) => notificationTransformer(JSON.parse(data)) + transformResponse: (data) => JSON.parse(data) }); }; diff --git a/src/frontend/src/components/NotificationCard.tsx b/src/frontend/src/components/NotificationCard.tsx new file mode 100644 index 0000000000..407a17e250 --- /dev/null +++ b/src/frontend/src/components/NotificationCard.tsx @@ -0,0 +1,59 @@ +import { Box, Icon, IconButton, Typography, useTheme } from '@mui/material'; +import React from 'react'; +import { Notification } from 'shared'; +import CloseIcon from '@mui/icons-material/Close'; + +interface NotificationCardProps { + notification: Notification; +} + +const NotificationCard: React.FC = ({ notification }) => { + const theme = useTheme(); + return ( + + + + {notification.iconName} + + + + {notification.text} + + + + + + ); +}; + +export default NotificationCard; diff --git a/src/frontend/src/pages/HomePage/AdminHomePage.tsx b/src/frontend/src/pages/HomePage/AdminHomePage.tsx index 4c1bf340f9..5c06b32b11 100644 --- a/src/frontend/src/pages/HomePage/AdminHomePage.tsx +++ b/src/frontend/src/pages/HomePage/AdminHomePage.tsx @@ -4,11 +4,12 @@ */ import { Typography } from '@mui/material'; -import { useSingleUserSettings } from '../../hooks/users.hooks'; +import { useSingleUserSettings, useUserNotifications } from '../../hooks/users.hooks'; import LoadingIndicator from '../../components/LoadingIndicator'; import ErrorPage from '../ErrorPage'; import PageLayout from '../../components/PageLayout'; import { AuthenticatedUser } from 'shared'; +import NotificationCard from '../../components/NotificationCard'; interface AdminHomePageProps { user: AuthenticatedUser; @@ -16,12 +17,23 @@ interface AdminHomePageProps { const AdminHomePage = ({ user }: AdminHomePageProps) => { const { isLoading, isError, error, data: userSettingsData } = useSingleUserSettings(user.userId); + const { + data: notifications, + isLoading: notificationsIsLoading, + error: notificationsError, + isError: notificationsIsError + } = useUserNotifications(user.userId); - if (isLoading || !userSettingsData) return ; + if (isLoading || !userSettingsData || notificationsIsLoading || !notifications) return ; if (isError) return ; + if (notificationsIsError) return ; + + const currentNotification = notifications.length > 0 ? notifications[0] : undefined; + if (!currentNotification) return ; return ( + {currentNotification && } Welcome, {user.firstName}! From 375df2e957eca01cb053e9e6044cedaea978947e Mon Sep 17 00:00:00 2001 From: jessegarcia1 Date: Thu, 21 Nov 2024 15:21:27 -0500 Subject: [PATCH 05/41] #2817 added card and section files --- .../src/pages/HomePage/LeadHomePage.tsx | 2 + .../HomePage/components/DesignReviewCard.tsx | 78 +++++++++++++++++++ .../components/UpcomingDesignReviews.tsx | 64 +++++++++++++++ 3 files changed, 144 insertions(+) create mode 100644 src/frontend/src/pages/HomePage/components/DesignReviewCard.tsx create mode 100644 src/frontend/src/pages/HomePage/components/UpcomingDesignReviews.tsx diff --git a/src/frontend/src/pages/HomePage/LeadHomePage.tsx b/src/frontend/src/pages/HomePage/LeadHomePage.tsx index a8f97fb977..d8e6a0cab7 100644 --- a/src/frontend/src/pages/HomePage/LeadHomePage.tsx +++ b/src/frontend/src/pages/HomePage/LeadHomePage.tsx @@ -10,6 +10,7 @@ import ErrorPage from '../ErrorPage'; import PageLayout, { PAGE_GRID_HEIGHT } from '../../components/PageLayout'; import { AuthenticatedUser } from 'shared'; import MyTeamsOverdueTasks from './components/MyTeamsOverdueTasks'; +import UpcomingDesignReviews from './components/UpcomingDesignReviews'; interface LeadHomePageProps { user: AuthenticatedUser; @@ -30,6 +31,7 @@ const LeadHomePage = ({ user }: LeadHomePageProps) => { + diff --git a/src/frontend/src/pages/HomePage/components/DesignReviewCard.tsx b/src/frontend/src/pages/HomePage/components/DesignReviewCard.tsx new file mode 100644 index 0000000000..f977c83b1f --- /dev/null +++ b/src/frontend/src/pages/HomePage/components/DesignReviewCard.tsx @@ -0,0 +1,78 @@ +import { Box, Card, CardContent, Link, Stack, Typography, useTheme } from '@mui/material'; +import { DesignReview } from 'shared'; +import { datePipe, projectWbsPipe } from '../../../utils/pipes'; +import { routes } from '../../../utils/routes'; +import { Link as RouterLink } from 'react-router-dom'; +import { CalendarTodayOutlined } from '@mui/icons-material'; +import { LocationOnOutlined } from '@mui/icons-material'; + +interface DesignReviewProps { + designReview: DesignReview; +} + +/* + Questions: + is there a better way to choose a day as a string? + is there a better way to remove the year from a date? + what does the list of times mean? I just chose the first time and hard coded it to be a pm. idk if this was right + how do i make the button to confirm your avalibilty? +*/ + +function getWeekday(date: Date): string { + const weekdays: string[] = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; + return weekdays[date.getDay()]; +} + +function removeYear(str: string): string { + return str.substring(0, str.length - 5); +} + +const UpcomingDesignReviewsCard: React.FC = ({ designReview }) => { + const theme = useTheme(); + return ( + + + + + + + {designReview.wbsName} + + + + {} + + {getWeekday(designReview.dateScheduled) + + ', ' + + removeYear(datePipe(designReview.dateScheduled)) + + ' @ ' + + designReview.meetingTimes[0] + + 'pm'} + + + + {} + + {designReview.location} + + + + {designReview.status} + + + + ); +}; + +export default UpcomingDesignReviewsCard; diff --git a/src/frontend/src/pages/HomePage/components/UpcomingDesignReviews.tsx b/src/frontend/src/pages/HomePage/components/UpcomingDesignReviews.tsx new file mode 100644 index 0000000000..4524b4354a --- /dev/null +++ b/src/frontend/src/pages/HomePage/components/UpcomingDesignReviews.tsx @@ -0,0 +1,64 @@ +/* + * 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 DesignReviewCard from './DesignReviewCard'; +import { useAllDesignReviews } from '../../../hooks/design-reviews.hooks'; +import ErrorPage from '../../ErrorPage'; +import { wbsPipe } from 'shared'; +import LoadingIndicator from '../../../components/LoadingIndicator'; +import ScrollablePageBlock from './ScrollablePageBlock'; +import EmptyPageBlockDisplay from './EmptyPageBlockDisplay'; +import { Box } from '@mui/material'; +import { Error } from '@mui/icons-material'; + +const NoUpcomingDesignReviewsDisplay: React.FC = () => { + return ( + + } + heading={'No Upcoming Design Reviews'} + message={'There are no Upcoming Design Reviews to Display'} + /> + + ); +}; + +const UpcomingDesignReviews: React.FC = () => { + const { data: designReviews, isLoading, isError, error } = useAllDesignReviews(); + + if (isLoading || !designReviews) return ; + if (isError) return ; + + const filteredDesignReviews = designReviews.filter((review) => { + const scheduledDate = review.dateScheduled; + const currentDate = new Date(); + const inTwoWeeks = new Date(); + inTwoWeeks.setDate(currentDate.getDate() + 14); + return scheduledDate >= currentDate && scheduledDate <= inTwoWeeks && !review.status.includes('DONE'); + }); + + const fullDisplay = ( + + {designReviews.length === 0 ? ( + + ) : ( + designReviews.map((d) => ) + )} + + ); + + return fullDisplay; +}; + +export default UpcomingDesignReviews; From d0c8d8c161ce941e423faa30a1cd0c9e5c164257 Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Thu, 21 Nov 2024 16:03:51 -0500 Subject: [PATCH 06/41] #3000-fixed styling --- src/frontend/src/components/NotificationCard.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/frontend/src/components/NotificationCard.tsx b/src/frontend/src/components/NotificationCard.tsx index 407a17e250..d0fe319a9c 100644 --- a/src/frontend/src/components/NotificationCard.tsx +++ b/src/frontend/src/components/NotificationCard.tsx @@ -43,7 +43,9 @@ const NotificationCard: React.FC = ({ notification }) => From f8b5b73dadefec298fc296c12533d39b42160b25 Mon Sep 17 00:00:00 2001 From: jessegarcia1 Date: Tue, 26 Nov 2024 00:52:58 -0500 Subject: [PATCH 07/41] #2817-added button --- .../HomePage/components/DesignReviewCard.tsx | 43 ++++++++++++++----- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/src/frontend/src/pages/HomePage/components/DesignReviewCard.tsx b/src/frontend/src/pages/HomePage/components/DesignReviewCard.tsx index f977c83b1f..b46f3bb74a 100644 --- a/src/frontend/src/pages/HomePage/components/DesignReviewCard.tsx +++ b/src/frontend/src/pages/HomePage/components/DesignReviewCard.tsx @@ -5,24 +5,48 @@ import { routes } from '../../../utils/routes'; import { Link as RouterLink } from 'react-router-dom'; import { CalendarTodayOutlined } from '@mui/icons-material'; import { LocationOnOutlined } from '@mui/icons-material'; +import { useHistory } from 'react-router-dom'; +import { NERButton } from '../../../components/NERButton'; interface DesignReviewProps { designReview: DesignReview; } -/* - Questions: - is there a better way to choose a day as a string? - is there a better way to remove the year from a date? - what does the list of times mean? I just chose the first time and hard coded it to be a pm. idk if this was right - how do i make the button to confirm your avalibilty? -*/ +const DisplayStatus: React.FC = ({ designReview }) => { + const history = useHistory(); + return ( + <> + {!designReview.status ? ( + { + { + history.push(`${routes.CALENDAR}/${designReview.designReviewId}`); + } + }} + component={RouterLink} + > + Confirm Availibility + + ) : ( + {designReview.status} + )} + + ); +}; function getWeekday(date: Date): string { const weekdays: string[] = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; return weekdays[date.getDay()]; } +function getTime(list: number[]): string { + const weekdays: string[] = ['10am', '11am', '12pm', '1pm', '2pm', '3pm', '4pm', '5pm', '6pm', '7pm', '8pm', '9pm']; + return weekdays[list[0] - 1]; +} + function removeYear(str: string): string { return str.substring(0, str.length - 5); } @@ -57,8 +81,7 @@ const UpcomingDesignReviewsCard: React.FC = ({ designReview } ', ' + removeYear(datePipe(designReview.dateScheduled)) + ' @ ' + - designReview.meetingTimes[0] + - 'pm'} + getTime(designReview.meetingTimes)} @@ -68,7 +91,7 @@ const UpcomingDesignReviewsCard: React.FC = ({ designReview } - {designReview.status} + From bef883a60b82f164e9dfc0fdd2e9e7ec27d65d29 Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Fri, 29 Nov 2024 21:32:12 -0500 Subject: [PATCH 08/41] #3000-animation is working --- src/backend/src/prisma/seed.ts | 5 +-- .../src/components/NotificationAlert.tsx | 31 +++++++++++++++++++ .../src/components/NotificationCard.tsx | 1 + .../src/pages/HomePage/AdminHomePage.tsx | 16 ++-------- src/frontend/src/pages/HomePage/Home.tsx | 26 ++++++++++------ 5 files changed, 53 insertions(+), 26 deletions(-) create mode 100644 src/frontend/src/components/NotificationAlert.tsx diff --git a/src/backend/src/prisma/seed.ts b/src/backend/src/prisma/seed.ts index 451cb37462..19c94e6e2d 100644 --- a/src/backend/src/prisma/seed.ts +++ b/src/backend/src/prisma/seed.ts @@ -33,6 +33,7 @@ import { writeFileSync } from 'fs'; import WorkPackageTemplatesService from '../services/work-package-template.services'; import RecruitmentServices from '../services/recruitment.services'; import OrganizationsService from '../services/organizations.services'; +import { sendNotificationToUsers } from '../utils/homepage-notifications.utils'; const prisma = new PrismaClient(); @@ -1894,8 +1895,8 @@ const performSeed: () => Promise = async () => { await RecruitmentServices.createFaq(batman, 'How many developers are working on FinishLine?', '178 as of 2024', ner); - await UsersService.sendNotification(thomasEmrax.userId, 'testing notifications', 'star'); - await UsersService.sendNotification(thomasEmrax.userId, 'testing notifications #2', 'star'); + await sendNotificationToUsers([thomasEmrax.userId], 'test', 'star', ner.organizationId); + await sendNotificationToUsers([thomasEmrax.userId], 'test2', 'star', ner.organizationId); }; performSeed() diff --git a/src/frontend/src/components/NotificationAlert.tsx b/src/frontend/src/components/NotificationAlert.tsx new file mode 100644 index 0000000000..9b60b48760 --- /dev/null +++ b/src/frontend/src/components/NotificationAlert.tsx @@ -0,0 +1,31 @@ +import { Box } from '@mui/material'; +import React from 'react'; +import { User } from 'shared'; +import NotificationCard from './NotificationCard'; +import { useUserNotifications } from '../hooks/users.hooks'; + +interface NotificationAlertProps { + user: User; +} + +const NotificationAlert: React.FC = ({ user }) => { + const { data: notifications } = useUserNotifications(user.userId); + + const currentNotification = notifications && notifications.length > 0 ? notifications[0] : undefined; + + return ( + + {currentNotification && } + + ); +}; + +export default NotificationAlert; diff --git a/src/frontend/src/components/NotificationCard.tsx b/src/frontend/src/components/NotificationCard.tsx index d0fe319a9c..432517fa98 100644 --- a/src/frontend/src/components/NotificationCard.tsx +++ b/src/frontend/src/components/NotificationCard.tsx @@ -9,6 +9,7 @@ interface NotificationCardProps { const NotificationCard: React.FC = ({ notification }) => { const theme = useTheme(); + return ( { const { isLoading, isError, error, data: userSettingsData } = useSingleUserSettings(user.userId); - const { - data: notifications, - isLoading: notificationsIsLoading, - error: notificationsError, - isError: notificationsIsError - } = useUserNotifications(user.userId); - if (isLoading || !userSettingsData || notificationsIsLoading || !notifications) return ; + if (isLoading || !userSettingsData) return ; if (isError) return ; - if (notificationsIsError) return ; - - const currentNotification = notifications.length > 0 ? notifications[0] : undefined; - if (!currentNotification) return ; return ( - {currentNotification && } Welcome, {user.firstName}! diff --git a/src/frontend/src/pages/HomePage/Home.tsx b/src/frontend/src/pages/HomePage/Home.tsx index 961430d92e..76db11f05a 100644 --- a/src/frontend/src/pages/HomePage/Home.tsx +++ b/src/frontend/src/pages/HomePage/Home.tsx @@ -11,20 +11,26 @@ import { useState } from 'react'; import MemberHomePage from './MemberHomePage'; import LeadHomePage from './LeadHomePage'; import AdminHomePage from './AdminHomePage'; +import NotificationAlert from '../../components/NotificationAlert'; const Home = () => { const user = useCurrentUser(); const [onMemberHomePage, setOnMemberHomePage] = useState(false); - return isGuest(user.role) && !onMemberHomePage ? ( - - ) : isMember(user.role) ? ( - - ) : isLead(user.role) ? ( - - ) : isAdmin(user.role) ? ( - - ) : ( - + return ( + <> + {!onMemberHomePage && } + {isGuest(user.role) && !onMemberHomePage ? ( + + ) : isMember(user.role) ? ( + + ) : isLead(user.role) ? ( + + ) : isAdmin(user.role) ? ( + + ) : ( + + )} + ); }; From 580f4173259edce2d0a9ec89f87fa405f9aa3cc1 Mon Sep 17 00:00:00 2001 From: jessegarcia1 Date: Wed, 4 Dec 2024 19:26:22 -0500 Subject: [PATCH 09/41] #2817 updated conditions that show reviews --- .../src/pages/HomePage/LeadHomePage.tsx | 2 +- .../HomePage/components/DesignReviewCard.tsx | 12 ++++++---- .../components/UpcomingDesignReviews.tsx | 23 +++++++++++++++---- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/frontend/src/pages/HomePage/LeadHomePage.tsx b/src/frontend/src/pages/HomePage/LeadHomePage.tsx index d8e6a0cab7..b28e1d502f 100644 --- a/src/frontend/src/pages/HomePage/LeadHomePage.tsx +++ b/src/frontend/src/pages/HomePage/LeadHomePage.tsx @@ -31,7 +31,7 @@ const LeadHomePage = ({ user }: LeadHomePageProps) => { - + diff --git a/src/frontend/src/pages/HomePage/components/DesignReviewCard.tsx b/src/frontend/src/pages/HomePage/components/DesignReviewCard.tsx index b46f3bb74a..1edea6d6e1 100644 --- a/src/frontend/src/pages/HomePage/components/DesignReviewCard.tsx +++ b/src/frontend/src/pages/HomePage/components/DesignReviewCard.tsx @@ -1,5 +1,5 @@ import { Box, Card, CardContent, Link, Stack, Typography, useTheme } from '@mui/material'; -import { DesignReview } from 'shared'; +import { DesignReview, User } from 'shared'; import { datePipe, projectWbsPipe } from '../../../utils/pipes'; import { routes } from '../../../utils/routes'; import { Link as RouterLink } from 'react-router-dom'; @@ -10,13 +10,15 @@ import { NERButton } from '../../../components/NERButton'; interface DesignReviewProps { designReview: DesignReview; + user: User; } -const DisplayStatus: React.FC = ({ designReview }) => { +const DisplayStatus: React.FC = ({ designReview, user }) => { const history = useHistory(); return ( + //is this what we want <> - {!designReview.status ? ( + {!designReview.status || !designReview.confirmedMembers.includes(user) ? ( = ({ designReview }) => { +const UpcomingDesignReviewsCard: React.FC = ({ designReview, user }) => { const theme = useTheme(); return ( = ({ designReview } - + diff --git a/src/frontend/src/pages/HomePage/components/UpcomingDesignReviews.tsx b/src/frontend/src/pages/HomePage/components/UpcomingDesignReviews.tsx index 4524b4354a..5c9128c0aa 100644 --- a/src/frontend/src/pages/HomePage/components/UpcomingDesignReviews.tsx +++ b/src/frontend/src/pages/HomePage/components/UpcomingDesignReviews.tsx @@ -6,13 +6,17 @@ import DesignReviewCard from './DesignReviewCard'; import { useAllDesignReviews } from '../../../hooks/design-reviews.hooks'; import ErrorPage from '../../ErrorPage'; -import { wbsPipe } from 'shared'; +import { AuthenticatedUser, wbsPipe } from 'shared'; import LoadingIndicator from '../../../components/LoadingIndicator'; import ScrollablePageBlock from './ScrollablePageBlock'; import EmptyPageBlockDisplay from './EmptyPageBlockDisplay'; import { Box } from '@mui/material'; import { Error } from '@mui/icons-material'; +interface UpcomingDesignReviewProps { + user: AuthenticatedUser; +} + const NoUpcomingDesignReviewsDisplay: React.FC = () => { return ( { ); }; -const UpcomingDesignReviews: React.FC = () => { +const UpcomingDesignReviews: React.FC = ({ user }) => { const { data: designReviews, isLoading, isError, error } = useAllDesignReviews(); if (isLoading || !designReviews) return ; @@ -45,7 +49,18 @@ const UpcomingDesignReviews: React.FC = () => { const currentDate = new Date(); const inTwoWeeks = new Date(); inTwoWeeks.setDate(currentDate.getDate() + 14); - return scheduledDate >= currentDate && scheduledDate <= inTwoWeeks && !review.status.includes('DONE'); + + /* + Since this is on the leads page, leads shouldn't see all design reviews here. + We should filter the design reviews on whether the current user is found the the + design review's required or optional members field +*/ + return ( + scheduledDate >= currentDate && + scheduledDate <= inTwoWeeks && + !review.status.includes('DONE') && + (review.requiredMembers.includes(user) || review.optionalMembers.includes(user)) + ); }); const fullDisplay = ( @@ -53,7 +68,7 @@ const UpcomingDesignReviews: React.FC = () => { {designReviews.length === 0 ? ( ) : ( - designReviews.map((d) => ) + designReviews.map((d) => ) )} ); From 52fab519af4ccb36e84cd59ad73328d8818ef669 Mon Sep 17 00:00:00 2001 From: jessegarcia1 Date: Wed, 4 Dec 2024 20:03:40 -0500 Subject: [PATCH 10/41] #2817 some stuff --- .../HomePage/components/DesignReviewCard.tsx | 18 ++++++------------ .../components/UpcomingDesignReviews.tsx | 7 +------ 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/src/frontend/src/pages/HomePage/components/DesignReviewCard.tsx b/src/frontend/src/pages/HomePage/components/DesignReviewCard.tsx index 1edea6d6e1..7c826de6ba 100644 --- a/src/frontend/src/pages/HomePage/components/DesignReviewCard.tsx +++ b/src/frontend/src/pages/HomePage/components/DesignReviewCard.tsx @@ -3,10 +3,11 @@ import { DesignReview, User } from 'shared'; import { datePipe, projectWbsPipe } from '../../../utils/pipes'; import { routes } from '../../../utils/routes'; import { Link as RouterLink } from 'react-router-dom'; -import { CalendarTodayOutlined } from '@mui/icons-material'; +import CalendarMonthIcon from '@mui/icons-material/CalendarMonth'; import { LocationOnOutlined } from '@mui/icons-material'; import { useHistory } from 'react-router-dom'; import { NERButton } from '../../../components/NERButton'; +import { meetingStartTimePipe } from '../../../../../backend/src/utils/design-reviews.utils'; interface DesignReviewProps { designReview: DesignReview; @@ -24,9 +25,7 @@ const DisplayStatus: React.FC = ({ designReview, user }) => { size="small" sx={{ color: 'white' }} onClick={() => { - { - history.push(`${routes.CALENDAR}/${designReview.designReviewId}`); - } + history.push(`${routes.CALENDAR}/${designReview.designReviewId}`); }} component={RouterLink} > @@ -44,11 +43,6 @@ function getWeekday(date: Date): string { return weekdays[date.getDay()]; } -function getTime(list: number[]): string { - const weekdays: string[] = ['10am', '11am', '12pm', '1pm', '2pm', '3pm', '4pm', '5pm', '6pm', '7pm', '8pm', '9pm']; - return weekdays[list[0] - 1]; -} - function removeYear(str: string): string { return str.substring(0, str.length - 5); } @@ -77,13 +71,13 @@ const UpcomingDesignReviewsCard: React.FC = ({ designReview, - {} + {} {getWeekday(designReview.dateScheduled) + ', ' + removeYear(datePipe(designReview.dateScheduled)) + ' @ ' + - getTime(designReview.meetingTimes)} + meetingStartTimePipe(designReview.meetingTimes)} @@ -93,7 +87,7 @@ const UpcomingDesignReviewsCard: React.FC = ({ designReview, - + diff --git a/src/frontend/src/pages/HomePage/components/UpcomingDesignReviews.tsx b/src/frontend/src/pages/HomePage/components/UpcomingDesignReviews.tsx index 5c9128c0aa..bf1097b53d 100644 --- a/src/frontend/src/pages/HomePage/components/UpcomingDesignReviews.tsx +++ b/src/frontend/src/pages/HomePage/components/UpcomingDesignReviews.tsx @@ -22,7 +22,7 @@ const NoUpcomingDesignReviewsDisplay: React.FC = () => { = ({ user }) => const inTwoWeeks = new Date(); inTwoWeeks.setDate(currentDate.getDate() + 14); - /* - Since this is on the leads page, leads shouldn't see all design reviews here. - We should filter the design reviews on whether the current user is found the the - design review's required or optional members field -*/ return ( scheduledDate >= currentDate && scheduledDate <= inTwoWeeks && From 33d090f96dd08af70684568b6955eba5ca0a9631 Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Thu, 5 Dec 2024 12:00:52 -0500 Subject: [PATCH 11/41] #2817-fixed filtering --- .../src/pages/HomePage/AdminHomePage.tsx | 7 ++++++- src/frontend/src/pages/HomePage/LeadHomePage.tsx | 2 +- .../HomePage/components/DesignReviewCard.tsx | 11 ++++------- .../components/UpcomingDesignReviews.tsx | 16 ++++++++++------ 4 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/frontend/src/pages/HomePage/AdminHomePage.tsx b/src/frontend/src/pages/HomePage/AdminHomePage.tsx index cf008c8020..81e148aab4 100644 --- a/src/frontend/src/pages/HomePage/AdminHomePage.tsx +++ b/src/frontend/src/pages/HomePage/AdminHomePage.tsx @@ -9,6 +9,7 @@ import LoadingIndicator from '../../components/LoadingIndicator'; import ErrorPage from '../ErrorPage'; import PageLayout, { PAGE_GRID_HEIGHT } from '../../components/PageLayout'; import { AuthenticatedUser } from 'shared'; +import UpcomingDesignReviews from './components/UpcomingDesignReviews'; interface AdminHomePageProps { user: AuthenticatedUser; @@ -25,7 +26,11 @@ const AdminHomePage = ({ user }: AdminHomePageProps) => { Welcome, {user.firstName}! - + + + + + ); }; diff --git a/src/frontend/src/pages/HomePage/LeadHomePage.tsx b/src/frontend/src/pages/HomePage/LeadHomePage.tsx index 3cc202f06d..8fc855fb10 100644 --- a/src/frontend/src/pages/HomePage/LeadHomePage.tsx +++ b/src/frontend/src/pages/HomePage/LeadHomePage.tsx @@ -27,7 +27,7 @@ const LeadHomePage = ({ user }: LeadHomePageProps) => { Welcome, {user.firstName}! - + diff --git a/src/frontend/src/pages/HomePage/components/DesignReviewCard.tsx b/src/frontend/src/pages/HomePage/components/DesignReviewCard.tsx index 7c826de6ba..7e43483bfc 100644 --- a/src/frontend/src/pages/HomePage/components/DesignReviewCard.tsx +++ b/src/frontend/src/pages/HomePage/components/DesignReviewCard.tsx @@ -23,7 +23,7 @@ const DisplayStatus: React.FC = ({ designReview, user }) => { { history.push(`${routes.CALENDAR}/${designReview.designReviewId}`); }} @@ -54,19 +54,16 @@ const UpcomingDesignReviewsCard: React.FC = ({ designReview, variant="outlined" sx={{ minWidth: 'fit-content', + minHeight: 'fit-content', mr: 3, background: theme.palette.background.default }} > - + - + {designReview.wbsName} diff --git a/src/frontend/src/pages/HomePage/components/UpcomingDesignReviews.tsx b/src/frontend/src/pages/HomePage/components/UpcomingDesignReviews.tsx index bf1097b53d..2b316a99cf 100644 --- a/src/frontend/src/pages/HomePage/components/UpcomingDesignReviews.tsx +++ b/src/frontend/src/pages/HomePage/components/UpcomingDesignReviews.tsx @@ -6,7 +6,7 @@ import DesignReviewCard from './DesignReviewCard'; import { useAllDesignReviews } from '../../../hooks/design-reviews.hooks'; import ErrorPage from '../../ErrorPage'; -import { AuthenticatedUser, wbsPipe } from 'shared'; +import { AuthenticatedUser, DesignReviewStatus, wbsPipe } from 'shared'; import LoadingIndicator from '../../../components/LoadingIndicator'; import ScrollablePageBlock from './ScrollablePageBlock'; import EmptyPageBlockDisplay from './EmptyPageBlockDisplay'; @@ -50,20 +50,24 @@ const UpcomingDesignReviews: React.FC = ({ user }) => const inTwoWeeks = new Date(); inTwoWeeks.setDate(currentDate.getDate() + 14); + const memberUserIds = [ + ...review.requiredMembers.map((user) => user.userId), + ...review.optionalMembers.map((user) => user.userId) + ]; return ( scheduledDate >= currentDate && scheduledDate <= inTwoWeeks && - !review.status.includes('DONE') && - (review.requiredMembers.includes(user) || review.optionalMembers.includes(user)) + review.status !== DesignReviewStatus.DONE && + memberUserIds.includes(user.userId) ); }); const fullDisplay = ( - - {designReviews.length === 0 ? ( + + {filteredDesignReviews.length === 0 ? ( ) : ( - designReviews.map((d) => ) + filteredDesignReviews.map((d) => ) )} ); From 95e722ef59c6fbf2773e0e3fb926c082442cdf37 Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Thu, 5 Dec 2024 12:05:35 -0500 Subject: [PATCH 12/41] #2817-confirmed members --- .../src/pages/HomePage/components/DesignReviewCard.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/frontend/src/pages/HomePage/components/DesignReviewCard.tsx b/src/frontend/src/pages/HomePage/components/DesignReviewCard.tsx index 7e43483bfc..46fbd73f2c 100644 --- a/src/frontend/src/pages/HomePage/components/DesignReviewCard.tsx +++ b/src/frontend/src/pages/HomePage/components/DesignReviewCard.tsx @@ -16,10 +16,13 @@ interface DesignReviewProps { const DisplayStatus: React.FC = ({ designReview, user }) => { const history = useHistory(); + + const confirmedMemberIds = designReview.confirmedMembers.map((user) => user.userId); + console.log('CONFIRMED:', confirmedMemberIds); + return ( - //is this what we want <> - {!designReview.status || !designReview.confirmedMembers.includes(user) ? ( + {!confirmedMemberIds.includes(user.userId) ? ( Date: Thu, 5 Dec 2024 15:33:34 -0500 Subject: [PATCH 13/41] #3000-updated controller --- .../src/controllers/users.controllers.ts | 4 +- .../migration.sql | 51 ------------------- 2 files changed, 2 insertions(+), 53 deletions(-) delete mode 100644 src/backend/src/prisma/migrations/20241122155216_updated_notifications/migration.sql diff --git a/src/backend/src/controllers/users.controllers.ts b/src/backend/src/controllers/users.controllers.ts index be57729561..e491d3f4f4 100644 --- a/src/backend/src/controllers/users.controllers.ts +++ b/src/backend/src/controllers/users.controllers.ts @@ -198,9 +198,9 @@ export default class UsersController { const { organization } = req; const unreadNotifications = await UsersService.getUserUnreadNotifications(userId, organization); - return res.status(200).json(unreadNotifications); + res.status(200).json(unreadNotifications); } catch (error: unknown) { - return next(error); + next(error); } } } diff --git a/src/backend/src/prisma/migrations/20241122155216_updated_notifications/migration.sql b/src/backend/src/prisma/migrations/20241122155216_updated_notifications/migration.sql deleted file mode 100644 index 192243b385..0000000000 --- a/src/backend/src/prisma/migrations/20241122155216_updated_notifications/migration.sql +++ /dev/null @@ -1,51 +0,0 @@ -/* - Warnings: - - - You are about to drop the column `userId` on the `Notification` table. All the data in the column will be lost. - - Added the required column `text` to the `Announcement` table without a default value. This is not possible if the table is not empty. - -*/ --- DropForeignKey -ALTER TABLE "Notification" DROP CONSTRAINT "Notification_userId_fkey"; - --- AlterTable -ALTER TABLE "Announcement" ADD COLUMN "text" TEXT NOT NULL; - --- AlterTable -ALTER TABLE "Notification" DROP COLUMN "userId"; - --- CreateTable -CREATE TABLE "_ReceivedAnnouncements" ( - "A" TEXT NOT NULL, - "B" TEXT NOT NULL -); - --- CreateTable -CREATE TABLE "_NotificationToUser" ( - "A" TEXT NOT NULL, - "B" TEXT NOT NULL -); - --- CreateIndex -CREATE UNIQUE INDEX "_ReceivedAnnouncements_AB_unique" ON "_ReceivedAnnouncements"("A", "B"); - --- CreateIndex -CREATE INDEX "_ReceivedAnnouncements_B_index" ON "_ReceivedAnnouncements"("B"); - --- CreateIndex -CREATE UNIQUE INDEX "_NotificationToUser_AB_unique" ON "_NotificationToUser"("A", "B"); - --- CreateIndex -CREATE INDEX "_NotificationToUser_B_index" ON "_NotificationToUser"("B"); - --- AddForeignKey -ALTER TABLE "_ReceivedAnnouncements" ADD CONSTRAINT "_ReceivedAnnouncements_A_fkey" FOREIGN KEY ("A") REFERENCES "Announcement"("announcementId") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "_ReceivedAnnouncements" ADD CONSTRAINT "_ReceivedAnnouncements_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("userId") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "_NotificationToUser" ADD CONSTRAINT "_NotificationToUser_A_fkey" FOREIGN KEY ("A") REFERENCES "Notification"("notificationId") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "_NotificationToUser" ADD CONSTRAINT "_NotificationToUser_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("userId") ON DELETE CASCADE ON UPDATE CASCADE; From 4ae2563ae54b418b2587687742d7d0d37eab8811 Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Fri, 6 Dec 2024 11:36:48 -0500 Subject: [PATCH 14/41] #2817-added zoom link and fixed confirmation --- .../HomePage/components/DesignReviewCard.tsx | 35 ++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/src/frontend/src/pages/HomePage/components/DesignReviewCard.tsx b/src/frontend/src/pages/HomePage/components/DesignReviewCard.tsx index 46fbd73f2c..2a2d6ef5f6 100644 --- a/src/frontend/src/pages/HomePage/components/DesignReviewCard.tsx +++ b/src/frontend/src/pages/HomePage/components/DesignReviewCard.tsx @@ -4,7 +4,7 @@ import { datePipe, projectWbsPipe } from '../../../utils/pipes'; import { routes } from '../../../utils/routes'; import { Link as RouterLink } from 'react-router-dom'; import CalendarMonthIcon from '@mui/icons-material/CalendarMonth'; -import { LocationOnOutlined } from '@mui/icons-material'; +import { LocationOnOutlined, Computer } from '@mui/icons-material'; import { useHistory } from 'react-router-dom'; import { NERButton } from '../../../components/NERButton'; import { meetingStartTimePipe } from '../../../../../backend/src/utils/design-reviews.utils'; @@ -14,6 +14,25 @@ interface DesignReviewProps { user: User; } +const DesignReviewInfo = ({ icon, text, link }: { icon: React.ReactNode; text: string; link?: boolean }) => { + return ( + + {icon} + {link ? ( + + + {text} + + + ) : ( + + {text} + + )} + + ); +}; + const DisplayStatus: React.FC = ({ designReview, user }) => { const history = useHistory(); @@ -28,7 +47,7 @@ const DisplayStatus: React.FC = ({ designReview, user }) => { size="small" sx={{ color: 'white', padding: 1 }} onClick={() => { - history.push(`${routes.CALENDAR}/${designReview.designReviewId}`); + history.push(`${routes.SETTINGS_PREFERENCES}?drId=${designReview.designReviewId}`); }} component={RouterLink} > @@ -80,12 +99,12 @@ const UpcomingDesignReviewsCard: React.FC = ({ designReview, meetingStartTimePipe(designReview.meetingTimes)} - - {} - - {designReview.location} - - + {designReview.isInPerson && !!designReview.location && ( + } text={designReview.location} /> + )} + {designReview.isOnline && !!designReview.zoomLink && ( + } text={designReview.zoomLink} link /> + )} From 0d27cb3a82f843f46ba9c0554f650f864f5bd2b0 Mon Sep 17 00:00:00 2001 From: jessegarcia1 Date: Sun, 8 Dec 2024 15:16:37 -0500 Subject: [PATCH 15/41] #2817 prettier --- src/frontend/src/pages/HomePage/AdminHomePage.tsx | 1 - .../pages/HomePage/components/UpcomingDesignReviews.tsx | 7 ++++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/frontend/src/pages/HomePage/AdminHomePage.tsx b/src/frontend/src/pages/HomePage/AdminHomePage.tsx index da2cb2d75c..946bdcdb5b 100644 --- a/src/frontend/src/pages/HomePage/AdminHomePage.tsx +++ b/src/frontend/src/pages/HomePage/AdminHomePage.tsx @@ -13,7 +13,6 @@ import UpcomingDesignReviews from './components/UpcomingDesignReviews'; import WorkPackagesSelectionView from './components/WorkPackagesSelectionView'; import ChangeRequestsToReview from './components/ChangeRequestsToReview'; - interface AdminHomePageProps { user: AuthenticatedUser; } diff --git a/src/frontend/src/pages/HomePage/components/UpcomingDesignReviews.tsx b/src/frontend/src/pages/HomePage/components/UpcomingDesignReviews.tsx index 2b316a99cf..15fe1c59a3 100644 --- a/src/frontend/src/pages/HomePage/components/UpcomingDesignReviews.tsx +++ b/src/frontend/src/pages/HomePage/components/UpcomingDesignReviews.tsx @@ -12,6 +12,7 @@ import ScrollablePageBlock from './ScrollablePageBlock'; import EmptyPageBlockDisplay from './EmptyPageBlockDisplay'; import { Box } from '@mui/material'; import { Error } from '@mui/icons-material'; +import designReviewsRouter from '../../../../../backend/src/routes/design-reviews.routes'; interface UpcomingDesignReviewProps { user: AuthenticatedUser; @@ -63,11 +64,11 @@ const UpcomingDesignReviews: React.FC = ({ user }) => }); const fullDisplay = ( - - {filteredDesignReviews.length === 0 ? ( + + {designReviews.length === 0 ? ( ) : ( - filteredDesignReviews.map((d) => ) + designReviews.map((d) => ) )} ); From 31887226f1ea91f339be38afc305ef8c366ce6e7 Mon Sep 17 00:00:00 2001 From: jessegarcia1 Date: Sun, 8 Dec 2024 15:19:16 -0500 Subject: [PATCH 16/41] #2817 linting --- .../src/pages/HomePage/components/UpcomingDesignReviews.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/frontend/src/pages/HomePage/components/UpcomingDesignReviews.tsx b/src/frontend/src/pages/HomePage/components/UpcomingDesignReviews.tsx index 15fe1c59a3..1d39e5442c 100644 --- a/src/frontend/src/pages/HomePage/components/UpcomingDesignReviews.tsx +++ b/src/frontend/src/pages/HomePage/components/UpcomingDesignReviews.tsx @@ -12,7 +12,6 @@ import ScrollablePageBlock from './ScrollablePageBlock'; import EmptyPageBlockDisplay from './EmptyPageBlockDisplay'; import { Box } from '@mui/material'; import { Error } from '@mui/icons-material'; -import designReviewsRouter from '../../../../../backend/src/routes/design-reviews.routes'; interface UpcomingDesignReviewProps { user: AuthenticatedUser; From df2865b3ce9437bc0a6552139b1552ea0725e055 Mon Sep 17 00:00:00 2001 From: jessegarcia1 Date: Sun, 8 Dec 2024 15:23:18 -0500 Subject: [PATCH 17/41] #2817 linting again --- .../src/pages/HomePage/components/UpcomingDesignReviews.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/frontend/src/pages/HomePage/components/UpcomingDesignReviews.tsx b/src/frontend/src/pages/HomePage/components/UpcomingDesignReviews.tsx index 1d39e5442c..2b316a99cf 100644 --- a/src/frontend/src/pages/HomePage/components/UpcomingDesignReviews.tsx +++ b/src/frontend/src/pages/HomePage/components/UpcomingDesignReviews.tsx @@ -63,11 +63,11 @@ const UpcomingDesignReviews: React.FC = ({ user }) => }); const fullDisplay = ( - - {designReviews.length === 0 ? ( + + {filteredDesignReviews.length === 0 ? ( ) : ( - designReviews.map((d) => ) + filteredDesignReviews.map((d) => ) )} ); From 314b2bb1c28734f2bc409264c2b387c2eab2b12f Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Sun, 8 Dec 2024 15:39:49 -0500 Subject: [PATCH 18/41] #2817-added components --- .../components/ChangeRequestsToReview.tsx | 51 +++++++ .../HomePage/components/WorkPackageSelect.tsx | 91 ++++++++++++ .../components/WorkPackagesSelectionView.tsx | 129 ++++++++++++++++++ 3 files changed, 271 insertions(+) create mode 100644 src/frontend/src/pages/HomePage/components/ChangeRequestsToReview.tsx create mode 100644 src/frontend/src/pages/HomePage/components/WorkPackageSelect.tsx create mode 100644 src/frontend/src/pages/HomePage/components/WorkPackagesSelectionView.tsx diff --git a/src/frontend/src/pages/HomePage/components/ChangeRequestsToReview.tsx b/src/frontend/src/pages/HomePage/components/ChangeRequestsToReview.tsx new file mode 100644 index 0000000000..e7161db966 --- /dev/null +++ b/src/frontend/src/pages/HomePage/components/ChangeRequestsToReview.tsx @@ -0,0 +1,51 @@ +import { useAllWorkPackages } from '../../../hooks/work-packages.hooks'; +import LoadingIndicator from '../../../components/LoadingIndicator'; +import ErrorPage from '../../ErrorPage'; +import { useAllChangeRequests } from '../../../hooks/change-requests.hooks'; +import { useAllProjects } from '../../../hooks/projects.hooks'; +import { getCRsToReview } from '../../../utils/change-request.utils'; +import ScrollablePageBlock from './ScrollablePageBlock'; +import { AuthenticatedUser, ChangeRequest } from 'shared'; +import ChangeRequestDetailCard from '../../../components/ChangeRequestDetailCard'; +import EmptyPageBlockDisplay from './EmptyPageBlockDisplay'; +import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined'; + +interface ChangeRequestsToReviewProps { + user: AuthenticatedUser; +} + +const NoChangeRequestsToReview: React.FC = () => { + return ( + } + heading={`You're all caught up!`} + message={'You have no unreviewed changre requests!'} + /> + ); +}; + +const ChangeRequestsToReview: React.FC = ({ user }) => { + const { data: changeRequests, isError: crIsError, isLoading: crIsLoading, error: crError } = useAllChangeRequests(); + const { data: projects, isError: projectIsError, isLoading: projectLoading, error: projectError } = useAllProjects(); + const { data: workPackages, isError: wpIsError, isLoading: wpLoading, error: wpError } = useAllWorkPackages(); + + if (crIsLoading || projectLoading || wpLoading || !changeRequests || !projects || !workPackages) + return ; + if (crIsError) return ; + if (projectIsError) return ; + if (wpIsError) return ; + + const crsToReview = getCRsToReview(projects, workPackages, user, changeRequests); + + return ( + + {crsToReview.length === 0 ? ( + + ) : ( + crsToReview.map((cr: ChangeRequest) => ) + )} + + ); +}; + +export default ChangeRequestsToReview; diff --git a/src/frontend/src/pages/HomePage/components/WorkPackageSelect.tsx b/src/frontend/src/pages/HomePage/components/WorkPackageSelect.tsx new file mode 100644 index 0000000000..996a28cb0c --- /dev/null +++ b/src/frontend/src/pages/HomePage/components/WorkPackageSelect.tsx @@ -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 = ({ options, onSelect, selected = 0 }) => { + const theme = useTheme(); + const [isOpen, setIsOpen] = useState(false); + const dropdownRef = useRef(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 ( + + setIsOpen(!isOpen)} + variant="h5" + sx={{ paddingX: 2, paddingY: 1, display: 'inline-block', cursor: 'pointer' }} + > + + {options[selected]} + + {isOpen && ( + setIsOpen(!isOpen)} sx={{ position: 'absolute', top: '-40%', cursor: 'pointer' }}> + + + {options[selected]} + + {options + .filter((option) => option !== options.at(selected)) + .map((option) => ( + { + 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} + + ))} + + + )} + + ); +}; + +export default WorkPackageSelect; diff --git a/src/frontend/src/pages/HomePage/components/WorkPackagesSelectionView.tsx b/src/frontend/src/pages/HomePage/components/WorkPackagesSelectionView.tsx new file mode 100644 index 0000000000..2b05907c4d --- /dev/null +++ b/src/frontend/src/pages/HomePage/components/WorkPackagesSelectionView.tsx @@ -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 ( + } + 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(defaultFirstDisplay); + + // destructuring tuple to get wps of selected option + const [, currentWps] = workPackageOptions[currentDisplayedWPs]; + + const WorkPackagesDisplay = (workPackages: WorkPackage[]) => ( + + {workPackages.map((wp) => ( + + ))} + + ); + + return ( + + + wp[0])} + onSelect={setCurrentDisplayedWPs} + selected={currentDisplayedWPs} + /> + + {currentWps.length === 0 ? : WorkPackagesDisplay(currentWps)} + + + + ); +}; + +export default WorkPackagesSelectionView; From 2da092d86b3206c36d26a6618685f5d2f6dde352 Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Sun, 8 Dec 2024 15:43:05 -0500 Subject: [PATCH 19/41] #2817-added to lead home page --- .../src/pages/HomePage/AdminHomePage.tsx | 3 --- .../src/pages/HomePage/LeadHomePage.tsx | 2 +- .../components/UpcomingDesignReviews.tsx | 21 +++++-------------- 3 files changed, 6 insertions(+), 20 deletions(-) diff --git a/src/frontend/src/pages/HomePage/AdminHomePage.tsx b/src/frontend/src/pages/HomePage/AdminHomePage.tsx index 946bdcdb5b..86ed4c5f33 100644 --- a/src/frontend/src/pages/HomePage/AdminHomePage.tsx +++ b/src/frontend/src/pages/HomePage/AdminHomePage.tsx @@ -32,9 +32,6 @@ const AdminHomePage = ({ user }: AdminHomePageProps) => { - - - diff --git a/src/frontend/src/pages/HomePage/LeadHomePage.tsx b/src/frontend/src/pages/HomePage/LeadHomePage.tsx index 4b432b212b..5466cdc285 100644 --- a/src/frontend/src/pages/HomePage/LeadHomePage.tsx +++ b/src/frontend/src/pages/HomePage/LeadHomePage.tsx @@ -40,7 +40,7 @@ const LeadHomePage = ({ user }: LeadHomePageProps) => { - + diff --git a/src/frontend/src/pages/HomePage/components/UpcomingDesignReviews.tsx b/src/frontend/src/pages/HomePage/components/UpcomingDesignReviews.tsx index 2b316a99cf..6c1a960888 100644 --- a/src/frontend/src/pages/HomePage/components/UpcomingDesignReviews.tsx +++ b/src/frontend/src/pages/HomePage/components/UpcomingDesignReviews.tsx @@ -19,22 +19,11 @@ interface UpcomingDesignReviewProps { const NoUpcomingDesignReviewsDisplay: React.FC = () => { return ( - - } - heading={'No Upcoming Design Reviews'} - message={'There are no Upcoming Design Reviews to Display'} - /> - + } + heading={'No Upcoming Design Reviews'} + message={'There are no Upcoming Design Reviews to Display'} + /> ); }; From f7b397307612251d0cd0b9e6c434a4475b0ba63d Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Sun, 8 Dec 2024 15:46:38 -0500 Subject: [PATCH 20/41] #2817-linting --- src/frontend/src/pages/HomePage/AdminHomePage.tsx | 1 - .../src/pages/HomePage/components/UpcomingDesignReviews.tsx | 1 - 2 files changed, 2 deletions(-) diff --git a/src/frontend/src/pages/HomePage/AdminHomePage.tsx b/src/frontend/src/pages/HomePage/AdminHomePage.tsx index 86ed4c5f33..4dc62858e0 100644 --- a/src/frontend/src/pages/HomePage/AdminHomePage.tsx +++ b/src/frontend/src/pages/HomePage/AdminHomePage.tsx @@ -9,7 +9,6 @@ import LoadingIndicator from '../../components/LoadingIndicator'; import ErrorPage from '../ErrorPage'; import PageLayout, { PAGE_GRID_HEIGHT } from '../../components/PageLayout'; import { AuthenticatedUser } from 'shared'; -import UpcomingDesignReviews from './components/UpcomingDesignReviews'; import WorkPackagesSelectionView from './components/WorkPackagesSelectionView'; import ChangeRequestsToReview from './components/ChangeRequestsToReview'; diff --git a/src/frontend/src/pages/HomePage/components/UpcomingDesignReviews.tsx b/src/frontend/src/pages/HomePage/components/UpcomingDesignReviews.tsx index 6c1a960888..68ee4bc4a2 100644 --- a/src/frontend/src/pages/HomePage/components/UpcomingDesignReviews.tsx +++ b/src/frontend/src/pages/HomePage/components/UpcomingDesignReviews.tsx @@ -10,7 +10,6 @@ import { AuthenticatedUser, DesignReviewStatus, wbsPipe } from 'shared'; import LoadingIndicator from '../../../components/LoadingIndicator'; import ScrollablePageBlock from './ScrollablePageBlock'; import EmptyPageBlockDisplay from './EmptyPageBlockDisplay'; -import { Box } from '@mui/material'; import { Error } from '@mui/icons-material'; interface UpcomingDesignReviewProps { From da6e24f577221cfc5771d687e135642666e635f4 Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Wed, 11 Dec 2024 20:36:42 -0500 Subject: [PATCH 21/41] #2998-upated endpoint --- .../src/controllers/users.controllers.ts | 4 +- .../migration.sql | 51 ------------------- src/backend/src/prisma/seed.ts | 3 ++ src/backend/src/services/users.services.ts | 2 +- src/backend/tests/unmocked/users.test.ts | 6 +-- 5 files changed, 9 insertions(+), 57 deletions(-) delete mode 100644 src/backend/src/prisma/migrations/20241118211532_updated_notifications/migration.sql diff --git a/src/backend/src/controllers/users.controllers.ts b/src/backend/src/controllers/users.controllers.ts index be57729561..e491d3f4f4 100644 --- a/src/backend/src/controllers/users.controllers.ts +++ b/src/backend/src/controllers/users.controllers.ts @@ -198,9 +198,9 @@ export default class UsersController { const { organization } = req; const unreadNotifications = await UsersService.getUserUnreadNotifications(userId, organization); - return res.status(200).json(unreadNotifications); + res.status(200).json(unreadNotifications); } catch (error: unknown) { - return next(error); + next(error); } } } diff --git a/src/backend/src/prisma/migrations/20241118211532_updated_notifications/migration.sql b/src/backend/src/prisma/migrations/20241118211532_updated_notifications/migration.sql deleted file mode 100644 index 192243b385..0000000000 --- a/src/backend/src/prisma/migrations/20241118211532_updated_notifications/migration.sql +++ /dev/null @@ -1,51 +0,0 @@ -/* - Warnings: - - - You are about to drop the column `userId` on the `Notification` table. All the data in the column will be lost. - - Added the required column `text` to the `Announcement` table without a default value. This is not possible if the table is not empty. - -*/ --- DropForeignKey -ALTER TABLE "Notification" DROP CONSTRAINT "Notification_userId_fkey"; - --- AlterTable -ALTER TABLE "Announcement" ADD COLUMN "text" TEXT NOT NULL; - --- AlterTable -ALTER TABLE "Notification" DROP COLUMN "userId"; - --- CreateTable -CREATE TABLE "_ReceivedAnnouncements" ( - "A" TEXT NOT NULL, - "B" TEXT NOT NULL -); - --- CreateTable -CREATE TABLE "_NotificationToUser" ( - "A" TEXT NOT NULL, - "B" TEXT NOT NULL -); - --- CreateIndex -CREATE UNIQUE INDEX "_ReceivedAnnouncements_AB_unique" ON "_ReceivedAnnouncements"("A", "B"); - --- CreateIndex -CREATE INDEX "_ReceivedAnnouncements_B_index" ON "_ReceivedAnnouncements"("B"); - --- CreateIndex -CREATE UNIQUE INDEX "_NotificationToUser_AB_unique" ON "_NotificationToUser"("A", "B"); - --- CreateIndex -CREATE INDEX "_NotificationToUser_B_index" ON "_NotificationToUser"("B"); - --- AddForeignKey -ALTER TABLE "_ReceivedAnnouncements" ADD CONSTRAINT "_ReceivedAnnouncements_A_fkey" FOREIGN KEY ("A") REFERENCES "Announcement"("announcementId") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "_ReceivedAnnouncements" ADD CONSTRAINT "_ReceivedAnnouncements_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("userId") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "_NotificationToUser" ADD CONSTRAINT "_NotificationToUser_A_fkey" FOREIGN KEY ("A") REFERENCES "Notification"("notificationId") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "_NotificationToUser" ADD CONSTRAINT "_NotificationToUser_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("userId") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/src/backend/src/prisma/seed.ts b/src/backend/src/prisma/seed.ts index a30e502baf..d19e61a98f 100644 --- a/src/backend/src/prisma/seed.ts +++ b/src/backend/src/prisma/seed.ts @@ -33,6 +33,7 @@ import { writeFileSync } from 'fs'; import WorkPackageTemplatesService from '../services/work-package-template.services'; import RecruitmentServices from '../services/recruitment.services'; import OrganizationsService from '../services/organizations.services'; +import NotificationsService from '../services/notifications.services'; const prisma = new PrismaClient(); @@ -1893,6 +1894,8 @@ const performSeed: () => Promise = async () => { await RecruitmentServices.createFaq(batman, 'When was FinishLine created?', 'FinishLine was created in 2019', ner); await RecruitmentServices.createFaq(batman, 'How many developers are working on FinishLine?', '178 as of 2024', ner); + + await NotificationsService.sendNotifcationToUsers('Admin!', 'star', [thomasEmrax.userId], ner.organizationId); }; performSeed() diff --git a/src/backend/src/services/users.services.ts b/src/backend/src/services/users.services.ts index a6ed708673..d9d37b259e 100644 --- a/src/backend/src/services/users.services.ts +++ b/src/backend/src/services/users.services.ts @@ -39,7 +39,7 @@ import authenticatedUserTransformer from '../transformers/auth-user.transformer' import { getTaskQueryArgs } from '../prisma-query-args/tasks.query-args'; import taskTransformer from '../transformers/tasks.transformer'; import { getNotificationQueryArgs } from '../prisma-query-args/notifications.query-args'; -import notificationTransformer from '../transformers/notification.transformer'; +import notificationTransformer from '../transformers/notifications.transformer'; export default class UsersService { /** diff --git a/src/backend/tests/unmocked/users.test.ts b/src/backend/tests/unmocked/users.test.ts index fa43dda9db..14d8b0bffe 100644 --- a/src/backend/tests/unmocked/users.test.ts +++ b/src/backend/tests/unmocked/users.test.ts @@ -3,7 +3,7 @@ import { createTestOrganization, createTestTask, createTestUser, resetUsers } fr import { batmanAppAdmin } from '../test-data/users.test-data'; import UsersService from '../../src/services/users.services'; import { NotFoundException } from '../../src/utils/errors.utils'; -import { sendNotificationToUsers } from '../../src/utils/homepage-notifications.utils'; +import NotificationsService from '../../src/services/notifications.services'; describe('User Tests', () => { let orgId: string; @@ -59,8 +59,8 @@ describe('User Tests', () => { it('Succeeds and gets user notifications', async () => { const testBatman = await createTestUser(batmanAppAdmin, orgId); - await sendNotificationToUsers([testBatman.userId], 'test1', 'test1', orgId); - await sendNotificationToUsers([testBatman.userId], 'test2', 'test2', orgId); + await NotificationsService.sendNotifcationToUsers('test1', 'test1', [testBatman.userId], orgId); + await NotificationsService.sendNotifcationToUsers('test2', 'test2', [testBatman.userId], orgId); const notifications = await UsersService.getUserUnreadNotifications(testBatman.userId, organization); From 327d89349adde046b9c7fa0d504bd7c9e412c1cc Mon Sep 17 00:00:00 2001 From: jessegarcia1 Date: Thu, 12 Dec 2024 18:19:08 -0500 Subject: [PATCH 22/41] #2817 added 1 to date and design review creator to userIDs --- .../src/pages/HomePage/components/DesignReviewCard.tsx | 8 +++++--- .../pages/HomePage/components/UpcomingDesignReviews.tsx | 3 ++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/frontend/src/pages/HomePage/components/DesignReviewCard.tsx b/src/frontend/src/pages/HomePage/components/DesignReviewCard.tsx index 2a2d6ef5f6..446ebfb12b 100644 --- a/src/frontend/src/pages/HomePage/components/DesignReviewCard.tsx +++ b/src/frontend/src/pages/HomePage/components/DesignReviewCard.tsx @@ -71,6 +71,8 @@ function removeYear(str: string): string { const UpcomingDesignReviewsCard: React.FC = ({ designReview, user }) => { const theme = useTheme(); + const datePlusOne = new Date(designReview.dateScheduled); + datePlusOne.setDate(datePlusOne.getDate() + 1); return ( = ({ designReview, > - + {designReview.wbsName} @@ -92,9 +94,9 @@ const UpcomingDesignReviewsCard: React.FC = ({ designReview, {} - {getWeekday(designReview.dateScheduled) + + {getWeekday(datePlusOne) + ', ' + - removeYear(datePipe(designReview.dateScheduled)) + + removeYear(datePipe(datePlusOne)) + ' @ ' + meetingStartTimePipe(designReview.meetingTimes)} diff --git a/src/frontend/src/pages/HomePage/components/UpcomingDesignReviews.tsx b/src/frontend/src/pages/HomePage/components/UpcomingDesignReviews.tsx index 68ee4bc4a2..7d0d4c0565 100644 --- a/src/frontend/src/pages/HomePage/components/UpcomingDesignReviews.tsx +++ b/src/frontend/src/pages/HomePage/components/UpcomingDesignReviews.tsx @@ -37,11 +37,12 @@ const UpcomingDesignReviews: React.FC = ({ user }) => const currentDate = new Date(); const inTwoWeeks = new Date(); inTwoWeeks.setDate(currentDate.getDate() + 14); - const memberUserIds = [ ...review.requiredMembers.map((user) => user.userId), ...review.optionalMembers.map((user) => user.userId) ]; + // added in case the person who created the design review forgets to add their name onto the required members + memberUserIds.concat(review.userCreated.userId); return ( scheduledDate >= currentDate && scheduledDate <= inTwoWeeks && From bcf10578f7534cbc4797fcf05c11697e7ae9e94f Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Sat, 14 Dec 2024 14:53:21 -0500 Subject: [PATCH 23/41] #2817 fixed scrolling on firefox --- .../components/ChangeRequestsToReview.tsx | 2 +- .../HomePage/components/DesignReviewCard.tsx | 7 +++--- .../components/OverdueWorkPackageView.tsx | 22 +++++++++++-------- .../components/ScrollablePageBlock.tsx | 11 ++++++---- .../HomePage/components/TeamTaskCard.tsx | 2 +- 5 files changed, 26 insertions(+), 18 deletions(-) diff --git a/src/frontend/src/pages/HomePage/components/ChangeRequestsToReview.tsx b/src/frontend/src/pages/HomePage/components/ChangeRequestsToReview.tsx index e7161db966..148ed6c822 100644 --- a/src/frontend/src/pages/HomePage/components/ChangeRequestsToReview.tsx +++ b/src/frontend/src/pages/HomePage/components/ChangeRequestsToReview.tsx @@ -19,7 +19,7 @@ const NoChangeRequestsToReview: React.FC = () => { } heading={`You're all caught up!`} - message={'You have no unreviewed changre requests!'} + message={'You have no unreviewed change requests!'} /> ); }; diff --git a/src/frontend/src/pages/HomePage/components/DesignReviewCard.tsx b/src/frontend/src/pages/HomePage/components/DesignReviewCard.tsx index 446ebfb12b..6357d9b4b5 100644 --- a/src/frontend/src/pages/HomePage/components/DesignReviewCard.tsx +++ b/src/frontend/src/pages/HomePage/components/DesignReviewCard.tsx @@ -77,13 +77,14 @@ const UpcomingDesignReviewsCard: React.FC = ({ designReview, - + diff --git a/src/frontend/src/pages/HomePage/components/OverdueWorkPackageView.tsx b/src/frontend/src/pages/HomePage/components/OverdueWorkPackageView.tsx index b3621c742c..fcf630d68a 100644 --- a/src/frontend/src/pages/HomePage/components/OverdueWorkPackageView.tsx +++ b/src/frontend/src/pages/HomePage/components/OverdueWorkPackageView.tsx @@ -57,32 +57,36 @@ const OverdueWorkPackagesView: React.FC = ({ workP - {isEmpty ? : workPackages.map((wp) => )} - + diff --git a/src/frontend/src/pages/HomePage/components/ScrollablePageBlock.tsx b/src/frontend/src/pages/HomePage/components/ScrollablePageBlock.tsx index 25045b6d39..54b11ce434 100644 --- a/src/frontend/src/pages/HomePage/components/ScrollablePageBlock.tsx +++ b/src/frontend/src/pages/HomePage/components/ScrollablePageBlock.tsx @@ -34,23 +34,26 @@ const ScrollablePageBlock: React.FC = ({ children, tit sx={{ mt: 2, display: 'flex', - flexDirection: horizontal ? 'row' : 'column', + flexDirection: 'row', + flexWrap: 'wrap', gap: 2, height: '100%', overflowX: horizontal ? 'auto' : 'hidden', overflowY: horizontal ? 'hidden' : 'auto', '&::-webkit-scrollbar': { - height: '20px' + width: '20px' }, '&::-webkit-scrollbar-track': { backgroundColor: 'transparent' }, '&::-webkit-scrollbar-thumb': { - backgroundColor: theme.palette.error.dark, + backgroundColor: theme.palette.primary.main, borderRadius: '20px', border: '6px solid transparent', backgroundClip: 'content-box' - } + }, + scrollbarWidth: 'auto', + scrollbarColor: `${theme.palette.primary.main} transparent` }} > {children} diff --git a/src/frontend/src/pages/HomePage/components/TeamTaskCard.tsx b/src/frontend/src/pages/HomePage/components/TeamTaskCard.tsx index 76b80e93c3..dfd1485e64 100644 --- a/src/frontend/src/pages/HomePage/components/TeamTaskCard.tsx +++ b/src/frontend/src/pages/HomePage/components/TeamTaskCard.tsx @@ -28,7 +28,7 @@ const TeamTaskCard: React.FC = ({ task, taskNumber }) => { Date: Sat, 14 Dec 2024 14:53:45 -0500 Subject: [PATCH 24/41] #2817-linting --- .../src/pages/HomePage/components/OverdueWorkPackageView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/src/pages/HomePage/components/OverdueWorkPackageView.tsx b/src/frontend/src/pages/HomePage/components/OverdueWorkPackageView.tsx index fcf630d68a..03c820e85b 100644 --- a/src/frontend/src/pages/HomePage/components/OverdueWorkPackageView.tsx +++ b/src/frontend/src/pages/HomePage/components/OverdueWorkPackageView.tsx @@ -1,4 +1,4 @@ -import { Box, Card, CardContent, Stack, Typography, useTheme } from '@mui/material'; +import { Box, Card, CardContent, Typography, useTheme } from '@mui/material'; import WorkPackageCard from './WorkPackageCard'; import { WorkPackage } from 'shared'; import EmptyPageBlockDisplay from './EmptyPageBlockDisplay'; From f429ccca852f1b7b70b683f4e311c5028694f8a4 Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Sat, 14 Dec 2024 15:34:30 -0500 Subject: [PATCH 25/41] #2820-added overdue days to new overdue work package card --- .../src/pages/HomePage/AdminHomePage.tsx | 4 +- .../components/OverdueWorkPackageCard.tsx | 143 ++++++++++++++++++ .../components/OverdueWorkPackageView.tsx | 6 +- 3 files changed, 148 insertions(+), 5 deletions(-) create mode 100644 src/frontend/src/pages/HomePage/components/OverdueWorkPackageCard.tsx diff --git a/src/frontend/src/pages/HomePage/AdminHomePage.tsx b/src/frontend/src/pages/HomePage/AdminHomePage.tsx index edee46bbab..b83e32a6ab 100644 --- a/src/frontend/src/pages/HomePage/AdminHomePage.tsx +++ b/src/frontend/src/pages/HomePage/AdminHomePage.tsx @@ -41,10 +41,10 @@ const AdminHomePage = ({ user }: AdminHomePageProps) => { - + - + diff --git a/src/frontend/src/pages/HomePage/components/OverdueWorkPackageCard.tsx b/src/frontend/src/pages/HomePage/components/OverdueWorkPackageCard.tsx new file mode 100644 index 0000000000..db54cad9aa --- /dev/null +++ b/src/frontend/src/pages/HomePage/components/OverdueWorkPackageCard.tsx @@ -0,0 +1,143 @@ +import { Construction, Work, CalendarMonth } from '@mui/icons-material'; +import { + Box, + Card, + CardContent, + Chip, + CircularProgress, + CircularProgressProps, + Link, + Stack, + Typography, + useTheme +} from '@mui/material'; +import { wbsPipe, WorkPackage } from 'shared'; +import { datePipe, fullNamePipe, projectWbsPipe, wbsNamePipe } from '../../../utils/pipes'; +import { routes } from '../../../utils/routes'; +import { Link as RouterLink } from 'react-router-dom'; +import { useGetManyWorkPackages } from '../../../hooks/work-packages.hooks'; +import { daysOverdue } from '../../../utils/datetime.utils'; +import LoadingIndicator from '../../../components/LoadingIndicator'; + +export const CircularProgressWithLabel = (props: CircularProgressProps & { value: number }) => { + return ( + + +
+ {`${Math.round(props.value)}%`} +
+
+ ); +}; + +const OverdueWorkPackageCard = ({ wp }: { wp: WorkPackage }) => { + const theme = useTheme(); + const { data: blockedByWps, isLoading } = useGetManyWorkPackages(wp.blockedBy); + const numDaysOverdue = daysOverdue(new Date(wp.endDate)); + if (isLoading || !blockedByWps) return ; + return ( + + + + + + + {projectWbsPipe(wp.wbsNum)} - {wp.projectName} + + + + + {wbsPipe(wp.wbsNum)} - {wp.name} + + + + Blocked By: + +
    + {blockedByWps.length === 0 ? ( +
  • + + No Blockers + +
  • + ) : ( + blockedByWps.map((wp) => ( +
  • + + {wbsNamePipe(wp)} + +
  • + )) + )} +
+
+ + + } label={fullNamePipe(wp.lead)} size={'small'} /> + } label={fullNamePipe(wp.manager)} size={'small'} /> + } label={datePipe(new Date(wp.endDate))} size={'small'} /> + + + + + {numDaysOverdue} + + + + Days + + + Overdue + + + + +
+
+
+ ); +}; + +export default OverdueWorkPackageCard; diff --git a/src/frontend/src/pages/HomePage/components/OverdueWorkPackageView.tsx b/src/frontend/src/pages/HomePage/components/OverdueWorkPackageView.tsx index b3621c742c..4686213017 100644 --- a/src/frontend/src/pages/HomePage/components/OverdueWorkPackageView.tsx +++ b/src/frontend/src/pages/HomePage/components/OverdueWorkPackageView.tsx @@ -1,8 +1,8 @@ import { Box, Card, CardContent, Stack, Typography, useTheme } from '@mui/material'; -import WorkPackageCard from './WorkPackageCard'; import { WorkPackage } from 'shared'; import EmptyPageBlockDisplay from './EmptyPageBlockDisplay'; import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined'; +import OverdueWorkPackageCard from './OverdueWorkPackageCard'; interface OverdueWorkPackagesViewProps { workPackages: WorkPackage[]; @@ -41,7 +41,7 @@ const OverdueWorkPackagesView: React.FC = ({ workP }} > - Overdue Work Packages + All Overdue Work Packages = ({ workP height: '100%' }} > - {isEmpty ? : workPackages.map((wp) => )} + {isEmpty ? : workPackages.map((wp) => )}
From f5ed749c2def867ce141733a8bfb6e52ce34a287 Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Sat, 14 Dec 2024 15:44:06 -0500 Subject: [PATCH 26/41] 2820-unused import --- .../pages/HomePage/components/OverdueWorkPackageCard.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/frontend/src/pages/HomePage/components/OverdueWorkPackageCard.tsx b/src/frontend/src/pages/HomePage/components/OverdueWorkPackageCard.tsx index db54cad9aa..deb86badb5 100644 --- a/src/frontend/src/pages/HomePage/components/OverdueWorkPackageCard.tsx +++ b/src/frontend/src/pages/HomePage/components/OverdueWorkPackageCard.tsx @@ -12,7 +12,7 @@ import { useTheme } from '@mui/material'; import { wbsPipe, WorkPackage } from 'shared'; -import { datePipe, fullNamePipe, projectWbsPipe, wbsNamePipe } from '../../../utils/pipes'; +import { datePipe, fullNamePipe, projectWbsPipe } from '../../../utils/pipes'; import { routes } from '../../../utils/routes'; import { Link as RouterLink } from 'react-router-dom'; import { useGetManyWorkPackages } from '../../../hooks/work-packages.hooks'; @@ -84,8 +84,10 @@ const OverdueWorkPackageCard = ({ wp }: { wp: WorkPackage }) => { ) : ( blockedByWps.map((wp) => (
  • - - {wbsNamePipe(wp)} + + + {projectWbsPipe(wp.wbsNum)} - {wp.projectName} +
  • )) From 5c2f759bd941a8fc0c14c458d4b70e95df03b262 Mon Sep 17 00:00:00 2001 From: jessegarcia1 Date: Sat, 14 Dec 2024 17:04:37 -0500 Subject: [PATCH 27/41] #2817 added datetime util to fix timezone offset --- .../src/pages/HomePage/components/DesignReviewCard.tsx | 8 ++++---- src/frontend/src/utils/datetime.utils.ts | 5 +++++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/frontend/src/pages/HomePage/components/DesignReviewCard.tsx b/src/frontend/src/pages/HomePage/components/DesignReviewCard.tsx index 6357d9b4b5..c26badf593 100644 --- a/src/frontend/src/pages/HomePage/components/DesignReviewCard.tsx +++ b/src/frontend/src/pages/HomePage/components/DesignReviewCard.tsx @@ -8,6 +8,7 @@ import { LocationOnOutlined, Computer } from '@mui/icons-material'; import { useHistory } from 'react-router-dom'; import { NERButton } from '../../../components/NERButton'; import { meetingStartTimePipe } from '../../../../../backend/src/utils/design-reviews.utils'; +import { timezoneOffset } from '../../../utils/datetime.utils'; interface DesignReviewProps { designReview: DesignReview; @@ -71,8 +72,7 @@ function removeYear(str: string): string { const UpcomingDesignReviewsCard: React.FC = ({ designReview, user }) => { const theme = useTheme(); - const datePlusOne = new Date(designReview.dateScheduled); - datePlusOne.setDate(datePlusOne.getDate() + 1); + const timezoneAdjustedDate = timezoneOffset(designReview.dateScheduled); return ( = ({ designReview, {} - {getWeekday(datePlusOne) + + {getWeekday(timezoneAdjustedDate) + ', ' + - removeYear(datePipe(datePlusOne)) + + removeYear(datePipe(timezoneAdjustedDate)) + ' @ ' + meetingStartTimePipe(designReview.meetingTimes)} diff --git a/src/frontend/src/utils/datetime.utils.ts b/src/frontend/src/utils/datetime.utils.ts index e728bb5cbf..1b23c43118 100644 --- a/src/frontend/src/utils/datetime.utils.ts +++ b/src/frontend/src/utils/datetime.utils.ts @@ -34,3 +34,8 @@ export const formatDate = (date: Date) => { export const daysOverdue = (deadline: Date) => { return Math.round((new Date().getTime() - deadline.getTime()) / (1000 * 60 * 60 * 24)); }; + +export const timezoneOffset = (date: Date) => { + const timestamp = new Date(date).getTime() - new Date(date).getTimezoneOffset() * -60000; + return new Date(timestamp); +}; From e84cf52c88fb39f222137667a6234da3879c8694 Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Sat, 14 Dec 2024 18:12:00 -0500 Subject: [PATCH 28/41] #2817-fxied mistake --- .../src/pages/HomePage/components/OverdueWorkPackageView.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/frontend/src/pages/HomePage/components/OverdueWorkPackageView.tsx b/src/frontend/src/pages/HomePage/components/OverdueWorkPackageView.tsx index 022e18d979..d0d42294b1 100644 --- a/src/frontend/src/pages/HomePage/components/OverdueWorkPackageView.tsx +++ b/src/frontend/src/pages/HomePage/components/OverdueWorkPackageView.tsx @@ -1,4 +1,4 @@ -import { Box, Card, CardContent, Stack, Typography, useTheme } from '@mui/material'; +import { Box, Card, CardContent, Typography, useTheme } from '@mui/material'; import { WorkPackage } from 'shared'; import EmptyPageBlockDisplay from './EmptyPageBlockDisplay'; import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined'; @@ -86,7 +86,7 @@ const OverdueWorkPackagesView: React.FC = ({ workP }} > {isEmpty ? : workPackages.map((wp) => )} - +
    From 3369a6e153b317f8335624eac841dac835ff2957 Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Sun, 15 Dec 2024 16:01:47 -0500 Subject: [PATCH 29/41] #2999-remove notification endpoint created --- src/backend/src/controllers/users.controllers.ts | 13 +++++++++++++ src/backend/src/routes/users.routes.ts | 5 +++++ src/backend/src/services/users.services.ts | 16 ++++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/src/backend/src/controllers/users.controllers.ts b/src/backend/src/controllers/users.controllers.ts index e491d3f4f4..bf965c8d8e 100644 --- a/src/backend/src/controllers/users.controllers.ts +++ b/src/backend/src/controllers/users.controllers.ts @@ -203,4 +203,17 @@ export default class UsersController { next(error); } } + + static async removeUserNotification(req: Request, res: Response, next: NextFunction) { + try { + const { userId } = req.params; + const { notificationId } = req.body; + const { organization } = req; + + const unreadNotifications = await UsersService.removeUserNotification(userId, notificationId, organization); + res.status(200).json(unreadNotifications); + } catch (error: unknown) { + next(error); + } + } } diff --git a/src/backend/src/routes/users.routes.ts b/src/backend/src/routes/users.routes.ts index 34ae1a0136..622a6fb01c 100644 --- a/src/backend/src/routes/users.routes.ts +++ b/src/backend/src/routes/users.routes.ts @@ -55,5 +55,10 @@ userRouter.post( UsersController.getManyUserTasks ); userRouter.get('/:userId/notifications', UsersController.getUserUnreadNotifications); +userRouter.post( + '/:userId/notifications/remove', + nonEmptyString(body('notificationId')), + UsersController.removeUserNotification +); export default userRouter; diff --git a/src/backend/src/services/users.services.ts b/src/backend/src/services/users.services.ts index d9d37b259e..3b5ecf3bdc 100644 --- a/src/backend/src/services/users.services.ts +++ b/src/backend/src/services/users.services.ts @@ -578,4 +578,20 @@ export default class UsersService { return requestedUser.unreadNotifications.map(notificationTransformer); } + + static async removeUserNotification(userId: string, notificationId: string, organization: Organization) { + const requestedUser = await prisma.user.update({ + where: { userId }, + data: { + unreadNotifications: { + disconnect: { + notificationId + } + } + }, + include: { unreadNotifications: getNotificationQueryArgs(organization.organizationId) } + }); + + return requestedUser.unreadNotifications.map(notificationTransformer); + } } From 4ad0bef2530b71866d31a7cb5df8b03f47447856 Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Sun, 15 Dec 2024 16:18:54 -0500 Subject: [PATCH 30/41] #2999-created tests --- src/backend/src/services/users.services.ts | 10 +++++-- src/backend/tests/unmocked/users.test.ts | 33 ++++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/backend/src/services/users.services.ts b/src/backend/src/services/users.services.ts index 3b5ecf3bdc..fce1944a57 100644 --- a/src/backend/src/services/users.services.ts +++ b/src/backend/src/services/users.services.ts @@ -580,7 +580,13 @@ export default class UsersService { } static async removeUserNotification(userId: string, notificationId: string, organization: Organization) { - const requestedUser = await prisma.user.update({ + const requestedUser = await prisma.user.findUnique({ + where: { userId } + }); + + if (!requestedUser) throw new NotFoundException('User', userId); + + const updatedUser = await prisma.user.update({ where: { userId }, data: { unreadNotifications: { @@ -592,6 +598,6 @@ export default class UsersService { include: { unreadNotifications: getNotificationQueryArgs(organization.organizationId) } }); - return requestedUser.unreadNotifications.map(notificationTransformer); + return updatedUser.unreadNotifications.map(notificationTransformer); } } diff --git a/src/backend/tests/unmocked/users.test.ts b/src/backend/tests/unmocked/users.test.ts index 14d8b0bffe..512a651b90 100644 --- a/src/backend/tests/unmocked/users.test.ts +++ b/src/backend/tests/unmocked/users.test.ts @@ -69,4 +69,37 @@ describe('User Tests', () => { expect(notifications[1].text).toBe('test2'); }); }); + + describe('Remove Notifications', () => { + it('Fails with invalid user', async () => { + const testBatman = await createTestUser(batmanAppAdmin, orgId); + await NotificationsService.sendNotifcationToUsers('test1', 'test1', [testBatman.userId], orgId); + const notifications = await UsersService.getUserUnreadNotifications(testBatman.userId, organization); + + await expect( + async () => await UsersService.removeUserNotification('1', notifications[0].notificationId, organization) + ).rejects.toThrow(new NotFoundException('User', '1')); + }); + + it('Succeeds and gets user notifications', async () => { + const testBatman = await createTestUser(batmanAppAdmin, orgId); + await NotificationsService.sendNotifcationToUsers('test1', 'test1', [testBatman.userId], orgId); + await NotificationsService.sendNotifcationToUsers('test2', 'test2', [testBatman.userId], orgId); + + const notifications = await UsersService.getUserUnreadNotifications(testBatman.userId, organization); + + expect(notifications).toHaveLength(2); + expect(notifications[0].text).toBe('test1'); + expect(notifications[1].text).toBe('test2'); + + const updatedNotifications = await UsersService.removeUserNotification( + testBatman.userId, + notifications[0].notificationId, + organization + ); + + expect(updatedNotifications).toHaveLength(1); + expect(updatedNotifications[0].text).toBe('test2'); + }); + }); }); From bc4200482fd5cd091cde97e9a3a3edd628becb0d Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Sun, 15 Dec 2024 18:47:18 -0500 Subject: [PATCH 31/41] #2999-javadocs --- src/backend/src/services/users.services.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/backend/src/services/users.services.ts b/src/backend/src/services/users.services.ts index fce1944a57..1358ca5f6c 100644 --- a/src/backend/src/services/users.services.ts +++ b/src/backend/src/services/users.services.ts @@ -569,6 +569,12 @@ export default class UsersService { return resolvedTasks.flat(); } + /** + * Gets all of a user's unread notifications + * @param userId id of user to get unread notifications from + * @param organization the user's orgainzation + * @returns the unread notifications of the user + */ static async getUserUnreadNotifications(userId: string, organization: Organization) { const requestedUser = await prisma.user.findUnique({ where: { userId }, @@ -579,6 +585,13 @@ export default class UsersService { return requestedUser.unreadNotifications.map(notificationTransformer); } + /** + * Removes a notification from the user's unread notifications + * @param userId id of the user to remove notification from + * @param notificationId id of the notification to remove + * @param organization the user's organization + * @returns the user's updated unread notifications + */ static async removeUserNotification(userId: string, notificationId: string, organization: Organization) { const requestedUser = await prisma.user.findUnique({ where: { userId } From 46b80c73ebf71042a8f949731ac5783c19d9b451 Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Sun, 15 Dec 2024 21:28:28 -0500 Subject: [PATCH 32/41] #3000-created remove hook --- .../src/services/design-reviews.services.ts | 3 +++ src/frontend/src/apis/users.api.ts | 15 ++++++------ .../src/components/NotificationAlert.tsx | 18 ++++++++++---- .../src/components/NotificationCard.tsx | 5 ++-- src/frontend/src/hooks/users.hooks.ts | 24 ++++++++++++++++++- src/frontend/src/utils/urls.ts | 4 ++-- 6 files changed, 51 insertions(+), 18 deletions(-) diff --git a/src/backend/src/services/design-reviews.services.ts b/src/backend/src/services/design-reviews.services.ts index 8f7511b73c..1804e9ffcc 100644 --- a/src/backend/src/services/design-reviews.services.ts +++ b/src/backend/src/services/design-reviews.services.ts @@ -39,6 +39,7 @@ import { getWorkPackageQueryArgs } from '../prisma-query-args/work-packages.quer import { UserWithSettings } from '../utils/auth.utils'; import { getUserScheduleSettingsQueryArgs } from '../prisma-query-args/user.query-args'; import { createCalendarEvent, deleteCalendarEvent, updateCalendarEvent } from '../utils/google-integration.utils'; +import NotificationsService from './notifications.services'; export default class DesignReviewsService { /** @@ -211,6 +212,8 @@ export default class DesignReviewsService { await sendSlackDRNotifications(teams, designReview, submitter, wbsElement.name); } + NotificationsService.sendNotifcationToUsers('DR created!', 'star', [submitter.userId], organization.organizationId); + return designReviewTransformer(designReview); } diff --git a/src/frontend/src/apis/users.api.ts b/src/frontend/src/apis/users.api.ts index 3bcc945281..5a91bff5fd 100644 --- a/src/frontend/src/apis/users.api.ts +++ b/src/frontend/src/apis/users.api.ts @@ -24,7 +24,6 @@ import { import { AuthenticatedUser, UserSettings } from 'shared'; import { projectTransformer } from './transformers/projects.transformers'; import { taskTransformer } from './transformers/tasks.transformers'; -import notificationTransformer from '../../../backend/src/transformers/notification.transformer'; /** * Fetches all users. @@ -162,13 +161,6 @@ export const getManyUserTasks = (userIds: string[]) => { ); }; -/* - * Sends a notification to the user with the given id - */ -export const sendNotification = (id: string, notification: Notification) => { - return axios.post(apiUrls.userSendNotifications(id), notification); -}; - /* * Gets all unread notifications of the user with the given id */ @@ -177,3 +169,10 @@ export const getNotifications = (id: string) => { transformResponse: (data) => JSON.parse(data) }); }; + +/* + * Removes a notification from the user with the given id + */ +export const removeNotification = (userId: string, notificationId: string) => { + return axios.post(apiUrls.userRemoveNotifications(userId), { notificationId }); +}; diff --git a/src/frontend/src/components/NotificationAlert.tsx b/src/frontend/src/components/NotificationAlert.tsx index 9b60b48760..e7295c119a 100644 --- a/src/frontend/src/components/NotificationAlert.tsx +++ b/src/frontend/src/components/NotificationAlert.tsx @@ -1,17 +1,23 @@ import { Box } from '@mui/material'; import React from 'react'; -import { User } from 'shared'; +import { Notification, User } from 'shared'; import NotificationCard from './NotificationCard'; -import { useUserNotifications } from '../hooks/users.hooks'; +import { useRemoveUserNotification, useUserNotifications } from '../hooks/users.hooks'; interface NotificationAlertProps { user: User; } const NotificationAlert: React.FC = ({ user }) => { - const { data: notifications } = useUserNotifications(user.userId); + const { data: notifications, isLoading: notificationIsLoading } = useUserNotifications(user.userId); + const { mutateAsync: removeNotification } = useRemoveUserNotification(user.userId); - const currentNotification = notifications && notifications.length > 0 ? notifications[0] : undefined; + const currentNotification = + !notificationIsLoading && notifications && notifications.length > 0 ? notifications[0] : undefined; + + const removeNotificationWrapper = async (notification: Notification) => { + await removeNotification(notification); + }; return ( = ({ user }) => { transition: 'transform 0.5s ease-out' }} > - {currentNotification && } + {currentNotification && ( + + )} ); }; diff --git a/src/frontend/src/components/NotificationCard.tsx b/src/frontend/src/components/NotificationCard.tsx index 432517fa98..5f2a213bc2 100644 --- a/src/frontend/src/components/NotificationCard.tsx +++ b/src/frontend/src/components/NotificationCard.tsx @@ -5,9 +5,10 @@ import CloseIcon from '@mui/icons-material/Close'; interface NotificationCardProps { notification: Notification; + removeNotification: (notificationId: Notification) => Promise; } -const NotificationCard: React.FC = ({ notification }) => { +const NotificationCard: React.FC = ({ notification, removeNotification }) => { const theme = useTheme(); return ( @@ -51,7 +52,7 @@ const NotificationCard: React.FC = ({ notification }) => }} > {notification.text} - + removeNotification(notification)}> diff --git a/src/frontend/src/hooks/users.hooks.ts b/src/frontend/src/hooks/users.hooks.ts index e2278ee136..32279217f9 100644 --- a/src/frontend/src/hooks/users.hooks.ts +++ b/src/frontend/src/hooks/users.hooks.ts @@ -20,7 +20,8 @@ import { updateUserScheduleSettings, getUserTasks, getManyUserTasks, - getNotifications + getNotifications, + removeNotification } from '../apis/users.api'; import { User, @@ -274,3 +275,24 @@ export const useUserNotifications = (userId: string) => { return data; }); }; + +/** + * Curstom react hook to remove a notification from a user's unread notifications + * @param userId id of user to get unread notifications from + * @returns + */ +export const useRemoveUserNotification = (userId: string) => { + const queryClient = useQueryClient(); + return useMutation( + ['users', userId, 'notifications', 'remove'], + async (notification: Notification) => { + const { data } = await removeNotification(userId, notification.notificationId); + return data; + }, + { + onSuccess: () => { + queryClient.invalidateQueries(['users', userId, 'notifications']); + } + } + ); +}; diff --git a/src/frontend/src/utils/urls.ts b/src/frontend/src/utils/urls.ts index e5e5a207c0..d50675ed69 100644 --- a/src/frontend/src/utils/urls.ts +++ b/src/frontend/src/utils/urls.ts @@ -26,8 +26,8 @@ const userScheduleSettings = (id: string) => `${usersById(id)}/schedule-settings const userScheduleSettingsSet = () => `${users()}/schedule-settings/set`; const userTasks = (id: string) => `${usersById(id)}/tasks`; const manyUserTasks = () => `${users()}/tasks/get-many`; -const userSendNotifications = (id: string) => `${usersById(id)}/notifications/send`; const userNotifications = (id: string) => `${usersById(id)}/notifications`; +const userRemoveNotifications = (id: string) => `${usersById(id)}/notifications/remove`; /**************** Projects Endpoints ****************/ const projects = () => `${API_URL}/projects`; @@ -214,8 +214,8 @@ export const apiUrls = { userScheduleSettingsSet, userTasks, manyUserTasks, - userSendNotifications, userNotifications, + userRemoveNotifications, projects, allProjects, From 6b4c7a9466b18f7f30a25c1d56a9ac8552a3c20c Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Mon, 16 Dec 2024 14:39:51 -0500 Subject: [PATCH 33/41] #3000-mulitple notifications showing --- .../src/services/design-reviews.services.ts | 3 --- .../src/components/NotificationAlert.tsx | 17 +++++++++++------ .../src/components/NotificationCard.tsx | 7 ++++--- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/backend/src/services/design-reviews.services.ts b/src/backend/src/services/design-reviews.services.ts index 1804e9ffcc..8f7511b73c 100644 --- a/src/backend/src/services/design-reviews.services.ts +++ b/src/backend/src/services/design-reviews.services.ts @@ -39,7 +39,6 @@ import { getWorkPackageQueryArgs } from '../prisma-query-args/work-packages.quer import { UserWithSettings } from '../utils/auth.utils'; import { getUserScheduleSettingsQueryArgs } from '../prisma-query-args/user.query-args'; import { createCalendarEvent, deleteCalendarEvent, updateCalendarEvent } from '../utils/google-integration.utils'; -import NotificationsService from './notifications.services'; export default class DesignReviewsService { /** @@ -212,8 +211,6 @@ export default class DesignReviewsService { await sendSlackDRNotifications(teams, designReview, submitter, wbsElement.name); } - NotificationsService.sendNotifcationToUsers('DR created!', 'star', [submitter.userId], organization.organizationId); - return designReviewTransformer(designReview); } diff --git a/src/frontend/src/components/NotificationAlert.tsx b/src/frontend/src/components/NotificationAlert.tsx index e7295c119a..e053d6fcaf 100644 --- a/src/frontend/src/components/NotificationAlert.tsx +++ b/src/frontend/src/components/NotificationAlert.tsx @@ -1,5 +1,5 @@ import { Box } from '@mui/material'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { Notification, User } from 'shared'; import NotificationCard from './NotificationCard'; import { useRemoveUserNotification, useUserNotifications } from '../hooks/users.hooks'; @@ -9,13 +9,18 @@ interface NotificationAlertProps { } const NotificationAlert: React.FC = ({ user }) => { - const { data: notifications, isLoading: notificationIsLoading } = useUserNotifications(user.userId); - const { mutateAsync: removeNotification } = useRemoveUserNotification(user.userId); + const { data: notifications, isLoading: notificationsIsLoading } = useUserNotifications(user.userId); + const { mutateAsync: removeNotification, isLoading: removeIsLoading } = useRemoveUserNotification(user.userId); + const [currentNotification, setCurrentNotification] = useState(); - const currentNotification = - !notificationIsLoading && notifications && notifications.length > 0 ? notifications[0] : undefined; + useEffect(() => { + if (notifications && notifications.length > 0) { + setCurrentNotification(notifications[0]); + } + }, [notifications]); const removeNotificationWrapper = async (notification: Notification) => { + setCurrentNotification(undefined); await removeNotification(notification); }; @@ -29,7 +34,7 @@ const NotificationAlert: React.FC = ({ user }) => { transition: 'transform 0.5s ease-out' }} > - {currentNotification && ( + {!removeIsLoading && !notificationsIsLoading && currentNotification && ( )} diff --git a/src/frontend/src/components/NotificationCard.tsx b/src/frontend/src/components/NotificationCard.tsx index 5f2a213bc2..a25f1d240e 100644 --- a/src/frontend/src/components/NotificationCard.tsx +++ b/src/frontend/src/components/NotificationCard.tsx @@ -1,4 +1,4 @@ -import { Box, Icon, IconButton, Typography, useTheme } from '@mui/material'; +import { Box, Card, Icon, IconButton, Typography, useTheme } from '@mui/material'; import React from 'react'; import { Notification } from 'shared'; import CloseIcon from '@mui/icons-material/Close'; @@ -12,7 +12,8 @@ const NotificationCard: React.FC = ({ notification, remov const theme = useTheme(); return ( - = ({ notification, remov - + ); }; From 56e668c7aef56b3bcca4f3636090451c6628b36d Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Mon, 16 Dec 2024 15:21:52 -0500 Subject: [PATCH 34/41] #3000-added notifications to sercives --- .../src/services/change-requests.services.ts | 14 ++++++++++++++ .../src/services/design-reviews.services.ts | 8 ++++++++ src/frontend/src/components/NotificationCard.tsx | 2 +- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/backend/src/services/change-requests.services.ts b/src/backend/src/services/change-requests.services.ts index 89cbaaa370..1cbd4a0611 100644 --- a/src/backend/src/services/change-requests.services.ts +++ b/src/backend/src/services/change-requests.services.ts @@ -46,6 +46,7 @@ import { import { ChangeRequestQueryArgs, getChangeRequestQueryArgs } from '../prisma-query-args/change-requests.query-args'; import proposedSolutionTransformer from '../transformers/proposed-solutions.transformer'; import { getProposedSolutionQueryArgs } from '../prisma-query-args/proposed-solutions.query-args'; +import NotificationsService from './notifications.services'; export default class ChangeRequestsService { /** @@ -153,6 +154,12 @@ export default class ChangeRequestsService { // send a reply to a CR's notifications of its updated status await sendSlackCRStatusToThread(updated.notificationSlackThreads, foundCR.crId, foundCR.identifier, accepted); + await NotificationsService.sendNotifcationToUsers( + `CR #${updated.identifier} has been ${accepted ? 'approved!' : 'denied.'}`, + accepted ? 'check_circle' : 'cancel', + [updated.submitter.userId], + organization.organizationId + ); return updated.crId; } @@ -1078,5 +1085,12 @@ export default class ChangeRequestsService { // send slack message to CR reviewers await sendSlackRequestedReviewNotification(newReviewers, changeRequestTransformer(foundCR)); + + await NotificationsService.sendNotifcationToUsers( + `Your review has been requested on CR #${foundCR.identifier}`, + 'edit_note', + newReviewers.map((reviewer) => reviewer.userId), + organization.organizationId + ); } } diff --git a/src/backend/src/services/design-reviews.services.ts b/src/backend/src/services/design-reviews.services.ts index 8f7511b73c..83ceb74ceb 100644 --- a/src/backend/src/services/design-reviews.services.ts +++ b/src/backend/src/services/design-reviews.services.ts @@ -39,6 +39,7 @@ import { getWorkPackageQueryArgs } from '../prisma-query-args/work-packages.quer import { UserWithSettings } from '../utils/auth.utils'; import { getUserScheduleSettingsQueryArgs } from '../prisma-query-args/user.query-args'; import { createCalendarEvent, deleteCalendarEvent, updateCalendarEvent } from '../utils/google-integration.utils'; +import NotificationsService from './notifications.services'; export default class DesignReviewsService { /** @@ -205,6 +206,13 @@ export default class DesignReviewsService { } } + await NotificationsService.sendNotifcationToUsers( + `You have been invited to the ${designReview.wbsElement.name} Design Review!`, + 'calendar_month', + members.map((member) => member.userId), + organization.organizationId + ); + const project = wbsElement.workPackage?.project; const teams = project?.teams; if (teams && teams.length > 0) { diff --git a/src/frontend/src/components/NotificationCard.tsx b/src/frontend/src/components/NotificationCard.tsx index a25f1d240e..d31e576588 100644 --- a/src/frontend/src/components/NotificationCard.tsx +++ b/src/frontend/src/components/NotificationCard.tsx @@ -31,7 +31,7 @@ const NotificationCard: React.FC = ({ notification, remov justifyContent: 'center', alignItems: 'center', padding: 2, - background: 'red', + background: theme.palette.primary.main, width: '30%', borderRadius: 4 }} From e5f2a44fa4d90eb0524df89059134c34e559e0dc Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Mon, 16 Dec 2024 15:25:17 -0500 Subject: [PATCH 35/41] #3000-removed test notifications --- src/backend/src/prisma/seed.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/backend/src/prisma/seed.ts b/src/backend/src/prisma/seed.ts index 6fed93e2c2..fdb5ffefdb 100644 --- a/src/backend/src/prisma/seed.ts +++ b/src/backend/src/prisma/seed.ts @@ -1894,9 +1894,6 @@ const performSeed: () => Promise = async () => { await RecruitmentServices.createFaq(batman, 'When was FinishLine created?', 'FinishLine was created in 2019', ner); await RecruitmentServices.createFaq(batman, 'How many developers are working on FinishLine?', '178 as of 2024', ner); - - await NotificationsService.sendNotifcationToUsers('test1', 'star', [thomasEmrax.userId], ner.organizationId); - await NotificationsService.sendNotifcationToUsers('test2', 'star', [thomasEmrax.userId], ner.organizationId); }; performSeed() From 07180f93136a419180f22e65690c2fc25ed96462 Mon Sep 17 00:00:00 2001 From: jessegarcia1 Date: Mon, 16 Dec 2024 15:51:28 -0500 Subject: [PATCH 36/41] #2817 functions to const and removed line --- .../src/pages/HomePage/components/DesignReviewCard.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/frontend/src/pages/HomePage/components/DesignReviewCard.tsx b/src/frontend/src/pages/HomePage/components/DesignReviewCard.tsx index c26badf593..7ecf0dac6a 100644 --- a/src/frontend/src/pages/HomePage/components/DesignReviewCard.tsx +++ b/src/frontend/src/pages/HomePage/components/DesignReviewCard.tsx @@ -36,9 +36,7 @@ const DesignReviewInfo = ({ icon, text, link }: { icon: React.ReactNode; text: s const DisplayStatus: React.FC = ({ designReview, user }) => { const history = useHistory(); - const confirmedMemberIds = designReview.confirmedMembers.map((user) => user.userId); - console.log('CONFIRMED:', confirmedMemberIds); return ( <> @@ -61,14 +59,14 @@ const DisplayStatus: React.FC = ({ designReview, user }) => { ); }; -function getWeekday(date: Date): string { +const getWeekday = (date: Date): string => { const weekdays: string[] = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; return weekdays[date.getDay()]; -} +}; -function removeYear(str: string): string { +const removeYear = (str: string): string => { return str.substring(0, str.length - 5); -} +}; const UpcomingDesignReviewsCard: React.FC = ({ designReview, user }) => { const theme = useTheme(); From 8806ff1084eed9904c7cedd9dff62eb40e40a728 Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Tue, 17 Dec 2024 20:30:41 -0500 Subject: [PATCH 37/41] #3000-goes to link on click --- .../migration.sql | 1 + src/backend/src/prisma/schema.prisma | 5 +- .../src/services/change-requests.services.ts | 17 ++---- .../src/services/design-reviews.services.ts | 9 +-- .../src/services/notifications.services.ts | 12 +++- .../transformers/notifications.transformer.ts | 3 +- src/backend/src/utils/notifications.utils.ts | 58 ++++++++++++++++++- .../notifications.transformers.ts | 13 ----- .../src/components/NotificationAlert.tsx | 16 ++++- .../src/components/NotificationCard.tsx | 51 +++++++++------- src/shared/src/types/notifications.types.ts | 1 + 11 files changed, 125 insertions(+), 61 deletions(-) rename src/backend/src/prisma/migrations/{20241211195435_announcements_and_notifications => 20241217232127_announcements_and_notifications}/migration.sql (98%) delete mode 100644 src/frontend/src/apis/transformers/notifications.transformers.ts diff --git a/src/backend/src/prisma/migrations/20241211195435_announcements_and_notifications/migration.sql b/src/backend/src/prisma/migrations/20241217232127_announcements_and_notifications/migration.sql similarity index 98% rename from src/backend/src/prisma/migrations/20241211195435_announcements_and_notifications/migration.sql rename to src/backend/src/prisma/migrations/20241217232127_announcements_and_notifications/migration.sql index c57afabd25..fac3e9f480 100644 --- a/src/backend/src/prisma/migrations/20241211195435_announcements_and_notifications/migration.sql +++ b/src/backend/src/prisma/migrations/20241217232127_announcements_and_notifications/migration.sql @@ -13,6 +13,7 @@ CREATE TABLE "Notification" ( "notificationId" TEXT NOT NULL, "text" TEXT NOT NULL, "iconName" TEXT NOT NULL, + "eventLink" TEXT, CONSTRAINT "Notification_pkey" PRIMARY KEY ("notificationId") ); diff --git a/src/backend/src/prisma/schema.prisma b/src/backend/src/prisma/schema.prisma index c06ca3d45f..e0157c3353 100644 --- a/src/backend/src/prisma/schema.prisma +++ b/src/backend/src/prisma/schema.prisma @@ -941,8 +941,9 @@ model Announcement { } model Notification { - notificationId String @id @default(uuid()) + notificationId String @id @default(uuid()) text String iconName String - users User[] @relation("userNotifications") + users User[] @relation("userNotifications") + eventLink String? } diff --git a/src/backend/src/services/change-requests.services.ts b/src/backend/src/services/change-requests.services.ts index 1cbd4a0611..85e0dc5579 100644 --- a/src/backend/src/services/change-requests.services.ts +++ b/src/backend/src/services/change-requests.services.ts @@ -46,7 +46,7 @@ import { import { ChangeRequestQueryArgs, getChangeRequestQueryArgs } from '../prisma-query-args/change-requests.query-args'; import proposedSolutionTransformer from '../transformers/proposed-solutions.transformer'; import { getProposedSolutionQueryArgs } from '../prisma-query-args/proposed-solutions.query-args'; -import NotificationsService from './notifications.services'; +import { sendHomeCrRequestReviewNotification, sendHomeCrReviewedNotification } from '../utils/notifications.utils'; export default class ChangeRequestsService { /** @@ -151,15 +151,11 @@ export default class ChangeRequestsService { // send a notification to the submitter that their change request has been reviewed await sendCRSubmitterReviewedNotification(updated); + await sendHomeCrReviewedNotification(foundCR, updated.submitter, accepted, organization.organizationId); + // send a reply to a CR's notifications of its updated status await sendSlackCRStatusToThread(updated.notificationSlackThreads, foundCR.crId, foundCR.identifier, accepted); - await NotificationsService.sendNotifcationToUsers( - `CR #${updated.identifier} has been ${accepted ? 'approved!' : 'denied.'}`, - accepted ? 'check_circle' : 'cancel', - [updated.submitter.userId], - organization.organizationId - ); return updated.crId; } @@ -1086,11 +1082,6 @@ export default class ChangeRequestsService { // send slack message to CR reviewers await sendSlackRequestedReviewNotification(newReviewers, changeRequestTransformer(foundCR)); - await NotificationsService.sendNotifcationToUsers( - `Your review has been requested on CR #${foundCR.identifier}`, - 'edit_note', - newReviewers.map((reviewer) => reviewer.userId), - organization.organizationId - ); + await sendHomeCrRequestReviewNotification(foundCR, newReviewers, organization.organizationId); } } diff --git a/src/backend/src/services/design-reviews.services.ts b/src/backend/src/services/design-reviews.services.ts index 83ceb74ceb..644903fa43 100644 --- a/src/backend/src/services/design-reviews.services.ts +++ b/src/backend/src/services/design-reviews.services.ts @@ -39,7 +39,7 @@ import { getWorkPackageQueryArgs } from '../prisma-query-args/work-packages.quer import { UserWithSettings } from '../utils/auth.utils'; import { getUserScheduleSettingsQueryArgs } from '../prisma-query-args/user.query-args'; import { createCalendarEvent, deleteCalendarEvent, updateCalendarEvent } from '../utils/google-integration.utils'; -import NotificationsService from './notifications.services'; +import { sendHomeDrNotification } from '../utils/notifications.utils'; export default class DesignReviewsService { /** @@ -206,12 +206,7 @@ export default class DesignReviewsService { } } - await NotificationsService.sendNotifcationToUsers( - `You have been invited to the ${designReview.wbsElement.name} Design Review!`, - 'calendar_month', - members.map((member) => member.userId), - organization.organizationId - ); + await sendHomeDrNotification(designReview, members, submitter, wbsElement.name, organization.organizationId); const project = wbsElement.workPackage?.project; const teams = project?.teams; diff --git a/src/backend/src/services/notifications.services.ts b/src/backend/src/services/notifications.services.ts index 483e6ed9d6..e0617301f5 100644 --- a/src/backend/src/services/notifications.services.ts +++ b/src/backend/src/services/notifications.services.ts @@ -202,13 +202,21 @@ export default class NotificationsService { * @param iconName icon that appears in the notification * @param userIds ids of users to send the notification to * @param organizationId + * @param eventLink link the notification will go to when clicked * @returns the created notification */ - static async sendNotifcationToUsers(text: string, iconName: string, userIds: string[], organizationId: string) { + static async sendNotifcationToUsers( + text: string, + iconName: string, + userIds: string[], + organizationId: string, + eventLink?: string + ) { const createdNotification = await prisma.notification.create({ data: { text, - iconName + iconName, + eventLink }, ...getNotificationQueryArgs(organizationId) }); diff --git a/src/backend/src/transformers/notifications.transformer.ts b/src/backend/src/transformers/notifications.transformer.ts index 32666b151a..45dd25dee9 100644 --- a/src/backend/src/transformers/notifications.transformer.ts +++ b/src/backend/src/transformers/notifications.transformer.ts @@ -6,7 +6,8 @@ const notificationTransformer = (notification: Prisma.NotificationGetPayload { endOfDay.setDate(startOfDay.getDate() + 1); return endOfDay; }; + +export const sendHomeDrNotification = async ( + designReview: Design_Review, + members: User[], + submitter: User, + workPackageName: string, + organizationId: string +) => { + const designReviewLink = `/settings/preferences?drId=${designReview.designReviewId}`; + + const msg = `Design Review for ${workPackageName} is being scheduled by ${submitter.firstName} ${submitter.lastName}`; + await NotificationsService.sendNotifcationToUsers( + msg, + 'calendar_month', + members.map((member) => member.userId), + organizationId, + designReviewLink + ); +}; + +export const sendHomeCrReviewedNotification = async ( + changeRequest: Change_Request, + submitter: User, + accepted: boolean, + organizationId: string +) => { + const isProd = process.env.NODE_ENV === 'production'; + + const changeRequestLink = isProd + ? `https://finishlinebyner.com/change-requests/${changeRequest.crId}` + : `http://localhost:3000/change-requests/${changeRequest.crId}`; + await NotificationsService.sendNotifcationToUsers( + `CR #${changeRequest.identifier} has been ${accepted ? 'approved!' : 'denied.'}`, + accepted ? 'check_circle' : 'cancel', + [submitter.userId], + organizationId, + changeRequestLink + ); +}; + +export const sendHomeCrRequestReviewNotification = async ( + changeRequest: Change_Request, + newReviewers: User[], + organizationId: string +) => { + const changeRequestLink = `/change-requests/${changeRequest.crId}`; + await NotificationsService.sendNotifcationToUsers( + `Your review has been requested on CR #${changeRequest.identifier}`, + 'edit_note', + newReviewers.map((reviewer) => reviewer.userId), + organizationId, + changeRequestLink + ); +}; diff --git a/src/frontend/src/apis/transformers/notifications.transformers.ts b/src/frontend/src/apis/transformers/notifications.transformers.ts deleted file mode 100644 index b429b61efc..0000000000 --- a/src/frontend/src/apis/transformers/notifications.transformers.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Notification } from 'shared'; - -/** - * Transforms a notification - * - * @param notification Incoming task object supplied by the HTTP response. - * @returns Properly transformed notification object. - */ -export const notificationTransformer = (notification: Notification): Notification => { - return { - ...notification - }; -}; diff --git a/src/frontend/src/components/NotificationAlert.tsx b/src/frontend/src/components/NotificationAlert.tsx index e053d6fcaf..f334e06064 100644 --- a/src/frontend/src/components/NotificationAlert.tsx +++ b/src/frontend/src/components/NotificationAlert.tsx @@ -3,6 +3,8 @@ import React, { useEffect, useState } from 'react'; import { Notification, User } from 'shared'; import NotificationCard from './NotificationCard'; import { useRemoveUserNotification, useUserNotifications } from '../hooks/users.hooks'; +import { useHistory } from 'react-router-dom'; +import { routes } from '../utils/routes'; interface NotificationAlertProps { user: User; @@ -12,6 +14,7 @@ const NotificationAlert: React.FC = ({ user }) => { const { data: notifications, isLoading: notificationsIsLoading } = useUserNotifications(user.userId); const { mutateAsync: removeNotification, isLoading: removeIsLoading } = useRemoveUserNotification(user.userId); const [currentNotification, setCurrentNotification] = useState(); + const history = useHistory(); useEffect(() => { if (notifications && notifications.length > 0) { @@ -24,6 +27,13 @@ const NotificationAlert: React.FC = ({ user }) => { await removeNotification(notification); }; + const onClick = async (notification: Notification) => { + if (!!notification.eventLink) { + await removeNotification(notification); + history.push(notification.eventLink); + } + }; + return ( = ({ user }) => { }} > {!removeIsLoading && !notificationsIsLoading && currentNotification && ( - + )} ); diff --git a/src/frontend/src/components/NotificationCard.tsx b/src/frontend/src/components/NotificationCard.tsx index d31e576588..1e4cfb4c02 100644 --- a/src/frontend/src/components/NotificationCard.tsx +++ b/src/frontend/src/components/NotificationCard.tsx @@ -6,11 +6,11 @@ import CloseIcon from '@mui/icons-material/Close'; interface NotificationCardProps { notification: Notification; removeNotification: (notificationId: Notification) => Promise; + onClick: (notificationId: Notification) => Promise; } -const NotificationCard: React.FC = ({ notification, removeNotification }) => { +const NotificationCard: React.FC = ({ notification, removeNotification, onClick }) => { const theme = useTheme(); - return ( = ({ notification, remov > - await onClick(notification)} sx={{ - fontSize: 36 + display: 'flex', + gap: 1, + cursor: !!notification.eventLink ? 'pointer' : 'default' }} > - {notification.iconName} - - - - {notification.text} + + + {notification.iconName} + + + {notification.text} + removeNotification(notification)}> diff --git a/src/shared/src/types/notifications.types.ts b/src/shared/src/types/notifications.types.ts index e4419ef2ed..abd16fcd21 100644 --- a/src/shared/src/types/notifications.types.ts +++ b/src/shared/src/types/notifications.types.ts @@ -2,4 +2,5 @@ export interface Notification { notificationId: string; text: string; iconName: string; + eventLink?: string; } From 0ea40750d43045480cc67de897bbfd77a5bf4852 Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Tue, 17 Dec 2024 21:07:15 -0500 Subject: [PATCH 38/41] #3000-javadocs --- src/backend/src/utils/notifications.utils.ts | 21 ++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/backend/src/utils/notifications.utils.ts b/src/backend/src/utils/notifications.utils.ts index da3ef08f19..7b28ae5b5c 100644 --- a/src/backend/src/utils/notifications.utils.ts +++ b/src/backend/src/utils/notifications.utils.ts @@ -38,6 +38,14 @@ export const endOfDayTomorrow = () => { return endOfDay; }; +/** + * Sends a finishline notification that a design review was scheduled + * @param designReview + * @param members + * @param submitter + * @param workPackageName + * @param organizationId + */ export const sendHomeDrNotification = async ( designReview: Design_Review, members: User[], @@ -57,6 +65,13 @@ export const sendHomeDrNotification = async ( ); }; +/** + * Sends a finishline notification that a change request was reviewed + * @param changeRequest + * @param submitter + * @param accepted + * @param organizationId + */ export const sendHomeCrReviewedNotification = async ( changeRequest: Change_Request, submitter: User, @@ -77,6 +92,12 @@ export const sendHomeCrReviewedNotification = async ( ); }; +/** + * Sends a finishline notification to all requested reviewers of a change request + * @param changeRequest + * @param newReviewers + * @param organizationId + */ export const sendHomeCrRequestReviewNotification = async ( changeRequest: Change_Request, newReviewers: User[], From 382e65b017c2fa462d9d0e617f99261b369c282f Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Tue, 17 Dec 2024 21:09:30 -0500 Subject: [PATCH 39/41] #3000-linting --- src/frontend/src/components/NotificationAlert.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/frontend/src/components/NotificationAlert.tsx b/src/frontend/src/components/NotificationAlert.tsx index f334e06064..8fcc707855 100644 --- a/src/frontend/src/components/NotificationAlert.tsx +++ b/src/frontend/src/components/NotificationAlert.tsx @@ -4,7 +4,6 @@ import { Notification, User } from 'shared'; import NotificationCard from './NotificationCard'; import { useRemoveUserNotification, useUserNotifications } from '../hooks/users.hooks'; import { useHistory } from 'react-router-dom'; -import { routes } from '../utils/routes'; interface NotificationAlertProps { user: User; From c12b8ede495d402f01881000b2639357efa09b5f Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Tue, 17 Dec 2024 21:10:39 -0500 Subject: [PATCH 40/41] #3000-small fix --- src/frontend/src/components/NotificationAlert.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/src/components/NotificationAlert.tsx b/src/frontend/src/components/NotificationAlert.tsx index 8fcc707855..581d849ef0 100644 --- a/src/frontend/src/components/NotificationAlert.tsx +++ b/src/frontend/src/components/NotificationAlert.tsx @@ -28,7 +28,7 @@ const NotificationAlert: React.FC = ({ user }) => { const onClick = async (notification: Notification) => { if (!!notification.eventLink) { - await removeNotification(notification); + await removeNotificationWrapper(notification); history.push(notification.eventLink); } }; From 47da71e6c99b92392230f85e0939f688f9a7dc2e Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Tue, 17 Dec 2024 22:14:49 -0500 Subject: [PATCH 41/41] #3000-consolidated migraations --- .../migration.sql | 8 ------ .../migration.sql | 9 ------ .../migration.sql | 9 ++++++ src/backend/src/utils/notifications.utils.ts | 28 +++++++++---------- 4 files changed, 23 insertions(+), 31 deletions(-) delete mode 100644 src/backend/src/prisma/migrations/20240910005616_add_logo_image_featured_project/migration.sql delete mode 100644 src/backend/src/prisma/migrations/20240911164338_changed_logo_image_name_to_logo_image_id/migration.sql rename src/backend/src/prisma/migrations/{20241217232127_announcements_and_notifications => 20241218031222_home_page_updates}/migration.sql (85%) diff --git a/src/backend/src/prisma/migrations/20240910005616_add_logo_image_featured_project/migration.sql b/src/backend/src/prisma/migrations/20240910005616_add_logo_image_featured_project/migration.sql deleted file mode 100644 index fe5771a6b1..0000000000 --- a/src/backend/src/prisma/migrations/20240910005616_add_logo_image_featured_project/migration.sql +++ /dev/null @@ -1,8 +0,0 @@ --- AlterTable -ALTER TABLE "Organization" ADD COLUMN "logoImage" TEXT; - --- AlterTable -ALTER TABLE "Project" ADD COLUMN "organizationId" TEXT; - --- AddForeignKey -ALTER TABLE "Project" ADD CONSTRAINT "Project_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("organizationId") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/src/backend/src/prisma/migrations/20240911164338_changed_logo_image_name_to_logo_image_id/migration.sql b/src/backend/src/prisma/migrations/20240911164338_changed_logo_image_name_to_logo_image_id/migration.sql deleted file mode 100644 index 2ddd9fa7b6..0000000000 --- a/src/backend/src/prisma/migrations/20240911164338_changed_logo_image_name_to_logo_image_id/migration.sql +++ /dev/null @@ -1,9 +0,0 @@ -/* - Warnings: - - - You are about to drop the column `logoImage` on the `Organization` table. All the data in the column will be lost. - -*/ --- AlterTable -ALTER TABLE "Organization" DROP COLUMN "logoImage", -ADD COLUMN "logoImageId" TEXT; diff --git a/src/backend/src/prisma/migrations/20241217232127_announcements_and_notifications/migration.sql b/src/backend/src/prisma/migrations/20241218031222_home_page_updates/migration.sql similarity index 85% rename from src/backend/src/prisma/migrations/20241217232127_announcements_and_notifications/migration.sql rename to src/backend/src/prisma/migrations/20241218031222_home_page_updates/migration.sql index fac3e9f480..c7975f2e21 100644 --- a/src/backend/src/prisma/migrations/20241217232127_announcements_and_notifications/migration.sql +++ b/src/backend/src/prisma/migrations/20241218031222_home_page_updates/migration.sql @@ -1,3 +1,9 @@ +-- AlterTable +ALTER TABLE "Organization" ADD COLUMN "logoImageId" TEXT; + +-- AlterTable +ALTER TABLE "Project" ADD COLUMN "organizationId" TEXT; + -- CreateTable CREATE TABLE "Announcement" ( "announcementId" TEXT NOT NULL, @@ -42,6 +48,9 @@ CREATE UNIQUE INDEX "_userNotifications_AB_unique" ON "_userNotifications"("A", -- CreateIndex CREATE INDEX "_userNotifications_B_index" ON "_userNotifications"("B"); +-- AddForeignKey +ALTER TABLE "Project" ADD CONSTRAINT "Project_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("organizationId") ON DELETE SET NULL ON UPDATE CASCADE; + -- AddForeignKey ALTER TABLE "Announcement" ADD CONSTRAINT "Announcement_userCreatedId_fkey" FOREIGN KEY ("userCreatedId") REFERENCES "User"("userId") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/src/backend/src/utils/notifications.utils.ts b/src/backend/src/utils/notifications.utils.ts index 7b28ae5b5c..c47ba9b7a0 100644 --- a/src/backend/src/utils/notifications.utils.ts +++ b/src/backend/src/utils/notifications.utils.ts @@ -40,11 +40,11 @@ export const endOfDayTomorrow = () => { /** * Sends a finishline notification that a design review was scheduled - * @param designReview - * @param members - * @param submitter - * @param workPackageName - * @param organizationId + * @param designReview dr that was created + * @param members optional and required members of the dr + * @param submitter the user who created the dr + * @param workPackageName the name of the work package associated witht the dr + * @param organizationId id of the organization of the dr */ export const sendHomeDrNotification = async ( designReview: Design_Review, @@ -67,10 +67,10 @@ export const sendHomeDrNotification = async ( /** * Sends a finishline notification that a change request was reviewed - * @param changeRequest - * @param submitter - * @param accepted - * @param organizationId + * @param changeRequest cr that was requested review + * @param submitter the user who submitted the cr + * @param accepted true if the cr changes were accepted, false if denied + * @param organizationId id of the organization of the cr */ export const sendHomeCrReviewedNotification = async ( changeRequest: Change_Request, @@ -94,20 +94,20 @@ export const sendHomeCrReviewedNotification = async ( /** * Sends a finishline notification to all requested reviewers of a change request - * @param changeRequest - * @param newReviewers - * @param organizationId + * @param changeRequest cr that was requested review + * @param reviewers user's reviewing the cr + * @param organizationId id of the organization of the cr */ export const sendHomeCrRequestReviewNotification = async ( changeRequest: Change_Request, - newReviewers: User[], + reviewers: User[], organizationId: string ) => { const changeRequestLink = `/change-requests/${changeRequest.crId}`; await NotificationsService.sendNotifcationToUsers( `Your review has been requested on CR #${changeRequest.identifier}`, 'edit_note', - newReviewers.map((reviewer) => reviewer.userId), + reviewers.map((reviewer) => reviewer.userId), organizationId, changeRequestLink );