dss-server / src /controllers /employeeController.js
yeshwanth-kr's picture
Upload 43 files
8c7b7ca verified
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
};