Spaces:
Runtime error
Runtime error
| 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 | |
| }; | |