Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#3074-Get Unread Announcements #3076

Merged
merged 19 commits into from
Dec 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/backend/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ import workPackageTemplatesRouter from './src/routes/work-package-templates.rout
import carsRouter from './src/routes/cars.routes';
import organizationRouter from './src/routes/organizations.routes';
import recruitmentRouter from './src/routes/recruitment.routes';
import announcementsRouter from './src/routes/announcements.routes';
import onboardingRouter from './src/routes/onboarding.routes';
import popUpsRouter from './src/routes/pop-up.routes';

const app = express();

Expand Down Expand Up @@ -69,6 +71,8 @@ app.use('/templates', workPackageTemplatesRouter);
app.use('/cars', carsRouter);
app.use('/organizations', organizationRouter);
app.use('/recruitment', recruitmentRouter);
app.use('/pop-ups', popUpsRouter);
app.use('/announcements', announcementsRouter);
app.use('/onboarding', onboardingRouter);
app.use('/', (_req, res) => {
res.status(200).json('Welcome to FinishLine');
Expand Down
18 changes: 18 additions & 0 deletions src/backend/src/controllers/announcements.controllers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { NextFunction, Request, Response } from 'express';
import AnnouncementService from '../services/announcement.service';

export default class AnnouncementController {
static async getUserUnreadAnnouncements(req: Request, res: Response, next: NextFunction) {
try {
const { organization, currentUser } = req;

const unreadAnnouncements = await AnnouncementService.getUserUnreadAnnouncements(
currentUser.userId,
organization.organizationId
);
res.status(200).json(unreadAnnouncements);
} catch (error: unknown) {
next(error);
}
}
}
27 changes: 27 additions & 0 deletions src/backend/src/controllers/popUps.controllers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { NextFunction, Request, Response } from 'express';
import { PopUpService } from '../services/pop-up.services';

export default class PopUpsController {
static async getUserUnreadPopUps(req: Request, res: Response, next: NextFunction) {
try {
const { organization, currentUser } = req;

const unreadPopUps = await PopUpService.getUserUnreadPopUps(currentUser.userId, organization.organizationId);
res.status(200).json(unreadPopUps);
} catch (error: unknown) {
next(error);
}
}

static async removeUserPopUps(req: Request, res: Response, next: NextFunction) {
try {
const { popUpId } = req.params;
const { organization, currentUser } = req;

const unreadPopUps = await PopUpService.removeUserPopUp(currentUser.userId, popUpId, organization.organizationId);
res.status(200).json(unreadPopUps);
} catch (error: unknown) {
next(error);
}
}
}
25 changes: 0 additions & 25 deletions src/backend/src/controllers/users.controllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,29 +191,4 @@ export default class UsersController {
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);
res.status(200).json(unreadNotifications);
} catch (error: unknown) {
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);
}
}
}
11 changes: 11 additions & 0 deletions src/backend/src/prisma-query-args/announcements.query.args.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Prisma } from '@prisma/client';
import { getUserQueryArgs } from './user.query-args';

export type AnnouncementQueryArgs = ReturnType<typeof getAnnouncementQueryArgs>;

export const getAnnouncementQueryArgs = (organizationId: string) =>
Prisma.validator<Prisma.AnnouncementDefaultArgs>()({
include: {
usersReceived: getUserQueryArgs(organizationId)
}
});
11 changes: 0 additions & 11 deletions src/backend/src/prisma-query-args/notifications.query-args.ts

This file was deleted.

11 changes: 11 additions & 0 deletions src/backend/src/prisma-query-args/pop-up.query-args.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Prisma } from '@prisma/client';
import { getUserQueryArgs } from './user.query-args';

export type PopUpQueryArgs = ReturnType<typeof getPopUpQueryArgs>;

