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