dss-server / src /controllers /hrController.js
yeshwanth-kr's picture
Upload 43 files
8c7b7ca verified
const { body, param } = require('express-validator');
const { v4: uuidv4 } = require('uuid');
const dayjs = require('dayjs');
const {
Employee,
Vendor,
Document,
SafetyBadge,
GatePass,
RenewRequest,
SafetyInduction
} = require('../models');
const validate = require('../utils/validation');
const { toValidObjectIdStrings } = require('../utils/objectId');
const { ensureMandatoryDocuments } = require('../services/workflowService');
const verifyDocumentValidators = [
param('id').isMongoId().withMessage('valid employee id is required'),
body('types').isArray({ min: 1 }).withMessage('types array is required'),
validate
];
const reviewDocumentValidators = [
param('id').isMongoId().withMessage('valid document id is required'),
body('action').isIn(['Approve', 'Reject']).withMessage('action must be Approve or Reject'),
body('remarks').optional({ nullable: true }).isLength({ max: 255 }).withMessage('remarks must be at most 255 characters'),
body('pvc_validity_date')
.optional({ nullable: true })
.isDate()
.withMessage('pvc_validity_date must be a valid date'),
validate
];
const approveEmployeeValidators = [
param('id').isMongoId().withMessage('valid employee id is required'),
validate
];
const createGatePassValidators = [
param('id').isMongoId().withMessage('valid employee id is required'),
body('issue_date').isDate().withMessage('issue_date must be valid'),
body('fee_paid').isBoolean().withMessage('fee_paid must be boolean'),
body('fee_amount').optional().isFloat({ min: 0 }).withMessage('fee_amount must be valid number'),
validate
];
const renewGatePassValidators = [
param('id').isMongoId().withMessage('valid gate pass id is required'),
body('issue_date').isDate().withMessage('issue_date is required'),
body('fee_paid').isBoolean().withMessage('fee_paid must be boolean'),
validate
];
const processRenewRequestValidators = [
param('id').isMongoId().withMessage('valid renewal request id is required'),
body('fee_paid').isBoolean().withMessage('fee_paid must be boolean'),
body('issue_date').optional().isDate().withMessage('issue_date must be valid'),
validate
];
function latestByEmployee(rows) {
const map = new Map();
for (const row of rows) {
const key = String(row.employee_id);
if (!map.has(key)) map.set(key, row);
}
return map;
}
async function getPendingVerification(req, res, next) {
try {
const employees = await Employee.find({ status: 'Pending', is_active: true })
.sort({ created_at: -1 })
.lean();
const vendorIds = toValidObjectIdStrings(employees.map((e) => e.vendor_id));
const vendors = vendorIds.length
? await Vendor.find({ _id: { $in: vendorIds }, is_active: true }, { name: 1 }).lean()
: [];
const vendorMap = new Map(vendors.map((v) => [String(v._id), v]));
const filteredEmployees = employees.filter((e) => vendorMap.has(String(e.vendor_id)));
const employeeIds = filteredEmployees.map((e) => e._id);
const docs = await Document.find({ employee_id: { $in: employeeIds } }).sort({ uploaded_at: -1 }).lean();
const badges = await SafetyBadge.find({ employee_id: { $in: employeeIds } }, { employee_id: 1, expiry_date: 1 }).lean();
const passes = await GatePass.find({ employee_id: { $in: employeeIds } }).sort({ created_at: -1 }).lean();
const badgeMap = new Map(badges.map((b) => [String(b.employee_id), b]));
const passMap = latestByEmployee(passes);
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(doc);
}
const rows = [];
for (const employee of filteredEmployees) {
const key = String(employee._id);
const vendor = vendorMap.get(String(employee.vendor_id));
const employeeDocs = docsByEmployee.get(key) || [];
const badge = badgeMap.get(key);
const pass = passMap.get(key);
if (employeeDocs.length === 0) {
rows.push({
employee_id: key,
employee_code: employee.employee_code,
employee_name: employee.name,
status: employee.status,
vendor_name: vendor?.name || 'N/A',
profile_photo_path: employee.profile_photo_path,
document_id: null,
type: null,
file_path: null,
verified_by_hr: false,
hr_status: null,
hr_remarks: null,
pvc_validity_date: null,
hse_due_date: badge?.expiry_date || null,
pass_due_date: pass?.expiry_date || null,
pass_status: pass?.status || null
});
continue;
}
for (const doc of employeeDocs) {
rows.push({
employee_id: key,
employee_code: employee.employee_code,
employee_name: employee.name,
status: employee.status,
vendor_name: vendor?.name || 'N/A',
profile_photo_path: employee.profile_photo_path,
document_id: String(doc._id),
type: doc.type,
file_path: doc.file_path,
pvc_validity_date: doc.pvc_validity_date,
verified_by_hr: doc.verified_by_hr,
hr_status: doc.hr_status,
hr_remarks: doc.hr_remarks,
uploaded_at: doc.uploaded_at,
hse_due_date: badge?.expiry_date || null,
pass_due_date: pass?.expiry_date || null,
pass_status: pass?.status || null
});
}
}
return res.json(rows);
} catch (error) {
return next(error);
}
}
async function verifyDocuments(req, res, next) {
try {
const employeeId = req.params.id;
const { types } = req.body;
if (!Array.isArray(types) || types.length === 0) {
return res.status(400).json({ message: 'types array is required' });
}
if (types.includes('PVC')) {
const pvcDoc = await Document.findOne({ employee_id: employeeId, type: 'PVC' }, { pvc_validity_date: 1 }).lean();
if (!pvcDoc?.pvc_validity_date) {
return res.status(400).json({ message: 'PVC validity date is required before approving PVC document' });
}
}
await Document.updateMany(
{ employee_id: employeeId, type: { $in: types } },
{
$set: {
verified_by_hr: true,
verified_at: new Date(),
hr_status: 'Approved',
hr_remarks: null,
reviewed_at: new Date(),
reviewed_by: req.user.id
}
}
);
return res.json({ message: 'Selected documents marked as verified' });
} catch (error) {
return next(error);
}
}
async function reviewDocument(req, res, next) {
try {
const documentId = req.params.id;
const { action, remarks, pvc_validity_date } = req.body;
const cleanRemarks = String(remarks || '').trim();
const doc = await Document.findById(documentId);
if (!doc) {
return res.status(404).json({ message: 'Document not found' });
}
if (action === 'Approve') {
if (doc.type === 'PVC') {
const nextPvcDate = pvc_validity_date ? new Date(pvc_validity_date) : doc.pvc_validity_date;
if (!nextPvcDate || Number.isNaN(new Date(nextPvcDate).getTime())) {
return res.status(400).json({ message: 'PVC validity date is required before approving PVC document' });
}
doc.pvc_validity_date = nextPvcDate;
}
doc.verified_by_hr = true;
doc.verified_at = new Date();
doc.hr_status = 'Approved';
doc.hr_remarks = null;
doc.reviewed_at = new Date();
doc.reviewed_by = req.user.id;
await doc.save();
return res.json({ message: 'Document approved' });
}
doc.verified_by_hr = false;
doc.verified_at = null;
doc.hr_status = 'Rejected';
doc.hr_remarks = cleanRemarks || null;
doc.reviewed_at = new Date();
doc.reviewed_by = req.user.id;
await doc.save();
return res.json({ message: 'Document rejected' });
} catch (error) {
return next(error);
}
}
async function approveForSafety(req, res, next) {
try {
const employeeId = req.params.id;
const mandatoryDocsExist = await ensureMandatoryDocuments(employeeId);
if (!mandatoryDocsExist) {
return res.status(400).json({ message: 'Mandatory document set is incomplete' });
}
const docs = await Document.find({ employee_id: employeeId }, { type: 1, verified_by_hr: 1 }).lean();
const isVerified = (type) => docs.some((d) => d.type === type && d.verified_by_hr === true);
const hasAadhar = isVerified('Aadhar');
const hasUan = isVerified('UAN');
const hasAlt = isVerified('ESI') || isVerified('Compensation_Policy');
const hasPvc = isVerified('PVC');
if (!(hasAadhar && hasUan && hasAlt && hasPvc)) {
return res.status(400).json({ message: 'HR verification is incomplete for mandatory docs' });
}
const employee = await Employee.findById(employeeId);
if (!employee) {
return res.status(404).json({ message: 'Employee not found' });
}
const vendor = await Vendor.findById(employee.vendor_id, { is_active: 1 }).lean();
if (!employee.is_active || !vendor?.is_active) {
return res.status(400).json({ message: 'Employee or vendor is inactive' });
}
employee.status = 'HR_Approved';
employee.hr_approved_at = new Date();
await employee.save();
return res.json({ message: 'Employee approved for Safety induction' });
} catch (error) {
return next(error);
}
}
async function generateGatePass(req, res, next) {
try {
const employeeId = req.params.id;
const { issue_date, fee_paid, fee_amount } = req.body;
if (!fee_paid) {
return res.status(400).json({ message: 'Gate pass can be generated only after fee is marked as paid' });
}
const employee = await Employee.findById(employeeId);
if (!employee) {
return res.status(404).json({ message: 'Employee not found' });
}
const vendor = await Vendor.findById(employee.vendor_id, { is_active: 1 }).lean();
if (!employee.is_active || !vendor?.is_active) {
return res.status(400).json({ message: 'Employee or vendor is inactive' });
}
if (!['Safety_Approved', 'Active'].includes(employee.status)) {
return res.status(400).json({ message: 'Employee must complete safety induction before pass issuance' });
}
const gatePass = await GatePass.create({
employee_id: employeeId,
issue_date: new Date(issue_date),
fee_paid,
fee_amount: fee_amount || 100,
barcode: `DSS-${employeeId}-${uuidv4().slice(0, 8).toUpperCase()}`,
status: 'Active',
issued_by: req.user.id
});
employee.status = 'Active';
employee.due_date = gatePass.expiry_date;
await employee.save();
return res.status(201).json({
id: String(gatePass._id),
employee_id: String(gatePass.employee_id),
issue_date: gatePass.issue_date,
expiry_date: gatePass.expiry_date,
fee_paid: gatePass.fee_paid,
fee_amount: gatePass.fee_amount,
barcode: gatePass.barcode,
status: gatePass.status
});
} catch (error) {
return next(error);
}
}
async function renewGatePass(req, res, next) {
try {
const gatePassId = req.params.id;
const { issue_date, fee_paid } = req.body;
if (!fee_paid) {
return res.status(400).json({ message: 'Renewal can be processed only after fee is paid' });
}
const previousPass = await GatePass.findById(gatePassId).lean();
if (!previousPass) {
return res.status(404).json({ message: 'Gate pass not found' });
}
const employee = await Employee.findById(previousPass.employee_id);
if (!employee) {
return res.status(404).json({ message: 'Employee not found' });
}
const vendor = await Vendor.findById(employee.vendor_id, { is_active: 1 }).lean();
if (!employee.is_active || !vendor?.is_active) {
return res.status(400).json({ message: 'Employee or vendor is inactive' });
}
if (new Date(previousPass.expiry_date) >= new Date()) {
return res.status(400).json({ message: 'Pass is not expired yet, renewal is not allowed' });
}
const newPass = await GatePass.create({
employee_id: previousPass.employee_id,
issue_date: new Date(issue_date),
fee_paid,
fee_amount: 100,
barcode: `DSS-${previousPass.employee_id}-${uuidv4().slice(0, 8).toUpperCase()}`,
status: 'Active',
issued_by: req.user.id
});
employee.status = 'Active';
employee.due_date = newPass.expiry_date;
await employee.save();
return res.status(201).json({
message: 'Pass renewed successfully',
new_gate_pass_id: String(newPass._id),
barcode: newPass.barcode
});
} catch (error) {
return next(error);
}
}
async function listRenewRequests(req, res, next) {
try {
const requests = await RenewRequest.find({ status: 'Requested' })
.sort({ created_at: -1 })
.lean();
const employeeIds = toValidObjectIdStrings(requests.map((r) => r.employee_id));
const vendorIds = toValidObjectIdStrings(requests.map((r) => r.vendor_id));
const gatePassIds = toValidObjectIdStrings(requests.map((r) => r.gate_pass_id));
const employees = employeeIds.length
? await Employee.find({ _id: { $in: employeeIds }, is_active: true }, { name: 1, vendor_id: 1 }).lean()
: [];
const vendors = vendorIds.length
? await Vendor.find({ _id: { $in: vendorIds }, is_active: true }, { name: 1 }).lean()
: [];
const gatePasses = gatePassIds.length
? await GatePass.find({ _id: { $in: gatePassIds } }, { expiry_date: 1 }).lean()
: [];
const employeeMap = new Map(employees.map((e) => [String(e._id), e]));
const vendorMap = new Map(vendors.map((v) => [String(v._id), v]));
const gatePassMap = new Map(gatePasses.map((g) => [String(g._id), g]));
const rows = requests
.filter((reqRow) => employeeMap.has(String(reqRow.employee_id)) && vendorMap.has(String(reqRow.vendor_id)))
.map((reqRow) => {
const employee = employeeMap.get(String(reqRow.employee_id));
const vendor = vendorMap.get(String(reqRow.vendor_id));
const gatePass = gatePassMap.get(String(reqRow.gate_pass_id));
return {
id: String(reqRow._id),
gate_pass_id: String(reqRow.gate_pass_id),
employee_id: String(reqRow.employee_id),
vendor_id: String(reqRow.vendor_id),
requested_issue_date: reqRow.requested_issue_date,
status: reqRow.status,
remarks: reqRow.remarks,
employee_name: employee?.name || 'N/A',
vendor_name: vendor?.name || 'N/A',
previous_expiry_date: gatePass?.expiry_date || null
};
});
return res.json(rows);
} catch (error) {
return next(error);
}
}
async function processRenewRequest(req, res, next) {
try {
const requestId = req.params.id;
const { fee_paid, issue_date } = req.body;
if (!fee_paid) {
return res.status(400).json({ message: 'Renewal request can be processed only after fee is paid' });
}
const renewRequest = await RenewRequest.findById(requestId);
if (!renewRequest) {
return res.status(404).json({ message: 'Renewal request not found' });
}
if (renewRequest.status !== 'Requested') {
return res.status(400).json({ message: 'Renewal request already processed' });
}
const previousPass = await GatePass.findById(renewRequest.gate_pass_id).lean();
if (!previousPass) {
return res.status(404).json({ message: 'Previous gate pass not found' });
}
const employee = await Employee.findById(renewRequest.employee_id);
if (!employee) {
return res.status(404).json({ message: 'Employee not found' });
}
const vendor = await Vendor.findById(renewRequest.vendor_id, { is_active: 1 }).lean();
if (!employee.is_active || !vendor?.is_active) {
return res.status(400).json({ message: 'Employee or vendor is inactive' });
}
if (new Date(previousPass.expiry_date) >= new Date()) {
return res.status(400).json({ message: 'Previous pass has not expired' });
}
const effectiveIssueDate = issue_date || dayjs(renewRequest.requested_issue_date).format('YYYY-MM-DD');
const newPass = await GatePass.create({
employee_id: renewRequest.employee_id,
issue_date: new Date(effectiveIssueDate),
fee_paid,
fee_amount: 100,
barcode: `DSS-${renewRequest.employee_id}-${uuidv4().slice(0, 8).toUpperCase()}`,
status: 'Active',
issued_by: req.user.id
});
renewRequest.status = 'Processed';
renewRequest.remarks = 'Processed and activated';
await renewRequest.save();
employee.status = 'Active';
employee.due_date = newPass.expiry_date;
await employee.save();
return res.json({
message: 'Renewal request processed successfully',
gate_pass_id: String(newPass._id),
barcode: newPass.barcode
});
} catch (error) {
return next(error);
}
}
module.exports = {
verifyDocumentValidators,
reviewDocumentValidators,
approveEmployeeValidators,
createGatePassValidators,
renewGatePassValidators,
processRenewRequestValidators,
getPendingVerification,
verifyDocuments,
reviewDocument,
approveForSafety,
generateGatePass,
renewGatePass,
listRenewRequests,
processRenewRequest
};