diff --git a/package-lock.json b/package-lock.json index 45ac2a910..462cca3e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15885,7 +15885,6 @@ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", "hasInstallScript": true, - "license": "Apache-2.0", "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", 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/injuryCategoryController.js b/src/controllers/bmdashboard/injuryCategoryController.js index a56a54427..160fef23c 100644 --- a/src/controllers/bmdashboard/injuryCategoryController.js +++ b/src/controllers/bmdashboard/injuryCategoryController.js @@ -174,3 +174,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 = []; + 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 + 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 c2efd620d..1e17edc1d 100644 --- a/src/routes/bmdashboard/injuryCategoryRouter.js +++ b/src/routes/bmdashboard/injuryCategoryRouter.js @@ -7,6 +7,8 @@ const { getUniqueSeverities, getUniqueInjuryTypes, getProjectsWithInjuries, + getInjuryTrendData, + createInjuries, } = require('../../controllers/bmdashboard/injuryCategoryController'); const { getInjuryOverTime } = require('../../controllers/bmdashboard/injuryOverTimeController'); @@ -14,6 +16,9 @@ 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); router.get('/over-time', getInjuryOverTime);