Added time adjustment api

This commit is contained in:
Aakash Mittal 2025-11-13 16:31:36 +05:30
parent 7fac56b5ec
commit dd118a2359
10 changed files with 440 additions and 163 deletions

View File

@ -40,7 +40,7 @@ async function va_login_callback(req, res) {
name: user.userDetails.firstName, name: user.userDetails.firstName,
}, },
process.env.JWT_SECRET_ACCESS_TOKEN || "DocTrackerSecretKey", process.env.JWT_SECRET_ACCESS_TOKEN || "DocTrackerSecretKey",
{ expiresIn: "24h" } // { expiresIn: "24h" }
); );
// Generate JWT refreshToken with 24h expiry // Generate JWT refreshToken with 24h expiry
@ -51,7 +51,7 @@ async function va_login_callback(req, res) {
name: user.userDetails.firstName, name: user.userDetails.firstName,
}, },
process.env.JWT_SECRET_REFRESH_TOKEN || "DocTrackerSecretKeyRefreshToken", process.env.JWT_SECRET_REFRESH_TOKEN || "DocTrackerSecretKeyRefreshToken",
{ expiresIn: "30d" } // { expiresIn: "30d" }
); );
await VAModel.updateOne({ "userDetails.email": userDetails.email }, { await VAModel.updateOne({ "userDetails.email": userDetails.email }, {

View File

@ -1,151 +0,0 @@
import jwt from "jsonwebtoken";
import { VAModel, ProjectModel, LoggedModel } from "../../../models/vaModel.js";
// Adjust missing hours by creating new records
async function va_timesheet_adjustment_function(req, res) {
try {
const accessToken = req.headers.token;
if (!accessToken) {
return res.status(401).json({ success: false, message: "Missing token" });
}
const decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN);
if (!decoded || !decoded.email) {
return res.status(401).json({ success: false, message: "Invalid token" });
}
const vaEmail = decoded.email;
const vaUser = await VAModel.findOne({ "userDetails.email": vaEmail });
if (!vaUser) {
return res.status(404).json({ success: false, message: "VA not found" });
}
const vaId = vaUser._id;
const { projectId, date } = req.params;
const { loginTime, logoutTime, reason } = req.body;
if (!projectId || !date) {
return res.status(400).json({ success: false, message: "Project ID and date are required" });
}
if (!loginTime || !logoutTime) {
return res.status(400).json({ success: false, message: "Login time and logout time are required" });
}
// Validate project exists and VA is assigned to it
const project = await ProjectModel.findOne({
_id: projectId,
vaAssigned: vaId
});
if (!project) {
return res.status(404).json({ success: false, message: "Project not found or VA not assigned to this project" });
}
// Parse and validate times
const adjustmentDate = new Date(date);
const loginDateTime = new Date(loginTime);
const logoutDateTime = new Date(logoutTime);
// Ensure times are within the target date
const startOfDay = new Date(adjustmentDate);
startOfDay.setHours(0, 0, 0, 0);
const endOfDay = new Date(adjustmentDate);
endOfDay.setHours(23, 59, 59, 999);
if (loginDateTime < startOfDay || loginDateTime > endOfDay ||
logoutDateTime < startOfDay || logoutDateTime > endOfDay) {
return res.status(400).json({
success: false,
message: "Login and logout times must be within the specified date"
});
}
if (loginDateTime >= logoutDateTime) {
return res.status(400).json({
success: false,
message: "Login time must be before logout time"
});
}
// Check for existing logs that might conflict with this adjustment
const existingLogs = await LoggedModel.find({
vaId,
projectId,
time: {
$gte: loginDateTime,
$lte: logoutDateTime
}
});
if (existingLogs.length > 0) {
return res.status(400).json({
success: false,
message: "There are existing logs in the specified time range. Please choose a different time range."
});
}
// Create adjustment logs
const loginLog = new LoggedModel({
loggedType: "LOGIN",
time: loginDateTime,
vaId,
vaEmail,
projectId,
isAdjustment: true,
adjustmentReason: reason
});
const logoutLog = new LoggedModel({
loggedType: "LOGOUT",
time: logoutDateTime,
vaId,
vaEmail,
projectId,
isAdjustment: true,
adjustmentReason: reason
});
// Save both logs in transaction
await loginLog.save();
await logoutLog.save();
// Calculate adjusted hours
const adjustedSeconds = (logoutDateTime - loginDateTime) / 1000;
const formatTime = (seconds) => {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
return `${h.toString().padStart(2, "0")}:${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`;
};
const response = {
success: true,
message: "Timesheet adjustment completed successfully",
data: {
adjustment: {
date: adjustmentDate.toISOString().split('T')[0],
loginTime: loginDateTime.toISOString(),
logoutTime: logoutDateTime.toISOString(),
adjustedHours: formatTime(adjustedSeconds),
adjustedHoursDecimal: (adjustedSeconds / 3600).toFixed(2),
reason: reason
},
logs: {
loginLogId: loginLog._id,
logoutLogId: logoutLog._id
}
}
};
return res.status(201).json(response);
} catch (error) {
console.error("Timesheet adjustment error:", error);
return res.status(500).json({ success: false, message: "Internal Server Error" });
}
}
export { va_timesheet_adjustment_function };

View File

@ -34,7 +34,7 @@ async function va_profile_callback(req, res) {
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
message: "VA profile", message: "VA profile",
data: vaUser data: vaUser.userDetails
}); });
} catch (error) { } catch (error) {
return res.status(500).json({ success: false, message: "Internal Server Error" }); return res.status(500).json({ success: false, message: "Internal Server Error" });

View File

@ -1,3 +1,4 @@
import jwt from "jsonwebtoken";
import { LoggedModel, WorkTrackModel, VAModel } from "../../../models/vaModel.js"; import { LoggedModel, WorkTrackModel, VAModel } from "../../../models/vaModel.js";
function formatHMS(ms) { function formatHMS(ms) {
@ -10,6 +11,23 @@ function formatHMS(ms) {
async function va_projects_report_function(req, res) { async function va_projects_report_function(req, res) {
try { try {
const accessToken = req.headers.token;
if (!accessToken) {
return res.status(401).json({ success: false, message: "Missing token" });
}
const decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN);
if (!decoded || !decoded.email) {
return res.status(401).json({ success: false, message: "Invalid token" });
}
const vaEmail = decoded.email;
const vaUser = await VAModel.findOne({ "userDetails.email": vaEmail });
if (!vaUser) {
return res.status(404).json({ success: false, message: "VA not found" });
}
const vaId = vaUser._id;
const { projectId, from, to } = req.params; const { projectId, from, to } = req.params;
if (!projectId) { if (!projectId) {
@ -25,6 +43,7 @@ async function va_projects_report_function(req, res) {
// Get all VA login/logout logs for this project in range // Get all VA login/logout logs for this project in range
const logs = await LoggedModel.find({ const logs = await LoggedModel.find({
projectId, projectId,
vaId,
time: { $gte: fromDate, $lte: toDate } time: { $gte: fromDate, $lte: toDate }
}).sort({ time: 1 }).lean(); }).sort({ time: 1 }).lean();

View File

@ -0,0 +1,71 @@
import { AdjustedHoursModel } from "../../../models/vaModel.js";
// Get all adjustment requests (with filtering options)
async function get_adjustment_requests_list(req, res) {
try {
const { status, projectId, vaId } = req.params;
// Build filter object
const filter = {};
if (status) filter.status = status;
if (projectId) filter.projectId = projectId;
if (vaId) filter.vaId = vaId;
const adjustments = await AdjustedHoursModel.find(filter)
.sort({ createdAt: -1 })
.populate('vaId', 'userDetails.name userDetails.email')
.populate('projectId', 'name')
.lean();
// Format response
const formattedData = adjustments.map(adj => ({
id: adj._id,
va: {
id: adj.vaId._id,
name: adj.vaId.userDetails?.name,
email: adj.vaId.userDetails?.email
},
project: {
id: adj.projectId._id,
name: adj.projectId.name
},
date: adj.date,
loginTime: adj.loginTime,
logoutTime: adj.logoutTime,
comment: adj.comment,
status: adj.status,
requestedAt: adj.createdAt,
reviewedAt: adj.reviewedAt,
rejectionReason: adj.rejectionReason,
adjustedHours: formatTime((new Date(adj.logoutTime) - new Date(adj.loginTime)) / 1000)
}));
return res.status(200).json({
success: true,
data: formattedData,
pagination: {
currentPage: adjustments.page,
totalPages: adjustments.totalPages,
totalItems: adjustments.totalDocs,
hasNext: adjustments.hasNextPage,
hasPrev: adjustments.hasPrevPage
}
});
} catch (error) {
console.error("Get adjustment requests error:", error);
return res.status(500).json({
success: false,
message: "Internal Server Error"
});
}
}
function formatTime(seconds) {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
return `${h}h ${m}m ${s}s`;
}
export { get_adjustment_requests_list }

View File

@ -0,0 +1,122 @@
import { AdjustedHoursModel, LoggedModel } from "../../../models/vaModel.js";
// Approve or reject adjustment request
async function va_timesheet_manage_adjustment_request(req, res) {
try {
const { adjustmentId } = req.params;
const { action, rejectionReason } = req.body; // action: "approve" or "reject"
if (!adjustmentId || !action) {
return res.status(400).json({
success: false,
message: "Adjustment ID and action are required"
});
}
// Find adjustment request
const adjustment = await AdjustedHoursModel.findById(adjustmentId)
.populate('vaId', 'userDetails.email');
if (!adjustment) {
return res.status(404).json({
success: false,
message: "Adjustment request not found"
});
}
if (adjustment.status !== "PENDING") {
return res.status(400).json({
success: false,
message: "Adjustment request has already been processed"
});
}
if (action === "approve") {
// Create new log entries in LoggedModel
const loginLog = new LoggedModel({
loggedType: "LOGIN",
time: adjustment.loginTime,
vaId: adjustment.vaId._id,
vaEmail: adjustment.vaId.userDetails.email,
projectId: adjustment.projectId,
isAdjustment: true,
adjustmentId: adjustment._id
});
const logoutLog = new LoggedModel({
loggedType: "LOGOUT",
time: adjustment.logoutTime,
vaId: adjustment.vaId._id,
vaEmail: adjustment.vaId.userDetails.email,
projectId: adjustment.projectId,
isAdjustment: true,
adjustmentId: adjustment._id
});
await loginLog.save();
await logoutLog.save();
// Update adjustment record
adjustment.status = "APPROVED";
adjustment.reviewedAt = new Date();
adjustment.adjustedLogs = [
{ loginLogId: loginLog._id },
{ logoutLogId: logoutLog._id }
];
await adjustment.save();
return res.status(200).json({
success: true,
message: "Adjustment request approved successfully",
data: {
adjustmentId: adjustment._id,
status: "APPROVED",
loginLogId: loginLog._id,
logoutLogId: logoutLog._id,
adjustedHours: formatTime((adjustment.logoutTime - adjustment.loginTime) / 1000)
}
});
} else if (action === "reject") {
// Update adjustment record as rejected
adjustment.status = "REJECTED";
adjustment.reviewedAt = new Date();
adjustment.rejectionReason = rejectionReason || "No reason provided";
await adjustment.save();
return res.status(200).json({
success: true,
message: "Adjustment request rejected",
data: {
adjustmentId: adjustment._id,
status: "REJECTED",
rejectionReason: adjustment.rejectionReason
}
});
} else {
return res.status(400).json({
success: false,
message: "Invalid action. Use 'approve' or 'reject'"
});
}
} catch (error) {
console.error("Manage adjustment error:", error);
return res.status(500).json({
success: false,
message: "Internal Server Error"
});
}
}
function formatTime(seconds) {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
return `${h}h ${m}m ${s}s`;
}
export { va_timesheet_manage_adjustment_request }

View File

@ -0,0 +1,159 @@
import jwt from "jsonwebtoken";
import { VAModel, ProjectModel, AdjustedHoursModel } from "../../../models/vaModel.js";
// Adjust missing hours by creating new records
async function va_timesheet_adjustment_function(req, res) {
try {
const accessToken = req.headers.token;
if (!accessToken) {
return res.status(401).json({ success: false, message: "Missing token" });
}
const decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN);
if (!decoded || !decoded.email) {
return res.status(401).json({ success: false, message: "Invalid token" });
}
const vaEmail = decoded.email;
const vaUser = await VAModel.findOne({ "userDetails.email": vaEmail });
if (!vaUser) {
return res.status(404).json({ success: false, message: "VA not found" });
}
const vaId = vaUser._id;
const { projectId, date, loginTime, logoutTime, comment } = req.body;
if (!projectId || !date || !loginTime || !logoutTime || !comment) {
return res.status(400).json({
success: false,
message: "Project ID, date, loginTime, logoutTime, and comment are required"
});
}
// Validate project exists and VA is assigned to it
const project = await ProjectModel.findOne({
_id: projectId,
vaAssigned: vaId
});
if (!project) {
return res.status(404).json({ success: false, message: "Project not found or VA not assigned to this project" });
}
let adjustmentDate;
try {
adjustmentDate = new Date(date);
if (isNaN(adjustmentDate.getTime())) {
return res.status(400).json({
success: false,
message: "Invalid date format. Use YYYY-MM-DD"
});
}
} catch (error) {
return res.status(400).json({
success: false,
message: "Invalid date format"
});
}
const combineDateTime = (dateObj, timeStr) => {
const [hours, minutes, seconds] = timeStr.split(':').map(Number);
// Extract date components from the Date object
const year = dateObj.getFullYear();
const month = dateObj.getMonth();
const day = dateObj.getDate();
// Create UTC date directly
return new Date(Date.UTC(year, month, day, hours, minutes, seconds));
};
const loginDateTime = combineDateTime(adjustmentDate, loginTime);
const logoutDateTime = combineDateTime(adjustmentDate, logoutTime);
if (isNaN(adjustmentDate.getTime()) || isNaN(loginDateTime.getTime()) || isNaN(logoutDateTime.getTime())) {
return res.status(400).json({
success: false,
message: "Invalid date format"
});
}
// Ensure login time is before logout time
if (loginDateTime >= logoutDateTime) {
return res.status(400).json({
success: false,
message: "Login time must be before logout time"
});
}
// Ensure times are within the target date
const startOfDay = new Date(adjustmentDate);
startOfDay.setHours(0, 0, 0, 0);
const endOfDay = new Date(adjustmentDate);
endOfDay.setHours(23, 59, 59, 999);
if (loginDateTime < startOfDay || loginDateTime > endOfDay ||
logoutDateTime < startOfDay || logoutDateTime > endOfDay) {
return res.status(400).json({
success: false,
message: "Login and logout times must be within the specified date"
});
}
const existingAdjustment = await AdjustedHoursModel.findOne({
vaId,
projectId,
date: startOfDay,
status: "PENDING"
});
if (existingAdjustment) {
return res.status(400).json({
success: false,
message: "You already have a pending adjustment request for this date and project"
});
}
const adjustmentRequest = new AdjustedHoursModel({
vaId,
projectId,
date: startOfDay,
loginTime: loginDateTime,
logoutTime: logoutDateTime,
comment,
status: "PENDING"
});
await adjustmentRequest.save();
const adjustedSeconds = (logoutDateTime - loginDateTime) / 1000;
const formatTime = (seconds) => {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
return `${h}h ${m}m ${s}s`;
};
return res.status(201).json({
success: true,
message: "Adjustment request submitted successfully",
data: {
adjustmentId: adjustmentRequest._id,
date: adjustmentDate.toISOString().split('T')[0],
loginTime: loginDateTime.toISOString(),
logoutTime: logoutDateTime.toISOString(),
adjustedHours: formatTime(adjustedSeconds),
comment: comment,
status: "PENDING"
}
});
} catch (error) {
console.error("Timesheet adjustment error:", error);
return res.status(500).json({ success: false, message: "Internal Server Error" });
}
}
export { va_timesheet_adjustment_function };

View File

@ -86,7 +86,7 @@ async function va_timesheet_summary_function(req, res) {
if (!firstLoginTime || logTime < firstLoginTime) { if (!firstLoginTime || logTime < firstLoginTime) {
firstLoginTime = logTime; firstLoginTime = logTime;
} }
} else if (log.loggedType === "LOGOUT" && lastLogin) { } else if (log.loggedType === "LOGOUT") {
if (loginStack.length > 0) { if (loginStack.length > 0) {
// Pop the last login from stack // Pop the last login from stack
const lastLogin = loginStack.pop(); const lastLogin = loginStack.pop();
@ -157,7 +157,6 @@ async function va_timesheet_summary_function(req, res) {
const response = { const response = {
success: true, success: true,
data: { data: {
date: targetDate.toISOString().split('T')[0],
project: { project: {
id: project._id, id: project._id,
name: project.name, name: project.name,
@ -171,10 +170,10 @@ async function va_timesheet_summary_function(req, res) {
summary: { summary: {
totalWorkedHours: formatTime(totalWorkSeconds), totalWorkedHours: formatTime(totalWorkSeconds),
totalWorkedHoursDetailed: formatTimeDetailed(totalWorkSeconds), totalWorkedHoursDetailed: formatTimeDetailed(totalWorkSeconds),
totalWorkedHoursDecimal: totalWorkedHours.toFixed(4), totalWorkedHoursDecimal: totalWorkedHours.toFixed(2),
totalWorkedMinutes: (totalWorkSeconds / 60).toFixed(2), totalWorkedMinutes: (totalWorkSeconds / 60).toFixed(2),
dedicatedHours: dedicatedHours, dedicatedHours: dedicatedHours,
pendingHours: pendingHours.toFixed(4), pendingHours: pendingHours.toFixed(2),
firstLoginTime: firstLoginTime ? firstLoginTime.toISOString() : null, firstLoginTime: firstLoginTime ? firstLoginTime.toISOString() : null,
lastLogoutTime: lastLogoutTime ? lastLogoutTime.toISOString() : null lastLogoutTime: lastLogoutTime ? lastLogoutTime.toISOString() : null
}, },

View File

@ -358,6 +358,57 @@ passwordResetSchema.index({ email: 1, used: 1 });
passwordResetSchema.index({ token: 1, used: 1, expiresAt: 1 }); passwordResetSchema.index({ token: 1, used: 1, expiresAt: 1 });
passwordResetSchema.index({ userType: 1, email: 1 }); passwordResetSchema.index({ userType: 1, email: 1 });
const adjustedHoursSchema = new Schema({
vaId: {
type: Types.ObjectId,
ref: "VA",
required: true
},
projectId: {
type: Types.ObjectId,
ref: "Project",
required: true
},
date: {
type: Date,
required: true
},
loginTime: {
type: Date,
required: true
},
logoutTime: {
type: Date,
required: true
},
comment: {
type: String,
required: true
},
status: {
type: String,
enum: ["PENDING", "APPROVED", "REJECTED"],
default: "PENDING"
},
reviewedAt: {
type: Date
},
rejectionReason: {
type: String // If rejected, store the reason
},
adjustedLogs: [{
loginLogId: {
type: Types.ObjectId,
ref: "Logged"
},
logoutLogId: {
type: Types.ObjectId,
ref: "Logged"
}
}]
}, { timestamps: true });
/* ---------------------- Model Exports ---------------------- */ /* ---------------------- Model Exports ---------------------- */
const VAModel = model("VA", vaSchema); const VAModel = model("VA", vaSchema);
const ClientModel = model("Client", clientSchema); const ClientModel = model("Client", clientSchema);
@ -365,5 +416,6 @@ const ProjectModel = model("Project", projectSchema);
const LoggedModel = model("TrackLog", loggedSchema); const LoggedModel = model("TrackLog", loggedSchema);
const WorkTrackModel = model("TrackWork", vaWorkTrackSchema); const WorkTrackModel = model("TrackWork", vaWorkTrackSchema);
const PasswordResetModel = model("PasswordReset", passwordResetSchema); const PasswordResetModel = model("PasswordReset", passwordResetSchema);
const AdjustedHoursModel = model("AdjustedHours", adjustedHoursSchema);
export { VAModel, ClientModel, ProjectModel, LoggedModel, WorkTrackModel, PasswordResetModel }; export { VAModel, ClientModel, ProjectModel, LoggedModel, WorkTrackModel, PasswordResetModel, AdjustedHoursModel };

View File

@ -18,8 +18,10 @@ import { va_log_work_report_callback } from "../../controllers/va/vaLog/vaLogWor
import { va_projects_report_function } from "../../controllers/va/vaProjects/vaProjectReport.controller.js"; import { va_projects_report_function } from "../../controllers/va/vaProjects/vaProjectReport.controller.js";
import { va_projects_log_list_function } from "../../controllers/va/vaProjects/vaProjectLogList.controller.js"; import { va_projects_log_list_function } from "../../controllers/va/vaProjects/vaProjectLogList.controller.js";
import { va_request_password_reset } from "../../controllers/passwordReset.controllers.js"; import { va_request_password_reset } from "../../controllers/passwordReset.controllers.js";
import { va_timesheet_summary_function } from "../../controllers/va/vaLog/vaTimesheetAdjustmentSummary.controller.js"; import { va_timesheet_summary_function } from "../../controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustmentSummary.controller.js";
import { va_timesheet_adjustment_function } from "../../controllers/va/vaLog/vaTimesheetAdjustment.controller.js"; import { va_timesheet_adjustment_function } from "../../controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustment.controller.js";
import { get_adjustment_requests_list } from "../../controllers/va/vaTimesheetAdjustment/vaAllTimesheetAdjustmentReuqest.controller.js"
import { va_timesheet_manage_adjustment_request } from "../../controllers/va/vaTimesheetAdjustment/vaManageTimesheetAdjustment.controller.js"
vaRoutes.get("/", va_callback); // VA Callback for getting active VA vaRoutes.get("/", va_callback); // VA Callback for getting active VA
@ -36,9 +38,13 @@ vaRoutes.patch("/profile", authValidator, va_profile_update_callback); // Update
vaRoutes.get("/:userName", va_username_callback); // VA Username Callback (NC) vaRoutes.get("/:userName", va_username_callback); // VA Username Callback (NC)
// timesheet adjustment routes
vaRoutes.get("/timesheet/summary/:projectId/:date", authValidator, va_timesheet_summary_function); // Get Timesheet Adjustment for VA on specific project and date vaRoutes.get("/timesheet/summary/:projectId/:date", authValidator, va_timesheet_summary_function); // Get Timesheet Adjustment for VA on specific project and date
vaRoutes.post("/timesheet/adjustment/:projectId/:date", authValidator, va_timesheet_adjustment_function); // Get Timesheet Adjustment for VA on specific project and date vaRoutes.post("/timesheet/adjustment/request", authValidator, va_timesheet_adjustment_function); // Timesheet Adjustment for VA
vaRoutes.get("/timesheet/adjustments/list/:status?/:projectId?/:vaId?", get_adjustment_requests_list); // show all adjustment list to approve and reject
vaRoutes.patch("/timesheet/adjustments/:adjustmentId", va_timesheet_manage_adjustment_request);
// timesheet adjustment routes
// Example: GET /va/search?name=john&email=@example.com&from=2025-01-01&to=2025-01-31 (NC) // Example: GET /va/search?name=john&email=@example.com&from=2025-01-01&to=2025-01-31 (NC)
vaRoutes.get("/search", va_filter_search); vaRoutes.get("/search", va_filter_search);