diff --git a/src/controllers/bmdashboard/__tests__/bmIssueController.test.js b/src/controllers/bmdashboard/__tests__/bmIssueController.test.js index 75d9b8b5f3..1545ff9d25 100644 --- a/src/controllers/bmdashboard/__tests__/bmIssueController.test.js +++ b/src/controllers/bmdashboard/__tests__/bmIssueController.test.js @@ -30,6 +30,20 @@ const mockBuildingIssue = { findByIdAndUpdate: jest.fn(), findByIdAndDelete: jest.fn(), }; +// Mocking the injuryIssue Model (used by getMostExpensiveIssues and injury issue CRUD) +const mockInjuryIssue = { + find: jest.fn(), + findById: jest.fn(), + findByIdAndUpdate: jest.fn(), + findByIdAndDelete: jest.fn(), + create: jest.fn(), +}; + +// Helper: builds the chained find mock used by getMostExpensiveIssues +const mockInjuryFindChain = (result) => ({ + select: jest.fn().mockReturnThis(), + lean: jest.fn().mockResolvedValue(result), +}); // Helper: call bmGetOpenIssue and return the find query that was used (for filter tests) async function getOpenIssueFindQuery(controller, req, res, findResult = []) { @@ -57,7 +71,7 @@ describe('Building Issue Controller', () => { let res; beforeEach(() => { - controller = bmIssueController(mockBuildingIssue); + controller = bmIssueController(mockBuildingIssue, mockInjuryIssue); req = { body: {}, @@ -796,8 +810,8 @@ describe('Building Issue Controller', () => { expect(result[0].durationOpen).toBeGreaterThan(0); }); - it('should filter by projects when provided', async () => { - req.query.projects = TEST_ISSUE_ID; + it('should filter by projectIds when provided', async () => { + req.query.projectIds = TEST_ISSUE_ID; mockBuildingIssue.find.mockReturnValue(mockFindChain([])); @@ -807,34 +821,34 @@ describe('Building Issue Controller', () => { expect(callArgs.projectId.$in).toContain(TEST_ISSUE_ID); }); - it('should filter by dates and return matching projects', async () => { - req.query.dates = '2022-01-01,2024-12-31'; - mockBuildingProjectFind.mockReturnValue(mockBuildingProjectChain([{ _id: TEST_ISSUE_ID }])); + it('should filter by startDate and endDate when provided', async () => { + req.query.startDate = '2022-01-01'; + req.query.endDate = '2024-12-31'; mockBuildingIssue.find.mockReturnValue(mockFindChain([])); await controller.getLongestOpenIssues(req, res); - expect(mockBuildingProjectFind).toHaveBeenCalled(); + const callArgs = mockBuildingIssue.find.mock.calls[0][0]; + expect(callArgs.issueDate.$gte).toBeInstanceOf(Date); + expect(callArgs.issueDate.$lte).toBeInstanceOf(Date); expect(res.json).toHaveBeenCalled(); }); - it('should return empty array when dates provided but no matching projects', async () => { req.query.dates = '2022-01-01,2024-12-31'; mockBuildingProjectFind.mockReturnValue(mockBuildingProjectChain([])); await controller.getLongestOpenIssues(req, res); expect(res.json).toHaveBeenCalledWith([]); }); - - it('should intersect project filters when both dates and projects provided', async () => { + it('should apply both projectIds and date filters when provided', async () => { const anotherProjectId = '507f1f77bcf86cd799439012'; - req.query.dates = '2022-01-01,2024-12-31'; - req.query.projects = `${TEST_ISSUE_ID},${anotherProjectId}`; - mockBuildingProjectFind.mockReturnValue(mockBuildingProjectChain([{ _id: TEST_ISSUE_ID }])); + req.query.startDate = '2022-01-01'; + req.query.endDate = '2024-12-31'; + req.query.projectIds = `${TEST_ISSUE_ID},${anotherProjectId}`; mockBuildingIssue.find.mockReturnValue(mockFindChain([])); await controller.getLongestOpenIssues(req, res); - expect(mockBuildingProjectFind).toHaveBeenCalled(); const callArgs = mockBuildingIssue.find.mock.calls[0][0]; expect(callArgs.projectId.$in).toContain(TEST_ISSUE_ID); + expect(callArgs.issueDate.$gte).toBeInstanceOf(Date); + expect(callArgs.issueDate.$lte).toBeInstanceOf(Date); }); - it('should return top 7 issues sorted by duration', async () => { const mockIssues = Array.from({ length: 10 }, (_, i) => ({ issueTitle: [`Issue ${i + 1}`], @@ -893,4 +907,371 @@ describe('Building Issue Controller', () => { expect(res.json).toHaveBeenCalledWith({ message: 'Error fetching longest open issues' }); }); }); + + // ==================== getMostExpensiveIssues Tests ==================== + describe('getMostExpensiveIssues', () => { + beforeEach(() => { + mockInjuryIssue.find.mockReturnValue(mockInjuryFindChain([])); + }); + + it('should fetch most expensive issues without filters', async () => { + const mockIssues = [ + { _id: '1', name: 'Cheap Issue', openDate: new Date('2024-01-01'), totalCost: 100 }, + { _id: '2', name: 'Expensive Issue', openDate: new Date('2024-01-01'), totalCost: 5000 }, + { _id: '3', name: 'Medium Issue', openDate: new Date('2024-01-01'), totalCost: 1000 }, + ]; + mockInjuryIssue.find.mockReturnValue(mockInjuryFindChain(mockIssues)); + + await controller.getMostExpensiveIssues(req, res); + + expect(mockInjuryIssue.find).toHaveBeenCalledWith({}); + expect(res.json).toHaveBeenCalled(); + const { data } = res.json.mock.calls[0][0]; + expect(data).toHaveLength(3); + // sorted descending by totalCost + expect(data[0].totalCost).toBe(5000); + expect(data[1].totalCost).toBe(1000); + expect(data[2].totalCost).toBe(100); + expect(data[0].title).toBe('Expensive Issue'); + expect(data[0].issueId).toBe('2'); + expect(typeof data[0].daysOpen).toBe('number'); + }); + + it('should filter by projectIds when provided', async () => { + const validId2 = '507f1f77bcf86cd799439012'; + req.query.projectIds = `${TEST_ISSUE_ID},${validId2}`; + mockInjuryIssue.find.mockReturnValue(mockInjuryFindChain([])); + + await controller.getMostExpensiveIssues(req, res); + + const callArgs = mockInjuryIssue.find.mock.calls[0][0]; + expect(callArgs.projectId.$in).toHaveLength(2); + expect(callArgs.projectId.$in).toContain(TEST_ISSUE_ID); + }); + + it('should not add projectId filter when projectIds is empty after trimming', async () => { + req.query.projectIds = ' , '; + mockInjuryIssue.find.mockReturnValue(mockInjuryFindChain([])); + + await controller.getMostExpensiveIssues(req, res); + + const callArgs = mockInjuryIssue.find.mock.calls[0][0]; + expect(callArgs.projectId).toBeUndefined(); + }); + + it('should filter by startDate and endDate when both provided', async () => { + req.query.startDate = '2024-01-01'; + req.query.endDate = '2024-12-31'; + mockInjuryIssue.find.mockReturnValue(mockInjuryFindChain([])); + + await controller.getMostExpensiveIssues(req, res); + + const callArgs = mockInjuryIssue.find.mock.calls[0][0]; + expect(callArgs.openDate.$gte).toBeInstanceOf(Date); + expect(callArgs.openDate.$lte).toBeInstanceOf(Date); + }); + + it('should filter by startDate only when endDate not provided', async () => { + req.query.startDate = '2024-01-01'; + mockInjuryIssue.find.mockReturnValue(mockInjuryFindChain([])); + + await controller.getMostExpensiveIssues(req, res); + + const callArgs = mockInjuryIssue.find.mock.calls[0][0]; + expect(callArgs.openDate.$gte).toBeInstanceOf(Date); + expect(callArgs.openDate.$lte).toBeUndefined(); + }); + + it('should filter by endDate only when startDate not provided', async () => { + req.query.endDate = '2024-12-31'; + mockInjuryIssue.find.mockReturnValue(mockInjuryFindChain([])); + + await controller.getMostExpensiveIssues(req, res); + + const callArgs = mockInjuryIssue.find.mock.calls[0][0]; + expect(callArgs.openDate.$lte).toBeInstanceOf(Date); + expect(callArgs.openDate.$gte).toBeUndefined(); + }); + + it('should apply both projectIds and date filters when provided', async () => { + req.query.projectIds = TEST_ISSUE_ID; + req.query.startDate = '2024-01-01'; + req.query.endDate = '2024-12-31'; + mockInjuryIssue.find.mockReturnValue(mockInjuryFindChain([])); + + await controller.getMostExpensiveIssues(req, res); + + const callArgs = mockInjuryIssue.find.mock.calls[0][0]; + expect(callArgs.projectId.$in).toContain(TEST_ISSUE_ID); + expect(callArgs.openDate.$gte).toBeInstanceOf(Date); + expect(callArgs.openDate.$lte).toBeInstanceOf(Date); + }); + + it('should exclude issues with null or undefined totalCost', async () => { + const mockIssues = [ + { _id: '1', name: 'No Cost', openDate: new Date('2024-01-01'), totalCost: null }, + { _id: '2', name: 'Undefined Cost', openDate: new Date('2024-01-01') }, + { _id: '3', name: 'Has Cost', openDate: new Date('2024-01-01'), totalCost: 500 }, + ]; + mockInjuryIssue.find.mockReturnValue(mockInjuryFindChain(mockIssues)); + + await controller.getMostExpensiveIssues(req, res); + + const { data } = res.json.mock.calls[0][0]; + expect(data).toHaveLength(1); + expect(data[0].title).toBe('Has Cost'); + }); + + it('should limit results to top 5 issues', async () => { + const mockIssues = Array.from({ length: 10 }, (_, i) => ({ + _id: `${i}`, + name: `Issue ${i}`, + openDate: new Date('2024-01-01'), + totalCost: (i + 1) * 100, + })); + mockInjuryIssue.find.mockReturnValue(mockInjuryFindChain(mockIssues)); + + await controller.getMostExpensiveIssues(req, res); + + const { data } = res.json.mock.calls[0][0]; + expect(data).toHaveLength(5); + // highest costs first + expect(data[0].totalCost).toBe(1000); + expect(data[4].totalCost).toBe(600); + }); + + it('should return empty data array when no issues found', async () => { + mockInjuryIssue.find.mockReturnValue(mockInjuryFindChain([])); + + await controller.getMostExpensiveIssues(req, res); + + expect(res.json).toHaveBeenCalledWith({ data: [] }); + }); + + it('should return 500 error when database error occurs', async () => { + const error = new Error('Database error'); + mockInjuryIssue.find.mockReturnValue({ + select: jest.fn().mockReturnThis(), + lean: jest.fn().mockRejectedValue(error), + }); + + await controller.getMostExpensiveIssues(req, res); + + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ message: 'Error fetching most expensive issues' }); + }); +}); + + // ==================== bmPostInjuryIssue Tests ==================== + describe('bmPostInjuryIssue', () => { + it('should create a new injury issue successfully', async () => { + const mockIssue = { + _id: TEST_ISSUE_ID, + projectId: TEST_ISSUE_ID, + name: 'Injury Issue', + openDate: new Date('2024-01-01'), + category: 'Safety', + assignedTo: TEST_ISSUE_ID, + totalCost: 500, + }; + req.body = { ...mockIssue }; + mockInjuryIssue.create.mockResolvedValue(mockIssue); + + await controller.bmPostInjuryIssue(req, res); + + expect(mockInjuryIssue.create).toHaveBeenCalledWith( + expect.objectContaining({ name: 'Injury Issue', totalCost: 500 }), + ); + expect(res.status).toHaveBeenCalledWith(201); + expect(res.json).toHaveBeenCalledWith(mockIssue); + }); + + it('should return 400 when creation fails', async () => { + req.body = { name: 'Bad Issue' }; + mockInjuryIssue.create.mockRejectedValue(new Error('Validation failed')); + + await controller.bmPostInjuryIssue(req, res); + + expect(res.status).toHaveBeenCalledWith(400); + expect(res.json).toHaveBeenCalledWith({ error: 'Validation failed' }); + }); + }); + + // ==================== bmGetInjuryIssue Tests ==================== + describe('bmGetInjuryIssue', () => { + it('should fetch all injury issues successfully', async () => { + const mockIssues = [{ _id: TEST_ISSUE_ID, name: 'Injury 1' }]; + mockInjuryIssue.find.mockReturnValue({ + populate: jest.fn().mockResolvedValue(mockIssues), + }); + + await controller.bmGetInjuryIssue(req, res); + + expect(mockInjuryIssue.find).toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith(mockIssues); + }); + + it('should return 500 when database error occurs', async () => { + mockInjuryIssue.find.mockReturnValue({ + populate: jest.fn().mockRejectedValue(new Error('Database error')), + }); + + await controller.bmGetInjuryIssue(req, res); + + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ error: 'Database error' }); + }); + }); + + // ==================== bmDeleteInjuryIssue Tests ==================== + describe('bmDeleteInjuryIssue', () => { + it('should delete an injury issue successfully', async () => { + req.params.id = TEST_ISSUE_ID; + const mockDeleted = { _id: TEST_ISSUE_ID }; + mockInjuryIssue.findByIdAndDelete.mockResolvedValue(mockDeleted); + + await controller.bmDeleteInjuryIssue(req, res); + + expect(mockInjuryIssue.findByIdAndDelete).toHaveBeenCalledWith(TEST_ISSUE_ID); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith({ + message: 'Deleted successfully', + deleted: mockDeleted, + }); + }); + + it('should return 404 when issue not found', async () => { + req.params.id = TEST_ISSUE_ID; + mockInjuryIssue.findByIdAndDelete.mockResolvedValue(null); + + await controller.bmDeleteInjuryIssue(req, res); + + expect(res.status).toHaveBeenCalledWith(404); + expect(res.json).toHaveBeenCalledWith({ message: 'Issue not found' }); + }); + + it('should return 500 when database error occurs', async () => { + req.params.id = TEST_ISSUE_ID; + mockInjuryIssue.findByIdAndDelete.mockRejectedValue(new Error('Database error')); + + await controller.bmDeleteInjuryIssue(req, res); + + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ error: 'Database error' }); + }); + }); + + // ==================== bmRenameInjuryIssue Tests ==================== + describe('bmRenameInjuryIssue', () => { + it('should rename an injury issue successfully', async () => { + req.params.id = TEST_ISSUE_ID; + req.body = { newName: 'Renamed Issue' }; + const mockUpdated = { _id: TEST_ISSUE_ID, name: 'Renamed Issue' }; + mockInjuryIssue.findByIdAndUpdate.mockResolvedValue(mockUpdated); + + await controller.bmRenameInjuryIssue(req, res); + + expect(mockInjuryIssue.findByIdAndUpdate).toHaveBeenCalledWith( + TEST_ISSUE_ID, + { name: 'Renamed Issue' }, + { new: true, runValidators: true }, + ); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith({ + message: 'Renamed successfully', + updated: mockUpdated, + }); + }); + + it('should return 400 when newName is not provided', async () => { + req.params.id = TEST_ISSUE_ID; + req.body = {}; + + await controller.bmRenameInjuryIssue(req, res); + + expect(res.status).toHaveBeenCalledWith(400); + expect(res.json).toHaveBeenCalledWith({ message: 'newName is required' }); + expect(mockInjuryIssue.findByIdAndUpdate).not.toHaveBeenCalled(); + }); + + it('should return 404 when issue not found', async () => { + req.params.id = TEST_ISSUE_ID; + req.body = { newName: 'New Name' }; + mockInjuryIssue.findByIdAndUpdate.mockResolvedValue(null); + + await controller.bmRenameInjuryIssue(req, res); + + expect(res.status).toHaveBeenCalledWith(404); + expect(res.json).toHaveBeenCalledWith({ message: 'Issue not found' }); + }); + + it('should return 500 when database error occurs', async () => { + req.params.id = TEST_ISSUE_ID; + req.body = { newName: 'New Name' }; + mockInjuryIssue.findByIdAndUpdate.mockRejectedValue(new Error('Database error')); + + await controller.bmRenameInjuryIssue(req, res); + + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ error: 'Database error' }); + }); + }); + + // ==================== bmCopyInjuryIssue Tests ==================== + describe('bmCopyInjuryIssue', () => { + it('should copy an injury issue successfully', async () => { + req.params.id = TEST_ISSUE_ID; + const mockOriginal = { + _id: TEST_ISSUE_ID, + projectId: 'project1', + name: 'Original Issue', + category: 'Safety', + assignedTo: 'user1', + totalCost: 500, + }; + const mockCopy = { _id: '507f1f77bcf86cd799439099', name: 'Original Issue (Copy)' }; + mockInjuryIssue.findById.mockReturnValue({ + lean: jest.fn().mockResolvedValue(mockOriginal), + }); + mockInjuryIssue.create.mockResolvedValue(mockCopy); + + await controller.bmCopyInjuryIssue(req, res); + + expect(mockInjuryIssue.findById).toHaveBeenCalledWith(TEST_ISSUE_ID); + expect(mockInjuryIssue.create).toHaveBeenCalledWith( + expect.objectContaining({ name: 'Original Issue (Copy)', totalCost: 500 }), + ); + expect(res.status).toHaveBeenCalledWith(201); + expect(res.json).toHaveBeenCalledWith({ + message: 'Copied successfully', + copy: mockCopy, + }); + }); + + it('should return 404 when original issue not found', async () => { + req.params.id = TEST_ISSUE_ID; + mockInjuryIssue.findById.mockReturnValue({ + lean: jest.fn().mockResolvedValue(null), + }); + + await controller.bmCopyInjuryIssue(req, res); + + expect(res.status).toHaveBeenCalledWith(404); + expect(res.json).toHaveBeenCalledWith({ message: 'Issue not found' }); + expect(mockInjuryIssue.create).not.toHaveBeenCalled(); + }); + + it('should return 500 when database error occurs', async () => { + req.params.id = TEST_ISSUE_ID; + mockInjuryIssue.findById.mockReturnValue({ + lean: jest.fn().mockRejectedValue(new Error('Database error')), + }); + + await controller.bmCopyInjuryIssue(req, res); + + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ error: 'Database error' }); + }); + }); }); diff --git a/src/controllers/bmdashboard/bmIssueController.js b/src/controllers/bmdashboard/bmIssueController.js index 707644a8f2..e9e3506028 100644 --- a/src/controllers/bmdashboard/bmIssueController.js +++ b/src/controllers/bmdashboard/bmIssueController.js @@ -502,18 +502,21 @@ const bmIssueController = function (BuildingIssue, injuryIssue) { /* -------------------- LONGEST OPEN ISSUES -------------------- */ const getLongestOpenIssues = async (req, res) => { try { - const { dates, projects } = req.query; + const { projectIds, startDate, endDate } = req.query; const query = { status: 'open' }; - let filteredProjectIds = getProjectFilterIds(projects); - filteredProjectIds = await filterProjectIdsByDates(dates, filteredProjectIds); - - if (dates && filteredProjectIds.length === 0) { - return res.json([]); + if (projectIds) { + const ids = projectIds + .split(',') + .map((id) => id.trim()) + .filter(Boolean); + if (ids.length > 0) query.projectId = { $in: ids }; } - if (filteredProjectIds.length) { - query.projectId = { $in: filteredProjectIds }; + if (startDate || endDate) { + query.issueDate = {}; + if (startDate) query.issueDate.$gte = new Date(startDate); + if (endDate) query.issueDate.$lte = new Date(endDate); } const issues = await BuildingIssue.find(query) @@ -524,9 +527,49 @@ const bmIssueController = function (BuildingIssue, injuryIssue) { const grouped = buildGroupedIssues(issues); const response = buildLongestOpenResponse(grouped); - res.json(response); + return res.json(response); + } catch (error) { + return res.status(500).json({ message: 'Error fetching longest open issues' }); + } + }; + + /* -------------------- MOST EXPENSIVE ISSUES -------------------- */ + const getMostExpensiveIssues = async (req, res) => { + try { + const { projectIds, startDate, endDate } = req.query; + const query = {}; + + if (projectIds) { + const ids = projectIds + .split(',') + .map((id) => id.trim()) + .filter(Boolean); + if (ids.length > 0) query.projectId = { $in: ids }; + } + + if (startDate || endDate) { + query.openDate = {}; + if (startDate) query.openDate.$gte = new Date(startDate); + if (endDate) query.openDate.$lte = new Date(endDate); + } + + const today = new Date(); + const issues = await injuryIssue.find(query).select('name openDate totalCost').lean(); + + const result = issues + .filter((issue) => issue.totalCost != null) + .map((issue) => ({ + issueId: issue._id, + title: issue.name, + totalCost: issue.totalCost, + daysOpen: Math.floor((today - new Date(issue.openDate)) / (1000 * 60 * 60 * 24)), + })) + .sort((a, b) => b.totalCost - a.totalCost) + .slice(0, 5); + + return res.json({ data: result }); } catch (error) { - res.status(500).json({ message: 'Error fetching longest open issues' }); + return res.status(500).json({ message: 'Error fetching most expensive issues' }); } }; @@ -538,6 +581,7 @@ const bmIssueController = function (BuildingIssue, injuryIssue) { bmDeleteIssue, bmGetIssueChart, getLongestOpenIssues, + getMostExpensiveIssues, getUniqueProjectIds, bmPostInjuryIssue, bmGetInjuryIssue, @@ -547,4 +591,4 @@ const bmIssueController = function (BuildingIssue, injuryIssue) { }; }; -module.exports = bmIssueController; +module.exports = bmIssueController; \ No newline at end of file diff --git a/src/helpers/overviewReportHelper.js b/src/helpers/overviewReportHelper.js index 339161f1ed..74b677d991 100644 --- a/src/helpers/overviewReportHelper.js +++ b/src/helpers/overviewReportHelper.js @@ -503,13 +503,8 @@ const overviewReportHelper = function () { const startStr = moment(start).format('YYYY-MM-DD'); const endStr = moment(end).format('YYYY-MM-DD'); - console.log(`\n[getTotalActiveTeamCount] ========== START ==========`); - console.log(`[getTotalActiveTeamCount] Processing date range: ${startStr} to ${endStr}`); - console.log(`[getTotalActiveTeamCount] Input dates - start: ${start}, end: ${end}`); - // Step 1: Get all active teams const activeTeamsCount = await Team.countDocuments({ isActive: true }); - console.log(`[getTotalActiveTeamCount] Total active teams (no filter): ${activeTeamsCount}`); const result = await Team.aggregate([ // Step 1: Match active teams created before/on the end date @@ -564,10 +559,6 @@ const overviewReportHelper = function () { ]); const activeTeamsWithHours = result[0]?.activeTeams || 0; - console.log( - `[getTotalActiveTeamCount] Teams with logged hours in range ${startStr} to ${endStr}: ${activeTeamsWithHours}`, - ); - console.log(`[getTotalActiveTeamCount] ========== END ==========\n`); return activeTeamsWithHours; }; @@ -897,12 +888,6 @@ const overviewReportHelper = function () { const getData = async (endDate) => { const baseFilters = { isActive: true, - weeklycommittedHours: { - $gte: 1, - }, - role: { - $ne: 'Mentor', - }, }; if (endDate) { @@ -1161,80 +1146,6 @@ const overviewReportHelper = function () { }; } - /** - * Get the volunteer hours stats, it retrieves the number of hours logged by users between the two input dates as well as their weeklycommittedHours. - * @param {*} startDate - * @param {*} endDate - */ - // async function getHoursStats(startDate, endDate) { - // const hoursStats = await UserProfile.aggregate([ - // { - // $match: { - // isActive: true, - // }, - // }, - // { - // $lookup: { - // from: 'timeEntries', // The collection to join - // localField: '_id', // Field from the userProfile collection - // foreignField: 'personId', // Field from the timeEntries collection - // as: 'timeEntries', // The array field that will contain the joined documents - // }, - // }, - // { - // $unwind: { - // path: '$timeEntries', - // preserveNullAndEmptyArrays: true, // Preserve users with no time entries - // }, - // }, - // { - // $match: { - // $or: [ - // { timeEntries: { $exists: false } }, - // { - // 'timeEntries.dateOfWork': { - // $gte: moment(startDate).format('YYYY-MM-DD'), - // $lte: moment(endDate).format('YYYY-MM-DD'), - // }, - // }, - // ], - // }, - // }, - // { - // $group: { - // _id: '$_id', - // personId: { $first: '$_id' }, - // totalSeconds: { $sum: '$timeEntries.totalSeconds' }, // Sum seconds from timeEntries - // weeklycommittedHours: { $first: `$weeklycommittedHours` }, // Include the weeklycommittedHours field - // }, - // }, - // { - // $project: { - // totalHours: { $divide: ['$totalSeconds', 3600] }, // Convert seconds to hours - // weeklycommittedHours: 1, // make sure we include it in the end result - // }, - // }, - // { - // $bucket: { - // groupBy: '$totalHours', - // boundaries: [0, 10, 20, 30, 40], - // default: 40, - // output: { - // count: { $sum: 1 }, - // }, - // }, - // }, - // ]); - - // for (let i = 0; i < 5; i++) { - // if (!hoursStats.find((x) => x._id === i * 10)) { - // hoursStats.push({ _id: i * 10, count: 0 }); - // } - // } - - // return hoursStats; - // } - /** * Helper function to get user-level hours data for internal use * This is used by getVolunteerHoursStats for percentage calculations @@ -2545,12 +2456,16 @@ const overviewReportHelper = function () { isoComparisonStartDate, isoComparisonEndDate, ) { - // Helper function to get count of volunteers meeting their commitment. const getCompletedHoursData = async (start, end) => { + const startStr = moment(start).tz('America/Los_Angeles').format('YYYY-MM-DD'); + const endStr = moment(end).tz('America/Los_Angeles').format('YYYY-MM-DD'); + const hoursStats = await UserProfile.aggregate([ { $match: { isActive: true, + weeklycommittedHours: { $gte: 1 }, + role: { $ne: 'Mentor' }, }, }, { @@ -2572,12 +2487,9 @@ const overviewReportHelper = function () { as: 'entry', cond: { $and: [ - { - $gte: ['$$entry.dateOfWork', moment(start).format('YYYY-MM-DD')], - }, - { - $lte: ['$$entry.dateOfWork', moment(end).format('YYYY-MM-DD')], - }, + { $gte: ['$$entry.dateOfWork', startStr] }, + { $lte: ['$$entry.dateOfWork', endStr] }, + { $not: [{ $in: ['$$entry.entryType', ['person', 'team', 'project']] }] }, ], }, }, @@ -2591,7 +2503,6 @@ const overviewReportHelper = function () { }, { $project: { - name: { $concat: ['$firstName', ' ', '$lastName'] }, totalHours: { $divide: ['$totalSeconds', 3600] }, weeklycommittedHours: 1, metCommitment: { @@ -2604,13 +2515,14 @@ const overviewReportHelper = function () { }, }, { - $match: { weeklycommittedHours: { $gte: 1 } }, + $match: { metCommitment: true }, + }, + { + $count: 'metCommitmentCount', }, ]); - const metCommitment = hoursStats.filter((user) => user.metCommitment); - const metCommitmentCount = metCommitment.length; - return metCommitmentCount; + return hoursStats[0]?.metCommitmentCount || 0; }; const currentCount = await getCompletedHoursData(isoStartDate, isoEndDate); @@ -2637,61 +2549,52 @@ const overviewReportHelper = function () { comparisonStartDate, comparisonEndDate, ) => { - // Helper function to count summaries submitted within a date range - const getSummariesCount = async (start, end) => - UserProfile.aggregate([ + const getSummariesCount = async (start, end) => { + const startDateObj = new Date(moment(start).tz('America/Los_Angeles').startOf('day').toISOString()); + const endDateObj = new Date(moment(end).tz('America/Los_Angeles').endOf('day').toISOString()); + + const result = await UserProfile.aggregate([ { - // Stage 1: Match users who have weeklySummaries array $match: { - weeklySummaries: { $exists: true, $ne: [] }, - }, + $or: [ + { weeklySummaries: { $exists: true, $ne: [] } }, + { summaries: { $exists: true, $ne: [] } } + ] + } }, { - // Stage 2: Project only the summaries that meet ALL criteria - $project: { - validSummaries: { - $filter: { - input: '$weeklySummaries', - as: 'summary', - cond: { - $and: [ - // Condition 1: Summary content is not empty - { $ne: ['$$summary.summary', ''] }, - { $ne: ['$$summary.summary', null] }, - // Condition 2: uploadDate field exists - { $ne: ['$$summary.uploadDate', null] }, - // Condition 3: uploadDate is within the date range - { $gte: ['$$summary.uploadDate', new Date(start)] }, - { $lte: ['$$summary.uploadDate', new Date(end)] }, - ], - }, - }, - }, - }, + $addFields: { + allSummaries: { $concatArrays: [{ $ifNull: ['$weeklySummaries', []] }, { $ifNull: ['$summaries', []] }] } + } }, + { $unwind: '$allSummaries' }, { - // Stage 3: Count the valid summaries for each user - $project: { - summaryCount: { $size: '$validSummaries' }, - }, + $addFields: { + summaryParsedDate: { + $cond: { + if: { $ne: ['$allSummaries.uploadDate', null] }, + then: { $toDate: '$allSummaries.uploadDate' }, + else: { $toDate: '$allSummaries.date' } + } + } + } }, { - // Stage 4: Sum across all users - $group: { - _id: null, - totalSummaries: { $sum: '$summaryCount' }, - }, + $match: { + 'allSummaries.summary': { $exists: true, $nin: ['', null] }, + summaryParsedDate: { $gte: startDateObj, $lte: endDateObj } + } }, + { $count: 'totalSummaries' } ]); - // Get summaries count for the current date range - const currentSummaries = await getSummariesCount(startDate, endDate); - const totalCurrentSummaries = currentSummaries[0]?.totalSummaries || 0; + return result[0]?.totalSummaries || 0; + }; + + const totalCurrentSummaries = await getSummariesCount(startDate, endDate); - // If comparison dates are provided, calculate the comparison percentage if (comparisonStartDate && comparisonEndDate) { - const comparisonSummaries = await getSummariesCount(comparisonStartDate, comparisonEndDate); - const totalComparisonSummaries = comparisonSummaries[0]?.totalSummaries || 0; + const totalComparisonSummaries = await getSummariesCount(comparisonStartDate, comparisonEndDate); const comparisonPercentage = calculateGrowthPercentage( totalCurrentSummaries, totalComparisonSummaries, @@ -2700,7 +2603,6 @@ const overviewReportHelper = function () { return { count: totalCurrentSummaries, comparisonPercentage }; } - // If no comparison dates, return only the count return { count: totalCurrentSummaries }; }; @@ -2715,6 +2617,7 @@ const overviewReportHelper = function () { getTasksStats, getWorkDistributionStats, getTotalHoursWorked, + getCodeStats: getHoursStats, getHoursStats, getFourPlusMembersTeamCount, getTotalBadgesAwardedCount, @@ -2734,4 +2637,4 @@ const overviewReportHelper = function () { }; }; -module.exports = overviewReportHelper; +module.exports = overviewReportHelper; \ No newline at end of file diff --git a/src/models/bmdashboard/injuryIssue.js b/src/models/bmdashboard/injuryIssue.js index b24b4f41b5..dee5dd4e8b 100644 --- a/src/models/bmdashboard/injuryIssue.js +++ b/src/models/bmdashboard/injuryIssue.js @@ -3,7 +3,7 @@ const mongoose = require('mongoose'); const { Schema } = mongoose; const injuryIssue = new Schema({ - projectId: { type: mongoose.SchemaTypes.ObjectId, ref: 'Project', required: true }, + projectId: { type: mongoose.SchemaTypes.ObjectId, ref: 'buildingProject', required: true }, name: { type: String, required: true }, openDate: { type: Date, default: Date.now }, category: { type: String, required: true }, diff --git a/src/routes/bmdashboard/bmIssueRouter.js b/src/routes/bmdashboard/bmIssueRouter.js index 907a1a4911..2efbbfe4cb 100644 --- a/src/routes/bmdashboard/bmIssueRouter.js +++ b/src/routes/bmdashboard/bmIssueRouter.js @@ -13,6 +13,7 @@ const routes = function (buildingIssue, injuryIssue) { IssueRouter.route('/issues/add').post(controller.bmPostInjuryIssue); IssueRouter.route('/issues/list').get(controller.bmGetInjuryIssue); IssueRouter.route('/issues/longest-open').get(controller.getLongestOpenIssues); + IssueRouter.route('/issues/most-expensive').get(controller.getMostExpensiveIssues); IssueRouter.route('/issues/:id/rename').put(controller.bmRenameInjuryIssue); IssueRouter.route('/issues/:id/copy').post(controller.bmCopyInjuryIssue); IssueRouter.route('/issues/:id').delete(controller.bmDeleteInjuryIssue);