export const getPopUpQueryArgs = (organizationId: string) =>
Prisma.validator<Prisma.PopUpDefaultArgs>()({
include: {
users: getUserQueryArgs(organizationId)
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,23 @@ ALTER TABLE "Project" ADD COLUMN "organizationId" TEXT;
CREATE TABLE "Announcement" (
"announcementId" TEXT NOT NULL,
"text" TEXT NOT NULL,
"dateCreated" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"userCreatedId" TEXT NOT NULL,
"dateMessageSent" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"dateDeleted" TIMESTAMP(3),
"senderName" TEXT NOT NULL,
"slackEventId" TEXT NOT NULL,
"slackChannelName" TEXT NOT NULL,

CONSTRAINT "Announcement_pkey" PRIMARY KEY ("announcementId")
);

-- CreateTable
CREATE TABLE "Notification" (
"notificationId" TEXT NOT NULL,
CREATE TABLE "PopUp" (
"popUpId" TEXT NOT NULL,
"text" TEXT NOT NULL,
"iconName" TEXT NOT NULL,
"eventLink" TEXT,

CONSTRAINT "Notification_pkey" PRIMARY KEY ("notificationId")
CONSTRAINT "PopUp_pkey" PRIMARY KEY ("popUpId")
);

-- CreateTable
Expand All @@ -32,37 +35,37 @@ CREATE TABLE "_receivedAnnouncements" (
);

-- CreateTable
CREATE TABLE "_userNotifications" (
CREATE TABLE "_userPopUps" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL
);

-- CreateIndex
CREATE UNIQUE INDEX "Announcement_slackEventId_key" ON "Announcement"("slackEventId");

-- CreateIndex
CREATE UNIQUE INDEX "_receivedAnnouncements_AB_unique" ON "_receivedAnnouncements"("A", "B");

-- CreateIndex
CREATE INDEX "_receivedAnnouncements_B_index" ON "_receivedAnnouncements"("B");

-- CreateIndex
CREATE UNIQUE INDEX "_userNotifications_AB_unique" ON "_userNotifications"("A", "B");
CREATE UNIQUE INDEX "_userPopUps_AB_unique" ON "_userPopUps"("A", "B");

-- CreateIndex
CREATE INDEX "_userNotifications_B_index" ON "_userNotifications"("B");
CREATE INDEX "_userPopUps_B_index" ON "_userPopUps"("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;

-- 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 "_userNotifications" ADD CONSTRAINT "_userNotifications_A_fkey" FOREIGN KEY ("A") REFERENCES "Notification"("notificationId") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "_userPopUps" ADD CONSTRAINT "_userPopUps_A_fkey" FOREIGN KEY ("A") REFERENCES "PopUp"("popUpId") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "_userNotifications" ADD CONSTRAINT "_userNotifications_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("userId") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "_userPopUps" ADD CONSTRAINT "_userPopUps_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("userId") ON DELETE CASCADE ON UPDATE CASCADE;
35 changes: 18 additions & 17 deletions src/backend/src/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -180,9 +180,8 @@ model User {
deletedFrequentlyAskedQuestions FrequentlyAskedQuestion[] @relation(name: "frequentlyAskedQuestionDeleter")
createdMilestones Milestone[] @relation(name: "milestoneCreator")
deletedMilestones Milestone[] @relation(name: "milestoneDeleter")
receivedAnnouncements Announcement[] @relation(name: "receivedAnnouncements")
createdAnnouncements Announcement[] @relation(name: "createdAnnouncements")
unreadNotifications Notification[] @relation(name: "userNotifications")
unreadAnnouncements Announcement[] @relation(name: "receivedAnnouncements")
unreadPopUps PopUp[] @relation(name: "userPopUps")
}

model Role {
Expand Down Expand Up @@ -933,18 +932,20 @@ model Milestone {
}

model Announcement {
announcementId String @id @default(uuid())
text String
usersReceived User[] @relation("receivedAnnouncements")
dateCreated DateTime @default(now())
userCreatedId String
userCreated User @relation("createdAnnouncements", fields: [userCreatedId], references: [userId])
}

model Notification {
notificationId String @id @default(uuid())
text String
iconName String
users User[] @relation("userNotifications")
eventLink String?
announcementId String @id @default(uuid())
text String
usersReceived User[] @relation("receivedAnnouncements")
dateMessageSent DateTime @default(now())
dateDeleted DateTime?
senderName String
slackEventId String @unique
slackChannelName String
}

model PopUp {
popUpId String @id @default(uuid())
text String
iconName String
users User[] @relation("userPopUps")
eventLink String?
}
12 changes: 11 additions & 1 deletion src/backend/src/prisma/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +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';
import AnnouncementService from '../services/announcement.service';

const prisma = new PrismaClient();

Expand Down Expand Up @@ -1894,6 +1894,16 @@ 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 AnnouncementService.createAnnouncement(
'Welcome to Finishline!',
[regina.userId],
new Date(),
'Thomas Emrax',
'1',
'software',
ner.organizationId
);
};

performSeed()
Expand Down
8 changes: 8 additions & 0 deletions src/backend/src/routes/announcements.routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import express from 'express';
import AnnouncementController from '../controllers/announcements.controllers';

const announcementsRouter = express.Router();

announcementsRouter.get('/current-user', AnnouncementController.getUserUnreadAnnouncements);

export default announcementsRouter;
9 changes: 9 additions & 0 deletions src/backend/src/routes/pop-up.routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import express from 'express';
import PopUpsController from '../controllers/popUps.controllers';

const popUpsRouter = express.Router();

popUpsRouter.get('/current-user', PopUpsController.getUserUnreadPopUps);
popUpsRouter.post('/:popUpId/remove', PopUpsController.removeUserPopUps);

export default popUpsRouter;
6 changes: 0 additions & 6 deletions src/backend/src/routes/users.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,5 @@ userRouter.post(
validateInputs,
UsersController.getManyUserTasks
);
userRouter.get('/:userId/notifications', UsersController.getUserUnreadNotifications);
userRouter.post(
'/:userId/notifications/remove',
nonEmptyString(body('notificationId')),
UsersController.removeUserNotification
);

export default userRouter;
68 changes: 68 additions & 0 deletions src/backend/src/services/announcement.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { Announcement } from 'shared';
import prisma from '../prisma/prisma';
import { getAnnouncementQueryArgs } from '../prisma-query-args/announcements.query.args';
import announcementTransformer from '../transformers/announcements.transformer';
import { HttpException } from '../utils/errors.utils';

export default class AnnouncementService {
/**
* Creates an announcement that is sent to users
* this data is populated from slack events
* @param text slack message text
* @param usersReceivedIds users to send announcements to
* @param dateCreated date created of slack message
* @param senderName name of user who sent slack message
* @param slackEventId id of slack event (provided by slack api)
* @param slackChannelName name of channel message was sent in
* @param organizationId id of organization of users
* @returns the created announcement
*/
static async createAnnouncement(
Peyton-McKee marked this conversation as resolved.
Show resolved Hide resolved
text: string,
usersReceivedIds: string[],
dateMessageSent: Date,
senderName: string,
slackEventId: string,
slackChannelName: string,
organizationId: string
): Promise<Announcement> {
const announcement = await prisma.announcement.create({
data: {
text,
usersReceived: {
connect: usersReceivedIds.map((id) => ({
userId: id
}))
},
dateMessageSent,
senderName,
slackEventId,
slackChannelName
},
...getAnnouncementQueryArgs(organizationId)
});

return announcementTransformer(announcement);
}

/**
* Gets all of a user's unread announcements
* @param userId id of the current user
* @param organization the user's orgainzation
* @returns the unread announcements of the user
*/
static async getUserUnreadAnnouncements(userId: string, organizationId: string) {
const unreadAnnouncements = await prisma.announcement.findMany({
where: {
usersReceived: {
some: { userId }
}
},
...getAnnouncementQueryArgs(organizationId)
});

if (!unreadAnnouncements) throw new HttpException(404, 'User Unread Announcements Not Found');

return unreadAnnouncements.map(announcementTransformer);
}
}
Loading
Loading