Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/controllers/bmdashboard/bmProjectController.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* 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');
// TODO: uncomment when executing auth checks
Expand Down
167 changes: 167 additions & 0 deletions src/controllers/bmdashboard/injuryCategoryController.js
Original file line number Diff line number Diff line change
Expand Up @@ -173,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' });
}
};
1 change: 0 additions & 1 deletion src/controllers/lbdashboard/bookingsController.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
const paypal = require('@paypal/checkout-server-sdk');

const nodemailer = require('nodemailer');
const Joi = require('joi');
require('dotenv').config();
Expand Down
12 changes: 12 additions & 0 deletions src/controllers/timeEntryController.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
5 changes: 5 additions & 0 deletions src/routes/bmdashboard/injuryCategoryRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,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;
Loading