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 };