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