From 7ee490c84eff2f862846e4e9ab6bdb1728aa4b5b Mon Sep 17 00:00:00 2001 From: Juhitha-Reddy Date: Fri, 12 Sep 2025 11:58:58 -0400 Subject: [PATCH 1/4] feat: implement injury trend chart APIs --- .../bmdashboard/injuryCategoryController.js | 218 ++++++++++++++++-- .../bmdashboard/injuryCategoryRouter.js | 6 + 2 files changed, 210 insertions(+), 14 deletions(-) diff --git a/src/controllers/bmdashboard/injuryCategoryController.js b/src/controllers/bmdashboard/injuryCategoryController.js index bd745ef3c..15932fee1 100644 --- a/src/controllers/bmdashboard/injuryCategoryController.js +++ b/src/controllers/bmdashboard/injuryCategoryController.js @@ -2,10 +2,14 @@ const mongoose = require('mongoose'); const InjuryCategory = require('../../models/bmdashboard/buildingInjury'); // ---------- helpers ---------- -const parseCSV = (s = '') => String(s).split(',').map(v => v.trim()).filter(Boolean); -const escapeRe = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +const parseCSV = (s = '') => + String(s) + .split(',') + .map((v) => v.trim()) + .filter(Boolean); +const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); -const parseYMD_UTC = s => { +const parseYmdUtc = (s) => { if (!s) return null; const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(s)); if (!m) return null; @@ -13,8 +17,8 @@ const parseYMD_UTC = s => { return new Date(Date.UTC(+y, +mo - 1, +d, 0, 0, 0, 0)); }; -const parseDateFlexibleUTC = s => { - const d1 = parseYMD_UTC(s); +const parseDateFlexibleUTC = (s) => { + const d1 = parseYmdUtc(s); if (d1) return d1; if (!s) return null; const d2 = new Date(s); @@ -23,10 +27,18 @@ const parseDateFlexibleUTC = s => { const parseObjectIdsCSV = (s = '') => parseCSV(s) - .filter(id => mongoose.Types.ObjectId.isValid(id)) - .map(id => new mongoose.Types.ObjectId(id)); + .filter((id) => mongoose.Types.ObjectId.isValid(id)) + .map((id) => new mongoose.Types.ObjectId(id)); -const buildMatch = ({ projectIds, startDate, endDate, severities, types, projectNames, projectName }) => { +const buildMatch = ({ + projectIds, + startDate, + endDate, + severities, + types, + projectNames, + projectName, +}) => { const match = {}; const ids = parseObjectIdsCSV(projectIds); @@ -38,7 +50,7 @@ const buildMatch = ({ projectIds, startDate, endDate, severities, types, project match.date = {}; if (s) match.date.$gte = s; if (e0) { - if (parseYMD_UTC(endDate)) { + if (parseYmdUtc(endDate)) { const endExclusive = new Date(e0); endExclusive.setUTCDate(endExclusive.getUTCDate() + 1); match.date.$lt = endExclusive; @@ -56,10 +68,11 @@ const buildMatch = ({ projectIds, startDate, endDate, severities, types, project const names = parseCSV(projectNames || projectName); if (names.length) { - match.$or = names.map(n => ({ projectName: { $regex: new RegExp(escapeRe(n), 'i') } })); + match.$or = names.map((n) => ({ projectName: { $regex: new RegExp(escapeRe(n), 'i') } })); } - const invalidDate = (startDate && !s && !parseYMD_UTC(startDate)) || (endDate && !e0 && !parseYMD_UTC(endDate)); + const invalidDate = + (startDate && !s && !parseYmdUtc(startDate)) || (endDate && !e0 && !parseYmdUtc(endDate)); return { match, invalidDate }; }; @@ -67,14 +80,21 @@ const buildMatch = ({ projectIds, startDate, endDate, severities, types, project exports.getCategoryBreakdown = async (req, res) => { try { const { match, invalidDate } = buildMatch(req.query); - if (invalidDate) return res.status(400).json({ error: 'Invalid startDate or endDate (use YYYY-MM-DD or ISO)' }); + if (invalidDate) + return res + .status(400) + .json({ error: 'Invalid startDate or endDate (use YYYY-MM-DD or ISO)' }); const results = await InjuryCategory.aggregate([ { $match: match }, { $addFields: { _nameTrim: { $trim: { input: { $ifNull: ['$projectName', ''] } } } } }, { $group: { - _id: { projectId: '$projectId', workerCategory: '$workerCategory', projectName: '$_nameTrim' }, + _id: { + projectId: '$projectId', + workerCategory: '$workerCategory', + projectName: '$_nameTrim', + }, totalInjuries: { $sum: { $ifNull: ['$count', 0] } }, }, }, @@ -120,7 +140,10 @@ exports.getUniqueInjuryTypes = async (_req, res) => { exports.getProjectsWithInjuries = async (req, res) => { try { const { match, invalidDate } = buildMatch(req.query); - if (invalidDate) return res.status(400).json({ error: 'Invalid startDate or endDate (use YYYY-MM-DD or ISO)' }); + if (invalidDate) + return res + .status(400) + .json({ error: 'Invalid startDate or endDate (use YYYY-MM-DD or ISO)' }); const projects = await InjuryCategory.aggregate([ { $match: match }, @@ -150,3 +173,170 @@ exports.getProjectsWithInjuries = async (req, res) => { res.status(500).json({ error: 'Internal server error' }); } }; + +// Returns monthly injury counts per severity between startDate and endDate for an optional projectId +// Response shape: +// { +// months: ['Jan', 'Feb', ...], +// serious: [..], +// medium: [..], +// low: [..] +// } +exports.getInjuryTrendData = async (req, res) => { + try { + const { projectId, startDate, endDate } = req.query || {}; + + // Build match using existing helpers for date parsing/validation + const { match, invalidDate } = buildMatch({ projectIds: projectId, startDate, endDate }); + if (invalidDate) + return res + .status(400) + .json({ error: 'Invalid startDate or endDate (use YYYY-MM-DD or ISO)' }); + + // Defaults: last 12 months if no range provided + const now = new Date(); + const defaultEnd = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1, 0, 0, 0, 0)); + const defaultStart = new Date( + Date.UTC(defaultEnd.getUTCFullYear(), defaultEnd.getUTCMonth() - 11, 1, 0, 0, 0, 0), + ); + + const start = match.date?.$gte || defaultStart; + const endExclusive = + match.date?.$lt || + new Date(Date.UTC(defaultEnd.getUTCFullYear(), defaultEnd.getUTCMonth() + 1, 1, 0, 0, 0, 0)); + + // Ensure match uses our computed range bounds + match.date = { $gte: start, $lt: endExclusive }; + + // Aggregate by year-month and severity + const agg = await InjuryCategory.aggregate([ + { $match: match }, + { + $group: { + _id: { + y: { $year: '$date' }, + m: { $month: '$date' }, + s: '$severity', + }, + c: { $sum: { $ifNull: ['$count', 0] } }, + }, + }, + { + $project: { + _id: 0, + year: '$_id.y', + month: '$_id.m', + severity: '$_id.s', + count: '$c', + }, + }, + { $sort: { year: 1, month: 1 } }, + ]).option({ allowDiskUse: true }); + + const monthNames = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec', + ]; + + // Build ordered list of months from start..endExclusive stepping by 1 month + const labels = []; + const monthKeys = []; + { + const d = new Date(start); + while (d < endExclusive) { + labels.push(monthNames[d.getUTCMonth()]); + monthKeys.push(`${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, '0')}`); + d.setUTCMonth(d.getUTCMonth() + 1); + } + } + + // Map of key(year-month)->severity->count + const map = new Map(); + agg.forEach((r) => { + const key = `${r.year}-${String(r.month).padStart(2, '0')}`; + if (!map.has(key)) map.set(key, {}); + const m2 = map.get(key); + const sev = String(r.severity || '').toLowerCase(); + m2[sev] = (m2[sev] || 0) + (Number(r.count) || 0); + }); + + const seriesSerious = []; + const seriesMedium = []; + const seriesLow = []; + monthKeys.forEach((k) => { + const entry = map.get(k) || {}; + seriesSerious.push(entry.serious || 0); + seriesMedium.push(entry.medium || 0); + seriesLow.push(entry.low || 0); + }); + + res + .status(200) + .json({ months: labels, serious: seriesSerious, medium: seriesMedium, low: seriesLow }); + } catch (err) { + console.error('[getInjuryTrendData] Error:', err); + res.status(500).json({ error: 'Internal server error' }); + } +}; + +// Create injury records (production) +exports.createInjuries = async (req, res) => { + try { + const body = Array.isArray(req.body) ? req.body : [req.body]; + if (!body.length) return res.status(400).json({ error: 'Empty payload' }); + + const allowedSeverity = new Map([ + ['serious', 'Serious'], + ['medium', 'Medium'], + ['low', 'Low'], + ]); + + const normalize = (x = {}) => { + const { projectId, projectName, date, injuryType, workerCategory, severity, count } = x; + + if (!projectId || !mongoose.Types.ObjectId.isValid(projectId)) { + throw new Error('projectId is required and must be a valid ObjectId'); + } + + const d = parseDateFlexibleUTC(date); + if (!d) throw new Error('Invalid or missing date (use YYYY-MM-DD or ISO)'); + + const sevNorm = allowedSeverity.get( + String(severity || '') + .trim() + .toLowerCase(), + ); + if (!sevNorm) throw new Error('severity must be one of: Serious | Medium | Low'); + + return { + projectId: new mongoose.Types.ObjectId(projectId), + projectName: projectName ? String(projectName) : undefined, + date: d, + injuryType: injuryType ? String(injuryType) : undefined, + workerCategory: workerCategory ? String(workerCategory) : undefined, + severity: sevNorm, + count: Number(count ?? 1), + }; + }; + + const docs = body.map(normalize); + const result = await InjuryCategory.insertMany(docs, { ordered: false }); + return res.status(201).json({ insertedCount: result.length, docs: result }); + } catch (err) { + const msg = err?.message || 'Failed to create injuries'; + console.error('[createInjuries] Error:', err); + // 400 for validation, 500 for others + if (/required|invalid|must be/i.test(msg)) return res.status(400).json({ error: msg }); + return res.status(500).json({ error: 'Internal server error' }); + } +}; diff --git a/src/routes/bmdashboard/injuryCategoryRouter.js b/src/routes/bmdashboard/injuryCategoryRouter.js index 410af7877..1a33844ff 100644 --- a/src/routes/bmdashboard/injuryCategoryRouter.js +++ b/src/routes/bmdashboard/injuryCategoryRouter.js @@ -1,4 +1,5 @@ const express = require('express'); + const router = express.Router(); const { @@ -6,11 +7,16 @@ const { getUniqueSeverities, getUniqueInjuryTypes, getProjectsWithInjuries, + getInjuryTrendData, + createInjuries, } = require('../../controllers/bmdashboard/injuryCategoryController'); router.get('/category-breakdown', getCategoryBreakdown); router.get('/injury-severities', getUniqueSeverities); router.get('/injury-types', getUniqueInjuryTypes); router.get('/project-injury', getProjectsWithInjuries); +router.get('/trend-data', getInjuryTrendData); +// Base path is '/api/bm/injuries' from startup/routes, so POST to '/api/bm/injuries' +router.post('/', createInjuries); module.exports = router; From 9492dc7dfe39514c85a90531b2d1ff4388ca7fa2 Mon Sep 17 00:00:00 2001 From: juhitha-reddy Date: Wed, 7 Jan 2026 20:04:17 -0500 Subject: [PATCH 2/4] unit test cases --- src/controllers/timeEntryController.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/controllers/timeEntryController.js b/src/controllers/timeEntryController.js index 2725b6388..7fee92952 100644 --- a/src/controllers/timeEntryController.js +++ b/src/controllers/timeEntryController.js @@ -1538,6 +1538,18 @@ const timeEntrycontroller = function (TimeEntry) { * recalculate the hoursByCategory for all users and update the field */ const recalculateHoursByCategoryAllUsers = async function (taskId) { + // Check if mongoose connection is ready before attempting session operations + // readyState: 0=disconnected, 1=connected, 2=connecting, 3=disconnecting + if (mongoose.connection.readyState !== 1) { + const recalculationTask = recalculationTaskQueue.find((task) => task.taskId === taskId); + if (recalculationTask) { + recalculationTask.status = 'Failed'; + recalculationTask.completionTime = new Date().toISOString(); + } + // Silently return - this is expected during test teardown + return; + } + const session = await mongoose.startSession(); session.startTransaction(); From 9cf8575ef42a7abf6bd7cb38e96a3e961b744b01 Mon Sep 17 00:00:00 2001 From: juhitha-reddy Date: Sun, 8 Mar 2026 20:37:56 -0400 Subject: [PATCH 3/4] Fixed coding standards issue --- .../bmdashboard/injuryCategoryController.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/controllers/bmdashboard/injuryCategoryController.js b/src/controllers/bmdashboard/injuryCategoryController.js index e63905266..160fef23c 100644 --- a/src/controllers/bmdashboard/injuryCategoryController.js +++ b/src/controllers/bmdashboard/injuryCategoryController.js @@ -252,13 +252,13 @@ exports.getInjuryTrendData = async (req, res) => { // Build ordered list of months from start..endExclusive stepping by 1 month const labels = []; const monthKeys = []; - { - const d = new Date(start); - while (d < endExclusive) { - labels.push(monthNames[d.getUTCMonth()]); - monthKeys.push(`${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, '0')}`); - d.setUTCMonth(d.getUTCMonth() + 1); - } + for ( + let d = new Date(start); + d.getTime() < endExclusive.getTime(); + d.setUTCMonth(d.getUTCMonth() + 1) + ) { + labels.push(monthNames[d.getUTCMonth()]); + monthKeys.push(`${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, '0')}`); } // Map of key(year-month)->severity->count From 466c1f8bb05927eddfea3cfa5f2d4674f07c0aa5 Mon Sep 17 00:00:00 2001 From: juhitha-reddy Date: Mon, 11 May 2026 19:25:13 -0400 Subject: [PATCH 4/4] Fixed lint issues --- src/app.js | 13 +++++-------- src/controllers/bmdashboard/bmProjectController.js | 1 - 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/app.js b/src/app.js index 1475d6446..ba8cba9ed 100644 --- a/src/app.js +++ b/src/app.js @@ -1,11 +1,13 @@ const express = require('express'); const Sentry = require('@sentry/node'); const testRoutes = require('./routes/testRoutes'); - -const app = express(); const logger = require('./startup/logger'); const globalErrorHandler = require('./utilities/errorHandling/globalErrorHandler'); -// const experienceRoutes = require('./routes/applicantAnalyticsRoutes'); +const helpFeedbackRouter = require('./routes/helpFeedbackRouter'); +const helpRequestRouter = require('./routes/helpRequestRouter'); +const weeklyReportsRouter = require('./routes/weeklyReportsRouter'); + +const app = express(); logger.init(); @@ -19,16 +21,11 @@ require('./startup/session')(app); // Add session before middleware and routes app.use('/api/test', testRoutes); -const helpFeedbackRouter = require('./routes/helpFeedbackRouter'); -const helpRequestRouter = require('./routes/helpRequestRouter'); - app.use('/api/feedback', helpFeedbackRouter); app.use('/api/helprequest', helpRequestRouter); require('./startup/middleware')(app); -const weeklyReportsRouter = require('./routes/weeklyReportsRouter'); - app.use('/api', weeklyReportsRouter); // ⚠ This must come *after* your custom /api routes diff --git a/src/controllers/bmdashboard/bmProjectController.js b/src/controllers/bmdashboard/bmProjectController.js index 9de0ebc03..62d8d983b 100644 --- a/src/controllers/bmdashboard/bmProjectController.js +++ b/src/controllers/bmdashboard/bmProjectController.js @@ -2,7 +2,6 @@ /* eslint-disable no-shadow */ /* eslint-disable prefer-destructuring */ const mongoose = require('mongoose'); -// const BuildingProject = require('../../models/bmdashboard/buildingProject'); const Task = require('../../models/task'); const BuildingProject = require('../../models/bmdashboard/buildingProject'); const BuildingIssue = require('../../models/bmdashboard/buildingIssue');