Spaces:
Runtime error
Runtime error
| const { body, param } = require('express-validator'); | |
| const dayjs = require('dayjs'); | |
| const { | |
| SafetyInduction, | |
| Employee, | |
| Vendor, | |
| SafetyBadge, | |
| User | |
| } = require('../models'); | |
| const { sendEmail, sendWhatsApp } = require('../services/notificationService'); | |
| const validate = require('../utils/validation'); | |
| const scheduleValidators = [ | |
| param('id').isMongoId().withMessage('valid employee id is required'), | |
| body('scheduled_date').isDate().withMessage('scheduled_date must be valid'), | |
| body('notes').optional().isString(), | |
| validate | |
| ]; | |
| const issueBadgeValidators = [ | |
| param('id').isMongoId().withMessage('valid employee id is required'), | |
| body('induction_date').isDate().withMessage('induction_date must be valid'), | |
| body('badge_no').notEmpty().withMessage('badge_no is required'), | |
| validate | |
| ]; | |
| const submitTestResultValidators = [ | |
| param('id').isMongoId().withMessage('valid induction id is required'), | |
| body('result').isIn(['Pass', 'Fail']).withMessage('result must be Pass or Fail'), | |
| body('remarks').optional({ nullable: true }).isLength({ max: 255 }).withMessage('remarks must be at most 255 characters'), | |
| validate | |
| ]; | |
| function computeExpiry(inductionDate) { | |
| const dt = new Date(inductionDate); | |
| dt.setDate(dt.getDate() + 365); | |
| return dt; | |
| } | |
| async function getCalendar(req, res, next) { | |
| try { | |
| const rows = await SafetyInduction.find({ test_status: 'Pending' }) | |
| .sort({ scheduled_date: 1 }) | |
| .populate({ path: 'employee_id', model: 'Employee', select: 'name vendor_id is_active' }) | |
| .lean(); | |
| const employeeIds = rows.map((row) => row.employee_id?._id).filter(Boolean); | |
| const badges = await SafetyBadge.find({ employee_id: { $in: employeeIds } }, { employee_id: 1, badge_no: 1, expiry_date: 1 }).lean(); | |
| const badgeByEmployee = new Map(badges.map((b) => [String(b.employee_id), b])); | |
| const payload = []; | |
| for (const row of rows) { | |
| const employee = row.employee_id; | |
| if (!employee || !employee.is_active) continue; | |
| const vendor = await Vendor.findById(employee.vendor_id, { name: 1, is_active: 1 }).lean(); | |
| if (!vendor || !vendor.is_active) continue; | |
| const badge = badgeByEmployee.get(String(employee._id)); | |
| payload.push({ | |
| id: String(row._id), | |
| employee_id: String(employee._id), | |
| employee_code: employee.employee_code, | |
| employee_name: employee.name, | |
| vendor_name: vendor.name, | |
| scheduled_date: row.scheduled_date, | |
| completed_date: row.completed_date, | |
| test_status: row.test_status, | |
| test_date: row.test_date, | |
| failure_remarks: row.failure_remarks, | |
| retest_requested: row.retest_requested, | |
| retest_requested_at: row.retest_requested_at, | |
| notes: row.notes, | |
| badge_no: badge?.badge_no || null, | |
| expiry_date: badge?.expiry_date || null | |
| }); | |
| } | |
| return res.json(payload); | |
| } catch (error) { | |
| return next(error); | |
| } | |
| } | |
| async function scheduleInduction(req, res, next) { | |
| try { | |
| const employeeId = req.params.id; | |
| const { scheduled_date, notes } = req.body; | |
| const employee = await Employee.findById(employeeId, { status: 1, is_active: 1, vendor_id: 1 }); | |
| if (!employee) { | |
| return res.status(404).json({ message: 'Employee not found' }); | |
| } | |
| const vendor = await Vendor.findById(employee.vendor_id, { is_active: 1 }).lean(); | |
| if (!vendor || !vendor.is_active || !employee.is_active) { | |
| return res.status(400).json({ message: 'Employee or vendor is inactive' }); | |
| } | |
| if (employee.status !== 'HR_Approved') { | |
| return res.status(400).json({ message: 'Employee must be HR_Approved before induction scheduling' }); | |
| } | |
| await SafetyInduction.findOneAndUpdate( | |
| { employee_id: employeeId }, | |
| { | |
| $set: { | |
| scheduled_date: new Date(scheduled_date), | |
| notes: notes || null, | |
| created_by: req.user.id, | |
| completed_date: null, | |
| test_status: 'Pending', | |
| test_date: null, | |
| failure_remarks: null, | |
| retest_requested: false, | |
| retest_requested_at: null | |
| } | |
| }, | |
| { upsert: true, new: true, setDefaultsOnInsert: true } | |
| ); | |
| return res.json({ message: 'Safety induction scheduled' }); | |
| } catch (error) { | |
| return next(error); | |
| } | |
| } | |
| async function issueBadge(req, res, next) { | |
| try { | |
| const employeeId = req.params.id; | |
| const { induction_date, badge_no } = req.body; | |
| const employee = await Employee.findById(employeeId); | |
| if (!employee) { | |
| return res.status(404).json({ message: 'Employee not found' }); | |
| } | |
| if (!['HR_Approved', 'Safety_Approved', 'Active'].includes(employee.status)) { | |
| return res.status(400).json({ message: 'Employee is not eligible for safety badge issuance' }); | |
| } | |
| await SafetyBadge.findOneAndUpdate( | |
| { employee_id: employeeId }, | |
| { | |
| $set: { | |
| induction_date: new Date(induction_date), | |
| expiry_date: computeExpiry(induction_date), | |
| badge_no, | |
| issued_by: req.user.id | |
| } | |
| }, | |
| { upsert: true, new: true, setDefaultsOnInsert: true } | |
| ); | |
| await SafetyInduction.updateOne( | |
| { employee_id: employeeId }, | |
| { $set: { completed_date: new Date(induction_date) } } | |
| ); | |
| employee.status = 'Safety_Approved'; | |
| employee.safety_completed_at = new Date(); | |
| await employee.save(); | |
| return res.json({ message: 'HSE card issued (valid for 1 year)' }); | |
| } catch (error) { | |
| return next(error); | |
| } | |
| } | |
| function generateBadgeNo(employeeId, testDateISO) { | |
| return `HSE-${employeeId}-${dayjs(testDateISO).format('YYYYMMDD')}-${String(Date.now()).slice(-5)}`; | |
| } | |
| async function submitTestResult(req, res, next) { | |
| try { | |
| const inductionId = req.params.id; | |
| const { result, remarks } = req.body; | |
| const effectiveTestDate = dayjs().format('YYYY-MM-DD'); | |
| const cleanRemarks = String(remarks || '').trim(); | |
| const induction = await SafetyInduction.findById(inductionId); | |
| if (!induction) { | |
| return res.status(404).json({ message: 'Induction record not found' }); | |
| } | |
| const employee = await Employee.findById(induction.employee_id); | |
| if (!employee) { | |
| return res.status(404).json({ message: 'Employee not found' }); | |
| } | |
| const vendor = await Vendor.findById(employee.vendor_id, { name: 1, is_active: 1 }).lean(); | |
| if (!vendor || !vendor.is_active || !employee.is_active) { | |
| return res.status(400).json({ message: 'Employee or vendor is inactive' }); | |
| } | |
| if (result === 'Pass') { | |
| if (!['HR_Approved', 'Safety_Approved', 'Active'].includes(String(employee.status))) { | |
| return res.status(400).json({ message: 'Employee is not eligible for test pass processing' }); | |
| } | |
| const badgeNo = generateBadgeNo(String(employee._id), effectiveTestDate); | |
| await SafetyBadge.findOneAndUpdate( | |
| { employee_id: employee._id }, | |
| { | |
| $set: { | |
| induction_date: new Date(effectiveTestDate), | |
| expiry_date: computeExpiry(effectiveTestDate), | |
| badge_no: badgeNo, | |
| issued_by: req.user.id | |
| } | |
| }, | |
| { upsert: true, new: true, setDefaultsOnInsert: true } | |
| ); | |
| induction.completed_date = new Date(effectiveTestDate); | |
| induction.test_status = 'Passed'; | |
| induction.test_date = new Date(effectiveTestDate); | |
| induction.failure_remarks = null; | |
| induction.retest_requested = false; | |
| induction.retest_requested_at = null; | |
| induction.vendor_notified_at = null; | |
| await induction.save(); | |
| employee.status = 'Safety_Approved'; | |
| employee.safety_completed_at = new Date(); | |
| await employee.save(); | |
| return res.json({ message: 'Test passed. HSE card issued automatically for 1 year.', badge_no: badgeNo }); | |
| } | |
| induction.completed_date = null; | |
| induction.test_status = 'Failed'; | |
| induction.test_date = new Date(effectiveTestDate); | |
| induction.failure_remarks = cleanRemarks || null; | |
| induction.retest_requested = false; | |
| induction.retest_requested_at = null; | |
| induction.vendor_notified_at = new Date(); | |
| await induction.save(); | |
| employee.status = 'HR_Approved'; | |
| employee.safety_completed_at = null; | |
| await employee.save(); | |
| const vendorUser = await User.findOne( | |
| { vendor_id: vendor._id, role: 'Vendor', is_active: true }, | |
| { email: 1, phone: 1 } | |
| ).lean(); | |
| const adminUsers = await User.find({ role: 'Admin', is_active: true }, { email: 1, phone: 1 }).lean(); | |
| const prettyDate = dayjs(effectiveTestDate).format('DD/MM/YYYY'); | |
| const failReason = cleanRemarks ? ` Remarks: ${cleanRemarks}` : ''; | |
| const text = `${employee.name} (${vendor.name}) failed safety test on ${prettyDate}.${failReason} Vendor can apply for retest from dashboard.`; | |
| try { | |
| if (vendorUser?.email) await sendEmail(vendorUser.email, `Safety Test Failed - ${employee.name}`, text); | |
| if (vendorUser?.phone) await sendWhatsApp(`whatsapp:${vendorUser.phone}`, text); | |
| for (const adminUser of adminUsers) { | |
| if (adminUser.email) await sendEmail(adminUser.email, `Safety Test Failed - ${employee.name}`, text); | |
| if (adminUser.phone) await sendWhatsApp(`whatsapp:${adminUser.phone}`, text); | |
| } | |
| } catch (_notifyError) { | |
| // Non-blocking notification path. | |
| } | |
| return res.json({ message: 'Test marked as failed. Vendor has been notified for retest.' }); | |
| } catch (error) { | |
| return next(error); | |
| } | |
| } | |
| module.exports = { | |
| scheduleValidators, | |
| issueBadgeValidators, | |
| submitTestResultValidators, | |
| getCalendar, | |
| scheduleInduction, | |
| issueBadge, | |
| submitTestResult | |
| }; | |