diff --git a/src/controllers/meetingController.js b/src/controllers/meetingController.js new file mode 100644 index 000000000..20af2ed7c --- /dev/null +++ b/src/controllers/meetingController.js @@ -0,0 +1,179 @@ +const moment = require('moment-timezone'); +const mongoose = require('mongoose'); +const logger = require('../startup/logger'); + +const UserProfile = require('../models/userProfile'); + +const meetingController = function (Meeting) { + const postMeeting = async function (req, res) { + const isInvalid = + !req.body.dateOfMeeting || + !moment(req.body.dateOfMeeting).isValid() || + req.body.startHour == null || + req.body.startMinute == null || + !req.body.startTimePeriod || + !['AM', 'PM'].includes(req.body.startTimePeriod) || + !req.body.duration || + !req.body.organizer || + !req.body.participantList || + req.body.participantList.length === 0 || + (req.body.location && !['Zoom', 'Phone call', 'On-site'].includes(req.body.location)); + + if (isInvalid) { + return res.status(400).send({ error: 'Bad request: Invalid form values' }); + } + + try { + await Promise.all( + req.body.participantList.map(async (userProfileId) => { + if (!mongoose.Types.ObjectId.isValid(userProfileId)) { + throw new Error('Invalid participant ID'); + } + const userProfileExists = await UserProfile.exists({ _id: userProfileId }); + if (!userProfileExists) { + throw new Error('Participant ID does not exist'); + } + }), + ); + if (!mongoose.Types.ObjectId.isValid(req.body.organizer)) { + throw new Error('Invalid organizer ID'); + } + const organizerExists = await UserProfile.exists({ _id: req.body.organizer }); + if (!organizerExists) { + throw new Error('Organizer ID does not exist'); + } + } catch (error) { + return res.status(400).send({ error: `Bad request: ${error.message}` }); + } + + const session = await mongoose.startSession(); + session.startTransaction(); + try { + const dateTimeString = `${req.body.dateOfMeeting} ${req.body.startHour}:${req.body.startMinute} ${req.body.startTimePeriod}`; + const dateTimeISO = moment(dateTimeString, 'YYYY-MM-DD hh:mm A').toISOString(); + + const meeting = new Meeting(); + meeting.dateTime = dateTimeISO; + meeting.duration = req.body.duration; + meeting.organizer = req.body.organizer; + meeting.participantList = req.body.participantList.map((participant) => ({ + participant, + notificationIsRead: false, + })); + meeting.location = req.body.location; + meeting.notes = req.body.notes; + + await meeting.save({ session }); + await session.commitTransaction(); + session.endSession(); + res.status(201).json({ message: 'Meeting saved successfully' }); + } catch (err) { + await session.abortTransaction(); + logger.logException(err); + return res.status(500).send({ error: err.toString() }); + } finally { + session.endSession(); + } + }; + + const getMeetings = async function (req, res) { + try { + const { startTime, endTime } = req.query; + const decodedStartTime = decodeURIComponent(startTime); + const decodedEndTime = decodeURIComponent(endTime); + + const meetings = await Meeting.aggregate([ + { + $match: { + dateTime: { + $gte: new Date(decodedStartTime), + $lte: new Date(decodedEndTime), + }, + }, + }, + { $unwind: '$participantList' }, + { + $project: { + _id: 1, + dateTime: 1, + duration: 1, + organizer: 1, + location: 1, + notes: 1, + recipient: '$participantList.participant', + isRead: '$participantList.notificationIsRead', + }, + }, + ]); + res.status(200).json(meetings); + } catch (error) { + console.error('Error fetching meetings:', error); + res.status(500).json({ error: 'Failed to fetch meetings' }); + } + }; + + const markMeetingAsRead = async function (req, res) { + try { + const { meetingId, recipient } = req.params; + const result = await Meeting.updateOne( + { _id: meetingId, 'participantList.participant': recipient }, + { $set: { 'participantList.$.notificationIsRead': true } }, + ); + if (result.nModified === 0) { + return res.status(404).json({ error: 'Meeting not found or already marked as read' }); + } + res.status(200).json({ message: 'Meeting marked as read successfully' }); + } catch (error) { + console.error('Error marking meeting as read:', error); + res.status(500).json({ error: 'Failed to mark meeting as read' }); + } + }; + + const getAllMeetingsByOrganizer = async function (req, res) { + try { + const { organizerId } = req.query; + if (!mongoose.Types.ObjectId.isValid(organizerId)) { + return res.status(400).json({ error: 'Invalid organizer userId' }); + } + const userProfileExists = await UserProfile.exists({ _id: organizerId }); + if (!userProfileExists) { + throw new Error('Organizer ID does not exist'); + } + + const currentTime = new Date(); + const meetings = await Meeting.aggregate([ + { + $match: { + dateTime: { $gt: currentTime }, + organizer: mongoose.Types.ObjectId(organizerId), + }, + }, + { + $project: { + _id: 1, + dateTime: 1, + duration: 1, + organizer: 1, + location: 1, + notes: 1, + participantList: 1, + }, + }, + ]); + + res.status(200).json(meetings); + } catch (error) { + console.error('Error fetching all upcoming meetings:', error); + res.status(500).json({ error: 'Failed to fetch all upcoming meetings' }); + } + }; + + return { + postMeeting, + getMeetings, + markMeetingAsRead, + getAllMeetingsByOrganizer, + }; +}; + +module.exports = meetingController; diff --git a/src/models/meeting.js b/src/models/meeting.js new file mode 100644 index 000000000..a55ff5911 --- /dev/null +++ b/src/models/meeting.js @@ -0,0 +1,17 @@ +const mongoose = require('mongoose'); + +const meetingSchema = new mongoose.Schema({ + dateTime: { type: Date, required: true }, + duration: { type: Number, required: true }, + organizer: { type: mongoose.Schema.Types.ObjectId, ref: 'userProfile', required: true }, + participantList: [ + { + participant: { type: mongoose.Schema.Types.ObjectId, ref: 'userProfile', required: true }, + notificationIsRead: { type: Boolean, default: false }, + }, + ], + location: { type: String }, + notes: { type: String }, +}); + +module.exports = mongoose.model('Meeting', meetingSchema); diff --git a/src/routes/meetingRouter.js b/src/routes/meetingRouter.js new file mode 100644 index 000000000..048ebf5e7 --- /dev/null +++ b/src/routes/meetingRouter.js @@ -0,0 +1,18 @@ +const express = require('express'); + +const routes = function (Meeting) { + const MeetingRouter = express.Router(); + + const controller = require('../controllers/meetingController')(Meeting); + + MeetingRouter.route('/meetings/new').post(controller.postMeeting); + MeetingRouter.route('/meetings').get(controller.getMeetings); + MeetingRouter.route('/meetings/markRead/:meetingId/:recipient').post( + controller.markMeetingAsRead, + ); + MeetingRouter.route('/meetings/upcoming/:organizerId').get(controller.getAllMeetingsByOrganizer); + + return MeetingRouter; +}; + +module.exports = routes; diff --git a/src/startup/routes.js b/src/startup/routes.js index b307ac4f4..ddcb140e9 100644 --- a/src/startup/routes.js +++ b/src/startup/routes.js @@ -45,6 +45,7 @@ const { } = require('../models/bmdashboard/buildingInventoryItem'); const timeOffRequest = require('../models/timeOffRequest'); const followUp = require('../models/followUp'); +const meeting = require('../models/meeting'); const userProfileRouter = require('../routes/userProfileRouter')(userProfile, project); const warningRouter = require('../routes/warningRouter')(userProfile); @@ -96,6 +97,7 @@ const timeOffRequestRouter = require('../routes/timeOffRequestRouter')( userProfile, ); const followUpRouter = require('../routes/followUpRouter')(followUp); +const meetingRouter = require('../routes/meetingRouter')(meeting); // bm dashboard const bmLoginRouter = require('../routes/bmdashboard/bmLoginRouter')(); @@ -162,6 +164,7 @@ module.exports = function (app) { app.use('/api', timeOffRequestRouter); app.use('/api', followUpRouter); app.use('/api', blueSquareEmailAssignmentRouter); + app.use('/api', meetingRouter); app.use('/api/jobs', jobsRouter) // bm dashboard app.use('/api/bm', bmLoginRouter); diff --git a/src/utilities/createInitialPermissions.js b/src/utilities/createInitialPermissions.js index 3a214bbec..825772a54 100644 --- a/src/utilities/createInitialPermissions.js +++ b/src/utilities/createInitialPermissions.js @@ -90,6 +90,8 @@ const permissionsRoles = [ 'seeUsersInDashboard', 'editTeamCode', + // Meeting + 'scheduleMeetings', ], }, { @@ -146,6 +148,7 @@ const permissionsRoles = [ 'postInvType', 'getTimeZoneAPIKey', 'checkLeadTeamOfXplus', + 'scheduleMeetings', ], }, { @@ -247,6 +250,7 @@ const permissionsRoles = [ 'checkLeadTeamOfXplus', 'editTeamCode', 'totalValidWeeklySummaries', + 'scheduleMeetings', // Title 'seeQSC',