|
| 1 | +const mongoose = require('mongoose'); |
| 2 | +const { SCHEMA_TYPE } = require('../../Config/schemaType'); |
| 3 | +const { MongoDbCrudOpration } = require('../../utils/mongo-handler/mongoQueries'); |
| 4 | +const { getRoleType, isPrivileged } = require('../../Config/permissionGuard'); |
| 5 | +const { removeCache } = require('../../utils/commonFunctions'); |
| 6 | +const logger = require('../../Config/loggerConfig'); |
| 7 | +const R = require('./helpers/ptoRules'); |
| 8 | + |
| 9 | +const companyOf = (req) => req.headers['companyid'] || (req.body && req.body.companyId) || (req.query && req.query.companyId); |
| 10 | +const audit = (req, entry) => { try { require('../Audit/recorder').recordAuditFromReq(req, entry); } catch (e) { /* best-effort */ } }; |
| 11 | + |
| 12 | +// POST /api/v1/pto — create a PTO entry. A member creates their own (pending); |
| 13 | +// owner/admin may create for another user and/or set status (e.g. auto-approve). |
| 14 | +exports.createPto = async (req, res) => { |
| 15 | + try { |
| 16 | + const companyId = companyOf(req); |
| 17 | + if (!companyId) return res.status(400).json({ status: false, statusText: 'companyId is required.' }); |
| 18 | + const roleType = await getRoleType(companyId, req.uid); |
| 19 | + const privileged = isPrivileged(roleType); |
| 20 | + const body = req.body || {}; |
| 21 | + const targetUser = privileged && body.userId ? String(body.userId) : String(req.uid); |
| 22 | + const status = privileged && body.status ? body.status : 'pending'; |
| 23 | + const check = R.validatePtoEntry({ ...body, userId: targetUser, status }); |
| 24 | + if (!check.valid) return res.status(400).json({ status: false, statusText: check.errors.join('; ') }); |
| 25 | + const data = { ...check.value, createdBy: String(req.uid || ''), deletedStatusKey: 0 }; |
| 26 | + if (check.value.status === 'approved') data.approvedBy = String(req.uid || ''); |
| 27 | + const saved = await MongoDbCrudOpration(companyId, { type: SCHEMA_TYPE.PTO_ENTRIES, data }, 'save'); |
| 28 | + removeCache(`pto:${companyId}`); |
| 29 | + audit(req, { action: 'pto.create', entityType: 'pto', entityId: String(saved._id), meta: { type: data.type, userId: targetUser } }); |
| 30 | + return res.status(201).json({ status: true, statusText: 'PTO entry created.', data: saved }); |
| 31 | + } catch (e) { logger.error(`createPto: ${e.message}`); return res.status(500).json({ status: false, statusText: e.message }); } |
| 32 | +}; |
| 33 | + |
| 34 | +// GET /api/v1/pto?userId=&from=&to=&status= — list (calendar feed). A member sees |
| 35 | +// their own; owner/admin see everyone (optionally filtered by userId). |
| 36 | +exports.listPto = async (req, res) => { |
| 37 | + try { |
| 38 | + const companyId = companyOf(req); |
| 39 | + if (!companyId) return res.status(400).json({ status: false, statusText: 'companyId is required.' }); |
| 40 | + const roleType = await getRoleType(companyId, req.uid); |
| 41 | + const privileged = isPrivileged(roleType); |
| 42 | + const q = req.query || {}; |
| 43 | + const match = { deletedStatusKey: { $ne: 1 } }; |
| 44 | + if (privileged) { if (q.userId) match.userId = String(q.userId); } |
| 45 | + else { match.userId = String(req.uid); } |
| 46 | + if (q.status) match.status = String(q.status); |
| 47 | + if (q.from || q.to) { |
| 48 | + const and = []; |
| 49 | + if (q.to) and.push({ startDate: { $lte: new Date(q.to) } }); |
| 50 | + if (q.from) and.push({ endDate: { $gte: new Date(q.from) } }); |
| 51 | + if (and.length) match.$and = and; |
| 52 | + } |
| 53 | + const rows = await MongoDbCrudOpration(companyId, { |
| 54 | + type: SCHEMA_TYPE.PTO_ENTRIES, data: [match, {}, { sort: { startDate: -1 } }], |
| 55 | + }, 'find'); |
| 56 | + return res.json({ status: true, data: rows || [] }); |
| 57 | + } catch (e) { logger.error(`listPto: ${e.message}`); return res.status(500).json({ status: false, statusText: e.message }); } |
| 58 | +}; |
| 59 | + |
| 60 | +// PUT /api/v1/pto/:id/status — approve / reject (owner/admin only). |
| 61 | +exports.updatePtoStatus = async (req, res) => { |
| 62 | + try { |
| 63 | + const companyId = companyOf(req); |
| 64 | + if (!companyId) return res.status(400).json({ status: false, statusText: 'companyId is required.' }); |
| 65 | + const roleType = await getRoleType(companyId, req.uid); |
| 66 | + if (!isPrivileged(roleType)) return res.status(403).json({ status: false, statusText: 'Owner/admin only.' }); |
| 67 | + const status = String(req.body.status || ''); |
| 68 | + if (!R.PTO_STATUS.includes(status)) return res.status(400).json({ status: false, statusText: 'Invalid status.' }); |
| 69 | + const id = req.params.id; |
| 70 | + const updated = await MongoDbCrudOpration(companyId, { |
| 71 | + type: SCHEMA_TYPE.PTO_ENTRIES, |
| 72 | + data: [{ _id: new mongoose.Types.ObjectId(String(id)) }, { $set: { status, approvedBy: String(req.uid || '') } }, { returnDocument: 'after' }], |
| 73 | + }, 'findOneAndUpdate'); |
| 74 | + if (!updated) return res.status(404).json({ status: false, statusText: 'Not found.' }); |
| 75 | + removeCache(`pto:${companyId}`); |
| 76 | + audit(req, { action: `pto.${status}`, entityType: 'pto', entityId: String(id) }); |
| 77 | + return res.json({ status: true, statusText: `PTO ${status}.`, data: updated }); |
| 78 | + } catch (e) { logger.error(`updatePtoStatus: ${e.message}`); return res.status(500).json({ status: false, statusText: e.message }); } |
| 79 | +}; |
| 80 | + |
| 81 | +// DELETE /api/v1/pto/:id — soft-delete. The entry's owner, or any owner/admin. |
| 82 | +exports.deletePto = async (req, res) => { |
| 83 | + try { |
| 84 | + const companyId = companyOf(req); |
| 85 | + if (!companyId) return res.status(400).json({ status: false, statusText: 'companyId is required.' }); |
| 86 | + const roleType = await getRoleType(companyId, req.uid); |
| 87 | + const privileged = isPrivileged(roleType); |
| 88 | + const id = req.params.id; |
| 89 | + const entry = await MongoDbCrudOpration(companyId, { |
| 90 | + type: SCHEMA_TYPE.PTO_ENTRIES, data: [{ _id: new mongoose.Types.ObjectId(String(id)) }], |
| 91 | + }, 'findOne'); |
| 92 | + if (!entry) return res.status(404).json({ status: false, statusText: 'Not found.' }); |
| 93 | + if (!privileged && String(entry.userId) !== String(req.uid)) { |
| 94 | + return res.status(403).json({ status: false, statusText: 'You can only remove your own time off.' }); |
| 95 | + } |
| 96 | + await MongoDbCrudOpration(companyId, { |
| 97 | + type: SCHEMA_TYPE.PTO_ENTRIES, |
| 98 | + data: [{ _id: new mongoose.Types.ObjectId(String(id)) }, { $set: { deletedStatusKey: 1 } }], |
| 99 | + }, 'updateOne'); |
| 100 | + removeCache(`pto:${companyId}`); |
| 101 | + return res.json({ status: true, statusText: 'PTO entry removed.' }); |
| 102 | + } catch (e) { logger.error(`deletePto: ${e.message}`); return res.status(500).json({ status: false, statusText: e.message }); } |
| 103 | +}; |
| 104 | + |
| 105 | +// GET /api/v1/pto/capacity?userId=&from=&to=&hoursPerDay= — available capacity for |
| 106 | +// a user over a range, with APPROVED PTO subtracted. This is the SEC-08 done-when: |
| 107 | +// PTO entries reduce a user's available capacity (feeds REP-06 capacity planning). |
| 108 | +exports.getCapacity = async (req, res) => { |
| 109 | + try { |
| 110 | + const companyId = companyOf(req); |
| 111 | + if (!companyId) return res.status(400).json({ status: false, statusText: 'companyId is required.' }); |
| 112 | + const roleType = await getRoleType(companyId, req.uid); |
| 113 | + const privileged = isPrivileged(roleType); |
| 114 | + const q = req.query || {}; |
| 115 | + const userId = privileged && q.userId ? String(q.userId) : String(req.uid); |
| 116 | + if (!q.from || !q.to) return res.status(400).json({ status: false, statusText: 'from and to are required.' }); |
| 117 | + const workingHoursPerDay = Number(q.hoursPerDay) > 0 ? Number(q.hoursPerDay) : R.DEFAULT_HOURS_PER_DAY; |
| 118 | + const entries = await MongoDbCrudOpration(companyId, { |
| 119 | + type: SCHEMA_TYPE.PTO_ENTRIES, |
| 120 | + data: [{ |
| 121 | + userId, status: 'approved', deletedStatusKey: { $ne: 1 }, |
| 122 | + startDate: { $lte: new Date(q.to) }, endDate: { $gte: new Date(q.from) }, |
| 123 | + }], |
| 124 | + }, 'find'); |
| 125 | + const capacity = R.computeAvailableCapacity({ |
| 126 | + rangeStart: q.from, rangeEnd: q.to, ptoEntries: entries || [], workingHoursPerDay, |
| 127 | + }); |
| 128 | + return res.json({ status: true, data: { userId, from: q.from, to: q.to, ...capacity } }); |
| 129 | + } catch (e) { logger.error(`getCapacity: ${e.message}`); return res.status(500).json({ status: false, statusText: e.message }); } |
| 130 | +}; |
0 commit comments