Spaces:
Runtime error
Runtime error
| const { body, param, query } = require('express-validator'); | |
| const bcrypt = require('bcryptjs'); | |
| const { | |
| User, | |
| Vendor, | |
| Employee, | |
| Document, | |
| SafetyInduction, | |
| SafetyBadge, | |
| GatePass, | |
| RenewRequest | |
| } = require('../models'); | |
| const validate = require('../utils/validation'); | |
| const { parseVendorCode } = require('../utils/vendorCode'); | |
| const allowedEmployeeStatuses = ['Pending', 'HR_Approved', 'Safety_Approved', 'Active', 'Rejected']; | |
| const listVendorsValidators = [ | |
| query('search').optional().isString(), | |
| validate | |
| ]; | |
| const updateVendorValidators = [ | |
| param('id').isMongoId().withMessage('valid vendor id is required'), | |
| body('name').optional().notEmpty().withMessage('name cannot be empty'), | |
| body('vendor_code').optional().notEmpty().withMessage('vendor_code cannot be empty'), | |
| body('vendor_code').optional().custom((value) => { | |
| if (!parseVendorCode(value)) { | |
| throw new Error('vendor_code must be in format NAME4-DSS-YY-SEQ (e.g., YESH-DSS-26-001)'); | |
| } | |
| return true; | |
| }), | |
| body('contact_name').optional().notEmpty().withMessage('contact_name cannot be empty'), | |
| body('contact_email').optional().isEmail().withMessage('contact_email must be valid'), | |
| body('contact_phone').optional().notEmpty().withMessage('contact_phone cannot be empty'), | |
| body('pan').optional({ nullable: true }).isString(), | |
| body('gstin').optional({ nullable: true }).isString(), | |
| body('epf_code').optional({ nullable: true }).isString(), | |
| body('esi_code').optional({ nullable: true }).isString(), | |
| body('work_order_no').optional({ nullable: true }).isString(), | |
| body('contract_start').optional({ nullable: true }).isDate().withMessage('contract_start must be valid date'), | |
| body('contract_end').optional({ nullable: true }).isDate().withMessage('contract_end must be valid date'), | |
| body('max_workers').optional({ nullable: true }).isInt({ min: 1 }).withMessage('max_workers must be at least 1'), | |
| body('is_active').optional().isBoolean(), | |
| validate | |
| ]; | |
| const setVendorStatusValidators = [ | |
| param('id').isMongoId().withMessage('valid vendor id is required'), | |
| body('is_active').isBoolean().withMessage('is_active must be boolean'), | |
| validate | |
| ]; | |
| const deleteVendorValidators = [ | |
| param('id').isMongoId().withMessage('valid vendor id is required'), | |
| validate | |
| ]; | |
| const listEmployeesValidators = [ | |
| query('search').optional().isString(), | |
| query('vendor_id').optional().isMongoId().withMessage('vendor_id must be a valid id'), | |
| query('include_inactive').optional().isIn(['true', 'false']).withMessage('include_inactive must be true or false'), | |
| validate | |
| ]; | |
| const updateEmployeeValidators = [ | |
| param('id').isMongoId().withMessage('valid employee id is required'), | |
| body('name').optional().notEmpty().withMessage('name cannot be empty'), | |
| body('aadhar_no').optional().notEmpty().withMessage('aadhar_no cannot be empty'), | |
| body('designation').optional({ nullable: true }).isString(), | |
| body('department').optional({ nullable: true }).isString(), | |
| body('contact_number').optional({ nullable: true }).isString(), | |
| body('uan_no').optional({ nullable: true }).isString(), | |
| body('esi_no').optional({ nullable: true }).isString(), | |
| body('bank_ifsc_code').optional({ nullable: true }).isString(), | |
| body('bank_account_number').optional({ nullable: true }).isString(), | |
| body('vehicle_no').optional({ nullable: true }).isString(), | |
| body('vehicle_type').optional({ nullable: true }).isString(), | |
| body('status').optional().isIn(allowedEmployeeStatuses).withMessage('invalid status'), | |
| body('is_active').optional().isBoolean(), | |
| validate | |
| ]; | |
| const setEmployeeStatusValidators = [ | |
| param('id').isMongoId().withMessage('valid employee id is required'), | |
| body('is_active').isBoolean().withMessage('is_active must be boolean'), | |
| validate | |
| ]; | |
| const deleteEmployeeValidators = [ | |
| param('id').isMongoId().withMessage('valid employee id is required'), | |
| validate | |
| ]; | |
| const getEmployeeIdCardValidators = [ | |
| param('id').isMongoId().withMessage('valid employee id is required'), | |
| validate | |
| ]; | |
| const resetUserPasswordValidators = [ | |
| body('email').isEmail().withMessage('valid email is required'), | |
| validate | |
| ]; | |
| function generateTempPassword(length = 12) { | |
| const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789@#$!'; | |
| let value = ''; | |
| for (let i = 0; i < length; i += 1) { | |
| value += chars[Math.floor(Math.random() * chars.length)]; | |
| } | |
| return value; | |
| } | |
| async function listVendors(req, res, next) { | |
| try { | |
| const search = String(req.query.search || '').trim().toLowerCase(); | |
| const vendors = await Vendor.find({}).sort({ created_at: -1 }).lean(); | |
| const vendorIds = vendors.map((v) => v._id); | |
| const vendorUsers = await User.find({ vendor_id: { $in: vendorIds }, role: 'Vendor' }) | |
| .sort({ created_at: 1 }) | |
| .lean(); | |
| const employees = await Employee.find({ vendor_id: { $in: vendorIds } }, { vendor_id: 1, is_active: 1 }).lean(); | |
| const firstUserByVendor = new Map(); | |
| for (const user of vendorUsers) { | |
| const key = String(user.vendor_id); | |
| if (!firstUserByVendor.has(key)) firstUserByVendor.set(key, user); | |
| } | |
| const employeeStats = new Map(); | |
| for (const employee of employees) { | |
| const key = String(employee.vendor_id); | |
| if (!employeeStats.has(key)) employeeStats.set(key, { total: 0, active: 0 }); | |
| const stats = employeeStats.get(key); | |
| stats.total += 1; | |
| if (employee.is_active) stats.active += 1; | |
| } | |
| const rows = vendors | |
| .map((vendor) => { | |
| const user = firstUserByVendor.get(String(vendor._id)); | |
| const stats = employeeStats.get(String(vendor._id)) || { total: 0, active: 0 }; | |
| return { | |
| id: String(vendor._id), | |
| name: vendor.name, | |
| vendor_code: vendor.vendor_code, | |
| pan: vendor.pan, | |
| gstin: vendor.gstin, | |
| epf_code: vendor.epf_code, | |
| esi_code: vendor.esi_code, | |
| work_order_no: vendor.work_order_no, | |
| contract_start: vendor.contract_start, | |
| contract_end: vendor.contract_end, | |
| max_workers: vendor.max_workers, | |
| form1_path: vendor.form1_path, | |
| form2_path: vendor.form2_path, | |
| is_active: vendor.is_active, | |
| vendor_user_id: user ? String(user._id) : null, | |
| contact_name: user?.full_name || null, | |
| contact_email: user?.email || null, | |
| contact_phone: user?.phone || null, | |
| employee_count: stats.total, | |
| active_employee_count: stats.active | |
| }; | |
| }) | |
| .filter((row) => { | |
| if (!search) return true; | |
| return String(row.name || '').toLowerCase().includes(search) | |
| || String(row.vendor_code || '').toLowerCase().includes(search) | |
| || String(row.contact_email || '').toLowerCase().includes(search); | |
| }); | |
| return res.json(rows); | |
| } catch (error) { | |
| return next(error); | |
| } | |
| } | |
| async function updateVendor(req, res, next) { | |
| try { | |
| const vendorId = req.params.id; | |
| const vendor = await Vendor.findById(vendorId); | |
| if (!vendor) { | |
| return res.status(404).json({ message: 'Vendor not found' }); | |
| } | |
| const vendorUser = await User.findOne({ vendor_id: vendorId, role: 'Vendor' }).sort({ created_at: 1 }); | |
| const nextVendorCodeRaw = req.body.vendor_code ?? vendor.vendor_code; | |
| const parsedNextVendorCode = parseVendorCode(nextVendorCodeRaw); | |
| if (!parsedNextVendorCode) { | |
| return res.status(400).json({ message: 'vendor_code must be in format NAME4-DSS-YY-SEQ (e.g., YESH-DSS-26-001)' }); | |
| } | |
| const nextVendorCode = parsedNextVendorCode.normalized; | |
| const parsedCurrentVendorCode = parseVendorCode(vendor.vendor_code); | |
| if (nextVendorCode !== vendor.vendor_code) { | |
| const dupVendorCode = await Vendor.findOne({ vendor_code: nextVendorCode, _id: { $ne: vendorId } }).lean(); | |
| if (dupVendorCode) { | |
| return res.status(409).json({ message: 'Vendor code is already in use' }); | |
| } | |
| } | |
| if ( | |
| parsedCurrentVendorCode | |
| && parsedCurrentVendorCode.name4 !== parsedNextVendorCode.name4 | |
| ) { | |
| const employeeExists = await Employee.exists({ vendor_id: vendorId }); | |
| if (employeeExists) { | |
| return res.status(400).json({ message: 'Cannot change NAME4 prefix after employees are created for this vendor' }); | |
| } | |
| } | |
| const dupName4 = await Vendor.findOne({ | |
| _id: { $ne: vendorId }, | |
| vendor_code: { $regex: `^${parsedNextVendorCode.name4}-DSS-`, $options: 'i' } | |
| }).lean(); | |
| if (dupName4) { | |
| return res.status(409).json({ message: `NAME4 prefix '${parsedNextVendorCode.name4}' is already assigned to another vendor` }); | |
| } | |
| if (vendorUser && req.body.contact_email && req.body.contact_email !== vendorUser.email) { | |
| const dupEmail = await User.findOne({ email: req.body.contact_email.toLowerCase(), _id: { $ne: vendorUser._id } }).lean(); | |
| if (dupEmail) { | |
| return res.status(409).json({ message: 'Contact email is already in use' }); | |
| } | |
| } | |
| const nextContractStart = req.body.contract_start ?? vendor.contract_start; | |
| const nextContractEnd = req.body.contract_end ?? vendor.contract_end; | |
| if (nextContractStart && nextContractEnd && new Date(nextContractEnd) < new Date(nextContractStart)) { | |
| return res.status(400).json({ message: 'contract_end must be on or after contract_start' }); | |
| } | |
| const nextEpf = (req.body.epf_code ?? vendor.epf_code ?? '').toString().trim(); | |
| const nextEsi = (req.body.esi_code ?? vendor.esi_code ?? '').toString().trim(); | |
| if (!nextEpf && !nextEsi) { | |
| return res.status(400).json({ message: 'Either epf_code or esi_code is required' }); | |
| } | |
| vendor.name = req.body.name ?? vendor.name; | |
| vendor.vendor_code = nextVendorCode; | |
| vendor.pan = req.body.pan ?? vendor.pan; | |
| vendor.gstin = req.body.gstin ?? vendor.gstin; | |
| vendor.epf_code = req.body.epf_code ?? vendor.epf_code; | |
| vendor.esi_code = req.body.esi_code ?? vendor.esi_code; | |
| vendor.work_order_no = req.body.work_order_no ?? vendor.work_order_no; | |
| vendor.contract_start = nextContractStart ? new Date(nextContractStart) : null; | |
| vendor.contract_end = nextContractEnd ? new Date(nextContractEnd) : null; | |
| vendor.max_workers = req.body.max_workers ?? vendor.max_workers; | |
| if (typeof req.body.is_active === 'boolean') vendor.is_active = req.body.is_active; | |
| await vendor.save(); | |
| if (vendorUser) { | |
| vendorUser.full_name = req.body.contact_name ?? vendorUser.full_name; | |
| vendorUser.email = (req.body.contact_email ?? vendorUser.email).toLowerCase(); | |
| vendorUser.phone = req.body.contact_phone ?? vendorUser.phone; | |
| if (typeof req.body.is_active === 'boolean') vendorUser.is_active = req.body.is_active; | |
| await vendorUser.save(); | |
| } | |
| return res.json({ message: 'Vendor updated successfully' }); | |
| } catch (error) { | |
| return next(error); | |
| } | |
| } | |
| async function setVendorStatus(req, res, next) { | |
| try { | |
| const vendorId = req.params.id; | |
| const { is_active } = req.body; | |
| const vendor = await Vendor.findById(vendorId); | |
| if (!vendor) { | |
| return res.status(404).json({ message: 'Vendor not found' }); | |
| } | |
| vendor.is_active = is_active; | |
| await vendor.save(); | |
| await User.updateMany({ vendor_id: vendorId, role: 'Vendor' }, { $set: { is_active } }); | |
| return res.json({ message: is_active ? 'Vendor activated' : 'Vendor deactivated' }); | |
| } catch (error) { | |
| return next(error); | |
| } | |
| } | |
| async function deleteVendor(req, res, next) { | |
| try { | |
| const vendorId = req.params.id; | |
| const vendor = await Vendor.findById(vendorId).lean(); | |
| if (!vendor) { | |
| return res.status(404).json({ message: 'Vendor not found' }); | |
| } | |
| const employees = await Employee.find({ vendor_id: vendorId }, { _id: 1 }).lean(); | |
| const employeeIds = employees.map((e) => e._id); | |
| const gatePasses = await GatePass.find({ employee_id: { $in: employeeIds } }, { _id: 1 }).lean(); | |
| const gatePassIds = gatePasses.map((g) => g._id); | |
| await Document.deleteMany({ employee_id: { $in: employeeIds } }); | |
| await SafetyInduction.deleteMany({ employee_id: { $in: employeeIds } }); | |
| await SafetyBadge.deleteMany({ employee_id: { $in: employeeIds } }); | |
| await RenewRequest.deleteMany({ $or: [{ employee_id: { $in: employeeIds } }, { gate_pass_id: { $in: gatePassIds } }, { vendor_id: vendorId }] }); | |
| await GatePass.deleteMany({ employee_id: { $in: employeeIds } }); | |
| await Employee.deleteMany({ vendor_id: vendorId }); | |
| await User.deleteMany({ vendor_id: vendorId, role: 'Vendor' }); | |
| await Vendor.deleteOne({ _id: vendorId }); | |
| return res.json({ message: 'Vendor deleted successfully' }); | |
| } catch (error) { | |
| return next(error); | |
| } | |
| } | |
| async function listEmployees(req, res, next) { | |
| try { | |
| const search = String(req.query.search || '').trim().toLowerCase(); | |
| const vendorId = req.query.vendor_id || null; | |
| const includeInactive = String(req.query.include_inactive || 'true') !== 'false'; | |
| const filter = {}; | |
| if (vendorId) filter.vendor_id = vendorId; | |
| if (!includeInactive) filter.is_active = true; | |
| const employees = await Employee.find(filter).sort({ created_at: -1 }).lean(); | |
| const vendorIds = [...new Set(employees.map((e) => String(e.vendor_id)))]; | |
| const vendors = await Vendor.find({ _id: { $in: vendorIds } }, { name: 1 }).lean(); | |
| const vendorMap = new Map(vendors.map((v) => [String(v._id), v.name])); | |
| const employeeIds = employees.map((e) => e._id); | |
| const docs = await Document.find({ employee_id: { $in: employeeIds } }).sort({ uploaded_at: -1 }).lean(); | |
| const docsByEmployee = new Map(); | |
| for (const doc of docs) { | |
| const key = String(doc.employee_id); | |
| if (!docsByEmployee.has(key)) docsByEmployee.set(key, []); | |
| docsByEmployee.get(key).push({ | |
| id: String(doc._id), | |
| employee_id: String(doc.employee_id), | |
| type: doc.type, | |
| file_path: doc.file_path, | |
| pvc_validity_date: doc.pvc_validity_date, | |
| hr_status: doc.hr_status, | |
| hr_remarks: doc.hr_remarks, | |
| verified_by_hr: doc.verified_by_hr, | |
| uploaded_at: doc.uploaded_at | |
| }); | |
| } | |
| const rows = employees | |
| .map((employee) => { | |
| const key = String(employee._id); | |
| const employeeDocs = docsByEmployee.get(key) || []; | |
| return { | |
| id: key, | |
| employee_code: employee.employee_code, | |
| vendor_id: String(employee.vendor_id), | |
| vendor_name: vendorMap.get(String(employee.vendor_id)) || 'N/A', | |
| name: employee.name, | |
| aadhar_no: employee.aadhar_no, | |
| designation: employee.designation, | |
| department: employee.department, | |
| contact_number: employee.contact_number, | |
| uan_no: employee.uan_no, | |
| esi_no: employee.esi_no, | |
| bank_ifsc_code: employee.bank_ifsc_code, | |
| bank_account_number: employee.bank_account_number, | |
| vehicle_no: employee.vehicle_no, | |
| vehicle_type: employee.vehicle_type, | |
| status: employee.status, | |
| due_date: employee.due_date, | |
| profile_photo_path: employee.profile_photo_path, | |
| is_active: employee.is_active, | |
| created_at: employee.created_at, | |
| total_docs_count: employeeDocs.length, | |
| pending_docs_count: employeeDocs.filter((d) => d.hr_status === 'Pending').length, | |
| approved_docs_count: employeeDocs.filter((d) => d.hr_status === 'Approved').length, | |
| rejected_docs_count: employeeDocs.filter((d) => d.hr_status === 'Rejected').length, | |
| documents: employeeDocs | |
| }; | |
| }) | |
| .filter((row) => { | |
| if (!search) return true; | |
| return String(row.name || '').toLowerCase().includes(search) | |
| || String(row.vendor_name || '').toLowerCase().includes(search) | |
| || String(row.employee_code || '').toLowerCase().includes(search) | |
| || String(row.aadhar_no || '').toLowerCase().includes(search); | |
| }); | |
| return res.json(rows); | |
| } catch (error) { | |
| return next(error); | |
| } | |
| } | |
| async function updateEmployee(req, res, next) { | |
| try { | |
| const employeeId = req.params.id; | |
| const employee = await Employee.findById(employeeId); | |
| if (!employee) { | |
| return res.status(404).json({ message: 'Employee not found' }); | |
| } | |
| const nextAadhar = req.body.aadhar_no ?? employee.aadhar_no; | |
| if (nextAadhar !== employee.aadhar_no) { | |
| const dup = await Employee.findOne({ vendor_id: employee.vendor_id, aadhar_no: nextAadhar, _id: { $ne: employeeId } }).lean(); | |
| if (dup) { | |
| return res.status(409).json({ message: 'aadhar_no already exists for this vendor' }); | |
| } | |
| } | |
| employee.name = req.body.name ?? employee.name; | |
| employee.aadhar_no = nextAadhar; | |
| employee.designation = req.body.designation ?? employee.designation; | |
| employee.department = req.body.department ?? employee.department; | |
| employee.contact_number = req.body.contact_number ?? employee.contact_number; | |
| employee.uan_no = req.body.uan_no ?? employee.uan_no; | |
| employee.esi_no = req.body.esi_no ?? employee.esi_no; | |
| employee.bank_ifsc_code = req.body.bank_ifsc_code ?? employee.bank_ifsc_code; | |
| employee.bank_account_number = req.body.bank_account_number ?? employee.bank_account_number; | |
| employee.vehicle_no = req.body.vehicle_no ?? employee.vehicle_no; | |
| employee.vehicle_type = req.body.vehicle_type ?? employee.vehicle_type; | |
| employee.status = req.body.status ?? employee.status; | |
| if (typeof req.body.is_active === 'boolean') employee.is_active = req.body.is_active; | |
| await employee.save(); | |
| return res.json({ message: 'Employee updated successfully' }); | |
| } catch (error) { | |
| return next(error); | |
| } | |
| } | |
| async function setEmployeeStatus(req, res, next) { | |
| try { | |
| const employeeId = req.params.id; | |
| const { is_active } = req.body; | |
| const employee = await Employee.findById(employeeId); | |
| if (!employee) { | |
| return res.status(404).json({ message: 'Employee not found' }); | |
| } | |
| employee.is_active = is_active; | |
| await employee.save(); | |
| return res.json({ message: is_active ? 'Employee activated' : 'Employee deactivated' }); | |
| } catch (error) { | |
| return next(error); | |
| } | |
| } | |
| async function deleteEmployee(req, res, next) { | |
| try { | |
| const employeeId = req.params.id; | |
| const employee = await Employee.findById(employeeId).lean(); | |
| if (!employee) { | |
| return res.status(404).json({ message: 'Employee not found' }); | |
| } | |
| const gatePasses = await GatePass.find({ employee_id: employeeId }, { _id: 1 }).lean(); | |
| const gatePassIds = gatePasses.map((g) => g._id); | |
| await Document.deleteMany({ employee_id: employeeId }); | |
| await SafetyInduction.deleteMany({ employee_id: employeeId }); | |
| await SafetyBadge.deleteMany({ employee_id: employeeId }); | |
| await RenewRequest.deleteMany({ $or: [{ employee_id: employeeId }, { gate_pass_id: { $in: gatePassIds } }] }); | |
| await GatePass.deleteMany({ employee_id: employeeId }); | |
| await Employee.deleteOne({ _id: employeeId }); | |
| return res.json({ message: 'Employee deleted successfully' }); | |
| } catch (error) { | |
| return next(error); | |
| } | |
| } | |
| async function getEmployeeIdCard(req, res, next) { | |
| try { | |
| const employeeId = req.params.id; | |
| const employee = await Employee.findById(employeeId).lean(); | |
| if (!employee) { | |
| return res.status(404).json({ message: 'Employee not found' }); | |
| } | |
| const vendor = await Vendor.findById(employee.vendor_id, { name: 1 }).lean(); | |
| const gatePass = await GatePass.findOne({ employee_id: employee._id }) | |
| .sort({ created_at: -1 }) | |
| .lean(); | |
| return res.json({ | |
| employee_id: String(employee._id), | |
| employee_code: employee.employee_code || null, | |
| name: employee.name, | |
| vendor_name: vendor?.name || 'N/A', | |
| aadhar_no: employee.aadhar_no || null, | |
| designation: employee.designation || null, | |
| department: employee.department || null, | |
| contact_number: employee.contact_number || null, | |
| status: employee.status, | |
| profile_photo_path: employee.profile_photo_path || null, | |
| barcode: gatePass?.barcode || null, | |
| gate_pass_status: gatePass?.status || null, | |
| gate_pass_issue_date: gatePass?.issue_date || null, | |
| gate_pass_expiry_date: gatePass?.expiry_date || employee.due_date || null | |
| }); | |
| } catch (error) { | |
| return next(error); | |
| } | |
| } | |
| async function resetUserPassword(req, res, next) { | |
| try { | |
| if (req.user.role !== 'Admin') { | |
| return res.status(403).json({ message: 'Only Admin can reset user passwords' }); | |
| } | |
| const email = String(req.body.email || '').trim().toLowerCase(); | |
| const user = await User.findOne({ email }); | |
| if (!user) { | |
| return res.status(404).json({ message: 'User not found for this email' }); | |
| } | |
| const temporaryPassword = generateTempPassword(); | |
| user.password_hash = await bcrypt.hash(temporaryPassword, 10); | |
| user.must_reset_password = true; | |
| user.password_changed_at = new Date(); | |
| await user.save(); | |
| return res.json({ | |
| message: 'Password reset successful. Share temporary password and request immediate password change.', | |
| credentials: { | |
| email: user.email, | |
| role: user.role, | |
| temporary_password: temporaryPassword | |
| } | |
| }); | |
| } catch (error) { | |
| return next(error); | |
| } | |
| } | |
| module.exports = { | |
| listVendorsValidators, | |
| updateVendorValidators, | |
| setVendorStatusValidators, | |
| deleteVendorValidators, | |
| listEmployeesValidators, | |
| updateEmployeeValidators, | |
| setEmployeeStatusValidators, | |
| deleteEmployeeValidators, | |
| getEmployeeIdCardValidators, | |
| resetUserPasswordValidators, | |
| listVendors, | |
| updateVendor, | |
| setVendorStatus, | |
| deleteVendor, | |
| listEmployees, | |
| updateEmployee, | |
| setEmployeeStatus, | |
| deleteEmployee, | |
| getEmployeeIdCard, | |
| resetUserPassword | |
| }; | |