File size: 13,996 Bytes
8c7b7ca
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
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
};