diff --git a/backend/graphql/resolvers/notificationResolvers.ts b/backend/graphql/resolvers/notificationResolvers.ts index 4c243d6..351b304 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 7635459..b8007d3 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: String! - 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 83b68f0..bf5fa14 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 e0b47e6..3433c5d 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 f1c85c7..db995cc 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 b9d6ce7..bb8828d 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 c74c33d..305c9e3 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 8934402..9bec479 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/taskService.ts b/backend/services/implementations/taskService.ts index 80c401c..1c91f4a 100644 --- a/backend/services/implementations/taskService.ts +++ b/backend/services/implementations/taskService.ts @@ -172,6 +172,12 @@ class TaskService implements ITaskService { async deleteTaskById(taskId: number): Promise { try { + await prisma.taskAssigned.deleteMany({ + where: { + taskId, + }, + }); + const deletedTask = await prisma.task.delete({ where: { id: taskId, diff --git a/backend/services/implementations/userService.ts b/backend/services/implementations/userService.ts index 012d78c..fe4a6cb 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 a8588e1..437f53a 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 bb33881..427b7dd 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 a11b95f..1624cd7 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 606c952..9c6157b 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/backend/yarn.lock b/backend/yarn.lock index bc2e2d3..0ab42f4 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -3637,6 +3637,11 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= +fsevents@2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + fsevents@^2.3.2, fsevents@~2.3.1: version "2.3.2" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" diff --git a/frontend/package.json b/frontend/package.json index 65defb8..6c2c783 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,6 +9,10 @@ "@chakra-ui/styled-system": "^2.9.2", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", + "@fullcalendar/core": "^6.1.15", + "@fullcalendar/daygrid": "^6.1.15", + "@fullcalendar/react": "^6.1.15", + "@fullcalendar/timegrid": "^6.1.15", "@mui/icons-material": "^5.15.14", "@mui/material": "^5.15.14", "@rjsf/bootstrap-4": "^2.5.1", diff --git a/frontend/src/APIClients/Mutations/NotificationMutations.ts b/frontend/src/APIClients/Mutations/NotificationMutations.ts new file mode 100644 index 0000000..257f710 --- /dev/null +++ b/frontend/src/APIClients/Mutations/NotificationMutations.ts @@ -0,0 +1,58 @@ +import { gql } from "@apollo/client"; + +export const SEND_NOTIFICATION = gql` + mutation SendNotification( + $authorId: ID! + $title: String! + $message: String! + $recipientIds: [ID!] + ) { + sendNotification( + authorId: $authorId + title: $title + message: $message + recipientIds: $recipientIds + ) { + id + authorId + title + message + createdAt + } + } +`; + +export const DELETE_USER_NOTIFICATION = gql` + mutation DeleteUserNotification($notificationId: ID!) { + deleteUserNotification(notificationId: $notificationId) { + id + authorId + title + message + createdAt + } + } +`; + +export const UPDATE_SEEN_NOTIFICATION = gql` + mutation UpdateSeenNotification($notificationId: ID!) { + updateSeenNotification(notificationId: $notificationId) { + id + notificationId + recipientId + seen + } + } +`; + +export const SEND_ANNOUNCEMENT = gql` + mutation SendAnnouncement($title: String, $message: String, $userId: ID) { + sendAnnouncement(title: $title, message: $message, userId: $userId) { + id + authorId + title + message + createdAt + } + } +`; diff --git a/frontend/src/APIClients/Mutations/ResidentsMutations.ts b/frontend/src/APIClients/Mutations/ResidentsMutations.ts index 62839d0..0cad114 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 } } `; \ No newline at end of file diff --git a/frontend/src/APIClients/Mutations/TaskMutations.ts b/frontend/src/APIClients/Mutations/TaskMutations.ts new file mode 100644 index 0000000..7f8c96e --- /dev/null +++ b/frontend/src/APIClients/Mutations/TaskMutations.ts @@ -0,0 +1,77 @@ +import { gql } from "@apollo/client"; + +export const CREATE_TASK = gql` + mutation CreateTask($task: InputTaskDTO!) { + createTask(task: $task) { + id + type + title + description + creditValue + endDate + recurrenceFrequency + specificDay + repeatDays + } + } +`; + +export const UPDATE_TASK = gql` + mutation UpdateTASK($taskId: Int!, $task: InputTaskDTO!) { + updateTask(taskId: $taskId, task: $task) { + id + type + title + description + creditValue + endDate + recurrenceFrequency + specificDay + repeatDays + } + } +`; + +export const DELETE_TASK = gql` + mutation DeleteStaff($taskId: Int!) { + deleteTask(taskId: $taskId) { + id + type + title + description + creditValue + endDate + recurrenceFrequency + specificDay + repeatDays + } + } +`; + +export const ASSIGN_TASK = gql` + mutation AssignTask($taskAssigned: InputTaskAssignedDTO!) { + assignTask(taskAssigned: $taskAssigned) { + id + taskId + assigneeId + assignerId + status + startDate + comments + } + } +`; + +export const CHANGE_TASK_STATUS = gql` + mutation ChangeTaskStatus($taskAssignedId: Int!, $status: Status!) { + changeTaskStatus(taskAssignedId: $taskAssignedId, status: $status) { + id + taskId + assigneeId + assignerId + status + startDate + comments + } + } +`; diff --git a/frontend/src/APIClients/Mutations/WarningsMutations.ts b/frontend/src/APIClients/Mutations/WarningsMutations.ts new file mode 100644 index 0000000..771420c --- /dev/null +++ b/frontend/src/APIClients/Mutations/WarningsMutations.ts @@ -0,0 +1,29 @@ +import { gql } from "@apollo/client"; + +export const ADD_WARNING = gql` + mutation AddWarning($warning: CreateWarningDTO!) { + addWarning(warning: $warning) { + id + title + description + dateIssued + assigneeId + assignerId + relatedTaskId + } + } +`; + +export const DELETE_WARNING = gql` + mutation DeleteWarning($warningId: ID!) { + deleteWarning(id: $warningId) { + id + title + description + dateIssued + assigneeId + assignerId + relatedTaskId + } + } +`; diff --git a/frontend/src/APIClients/Queries/NotificationQueries.ts b/frontend/src/APIClients/Queries/NotificationQueries.ts new file mode 100644 index 0000000..8206c6b --- /dev/null +++ b/frontend/src/APIClients/Queries/NotificationQueries.ts @@ -0,0 +1,23 @@ +import { gql } from "@apollo/client"; + +export const GET_NOTIFICATIONS_BY_USER_ID = gql` + query GetNotificationsByUserId($userId: ID!) { + getNotificationsByUserId(userId: $userId) { + id + notificationId + recipientId + seen + } + } +`; + +export const GET_NOTIFCATION_BY_ID = gql` + query GetNotificationById($id: ID!) { + getNotificationById(id: $id) { + id + notificationId + recipientId + seen + } + } +`; diff --git a/frontend/src/APIClients/Queries/TaskQueries.ts b/frontend/src/APIClients/Queries/TaskQueries.ts new file mode 100644 index 0000000..d205735 --- /dev/null +++ b/frontend/src/APIClients/Queries/TaskQueries.ts @@ -0,0 +1,89 @@ +import { gql } from "@apollo/client"; + +export const GET_TASK_BY_ID = gql` + query getTaskById($taskId: Int!) { + getTaskById(taskId: $taskId) { + id + type + title + description + creditValue + endDate + recurrenceFrequency + specificDay + repeatDays + } + } +`; + +export const GET_TASKS_BY_TYPE = gql` + query getTasksByType($type: TaskType!) { + getTasksByType(type: $type) { + id + type + title + description + creditValue + endDate + recurrenceFrequency + specificDay + repeatDays + } + } +`; + +export const GET_TASKS_BY_ASSIGNEE_ID = gql` + query getTasksByAssigneeId($assigneeId: Int!) { + getTasksByAssigneeId(assigneeId: $assigneeId) { + id + taskId + assigneeId + assignerId + status + startDate + comments + } + } +`; + +export const GET_TASKS_BY_ASSIGNER_ID = gql` + query GetTasksByAssignerId($assignerId: Int!) { + getTasksByAssignerId(assignerId: $assignerId) { + id + taskId + assigneeId + assignerId + status + startDate + comments + } + } +`; + +export const GET_TASKS_BY_START_DATE = gql` + query GetTasksByStartDate($startDate: DateTime!) { + getTasksByStartDate(startDate: $startDate) { + id + taskId + assigneeId + assignerId + status + startDate + comments + } + } +`; + +export const GET_TASKS_BY_STATUS = gql` + query GetTasksByStatus($status: Status!) { + getTasksByStatus(status: $status) { + id + taskId + assigneeId + assignerId + status + startDate + comments + } + } +`; diff --git a/frontend/src/APIClients/Types/NotificationType.ts b/frontend/src/APIClients/Types/NotificationType.ts new file mode 100644 index 0000000..464e1c4 --- /dev/null +++ b/frontend/src/APIClients/Types/NotificationType.ts @@ -0,0 +1,15 @@ +export type NotificationResponse = { + id: string; + authorId?: string; + title: string; + message: string; + createdAt: Date; + recipients?: [NotificationReceived]; +}; + +export type NotificationReceived = { + id: string; + notificationId: string; + recipientId: number; + seen: boolean; +}; diff --git a/frontend/src/APIClients/Types/TaskType.ts b/frontend/src/APIClients/Types/TaskType.ts new file mode 100644 index 0000000..bd4c193 --- /dev/null +++ b/frontend/src/APIClients/Types/TaskType.ts @@ -0,0 +1,81 @@ +export enum Status { + PENDING_APPROVAL = "PENDING_APPROVAL", + ASSIGNED = "ASSIGNED", + INCOMPLETE = "INCOMPLETE", + COMPLETE = "COMPLETE", + EXCUSED = "EXCUSED", +} + +export enum RecurrenceFrequency { + ONE_TIME = "ONE_TIME", + REPEATS_PER_WEEK_SELECTED = "REPEATS_PER_WEEK_SELECTED", + REPEATS_PER_WEEK_ONCE = "REPEATS_PER_WEEK_ONCE", +} + +export enum DaysOfWeek { + MONDAY = "MONDAY", + TUESDAY = "TUESDAY", + WEDNESDAY = "WEDNESDAY", + THURSDAY = "THURSDAY", + FRIDAY = "FRIDAY", + SATURDAY = "SATURDAY", + SUNDAY = "SUNDAY", +} + +export enum TaskTypeEnum { + REQUIRED = "REQUIRED", + OPTIONAL = "OPTIONAL", + CHORE = "CHORE", + ACHIEVEMENT = "ACHIEVEMENT", +} + +export type TaskLocation = { + id: number; + title: string; + description: string; +}; + +export type TaskResponse = { + id: number; + type: TaskTypeEnum; + title: string; + description: string; + creditValue: number; + location: TaskLocation; + tasksAssigned: TaskResponse[]; + endDate?: Date; + recurrenceFrequency: RecurrenceFrequency; + specificDay?: DaysOfWeek; + repeatDays?: DaysOfWeek[]; +}; + +export type TaskRequest = { + type: TaskTypeEnum; + title: string; + description: string; + creditValue: number; + locationId: number; + endDate?: Date; + recurrenceFrequency: RecurrenceFrequency; + specificDay?: DaysOfWeek; + repeatDays?: DaysOfWeek[]; +}; + +export type TaskAssignedResponse = { + id: number; + taskId: number; + assigneeId: number; + assignerId: number; + status: Status; + startDate: Date; + comments?: string; +}; + +export type TaskAssignedRequest = { + taskId?: number; + assigneeId?: number; + assignerId?: number; + status?: Status; + startDate?: Date; + comments?: string; +}; diff --git a/frontend/src/APIClients/Types/WarningsType.ts b/frontend/src/APIClients/Types/WarningsType.ts new file mode 100644 index 0000000..eb9a32b --- /dev/null +++ b/frontend/src/APIClients/Types/WarningsType.ts @@ -0,0 +1,18 @@ +export type WarningDTO = { + id: number; + title: string; + description: string; + dateIssued: Date; + assigneeId: number; + assignerId: number; + relatedTaskId: number; +}; + +export type CreateWarningDTO = { + title: string; + description: string; + dateIssued: Date; + assigneeId: number; + assignerId: number; + relatedTaskId: number; +}; diff --git a/frontend/src/components/common/CommonTable.tsx b/frontend/src/components/common/CommonTable.tsx index dc1146b..c664760 100644 --- a/frontend/src/components/common/CommonTable.tsx +++ b/frontend/src/components/common/CommonTable.tsx @@ -13,14 +13,16 @@ import { Flex, IconButton, Icon, + Text, } from "@chakra-ui/react"; import EditOutlinedIcon from "@mui/icons-material/EditOutlined"; import ChevronLeftOutlinedIcon from "@mui/icons-material/ChevronLeftOutlined"; import ChevronRightOutlinedIcon from "@mui/icons-material/ChevronRightOutlined"; import KeyboardArrowUpOutlinedIcon from "@mui/icons-material/KeyboardArrowUpOutlined"; import KeyboardArrowDownOutlinedIcon from "@mui/icons-material/KeyboardArrowDownOutlined"; +import ModalContainer from "./ModalContainer"; -type TableTypes = string | number | boolean | Date; +type TableTypes = string | number | boolean | Date | string[]; export type ColumnInfoTypes = { header: string; key: string }; @@ -53,6 +55,8 @@ const CommonTable = ({ const [sortingColumn, setSortingColumn] = useState({}); const [originalData, setOriginalData] = useState(data); const [sortedData, setSortedData] = useState(data); + const [isPreviewModalOpen, setIsPreviewModalOpen] = useState(false); + const [selectedRow, setSelectedRow] = useState({}); useEffect(() => { return Math.ceil(data.length / maxResults) >= 5 @@ -70,6 +74,24 @@ const CommonTable = ({ setSortedData(data); }, [data]); + const handleRowClick = (row: TableData) => { + setSelectedRow(row); + setIsPreviewModalOpen(true); + }; + + interface ColumnInfo { + header: string; + value: string; + } + + const colData: ColumnInfo[] = columnInfo.map((col, index) => { + const value = Object.entries(selectedRow)[index]?.[1] || " "; + return { + header: String(col.header), + value: String(value), + }; + }); + // sorting the columns by ascending and descending order based on column indicated const sortColumn = (column: string) => { const newSortingColumn: SortState = {}; @@ -240,9 +262,21 @@ const CommonTable = ({ ) : null} {columnInfo.map((column, i) => ( - {String(row[column.key])} + { + handleRowClick(row); + }} + key={i} + > + {String(row[column.key])} + ))} - onEdit(row)}> + { + e.stopPropagation(); + onEdit(row); + }} + > + {isPreviewModalOpen && selectedRow && ( + + + {colData.slice(1).map((column, index) => ( + + + {column.header}:{" "} + {" "} + {column.value} + + ))} + + + )} + - {`Showing ${(page - 1) * maxResults + 1} to ${page * maxResults} of ${ - data.length - } entries`} + {`Showing ${(page - 1) * maxResults + 1} to ${Math.min( + page * maxResults, + data.length, + )} of ${data.length} entries`} ) => void; onBlur?: () => void; @@ -42,9 +42,11 @@ const FormField = ({ }) => ( - - {label} - + {label && ( + + {label} + + )} {leftElement && ( diff --git a/frontend/src/components/common/ModalContainer.tsx b/frontend/src/components/common/ModalContainer.tsx index b13cb8c..b1e3a97 100644 --- a/frontend/src/components/common/ModalContainer.tsx +++ b/frontend/src/components/common/ModalContainer.tsx @@ -8,6 +8,7 @@ import { Button, } from "@chakra-ui/react"; import DeleteOutlinedIcon from "@mui/icons-material/DeleteOutlined"; +import CloseIcon from "@mui/icons-material/Close"; type Props = { title: string; @@ -40,7 +41,16 @@ const ModalContainer = ({ Delete - ) : null} + ) : ( + + )} {children} diff --git a/frontend/src/components/pages/announcements/AnnouncementsPage.tsx b/frontend/src/components/pages/announcements/AnnouncementsPage.tsx index 6e762e4..6f973bc 100644 --- a/frontend/src/components/pages/announcements/AnnouncementsPage.tsx +++ b/frontend/src/components/pages/announcements/AnnouncementsPage.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useState } from "react"; -import { Flex } from "@chakra-ui/react"; +import { Flex, Button } from "@chakra-ui/react"; +import { useMutation, useQuery } from "@apollo/client"; import { GroupAnnouncements, Announcement, @@ -8,12 +9,117 @@ import AnnouncementsGroups from "./AnnouncementsGroups"; import AnnouncementsView from "./AnnouncementsView"; import { announcementsMockData } from "../../../mocks/notifications"; +import { + SEND_NOTIFICATION, + DELETE_USER_NOTIFICATION, + UPDATE_SEEN_NOTIFICATION, + SEND_ANNOUNCEMENT, +} from "../../../APIClients/Mutations/NotificationMutations"; + +import { + GET_NOTIFCATION_BY_ID, + GET_NOTIFICATIONS_BY_USER_ID, +} from "../../../APIClients/Queries/NotificationQueries"; + +import { + NotificationResponse, + NotificationReceived, +} from "../../../APIClients/Types/NotificationType"; + const AnnouncementsPage = (): React.ReactElement => { const [announcements, setAnnouncements] = useState({}); const [selectedGroup, setSelectedGroup] = useState(""); const [addingNewRoom, setAddingNewRoom] = useState(false); const [selectedRooms, setSelectedRooms] = useState([]); + // const [sendNotification] = useMutation<{ + // authorId: string; + // title: string; + // message: string; + // recipientIds: [number]; + // }>(SEND_NOTIFICATION); + + // const [deleteUserNotification] = useMutation<{ + // notificationId: string; + // }>(DELETE_USER_NOTIFICATION); + + // const [updateSeenNotification] = useMutation<{ + // notificationId: string; + // }>(UPDATE_SEEN_NOTIFICATION); + + // const [sendAnnouncement] = useMutation<{ + // title: string; + // message: string; + // userId: number; + // }>(SEND_ANNOUNCEMENT); + + // const { + // loading: notificationsByUserIdLoading, + // error: notificationsByUserIdError, + // data: notificationsByUserIdData, + // } = useQuery<{ userId: string }>(GET_NOTIFICATIONS_BY_USER_ID, { + // variables: { userId: "4" }, + // }); + + // const { + // loading: notificationByIdLoading, + // error: notificationByIdError, + // data: notificationByIdData, + // } = useQuery<{ id: string }>(GET_NOTIFCATION_BY_ID, { + // variables: { id: "8" }, + // }); + + // const handleSendNotification = async () => { + // try { + // const authorId = "6"; + // const title = "TITLE NOTIF"; + // const message = "message"; + // const recipientIds = [4]; + // await sendNotification({ + // variables: { authorId, title, message, recipientIds }, + // }); + // } catch (e) { + // console.log(e); + // } + // }; + + // const handleSendAnnouncement = async () => { + // console.log(notificationsByUserIdData); + // console.log(notificationByIdData); + // try { + // const title = "TITLE NOTIF"; + // const message = "message"; + // const userId = 4; + // await sendAnnouncement({ + // variables: { title, message, userId }, + // }); + // } catch (e) { + // console.log(e); + // } + // }; + + // const handleDeleteNotification = async () => { + // try { + // const notificationId = "4"; + // await deleteUserNotification({ + // variables: { notificationId }, + // }); + // } catch (e) { + // console.log(e); + // } + // }; + + // const handleUpdateSeenNotification = async () => { + // try { + // const notificationId = "4"; + // await updateSeenNotification({ + // variables: { notificationId }, + // }); + // } catch (e) { + // console.log(e); + // } + // }; + useEffect(() => { // TODO: Fetch announcements from API const combinedAnnouncements: GroupAnnouncements = {}; diff --git a/frontend/src/components/pages/residents/ResidentsPage.tsx b/frontend/src/components/pages/residents/ResidentsPage.tsx index 6b76042..3fc5d9e 100644 --- a/frontend/src/components/pages/residents/ResidentsPage.tsx +++ b/frontend/src/components/pages/residents/ResidentsPage.tsx @@ -148,6 +148,13 @@ const ResidentsPage = (): React.ReactElement => { setEditInfo(row); }; + // CHANGE + const handleRowClick = (row: any) => { + setIsModalOpen("edit"); + // console.log(row); + setEditInfo(row); + }; + const handleResidentSubmitEdit = () => { setEditInfo(undefined); diff --git a/frontend/src/components/pages/schedule/SchedulePage.tsx b/frontend/src/components/pages/schedule/SchedulePage.tsx index add399f..491cc69 100644 --- a/frontend/src/components/pages/schedule/SchedulePage.tsx +++ b/frontend/src/components/pages/schedule/SchedulePage.tsx @@ -1,11 +1,11 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useImperativeHandle, useRef, useState } from "react"; import { Flex, Tabs, TabList, Tab, - Heading, Box, + Heading, Button, IconButton, Icon, @@ -19,13 +19,33 @@ import { CalendarMonth, } from "@mui/icons-material"; +import FullCalendar from "@fullcalendar/react"; +import { CalendarApi } from "@fullcalendar/core"; import { ScheduleType } from "../../../types/ScheduleTypes"; +import ScheduleListView from "./listView/ScheduleListView"; +import ScheduleCalendar from "./calendarView/ScheduleCalendar"; const SchedulePage = (): React.ReactElement => { const [rooms, setRooms] = useState([]); const [scheduleType, setScheduleType] = useState("LIST"); const [scheduleData, setScheduleData] = useState(""); const [active, setActive] = useState("List"); + const [dateRange, setDateRange] = useState("Jan 1 - 7"); + + const calendarRef = useRef(null); + + const handleNext = () => { + console.log(scheduleType); + if (scheduleType === "CALENDAR") { + calendarRef.current?.next(); + } + }; + + const handlePrev = () => { + if (scheduleType === "CALENDAR") { + calendarRef.current?.prev(); + } + }; useEffect(() => { // TODO: Fetch occupied rooms from API? @@ -64,15 +84,21 @@ const SchedulePage = (): React.ReactElement => { {formatTabs(rooms)} - + - + January 2025 {/* see announcements page for how to determine what text shows */} { size="md" fontSize="lg" > - Jan 1 - 7 + {dateRange} { > 200 M-Bucks - - @@ -130,7 +148,7 @@ const SchedulePage = (): React.ReactElement => { - + {scheduleType === "CALENDAR" ? ( - TEMP CALENDAR + setDateRange(range)} + /> ) : ( - {scheduleData} + )} diff --git a/frontend/src/components/pages/schedule/calendarView/ScheduleCalendar.css b/frontend/src/components/pages/schedule/calendarView/ScheduleCalendar.css new file mode 100644 index 0000000..48214f0 --- /dev/null +++ b/frontend/src/components/pages/schedule/calendarView/ScheduleCalendar.css @@ -0,0 +1,36 @@ +.fc .fc-toolbar-title { + font-size: 2em; + font-weight: 700; +} + +.fc-timegrid-slot + +.fc-timegrid-slot-lane + +.fc-timegrid-slot-minor { + display: none; + color: transparent; +} + +.fc-scrollgrid-liquid { + border-left: none!important; +} + +.fc-timegrid-slot-label { + border: none!important; + padding-right: 5px!important; +} + +col { + width: 100px!important; +} + +:root { + --fc-today-bg-color: rgba(87,70,157,0.10); + --fc-button-bg-color: rgb(87, 70, 157); + --fc-button-border-color: rgb(87, 70, 157); + --fc-button-hover-bg-color: rgb(71, 53, 147); + --fc-button-hover-border-color: rgb(71, 53, 147); + --fc-button-active-bg-color: rgb(62, 42, 153); + --fc-button-active-border-color: rgb(62, 42, 153); +} \ No newline at end of file diff --git a/frontend/src/components/pages/schedule/calendarView/ScheduleCalendar.tsx b/frontend/src/components/pages/schedule/calendarView/ScheduleCalendar.tsx new file mode 100644 index 0000000..23f7896 --- /dev/null +++ b/frontend/src/components/pages/schedule/calendarView/ScheduleCalendar.tsx @@ -0,0 +1,148 @@ +import FullCalendar from "@fullcalendar/react"; +import timeGridPlugin from "@fullcalendar/timegrid"; +import { DayHeaderContentArg, EventContentArg } from "@fullcalendar/core"; +import React, { + forwardRef, + useEffect, + useImperativeHandle, + useRef, +} from "react"; +import ModeCommentOutlinedIcon from "@mui/icons-material/ModeCommentOutlined"; +import "./ScheduleCalendar.css"; +import { ConnectingAirportsOutlined } from "@mui/icons-material"; + +const events = [ + { + title: "test", + start: new Date("2024-09-29T09:00:00"), + end: new Date("2024-09-29T10:00:00"), + allDay: true, + }, +]; + +function renderEventContent(eventInfo: EventContentArg) { + const isAllDay = eventInfo.event.allDay; + return ( +
+
+ {eventInfo.event.title} +
+ {isAllDay + ? "" + : `${new Date(eventInfo.event.start!).toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + })} - ${new Date(eventInfo.event.end!).toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + })}`} + +
+ ); +} + +function renderHeaderContent(date: DayHeaderContentArg) { + const dayOfWeek = date.date.toLocaleString("en-US", { weekday: "long" }); + const dayOfMonth = date.date.getDate(); + + return ( +
+ {dayOfWeek.substring(0, 3).toUpperCase()} +
+ {dayOfMonth} +
+ ); +} + +interface ScheduleCalendarProps { + setDateRange: (range: string) => void; +} + +interface ScheduleCalendarHandle { + next: () => void; + prev: () => void; +} + +const ScheduleCalendar = forwardRef< + ScheduleCalendarHandle, + ScheduleCalendarProps +>((props, ref) => { + const calendarRef = useRef(null); + + const handleAllDayContent = (arg: any) => { + return {arg.text ? "" : ""}; + }; + + const formatDateRange = (startDate: Date, endDate: Date) => { + const startFormat = startDate.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }); + const endFormat = endDate.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }); + + if (startDate.getMonth() === endDate.getMonth()) { + return `${startFormat} - ${endFormat.split(" ")[1]}`; + } + return `${startFormat} - ${endFormat}`; + }; + + useImperativeHandle(ref, () => ({ + next: () => { + if (calendarRef.current) calendarRef.current.getApi().next(); + }, + prev: () => { + if (calendarRef.current) calendarRef.current.getApi().prev(); + }, + })); + + return ( +
+ { + props.setDateRange(formatDateRange(dateInfo.start, dateInfo.end)); + }} + headerToolbar={false} + /> +
+ ); +}); + +ScheduleCalendar.displayName = "ScheduleCalendar"; + +export default ScheduleCalendar; diff --git a/frontend/src/components/pages/schedule/listView/ScheduleListView.tsx b/frontend/src/components/pages/schedule/listView/ScheduleListView.tsx new file mode 100644 index 0000000..c3e43e5 --- /dev/null +++ b/frontend/src/components/pages/schedule/listView/ScheduleListView.tsx @@ -0,0 +1,154 @@ +import React, { useEffect, useState } from "react"; +import { Flex, Tabs, TabList, Tab, Text } from "@chakra-ui/react"; + +import { tasksColumnTypes } from "./columnKeys"; + +import { + scheduleTasksMockData, + sundayScheduleTasksMockData, + mondayScheduleTasksMockData, + tuesdayScheduleTasksMockData, + wednesdayScheduleTasksMockData, + thursdayScheduleTasksMockData, + fridayScheduleTasksMockData, + saturdayScheduleTasksMockData, +} from "../../../../mocks/scheduletasks"; + +import ScheduleTable, { ColumnInfoTypes, TableData } from "./ScheduleTable"; + +const ScheduleListView = (): React.ReactElement => { + const enum Dates { + SUNDAY = "SUNDAY", + MONDAY = "MONDAY", + TUESDAY = "TUESDAY", + WEDNESDAY = "WEDNESDAY", + THURSDAY = "THURSDAY", + FRIDAY = "FRIDAY", + SATURDAY = "SATURDAY", + } + + const [taskData, setTaskData] = useState([]); + const [taskDataColumns, setTaskDataColumns] = useState([]); + const [taskDate, setTaskDate] = useState(Dates.SUNDAY); + const [dailyTaskData, setDailyTaskData] = useState([]); + + useEffect(() => { + setTaskDataColumns(tasksColumnTypes); + setTaskData(scheduleTasksMockData); + if (taskDate === Dates.SUNDAY) { + setDailyTaskData(sundayScheduleTasksMockData); + } else if (taskDate === Dates.MONDAY) { + setDailyTaskData(mondayScheduleTasksMockData); + } else if (taskDate === Dates.TUESDAY) { + setDailyTaskData(tuesdayScheduleTasksMockData); + } else if (taskDate === Dates.WEDNESDAY) { + setDailyTaskData(wednesdayScheduleTasksMockData); + } else if (taskDate === Dates.THURSDAY) { + setDailyTaskData(thursdayScheduleTasksMockData); + } else if (taskDate === Dates.FRIDAY) { + setDailyTaskData(fridayScheduleTasksMockData); + } else if (taskDate === Dates.SATURDAY) { + setDailyTaskData(saturdayScheduleTasksMockData); + } else { + setDailyTaskData(sundayScheduleTasksMockData); + } + }, [taskDate]); + + return ( + <> + + + Weekly Tasks + + {}} + isSelectable + /> + + + + Daily Tasks + + + + { + setTaskDate(Dates.SUNDAY); + }} + > + Sunday + + { + setTaskDate(Dates.MONDAY); + }} + > + Monday + + { + setTaskDate(Dates.TUESDAY); + }} + > + Tuesday + + { + setTaskDate(Dates.WEDNESDAY); + }} + > + Wednesday + + { + setTaskDate(Dates.THURSDAY); + }} + > + Thursday + + { + setTaskDate(Dates.FRIDAY); + }} + > + Friday + + { + setTaskDate(Dates.SATURDAY); + }} + > + Saturday + + + + {}} + isSelectable + /> + + + ); +}; + +export default ScheduleListView; diff --git a/frontend/src/components/pages/schedule/listView/ScheduleTable.tsx b/frontend/src/components/pages/schedule/listView/ScheduleTable.tsx new file mode 100644 index 0000000..e0d6da4 --- /dev/null +++ b/frontend/src/components/pages/schedule/listView/ScheduleTable.tsx @@ -0,0 +1,349 @@ +import React, { useState, useEffect } from "react"; +import { + Table, + Thead, + Tbody, + Tr, + Th, + Td, + TableContainer, + Checkbox, + Center, + Box, + Flex, + IconButton, + Icon, +} from "@chakra-ui/react"; +import ModeCommentOutlinedIcon from "@mui/icons-material/ModeCommentOutlined"; +import ChevronLeftOutlinedIcon from "@mui/icons-material/ChevronLeftOutlined"; +import ChevronRightOutlinedIcon from "@mui/icons-material/ChevronRightOutlined"; +import KeyboardArrowUpOutlinedIcon from "@mui/icons-material/KeyboardArrowUpOutlined"; +import KeyboardArrowDownOutlinedIcon from "@mui/icons-material/KeyboardArrowDownOutlined"; + +type TableTypes = string | number | boolean | Date; + +export type ColumnInfoTypes = { header: string; key: string }; + +export interface TableData { + [key: string]: TableTypes; +} + +type Props = { + data: TableData[]; + columnInfo: ColumnInfoTypes[]; + onEdit: (row: unknown) => unknown; + maxResults?: number; + isSelectable?: boolean; +}; + +type SortState = { + [key: string]: number; +}; + +const ScheduleTable = ({ + columnInfo, + data, + onEdit, + maxResults = 10, + isSelectable = false, +}: Props): React.ReactElement => { + const [checked, setChecked] = useState(data.map(() => false)); + const [page, setPage] = useState(1); + const [pageArray, setPageArray] = useState([]); + const [sortingColumn, setSortingColumn] = useState({}); + const [originalData, setOriginalData] = useState(data); + const [sortedData, setSortedData] = useState(data); + + useEffect(() => { + return Math.ceil(data.length / maxResults) >= 5 + ? setPageArray([1, 2, 3, 4, 5]) + : setPageArray( + Array.from( + { length: Math.ceil(data.length / maxResults) }, + (_, i) => i + 1, + ), + ); + }, [data, maxResults]); + + useEffect(() => { + setOriginalData(data); + setSortedData(data); + }, [data]); + + // sorting the columns by ascending and descending order based on column indicated + const sortColumn = (column: string) => { + const newSortingColumn: SortState = {}; + columnInfo.forEach((col) => { + newSortingColumn[col.key] = + col.key === column ? sortingColumn[column] : 0; + }); + + // increment column sorting state + sortingColumn[column] = sortingColumn[column] + ? sortingColumn[column] + 1 + : 1; + + // if at the end, go back to 0 + if (sortingColumn[column] === 3) { + setSortingColumn({ ...sortingColumn, [column]: 0 }); + setSortedData(originalData); + return; + } + setSortingColumn({ + ...newSortingColumn, + [column]: sortingColumn[column], + }); + + // apply sorting based on which sorting state the column's in + const sorted = [...originalData].sort((a, b) => { + if (sortingColumn[column] === 1) { + return a[column] > b[column] ? 1 : -1; + } + if (sortingColumn[column] === 2) { + return a[column] < b[column] ? 1 : -1; + } + return 0; + }); + setSortedData(sorted); + }; + + // constants for pagination UI + const checkedPage = checked.slice((page - 1) * maxResults, page * maxResults); + const allChecked = checkedPage.every(Boolean); + const isIndeterminate = checkedPage.some(Boolean) && !allChecked; + + // pagination functions + const leftPaginate = () => { + if (page > 1) setPage(page - 1); + if (pageArray[0] > 1 && pageArray.length === 5) { + setPageArray(pageArray.map((item) => item - 1)); + } + }; + + const rightPaginate = () => { + if (page < Math.ceil(data.length / maxResults)) setPage(page + 1); + if ( + pageArray[pageArray.length - 1] < Math.ceil(data.length / maxResults) && + pageArray.length === 5 + ) { + setPageArray(pageArray.map((item) => item + 1)); + } + }; + + const numberPaginate = (n: number) => { + setPage(n); + // Sets n as the center of the page array when possible. + if ( + n - 2 >= 1 && + n + 2 <= Math.ceil(data.length / maxResults) && + pageArray.length === 5 + ) { + setPageArray([n - 2, n - 1, n, n + 1, n + 2]); + } + }; + + return ( + + + + + + {isSelectable ? ( + + ) : null} + {columnInfo.map((header, index) => ( + + ))} + + + + {sortedData + .slice((page - 1) * maxResults, page * maxResults) + .map((row, index) => { + return ( + + {isSelectable ? ( + + ) : null} + {columnInfo.map((column, i) => { + const getColor = (status: string) => { + if (status === "Completed") return "#0D8312"; + if (status === "Excused") return "#B07D18"; + if (status === "Incomplete") return "#B21D2F"; + return "black"; + }; + + const getBoxColor = (status: string) => { + if (status === "Completed") return "#CDEECE"; + if (status === "Excused") return "#FFE5B2"; + if (status === "Incomplete") return "#F8D7DB"; + return "black"; + }; + + return column.key === "status" ? ( + + ) : ( + + ); + })} + {row.status !== "Incomplete" && ( + + )} + + ); + })} + +
+ {null} + + + {header.header} + + { + sortColumn(header.key); + }} + /> + { + sortColumn(header.key); + }} + /> + + + +
+ { + const newChecked = [...checked]; + newChecked[index + (page - 1) * maxResults] = + e.target.checked; + setChecked(newChecked); + }} + /> + + + {row[column.key] ? ( + {String(row[column.key])} + ) : ( + "" + )} + + + {row[column.key] ? String(row[column.key]) : ""} + onEdit(row)}> + +
+
+ + + + {`Showing ${(page - 1) * maxResults + 1} to ${Math.min( + page * maxResults, + data.length, + )} of ${data.length} entries`} + + + + } + onClick={() => leftPaginate()} + /> + {pageArray.map((item, index) => { + return ( +
numberPaginate(item)} + key={index} + > + {item} +
+ ); + })} + } + onClick={() => rightPaginate()} + /> +
+
+
+
+ ); +}; + +export default ScheduleTable; diff --git a/frontend/src/components/pages/schedule/listView/columnKeys.ts b/frontend/src/components/pages/schedule/listView/columnKeys.ts new file mode 100644 index 0000000..665de89 --- /dev/null +++ b/frontend/src/components/pages/schedule/listView/columnKeys.ts @@ -0,0 +1,22 @@ +import { ColumnInfoTypes } from "../../../common/CommonTable"; + +export const tasksColumnTypes: ColumnInfoTypes[] = [ + { + header: " Name", + key: "title", + }, + { + header: "Status", + key: "status", + }, + { + header: "Time", + key: "time", + }, + { + header: "Marillac Bucks", + key: "creditValue", + }, +]; + +export default {}; diff --git a/frontend/src/components/pages/tasks/TaskModal.tsx b/frontend/src/components/pages/tasks/TaskModal.tsx index 0fa0c11..afa777f 100644 --- a/frontend/src/components/pages/tasks/TaskModal.tsx +++ b/frontend/src/components/pages/tasks/TaskModal.tsx @@ -1,23 +1,38 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import { - Text, Button, - Input, Select, Flex, FormControl, FormLabel, - Checkbox, + RadioGroup, + Radio, } from "@chakra-ui/react"; - +import colors from "../../../theme/colors"; import ModalContainer from "../../common/ModalContainer"; import FormField from "../../common/FormField"; +import { + TaskType, + Task, + CustomTask, + ChoreTask, +} from "../../../types/TaskTypes"; +import { + TaskRequest, + TaskTypeEnum, + RecurrenceFrequency, + DaysOfWeek, +} from "../../../APIClients/Types/TaskType"; type Props = { isOpen: boolean; setIsOpen: React.Dispatch>; + task: Task | null; + handleDeleteTask?: (taskId: string) => Promise; + handleSaveClick: (taskId: string, task: TaskRequest) => Promise; }; +// returns an array of times in 30 minute increments const generateOptions = () => { const options = []; for (let hour = 0; hour < 24; hour += 1) { @@ -40,117 +55,127 @@ const generateOptions = () => { const options = generateOptions(); -const DateInput = ({ - dueDate, - setDueDate, - dueTime, - setDueTime, - isAllDay, - setIsAllDay, - recurrenceFrequency, - setRecurrenceFrequency, - submitPressed, -}: { - dueDate: string; - setDueDate: React.Dispatch>; - dueTime: string; - setDueTime: React.Dispatch>; - isAllDay: boolean; - setIsAllDay: React.Dispatch>; - recurrenceFrequency: string; - setRecurrenceFrequency: React.Dispatch>; - submitPressed: boolean; -}) => { - return ( - - - - Due Date - - - - setDueDate(e.target.value)} - /> - - at - - - - - setIsAllDay(e.target.checked)} - m={0} - > - All Day - - - - - - +const TaskModal = ({ + isOpen, + setIsOpen, + task, + handleDeleteTask, + handleSaveClick, +}: Props): React.ReactElement => { + const [taskType, setTaskType] = useState("OPTIONAL"); + const [recurrence, setRecurrence] = useState("Does Not Repeat"); + const [marillacBucks, setMarillacBucks] = useState( + undefined, ); -}; + // const [comments, setComments] = useState(""); + const [selectedDays, setSelectedDays] = useState([]); + const days = ["Su", "M", "Tu", "W", "Th", "F", "Sa"]; -const TaskModal = ({ isOpen, setIsOpen }: Props): React.ReactElement => { const [title, setTitle] = useState(""); const [location, setLocation] = useState(""); const [dueDate, setDueDate] = useState(""); const [dueTime, setDueTime] = useState(""); - const [isAllDay, setIsAllDay] = useState(false); - const [recurrenceFrequency, setRecurrenceFrequency] = useState(""); - const [marillacBucks, setMarillacBucks] = useState(""); + const [completedOn, setCompletedOn] = useState("every"); + const [endsOn, setEndsOn] = useState("never"); + const [endsOnDate, setEndsOnDate] = useState(""); + // const [isAllDay, setIsAllDay] = useState(false); + // const [recurrenceFrequency, setRecurrenceFrequency] = useState(""); const [submitPressed, setSubmitPressed] = useState(false); + const [errorSubmitting, setError] = useState(""); + const isEditMode = !!task; + + const dayIdMap = [ + { key: "MONDAY", short: "M" }, + { key: "TUESDAY", short: "Tu" }, + { key: "WEDNESDAY", short: "W" }, + { key: "THURSDAY", short: "Th" }, + { key: "FRIDAY", short: "F" }, + { key: "SATURDAY", short: "Sa" }, + { key: "SUNDAY", short: "Su" }, + ]; + + useEffect(() => { + if (!isOpen) { + return; + } + if (task) { + setTaskType(task.type); + setRecurrence( + task.recurrenceFrequency === "ONE_TIME" ? "Does Not Repeat" : "Repeats", + ); + setMarillacBucks(task.creditValue); + + if (task.recurrenceFrequency === "ONE_TIME") { + const day = dayIdMap.find( + (dayMp) => dayMp.key === task.specificDay, + )?.short; + if (day) { + setSelectedDays([day]); + } + } else { + setSelectedDays( + task.repeatDays + .map((day) => dayIdMap.find((dayMp) => dayMp.key === day)?.short) + .filter((day): day is string => day !== undefined), + ); + } + + setTitle(task.title); + setDueDate(task.endDate ? task.endDate.toString() : ""); + setDueTime(""); + } else { + setTaskType("OPTIONAL"); + setRecurrence("Does Not Repeat"); + setMarillacBucks(undefined); + setSelectedDays([]); + setTitle(""); + setDueDate(""); + setDueTime(""); + } + }, [task, isOpen]); const handleSubmit = () => { setSubmitPressed(true); - if (!title || !location || !dueDate || !dueTime || !marillacBucks) { - // TODO: Add error handling + if (title === "") { + console.log("Title is required"); + return; } + // TODO: API call to add task + let recurrenceFrequency = "ONE_TIME"; + if (recurrence === "Repeats") { + if (completedOn === "every") { + recurrenceFrequency = "REPEATS_PER_WEEK_SELECTED"; + } else { + recurrenceFrequency = "REPEATS_PER_WEEK_ONCE"; + } + + if (selectedDays.length === 0) { + console.log("Days are required"); + return; + } + } + + const taskRequest: TaskRequest = { + type: taskType as TaskTypeEnum, + title, + description: task?.description || "No field in modal", + creditValue: marillacBucks || 0, + locationId: task?.locationId || 1234, // no field in modal + endDate: endsOn === "never" ? undefined : new Date(endsOnDate), + recurrenceFrequency: recurrenceFrequency as RecurrenceFrequency, + repeatDays: + recurrenceFrequency === "ONE_TIME" + ? [] + : selectedDays.map( + (day) => + dayIdMap.find((dayMp) => dayMp.short === day) + ?.key as DaysOfWeek, + ), + }; + handleSaveClick(task?.id || "", taskRequest); + setIsOpen(false); }; const resetFormState = () => { @@ -158,80 +183,188 @@ const TaskModal = ({ isOpen, setIsOpen }: Props): React.ReactElement => { setLocation(""); setDueDate(""); setDueTime(""); - setIsAllDay(false); - setRecurrenceFrequency(""); - setMarillacBucks(""); + // setIsAllDay(false); + // setRecurrenceFrequency(""); + setMarillacBucks(undefined); setSubmitPressed(false); }; const handleMoneyInput = () => { - const inputValue = marillacBucks.replace(/[^0-9.]/g, ""); // Remove non-numeric and non-period characters + // const inputValue = marillacBucks.replace(/[^0-9.]/g, ""); // Remove non-numeric and non-period characters + // if (inputValue) { + // const numberValue = parseFloat(inputValue).toFixed(2); + // setMarillacBucks(numberValue); + // } + }; - if (inputValue) { - const numberValue = parseFloat(inputValue).toFixed(2); - setMarillacBucks(numberValue); + // delete task api stuff + const handleDelete = () => { + if (handleDeleteTask && task) { + handleDeleteTask(task.id); + setIsOpen(false); + resetFormState(); } }; - // delete task api stuff - const handleDelete = () => {}; + const selectDay = (day: string) => { + if (selectedDays.includes(day)) { + setSelectedDays(selectedDays.filter((d) => d !== day)); + } else { + setSelectedDays([...selectedDays, day]); + } + }; return ( + + + Task Type + + + + + setTitle(e.target.value)} submitPressed={submitPressed} - required /> - - + - Location + Recurrence - + {recurrence === "Repeats" && ( + <> + +
Select Days:
+ + {days.map((day, i) => ( + + ))} +
+ + +
Completed On
+ + setCompletedOn(value)} + colorScheme="purple" + style={{ flexDirection: "column", display: "flex" }} + > + Every Selected Day + One of the selected days + +
+ + +
Ends On
+ setEndsOn(value)} + colorScheme="purple" + style={{ flexDirection: "column", display: "flex" }} + > + + Never + + + + On + { + setEndsOnDate(e.target.value); + }} + submitPressed={submitPressed} + /> + + + +
+ + )} setMarillacBucks(e.target.value)} + value={marillacBucks || ""} + type="number" + onChange={(e) => { + if (e.target.value) { + setMarillacBucks(parseInt(e.target.value, 10)); + } else if (e.target.value === "") { + setMarillacBucks(undefined); + } + }} onBlur={handleMoneyInput} submitPressed={submitPressed} leftElement="$" - required /> - { - setIsModalOpen(true); - }} + {loading || error ? ( +

Loading...

+ ) : ( + { + setModalTask(row); + setIsModalOpen(true); + }} + /> + )} + -
); diff --git a/frontend/src/components/pages/tasks/columnKeys.ts b/frontend/src/components/pages/tasks/columnKeys.ts index b7f7a1f..dca7c4f 100644 --- a/frontend/src/components/pages/tasks/columnKeys.ts +++ b/frontend/src/components/pages/tasks/columnKeys.ts @@ -7,7 +7,7 @@ export const tasksColumnTypes: ColumnInfoTypes[] = [ }, { header: "Due Date", - key: "dueDate", + key: "endDate", }, { header: "Marillac Bucks", @@ -26,7 +26,7 @@ export const customTasksColumnTypes: ColumnInfoTypes[] = [ }, { header: "Due Date", - key: "dueDate", + key: "endDate", }, { header: "Marillac Bucks", @@ -45,7 +45,7 @@ export const choreTasksColumnTypes: ColumnInfoTypes[] = [ }, { header: "Due Date", - key: "dueDate", + key: "endDate", }, { header: "Marillac Bucks", diff --git a/frontend/src/mocks/scheduletasks.ts b/frontend/src/mocks/scheduletasks.ts new file mode 100644 index 0000000..342e73a --- /dev/null +++ b/frontend/src/mocks/scheduletasks.ts @@ -0,0 +1,169 @@ +export const scheduleTasksMockData = [ + { + title: "Weekly Review with CM", + status: "Completed", + time: "All day", + creditValue: "$5.00", + }, + { + title: "Skills Session with PM", + status: "Completed", + time: "All day", + creditValue: "$5.00", + }, + { + title: "Housing Plan", + status: "Excused", + time: "All day", + creditValue: "$5.00", + }, +]; + +export const sundayScheduleTasksMockData = [ + { + title: "Weekly Review with CM", + status: "Completed", + time: "All day", + creditValue: "$5.00", + }, + { + title: "Skills Session with PM", + status: "Completed", + time: "All day", + creditValue: "$5.00", + }, + { + title: "Housing Plan", + status: "Excused", + time: "All day", + creditValue: "$5.00", + }, +]; + +export const mondayScheduleTasksMockData = [ + { + title: "Weekly Review with CM", + status: "Excused", + time: "All day", + creditValue: "$10.00", + }, + { + title: "Skills Session with PM", + status: "Incomplete", + time: "All day", + creditValue: "$5.00", + }, + { + title: "Housing Plan", + status: "Excused", + time: "All day", + creditValue: "$5.00", + }, +]; + +export const tuesdayScheduleTasksMockData = [ + { + title: "Weekly Review with CM1", + status: "Completed", + time: "All day", + creditValue: "$5.00", + }, + { + title: "Skills Session with PM", + status: "Completed", + time: "All day", + creditValue: "$5.00", + }, + { + title: "Housing Plan", + status: "Excused", + time: "All day", + creditValue: "$5.00", + }, +]; + +export const wednesdayScheduleTasksMockData = [ + { + title: "Weekly Review with CM2", + status: "Completed", + time: "All day", + creditValue: "$5.00", + }, + { + title: "Skills Session with PM", + status: "Completed", + time: "All day", + creditValue: "$5.00", + }, + { + title: "Housing Plan", + status: "Excused", + time: "All day", + creditValue: "$5.00", + }, +]; + +export const thursdayScheduleTasksMockData = [ + { + title: "Weekly Review with CM3", + status: "Completed", + time: "All day", + creditValue: "$5.00", + }, + { + title: "Skills Session with PM", + status: "Completed", + time: "All day", + creditValue: "$5.00", + }, + { + title: "Housing Plan", + status: "Excused", + time: "All day", + creditValue: "$5.00", + }, +]; + +export const fridayScheduleTasksMockData = [ + { + title: "Weekly Review with CM4", + status: "Completed", + time: "All day", + creditValue: "$5.00", + }, + { + title: "Skills Session with PM", + status: "Completed", + time: "All day", + creditValue: "$5.00", + }, + { + title: "Housing Plan", + status: "Excused", + time: "All day", + creditValue: "$5.00", + }, +]; + +export const saturdayScheduleTasksMockData = [ + { + title: "Weekly Review with CM5", + status: "Completed", + time: "All day", + creditValue: "$5.00", + }, + { + title: "Skills Session with PM", + status: "Completed", + time: "All day", + creditValue: "$5.00", + }, + { + title: "Housing Plan", + status: "Excused", + time: "All day", + creditValue: "$5.00", + }, +]; + +export default {}; diff --git a/frontend/src/mocks/tasks.ts b/frontend/src/mocks/tasks.ts index 57aa31c..a17ad2b 100644 --- a/frontend/src/mocks/tasks.ts +++ b/frontend/src/mocks/tasks.ts @@ -1,152 +1,304 @@ +import { RecurrenceFrequency } from "../APIClients/Types/TaskType"; + export const requiredTasksMockData = [ { + id: "3", + type: "REQUIRED", title: "Weekly Review with CM", dueDate: "Every Friday", - creditValue: "$5.00", + creditValue: 5, + locationId: 1234, + recurrenceFrequency: "REPEATS_PER_WEEK_SELECTED", + specificDay: "", + repeatDays: ["MONDAY", "TUESDAY", "WEDNESDAY"], }, { + id: "34989383", + type: "REQUIRED", title: "Skills Session with PM", dueDate: "Every Friday", - creditValue: "$5.00", + creditValue: 5, + locationId: 1, + recurrenceFrequency: "ONE_TIME", + specificDay: "MONDAY", + repeatDays: [], }, { + id: "fhoudsahfo", + type: "REQUIRED", title: "Housing Plan", dueDate: "Every Friday", - creditValue: "$5.00", + creditValue: 5, + locationId: 1, + recurrenceFrequency: "ONE_TIME", + specificDay: "MONDAY", + repeatDays: [], }, ]; export const optionalTasksMockData = [ { + id: "xvzcpivpcxz", + type: "OPTIONAL", title: "Weekly House Meeting", dueDate: "Every Monday at 12:00pm", - creditValue: "$5.00", + creditValue: 5, + locationId: 1, + recurrenceFrequency: "ONE_TIME", + specificDay: "MONDAY", + repeatDays: [], }, { + id: "xcovzjxi", + type: "OPTIONAL", title: "Curfew", dueDate: "Everyday at 10:00pm", - creditValue: "$5.00", + creditValue: 5, + locationId: 1, + recurrenceFrequency: "ONE_TIME", + specificDay: "MONDAY", + repeatDays: [], }, { + id: "voixzczc", + type: "OPTIONAL", title: "Bedroom Inspection", dueDate: "Everyday at 10:00pm", - creditValue: "$2.00", + creditValue: 2, + locationId: 1, + recurrenceFrequency: "ONE_TIME", + specificDay: "MONDAY", + repeatDays: [], }, { + id: "43rt43rew", + type: "OPTIONAL", title: "Speaker's Corner", dueDate: "Everyday at 10:00pm", - creditValue: "$10.00", + creditValue: 10, + locationId: 1, + recurrenceFrequency: "ONE_TIME", + specificDay: "MONDAY", + repeatDays: [], }, { + id: "cxzvsd", + type: "OPTIONAL", title: "Counselling", dueDate: "Every Friday", - creditValue: "$10.00", + creditValue: 10, + locationId: 1, + recurrenceFrequency: "ONE_TIME", + specificDay: "MONDAY", + repeatDays: [], }, { + id: "vxz,cxvcxz", + type: "OPTIONAL", title: "Community Program/School", dueDate: "Everyday at 2:00pm", - creditValue: "$10.00", + creditValue: 10, + locationId: 1, + recurrenceFrequency: "ONE_TIME", + specificDay: "MONDAY", + repeatDays: [], }, ]; export const customTasksMockData = [ { + id: "oipuvxuczp", + type: "CUSTOM", title: "Room 1's custom task", roomNumber: 1, dueDate: "Every Monday at 10:00am", - creditValue: "$5.00", + creditValue: 5, + locationId: 1, + recurrenceFrequency: "ONE_TIME", + specificDay: "MONDAY", + repeatDays: [], }, { + id: "jijiljvcxz", + type: "CUSTOM", title: "Room 2's custom task", roomNumber: 2, dueDate: "Every Tuesday at 11:00am", - creditValue: "$5.00", + creditValue: 5, + locationId: 1, + recurrenceFrequency: "ONE_TIME", + specificDay: "MONDAY", + repeatDays: [], }, { + id: "asdasldasda", + type: "CUSTOM", title: "Room 3's custom task", roomNumber: 3, dueDate: "Every Wednesday at 12:00pm", - creditValue: "$5.00", + creditValue: 5, + locationId: 1, + recurrenceFrequency: "ONE_TIME", + specificDay: "MONDAY", + repeatDays: [], }, { + id: "xzcvcxzvxczvcx", + type: "CUSTOM", title: "Room 4's custom task", roomNumber: 4, dueDate: "Every Thursday at 1:00pm", - creditValue: "$5.00", + creditValue: 5, + locationId: 1, + recurrenceFrequency: "ONE_TIME", + specificDay: "MONDAY", + repeatDays: [], }, { + id: "qweoqweiuwoqe", + type: "CUSTOM", title: "Room 5's custom task", roomNumber: 5, dueDate: "Every Friday at 2:00pm", - creditValue: "$5.00", + creditValue: 5, + locationId: 1, + recurrenceFrequency: "ONE_TIME", + specificDay: "MONDAY", + repeatDays: [], }, { + id: "sdffjdsafjldsaflsad", + type: "CUSTOM", title: "Room 6's custom task", roomNumber: 6, dueDate: "Every Saturday at 3:00pm", - creditValue: "$5.00", + creditValue: 5, + locationId: 1, + recurrenceFrequency: "ONE_TIME", + specificDay: "MONDAY", + repeatDays: [], }, { + id: "31491331r1", + type: "CUSTOM", title: "Room 7's custom task", roomNumber: 7, dueDate: "Every Sunday at 4:00pm", - creditValue: "$5.00", + creditValue: 5, + locationId: 1, + recurrenceFrequency: "ONE_TIME", + specificDay: "MONDAY", + repeatDays: [], }, { + id: "safodsafhusda", + type: "CUSTOM", title: "Room 8's custom task", roomNumber: 8, dueDate: "Everyday at 5:00pm", - creditValue: "$5.00", + creditValue: 5, + locationId: 1, + recurrenceFrequency: "ONE_TIME", + specificDay: "MONDAY", + repeatDays: [], }, { + id: "Sfhosadfuhdsaf", + type: "CUSTOM", title: "Room 9's custom task", roomNumber: 9, dueDate: "Everyday at 6:00pm", - creditValue: "$5.00", + creditValue: 5, + locationId: 1, + recurrenceFrequency: "ONE_TIME", + specificDay: "MONDAY", + repeatDays: [], }, { + id: "asdfosaudfhsdaofuhsdaf", + type: "CUSTOM", title: "Room 10's custom task", roomNumber: 10, dueDate: "Everyday at 7:00pm", - creditValue: "$5.00", + creditValue: 5, + locationId: 1, + recurrenceFrequency: "ONE_TIME", + specificDay: "MONDAY", + repeatDays: [], }, ]; export const choreTasksMockData = [ { + id: "aosdfasdfsda", + type: "CHORE", title: "Wash the dishes", location: "Kitchen", dueDate: "Everyday at 11:00am", - creditValue: "$5.00", + creditValue: 5, + locationId: 1, + recurrenceFrequency: "ONE_TIME", + specificDay: "MONDAY", + repeatDays: [], }, { + id: "ofhosduafsda", + type: "CHORE", title: "Clean the bathroom", location: "Bathroom", dueDate: "Everyday at 11:00am", - creditValue: "$5.00", + creditValue: 5, + locationId: 1, + recurrenceFrequency: "ONE_TIME", + specificDay: "MONDAY", + repeatDays: [], }, { + id: "avsuhoudvs", + type: "CHORE", title: "Mop the floors", location: "Living Room", dueDate: "Everyday at 11:00am", - creditValue: "$5.00", + creditValue: 5, + locationId: 1, + recurrenceFrequency: "ONE_TIME", + specificDay: "MONDAY", + repeatDays: [], }, { + id: "sdfdsaf", + type: "CHORE", title: "Take out the trash", location: "Kitchen", dueDate: "Everyday at 11:00am", - creditValue: "$5.00", + creditValue: 5, + locationId: 1, + recurrenceFrequency: "ONE_TIME", + specificDay: "MONDAY", + repeatDays: [], }, { + id: "asofusad", + type: "CHORE", title: "Vacuum the carpets", location: "Living Room", dueDate: "Everyday at 11:00am", - creditValue: "$5.00", + creditValue: 5, + locationId: 1, + recurrenceFrequency: "ONE_TIME", + specificDay: "MONDAY", + repeatDays: [], }, { + id: "1892419413", + type: "CHORE", title: "Dust the furniture", location: "Living Room", dueDate: "Everyday at 11:00am", - creditValue: "$5.00", + creditValue: 5, + locationId: 1, + recurrenceFrequency: "ONE_TIME", + specificDay: "MONDAY", + repeatDays: [], }, ]; diff --git a/frontend/src/types/TaskTypes.ts b/frontend/src/types/TaskTypes.ts index 24a5d4a..527415f 100644 --- a/frontend/src/types/TaskTypes.ts +++ b/frontend/src/types/TaskTypes.ts @@ -1,9 +1,28 @@ 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 interface Task { + id: string; + type: TaskType; title: string; description: string; creditValue: number; + locationId: number; + endDate?: Date; + recurrenceFrequency: RecurrenceFrequency; + specificDay?: DaysOfWeek; + repeatDays: DaysOfWeek[]; } export interface CustomTask extends Task { diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 7ff0043..e2de8c8 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2285,31 +2285,31 @@ "@floating-ui/dom" "^1.6.1" "@floating-ui/utils@^0.2.0", "@floating-ui/utils@^0.2.1": - "integrity" "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" - "resolved" "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz" - "version" "0.2.1" + version "0.2.1" + resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.1.tgz#16308cea045f0fc777b6ff20a9f25474dd8293d2" + integrity sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q== -"@fullcalendar/core@^6.1.15", "@fullcalendar/core@~6.1.15": - "integrity" "sha512-BuX7o6ALpLb84cMw1FCB9/cSgF4JbVO894cjJZ6kP74jzbUZNjtwffwRdA+Id8rrLjT30d/7TrkW90k4zbXB5Q==" - "resolved" "https://registry.npmjs.org/@fullcalendar/core/-/core-6.1.15.tgz" - "version" "6.1.15" +"@fullcalendar/core@^6.1.15": + version "6.1.15" + resolved "https://registry.yarnpkg.com/@fullcalendar/core/-/core-6.1.15.tgz#6c3f5259fc4589870228853072131219bb533f6e" + integrity sha512-BuX7o6ALpLb84cMw1FCB9/cSgF4JbVO894cjJZ6kP74jzbUZNjtwffwRdA+Id8rrLjT30d/7TrkW90k4zbXB5Q== dependencies: - "preact" "~10.12.1" + preact "~10.12.1" "@fullcalendar/daygrid@^6.1.15", "@fullcalendar/daygrid@~6.1.15": - "integrity" "sha512-j8tL0HhfiVsdtOCLfzK2J0RtSkiad3BYYemwQKq512cx6btz6ZZ2RNc/hVnIxluuWFyvx5sXZwoeTJsFSFTEFA==" - "resolved" "https://registry.npmjs.org/@fullcalendar/daygrid/-/daygrid-6.1.15.tgz" - "version" "6.1.15" + version "6.1.15" + resolved "https://registry.yarnpkg.com/@fullcalendar/daygrid/-/daygrid-6.1.15.tgz#91208b0955ba805ddad285a53ee6f53855146963" + integrity sha512-j8tL0HhfiVsdtOCLfzK2J0RtSkiad3BYYemwQKq512cx6btz6ZZ2RNc/hVnIxluuWFyvx5sXZwoeTJsFSFTEFA== "@fullcalendar/react@^6.1.15": - "integrity" "sha512-L0b9hybS2J4e7lq6G2CD4nqriyLEqOH1tE8iI6JQjAMTVh5JicOo5Mqw+fhU5bJ7hLfMw2K3fksxX3Ul1ssw5w==" - "resolved" "https://registry.npmjs.org/@fullcalendar/react/-/react-6.1.15.tgz" - "version" "6.1.15" + version "6.1.15" + resolved "https://registry.yarnpkg.com/@fullcalendar/react/-/react-6.1.15.tgz#3198b4a64e256afd37c9760c8741a9af89ade894" + integrity sha512-L0b9hybS2J4e7lq6G2CD4nqriyLEqOH1tE8iI6JQjAMTVh5JicOo5Mqw+fhU5bJ7hLfMw2K3fksxX3Ul1ssw5w== "@fullcalendar/timegrid@^6.1.15": - "integrity" "sha512-61ORr3A148RtxQ2FNG7JKvacyA/TEVZ7z6I+3E9Oeu3dqTf6M928bFcpehRTIK6zIA6Yifs7BeWHgOE9dFnpbw==" - "resolved" "https://registry.npmjs.org/@fullcalendar/timegrid/-/timegrid-6.1.15.tgz" - "version" "6.1.15" + version "6.1.15" + resolved "https://registry.yarnpkg.com/@fullcalendar/timegrid/-/timegrid-6.1.15.tgz#c4630b7c03c813065154c6e3981f8d51d9d692e5" + integrity sha512-61ORr3A148RtxQ2FNG7JKvacyA/TEVZ7z6I+3E9Oeu3dqTf6M928bFcpehRTIK6zIA6Yifs7BeWHgOE9dFnpbw== dependencies: "@fullcalendar/daygrid" "~6.1.15" @@ -9294,28 +9294,29 @@ "resolved" "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz" "version" "7.0.39" dependencies: - "picocolors" "^0.2.1" - "source-map" "^0.6.1" - -"preact@~10.12.1": - "integrity" "sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==" - "resolved" "https://registry.npmjs.org/preact/-/preact-10.12.1.tgz" - "version" "10.12.1" - -"prelude-ls@^1.2.1": - "integrity" "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==" - "resolved" "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" - "version" "1.2.1" - -"prelude-ls@~1.1.2": - "integrity" "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==" - "resolved" "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz" - "version" "1.1.2" - -"prettier-linter-helpers@^1.0.0": - "integrity" "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==" - "resolved" "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz" - "version" "1.0.0" + nanoid "^3.3.7" + picocolors "^1.0.0" + source-map-js "^1.0.2" + +preact@~10.12.1: + version "10.12.1" + resolved "https://registry.yarnpkg.com/preact/-/preact-10.12.1.tgz#8f9cb5442f560e532729b7d23d42fd1161354a21" + integrity sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg== + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w== + +prettier-linter-helpers@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" + integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w== dependencies: "fast-diff" "^1.1.2"