diff --git a/src/backend/index.ts b/src/backend/index.ts index abe9cc0a61..d30eea4b07 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -17,7 +17,12 @@ 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'; +<<<<<<< HEAD import { slackEvents } from './src/routes/slack.routes'; +======= +import announcementsRouter from './src/routes/announcements.routes'; +import onboardingRouter from './src/routes/onboarding.routes'; +>>>>>>> #3074-GetUnreadAnnouncements const app = express(); @@ -73,6 +78,8 @@ app.use('/templates', workPackageTemplatesRouter); app.use('/cars', carsRouter); app.use('/organizations', organizationRouter); app.use('/recruitment', recruitmentRouter); +app.use('/announcements', announcementsRouter); +app.use('/onboarding', onboardingRouter); app.use('/', (_req, res) => { res.status(200).json('Welcome to FinishLine'); }); diff --git a/src/backend/src/controllers/announcements.controllers.ts b/src/backend/src/controllers/announcements.controllers.ts new file mode 100644 index 0000000000..e5ccafbe06 --- /dev/null +++ b/src/backend/src/controllers/announcements.controllers.ts @@ -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); + } + } +} diff --git a/src/backend/src/controllers/notifications.controllers.ts b/src/backend/src/controllers/notifications.controllers.ts index bdf9c44f13..e85846405b 100644 --- a/src/backend/src/controllers/notifications.controllers.ts +++ b/src/backend/src/controllers/notifications.controllers.ts @@ -11,4 +11,34 @@ export default class NotificationsController { next(error); } } + + static async getUserUnreadNotifications(req: Request, res: Response, next: NextFunction) { + try { + const { organization, currentUser } = req; + + const unreadNotifications = await NotificationsService.getUserUnreadNotifications( + currentUser.userId, + organization.organizationId + ); + res.status(200).json(unreadNotifications); + } catch (error: unknown) { + next(error); + } + } + + static async removeUserNotification(req: Request, res: Response, next: NextFunction) { + try { + const { notificationId } = req.params; + const { organization, currentUser } = req; + + const unreadNotifications = await NotificationsService.removeUserNotification( + currentUser.userId, + notificationId, + organization.organizationId + ); + res.status(200).json(unreadNotifications); + } catch (error: unknown) { + next(error); + } + } } diff --git a/src/backend/src/controllers/onboarding.controller.ts b/src/backend/src/controllers/onboarding.controller.ts new file mode 100644 index 0000000000..2bd8660a35 --- /dev/null +++ b/src/backend/src/controllers/onboarding.controller.ts @@ -0,0 +1,21 @@ +import { NextFunction, Request, Response } from 'express'; +import OnboardingServices from '../services/onboarding.services'; + +export default class OnboardingController { + static async downloadImage(req: Request, res: Response, next: NextFunction) { + try { + const { fileId } = req.params; + + const imageData = await OnboardingServices.downloadImage(fileId); + + // Set the appropriate headers for the HTTP response + res.setHeader('content-type', String(imageData.type)); + res.setHeader('content-length', imageData.buffer.length); + + // Send the Buffer as the response body + res.send(imageData.buffer); + } catch (error: unknown) { + return next(error); + } + } +} diff --git a/src/backend/src/controllers/organizations.controllers.ts b/src/backend/src/controllers/organizations.controllers.ts index af14eb1dd8..64a59bb0ee 100644 --- a/src/backend/src/controllers/organizations.controllers.ts +++ b/src/backend/src/controllers/organizations.controllers.ts @@ -78,6 +78,7 @@ export default class OrganizationsController { if (!req.file) { throw new HttpException(400, 'Invalid or undefined image data'); } + const updatedOrg = await OrganizationsService.setLogoImage(req.file, req.currentUser, req.organization); res.status(200).json(updatedOrg); @@ -88,7 +89,9 @@ export default class OrganizationsController { static async getOrganizationLogoImage(req: Request, res: Response, next: NextFunction) { try { - const logoImageId = await OrganizationsService.getLogoImage(req.organization.organizationId); + const { organization } = req; + + const logoImageId = await OrganizationsService.getLogoImage(organization.organizationId); res.status(200).json(logoImageId); } catch (error: unknown) { next(error); @@ -117,4 +120,19 @@ export default class OrganizationsController { next(error); } } + + static async setSlackWorkspaceId(req: Request, res: Response, next: NextFunction) { + try { + const { workspaceId } = req.body; + + const updatedOrg = await OrganizationsService.setSlackWorkspaceId( + workspaceId, + req.currentUser, + req.organization.organizationId + ); + res.status(200).json(updatedOrg); + } catch (error: unknown) { + next(error); + } + } } diff --git a/src/backend/src/controllers/users.controllers.ts b/src/backend/src/controllers/users.controllers.ts index b3fee931c2..8076e225d7 100644 --- a/src/backend/src/controllers/users.controllers.ts +++ b/src/backend/src/controllers/users.controllers.ts @@ -191,42 +191,4 @@ export default class UsersController { next(error); } } - - static async getUserUnreadNotifications(req: Request, res: Response, next: NextFunction) { - try { - const { organization, currentUser } = req; - - const unreadNotifications = await UsersService.getUserUnreadNotifications(currentUser.userId, organization); - res.status(200).json(unreadNotifications); - } catch (error: unknown) { - next(error); - } - } - - static async removeUserNotification(req: Request, res: Response, next: NextFunction) { - try { - const { notificationId } = req.params; - const { organization, currentUser } = req; - - const unreadNotifications = await UsersService.removeUserNotification( - currentUser.userId, - notificationId, - organization - ); - res.status(200).json(unreadNotifications); - } catch (error: unknown) { - next(error); - } - } - - static async getUserUnreadAnnouncements(req: Request, res: Response, next: NextFunction) { - try { - const { organization, currentUser } = req; - - const unreadAnnouncements = await UsersService.getUserUnreadAnnouncements(currentUser.userId, organization); - res.status(200).json(unreadAnnouncements); - } catch (error: unknown) { - next(error); - } - } } diff --git a/src/backend/src/prisma/migrations/20241218160143_homepage_updates/migration.sql b/src/backend/src/prisma/migrations/20241218160143_homepage_updates/migration.sql deleted file mode 100644 index 37c468e58e..0000000000 --- a/src/backend/src/prisma/migrations/20241218160143_homepage_updates/migration.sql +++ /dev/null @@ -1,70 +0,0 @@ --- AlterTable -ALTER TABLE "Organization" ADD COLUMN "logoImageId" TEXT; - --- AlterTable -ALTER TABLE "Project" ADD COLUMN "organizationId" TEXT; - --- CreateTable -CREATE TABLE "Announcement" ( - "announcementId" TEXT NOT NULL, - "text" TEXT NOT NULL, - "dateCreated" TIMESTAMP(3) NOT NULL, - "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, - "text" TEXT NOT NULL, - "iconName" TEXT NOT NULL, - "eventLink" TEXT, - - CONSTRAINT "Notification_pkey" PRIMARY KEY ("notificationId") -); - --- CreateTable -CREATE TABLE "_receivedAnnouncements" ( - "A" TEXT NOT NULL, - "B" TEXT NOT NULL -); - --- CreateTable -CREATE TABLE "_userNotifications" ( - "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"); - --- 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 "_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; - --- AddForeignKey -ALTER TABLE "_userNotifications" ADD CONSTRAINT "_userNotifications_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("userId") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/src/backend/src/prisma/migrations/20241220204512_home_page_updates/migration.sql b/src/backend/src/prisma/migrations/20241222131945_homepage_redesign/migration.sql similarity index 97% rename from src/backend/src/prisma/migrations/20241220204512_home_page_updates/migration.sql rename to src/backend/src/prisma/migrations/20241222131945_homepage_redesign/migration.sql index e20892547e..3e40275240 100644 --- a/src/backend/src/prisma/migrations/20241220204512_home_page_updates/migration.sql +++ b/src/backend/src/prisma/migrations/20241222131945_homepage_redesign/migration.sql @@ -9,7 +9,7 @@ ALTER TABLE "Project" ADD COLUMN "organizationId" TEXT; CREATE TABLE "Announcement" ( "announcementId" TEXT NOT NULL, "text" TEXT NOT NULL, - "dateCreated" TIMESTAMP(3) NOT NULL, + "dateMessageSent" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "dateDeleted" TIMESTAMP(3), "senderName" TEXT NOT NULL, "slackEventId" TEXT NOT NULL, diff --git a/src/backend/src/prisma/schema.prisma b/src/backend/src/prisma/schema.prisma index 3068c9558f..8c4e6e1642 100644 --- a/src/backend/src/prisma/schema.prisma +++ b/src/backend/src/prisma/schema.prisma @@ -935,7 +935,7 @@ model Announcement { announcementId String @id @default(uuid()) text String usersReceived User[] @relation("receivedAnnouncements") - dateCreated DateTime + dateMessageSent DateTime @default(now()) dateDeleted DateTime? senderName String slackEventId String @unique diff --git a/src/backend/src/routes/announcements.routes.ts b/src/backend/src/routes/announcements.routes.ts new file mode 100644 index 0000000000..b772f09fbb --- /dev/null +++ b/src/backend/src/routes/announcements.routes.ts @@ -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; diff --git a/src/backend/src/routes/notifications.routes.ts b/src/backend/src/routes/notifications.routes.ts index 4701b0f3ea..d5411bddde 100644 --- a/src/backend/src/routes/notifications.routes.ts +++ b/src/backend/src/routes/notifications.routes.ts @@ -4,5 +4,7 @@ import NotificationsController from '../controllers/notifications.controllers'; const notificationsRouter = express.Router(); notificationsRouter.post('/task-deadlines', NotificationsController.sendDailySlackNotifications); +notificationsRouter.get('/current-user', NotificationsController.getUserUnreadNotifications); +notificationsRouter.post('/:notificationId/remove', NotificationsController.removeUserNotification); export default notificationsRouter; diff --git a/src/backend/src/routes/onboarding.routes.ts b/src/backend/src/routes/onboarding.routes.ts new file mode 100644 index 0000000000..fabce68796 --- /dev/null +++ b/src/backend/src/routes/onboarding.routes.ts @@ -0,0 +1,8 @@ +import express from 'express'; +import OnboardingController from '../controllers/onboarding.controller'; + +const onboardingRouter = express.Router(); + +onboardingRouter.get('/image/:fileId', OnboardingController.downloadImage); + +export default onboardingRouter; diff --git a/src/backend/src/routes/organizations.routes.ts b/src/backend/src/routes/organizations.routes.ts index 4b3c534fb6..991eb8f95f 100644 --- a/src/backend/src/routes/organizations.routes.ts +++ b/src/backend/src/routes/organizations.routes.ts @@ -36,4 +36,9 @@ organizationRouter.post( OrganizationsController.setOrganizationDescription ); organizationRouter.get('/featured-projects', OrganizationsController.getOrganizationFeaturedProjects); +organizationRouter.post( + '/workspaceId/set', + nonEmptyString(body('workspaceId')), + OrganizationsController.setSlackWorkspaceId +); export default organizationRouter; diff --git a/src/backend/src/routes/users.routes.ts b/src/backend/src/routes/users.routes.ts index 96be49828c..2f95201f6f 100644 --- a/src/backend/src/routes/users.routes.ts +++ b/src/backend/src/routes/users.routes.ts @@ -54,8 +54,5 @@ userRouter.post( validateInputs, UsersController.getManyUserTasks ); -userRouter.get('/notifications/current-user', UsersController.getUserUnreadNotifications); -userRouter.get('/announcements/current-user', UsersController.getUserUnreadAnnouncements); -userRouter.post('/notifications/:notificationId/remove', UsersController.removeUserNotification); export default userRouter; diff --git a/src/backend/src/services/announcement.service.ts b/src/backend/src/services/announcement.service.ts index 9ff1e7007e..42f83b70e4 100644 --- a/src/backend/src/services/announcement.service.ts +++ b/src/backend/src/services/announcement.service.ts @@ -3,6 +3,7 @@ import prisma from '../prisma/prisma'; import { getAnnouncementQueryArgs } from '../prisma-query-args/announcements.query.args'; import announcementTransformer from '../transformers/announcements.transformer'; import { NotFoundException } from '../utils/errors.utils'; +import { HttpException } from '../utils/errors.utils'; export default class AnnouncementService { /** @@ -20,7 +21,7 @@ export default class AnnouncementService { static async createAnnouncement( text: string, usersReceivedIds: string[], - dateCreated: Date, + dateMessageSent: Date, senderName: string, slackEventId: string, slackChannelName: string, @@ -34,7 +35,7 @@ export default class AnnouncementService { userId: id })) }, - dateCreated, + dateMessageSent, senderName, slackEventId, slackChannelName @@ -104,4 +105,25 @@ export default class AnnouncementService { 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); + } } diff --git a/src/backend/src/services/notifications.services.ts b/src/backend/src/services/notifications.services.ts index e0617301f5..3da62b744f 100644 --- a/src/backend/src/services/notifications.services.ts +++ b/src/backend/src/services/notifications.services.ts @@ -196,6 +196,52 @@ export default class NotificationsService { await Promise.all(promises); } + /** + * 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, organizationId: string) { + const unreadNotifications = await prisma.notification.findMany({ + where: { + users: { + some: { userId } + } + }, + ...getNotificationQueryArgs(organizationId) + }); + + if (!unreadNotifications) throw new HttpException(404, 'User Unread Notifications Not Found'); + + return unreadNotifications.map(notificationTransformer); + } + + /** + * Removes a notification from the user's unread notifications + * @param userId id of the current user + * @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, organizationId: string) { + const updatedUser = await prisma.user.update({ + where: { userId }, + data: { + unreadNotifications: { + disconnect: { + notificationId + } + } + }, + include: { unreadNotifications: getNotificationQueryArgs(organizationId) } + }); + + if (!updatedUser) throw new HttpException(404, `Failed to remove notication: ${notificationId}`); + + return updatedUser.unreadNotifications.map(notificationTransformer); + } + /** * Creates and sends a notification to all users with the given userIds * @param text writing in the notification diff --git a/src/backend/src/services/onboarding.services.ts b/src/backend/src/services/onboarding.services.ts new file mode 100644 index 0000000000..b351952bf9 --- /dev/null +++ b/src/backend/src/services/onboarding.services.ts @@ -0,0 +1,11 @@ +import { NotFoundException } from '../utils/errors.utils'; +import { downloadImageFile } from '../utils/google-integration.utils'; + +export default class OnboardingServices { + static async downloadImage(fileId: string) { + const fileData = await downloadImageFile(fileId); + + if (!fileData) throw new NotFoundException('Image File', fileId); + return fileData; + } +} diff --git a/src/backend/src/services/organizations.services.ts b/src/backend/src/services/organizations.services.ts index acd6f24865..53c118086c 100644 --- a/src/backend/src/services/organizations.services.ts +++ b/src/backend/src/services/organizations.services.ts @@ -209,6 +209,10 @@ export default class OrganizationsService { const logoImageData = await uploadFile(logoImage); + if (!logoImageData?.name) { + throw new HttpException(500, 'Image Name not found'); + } + const updatedOrg = await prisma.organization.update({ where: { organizationId: organization.organizationId }, data: { @@ -224,7 +228,7 @@ export default class OrganizationsService { * @param organizationId the id of the organization * @returns the id of the image */ - static async getLogoImage(organizationId: string): Promise { + static async getLogoImage(organizationId: string): Promise { const organization = await prisma.organization.findUnique({ where: { organizationId } }); @@ -233,10 +237,6 @@ export default class OrganizationsService { throw new NotFoundException('Organization', organizationId); } - if (!organization.logoImageId) { - throw new HttpException(404, `Organization ${organizationId} does not have a logo image`); - } - return organization.logoImageId; } @@ -285,28 +285,21 @@ export default class OrganizationsService { } /** - * Sets the slack workspace id used to initialize slack bots for this organization - * @param slackWorkspaceId the id of the organization's slack workspace - * @param submitter the user making this submission (must be an admin) - * @param organization the organization being changed - * @returns the changed organization + * sets the slack workspace id of the organization + * @param workspaceId workspace id to set + * @param submitter user who submitted the workspace id + * @param organizationId id of organization to update with workspace id + * @returns updated organization */ - static async setOrganizationSlackWorkspaceId( - slackWorkspaceId: string, - submitter: User, - organization: Organization - ): Promise { - if (!(await userHasPermission(submitter.userId, organization.organizationId, isAdmin))) { - throw new AccessDeniedAdminOnlyException('set slack workspace id'); + static async setSlackWorkspaceId(workspaceId: string, submitter: User, organizationId: string) { + if (!(await userHasPermission(submitter.userId, organizationId, isAdmin))) { + throw new AccessDeniedAdminOnlyException('set workspace id'); } - const updatedOrg = prisma.organization.update({ - where: { - organizationId: organization.organizationId - }, - data: { - slackWorkspaceId - } + const updatedOrg = await prisma.organization.update({ + where: { organizationId }, + data: { slackWorkspaceId: workspaceId } }); + return updatedOrg; } } diff --git a/src/backend/src/services/users.services.ts b/src/backend/src/services/users.services.ts index b5b64e5f7f..d786c04137 100644 --- a/src/backend/src/services/users.services.ts +++ b/src/backend/src/services/users.services.ts @@ -38,10 +38,6 @@ 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/notifications.transformer'; -import { getAnnouncementQueryArgs } from '../prisma-query-args/announcements.query.args'; -import announcementTransformer from '../transformers/announcements.transformer'; export default class UsersService { /** @@ -570,70 +566,4 @@ export default class UsersService { const resolvedTasks = await Promise.all(tasksPromises); 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 unreadNotifications = await prisma.notification.findMany({ - where: { - users: { - some: { userId } - } - }, - ...getNotificationQueryArgs(organization.organizationId) - }); - - if (!unreadNotifications) throw new HttpException(404, 'User Unread Notifications Not Found'); - - return unreadNotifications.map(notificationTransformer); - } - - /** - * Gets all of a user's unread announcements - * @param userId id of user to get unread announcements from - * @param organization the user's orgainzation - * @returns the unread announcements of the user - */ - static async getUserUnreadAnnouncements(userId: string, organization: Organization) { - const requestedUser = await prisma.user.findUnique({ - where: { userId }, - include: { unreadAnnouncements: getAnnouncementQueryArgs(organization.organizationId) } - }); - if (!requestedUser) throw new NotFoundException('User', userId); - - return requestedUser.unreadAnnouncements.map(announcementTransformer); - } - - /** - * 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 } - }); - - if (!requestedUser) throw new NotFoundException('User', userId); - - const updatedUser = await prisma.user.update({ - where: { userId }, - data: { - unreadNotifications: { - disconnect: { - notificationId - } - } - }, - include: { unreadNotifications: getNotificationQueryArgs(organization.organizationId) } - }); - - return updatedUser.unreadNotifications.map(notificationTransformer); - } } diff --git a/src/backend/src/transformers/announcements.transformer.ts b/src/backend/src/transformers/announcements.transformer.ts index 4fee90eac2..8b43031390 100644 --- a/src/backend/src/transformers/announcements.transformer.ts +++ b/src/backend/src/transformers/announcements.transformer.ts @@ -5,13 +5,8 @@ import { userTransformer } from './user.transformer'; const announcementTransformer = (announcement: Prisma.AnnouncementGetPayload): Announcement => { return { - announcementId: announcement.announcementId, - text: announcement.text, + ...announcement, usersReceived: announcement.usersReceived.map(userTransformer), - dateCreated: announcement.dateCreated, - senderName: announcement.senderName, - slackEventId: announcement.slackEventId, - slackChannelName: announcement.slackChannelName, dateDeleted: announcement.dateDeleted ?? undefined }; }; diff --git a/src/backend/src/utils/auth.utils.ts b/src/backend/src/utils/auth.utils.ts index 277010b66d..50b01280cf 100644 --- a/src/backend/src/utils/auth.utils.ts +++ b/src/backend/src/utils/auth.utils.ts @@ -68,7 +68,7 @@ export const requireJwtDev = (req: Request, res: Response, next: NextFunction) = ) { next(); } else if ( - req.path.startsWith('/notifications') // task deadline notification endpoint + req.path.startsWith('/notifications/taskdeadlines') // task deadline notification endpoint ) { notificationEndpointAuth(req, res, next); } else { diff --git a/src/backend/src/utils/notifications.utils.ts b/src/backend/src/utils/notifications.utils.ts index c47ba9b7a0..445f36ed67 100644 --- a/src/backend/src/utils/notifications.utils.ts +++ b/src/backend/src/utils/notifications.utils.ts @@ -78,11 +78,7 @@ export const sendHomeCrReviewedNotification = async ( 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}`; + const changeRequestLink = `/change-requests/${changeRequest.crId}`; await NotificationsService.sendNotifcationToUsers( `CR #${changeRequest.identifier} has been ${accepted ? 'approved!' : 'denied.'}`, accepted ? 'check_circle' : 'cancel', diff --git a/src/backend/tests/unit/announcements.test.ts b/src/backend/tests/unit/announcements.test.ts index 7bd6781a1d..162bd9965e 100644 --- a/src/backend/tests/unit/announcements.test.ts +++ b/src/backend/tests/unit/announcements.test.ts @@ -9,7 +9,6 @@ import { wonderwomanSettings } from '../test-data/users.test-data'; import AnnouncementService from '../../src/services/announcement.service'; -import UsersService from '../../src/services/users.services'; import { NotFoundException } from '../../src/utils/errors.utils'; describe('announcement tests', () => { @@ -44,20 +43,20 @@ describe('announcement tests', () => { expect(announcement?.text).toBe('text'); expect(announcement?.usersReceived).toHaveLength(2); expect(announcement?.senderName).toBe('sender name'); - expect(announcement?.dateCreated).toStrictEqual(new Date(1000000000000)); + expect(announcement?.dateMessageSent).toStrictEqual(new Date(1000000000000)); expect(announcement?.slackEventId).toBe('slack id'); expect(announcement?.slackChannelName).toBe('channel name'); expect(announcement?.dateDeleted).toBeUndefined(); - const smAnnouncements = await UsersService.getUserUnreadAnnouncements(superman.userId, organization); - const bmAnnouncements = await UsersService.getUserUnreadAnnouncements(batman.userId, organization); - const wwAnnouncements = await UsersService.getUserUnreadAnnouncements(wonderwoman.userId, organization); + const smAnnouncements = await AnnouncementService.getUserUnreadAnnouncements(superman.userId, orgId); + const bmAnnouncements = await AnnouncementService.getUserUnreadAnnouncements(batman.userId, orgId); + const wwAnnouncements = await AnnouncementService.getUserUnreadAnnouncements(wonderwoman.userId, orgId); expect(smAnnouncements).toHaveLength(1); expect(smAnnouncements[0]?.text).toBe('text'); expect(smAnnouncements[0]?.usersReceived).toHaveLength(2); expect(smAnnouncements[0]?.senderName).toBe('sender name'); - expect(smAnnouncements[0]?.dateCreated).toStrictEqual(new Date(1000000000000)); + expect(smAnnouncements[0]?.dateMessageSent).toStrictEqual(new Date(1000000000000)); expect(smAnnouncements[0]?.slackEventId).toBe('slack id'); expect(smAnnouncements[0]?.slackChannelName).toBe('channel name'); expect(smAnnouncements[0]?.dateDeleted).toBeUndefined(); @@ -77,9 +76,9 @@ describe('announcement tests', () => { orgId ); - let smAnnouncements = await UsersService.getUserUnreadAnnouncements(superman.userId, organization); - let bmAnnouncements = await UsersService.getUserUnreadAnnouncements(batman.userId, organization); - let wwAnnouncements = await UsersService.getUserUnreadAnnouncements(wonderwoman.userId, organization); + let smAnnouncements = await AnnouncementService.getUserUnreadAnnouncements(superman.userId, orgId); + let bmAnnouncements = await AnnouncementService.getUserUnreadAnnouncements(batman.userId, orgId); + let wwAnnouncements = await AnnouncementService.getUserUnreadAnnouncements(wonderwoman.userId, orgId); expect(smAnnouncements).toHaveLength(1); expect(bmAnnouncements).toHaveLength(1); @@ -95,9 +94,9 @@ describe('announcement tests', () => { orgId ); - smAnnouncements = await UsersService.getUserUnreadAnnouncements(superman.userId, organization); - bmAnnouncements = await UsersService.getUserUnreadAnnouncements(batman.userId, organization); - wwAnnouncements = await UsersService.getUserUnreadAnnouncements(wonderwoman.userId, organization); + smAnnouncements = await AnnouncementService.getUserUnreadAnnouncements(superman.userId, orgId); + bmAnnouncements = await AnnouncementService.getUserUnreadAnnouncements(batman.userId, orgId); + wwAnnouncements = await AnnouncementService.getUserUnreadAnnouncements(wonderwoman.userId, orgId); expect(smAnnouncements).toHaveLength(0); expect(bmAnnouncements).toHaveLength(1); @@ -133,16 +132,16 @@ describe('announcement tests', () => { orgId ); - let smAnnouncements = await UsersService.getUserUnreadAnnouncements(superman.userId, organization); - let bmAnnouncements = await UsersService.getUserUnreadAnnouncements(batman.userId, organization); + let smAnnouncements = await AnnouncementService.getUserUnreadAnnouncements(superman.userId, orgId); + let bmAnnouncements = await AnnouncementService.getUserUnreadAnnouncements(batman.userId, orgId); expect(smAnnouncements).toHaveLength(1); expect(bmAnnouncements).toHaveLength(1); const deletedAnnouncement = await AnnouncementService.deleteAnnouncement('slack id', orgId); - smAnnouncements = await UsersService.getUserUnreadAnnouncements(superman.userId, organization); - bmAnnouncements = await UsersService.getUserUnreadAnnouncements(batman.userId, organization); + smAnnouncements = await AnnouncementService.getUserUnreadAnnouncements(superman.userId, orgId); + bmAnnouncements = await AnnouncementService.getUserUnreadAnnouncements(batman.userId, orgId); expect(smAnnouncements).toHaveLength(0); expect(bmAnnouncements).toHaveLength(0); @@ -155,4 +154,37 @@ describe('announcement tests', () => { new NotFoundException('Announcement', 'non-existent id') ); }); + + describe('Get Announcements', () => { + it('Succeeds and gets user announcements', async () => { + const testBatman = await createTestUser(batmanAppAdmin, orgId); + await AnnouncementService.createAnnouncement( + 'test1', + [testBatman.userId], + new Date(), + 'Thomas Emrax', + '1', + 'software', + organization.organizationId + ); + await AnnouncementService.createAnnouncement( + 'test2', + [testBatman.userId], + new Date(), + 'Superman', + '50', + 'mechanical', + organization.organizationId + ); + + const announcements = await AnnouncementService.getUserUnreadAnnouncements( + testBatman.userId, + organization.organizationId + ); + + expect(announcements).toHaveLength(2); + expect(announcements[0].text).toBe('test1'); + expect(announcements[1].text).toBe('test2'); + }); + }); }); diff --git a/src/backend/tests/unit/notifications.test.ts b/src/backend/tests/unit/notifications.test.ts index d3cce68361..026099c1c3 100644 --- a/src/backend/tests/unit/notifications.test.ts +++ b/src/backend/tests/unit/notifications.test.ts @@ -56,4 +56,47 @@ describe('Notifications Tests', () => { expect(supermanWithNotifications?.unreadNotifications[0].text).toBe('test notification'); }); }); + + describe('Get Notifications', () => { + it('Succeeds and gets user notifications', async () => { + const testBatman = await createTestUser(batmanAppAdmin, orgId); + await NotificationService.sendNotifcationToUsers('test1', 'test1', [testBatman.userId], orgId); + await NotificationService.sendNotifcationToUsers('test2', 'test2', [testBatman.userId], orgId); + + const notifications = await NotificationService.getUserUnreadNotifications( + testBatman.userId, + organization.organizationId + ); + + expect(notifications).toHaveLength(2); + expect(notifications[0].text).toBe('test1'); + expect(notifications[1].text).toBe('test2'); + }); + }); + + describe('Remove Notifications', () => { + it('Succeeds and removes user notification', async () => { + const testBatman = await createTestUser(batmanAppAdmin, orgId); + await NotificationService.sendNotifcationToUsers('test1', 'test1', [testBatman.userId], orgId); + await NotificationService.sendNotifcationToUsers('test2', 'test2', [testBatman.userId], orgId); + + const notifications = await NotificationService.getUserUnreadNotifications( + testBatman.userId, + organization.organizationId + ); + + expect(notifications).toHaveLength(2); + expect(notifications[0].text).toBe('test1'); + expect(notifications[1].text).toBe('test2'); + + const updatedNotifications = await NotificationService.removeUserNotification( + testBatman.userId, + notifications[0].notificationId, + organization.organizationId + ); + + expect(updatedNotifications).toHaveLength(1); + expect(updatedNotifications[0].text).toBe('test2'); + }); + }); }); diff --git a/src/backend/tests/unit/organization.test.ts b/src/backend/tests/unit/organization.test.ts index 295b96031d..798c5ed593 100644 --- a/src/backend/tests/unit/organization.test.ts +++ b/src/backend/tests/unit/organization.test.ts @@ -55,7 +55,7 @@ describe('Organization Tests', () => { it('Succeeds and updates all the images', async () => { const testBatman = await createTestUser(batmanAppAdmin, orgId); (uploadFile as Mock).mockImplementation((file) => { - return Promise.resolve({ id: `uploaded-${file.originalname}` }); + return Promise.resolve({ name: `${file.originalname}`, id: `uploaded-${file.originalname}` }); }); await OrganizationsService.setImages(file1, file2, testBatman, organization); @@ -240,7 +240,7 @@ describe('Organization Tests', () => { it('Succeeds and updates the logo', async () => { const testBatman = await createTestUser(batmanAppAdmin, orgId); (uploadFile as Mock).mockImplementation((file) => { - return Promise.resolve({ id: `uploaded-${file.originalname}` }); + return Promise.resolve({ name: `${file.originalname}`, id: `uploaded-${file.originalname}` }); }); await OrganizationsService.setLogoImage(file1, testBatman, organization); @@ -273,12 +273,6 @@ describe('Organization Tests', () => { ); }); - it('Fails if the organization does not have a logo image', async () => { - await expect(async () => await OrganizationsService.getLogoImage(orgId)).rejects.toThrow( - new HttpException(404, `Organization ${orgId} does not have a logo image`) - ); - }); - it('Succeeds and gets the image', async () => { const testBatman = await createTestUser(batmanAppAdmin, orgId); await OrganizationsService.setLogoImage( @@ -326,35 +320,14 @@ describe('Organization Tests', () => { }); }); - describe('Set Organization slack id', () => { - it('Fails if user is not an admin', async () => { - await expect( - OrganizationsService.setOrganizationSlackWorkspaceId( - 'test slack id', - await createTestUser(wonderwomanGuest, orgId), - organization - ) - ).rejects.toThrow(new AccessDeniedAdminOnlyException('set slack workspace id')); - }); - - it('Succeeds and updates the slack id', async () => { + describe('Set Organization Workspace Id', () => { + it('Succeeds and updates the workspace id', async () => { const testBatman = await createTestUser(batmanAppAdmin, orgId); - const returnedOrganization = await OrganizationsService.setOrganizationSlackWorkspaceId( - 'sample slack id', - testBatman, - organization - ); - - const oldOrganization = await prisma.organization.findUnique({ - where: { - organizationId: orgId - } - }); + const updatedOrganization = await OrganizationsService.setSlackWorkspaceId('1234', testBatman, orgId); - expect(oldOrganization).not.toBeNull(); - expect(oldOrganization?.slackWorkspaceId).toBe('sample slack id'); - expect(oldOrganization?.slackWorkspaceId).toBe(returnedOrganization.slackWorkspaceId); + expect(updatedOrganization).not.toBeNull(); + expect(updatedOrganization.slackWorkspaceId).toBe('1234'); }); }); }); diff --git a/src/backend/tests/unit/users.test.ts b/src/backend/tests/unit/users.test.ts index bd0b934270..c13a0c857f 100644 --- a/src/backend/tests/unit/users.test.ts +++ b/src/backend/tests/unit/users.test.ts @@ -3,8 +3,6 @@ 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 NotificationsService from '../../src/services/notifications.services'; -import AnnouncementService from '../../src/services/announcement.service'; describe('User Tests', () => { let orgId: string; @@ -50,71 +48,4 @@ describe('User Tests', () => { expect(userTasks).toStrictEqual([batmanTask, batmanTask]); }); }); - - describe('Get Notifications', () => { - 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'); - }); - }); - - describe('Remove Notifications', () => { - it('Succeeds and removes user notification', 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'); - }); - }); - - describe('Get Announcements', () => { - it('Succeeds and gets user announcements', async () => { - const testBatman = await createTestUser(batmanAppAdmin, orgId); - await AnnouncementService.createAnnouncement( - 'test1', - [testBatman.userId], - new Date(), - 'Thomas Emrax', - '1', - 'software', - organization.organizationId - ); - await AnnouncementService.createAnnouncement( - 'test2', - [testBatman.userId], - new Date(), - 'Superman', - '50', - 'mechanical', - organization.organizationId - ); - - const announcements = await UsersService.getUserUnreadAnnouncements(testBatman.userId, organization); - - expect(announcements).toHaveLength(2); - expect(announcements[0].text).toBe('test1'); - expect(announcements[1].text).toBe('test2'); - }); - }); }); diff --git a/src/frontend/src/apis/notifications.api.ts b/src/frontend/src/apis/notifications.api.ts new file mode 100644 index 0000000000..454e9d4170 --- /dev/null +++ b/src/frontend/src/apis/notifications.api.ts @@ -0,0 +1,19 @@ +import axios from '../utils/axios'; +import { apiUrls } from '../utils/urls'; +import { Notification } from 'shared'; + +/* + * Gets all unread notifications of the user with the given id + */ +export const getNotifications = () => { + return axios.get(apiUrls.notificationsCurrentUser(), { + transformResponse: (data) => JSON.parse(data) + }); +}; + +/* + * Removes a notification from the user with the given id + */ +export const removeNotification = (notificationId: string) => { + return axios.post(apiUrls.notificationsRemove(notificationId)); +}; diff --git a/src/frontend/src/apis/onboarding.api.ts b/src/frontend/src/apis/onboarding.api.ts new file mode 100644 index 0000000000..7912e80516 --- /dev/null +++ b/src/frontend/src/apis/onboarding.api.ts @@ -0,0 +1,16 @@ +import axios from 'axios'; +import { apiUrls } from '../utils/urls'; + +/** + * API Call to download a google image + * @param fileId file id to be downloaded + * @returns an image blob + */ +export const downloadGoogleImage = async (fileId: string): Promise => { + const response = await axios.get(apiUrls.imageById(fileId), { + responseType: 'arraybuffer' // Set the response type to 'arraybuffer' to receive the image as a Buffer + }); + const imageBuffer = new Uint8Array(response.data); + const imageBlob = new Blob([imageBuffer], { type: response.headers['content-type'] }); + return imageBlob; +}; diff --git a/src/frontend/src/apis/organizations.api.ts b/src/frontend/src/apis/organizations.api.ts index e5903575c9..6821c74543 100644 --- a/src/frontend/src/apis/organizations.api.ts +++ b/src/frontend/src/apis/organizations.api.ts @@ -24,8 +24,41 @@ export const setOrganizationDescription = async (description: string) => { }); }; +export const getOrganizationLogo = async () => { + return axios.get(apiUrls.organizationsLogoImage(), { + transformResponse: (data) => JSON.parse(data) + }); +}; + +export const setOrganizationLogo = async (file: File) => { + const formData = new FormData(); + formData.append('logo', file); + return axios.post(apiUrls.organizationsSetLogoImage(), formData); +}; + export const setOrganizationFeaturedProjects = async (featuredProjectIds: string[]) => { return axios.post(apiUrls.organizationsSetFeaturedProjects(), { projectIds: featuredProjectIds }); }; + +export const setOrganizationWorkspaceId = async (workspaceId: string) => { + return axios.post(apiUrls.organizationsSetWorkspaceId(), { + workspaceId + }); +}; + +/** + * Downloads a given fileId from google drive into a blob + * + * @param fileId the google id of the file to download + * @returns the downloaded file as a Blob + */ +export const downloadGoogleImage = async (fileId: string): Promise => { + const response = await axios.get(apiUrls.imageById(fileId), { + responseType: 'arraybuffer' // Set the response type to 'arraybuffer' to receive the image as a Buffer + }); + const imageBuffer = new Uint8Array(response.data); + const imageBlob = new Blob([imageBuffer], { type: response.headers['content-type'] }); + return imageBlob; +}; diff --git a/src/frontend/src/apis/users.api.ts b/src/frontend/src/apis/users.api.ts index 5a91bff5fd..afa5ea00f6 100644 --- a/src/frontend/src/apis/users.api.ts +++ b/src/frontend/src/apis/users.api.ts @@ -5,7 +5,6 @@ import axios from '../utils/axios'; import { - Notification, Project, SetUserScheduleSettingsPayload, Task, @@ -160,19 +159,3 @@ export const getManyUserTasks = (userIds: string[]) => { } ); }; - -/* - * Gets all unread notifications of the user with the given id - */ -export const getNotifications = (id: string) => { - return axios.get(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(apiUrls.userRemoveNotifications(userId), { notificationId }); -}; diff --git a/src/frontend/src/components/NotificationAlert.tsx b/src/frontend/src/components/NotificationAlert.tsx index 581d849ef0..53b01a5aaa 100644 --- a/src/frontend/src/components/NotificationAlert.tsx +++ b/src/frontend/src/components/NotificationAlert.tsx @@ -1,17 +1,13 @@ import { Box } from '@mui/material'; import React, { useEffect, useState } from 'react'; -import { Notification, User } from 'shared'; +import { Notification } from 'shared'; import NotificationCard from './NotificationCard'; -import { useRemoveUserNotification, useUserNotifications } from '../hooks/users.hooks'; import { useHistory } from 'react-router-dom'; +import { useCurrentUserNotifications, useRemoveUserNotification } from '../hooks/notifications.hooks'; -interface NotificationAlertProps { - user: User; -} - -const NotificationAlert: React.FC = ({ user }) => { - const { data: notifications, isLoading: notificationsIsLoading } = useUserNotifications(user.userId); - const { mutateAsync: removeNotification, isLoading: removeIsLoading } = useRemoveUserNotification(user.userId); +const NotificationAlert: React.FC = () => { + const { data: notifications, isLoading: notificationsIsLoading } = useCurrentUserNotifications(); + const { mutateAsync: removeNotification, isLoading: removeIsLoading } = useRemoveUserNotification(); const [currentNotification, setCurrentNotification] = useState(); const history = useHistory(); diff --git a/src/frontend/src/components/NotificationCard.tsx b/src/frontend/src/components/NotificationCard.tsx index 1e4cfb4c02..38dd994029 100644 --- a/src/frontend/src/components/NotificationCard.tsx +++ b/src/frontend/src/components/NotificationCard.tsx @@ -37,6 +37,7 @@ const NotificationCard: React.FC = ({ notification, remov onClick={async () => await onClick(notification)} sx={{ display: 'flex', + alignItems: 'center', gap: 1, cursor: !!notification.eventLink ? 'pointer' : 'default' }} diff --git a/src/frontend/src/hooks/notifications.hooks.ts b/src/frontend/src/hooks/notifications.hooks.ts new file mode 100644 index 0000000000..5ca9ae621b --- /dev/null +++ b/src/frontend/src/hooks/notifications.hooks.ts @@ -0,0 +1,36 @@ +import { useMutation, useQuery, useQueryClient } from 'react-query'; +import { Notification } from 'shared'; +import { getNotifications, removeNotification } from '../apis/notifications.api'; + +/** + * Curstom react hook to get all unread notifications from a user + * @param userId id of user to get unread notifications from + * @returns + */ +export const useCurrentUserNotifications = () => { + return useQuery(['notifications', 'current-user'], async () => { + const { data } = await getNotifications(); + 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 = () => { + const queryClient = useQueryClient(); + return useMutation( + ['notifications', 'current-user', 'remove'], + async (notification: Notification) => { + const { data } = await removeNotification(notification.notificationId); + return data; + }, + { + onSuccess: () => { + queryClient.invalidateQueries(['notifications', 'current-user']); + } + } + ); +}; diff --git a/src/frontend/src/hooks/organizations.hooks.ts b/src/frontend/src/hooks/organizations.hooks.ts index 24da9a5653..c317f040b8 100644 --- a/src/frontend/src/hooks/organizations.hooks.ts +++ b/src/frontend/src/hooks/organizations.hooks.ts @@ -6,8 +6,12 @@ import { getFeaturedProjects, getCurrentOrganization, setOrganizationDescription, - setOrganizationFeaturedProjects + setOrganizationFeaturedProjects, + setOrganizationWorkspaceId, + setOrganizationLogo, + getOrganizationLogo } from '../apis/organizations.api'; +import { downloadGoogleImage } from '../apis/organizations.api'; interface OrganizationProvider { organizationId: string; @@ -84,3 +88,38 @@ export const useSetFeaturedProjects = () => { } ); }; + +export const useSetWorkspaceId = () => { + const queryClient = useQueryClient(); + return useMutation( + ['organizations', 'featured-projects'], + async (workspaceId: string) => { + const { data } = await setOrganizationWorkspaceId(workspaceId); + return data; + }, + { + onSuccess: () => { + queryClient.invalidateQueries(['organizations']); + } + } + ); +}; + +export const useSetOrganizationLogo = () => { + const queryClient = useQueryClient(); + return useMutation(['reimbursement-requsts', 'edit'], async (file: File) => { + const { data } = await setOrganizationLogo(file); + queryClient.invalidateQueries(['organizations']); + return data; + }); +}; + +export const useOrganizationLogo = () => { + return useQuery(['organizations', 'logo'], async () => { + const { data: fileId } = await getOrganizationLogo(); + if (!fileId) { + return; + } + return await downloadGoogleImage(fileId); + }); +}; diff --git a/src/frontend/src/hooks/users.hooks.ts b/src/frontend/src/hooks/users.hooks.ts index 32279217f9..96b659c1f1 100644 --- a/src/frontend/src/hooks/users.hooks.ts +++ b/src/frontend/src/hooks/users.hooks.ts @@ -19,9 +19,7 @@ import { getUserScheduleSettings, updateUserScheduleSettings, getUserTasks, - getManyUserTasks, - getNotifications, - removeNotification + getManyUserTasks } from '../apis/users.api'; import { User, @@ -33,8 +31,7 @@ import { UserScheduleSettings, UserWithScheduleSettings, SetUserScheduleSettingsPayload, - Task, - Notification + Task } from 'shared'; import { useAuth } from './auth.hooks'; import { useContext } from 'react'; @@ -263,36 +260,3 @@ 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; - }); -}; - -/** - * 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/pages/AdminToolsPage/AdminToolsPage.tsx b/src/frontend/src/pages/AdminToolsPage/AdminToolsPage.tsx index b96aa4c93b..f99d21f08e 100644 --- a/src/frontend/src/pages/AdminToolsPage/AdminToolsPage.tsx +++ b/src/frontend/src/pages/AdminToolsPage/AdminToolsPage.tsx @@ -19,6 +19,7 @@ import { routes } from '../../utils/routes'; import { Box } from '@mui/system'; import AdminToolsRecruitmentConfig from './RecruitmentConfig/AdminToolsRecruitmentConfig'; import GuestViewConfig from './EditGuestView/GuestViewConfig'; +import AdminToolsWorkspaceId from './AdminToolsSlackWorkspaceId'; const AdminToolsPage: React.FC = () => { const currentUser = useCurrentUser(); @@ -100,6 +101,7 @@ const AdminToolsPage: React.FC = () => { + )} diff --git a/src/frontend/src/pages/AdminToolsPage/AdminToolsSlackWorkspaceId.tsx b/src/frontend/src/pages/AdminToolsPage/AdminToolsSlackWorkspaceId.tsx new file mode 100644 index 0000000000..57404352d9 --- /dev/null +++ b/src/frontend/src/pages/AdminToolsPage/AdminToolsSlackWorkspaceId.tsx @@ -0,0 +1,72 @@ +/* + * 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 { NERButton } from '../../components/NERButton'; +import { Box, Link, TextField, Typography } from '@mui/material'; +import { useState } from 'react'; +import { useToast } from '../../hooks/toasts.hooks'; +import { useCurrentOrganization, useSetWorkspaceId } from '../../hooks/organizations.hooks'; +import LoadingIndicator from '../../components/LoadingIndicator'; +import ErrorPage from '../ErrorPage'; +import { Organization } from 'shared'; +import HelpIcon from '@mui/icons-material/Help'; + +interface AdminToolsWorkspaceIdViewProps { + organization: Organization; +} + +const AdminToolsWorkspaceId: React.FC = () => { + const { data: organization, isLoading, isError, error } = useCurrentOrganization(); + if (!organization || isLoading) return ; + if (isError) return ; + + return ; +}; + +const AdminToolsWorkspaceIdView: React.FC = ({ organization }) => { + const toast = useToast(); + const { mutateAsync, isLoading } = useSetWorkspaceId(); + const [workspaceId, setWorkspaceId] = useState(organization.slackWorkspaceId ?? ''); + + if (isLoading) return ; + + const slackWorkspaceId = async () => { + try { + await mutateAsync(workspaceId); + toast.success('Successfully updated the slack workspace id'); + } catch (error: unknown) { + if (error instanceof Error) { + toast.error(error.message); + } + } + }; + + return ( + + + {organization.name} Slack Workspace Id + + + + + + setWorkspaceId(e.target.value)} /> + + + Update + + + + + ); +}; + +export default AdminToolsWorkspaceId; diff --git a/src/frontend/src/pages/AdminToolsPage/EditGuestView/EditDescription.tsx b/src/frontend/src/pages/AdminToolsPage/EditGuestView/EditDescription.tsx index 06e44ac673..e00599f669 100644 --- a/src/frontend/src/pages/AdminToolsPage/EditGuestView/EditDescription.tsx +++ b/src/frontend/src/pages/AdminToolsPage/EditGuestView/EditDescription.tsx @@ -36,7 +36,7 @@ const EditDescription: React.FC = () => { return ( { return ( { + const { + data: organization, + isLoading: organizationIsLoading, + isError: organizationIsError, + error: organizationError + } = useCurrentOrganization(); + const { data: imageData, isLoading: imageDataIsLoading, isError: imageIsError, error: imageError } = useOrganizationLogo(); + const { mutateAsync, isLoading } = useSetOrganizationLogo(); + const toast = useToast(); + const [isEditMode, setIsEditMode] = useState(false); + const theme = useTheme(); + + if (isLoading || !mutateAsync || organizationIsLoading || !organization || imageDataIsLoading) return ; + if (organizationIsError) return ; + if (imageIsError) return ; + + const handleClose = () => { + setIsEditMode(false); + }; + + const onSubmit = async (logoInput: EditLogoInput) => { + try { + if (!logoInput.logoImage) { + toast.error('No logo image submitted.'); + handleClose(); + return; + } + await mutateAsync(logoInput.logoImage); + toast.success('Logo updated successfully!'); + } catch (e) { + if (e instanceof Error) { + toast.error(e.message); + } + } + handleClose(); + }; + + return ( + + + {organization.name} Logo + + {isEditMode ? ( + + ) : ( + <> + + + + setIsEditMode(true)}> + Update + + + + + )} + + ); +}; + +export default EditLogo; diff --git a/src/frontend/src/pages/AdminToolsPage/EditGuestView/EditLogoForm.tsx b/src/frontend/src/pages/AdminToolsPage/EditGuestView/EditLogoForm.tsx new file mode 100644 index 0000000000..72be9d618e --- /dev/null +++ b/src/frontend/src/pages/AdminToolsPage/EditGuestView/EditLogoForm.tsx @@ -0,0 +1,111 @@ +import React from 'react'; +import { Box, Button, FormControl, Stack, Typography } from '@mui/material'; +import FileUploadIcon from '@mui/icons-material/FileUpload'; +import { Controller, useForm } from 'react-hook-form'; +import NERFailButton from '../../../components/NERFailButton'; +import NERSuccessButton from '../../../components/NERSuccessButton'; +import ImageIcon from '@mui/icons-material/Image'; + +export interface EditLogoInput { + logoImage?: File; +} + +interface EditLogoFormProps { + onSubmit: (logoImage: EditLogoInput) => Promise; + onHide: () => void; + orgLogo?: File; +} + +const EditLogoForm: React.FC = ({ onSubmit, orgLogo, onHide }) => { + const { handleSubmit, control, reset } = useForm({ + defaultValues: { + logoImage: orgLogo + } + }); + + const onHideWrapper = () => { + onHide(); + reset(); + }; + + return ( + + ); +}; + +export default EditLogoForm; diff --git a/src/frontend/src/pages/AdminToolsPage/EditGuestView/GuestViewConfig.tsx b/src/frontend/src/pages/AdminToolsPage/EditGuestView/GuestViewConfig.tsx index 7e47085d0f..419bb2548c 100644 --- a/src/frontend/src/pages/AdminToolsPage/EditGuestView/GuestViewConfig.tsx +++ b/src/frontend/src/pages/AdminToolsPage/EditGuestView/GuestViewConfig.tsx @@ -1,13 +1,21 @@ -import { Stack } from '@mui/material'; +import { Stack, Grid } from '@mui/material'; import EditDescription from './EditDescription'; import EditFeaturedProjects from './EditFeaturedProjects'; +import EditLogo from './EditLogo'; const GuestViewConfig: React.FC = () => { return ( - - - - + + + + + + + + + + + ); }; diff --git a/src/frontend/src/pages/HomePage/GuestHomePage.tsx b/src/frontend/src/pages/HomePage/GuestHomePage.tsx index 57fd84f323..297559c47a 100644 --- a/src/frontend/src/pages/HomePage/GuestHomePage.tsx +++ b/src/frontend/src/pages/HomePage/GuestHomePage.tsx @@ -3,7 +3,7 @@ * See the LICENSE file in the repository root folder for details. */ -import { Box, Typography } from '@mui/material'; +import { Box, Grid, Stack, Typography } from '@mui/material'; import { useSingleUserSettings } from '../../hooks/users.hooks'; import LoadingIndicator from '../../components/LoadingIndicator'; import ErrorPage from '../ErrorPage'; @@ -12,6 +12,7 @@ import { AuthenticatedUser } from 'shared'; import MemberEncouragement from './components/MemberEncouragement'; import GuestOrganizationInfo from './components/GuestOrganizationInfo'; import FeaturedProjects from './components/FeaturedProjects'; +import OrganizationLogo from './components/OrganizationLogo'; interface GuestHomePageProps { user: AuthenticatedUser; @@ -32,18 +33,27 @@ const GuestHomePage = ({ user }: GuestHomePageProps) => { sx={{ display: 'flex', flexDirection: 'column', - gap: 2, + gap: 1.5, height: `${PAGE_GRID_HEIGHT}vh`, mt: 2 }} > - - - - - - - + + + + + + + + + + + + + + + + diff --git a/src/frontend/src/pages/HomePage/Home.tsx b/src/frontend/src/pages/HomePage/Home.tsx index 76db11f05a..f76ecc0e4c 100644 --- a/src/frontend/src/pages/HomePage/Home.tsx +++ b/src/frontend/src/pages/HomePage/Home.tsx @@ -18,7 +18,7 @@ const Home = () => { const [onMemberHomePage, setOnMemberHomePage] = useState(false); return ( <> - {!onMemberHomePage && } + {!onMemberHomePage && } {isGuest(user.role) && !onMemberHomePage ? ( ) : isMember(user.role) ? ( diff --git a/src/frontend/src/pages/HomePage/LeadHomePage.tsx b/src/frontend/src/pages/HomePage/LeadHomePage.tsx index e927f9adf9..5466cdc285 100644 --- a/src/frontend/src/pages/HomePage/LeadHomePage.tsx +++ b/src/frontend/src/pages/HomePage/LeadHomePage.tsx @@ -11,6 +11,7 @@ import PageLayout, { PAGE_GRID_HEIGHT } from '../../components/PageLayout'; import { AuthenticatedUser } from 'shared'; import ChangeRequestsToReview from './components/ChangeRequestsToReview'; import MyTeamsOverdueTasks from './components/MyTeamsOverdueTasks'; +import UpcomingDesignReviews from './components/UpcomingDesignReviews'; interface LeadHomePageProps { user: AuthenticatedUser; @@ -39,10 +40,13 @@ const LeadHomePage = ({ user }: LeadHomePageProps) => { - + + + + 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 new file mode 100644 index 0000000000..7ecf0dac6a --- /dev/null +++ b/src/frontend/src/pages/HomePage/components/DesignReviewCard.tsx @@ -0,0 +1,117 @@ +import { Box, Card, CardContent, Link, Stack, Typography, useTheme } from '@mui/material'; +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 CalendarMonthIcon from '@mui/icons-material/CalendarMonth'; +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; + 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(); + const confirmedMemberIds = designReview.confirmedMembers.map((user) => user.userId); + + return ( + <> + {!confirmedMemberIds.includes(user.userId) ? ( + { + history.push(`${routes.SETTINGS_PREFERENCES}?drId=${designReview.designReviewId}`); + }} + component={RouterLink} + > + Confirm Availibility + + ) : ( + {designReview.status} + )} + + ); +}; + +const getWeekday = (date: Date): string => { + const weekdays: string[] = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; + return weekdays[date.getDay()]; +}; + +const removeYear = (str: string): string => { + return str.substring(0, str.length - 5); +}; + +const UpcomingDesignReviewsCard: React.FC = ({ designReview, user }) => { + const theme = useTheme(); + const timezoneAdjustedDate = timezoneOffset(designReview.dateScheduled); + return ( + + + + + + + {designReview.wbsName} + + + + {} + + {getWeekday(timezoneAdjustedDate) + + ', ' + + removeYear(datePipe(timezoneAdjustedDate)) + + ' @ ' + + meetingStartTimePipe(designReview.meetingTimes)} + + + {designReview.isInPerson && !!designReview.location && ( + } text={designReview.location} /> + )} + {designReview.isOnline && !!designReview.zoomLink && ( + } text={designReview.zoomLink} link /> + )} + + + + + + ); +}; + +export default UpcomingDesignReviewsCard; diff --git a/src/frontend/src/pages/HomePage/components/LogoDisplay.tsx b/src/frontend/src/pages/HomePage/components/LogoDisplay.tsx new file mode 100644 index 0000000000..0ebeb68db5 --- /dev/null +++ b/src/frontend/src/pages/HomePage/components/LogoDisplay.tsx @@ -0,0 +1,35 @@ +import { Box, useTheme, Card } from '@mui/material'; +import React from 'react'; + +interface LogoDisplayProps { + imageUrl?: string; +} + +const LogoDisplay: React.FC = ({ imageUrl }) => { + const theme = useTheme(); + return ( + + {imageUrl && ( + + )} + + ); +}; + +export default LogoDisplay; diff --git a/src/frontend/src/pages/HomePage/components/OrganizationLogo.tsx b/src/frontend/src/pages/HomePage/components/OrganizationLogo.tsx new file mode 100644 index 0000000000..c16a273c31 --- /dev/null +++ b/src/frontend/src/pages/HomePage/components/OrganizationLogo.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import LogoDisplay from './LogoDisplay'; +import { useOrganizationLogo } from '../../../hooks/organizations.hooks'; +import LoadingIndicator from '../../../components/LoadingIndicator'; +import ErrorPage from '../../ErrorPage'; + +const OrganizationLogo = () => { + const { data: imageData, isLoading, isError, error } = useOrganizationLogo(); + if (isLoading) return ; + if (isError) return ; + + return ; +}; + +export default OrganizationLogo; diff --git a/src/frontend/src/pages/HomePage/components/OverdueWorkPackageView.tsx b/src/frontend/src/pages/HomePage/components/OverdueWorkPackageView.tsx index 4686213017..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'; @@ -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/TaskDetailCard.tsx b/src/frontend/src/pages/HomePage/components/TaskDetailCard.tsx index 55d0f4e04c..1e4045ea07 100644 --- a/src/frontend/src/pages/HomePage/components/TaskDetailCard.tsx +++ b/src/frontend/src/pages/HomePage/components/TaskDetailCard.tsx @@ -58,8 +58,8 @@ const TaskDetailCard: React.FC = ({ task, taskNumber }) => onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)} sx={{ - minWidth: 'fit-content', - minHeight: 'fit-content', + width: '100%', + height: 'fit-content', mr: 3, background: theme.palette.background.default, border: taskOverdue && hover ? '1px solid red' : undefined 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 }) => { { + return ( + } + heading={'No Upcoming Design Reviews'} + message={'There are no Upcoming Design Reviews to Display'} + /> + ); +}; + +const UpcomingDesignReviews: React.FC = ({ user }) => { + 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); + 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 && + review.status !== DesignReviewStatus.DONE && + memberUserIds.includes(user.userId) + ); + }); + + const fullDisplay = ( + + {filteredDesignReviews.length === 0 ? ( + + ) : ( + filteredDesignReviews.map((d) => ) + )} + + ); + + return fullDisplay; +}; + +export default UpcomingDesignReviews; diff --git a/src/frontend/src/pages/HomePage/components/WorkPackageCard.tsx b/src/frontend/src/pages/HomePage/components/WorkPackageCard.tsx index 5419731678..11183972c1 100644 --- a/src/frontend/src/pages/HomePage/components/WorkPackageCard.tsx +++ b/src/frontend/src/pages/HomePage/components/WorkPackageCard.tsx @@ -40,7 +40,7 @@ const WorkPackageCard = ({ wp }: { wp: WorkPackage }) => { { 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); +}; diff --git a/src/frontend/src/utils/urls.ts b/src/frontend/src/utils/urls.ts index d50675ed69..a924917909 100644 --- a/src/frontend/src/utils/urls.ts +++ b/src/frontend/src/utils/urls.ts @@ -26,8 +26,6 @@ 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 userNotifications = (id: string) => `${usersById(id)}/notifications`; -const userRemoveNotifications = (id: string) => `${usersById(id)}/notifications/remove`; /**************** Projects Endpoints ****************/ const projects = () => `${API_URL}/projects`; @@ -179,7 +177,10 @@ const organizationsUsefulLinks = () => `${organizations()}/useful-links`; const organizationsSetUsefulLinks = () => `${organizationsUsefulLinks()}/set`; const organizationsSetDescription = () => `${organizations()}/description/set`; const organizationsFeaturedProjects = () => `${organizations()}/featured-projects`; +const organizationsLogoImage = () => `${organizations()}/logo`; +const organizationsSetLogoImage = () => `${organizations()}/logo/update`; const organizationsSetFeaturedProjects = () => `${organizationsFeaturedProjects()}/set`; +const organizationsSetWorkspaceId = () => `${organizations()}/workspaceId/set`; /******************* Car Endpoints ********************/ const cars = () => `${API_URL}/cars`; @@ -196,6 +197,15 @@ const faqCreate = () => `${recruitment()}/faq/create`; const faqEdit = (id: string) => `${recruitment()}/faq/${id}/edit`; const faqDelete = (id: string) => `${recruitment()}/faq/${id}/delete`; +/************** Notification Endpoints ***************/ +const notifications = () => `${API_URL}/notifications`; +const notificationsCurrentUser = () => `${notifications()}/current-user`; +const notificationsRemove = (id: string) => `${notifications()}/${id}/remove`; + +/************** Onboarding Endpoints ***************/ +const onboarding = () => `${API_URL}/onboarding`; +const imageById = (imageId: string) => `${onboarding()}/image/${imageId}`; + /**************** Other Endpoints ****************/ const version = () => `https://api.github.com/repos/Northeastern-Electric-Racing/FinishLine/releases/latest`; @@ -214,8 +224,6 @@ export const apiUrls = { userScheduleSettingsSet, userTasks, manyUserTasks, - userNotifications, - userRemoveNotifications, projects, allProjects, @@ -341,7 +349,10 @@ export const apiUrls = { organizationsSetUsefulLinks, organizationsFeaturedProjects, organizationsSetDescription, + organizationsLogoImage, + organizationsSetLogoImage, organizationsSetFeaturedProjects, + organizationsSetWorkspaceId, cars, carsCreate, @@ -354,6 +365,11 @@ export const apiUrls = { faqCreate, faqEdit, faqDelete, + imageById, + + notifications, + notificationsCurrentUser, + notificationsRemove, version }; diff --git a/src/shared/src/types/announcements.types.ts b/src/shared/src/types/announcements.types.ts index c0e2d615a7..ac31f72062 100644 --- a/src/shared/src/types/announcements.types.ts +++ b/src/shared/src/types/announcements.types.ts @@ -5,7 +5,7 @@ export interface Announcement { text: string; usersReceived: User[]; senderName: string; - dateCreated: Date; + dateMessageSent: Date; slackEventId: string; slackChannelName: string; dateDeleted?: Date; diff --git a/src/shared/src/types/user-types.ts b/src/shared/src/types/user-types.ts index 2dc8fbb205..f4432cc60a 100644 --- a/src/shared/src/types/user-types.ts +++ b/src/shared/src/types/user-types.ts @@ -44,6 +44,7 @@ export interface Organization { treasurer?: UserPreview; advisor?: UserPreview; description: string; + slackWorkspaceId?: string; } /**