diff --git a/backend/graphql/resolvers/notificationResolvers.ts b/backend/graphql/resolvers/notificationResolvers.ts index 4c243d67..351b304f 100644 --- a/backend/graphql/resolvers/notificationResolvers.ts +++ b/backend/graphql/resolvers/notificationResolvers.ts @@ -1,7 +1,10 @@ import NotificationService from "../../services/implementations/notificationService"; import INotificationService, { NotificationDTO, + NotificationGroupDTO, NotificationReceivedDTO, + UpdateNotificationDTO, + CreateNotificationDTO, } from "../../services/interfaces/notificationService"; import IResidentService from "../../services/interfaces/residentService"; import ResidentService from "../../services/implementations/residentService"; @@ -13,75 +16,117 @@ const notificationService: INotificationService = new NotificationService( const notificationResolvers = { Query: { - getNotificationsByUserId: async ( + getNotificationsByIds: async ( _parent: undefined, - { userId }: { userId: string }, + { notificationIds }: { notificationIds: string[] }, ): Promise => { - return notificationService.getNotificationsByUserId(Number(userId)); + const notificationReceived = await notificationService.getNotificationsByIds( + notificationIds.map(Number), + ); + return notificationReceived; }, - getNotificationById: async ( + getNotificationByResident: async ( _parent: undefined, - { id }: { id: string }, - ): Promise => { - return notificationService.getNotificationById(Number(id)); + { residentId }: { residentId: string }, + ): Promise => { + const notificationReceived = await notificationService.getNotificationByResident( + Number(residentId), + ); + return notificationReceived; + }, + getAllGroupsAndNotifications: async (): Promise => { + const notificationGroups = await notificationService.getAllGroupsAndNotifications(); + return notificationGroups; }, }, Mutation: { - sendNotification: async ( + createNotificationGroup: async ( _parent: undefined, { - authorId, - title, - message, - recipientIds, + roomIds, }: { - authorId: number; - title: string; - message: string; - recipientIds: number[]; + roomIds: number[]; }, - ): Promise => { - const ids = recipientIds.map((id) => Number(id)); - const newNotification = await notificationService.sendNotification( - Number(authorId), - title, - message, + ): Promise => { + const ids = roomIds.map((id) => Number(id)); + const newNotificationGroup = await notificationService.createNotificationGroup( ids, ); - return newNotification; + return newNotificationGroup; }, - deleteUserNotification: async ( + createAnnouncementGroup: async (): Promise => { + const newNotificationGroup = await notificationService.createAnnouncementGroup(); + return newNotificationGroup; + }, + sendNotificationToGroup: async ( _parent: undefined, - { notificationId }: { notificationId: number }, + { + groupId, + notification, + }: { + groupId: number; + notification: CreateNotificationDTO; + }, ): Promise => { - const deletedNotification = await notificationService.deleteUserNotification( - Number(notificationId), + const newNotification = await notificationService.sendNotificationToGroup( + Number(groupId), + notification, ); - return deletedNotification; + return newNotification; }, - updateSeenNotification: async ( + deleteNotificationGroup: async ( _parent: undefined, - { notificationId }: { notificationId: number }, - ): Promise => { - const updatedNotification = await notificationService.updateSeenNotification( + { + groupId, + }: { + groupId: number; + }, + ): Promise => { + const deletedGroup = await notificationService.deleteNotificationGroup( + Number(groupId), + ); + return deletedGroup; + }, + updateNotificationById: async ( + _parent: undefined, + { + notificationId, + notification, + }: { + notificationId: number; + notification: UpdateNotificationDTO; + }, + ): Promise => { + const updatedNotification = await notificationService.updateNotificationById( Number(notificationId), + notification, ); return updatedNotification; }, - sendAnnouncement: async ( + deleteNotificationByIds: async ( _parent: undefined, { - title, - message, - userId, - }: { title: string; message: string; userId: number }, - ): Promise => { - const newAnnouncement = await notificationService.sendAnnouncement( - title, - message, - Number(userId), + notificationIds, + }: { + notificationIds: number[]; + }, + ): Promise => { + const ids = notificationIds.map((id) => Number(id)); + await notificationService.deleteNotificationByIds(ids); + return true; + }, + updateSeenNotification: async ( + _parent: undefined, + { + notificationSeenId, + }: { + notificationSeenId: number; + }, + ): Promise => { + const updatedNotificationReceived = await notificationService.updateSeenNotification( + Number(notificationSeenId), ); - return newAnnouncement; + return updatedNotificationReceived; }, }, }; diff --git a/backend/graphql/types/notificationType.ts b/backend/graphql/types/notificationType.ts index 212cc70a..b8007d3e 100644 --- a/backend/graphql/types/notificationType.ts +++ b/backend/graphql/types/notificationType.ts @@ -3,39 +3,59 @@ import { gql } from "apollo-server-express"; const notificationType = gql` type NotificationDTO { id: ID! - authorId: ID - title: String! message: String! - createdAt: DateTime! - recipients: [NotificationReceivedDTO!] + createdAt: DateTime + authorId: ID + recipients: [NotificationReceivedDTO] + } + + type NotificationGroupDTO { + id: ID! + recipients: [ResidentDTO!] + notifications: [NotificationDTO!] + announcementGroup: Boolean! } type NotificationReceivedDTO { id: ID! notificationId: ID! + notification: NotificationDTO recipientId: ID! seen: Boolean! } + input UpdateNotificationDTO { + authorId: ID + message: String + createdAt: DateTime + } + + input CreateNotificationDTO { + authorId: ID + message: String! + createdAt: DateTime + } + extend type Query { - getNotificationsByUserId(userId: ID!): [NotificationReceivedDTO!] - getNotificationById(id: ID!): NotificationReceivedDTO! + getNotificationsByIds(notificationIds: [ID!]): [NotificationReceivedDTO!] + getNotificationByResident(residentId: ID!): [NotificationReceivedDTO!] + getAllGroupsAndNotifications: [NotificationGroupDTO!] } extend type Mutation { - sendNotification( - authorId: ID! - title: String! - message: String! - recipientIds: [ID!] + createNotificationGroup(roomIds: [Int!]): NotificationGroupDTO! + createAnnouncementGroup: NotificationGroupDTO! + sendNotificationToGroup( + groupId: ID! + notification: CreateNotificationDTO! ): NotificationDTO! - deleteUserNotification(notificationId: ID!): NotificationDTO! - updateSeenNotification(notificationId: ID!): NotificationReceivedDTO! - sendAnnouncement( - title: String - message: String - userId: ID + deleteNotificationGroup(groupId: ID!): NotificationGroupDTO! + updateNotificationById( + notificationId: ID! + notification: UpdateNotificationDTO! ): NotificationDTO! + deleteNotificationByIds(notificationIds: [ID!]): Boolean! + updateSeenNotification(notificationSeenId: ID!): NotificationReceivedDTO! } `; diff --git a/backend/graphql/types/residentType.ts b/backend/graphql/types/residentType.ts index 83b68f05..bf5fa145 100644 --- a/backend/graphql/types/residentType.ts +++ b/backend/graphql/types/residentType.ts @@ -6,53 +6,37 @@ const residentType = gql` type ResidentDTO { userId: Int! residentId: Int! - email: String! - phoneNumber: String - firstName: String! - lastName: String! displayName: String profilePictureURL: String isActive: Boolean! - birthDate: Date! roomNumber: Int! credits: Float! dateJoined: Date! dateLeft: Date - notes: String + notificationGroup: [NotificationGroupDTO!]! + notificationRecieved: [NotificationReceivedDTO]! } input CreateResidentDTO { - email: String! password: String! - phoneNumber: String - firstName: String! - lastName: String! displayName: String profilePictureURL: String residentId: Int! - birthDate: Date! roomNumber: Int! credits: Float dateJoined: Date dateLeft: Date - notes: String } input UpdateResidentDTO { - email: String password: String - phoneNumber: String - firstName: String - lastName: String displayName: String profilePictureURL: String residentId: Int - birthDate: Date roomNumber: Int credits: Float dateJoined: Date dateLeft: Date - notes: String } enum RedeemCreditResponse { diff --git a/backend/graphql/types/taskType.ts b/backend/graphql/types/taskType.ts index e0b47e63..3433c5d1 100644 --- a/backend/graphql/types/taskType.ts +++ b/backend/graphql/types/taskType.ts @@ -65,11 +65,11 @@ const taskType = gql` } input InputTaskAssignedDTO { - taskId: Int - assigneeId: Int - assignerId: Int - status: Status - startDate: DateTime + taskId: Int! + assigneeId: Int! + assignerId: Int! + status: Status! + startDate: DateTime! comments: String } diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index f1c85c7e..db995cca 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -15,46 +15,45 @@ enum UserType { } model User { - id Int @id @default(autoincrement()) - type UserType - staff Staff? - resident Resident? - authId String @unique @map("auth_id") - email String @unique - phoneNumber String? @unique @map("phone_number") - firstName String @map("first_name") - lastName String @map("last_name") - displayName String? @map("display_name") - profilePictureURL String? @map("profile_picture_url") - isActive Boolean @default(true) @map("is_active") - notificationsSent Notification[] - notificationsReceived NotificationReceived[] + id Int @id @default(autoincrement()) + type UserType + staff Staff? + resident Resident? + authId String @unique @map("auth_id") + displayName String? @map("display_name") + profilePictureURL String? @map("profile_picture_url") + isActive Boolean @default(true) @map("is_active") @@map("users") } model Staff { - user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) - userId Int @id @map("user_id") - isAdmin Boolean @default(false) @map("is_admin") - tasksAssigned TaskAssigned[] - warningsAssigned Warning[] + user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) + userId Int @id @map("user_id") + email String @unique @map("email") + phoneNumber String? @unique @map("phone_number") + firstName String @map("first_name") + lastName String @map("last_name") + isAdmin Boolean @default(false) @map("is_admin") + tasksAssigned TaskAssigned[] + warningsAssigned Warning[] + NotificationsSent Notification[] @@map("staff") } model Resident { - user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) - userId Int @id @map("user_id") - residentId Int @unique @map("resident_id") // Differs from user id, this id is assigned by the staff - birthDate DateTime @map("birth_date") @db.Date - roomNumber Int @map("room_number") - credits Float @default(0) - dateJoined DateTime @default(now()) @map("date_joined") @db.Date - dateLeft DateTime? @map("date_left") @db.Date - notes String? - tasks TaskAssigned[] - warnings Warning[] + user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) + userId Int @id @map("user_id") + residentId Int @unique @map("resident_id") // Differs from user id, this id is assigned by the staff + roomNumber Int @map("room_number") + credits Float @default(0) + dateJoined DateTime @default(now()) @map("date_joined") @db.Date + dateLeft DateTime? @map("date_left") @db.Date + tasks TaskAssigned[] + warnings Warning[] + notificationGroup NotificationGroup[] + notificationRecieved NotificationReceived[] @@map("residents") } @@ -83,19 +82,19 @@ enum RecurrenceFrequency { } model Task { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) type TaskType title String description String - creditValue Float @map("credit_value") - location TaskLocation @relation(fields: [locationId], references: [id], onDelete: Cascade, onUpdate: Cascade) - locationId Int @map("location_id") + creditValue Float @map("credit_value") + location TaskLocation @relation(fields: [locationId], references: [id], onDelete: Cascade, onUpdate: Cascade) + locationId Int @map("location_id") tasksAssigned TaskAssigned[] relatedWarnings Warning[] - endDate DateTime? @map("end_date") - recurrenceFrequency RecurrenceFrequency @map("recurrence_frequency") - specificDay DaysOfWeek? @map("specific_day") // used for one time tasks - repeatDays DaysOfWeek[] @map("repeat_days") // used for repeating tasks + endDate DateTime? @map("end_date") + recurrenceFrequency RecurrenceFrequency @map("recurrence_frequency") + specificDay DaysOfWeek? @map("specific_day") // used for one time tasks + repeatDays DaysOfWeek[] @map("repeat_days") // used for repeating tasks @@map("tasks") } @@ -110,16 +109,16 @@ model TaskLocation { } model TaskAssigned { - id Int @id @default(autoincrement()) - task Task @relation(fields: [taskId], references: [id]) - taskId Int @map("task_id") - assigner Staff? @relation(fields: [assignerId], references: [userId], onDelete: SetNull, onUpdate: Cascade) - assignerId Int? @map("assigner_id") - assignee Resident @relation(fields: [assigneeId], references: [userId], onDelete: Cascade, onUpdate: Cascade) - assigneeId Int @map("assignee_id") - status Status - startDate DateTime @map("start_date") - comments String? + id Int @id @default(autoincrement()) + task Task @relation(fields: [taskId], references: [id]) + taskId Int @map("task_id") + assigner Staff? @relation(fields: [assignerId], references: [userId], onDelete: SetNull, onUpdate: Cascade) + assignerId Int? @map("assigner_id") + assignee Resident @relation(fields: [assigneeId], references: [userId], onDelete: Cascade, onUpdate: Cascade) + assigneeId Int @map("assignee_id") + status Status + startDate DateTime @map("start_date") + comments String? @@map("tasks_assigned") } @@ -147,14 +146,22 @@ model Warning { @@map("warnings") } +model NotificationGroup { + id Int @id @default(autoincrement()) + recipients Resident[] + notifications Notification[] + announcementGroup Boolean +} + model Notification { - id Int @id @default(autoincrement()) - author User? @relation(fields: [authorId], references: [id], onDelete: SetNull, onUpdate: Cascade) - authorId Int? @map("author_id") - title String - message String - createdAt DateTime @default(now()) @map("created_at") @db.Date - recipients NotificationReceived[] + id Int @id @default(autoincrement()) + message String + createdAt DateTime @default(now()) @map("created_at") @db.Date + author Staff? @relation(fields: [authorId], references: [userId], onDelete: SetNull, onUpdate: Cascade) + authorId Int? @map("author_id") + group NotificationGroup @relation(fields: [groupId], references: [id], onDelete: Cascade) + groupId Int @map("group_id") + notificationReceived NotificationReceived[] @@map("notifications") } @@ -163,7 +170,7 @@ model NotificationReceived { id Int @id @default(autoincrement()) notification Notification @relation(fields: [notificationId], references: [id], onDelete: Cascade, onUpdate: Cascade) notificationId Int @map("notification_id") - recipient User @relation(fields: [recipientId], references: [id], onDelete: Cascade, onUpdate: Cascade) + recipient Resident @relation(fields: [recipientId], references: [userId], onDelete: Cascade, onUpdate: Cascade) recipientId Int @map("recipient_id") seen Boolean @default(false) diff --git a/backend/services/implementations/notificationService.ts b/backend/services/implementations/notificationService.ts index b9d6ce77..bb8828da 100644 --- a/backend/services/implementations/notificationService.ts +++ b/backend/services/implementations/notificationService.ts @@ -2,6 +2,9 @@ import prisma from "../../prisma"; import INotificationService, { NotificationDTO, NotificationReceivedDTO, + NotificationGroupDTO, + CreateNotificationDTO, + UpdateNotificationDTO, } from "../interfaces/notificationService"; import IResidentService from "../interfaces/residentService"; import logger from "../../utilities/logger"; @@ -16,72 +19,90 @@ class NotificationService implements INotificationService { this.residentService = residentService; } - async getNotificationsByUserId( - id: number, - ): Promise { + async createNotificationGroup( + roomIds: number[], + ): Promise { try { - const user = await prisma.user.findUnique({ - where: { - id, - }, - include: { - notificationsReceived: true, + const residents = await prisma.resident.findMany({ + where: { roomNumber: { in: roomIds } }, + }); + const residentIds = residents.map((resident) => resident.userId); + + const newNotificationGroup = await prisma.notificationGroup.create({ + data: { + recipients: { + connect: residentIds.map((id) => ({ userId: id })), + }, + announcementGroup: false, }, }); - if (!user) throw new Error(`No User found.`); - return user.notificationsReceived; + + return newNotificationGroup; } catch (error) { Logger.error( - `Failed to get Notification. Reason = ${getErrorMessage(error)}`, + `Failed to create Notification Group. Reason = ${getErrorMessage( + error, + )}`, ); throw error; } } - async getNotificationById(id: number): Promise { + async createAnnouncementGroup(): Promise { try { - const notification = await prisma.notificationReceived.findUnique({ - where: { - id, + const residents = await prisma.resident.findMany(); + const residentIds = residents.map((resident) => resident.userId); + + const newNotificationGroup = await prisma.notificationGroup.create({ + data: { + recipients: { + connect: residentIds.map((id) => ({ userId: id })), + }, + announcementGroup: true, }, }); - if (!notification) throw new Error(`notification id ${id} not found`); - return notification; - } catch (error: unknown) { + return newNotificationGroup; + } catch (error) { Logger.error( - `Failed to get Notification. Reason = ${getErrorMessage(error)}`, + `Failed to create Notification Group. Reason = ${getErrorMessage( + error, + )}`, ); throw error; } } - async sendNotification( - authorId: number, - title: string, - message: string, - recipientIds: number[], + async sendNotificationToGroup( + groupId: number, + notification: CreateNotificationDTO, ): Promise { try { + const notificationGroup = await prisma.notificationGroup.findUnique({ + where: { id: groupId }, + include: { recipients: true }, + }); const newNotification = await prisma.notification.create({ data: { - title, - message, + message: notification.message, + createdAt: notification.createdAt, author: { - connect: { id: authorId }, + connect: notification.authorId + ? { userId: notification.authorId } + : undefined, }, - recipients: { - create: recipientIds.map((recipient) => ({ - recipient: { - connect: { - id: recipient, - }, - }, - })), + group: { + connect: { id: groupId }, + }, + notificationReceived: { + create: notificationGroup + ? notificationGroup.recipients.map((resident) => ({ + recipient: { + connect: { userId: resident.userId }, + }, + })) + : undefined, }, - }, - include: { - recipients: true, }, }); @@ -94,21 +115,99 @@ class NotificationService implements INotificationService { } } - async deleteUserNotification( + async deleteNotificationGroup( + groupId: number, + ): Promise { + try { + const deletedNotificationGroup = await prisma.notificationGroup.delete({ + where: { + id: groupId, + }, + }); + + if (!deletedNotificationGroup) + throw new Error(`notification id ${groupId} not found`); + + return deletedNotificationGroup; + } catch (error) { + Logger.error( + `Failed to set isDelete flag. Reason = ${getErrorMessage(error)}`, + ); + throw error; + } + } + + async getAllGroupsAndNotifications(): Promise { + try { + const notificationGroups = await prisma.notificationGroup.findMany({ + include: { + // recipients: true, // TODO: resident type is incompatiable at time of writing + notifications: true, + }, + }); + return notificationGroups; + } catch (error) { + Logger.error( + `Failed to create Notification. Reason = ${getErrorMessage(error)}`, + ); + throw error; + } + } + + async getNotificationsByIds( + notificationIds: number[], + ): Promise { + try { + const notificationReceived = await prisma.notificationReceived.findMany({ + where: { notificationId: { in: notificationIds } }, + include: { notification: true }, + }); + + if (!notificationReceived) throw new Error(`No User found.`); + return notificationReceived; + } catch (error) { + Logger.error( + `Failed to get Notification. Reason = ${getErrorMessage(error)}`, + ); + throw error; + } + } + + async updateNotificationById( notificationId: number, + notification: UpdateNotificationDTO, ): Promise { try { - const deletedNotification = await prisma.notification.delete({ + const updatedNotification = await prisma.notification.update({ where: { id: notificationId, }, - include: { recipients: true }, + data: { + ...notification, + }, }); - if (!deletedNotification) + if (!updatedNotification) throw new Error(`notification id ${notificationId} not found`); - return deletedNotification; + return updatedNotification; + } catch (error) { + Logger.error( + `Failed to set seen flag. Reason = ${getErrorMessage(error)}`, + ); + throw error; + } + } + + async deleteNotificationByIds(notficationId: number[]): Promise { + try { + const deletedNotification = await prisma.notification.deleteMany({ + where: { id: { in: notficationId } }, + }); + + if (!deletedNotification) + throw new Error(`notification id ${notficationId} not found`); + return true; } catch (error) { Logger.error( `Failed to set isDelete flag. Reason = ${getErrorMessage(error)}`, @@ -118,12 +217,12 @@ class NotificationService implements INotificationService { } async updateSeenNotification( - notificationRecievedId: number, + notificationSeenId: number, ): Promise { try { await prisma.notificationReceived.update({ where: { - id: notificationRecievedId, + id: notificationSeenId, }, data: { seen: true, @@ -132,12 +231,12 @@ class NotificationService implements INotificationService { const updatedNotification = await prisma.notificationReceived.findUnique({ where: { - id: notificationRecievedId, + id: notificationSeenId, }, }); if (!updatedNotification) - throw new Error(`notification id ${notificationRecievedId} not found`); + throw new Error(`notification id ${notificationSeenId} not found`); return updatedNotification; } catch (error) { @@ -148,44 +247,232 @@ class NotificationService implements INotificationService { } } - async sendAnnouncement( - title: string, - message: string, - userId: number, - ): Promise { + async getNotificationByResident( + residentId: number, + ): Promise { try { - const activeResidents = await this.residentService.getActiveResidents(); - const newNotification = await prisma.notification.create({ - data: { - title, - message, - author: { - connect: { id: userId }, - }, - recipients: { - create: activeResidents.map((recipient) => ({ - recipient: { - connect: { - id: recipient.userId, - }, - }, - })), - }, - }, - include: { - recipients: true, + const notification = await prisma.notificationReceived.findMany({ + where: { + recipientId: residentId, }, + include: { notification: true }, }); - return newNotification; - } catch (error) { + if (!notification) + throw new Error(`notification id ${residentId} not found`); + + return notification; + } catch (error: unknown) { Logger.error( - `Failed to create Notification for Announcement. Reason = ${getErrorMessage( - error, - )}`, + `Failed to get Notification. Reason = ${getErrorMessage(error)}`, ); throw error; } } + + // async getNotificationsByRoomIds( + // roomIds: number[], + // ): Promise { + // try { + // const residents = await prisma.resident.findMany({ + // where: { roomNumber: { in: roomIds } }, + // }); + // const residentIds = residents.map((resident) => resident.userId); + + // const notificationReceived = await prisma.notificationReceived.findMany({ + // where: { recipientId: { in: residentIds } }, + // }); + // if (!notificationReceived) throw new Error(`No User found.`); + // return notificationReceived; + // } catch (error) { + // Logger.error( + // `Failed to get Notification. Reason = ${getErrorMessage(error)}`, + // ); + // throw error; + // } + // } + + // async getNotificationById(id: number): Promise { + // try { + // const notification = await prisma.notificationReceived.findUnique({ + // where: { + // id, + // }, + // }); + // if (!notification) throw new Error(`notification id ${id} not found`); + + // return notification; + // } catch (error: unknown) { + // Logger.error( + // `Failed to get Notification. Reason = ${getErrorMessage(error)}`, + // ); + // throw error; + // } + // } + + // async sendNotification( + // authorId: number, + // title: string, + // message: string, + // roomIds: number[], + // ): Promise { + // try { + // const residents = await prisma.resident.findMany({ + // where: { roomNumber: { in: roomIds } }, + // }); + // const residentIds = residents.map((resident) => resident.userId); + + // const newNotification = await prisma.notification.create({ + // data: { + // title, + // message, + // author: { + // connect: { id: authorId }, + // }, + // recipients: { + // create: residentIds.map((resident) => ({ + // recipient: { + // connect: { + // id: resident, + // }, + // }, + // })), + // }, + // }, + // include: { + // recipients: true, + // }, + // }); + + // return newNotification; + // } catch (error) { + // Logger.error( + // `Failed to create Notification. Reason = ${getErrorMessage(error)}`, + // ); + // throw error; + // } + // } + + // async deleteUserNotification( + // notificationId: number, + // ): Promise { + // try { + // const deletedNotification = await prisma.notification.delete({ + // where: { + // id: notificationId, + // }, + // include: { recipients: true }, + // }); + + // if (!deletedNotification) + // throw new Error(`notification id ${notificationId} not found`); + + // return deletedNotification; + // } catch (error) { + // Logger.error( + // `Failed to set isDelete flag. Reason = ${getErrorMessage(error)}`, + // ); + // throw error; + // } + // } + + // async updateSeenNotification( + // notificationRecievedId: number, + // ): Promise { + // try { + // await prisma.notificationReceived.update({ + // where: { + // id: notificationRecievedId, + // }, + // data: { + // seen: true, + // }, + // }); + + // const updatedNotification = await prisma.notificationReceived.findUnique({ + // where: { + // id: notificationRecievedId, + // }, + // }); + + // if (!updatedNotification) + // throw new Error(`notification id ${notificationRecievedId} not found`); + + // return updatedNotification; + // } catch (error) { + // Logger.error( + // `Failed to set seen flag. Reason = ${getErrorMessage(error)}`, + // ); + // throw error; + // } + // } + + // async updateNotificationById( + // notificationId: number, + // notification: UpdateNotificationDTO, + // ): Promise { + // try { + // const updatedNotification = await prisma.notification.update({ + // where: { + // id: notificationId, + // }, + // data: { + // ...notification, + // }, + // include: { + // recipients: true, + // }, + // }); + + // if (!updatedNotification) + // throw new Error(`notification id ${notificationId} not found`); + + // return updatedNotification; + // } catch (error) { + // Logger.error( + // `Failed to set seen flag. Reason = ${getErrorMessage(error)}`, + // ); + // throw error; + // } + // } + + // async sendAnnouncement( + // title: string, + // message: string, + // userId: number, + // ): Promise { + // try { + // const activeResidents = await this.residentService.getActiveResidents(); + // const newNotification = await prisma.notification.create({ + // data: { + // title, + // message, + // author: { + // connect: { id: userId }, + // }, + // recipients: { + // create: activeResidents.map((recipient) => ({ + // recipient: { + // connect: { + // id: recipient.userId, + // }, + // }, + // })), + // }, + // }, + // include: { + // recipients: true, + // }, + // }); + // return newNotification; + // } catch (error) { + // Logger.error( + // `Failed to create Notification for Announcement. Reason = ${getErrorMessage( + // error, + // )}`, + // ); + // throw error; + // } + // } } export default NotificationService; diff --git a/backend/services/implementations/residentService.ts b/backend/services/implementations/residentService.ts index c74c33d4..305c9e34 100644 --- a/backend/services/implementations/residentService.ts +++ b/backend/services/implementations/residentService.ts @@ -16,33 +16,45 @@ class ResidentService implements IResidentService { async addResident(resident: CreateResidentDTO): Promise { try { const firebaseUser = await firebaseAdmin.auth().createUser({ - email: resident.email, password: resident.password, }); try { + const announcementGroups = await prisma.notificationGroup.findMany({ + where: { + announcementGroup: true, + }, + include: { notifications: true }, + }); + const newResident = await prisma.resident.create({ data: { residentId: resident.residentId, - birthDate: resident.birthDate, roomNumber: resident.roomNumber, credits: resident.credits, dateJoined: resident.dateJoined, dateLeft: resident.dateLeft, - notes: resident.notes, user: { create: { authId: firebaseUser.uid, type: UserType.RESIDENT, - email: resident.email, - phoneNumber: resident.phoneNumber, - firstName: resident.firstName, - lastName: resident.lastName, displayName: resident.displayName, profilePictureURL: resident.profilePictureURL, isActive: true, }, }, + notificationGroup: { + connect: announcementGroups.map((group) => ({ id: group.id })), + }, + notificationRecieved: { + create: announcementGroups.flatMap((group) => + group.notifications.map((notif) => ({ + notification: { + connect: { id: notif.id }, + }, + })), + ), + }, }, include: { user: true }, }); @@ -50,16 +62,10 @@ class ResidentService implements IResidentService { return { userId: newResident.userId, residentId: newResident.residentId, - birthDate: newResident.birthDate, roomNumber: newResident.roomNumber, credits: newResident.credits, dateJoined: newResident.dateJoined, dateLeft: newResident.dateLeft, - notes: newResident.notes, - email: newResident.user.email, - phoneNumber: newResident.user.phoneNumber, - firstName: newResident.user.firstName, - lastName: newResident.user.lastName, displayName: newResident.user.displayName, profilePictureURL: newResident.user.profilePictureURL, isActive: newResident.user.isActive, @@ -105,34 +111,28 @@ class ResidentService implements IResidentService { } const { authId } = oldUser; - const email = "email" in resident ? resident.email : oldUser.email; + // const email = "email" in resident ? resident.email : oldUser.email; if ("password" in resident) { await firebaseAdmin.auth().updateUser(authId, { - email, + // email, password: resident.password, }); } else { - await firebaseAdmin.auth().updateUser(authId, { email }); + await firebaseAdmin.auth().updateUser(authId, {}); } const updatedResident = await prisma.resident.update({ where: { userId }, data: { residentId: resident.residentId || undefined, - birthDate: resident.birthDate || undefined, roomNumber: resident.roomNumber || undefined, credits: resident.credits || undefined, dateJoined: resident.dateJoined || undefined, dateLeft: resident.dateLeft || undefined, - notes: resident.notes || undefined, user: { update: { data: { - email: resident.email || undefined, - phoneNumber: resident.phoneNumber || undefined, - firstName: resident.firstName || undefined, - lastName: resident.lastName || undefined, displayName: resident.displayName || undefined, profilePictureURL: resident.profilePictureURL || undefined, isActive: resident.isActive || undefined, @@ -146,16 +146,10 @@ class ResidentService implements IResidentService { return { userId: updatedResident.userId, residentId: updatedResident.residentId, - birthDate: updatedResident.birthDate, roomNumber: updatedResident.roomNumber, credits: updatedResident.credits, dateJoined: updatedResident.dateJoined, dateLeft: updatedResident.dateLeft, - notes: updatedResident.notes, - email: updatedResident.user.email, - phoneNumber: updatedResident.user.phoneNumber, - firstName: updatedResident.user.firstName, - lastName: updatedResident.user.lastName, displayName: updatedResident.user.displayName, profilePictureURL: updatedResident.user.profilePictureURL, isActive: updatedResident.user.isActive, @@ -198,16 +192,10 @@ class ResidentService implements IResidentService { return { userId: deletedResident.userId, residentId: deletedResident.residentId, - birthDate: deletedResident.birthDate, roomNumber: deletedResident.roomNumber, credits: deletedResident.credits, dateJoined: deletedResident.dateJoined, dateLeft: deletedResident.dateLeft, - notes: deletedResident.notes, - email: deletedResident.user.email, - phoneNumber: deletedResident.user.phoneNumber, - firstName: deletedResident.user.firstName, - lastName: deletedResident.user.lastName, displayName: deletedResident.user.displayName, profilePictureURL: deletedResident.user.profilePictureURL, isActive: deletedResident.user.isActive, @@ -231,16 +219,10 @@ class ResidentService implements IResidentService { return { userId: resident.userId, residentId: resident.residentId, - birthDate: resident.birthDate, roomNumber: resident.roomNumber, credits: resident.credits, dateJoined: resident.dateJoined, dateLeft: resident.dateLeft, - notes: resident.notes, - email: resident.user.email, - phoneNumber: resident.user.phoneNumber, - firstName: resident.user.firstName, - lastName: resident.user.lastName, displayName: resident.user.displayName, profilePictureURL: resident.user.profilePictureURL, isActive: resident.user.isActive, @@ -264,16 +246,10 @@ class ResidentService implements IResidentService { return { userId: resident.userId, residentId: resident.residentId, - birthDate: resident.birthDate, roomNumber: resident.roomNumber, credits: resident.credits, dateJoined: resident.dateJoined, dateLeft: resident.dateLeft, - notes: resident.notes, - email: resident.user.email, - phoneNumber: resident.user.phoneNumber, - firstName: resident.user.firstName, - lastName: resident.user.lastName, displayName: resident.user.displayName, profilePictureURL: resident.user.profilePictureURL, isActive: resident.user.isActive, @@ -304,16 +280,10 @@ class ResidentService implements IResidentService { return { userId: resident.userId, residentId: resident.residentId, - birthDate: resident.birthDate, roomNumber: resident.roomNumber, credits: resident.credits, dateJoined: resident.dateJoined, dateLeft: resident.dateLeft, - notes: resident.notes, - email: resident.user.email, - phoneNumber: resident.user.phoneNumber, - firstName: resident.user.firstName, - lastName: resident.user.lastName, displayName: resident.user.displayName, profilePictureURL: resident.user.profilePictureURL, isActive: resident.user.isActive, diff --git a/backend/services/implementations/staffService.ts b/backend/services/implementations/staffService.ts index 8934402a..9bec479a 100644 --- a/backend/services/implementations/staffService.ts +++ b/backend/services/implementations/staffService.ts @@ -23,14 +23,14 @@ class StaffService implements IStaffService { const newStaff = await prisma.staff.create({ data: { isAdmin: staff.isAdmin, + email: staff.email, + firstName: staff.firstName, + lastName: staff.lastName, + phoneNumber: staff.phoneNumber, user: { create: { authId: firebaseUser.uid, type: UserType.STAFF, - email: staff.email, - phoneNumber: staff.phoneNumber, - firstName: staff.firstName, - lastName: staff.lastName, displayName: staff.displayName, profilePictureURL: staff.profilePictureURL, }, @@ -43,9 +43,9 @@ class StaffService implements IStaffService { userId: newStaff.userId, isAdmin: newStaff.isAdmin, email: firebaseUser.email ?? "", - phoneNumber: newStaff.user.phoneNumber, - firstName: newStaff.user.firstName, - lastName: newStaff.user.lastName, + phoneNumber: newStaff.phoneNumber, + firstName: newStaff.firstName, + lastName: newStaff.lastName, displayName: newStaff.user.displayName, profilePictureURL: newStaff.user.profilePictureURL, isActive: newStaff.user.isActive, @@ -83,8 +83,8 @@ class StaffService implements IStaffService { } const { authId } = originalUser; - const email = "email" in staff ? staff.email : originalUser.email; - + // const email = "email" in staff ? staff.email : originalUser.email; + const { email } = staff; if ("password" in staff) { await firebaseAdmin.auth().updateUser(authId, { email, @@ -98,13 +98,13 @@ class StaffService implements IStaffService { where: { userId }, data: { isAdmin: staff.isAdmin, + email: staff.email, + phoneNumber: staff.phoneNumber, + firstName: staff.firstName, + lastName: staff.lastName, user: { update: { data: { - email: staff.email, - phoneNumber: staff.phoneNumber, - firstName: staff.firstName, - lastName: staff.lastName, displayName: staff.displayName, profilePictureURL: staff.profilePictureURL, isActive: staff.isActive, @@ -120,10 +120,10 @@ class StaffService implements IStaffService { return { userId: updatedStaff.userId, isAdmin: updatedStaff.isAdmin, - email: updatedStaff.user.email, - phoneNumber: updatedStaff.user.phoneNumber, - firstName: updatedStaff.user.firstName, - lastName: updatedStaff.user.lastName, + email: updatedStaff.email, + phoneNumber: updatedStaff.phoneNumber, + firstName: updatedStaff.firstName, + lastName: updatedStaff.lastName, displayName: updatedStaff.user.displayName, profilePictureURL: updatedStaff.user.profilePictureURL, isActive: updatedStaff.user.isActive, @@ -162,10 +162,10 @@ class StaffService implements IStaffService { return { userId: deletedStaff.userId, isAdmin: deletedStaff.isAdmin, - email: deletedUser.email, - phoneNumber: deletedUser.phoneNumber, - firstName: deletedUser.firstName, - lastName: deletedUser.lastName, + email: deletedStaff.email, + phoneNumber: deletedStaff.phoneNumber, + firstName: deletedStaff.firstName, + lastName: deletedStaff.lastName, displayName: deletedUser.displayName, profilePictureURL: deletedUser.profilePictureURL, isActive: deletedUser.isActive, @@ -190,10 +190,10 @@ class StaffService implements IStaffService { return { userId: staff.userId, isAdmin: staff.isAdmin, - email: staff.user.email, - phoneNumber: staff.user.phoneNumber, - firstName: staff.user.firstName, - lastName: staff.user.lastName, + email: staff.email, + phoneNumber: staff.phoneNumber, + firstName: staff.firstName, + lastName: staff.lastName, displayName: staff.user.displayName, profilePictureURL: staff.user.profilePictureURL, isActive: staff.user.isActive, @@ -216,10 +216,10 @@ class StaffService implements IStaffService { return { userId: staff.userId, isAdmin: staff.isAdmin, - email: staff.user.email, - phoneNumber: staff.user.phoneNumber, - firstName: staff.user.firstName, - lastName: staff.user.lastName, + email: staff.email, + phoneNumber: staff.phoneNumber, + firstName: staff.firstName, + lastName: staff.lastName, displayName: staff.user.displayName, profilePictureURL: staff.user.profilePictureURL, isActive: staff.user.isActive, diff --git a/backend/services/implementations/userService.ts b/backend/services/implementations/userService.ts index 012d78c2..fe4a6cb9 100644 --- a/backend/services/implementations/userService.ts +++ b/backend/services/implementations/userService.ts @@ -20,9 +20,6 @@ class UserService implements IUserService { return { id: user.id, type: user.type, - email: firebaseUser.email ?? "", - firstName: user.firstName, - lastName: user.lastName, }; } catch (error: unknown) { Logger.error(`Failed to get user. Reason = ${getErrorMessage(error)}`); diff --git a/backend/services/interfaces/notificationService.ts b/backend/services/interfaces/notificationService.ts index a8588e14..437f53ab 100644 --- a/backend/services/interfaces/notificationService.ts +++ b/backend/services/interfaces/notificationService.ts @@ -1,51 +1,158 @@ +import type { ResidentDTO } from "./residentService"; + export interface NotificationDTO { id: number; + message: string; + createdAt?: Date; authorId: number | null; - title: string; + groupId: number; + recipients?: NotificationReceivedDTO[]; +} + +export interface CreateNotificationDTO { message: string; - createdAt: Date; - recipients: NotificationReceivedDTO[]; + createdAt?: Date; + authorId: number | null; +} + +export interface UpdateNotificationDTO { + authorId?: number; + message?: string; + createdAt?: Date; +} + +export interface NotificationGroupDTO { + id: number; + recipients?: ResidentDTO[]; + notifications?: NotificationDTO[]; + announcementGroup: boolean; } export interface NotificationReceivedDTO { id: number; notificationId: number; + notification?: NotificationDTO; recipientId: number; seen: boolean; } interface INotificationService { /** - * Get all notifications for a given user id - * @param id user id + * create a new notification group + * @param roomIds list of room ids that correspond to resideents + * @returns a NotificationGroupDTO that was just created + * @throws Error if cration fails + */ + createNotificationGroup(roomIds: number[]): Promise; + + /** + * Create a new notification group + * @returns a NotificationGroupDTO that was deleted + * @throws Error if creation fails + */ + createAnnouncementGroup(): Promise; + + /** + * send a notification to a group + * @param groupId notification group to send to + * @param notification information related to the notification to be created + * @returns a NotificationDTO that was just created + * @throws Error if creation fails + */ + sendNotificationToGroup( + groupId: number, + notification: CreateNotificationDTO, + ): Promise; + + /** + * Delete a notification group + * @param groupId notification group to delete + * @returns a NotificationGroupDTO that was deleted + * @throws Error if deletion fails + */ + deleteNotificationGroup(groupId: number): Promise; + + /** + * Get all groups and their associated notifications + * @returns a list of NotificationGroupDTOs with their notifications + * @throws Error if retrieval fails + */ + getAllGroupsAndNotifications(): Promise; + + /** + * Get all notifications for given notification id + * @param id notification id * @returns a NotificationDTO[] associated with that users notifications * @throws Error if retrieval fails */ - getNotificationsByUserId(id: number): Promise; + getNotificationsByIds( + notificationIds: number[], + ): Promise; + + /** + * Updates notification for a given notification id + * @param id notification id + * @returns a NotificationDTO associated with the updated Notification + * @throws Error if retrieval fails + */ + updateNotificationById( + notificationId: number, + notification: UpdateNotificationDTO, + ): Promise; /** - * Get a notification by a defined id + * Deletes notifications for given notification ids * @param id notification id - * @returns a NotificationDTO associated with the notification id + * @returns a NotificationDTO associated with the updated Notification + * @throws Error if retrieval fails + */ + deleteNotificationByIds(notficationId: number[]): Promise; + + /** + * Update a user notification to be seen + * @param notificationId notification id + * @returns a NotificationDTO associated with the now seen Notification * @throws Error if retrieval fails */ - getNotificationById(id: number): Promise; + updateSeenNotification( + notificationSeenId: number, + ): Promise; + + /** + * Gets the notifications associated with a resident + * @param residentId resident id + * @returns a list of NotificationDTOs that the resident has + * @throws Error if retrieval fails + */ + getNotificationByResident( + residentId: number, + ): Promise; + + /** + * Get all notifications for a given user id + * @param id user id + * @returns a NotificationDTO[] associated with that users notifications + * @throws Error if retrieval fails + */ + // getNotificationsByRoomIds( + // roomIds: number[], + // ): Promise; /** * Post a notification to a specified resident or residents * @param authorId user id of author of notification * @param title title of notification * @param message message of notification - * @param recipientIds user ids of recipients of notification + * @param roomIds room ids of recipients of notification * @returns a NotificationDTO associated with the posted notifications * @throws Error if creation fails */ - sendNotification( - authorId: number, - title: string, - message: string, - recipientIds: number[], - ): Promise; + // sendNotification( + // authorId: number, + // title: string, + // message: string, + // roomIds: number[], + // ): Promise; /** * Delete a user notification based on a notification id and user id @@ -54,18 +161,28 @@ interface INotificationService { * @returns a NotificationDTO associated with the deleted Notification * @throws Error if retrieval fails */ - deleteUserNotification(notificationId: number): Promise; + // deleteUserNotification(notificationId: number): Promise; /** * Update a user notification to be seen - * @param userId user id * @param notificationId notification id * @returns a NotificationDTO associated with the now seen Notification * @throws Error if retrieval fails */ - updateSeenNotification( - notificationId: number, - ): Promise; + // updateSeenNotification( + // notificationId: number, + // ): Promise; + + /** + * Update a user notification to be seen + * @param notificationId notification id + * @returns a NotificationDTO associated with the updated Notification + * @throws Error if retrieval fails + */ + // updateNotificationById( + // notificationId: number, + // notification: UpdateNotificationDTO, + // ): Promise; /** * Post an announcement notification to all active residents @@ -75,11 +192,11 @@ interface INotificationService { * @returns the new updated NotificationDTO * @throws Error if creation fails */ - sendAnnouncement( - title: string, - message: string, - userId: number, - ): Promise; + // sendAnnouncement( + // title: string, + // message: string, + // userId: number, + // ): Promise; } export default INotificationService; diff --git a/backend/services/interfaces/residentService.ts b/backend/services/interfaces/residentService.ts index bb33881a..427b7dda 100644 --- a/backend/services/interfaces/residentService.ts +++ b/backend/services/interfaces/residentService.ts @@ -1,34 +1,35 @@ +// eslint-disable-next-line import/no-cycle +import { + NotificationGroupDTO, + NotificationReceivedDTO, +} from "./notificationService"; import { UserDTO, CreateUserDTO, UpdateUserDTO } from "./userService"; export interface ResidentDTO extends Omit { userId: number; residentId: number; - birthDate: Date; roomNumber: number; credits: number; dateJoined: Date; dateLeft: Date | null; - notes: string | null; + notificationGroup?: NotificationGroupDTO[]; + notificationRecieved?: NotificationReceivedDTO[]; } export interface CreateResidentDTO extends CreateUserDTO { residentId: number; - birthDate: Date; roomNumber: number; credits?: number; dateJoined?: Date; dateLeft?: Date; - notes?: string; } export interface UpdateResidentDTO extends UpdateUserDTO { residentId?: number; - birthDate?: Date; roomNumber?: number; credits?: number; dateJoined?: Date; dateLeft?: Date; - notes?: string; } // Have to manually map enums as ts treats enums as numbers diff --git a/backend/services/interfaces/staffService.ts b/backend/services/interfaces/staffService.ts index a11b95f1..1624cd7e 100644 --- a/backend/services/interfaces/staffService.ts +++ b/backend/services/interfaces/staffService.ts @@ -2,15 +2,27 @@ import { UserDTO, CreateUserDTO, UpdateUserDTO } from "./userService"; export interface StaffDTO extends Omit { userId: number; + email: string; + phoneNumber: string | null; + firstName: string; + lastName: string; isAdmin: boolean; } export interface CreateStaffDTO extends CreateUserDTO { isAdmin: boolean; + email: string; + phoneNumber: string | null; + firstName: string; + lastName: string; } export interface UpdateStaffDTO extends UpdateUserDTO { isAdmin?: boolean; + email: string; + phoneNumber: string | null; + firstName: string; + lastName: string; } interface IStaffService { diff --git a/backend/services/interfaces/userService.ts b/backend/services/interfaces/userService.ts index 606c9520..9c6157bb 100644 --- a/backend/services/interfaces/userService.ts +++ b/backend/services/interfaces/userService.ts @@ -3,36 +3,21 @@ import { UserType } from "../../prisma"; export type UserDTO = { id: number; type: UserType; - email: string; - phoneNumber: string | null; - firstName: string; - lastName: string; displayName: string | null; profilePictureURL: string | null; isActive: boolean; }; -export type SimplifiedUserDTO = Pick< - UserDTO, - "id" | "type" | "email" | "firstName" | "lastName" ->; +export type SimplifiedUserDTO = Pick; export interface CreateUserDTO { - email: string; password: string; - phoneNumber?: string; - firstName: string; - lastName: string; displayName?: string; profilePictureURL?: string; } export interface UpdateUserDTO { - email?: string; password?: string; - phoneNumber?: string; - firstName?: string; - lastName?: string; displayName?: string; profilePictureURL?: string; isActive?: boolean; diff --git a/frontend/src/APIClients/Mutations/ResidentsMutations.ts b/frontend/src/APIClients/Mutations/ResidentsMutations.ts index 330cc81b..7a99695e 100644 --- a/frontend/src/APIClients/Mutations/ResidentsMutations.ts +++ b/frontend/src/APIClients/Mutations/ResidentsMutations.ts @@ -5,19 +5,13 @@ export const ADD_RESIDENT = gql` addResident(resident: $resident) { userId residentId - email - phoneNumber - firstName - lastName displayName profilePictureURL isActive - birthDate roomNumber credits dateJoined dateLeft - notes } } `; @@ -27,19 +21,13 @@ export const UPDATE_RESIDENT = gql` updateResident(userId: $userId, resident: $resident) { userId residentId - email - phoneNumber - firstName - lastName displayName profilePictureURL isActive - birthDate roomNumber credits dateJoined dateLeft - notes } } `; @@ -49,19 +37,13 @@ export const DELETE_RESIDENT = gql` deleteResident(userId: $userId) { userId residentId - email - phoneNumber - firstName - lastName displayName profilePictureURL isActive - birthDate roomNumber credits dateJoined dateLeft - notes } } `; diff --git a/frontend/src/components/pages/announcements/AnnouncementsPage.tsx b/frontend/src/components/pages/announcements/AnnouncementsPage.tsx index ccfd47fc..6f973bc5 100644 --- a/frontend/src/components/pages/announcements/AnnouncementsPage.tsx +++ b/frontend/src/components/pages/announcements/AnnouncementsPage.tsx @@ -17,7 +17,7 @@ import { } from "../../../APIClients/Mutations/NotificationMutations"; import { - GET_NOTIFCATION_BY_ID, + GET_NOTIFCATION_BY_ID, GET_NOTIFICATIONS_BY_USER_ID, } from "../../../APIClients/Queries/NotificationQueries"; diff --git a/frontend/src/types/TaskTypes.ts b/frontend/src/types/TaskTypes.ts index 99712dac..527415f2 100644 --- a/frontend/src/types/TaskTypes.ts +++ b/frontend/src/types/TaskTypes.ts @@ -1,6 +1,16 @@ export type TaskType = "REQUIRED" | "OPTIONAL" | "CUSTOM" | "CHORE"; -export type RecurrenceFrequency = "ONE_TIME" | "REPEATS_PER_WEEK_SELECTED" | "REPEATS_PER_WEEK_ONCE"; -export type DaysOfWeek = "MONDAY" | "TUESDAY" | "WEDNESDAY" | "THURSDAY" | "FRIDAY" | "SATURDAY" | "SUNDAY"; +export type RecurrenceFrequency = + | "ONE_TIME" + | "REPEATS_PER_WEEK_SELECTED" + | "REPEATS_PER_WEEK_ONCE"; +export type DaysOfWeek = + | "MONDAY" + | "TUESDAY" + | "WEDNESDAY" + | "THURSDAY" + | "FRIDAY" + | "SATURDAY" + | "SUNDAY"; export interface Task { id: string;