Skip to content

Commit

Permalink
Merge pull request #3070 from Northeastern-Electric-Racing/#3000-Caio…
Browse files Browse the repository at this point in the history
…-DisplayNotifications

#3000-Displaying Notifications
  • Loading branch information
Peyton-McKee authored Dec 18, 2024
2 parents e2327d6 + 47da71e commit bc93cf1
Show file tree
Hide file tree
Showing 17 changed files with 317 additions and 37 deletions.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -13,6 +19,7 @@ CREATE TABLE "Notification" (
"notificationId" TEXT NOT NULL,
"text" TEXT NOT NULL,
"iconName" TEXT NOT NULL,
"eventLink" TEXT,

CONSTRAINT "Notification_pkey" PRIMARY KEY ("notificationId")
);
Expand Down Expand Up @@ -41,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;

Expand Down
5 changes: 3 additions & 2 deletions src/backend/src/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -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?
}
2 changes: 0 additions & 2 deletions src/backend/src/prisma/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1894,8 +1894,6 @@ const performSeed: () => Promise<void> = 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()
Expand Down
5 changes: 5 additions & 0 deletions src/backend/src/services/change-requests.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { sendHomeCrRequestReviewNotification, sendHomeCrReviewedNotification } from '../utils/notifications.utils';

export default class ChangeRequestsService {
/**
Expand Down Expand Up @@ -150,6 +151,8 @@ 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);

Expand Down Expand Up @@ -1078,5 +1081,7 @@ export default class ChangeRequestsService {

// send slack message to CR reviewers
await sendSlackRequestedReviewNotification(newReviewers, changeRequestTransformer(foundCR));

await sendHomeCrRequestReviewNotification(foundCR, newReviewers, organization.organizationId);
}
}
3 changes: 3 additions & 0 deletions src/backend/src/services/design-reviews.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { sendHomeDrNotification } from '../utils/notifications.utils';

export default class DesignReviewsService {
/**
Expand Down Expand Up @@ -205,6 +206,8 @@ export default class DesignReviewsService {
}
}

await sendHomeDrNotification(designReview, members, submitter, wbsElement.name, organization.organizationId);

const project = wbsElement.workPackage?.project;
const teams = project?.teams;
if (teams && teams.length > 0) {
Expand Down
12 changes: 10 additions & 2 deletions src/backend/src/services/notifications.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
});
Expand Down
3 changes: 2 additions & 1 deletion src/backend/src/transformers/notifications.transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ const notificationTransformer = (notification: Prisma.NotificationGetPayload<Not
return {
notificationId: notification.notificationId,
text: notification.text,
iconName: notification.iconName
iconName: notification.iconName,
eventLink: notification.eventLink ?? undefined
};
};

Expand Down
79 changes: 78 additions & 1 deletion src/backend/src/utils/notifications.utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Task as Prisma_Task, WBS_Element, Design_Review } from '@prisma/client';
import { Task as Prisma_Task, WBS_Element, Design_Review, Change_Request } from '@prisma/client';
import { UserWithSettings } from './auth.utils';
import NotificationsService from '../services/notifications.services';
import { User } from '@prisma/client';

export type TaskWithAssignees = Prisma_Task & {
assignees: UserWithSettings[] | null;
Expand Down Expand Up @@ -35,3 +37,78 @@ export const endOfDayTomorrow = () => {
endOfDay.setDate(startOfDay.getDate() + 1);
return endOfDay;
};

/**
* Sends a finishline notification that a design review was scheduled
* @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,
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
);
};

/**
* Sends a finishline notification that a change request was reviewed
* @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,
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
);
};

/**
* Sends a finishline notification to all requested reviewers of a change request
* @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,
reviewers: User[],
organizationId: string
) => {
const changeRequestLink = `/change-requests/${changeRequest.crId}`;
await NotificationsService.sendNotifcationToUsers(
`Your review has been requested on CR #${changeRequest.identifier}`,
'edit_note',
reviewers.map((reviewer) => reviewer.userId),
organizationId,
changeRequestLink
);
};
17 changes: 17 additions & 0 deletions src/frontend/src/apis/users.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import axios from '../utils/axios';
import {
Notification,
Project,
SetUserScheduleSettingsPayload,
Task,
Expand Down Expand Up @@ -159,3 +160,19 @@ export const getManyUserTasks = (userIds: string[]) => {
}
);
};

/*
* Gets all unread notifications of the user with the given id
*/
export const getNotifications = (id: string) => {
return axios.get<Notification[]>(apiUrls.userNotifications(id), {
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<Notification[]>(apiUrls.userRemoveNotifications(userId), { notificationId });
};
57 changes: 57 additions & 0 deletions src/frontend/src/components/NotificationAlert.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Box } from '@mui/material';
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';

interface NotificationAlertProps {
user: User;
}

const NotificationAlert: React.FC<NotificationAlertProps> = ({ user }) => {
const { data: notifications, isLoading: notificationsIsLoading } = useUserNotifications(user.userId);
const { mutateAsync: removeNotification, isLoading: removeIsLoading } = useRemoveUserNotification(user.userId);
const [currentNotification, setCurrentNotification] = useState<Notification>();
const history = useHistory();

useEffect(() => {
if (notifications && notifications.length > 0) {
setCurrentNotification(notifications[0]);
}
}, [notifications]);

const removeNotificationWrapper = async (notification: Notification) => {
setCurrentNotification(undefined);
await removeNotification(notification);
};

const onClick = async (notification: Notification) => {
if (!!notification.eventLink) {
await removeNotificationWrapper(notification);
history.push(notification.eventLink);
}
};

return (
<Box
sx={{
position: 'fixed',
top: 16,
right: 16,
transform: !!currentNotification ? 'translateX(0)' : 'translateX(110%)',
transition: 'transform 0.5s ease-out'
}}
>
{!removeIsLoading && !notificationsIsLoading && currentNotification && (
<NotificationCard
notification={currentNotification}
removeNotification={removeNotificationWrapper}
onClick={onClick}
/>
)}
</Box>
);
};

export default NotificationAlert;
73 changes: 73 additions & 0 deletions src/frontend/src/components/NotificationCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
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';

interface NotificationCardProps {
notification: Notification;
removeNotification: (notificationId: Notification) => Promise<void>;
onClick: (notificationId: Notification) => Promise<void>;
}

const NotificationCard: React.FC<NotificationCardProps> = ({ notification, removeNotification, onClick }) => {
const theme = useTheme();
return (
<Card
variant={'outlined'}
sx={{
display: 'flex',
justifyContent: 'left',
alignItems: 'center',
gap: 1,
background: theme.palette.background.paper,
width: 300,
borderRadius: 4,
padding: 1
}}
>
<Box
sx={{
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}}
>
<Box
onClick={async () => await onClick(notification)}
sx={{
display: 'flex',
gap: 1,
cursor: !!notification.eventLink ? 'pointer' : 'default'
}}
>
<Box
sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
padding: 2,
background: theme.palette.primary.main,
width: '30%',
borderRadius: 4
}}
>
<Icon
sx={{
fontSize: 36
}}
>
{notification.iconName}
</Icon>
</Box>
<Typography variant="subtitle2">{notification.text}</Typography>
</Box>
<IconButton onClick={() => removeNotification(notification)}>
<CloseIcon />
</IconButton>
</Box>
</Card>
);
};

export default NotificationCard;
Loading

0 comments on commit bc93cf1

Please sign in to comment.