From 942e670f981f6c626bbb6889009e09d07ed1e15d Mon Sep 17 00:00:00 2001 From: Nirali Date: Wed, 6 May 2026 22:01:07 -0400 Subject: [PATCH] feat: add volunteer hours reporting --- src/controllers/projectController.js | 68 ++++++++++++++++++++++++++++ src/controllers/teamController.js | 64 ++++++++++++++++++++++++++ src/routes/projectRouter.js | 2 + src/routes/teamRouter.js | 1 + 4 files changed, 135 insertions(+) diff --git a/src/controllers/projectController.js b/src/controllers/projectController.js index 4f2a92ddf..4c0cb24d1 100644 --- a/src/controllers/projectController.js +++ b/src/controllers/projectController.js @@ -27,6 +27,73 @@ const projectController = function (Project) { } }; + const getProjectsCommittedHours = async function (req, res) { + try { + const { fromDate, toDate } = req.body; + logger.logInfo( + `Fetching projects with committed hours. Date filter: ${JSON.stringify({ fromDate, toDate })}`, + ); + + const taskDateFilter = {}; + + if (fromDate && toDate) { + const start = new Date(fromDate); + + const end = new Date(toDate); + end.setHours(23, 59, 59, 999); + + taskDateFilter.$or = [ + { + startedDatetime: { $lte: end }, + dueDatetime: { $gte: start }, + }, + { + startedDatetime: { $gte: start, $lte: end }, + }, + { + dueDatetime: { $gte: start, $lte: end }, + }, + ]; + } + + logger.logInfo( + `Fetching projects with committed hours. Date filter: ${JSON.stringify(taskDateFilter)}`, + ); + const projects = await Project.find({ isArchived: { $ne: true } }, '_id projectName'); + + const result = await Promise.all( + projects.map(async (project) => { + const wbsList = await wbs.find({ projectId: project._id }, '_id'); + const wbsIds = wbsList.map((wbsItem) => wbsItem._id); + + const tasks = await task.find( + { + wbsId: { $in: wbsIds }, + ...taskDateFilter, + }, + 'estimatedHours', + ); + + const committedHours = tasks.reduce( + (total, taskItem) => total + Number(taskItem.estimatedHours || 0), + 0, + ); + + return { + projectId: project._id, + projectName: project.projectName, + committedHours, + }; + }), + ); + + res.status(200).send(result); + } catch (error) { + logger.logException(error); + res.status(500).send('Error fetching project committed hours.'); + } + }; + const getArchivedProjects = async function (req, res) { try { const archivedProjects = await Project.find( @@ -667,6 +734,7 @@ const projectController = function (Project) { getprojectMembershipSummary, searchProjectMembers, getProjectsWithActiveUserCounts, + getProjectsCommittedHours, }; }; diff --git a/src/controllers/teamController.js b/src/controllers/teamController.js index 8e7481986..84e8e98d3 100644 --- a/src/controllers/teamController.js +++ b/src/controllers/teamController.js @@ -6,8 +6,10 @@ const { hasPermission } = require('../utilities/permissions'); const cache = require('../utilities/nodeCache')(); const Logger = require('../startup/logger'); const helper = require('../utilities/permissions'); +const TimeEntry = require('../models/timeentry'); const INTERNAL_SERVER_ERROR = 'Internal server error'; +const moment = require('moment-timezone'); const teamcontroller = function (Team) { const getAllTeams = function (req, res) { @@ -81,6 +83,67 @@ const teamcontroller = function (Team) { }); }; + const getTeamsCommittedHours = async function (req, res) { + try { + const { fromDate, toDate } = req.body; + + const teams = await Team.find({ isActive: true }).select('_id teamName members').lean(); + Logger.logInfo( + `Calculating committed hours for ${teams.length} active teams between ${fromDate} and ${toDate}`, + ); + const result = await Promise.all( + teams.map(async (teamData) => { + const memberIds = teamData.members + ?.map((member) => member.userId || member._id) + .filter(Boolean); + + const hoursSummary = await TimeEntry.aggregate([ + { + $match: { + personId: { + $in: memberIds.map((id) => mongoose.Types.ObjectId(id)), + }, + dateOfWork: { + $gte: moment(fromDate).format('YYYY-MM-DD'), + $lte: moment(toDate).format('YYYY-MM-DD'), + }, + entryType: { $in: ['default', 'person', null] }, + }, + }, + { + $group: { + _id: '$personId', + totalHours: { + $sum: { $divide: ['$totalSeconds', 3600] }, + }, + }, + }, + ]); + + const committedHours = hoursSummary.reduce((total, user) => total + user.totalHours, 0); + + return { + teamId: teamData._id, + teamName: teamData.teamName, + committedHours: Math.round(committedHours * 10) / 10, + membersHours: hoursSummary.map((user) => ({ + userId: user._id, + totalHours: Math.round(user.totalHours * 10) / 10, + })), + }; + }), + ); + + return res.status(200).json(result); + } catch (error) { + Logger.logException(error); + return res.status(500).json({ + message: 'Error calculating team member hours', + error: error.message, + }); + } + }; + const getTeamById = function (req, res) { const { teamId } = req.params; @@ -573,6 +636,7 @@ const teamcontroller = function (Team) { updateTeamVisibility, getAllTeamMembers, getTeamMembersSkillsAndContact, + getTeamsCommittedHours, }; }; diff --git a/src/routes/projectRouter.js b/src/routes/projectRouter.js index 294e6a969..da03dad43 100644 --- a/src/routes/projectRouter.js +++ b/src/routes/projectRouter.js @@ -6,6 +6,8 @@ const routes = function (project) { projectRouter.route('/projects').get(controller.getAllProjects).post(controller.postProject); + projectRouter.route('/projects/committed-hours').post(controller.getProjectsCommittedHours); + projectRouter.route('/archivedProjects').get(controller.getArchivedProjects); projectRouter diff --git a/src/routes/teamRouter.js b/src/routes/teamRouter.js index ccb5e72d9..80df059de 100644 --- a/src/routes/teamRouter.js +++ b/src/routes/teamRouter.js @@ -11,6 +11,7 @@ const router = function (team) { .post(controller.postTeam) .put(controller.updateTeamVisibility); + teamRouter.route('/team/committed-hours').post(controller.getTeamsCommittedHours); teamRouter.route('/team/reports').post(controller.getAllTeamMembers); teamRouter