const { body, param } = require('express-validator'); const { Vendor, Employee, Document, GatePass, RenewRequest, SafetyInduction, User } = require('../models'); const { sendEmail, sendWhatsApp } = require('../services/notificationService'); const validate = require('../utils/validation'); const { countVendorWorkers } = require('../services/workflowService'); const { parseVendorCode } = require('../utils/vendorCode'); const createEmployeeValidators = [ body('name').notEmpty().withMessage('name is required'), body('aadhar_no').notEmpty().withMessage('aadhar_no is required'), body('designation').notEmpty().withMessage('designation is required'), body('department').notEmpty().withMessage('department is required'), body('contact_number').notEmpty().withMessage('contact_number is required'), body('uan_no').optional({ nullable: true }).isString(), body('esi_no').optional({ nullable: true }).isString(), body('bank_ifsc_code').notEmpty().withMessage('bank_ifsc_code is required'), body('bank_account_number').notEmpty().withMessage('bank_account_number is required'), body('vehicle_no').notEmpty().withMessage('vehicle_no is required'), body('vehicle_type').notEmpty().withMessage('vehicle_type is required'), validate ]; const uploadDocumentValidators = [ param('id').isMongoId().withMessage('valid employee id is required'), body('type').isIn(['Aadhar', 'UAN', 'ESI', 'Compensation_Policy', 'PVC']).withMessage('invalid document type'), body('pvc_validity_date') .optional({ nullable: true }) .isDate() .withMessage('pvc_validity_date must be a valid date'), body('type').custom((value, { req }) => { if (value === 'PVC' && !req.body.pvc_validity_date) { throw new Error('pvc_validity_date is required for PVC document'); } return true; }), validate ]; const requestRenewValidators = [ param('id').isMongoId().withMessage('valid gate pass id is required'), body('requested_issue_date').isDate().withMessage('requested_issue_date must be valid'), validate ]; const requestRetestValidators = [ param('id').isMongoId().withMessage('valid employee id is required'), validate ]; function isVendorProfileCompleted(vendor) { return Boolean( vendor.pan && vendor.gstin && (vendor.epf_code || vendor.esi_code) && vendor.work_order_no && vendor.contract_start && vendor.contract_end && vendor.max_workers && vendor.form1_path && vendor.form2_path ); } function formatEmployeeCode(name4, sequence) { return `${name4}${String(sequence).padStart(4, '0')}`; } async function generateEmployeeCode(vendorId, vendorCode) { const parsedVendorCode = parseVendorCode(vendorCode); if (!parsedVendorCode) { throw new Error('Vendor code format is invalid. Update vendor code to NAME4-DSS-YY-SEQ before adding employees.'); } const prefix = parsedVendorCode.name4; const latest = await Employee.findOne({ vendor_id: vendorId, employee_code: { $regex: `^${prefix}\\d{4}$` } }) .sort({ employee_code: -1 }) .lean(); let nextSequence = 1; if (latest?.employee_code) { const numericPart = Number(String(latest.employee_code).slice(prefix.length)); if (Number.isFinite(numericPart) && numericPart > 0) { nextSequence = numericPart + 1; } } while (nextSequence <= 9999) { const candidate = formatEmployeeCode(prefix, nextSequence); const exists = await Employee.exists({ employee_code: candidate }); if (!exists) return candidate; nextSequence += 1; } throw new Error(`Employee code range exhausted for prefix ${prefix}`); } async function createEmployee(req, res, next) { try { const vendorId = req.user.vendor_id; if (!vendorId) { return res.status(400).json({ message: 'Vendor context not found' }); } const vendor = await Vendor.findById(vendorId).lean(); if (!vendor) { return res.status(404).json({ message: 'Vendor not found' }); } if (!vendor.is_active) { return res.status(403).json({ message: 'Vendor account is inactive. Contact HR/Admin.' }); } if (!isVendorProfileCompleted(vendor)) { return res.status(400).json({ message: 'Complete vendor profile details before adding employees' }); } const currentCount = await countVendorWorkers(vendorId); if (currentCount >= vendor.max_workers) { return res.status(400).json({ message: `Worker limit reached (${vendor.max_workers}). Increase max workers before adding more.` }); } if (!req.file) { return res.status(400).json({ message: 'Employee profile photo is required' }); } if (!String(req.file.mimetype || '').startsWith('image/')) { return res.status(400).json({ message: 'Employee profile photo must be an image file' }); } const employeeCode = await generateEmployeeCode(vendorId, vendor.vendor_code); const employee = await Employee.create({ vendor_id: vendorId, employee_code: employeeCode, name: req.body.name, aadhar_no: req.body.aadhar_no, designation: req.body.designation, department: req.body.department, contact_number: req.body.contact_number, uan_no: req.body.uan_no || null, esi_no: req.body.esi_no || null, bank_ifsc_code: req.body.bank_ifsc_code, bank_account_number: req.body.bank_account_number, vehicle_no: req.body.vehicle_no, vehicle_type: req.body.vehicle_type, profile_photo_path: `uploads/${req.file.filename}`, status: 'Pending', is_active: true }); return res.status(201).json({ message: 'Employee created', employee_id: String(employee._id), employee_code: employeeCode }); } catch (error) { return next(error); } } async function uploadEmployeeDocument(req, res, next) { try { const employeeId = req.params.id; const vendorId = req.user.vendor_id; const employee = await Employee.findById(employeeId).lean(); if (!employee) { return res.status(404).json({ message: 'Employee not found' }); } if (req.user.role === 'Vendor' && String(employee.vendor_id) !== String(vendorId)) { return res.status(403).json({ message: 'You cannot upload docs for this employee' }); } if (req.user.role === 'Vendor' && !employee.is_active) { return res.status(403).json({ message: 'Employee is inactive and cannot be modified' }); } if ( req.user.role === 'Vendor' && ['HR_Approved', 'Safety_Approved', 'Active'].includes(String(employee.status)) ) { return res.status(403).json({ message: 'Documents cannot be changed after HR approval' }); } if (!req.file) { return res.status(400).json({ message: 'File is required' }); } const isPvc = req.body.type === 'PVC'; const pvcValidityDate = req.body.pvc_validity_date ? new Date(req.body.pvc_validity_date) : null; if (isPvc && (!pvcValidityDate || Number.isNaN(pvcValidityDate.getTime()))) { return res.status(400).json({ message: 'Valid pvc_validity_date is required for PVC document' }); } const existingDoc = await Document.findOne({ employee_id: employeeId, type: req.body.type }).lean(); if (req.user.role === 'Vendor' && existingDoc && existingDoc.verified_by_hr) { return res.status(403).json({ message: 'This document is already approved by HR and cannot be changed' }); } await Document.findOneAndUpdate( { employee_id: employeeId, type: req.body.type }, { $set: { file_path: `uploads/${req.file.filename}`, pvc_validity_date: isPvc ? pvcValidityDate : null, uploaded_at: new Date(), verified_by_hr: false, verified_at: null, hr_status: 'Pending', hr_remarks: null, reviewed_at: null, reviewed_by: null } }, { upsert: true, new: true, setDefaultsOnInsert: true } ); return res.json({ message: 'Document uploaded successfully' }); } catch (error) { return next(error); } } async function listVendorEmployees(req, res, next) { try { const vendorId = req.user.vendor_id; const employees = await Employee.find({ vendor_id: vendorId }).sort({ created_at: -1 }).lean(); const employeeIds = employees.map((emp) => emp._id); const gatePasses = await GatePass.find({ employee_id: { $in: employeeIds } }) .sort({ created_at: -1 }) .lean(); const latestPassByEmployee = new Map(); for (const pass of gatePasses) { const key = String(pass.employee_id); if (!latestPassByEmployee.has(key)) latestPassByEmployee.set(key, pass); } const rows = employees.map((emp) => { const pass = latestPassByEmployee.get(String(emp._id)); const gatePassExpiry = pass?.expiry_date || null; return { id: String(emp._id), employee_code: emp.employee_code, name: emp.name, designation: emp.designation, department: emp.department, contact_number: emp.contact_number, status: emp.status, due_date: emp.due_date, profile_photo_path: emp.profile_photo_path, is_active: emp.is_active, gate_pass_expiry: gatePassExpiry, gate_pass_expired: gatePassExpiry ? new Date(gatePassExpiry) < new Date() : false }; }); return res.json(rows); } catch (error) { return next(error); } } async function requestGatePassRenewal(req, res, next) { try { const vendorId = req.user.vendor_id; const gatePassId = req.params.id; const { requested_issue_date } = req.body; const gatePass = await GatePass.findById(gatePassId).lean(); if (!gatePass) { return res.status(404).json({ message: 'Gate pass not found' }); } const employee = await Employee.findById(gatePass.employee_id, { vendor_id: 1, is_active: 1 }).lean(); if (!employee) { return res.status(404).json({ message: 'Employee not found' }); } if (String(employee.vendor_id) !== String(vendorId)) { return res.status(403).json({ message: 'You cannot request renewal for this pass' }); } if (!employee.is_active) { return res.status(400).json({ message: 'Cannot request renewal for an inactive employee' }); } if (new Date(gatePass.expiry_date) >= new Date()) { return res.status(400).json({ message: 'Renewal allowed only for expired passes' }); } const existingRenewRequest = await RenewRequest.findOne({ gate_pass_id: gatePass._id }).lean(); if (existingRenewRequest) { return res.status(409).json({ message: 'Renewal request already submitted for this pass' }); } await RenewRequest.create({ gate_pass_id: gatePass._id, employee_id: gatePass.employee_id, vendor_id: vendorId, requested_issue_date: new Date(requested_issue_date), status: 'Requested' }); return res.status(201).json({ message: 'Renewal request submitted to HR' }); } catch (error) { return next(error); } } async function requestSafetyRetest(req, res, next) { try { const vendorId = req.user.vendor_id; const employeeId = req.params.id; const employee = await Employee.findById(employeeId, { name: 1, vendor_id: 1, is_active: 1 }).lean(); if (!employee) { return res.status(404).json({ message: 'Employee not found' }); } if (String(employee.vendor_id) !== String(vendorId)) { return res.status(403).json({ message: 'You cannot request retest for this employee' }); } if (!employee.is_active) { return res.status(400).json({ message: 'Cannot request retest for an inactive employee' }); } const induction = await SafetyInduction.findOne({ employee_id: employeeId }); if (!induction || induction.test_status !== 'Failed') { return res.status(400).json({ message: 'Retest can be requested only after safety test failure' }); } if (Boolean(induction.retest_requested)) { return res.status(400).json({ message: 'Retest has already been requested' }); } induction.retest_requested = true; induction.retest_requested_at = new Date(); await induction.save(); const vendor = await Vendor.findById(employee.vendor_id, { name: 1 }).lean(); const safetyUsers = await User.find({ role: 'Safety_Officer', is_active: true }, { email: 1, phone: 1 }).lean(); const adminUsers = await User.find({ role: 'Admin', is_active: true }, { email: 1, phone: 1 }).lean(); const message = `Retest requested for ${employee.name} (${vendor?.name || 'Vendor'}) after failed safety test.`; const notifiedEmails = new Set(); for (const safetyUser of safetyUsers) { try { if (safetyUser.email && !notifiedEmails.has(safetyUser.email)) { await sendEmail(safetyUser.email, `Retest Request - ${employee.name}`, message); notifiedEmails.add(safetyUser.email); } if (safetyUser.phone) { await sendWhatsApp(`whatsapp:${safetyUser.phone}`, message); } } catch (_notifyError) { // Non-blocking notification path. } } for (const adminUser of adminUsers) { try { if (adminUser.email && !notifiedEmails.has(adminUser.email)) { await sendEmail(adminUser.email, `Retest Request - ${employee.name}`, message); notifiedEmails.add(adminUser.email); } if (adminUser.phone) { await sendWhatsApp(`whatsapp:${adminUser.phone}`, message); } } catch (_notifyError) { // Non-blocking notification path. } } return res.status(201).json({ message: 'Retest request sent to Safety team' }); } catch (error) { return next(error); } } module.exports = { createEmployeeValidators, uploadDocumentValidators, requestRenewValidators, requestRetestValidators, createEmployee, uploadEmployeeDocument, listVendorEmployees, requestGatePassRenewal, requestSafetyRetest };