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