From dd118a235982f78e3665c0602a6f548c491cf691 Mon Sep 17 00:00:00 2001 From: Aakash Date: Thu, 13 Nov 2025 16:31:36 +0530 Subject: [PATCH 1/4] Added time adjustment api --- .../va/vaAuth/vaLogin.controller.js | 4 +- .../vaLog/vaTimesheetAdjustment.controller.js | 151 ----------------- .../va/vaProfile/vaProfile.controller.js | 2 +- .../vaProjects/vaProjectReport.controller.js | 19 +++ ...llTimesheetAdjustmentReuqest.controller.js | 71 ++++++++ .../vaManageTimesheetAdjustment.controller.js | 122 ++++++++++++++ .../vaTimesheetAdjustment.controller.js | 159 ++++++++++++++++++ ...vaTimesheetAdjustmentSummary.controller.js | 7 +- src/models/vaModel.js | 54 +++++- src/routes/v1/va.routes.js | 14 +- 10 files changed, 440 insertions(+), 163 deletions(-) delete mode 100644 src/controllers/va/vaLog/vaTimesheetAdjustment.controller.js create mode 100644 src/controllers/va/vaTimesheetAdjustment/vaAllTimesheetAdjustmentReuqest.controller.js create mode 100644 src/controllers/va/vaTimesheetAdjustment/vaManageTimesheetAdjustment.controller.js create mode 100644 src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustment.controller.js rename src/controllers/va/{vaLog => vaTimesheetAdjustment}/vaTimesheetAdjustmentSummary.controller.js (96%) diff --git a/src/controllers/va/vaAuth/vaLogin.controller.js b/src/controllers/va/vaAuth/vaLogin.controller.js index 625280f..d603004 100644 --- a/src/controllers/va/vaAuth/vaLogin.controller.js +++ b/src/controllers/va/vaAuth/vaLogin.controller.js @@ -40,7 +40,7 @@ async function va_login_callback(req, res) { name: user.userDetails.firstName, }, process.env.JWT_SECRET_ACCESS_TOKEN || "DocTrackerSecretKey", - { expiresIn: "24h" } + // { expiresIn: "24h" } ); // Generate JWT refreshToken with 24h expiry @@ -51,7 +51,7 @@ async function va_login_callback(req, res) { name: user.userDetails.firstName, }, process.env.JWT_SECRET_REFRESH_TOKEN || "DocTrackerSecretKeyRefreshToken", - { expiresIn: "30d" } + // { expiresIn: "30d" } ); await VAModel.updateOne({ "userDetails.email": userDetails.email }, { diff --git a/src/controllers/va/vaLog/vaTimesheetAdjustment.controller.js b/src/controllers/va/vaLog/vaTimesheetAdjustment.controller.js deleted file mode 100644 index fafbc59..0000000 --- a/src/controllers/va/vaLog/vaTimesheetAdjustment.controller.js +++ /dev/null @@ -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 }; \ No newline at end of file diff --git a/src/controllers/va/vaProfile/vaProfile.controller.js b/src/controllers/va/vaProfile/vaProfile.controller.js index 869de05..6417dce 100644 --- a/src/controllers/va/vaProfile/vaProfile.controller.js +++ b/src/controllers/va/vaProfile/vaProfile.controller.js @@ -34,7 +34,7 @@ async function va_profile_callback(req, res) { return res.status(200).json({ success: true, message: "VA profile", - data: vaUser + data: vaUser.userDetails }); } catch (error) { return res.status(500).json({ success: false, message: "Internal Server Error" }); diff --git a/src/controllers/va/vaProjects/vaProjectReport.controller.js b/src/controllers/va/vaProjects/vaProjectReport.controller.js index c140fa1..2b2bbc6 100644 --- a/src/controllers/va/vaProjects/vaProjectReport.controller.js +++ b/src/controllers/va/vaProjects/vaProjectReport.controller.js @@ -1,3 +1,4 @@ +import jwt from "jsonwebtoken"; import { LoggedModel, WorkTrackModel, VAModel } from "../../../models/vaModel.js"; function formatHMS(ms) { @@ -10,6 +11,23 @@ function formatHMS(ms) { async function va_projects_report_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, from, to } = req.params; 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 const logs = await LoggedModel.find({ projectId, + vaId, time: { $gte: fromDate, $lte: toDate } }).sort({ time: 1 }).lean(); diff --git a/src/controllers/va/vaTimesheetAdjustment/vaAllTimesheetAdjustmentReuqest.controller.js b/src/controllers/va/vaTimesheetAdjustment/vaAllTimesheetAdjustmentReuqest.controller.js new file mode 100644 index 0000000..4ba9488 --- /dev/null +++ b/src/controllers/va/vaTimesheetAdjustment/vaAllTimesheetAdjustmentReuqest.controller.js @@ -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 } \ No newline at end of file diff --git a/src/controllers/va/vaTimesheetAdjustment/vaManageTimesheetAdjustment.controller.js b/src/controllers/va/vaTimesheetAdjustment/vaManageTimesheetAdjustment.controller.js new file mode 100644 index 0000000..f114406 --- /dev/null +++ b/src/controllers/va/vaTimesheetAdjustment/vaManageTimesheetAdjustment.controller.js @@ -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 } \ No newline at end of file diff --git a/src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustment.controller.js b/src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustment.controller.js new file mode 100644 index 0000000..6614c71 --- /dev/null +++ b/src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustment.controller.js @@ -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 }; \ No newline at end of file diff --git a/src/controllers/va/vaLog/vaTimesheetAdjustmentSummary.controller.js b/src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustmentSummary.controller.js similarity index 96% rename from src/controllers/va/vaLog/vaTimesheetAdjustmentSummary.controller.js rename to src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustmentSummary.controller.js index 010afea..e0d2096 100644 --- a/src/controllers/va/vaLog/vaTimesheetAdjustmentSummary.controller.js +++ b/src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustmentSummary.controller.js @@ -86,7 +86,7 @@ async function va_timesheet_summary_function(req, res) { if (!firstLoginTime || logTime < firstLoginTime) { firstLoginTime = logTime; } - } else if (log.loggedType === "LOGOUT" && lastLogin) { + } else if (log.loggedType === "LOGOUT") { if (loginStack.length > 0) { // Pop the last login from stack const lastLogin = loginStack.pop(); @@ -157,7 +157,6 @@ async function va_timesheet_summary_function(req, res) { const response = { success: true, data: { - date: targetDate.toISOString().split('T')[0], project: { id: project._id, name: project.name, @@ -171,10 +170,10 @@ async function va_timesheet_summary_function(req, res) { summary: { totalWorkedHours: formatTime(totalWorkSeconds), totalWorkedHoursDetailed: formatTimeDetailed(totalWorkSeconds), - totalWorkedHoursDecimal: totalWorkedHours.toFixed(4), + totalWorkedHoursDecimal: totalWorkedHours.toFixed(2), totalWorkedMinutes: (totalWorkSeconds / 60).toFixed(2), dedicatedHours: dedicatedHours, - pendingHours: pendingHours.toFixed(4), + pendingHours: pendingHours.toFixed(2), firstLoginTime: firstLoginTime ? firstLoginTime.toISOString() : null, lastLogoutTime: lastLogoutTime ? lastLogoutTime.toISOString() : null }, diff --git a/src/models/vaModel.js b/src/models/vaModel.js index 179bbc0..aaf9fb3 100644 --- a/src/models/vaModel.js +++ b/src/models/vaModel.js @@ -358,6 +358,57 @@ passwordResetSchema.index({ email: 1, used: 1 }); passwordResetSchema.index({ token: 1, used: 1, expiresAt: 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 ---------------------- */ const VAModel = model("VA", vaSchema); const ClientModel = model("Client", clientSchema); @@ -365,5 +416,6 @@ const ProjectModel = model("Project", projectSchema); const LoggedModel = model("TrackLog", loggedSchema); const WorkTrackModel = model("TrackWork", vaWorkTrackSchema); const PasswordResetModel = model("PasswordReset", passwordResetSchema); +const AdjustedHoursModel = model("AdjustedHours", adjustedHoursSchema); -export { VAModel, ClientModel, ProjectModel, LoggedModel, WorkTrackModel, PasswordResetModel }; \ No newline at end of file +export { VAModel, ClientModel, ProjectModel, LoggedModel, WorkTrackModel, PasswordResetModel, AdjustedHoursModel }; \ No newline at end of file diff --git a/src/routes/v1/va.routes.js b/src/routes/v1/va.routes.js index 99e431d..59533cf 100644 --- a/src/routes/v1/va.routes.js +++ b/src/routes/v1/va.routes.js @@ -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_log_list_function } from "../../controllers/va/vaProjects/vaProjectLogList.controller.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_adjustment_function } from "../../controllers/va/vaLog/vaTimesheetAdjustment.controller.js"; +import { va_timesheet_summary_function } from "../../controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustmentSummary.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 @@ -36,9 +38,13 @@ vaRoutes.patch("/profile", authValidator, va_profile_update_callback); // Update 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.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) vaRoutes.get("/search", va_filter_search); From 0eec95294aae96f461b650b74d3696395bdf31e9 Mon Sep 17 00:00:00 2001 From: Aakash Date: Thu, 13 Nov 2025 20:05:19 +0530 Subject: [PATCH 2/4] Added adjusted hours route and merge commits --- package-lock.json | 240 +++++++++++++- package.json | 1 + .../va/vaLog/vaListLog.controller.js | 130 +++++++- .../va/vaLog/vaLogTrackWork.controller.js | 3 +- .../vaProfile/vaProfileUpdate.controller.js | 47 +-- .../vaProjects/vaProjectLogList.controller.js | 7 +- src/middlewares/auth.middleware.js | 51 +-- src/models/vaModel.js | 4 + src/routes/v1/va.routes.js | 2 +- src/server.js | 249 ++++++++++++++- src/testApiVpnConnection.js | 32 ++ views/client_login.ejs | 297 ++++++++++++++++++ views/forgot_password.ejs | 193 ++++++++++++ views/va_live_tracker.ejs | 143 +++++++++ 14 files changed, 1328 insertions(+), 71 deletions(-) create mode 100644 src/testApiVpnConnection.js create mode 100644 views/client_login.ejs create mode 100644 views/forgot_password.ejs create mode 100644 views/va_live_tracker.ejs diff --git a/package-lock.json b/package-lock.json index 0dfee86..ea79ccf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,6 +36,7 @@ "puppeteer": "^24.10.1", "request": "^2.88.2", "require": "^2.4.20", + "socket.io": "^4.8.1", "uglify-js": "^3.19.3", "uuid": "^11.0.3", "winston": "^3.17.0", @@ -1976,12 +1977,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, "node_modules/@tootallnate/quickjs-emscripten": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", "license": "MIT" }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -2022,7 +2038,6 @@ "version": "22.10.6", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.6.tgz", "integrity": "sha512-qNiuwC4ZDAUNcY47xgaSuS92cjf8JbSUoaKS77bmLG1rU7MlATVSiw/IlrjtIyyskXBZ8KkNfjK/P5na7rgXbQ==", - "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~6.20.0" @@ -2729,6 +2744,15 @@ ], "license": "MIT" }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/base64url": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", @@ -3867,6 +3891,88 @@ "once": "^1.4.0" } }, + "node_modules/engine.io": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", + "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/engine.io/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/enhanced-resolve": { "version": "5.18.3", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", @@ -8127,6 +8233,137 @@ "npm": ">= 3.0.0" } }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "license": "MIT", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-adapter/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io-adapter/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/socks": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.5.tgz", @@ -8642,7 +8879,6 @@ "version": "6.20.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "devOptional": true, "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { diff --git a/package.json b/package.json index b5a7865..846e8c2 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "puppeteer": "^24.10.1", "request": "^2.88.2", "require": "^2.4.20", + "socket.io": "^4.8.1", "uglify-js": "^3.19.3", "uuid": "^11.0.3", "winston": "^3.17.0", diff --git a/src/controllers/va/vaLog/vaListLog.controller.js b/src/controllers/va/vaLog/vaListLog.controller.js index 770952d..f0249ed 100644 --- a/src/controllers/va/vaLog/vaListLog.controller.js +++ b/src/controllers/va/vaLog/vaListLog.controller.js @@ -3,34 +3,138 @@ import { VAModel, LoggedModel } from "../../../models/vaModel.js"; // Getting all active VA's and their details async function va_callback(req, res) { try { - // Find the latest log entry for each VA and return those whose last entry is LOGIN + // Find the latest log entry for each VA const agg = await LoggedModel.aggregate([ { $sort: { time: -1 } }, - { $group: { _id: "$vaId", lastLog: { $first: "$$ROOT" } } }, - { $match: { "lastLog.loggedType": "LOGIN" } } + { + $group: { + _id: "$vaId", + lastLog: { $first: "$$ROOT" }, + totalLogins: { + $sum: { + $cond: [{ $eq: ["$loggedType", "LOGIN"] }, 1, 0] + } + }, + lastActivity: { $first: "$time" } + } + }, + { + $match: { + "lastLog.loggedType": "LOGIN" + } + } ]); const vaIds = agg.map((a) => a._id).filter(Boolean); if (vaIds.length === 0) { - return res.status(200).json({ success: true, count: 0, data: [] }); + return res.status(200).json({ + success: true, + count: 0, + data: [], + message: "No active VAs found" + }); } + // Get VA details with additional fields + const vas = await VAModel.find({ _id: { $in: vaIds } }) + .select('userDetails.name userDetails.email userDetails.phone status createdAt') + .lean() + .exec(); - - const vas = await VAModel.find({ _id: { $in: vaIds } }).lean().exec(); - - // map lastLog by VA id for easy attachment - const lastLogByVa = agg.reduce((acc, a) => { - acc[String(a._id)] = a.lastLog; + // Create mapping for last log and activity data + const activityByVa = agg.reduce((acc, a) => { + acc[String(a._id)] = { + lastLog: a.lastLog, + totalLogins: a.totalLogins, + lastActivity: a.lastActivity + }; return acc; }, {}); - const results = vas.map((v) => ({ ...v, lastLog: lastLogByVa[String(v._id)] || null })); + // Calculate session duration for currently active VAs + const currentTime = new Date(); + const results = vas.map((v) => { + const activity = activityByVa[String(v._id)] || {}; + const lastLog = activity.lastLog; + + // Calculate current session duration if still logged in + let currentSessionDuration = null; + let isCurrentlyActive = false; + + if (lastLog && lastLog.loggedType === "LOGIN") { + const loginTime = new Date(lastLog.time); + currentSessionDuration = Math.floor((currentTime - loginTime) / 1000); // in seconds + isCurrentlyActive = true; + + // Consider VA inactive if last login was more than 24 hours ago + const hoursSinceLastActivity = (currentTime - loginTime) / (1000 * 60 * 60); + if (hoursSinceLastActivity > 24) { + isCurrentlyActive = false; + } + } + + return { + id: v._id, + name: v.userDetails?.name || 'Unknown', + email: v.userDetails?.email, + phone: v.userDetails?.phone, + status: v.status || 'active', + isCurrentlyActive: isCurrentlyActive, + lastLoginTime: lastLog?.time || null, + lastActivity: activity.lastActivity, + currentSessionDuration: currentSessionDuration, + currentSessionFormatted: currentSessionDuration ? formatTime(currentSessionDuration) : null, + totalLogins: activity.totalLogins || 0, + lastProject: lastLog?.projectId || null, + vaSince: v.createdAt + }; + }); + + // Separate active and inactive VAs + const activeVAs = results.filter(va => va.isCurrentlyActive); + const inactiveButLoggedIn = results.filter(va => !va.isCurrentlyActive); + + return res.status(200).json({ + success: true, + count: { + total: results.length, + active: activeVAs.length, + inactive: inactiveButLoggedIn.length + }, + data: { + active: activeVAs, + inactive: inactiveButLoggedIn + }, + summary: { + currentlyWorking: activeVAs.length, + totalLoggedIn: results.length, + lastUpdated: currentTime.toISOString() + } + }); - return res.status(200).json({ success: true, count: results.length, data: results }); } catch (error) { - return res.status(500).send("Internal Server Error"); + console.error("VA callback error:", error); + return res.status(500).json({ + success: false, + message: "Internal Server Error", + error: error.message + }); + } +} + +// Helper function to format time +function formatTime(seconds) { + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const remainingSeconds = seconds % 60; + + if (hours > 0) { + return `${hours}h ${minutes}m ${remainingSeconds}s`; + } else if (minutes > 0) { + return `${minutes}m ${remainingSeconds}s`; + } else { + return `${remainingSeconds}s`; } } diff --git a/src/controllers/va/vaLog/vaLogTrackWork.controller.js b/src/controllers/va/vaLog/vaLogTrackWork.controller.js index e1d908e..97bd382 100644 --- a/src/controllers/va/vaLog/vaLogTrackWork.controller.js +++ b/src/controllers/va/vaLog/vaLogTrackWork.controller.js @@ -4,8 +4,7 @@ import { WorkTrackModel, VAModel, ProjectModel, LoggedModel } from "../../../mod // Getting logged hours in a specific project of VA async function va_log_track_work_callback(req, res) { try { - const accessToken = req.headers.token; - + const accessToken = req.headers.token; if (!accessToken) { return res.status(401).json({ success: false, message: "Missing token" }); } diff --git a/src/controllers/va/vaProfile/vaProfileUpdate.controller.js b/src/controllers/va/vaProfile/vaProfileUpdate.controller.js index b5229d3..66a78b3 100644 --- a/src/controllers/va/vaProfile/vaProfileUpdate.controller.js +++ b/src/controllers/va/vaProfile/vaProfileUpdate.controller.js @@ -29,23 +29,23 @@ async function va_profile_update_callback(req, res) { const payload = req.body || {}; const { userDetails } = payload; - + if (!userDetails) { return res.status(400).json({ success: false, message: "Missing userDetails" }); } // Allowed fields to update - const allowedFields = [ - "firstName", "lastName", "dob", "age", "address", - "bankName", "accountHolderName", "accountNumber", "routingNumber" - ]; + // const allowedFields = [ + // "firstName", "lastName", "dob", "age", "address", + // "bankName", "accountHolderName", "accountNumber", "routingNumber" + // ]; - // Update allowed fields only - allowedFields.forEach(field => { - if (userDetails[field] !== undefined) { - vaUser.userDetails[field] = userDetails[field]; - } - }); + // // Update allowed fields only + // allowedFields.forEach(field => { + // if (userDetails[field] !== undefined) { + // vaUser.userDetails[field] = userDetails[field]; + // } + // }); /* ---------------------- PASSWORD UPDATE ---------------------- */ if (userDetails.password || userDetails.cPassword || userDetails.oldPassword) { @@ -78,28 +78,13 @@ async function va_profile_update_callback(req, res) { const saltRounds = 10; const hashedPassword = await bcrypt.hash(password, saltRounds); vaUser.userDetails.password = hashedPassword; + } else { + return res.status(400).json({ + success: false, + message: "Data not found", + }); } - // if (userDetails.password) { - // if (!userDetails.cPassword) { - // return res.status(400).json({ - // success: false, - // message: "Please confirm your new password", - // }); - // } - - // if (userDetails.password !== userDetails.cPassword) { - // return res.status(400).json({ - // success: false, - // message: "Passwords do not match", - // }); - // } - - // const saltRounds = 10; - // const hashedPassword = await bcrypt.hash(userDetails.password, saltRounds); - // vaUser.userDetails.password = hashedPassword; - // } - await vaUser.save(); const updatedUser = await VAModel.findById(vaUser._id) diff --git a/src/controllers/va/vaProjects/vaProjectLogList.controller.js b/src/controllers/va/vaProjects/vaProjectLogList.controller.js index be7b7fb..bd7e541 100644 --- a/src/controllers/va/vaProjects/vaProjectLogList.controller.js +++ b/src/controllers/va/vaProjects/vaProjectLogList.controller.js @@ -52,7 +52,12 @@ async function va_projects_log_list_function(req, res) { projectList = [project]; } else { - projectList = await ProjectModel.find({ vaAssigned: vaId }); + const all = await ProjectModel.find().lean(); + console.log(all.map(p => p.vaAssigned)); + + console.log("VA ID:", vaId.toString()); + + projectList = await ProjectModel.find({ vaAssigned: vaUser._id }); } if (!projectList.length) { diff --git a/src/middlewares/auth.middleware.js b/src/middlewares/auth.middleware.js index 0f1ea53..cb8a078 100644 --- a/src/middlewares/auth.middleware.js +++ b/src/middlewares/auth.middleware.js @@ -4,28 +4,39 @@ import logger from "../config/logger.js"; async function authValidator(req, res, next) { try { - const accessToken = req.headers.token; - if (!accessToken) return res.status(401).json({ success: false, message: "Not Authorized" }); + const sf_token = req.header.sf_token; + if (sf_token) { - const decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN); + } else { + const accessToken = req.headers.token; + if (!accessToken) return res.status(401).json({ success: false, message: "Not Authorized" }); - if (decoded && ("id" in decoded) && decoded.id !== null && ("email" in decoded)) { - const vaEmail = decoded.email; - - const vaUser = await VAModel.findOne({ "userDetails.email": vaEmail }) - if (!vaUser) { - return res.status(401).json({ success: false, message: "Not Authorized" }); - } - if (!("accessToken" in vaUser) || !vaUser.accessToken) { - return res.status(401).json({ success: false, message: "Not Authorized" }); - } - - if (vaUser.accessToken === accessToken) { - req.body.vaId = vaUser._id - req.body.vaEmail = vaEmail - next(); - } else { - return res.status(401).json({ success: false, message: "Not Authorized" }); + // return res.status(200).json({ + // success: true, + // message: "Log Out Successfull", + // }); + + const decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN); + + if (decoded && ("id" in decoded) && decoded.id !== null && ("email" in decoded)) { + const vaEmail = decoded.email; + + const vaUser = await VAModel.findOne({ "userDetails.email": vaEmail }) + if (!vaUser) { + return res.status(401).json({ success: false, message: "Not Authorized" }); + } + if (!("accessToken" in vaUser) || !vaUser.accessToken) { + return res.status(401).json({ success: false, message: "Not Authorized" }); + } + + if (vaUser.accessToken === accessToken) { + req.body.vaId = vaUser._id + req.body.vaEmail = vaEmail + next(); + } else { + return res.status(401).json({ success: false, message: "Not Authorized" }); + } + } } diff --git a/src/models/vaModel.js b/src/models/vaModel.js index aaf9fb3..3f6a423 100644 --- a/src/models/vaModel.js +++ b/src/models/vaModel.js @@ -80,6 +80,10 @@ const projectSchema = new Schema({ type: Number, default: 0 }, + clientId: { + type: Types.ObjectId, + ref: "Client" + }, vaAssigned: [ { type: Types.ObjectId, diff --git a/src/routes/v1/va.routes.js b/src/routes/v1/va.routes.js index 59533cf..7beaf43 100644 --- a/src/routes/v1/va.routes.js +++ b/src/routes/v1/va.routes.js @@ -36,7 +36,7 @@ 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 // timesheet adjustment routes vaRoutes.get("/timesheet/summary/:projectId/:date", authValidator, va_timesheet_summary_function); // Get Timesheet Adjustment for VA on specific project and date diff --git a/src/server.js b/src/server.js index 489708d..53d0c95 100644 --- a/src/server.js +++ b/src/server.js @@ -1,19 +1,25 @@ import dotenv from "dotenv"; dotenv.config() +process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; import express from 'express'; +import { createServer } from 'http'; import mongoose from "mongoose"; import bodyParser from "body-parser"; +import { Server } from 'socket.io'; import cors from 'cors'; import path from 'path'; import { fileURLToPath } from 'url'; +import fs from "node:fs" import apiRoutes from './routes/index.js' import { render_reset_form } from "./controllers/passwordReset.controllers.js"; const app = express(); +const server = new createServer(app); +const io = new Server(server); app.set("view engine", "ejs"); const PORT = process.env.PORT || 8000; @@ -47,9 +53,250 @@ app.get('/health', (req, res) => { res.status(201).send({ status: "Server is healthy" }); }) +app.get("/client/login", (req, res) => { + res.render("client_login") +}) +app.get("/client/forgot-password", (req, res) => { + res.render("forgot_password") +}) + +app.get("/live/:projectId", (req, res) => { + const { projectId } = req.params; + res.render("va_live_tracker", { title: "VA Live Tracker", projectId }) +}) + +// Handling Socket +import jwt from "jsonwebtoken"; +import { VAModel, ProjectModel, ClientModel } from './models/vaModel.js'; +import { ObjectId } from 'mongodb/lib/bson.js'; + + +const vaSocketRespectToClient = new Map(); + +io.use(async (socket, next) => { + try { + const token = socket.handshake.auth?.token; + const projectId = socket.handshake.auth?.projectId; + const userType = socket.handshake.auth?.userType; + + console.log("🔐 Received token:", token); + + if (!token) { + return next(new Error("Not Authorized: Missing Token")); + } + + const decoded = jwt.verify(token, process.env.JWT_SECRET_ACCESS_TOKEN); + + if (!decoded?.email) { + return next(new Error("Invalid token payload")); + } + + /* ---------------------- 1⃣ Check if VA ---------------------- */ + let vaUser = null; + let clientData = null; + + if (userType === "va") { + vaUser = await VAModel.findOne({ "userDetails.email": decoded.email }); + clientData = await ProjectModel.findById(projectId).populate("clientId"); + } else if (userType === "client") { + clientData = await ProjectModel.findById(projectId).populate("clientId"); + } + + if (clientData == null) return next(new Error("Invalid Project ID")); + const vaProjClientId = clientData.clientId._id; + + if (vaUser && vaUser.accessToken === token) { + socket.data.role = "va"; + socket.data.vaId = vaUser._id.toString(); + socket.data.vaEmail = decoded.email; + + const a = vaProjClientId.toString(); + const b = clientData.clientId._id.toString(); + const c = JSON.stringify(clientData); + + console.log(a, b, c, "vaProjClientId vs clientData.clientId"); + + vaSocketRespectToClient.set(vaProjClientId.toString(), socket.id) + + console.log("✅ Authenticated VA:", vaUser.userDetails.email); + return next(); + } + + /* ---------------------- 2⃣ Check if Client ---------------------- */ + // const clientUser = await ClientModel.findOne({ "userDetails.email": decoded.email }); + + // if (clientUser) { + // socket.data.role = "client"; + // socket.data.clientId = clientUser._id.toString(); + + // // Get all VA IDs assigned to this client + // socket.data.allowedVAs = clientUser.vaIds.map((va) => va.toString()); + + // console.log("✅ Authenticated Client:", clientUser.userDetails.email); + // return next(); + // } + const clientUser = clientData.clientId; + + if (clientUser) { + socket.data.role = "client"; + socket.data.clientId = clientUser._id.toString(); + + // Get all VA IDs assigned to this client + socket.data.allowedVAs = clientData.vaAssigned.map((va) => va.toString()); + + // console.log("✅ Authenticated Client:", clientUser.userDetails.email); + return next(); + } + + return next(new Error("Not Authorized: Invalid User")); + } catch (error) { + console.error("❌ Socket Auth Error:", error.message); + next(new Error("Authentication failed: " + error.message)); + } +}); + + + +io.on("connection", (socket) => { + console.log("User connected:", socket.id); + + // Register role and ID (client or VA) + socket.on("register", async ({ role, id }) => { + if (role === "va") { + socket.data.role = "va"; + socket.data.vaId = id; + console.log(`VA registered: ${id}`); + } + + if (role === "client") { + socket.data.role = "client"; + socket.data.clientId = id; + + // 🧩 Get allowed VAs for this client + const client = await ClientModel.findById(id) + .populate("projectIds") // to access their projects + .populate("vaIds"); // optional if you track directly + + // Collect all VA IDs from their projects + const projectVAIds = new Set(); + for (const project of client.projectIds) { + const populated = await ProjectModel.findById(project._id).populate("vaAssigned"); + populated.vaAssigned.forEach((va) => projectVAIds.add(va._id.toString())); + } + + // Store allowed VA IDs in socket memory + socket.data.allowedVAs = Array.from(projectVAIds); + console.log(`Client ${id} can view VAs:`, socket.data.allowedVAs); + } + }); + + // When VA sends live image + socket.on("sendImage", async ({ token, image, title }) => { + try { + // Verify token + const decoded = jwt.verify(token, process.env.JWT_SECRET_ACCESS_TOKEN); + const vaEmail = decoded.email; + + // Find VA in DB + const vaUser = await VAModel.findOne({ "userDetails.email": vaEmail }); + if (!vaUser) { + console.log("❌ Unauthorized VA"); + return; + } + + const vaId = vaUser._id.toString(); + console.log("🟢 Received image from VA:", vaId); + + // Broadcast image only to allowed clients + for (let [_, clientSocket] of io.sockets.sockets) { + if ( + clientSocket.data.role === "client" && + clientSocket.data.allowedVAs?.includes(vaId) + ) { + clientSocket.emit("receiveImage", image, title); + } + } + } catch (err) { + console.error("❌ Token verification failed:", err.message); + } + }); + + socket.on("selectApp", ({ appTitle, clientId }) => { + console.log(`VA selected app: ${appTitle}`); + + const sssrrrr = vaSocketRespectToClient.get(clientId); + console.log(vaSocketRespectToClient.get(clientId), sssrrrr, "vaSocketRespectToClient[clientId]") + // You can forward this info to the respective Electron client + socket.to(vaSocketRespectToClient.get(clientId)).emit("appToCapture", { appTitle }); + }); + + socket.on("getVisibleApps", async ({ token, apps }) => { + try { + // Verify token + const decoded = jwt.verify(token, process.env.JWT_SECRET_ACCESS_TOKEN); + const vaEmail = decoded.email; + + // Find VA in DB + const vaUser = await VAModel.findOne({ "userDetails.email": vaEmail }); + if (!vaUser) { + console.log("❌ Unauthorized VA"); + return; + } + + const vaId = vaUser._id.toString(); + console.log("🟢 Received Apps from VA:", vaId); + + // Broadcast image only to allowed clients + for (let [_, clientSocket] of io.sockets.sockets) { + if ( + clientSocket.data.role === "client" && + clientSocket.data.allowedVAs?.includes(vaId) + ) { + clientSocket.emit("receiveVisibleApps", apps); + } + } + } catch (err) { + console.error("❌ Token verification failed:", err.message); + } + }); + + + socket.on('disconnect', () => { + console.log('a user Disconnected'); + }); +}); + + + + +// io.on('connection', (socket) => { + +// socket.on("sendImage", (base64String) => { +// io.emit("receiveImage", base64String); +// }); + + +// socket.on('disconnect', () => { +// console.log('a user Disconnected'); +// }); +// }); + // Express App Listening -app.listen(PORT, () => { +// app.listen(PORT, () => { +// mongoose +// .connect( +// process.env.MONGO_URI, +// ) +// .then(() => console.log("mongodb connected")) +// .catch((err) => { +// console.log(err.message); +// process.exit(1); +// }); +// console.log(`Express started on http://localhost:${PORT}`); +// }); + +server.listen(PORT, () => { mongoose .connect( process.env.MONGO_URI, diff --git a/src/testApiVpnConnection.js b/src/testApiVpnConnection.js new file mode 100644 index 0000000..e985b2a --- /dev/null +++ b/src/testApiVpnConnection.js @@ -0,0 +1,32 @@ +// testVpnApi.js +import axios from "axios"; + +const apiUrl = "http://165.140.240.203:3000/api"; +const apiKey = "hellovpn123"; + +const possiblePaths = [ + "/", "/v1", "/servers", "/status", "/vpn", "/health", "/ping", + "/nodes", "/list", "/connect", "/config", "/create", "/createConfig", + "/check", "/organizations", "/users" +]; + +async function testPaths() { + for (const path of possiblePaths) { + try { + const res = await axios.get(`${apiUrl}${path}`, { + headers: { "x-api-key": apiKey }, + timeout: 3000 + }); + console.log(`✅ ${path} → ${res.status} ${res.statusText}`); + console.log("Response:", res.data); + } catch (err) { + if (err.response) { + console.log(`❌ ${path} → ${err.response.status}`); + } else { + console.log(`⚠️ ${path} → No response`); + } + } + } +} + +testPaths(); \ No newline at end of file diff --git a/views/client_login.ejs b/views/client_login.ejs new file mode 100644 index 0000000..399c259 --- /dev/null +++ b/views/client_login.ejs @@ -0,0 +1,297 @@ + + + + + + + DocVA | Login + + + + + + + + + + + + + \ No newline at end of file diff --git a/views/forgot_password.ejs b/views/forgot_password.ejs new file mode 100644 index 0000000..c9e1ff6 --- /dev/null +++ b/views/forgot_password.ejs @@ -0,0 +1,193 @@ + + + + + + + DocVA | Reset Password + + + + + +
+ +
+

Reset your password

+ + + Go back to login +
+
+ + + + + \ No newline at end of file diff --git a/views/va_live_tracker.ejs b/views/va_live_tracker.ejs new file mode 100644 index 0000000..d05223c --- /dev/null +++ b/views/va_live_tracker.ejs @@ -0,0 +1,143 @@ + + + + + + Live Screen Feed + + + + + + +
+

Select an app to view

+ Waiting for image... +
+ + + + + + \ No newline at end of file From 28226c30f8d7afaf3b508e0747724c4aa8f41a16 Mon Sep 17 00:00:00 2001 From: Aakash Date: Fri, 14 Nov 2025 18:19:16 +0530 Subject: [PATCH 3/4] Added dashboard --- public/css/dashboard.css | 635 ++++++++++++++++++ public/css/va-register.css | 4 +- public/js/va-client-register.js | 4 + public/js/va-register.js | 3 + src/controllers/client/vaClient.controller.js | 8 +- src/controllers/passwordReset.controllers.js | 6 +- .../va/vaAuth/vaLogin.controller.js | 45 +- .../va/vaProfile/vaRegister.controller.js | 32 +- src/middlewares/auth.middleware.js | 16 +- src/server.js | 51 ++ src/services/salesforce.service.js | 78 +++ src/services/vaClient.service.js | 29 +- views/auth/reset-password-error.ejs | 13 +- views/auth/reset-password-form.ejs | 9 +- views/auth/reset-password-success.ejs | 8 +- views/client/dashboard-dashboard.ejs | 292 ++++++++ views/client/dashboard-invoices.ejs | 0 views/client/dashboard-referrals.ejs | 0 views/client/dashboard-timesheets.ejs | 0 views/client/dashboard-virtual-assistants.ejs | 286 ++++++++ views/client_login.ejs | 55 +- views/forgot_password.ejs | 10 +- 22 files changed, 1537 insertions(+), 47 deletions(-) create mode 100644 public/css/dashboard.css create mode 100644 src/services/salesforce.service.js create mode 100644 views/client/dashboard-dashboard.ejs create mode 100644 views/client/dashboard-invoices.ejs create mode 100644 views/client/dashboard-referrals.ejs create mode 100644 views/client/dashboard-timesheets.ejs create mode 100644 views/client/dashboard-virtual-assistants.ejs diff --git a/public/css/dashboard.css b/public/css/dashboard.css new file mode 100644 index 0000000..ce02b96 --- /dev/null +++ b/public/css/dashboard.css @@ -0,0 +1,635 @@ +:root { + --color-primary: #0d0b57; + --color-primary-light: #2b8efc; + --color-secondary: #6366f1; + --color-success: #10b981; + --color-warning: #f59e0b; + --color-danger: #ef4444; + --color-gray-50: #f9fafb; + --color-gray-100: #f3f4f6; + --color-gray-200: #e5e7eb; + --color-gray-300: #d1d5db; + --color-gray-400: #9ca3af; + --color-gray-500: #6b7280; + --color-gray-600: #4b5563; + --color-gray-700: #374151; + --color-gray-800: #1f2937; + --color-gray-900: #111827; + + --bg-primary: #ffffff; + --bg-secondary: #f8fafc; + --bg-card: #ffffff; + --text-primary: #1f2937; + --text-secondary: #6b7280; + --text-muted: #9ca3af; + --border-color: #e5e7eb; + --shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); +} + +[data-theme="dark"] { + --bg-primary: #121212; + --bg-secondary: #1e1e1e; + --bg-card: #1e1e1e; + --text-primary: #f5f5f5; + --text-secondary: #d1d5db; + --text-muted: #9ca3af; + --border-color: #374151; + --shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.3), 0 1px 2px 0 rgba(0, 0, 0, 0.2); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.2); +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; + background-color: var(--bg-primary); + color: var(--text-primary); + transition: background-color 0.3s ease, color 0.3s ease; +} + +.dashboard-body { + overflow: hidden; +} + +/* Header Styles */ +.dashboard-header { + position: sticky; + top: 0; + z-index: 40; + width: 100%; + height: 4rem; + display: flex; + align-items: center; + justify-content: center; + backdrop-filter: blur(16px); + background-color: rgba(255, 255, 255, 0.7); + border-bottom: 1px solid var(--border-color); +} + +[data-theme="dark"] .dashboard-header { + background-color: rgba(30, 30, 30, 0.7); +} + +.header-content { + display: flex; + padding: 0 1.5rem; + gap: 1rem; + width: 100%; + max-width: 1024px; + flex-direction: row; + align-items: center; + justify-content: space-between; + position: relative; + flex-wrap: nowrap; +} + +.header-logo { + display: flex; + flex-basis: 0; + flex-grow: 1; + flex-direction: row; + flex-wrap: nowrap; + justify-content: flex-start; + align-items: center; + background: transparent; + text-decoration: none; + color: inherit; + white-space: nowrap; + box-sizing: border-box; +} + +.header-nav { + display: flex; + height: 100%; + flex-direction: row; + flex-wrap: nowrap; + gap: 0.5rem; + align-items: center; + justify-content: flex-end; +} + +.nav-list { + display: flex; + list-style: none; + gap: 0.5rem; +} + +.nav-button { + padding: 0.5rem 1rem; + border-radius: 0.5rem; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + transition: all 0.2s; + font-size: 0.875rem; + font-weight: 500; + display: flex; + align-items: center; + gap: 0.5rem; + color: var(--text-secondary); + background: none; + border: none; + cursor: pointer; + text-decoration: none; +} + +.nav-button:hover { + background-color: var(--color-gray-100); + color: var(--color-primary-light); +} + +[data-theme="dark"] .nav-button:hover { + background-color: var(--color-gray-800); +} + +.nav-button.active { + color: var(--color-primary-light); + border-bottom: 2px solid var(--color-primary-light); +} + +.user-avatar { + width: 2rem; + height: 2rem; + background-color: var(--color-primary); + color: white; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.75rem; + font-weight: 600; + cursor: pointer; +} + +.mobile-menu-button { + display: none; + width: 2rem; + height: 2rem; + align-items: center; + justify-content: center; + color: var(--text-secondary); + background: none; + border: none; + cursor: pointer; +} + +/* Container Layout */ +.dashboard-container { + display: flex; + height: calc(100vh - 4rem); +} + +/* Sidebar Styles */ +.dashboard-sidebar { + width: 16rem; + background-color: var(--bg-card); + border-right: 1px solid var(--border-color); + display: none; +} + +@media (min-width: 768px) { + .dashboard-sidebar { + display: block; + } +} + +.sidebar-header { + padding: 1.5rem; +} + +.sidebar-title { + font-size: 1.125rem; + font-weight: 600; + color: var(--text-primary); +} + +.sidebar-nav { + margin-top: 1.5rem; +} + +.sidebar-list { + list-style: none; + padding: 0 1rem; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.sidebar-item { + margin-bottom: 0.5rem; +} + +.sidebar-link { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem 1rem; + color: var(--text-secondary); + text-decoration: none; + border-radius: 0.5rem; + transition: all 0.2s; +} + +.sidebar-link:hover { + background-color: var(--color-gray-100); + color: var(--text-primary); +} + +[data-theme="dark"] .sidebar-link:hover { + background-color: var(--color-gray-800); +} + +.sidebar-link.active { + background-color: rgba(59, 130, 246, 0.1); + color: var(--color-primary-light); +} + +.sidebar-icon { + width: 1.25rem; + text-align: center; +} + +/* Main Content */ +.dashboard-main { + flex: 1; + overflow: auto; + padding: 1.5rem; + background-color: var(--bg-secondary); +} + +.dashboard-content { + max-width: 80rem; + margin: 0 auto; +} + +/* Stats Grid */ +.stats-grid { + display: grid; + grid-template-columns: 1fr; + gap: 1.5rem; + margin-bottom: 2rem; +} + +@media (min-width: 768px) { + .stats-grid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (min-width: 1024px) { + .stats-grid { + grid-template-columns: repeat(4, 1fr); + } +} + +.stat-card { + background-color: var(--bg-card); + border-radius: 0.75rem; + padding: 1.5rem; + box-shadow: var(--shadow); + border: 1px solid var(--border-color); + transition: box-shadow 0.2s; +} + +.stat-card:hover { + box-shadow: var(--shadow-lg); +} + +.stat-content { + display: flex; + align-items: center; + justify-content: space-between; +} + +.stat-info h3 { + font-size: 0.875rem; + color: var(--text-secondary); + margin-bottom: 0.25rem; + font-weight: 500; +} + +.stat-value { + font-size: 1.5rem; + font-weight: 700; + color: var(--text-primary); +} + +.stat-icon { + width: 3rem; + height: 3rem; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; +} + +.stat-icon.blue { + background-color: rgba(59, 130, 246, 0.1); + color: var(--color-primary-light); +} + +.stat-icon.green { + background-color: rgba(16, 185, 129, 0.1); + color: var(--color-success); +} + +.stat-icon.red { + background-color: rgba(239, 68, 68, 0.1); + color: var(--color-danger); +} + +.stat-icon.yellow { + background-color: rgba(245, 158, 11, 0.1); + color: var(--color-warning); +} + +/* Content Grid */ +.content-grid { + display: grid; + grid-template-columns: 1fr; + gap: 1.5rem; +} + +@media (min-width: 1024px) { + .content-grid { + grid-template-columns: repeat(2, 1fr); + } +} + +.content-card { + background-color: var(--bg-card); + border-radius: 0.75rem; + padding: 1.5rem; + box-shadow: var(--shadow); + border: 1px solid var(--border-color); +} + +.card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; +} + +.card-title { + font-size: 1.125rem; + font-weight: 600; + color: var(--text-primary); +} + +.card-link { + font-size: 0.875rem; + color: var(--color-primary-light); + text-decoration: none; +} + +.card-link:hover { + text-decoration: underline; +} + +/* Activity Items */ +.activity-list { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.activity-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.75rem; + background-color: var(--color-gray-50); + border-radius: 0.5rem; +} + +[data-theme="dark"] .activity-item { + background-color: var(--color-gray-800); +} + +.activity-info h4 { + font-weight: 500; + color: var(--text-primary); + margin-bottom: 0.25rem; +} + +.activity-info p { + font-size: 0.875rem; + color: var(--text-secondary); +} + +.activity-meta { + text-align: right; +} + +.activity-meta .value { + font-weight: 600; + color: var(--text-primary); + margin-bottom: 0.25rem; +} + +.activity-meta .date { + font-size: 0.875rem; + color: var(--text-secondary); +} + +.status-badge { + display: inline-block; + padding: 0.25rem 0.5rem; + font-size: 0.75rem; + border-radius: 9999px; + font-weight: 600; +} + +.status-paid { + background-color: rgba(16, 185, 129, 0.1); + color: var(--color-success); +} + +.status-unpaid { + background-color: rgba(239, 68, 68, 0.1); + color: var(--color-danger); +} + +/* Page Header */ +.page-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; +} + +.page-title { + font-size: 1.5rem; + font-weight: 300; + color: var(--text-primary); +} + +.action-button { + padding: 0.5rem 1rem; + background-color: rgba(59, 130, 246, 0.1); + color: var(--color-primary-light); + border-radius: 0.5rem; + font-weight: 500; + display: flex; + align-items: center; + gap: 0.5rem; + text-decoration: none; + border: none; + cursor: pointer; + transition: background-color 0.2s; +} + +.action-button:hover { + background-color: rgba(59, 130, 246, 0.2); +} + +/* Table Styles */ +.table-container { + background-color: var(--bg-card); + border-radius: 0.5rem; + box-shadow: var(--shadow); + border: 1px solid var(--border-color); + overflow: hidden; +} + +.table-wrapper { + overflow-x: auto; +} + +.data-table { + width: 100%; + border-collapse: collapse; +} + +.data-table thead { + background-color: var(--color-gray-50); +} + +[data-theme="dark"] .data-table thead { + background-color: var(--color-gray-800); +} + +.data-table th { + padding: 0.75rem 1.5rem; + text-align: left; + font-size: 0.75rem; + font-weight: 500; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.05em; + border-bottom: 1px solid var(--border-color); +} + +.data-table tbody tr { + border-bottom: 1px solid var(--border-color); + transition: background-color 0.2s; + cursor: pointer; +} + +.data-table tbody tr:hover { + background-color: var(--color-gray-50); +} + +[data-theme="dark"] .data-table tbody tr:hover { + background-color: var(--color-gray-800); +} + +.data-table td { + padding: 1rem 1.5rem; + font-size: 0.875rem; + color: var(--text-primary); +} + +.data-table .status-active { + background-color: rgba(16, 185, 129, 0.1); + color: var(--color-success); +} + +.data-table .status-badge { + padding: 0.25rem 0.75rem; + border-radius: 9999px; + font-size: 0.75rem; + font-weight: 600; +} + +.hours-badge { + display: inline-flex; + align-items: center; + gap: 0.25rem; + padding: 0.25rem 0.75rem; + border-radius: 9999px; + font-size: 0.75rem; + font-weight: 600; +} + +.hours-default { + background-color: rgba(107, 114, 128, 0.1); + color: var(--color-gray-600); +} + +.hours-primary { + background-color: rgba(59, 130, 246, 0.1); + color: var(--color-primary-light); +} + +.hours-secondary { + background-color: rgba(99, 102, 241, 0.1); + color: var(--color-secondary); +} + +.hours-warning { + background-color: rgba(245, 158, 11, 0.1); + color: var(--color-warning); +} + +.amount-positive { + color: var(--color-success); +} + +.amount-tips { + color: var(--color-warning); +} + +/* Empty State */ +.empty-state { + padding: 3rem 1.5rem; + text-align: center; + color: var(--text-secondary); +} + +/* Responsive */ +@media (max-width: 768px) { + .dashboard-main { + padding: 1rem; + } + + .mobile-menu-button { + display: flex; + } + + .nav-list { + display: none; + } + + .stats-grid { + grid-template-columns: 1fr; + } + + .content-grid { + grid-template-columns: 1fr; + } + + .data-table { + font-size: 0.75rem; + } + + .data-table th, + .data-table td { + padding: 0.5rem 0.75rem; + } +} \ No newline at end of file diff --git a/public/css/va-register.css b/public/css/va-register.css index 97cc23a..659500e 100644 --- a/public/css/va-register.css +++ b/public/css/va-register.css @@ -20,8 +20,8 @@ body { .sidebar { display: none; flex-direction: column; - width: 280px; - background: linear-gradient(to bottom, #0b57d0bd, #0b57d0d6, #0b57d0); + width: 388px; + background: linear-gradient(to bottom, #f4f4f5, #d0f9f9, #cac5f7); padding: 24px; border-radius: 14px; margin: 20px; diff --git a/public/js/va-client-register.js b/public/js/va-client-register.js index 783bcc2..c44cba4 100644 --- a/public/js/va-client-register.js +++ b/public/js/va-client-register.js @@ -603,6 +603,10 @@ async function submitForm() { stepNumber.style.color = 'white'; }); nextStep(6); + + setTimeout(() => { + window.location.href = '/client/login'; + }, 1000); } else { // Handle server errors if (response.status === 409) { diff --git a/public/js/va-register.js b/public/js/va-register.js index d343149..5f73dbf 100644 --- a/public/js/va-register.js +++ b/public/js/va-register.js @@ -593,6 +593,9 @@ }); nextStep(5); + setTimeout(() => { + window.location.href = '/login'; + }, 1000); } else { // Handle server errors alert('There was an error submitting your registration. Please try again.'); diff --git a/src/controllers/client/vaClient.controller.js b/src/controllers/client/vaClient.controller.js index f34de5d..700b238 100644 --- a/src/controllers/client/vaClient.controller.js +++ b/src/controllers/client/vaClient.controller.js @@ -11,7 +11,6 @@ async function va_register_client(req, res) { try { const payload = req.body || {}; const { userDetails } = payload; - if (!userDetails || !userDetails.email || !userDetails.name) { return res.status(400).json({ success: false, @@ -37,6 +36,13 @@ async function va_register_client(req, res) { }); } + if (error.message.includes("Salesforce API is unreachable")) { + return res.status(503).json({ + success: false, + message: "Salesforce service is currently unavailable. Please try again later." + }); + } + return res.status(500).json({ success: false, message: "Internal Server Error" diff --git a/src/controllers/passwordReset.controllers.js b/src/controllers/passwordReset.controllers.js index e23a485..45da602 100644 --- a/src/controllers/passwordReset.controllers.js +++ b/src/controllers/passwordReset.controllers.js @@ -56,14 +56,14 @@ async function render_reset_form(req, res) { const { token, userType } = req.query; if (!token || !userType) { - return res.render('auth/reset-password-error', { error: 'Reset token or user type is missing' }); + return res.render('auth/reset-password-error', { error: 'Reset token or user type is missing', userType: userType || 'va' }); } // Validate token const validation = await passwordResetService.validateResetToken(token, userType); if (!validation.valid) { - return res.render('auth/reset-password-error', { error: validation.message }); + return res.render('auth/reset-password-error', { error: validation.message, userType: userType || 'va' }); } // Render reset form with token @@ -102,7 +102,7 @@ async function process_reset(req, res) { await passwordResetService.resetPassword(token, password, userType); // Render success page - res.render('auth/reset-password-success'); + res.render('auth/reset-password-success', {userType}); } catch (error) { logger.error(`PasswordResetController - Process reset error: ${error.message}`); diff --git a/src/controllers/va/vaAuth/vaLogin.controller.js b/src/controllers/va/vaAuth/vaLogin.controller.js index d603004..580552d 100644 --- a/src/controllers/va/vaAuth/vaLogin.controller.js +++ b/src/controllers/va/vaAuth/vaLogin.controller.js @@ -1,6 +1,6 @@ import bcrypt from "bcrypt"; import jwt from "jsonwebtoken"; -import { VAModel } from "../../../models/vaModel.js"; +import { VAModel, LoggedModel } from "../../../models/vaModel.js"; // Login VA async function va_login_callback(req, res) { @@ -32,6 +32,36 @@ async function va_login_callback(req, res) { return res.status(401).json({ success: false, message: "Invalid password" }); } + // Check if VA has any active sessions (LOGIN without LOGOUT) + const activeSessions = await LoggedModel.find({ + vaId: user._id, + loggedType: "LOGIN" + }).sort({ time: -1 }); + + if (activeSessions.length > 0) { + // Get the latest login to check if there's no corresponding logout + const latestLogin = activeSessions[0]; + + // Check if there's a logout after this login + const correspondingLogout = await LoggedModel.findOne({ + vaId: user._id, + loggedType: "LOGOUT", + time: { $gt: latestLogin.time } + }); + + if (!correspondingLogout) { + // VA is still logged in to a project + const projectInfo = await LoggedModel.findById(latestLogin._id) + .populate('projectId', 'name') + .lean(); + + return res.status(400).json({ + success: false, + message: "You are still logged in to a project. Please logout first before logging in again." + }); + } + } + // Generate JWT accessToken with 24h expiry const accessToken = jwt.sign( { @@ -76,4 +106,17 @@ async function va_login_callback(req, res) { } } +// Helper function to format session duration +function formatSessionDuration(loginTime) { + const now = new Date(); + const login = new Date(loginTime); + const durationMs = now - login; + + const hours = Math.floor(durationMs / (1000 * 60 * 60)); + const minutes = Math.floor((durationMs % (1000 * 60 * 60)) / (1000 * 60)); + const seconds = Math.floor((durationMs % (1000 * 60)) / 1000); + + return `${hours}h ${minutes}m ${seconds}s`; +} + export { va_login_callback }; \ No newline at end of file diff --git a/src/controllers/va/vaProfile/vaRegister.controller.js b/src/controllers/va/vaProfile/vaRegister.controller.js index 61352fc..da2f8fb 100644 --- a/src/controllers/va/vaProfile/vaRegister.controller.js +++ b/src/controllers/va/vaProfile/vaRegister.controller.js @@ -47,7 +47,37 @@ async function va_register(req, res) { const created = await VAModel.create(toCreate); - return res.status(201).json({ success: true, data: created }); + // Send data to Salesforce API after successful registration + try { + const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/virtualassistant`; + + const response = await fetch(salesforceUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + // Add any required headers for Salesforce authentication + // 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN + }, + body: JSON.stringify(created) + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`); + } + + const result = await response.json(); + console.log("✅ Successfully synced VA data to Salesforce:", result); + + res.status(201).json({ success: true, data: created, SyncSalesforce: result }); + } catch (error) { + console.error("❌ Failed to sync with Salesforce:", error.message); + + // Create a custom error to distinguish Salesforce errors + const salesforceError = new Error(`Salesforce sync failed: ${error.message}`); + salesforceError.isSalesforceError = true; + throw salesforceError; + } } catch (error) { console.error("va_register error:", error); return res.status(500).json({ success: false, message: "Internal Server Error" }); diff --git a/src/middlewares/auth.middleware.js b/src/middlewares/auth.middleware.js index cb8a078..36b2ff5 100644 --- a/src/middlewares/auth.middleware.js +++ b/src/middlewares/auth.middleware.js @@ -1,12 +1,12 @@ import jwt from "jsonwebtoken"; -import { VAModel } from "../models/vaModel.js"; +import { VAModel, ClientModel } from "../models/vaModel.js"; import logger from "../config/logger.js"; async function authValidator(req, res, next) { try { - const sf_token = req.header.sf_token; + const sf_token = req.headers.sf_token; if (sf_token) { - + next(); } else { const accessToken = req.headers.token; if (!accessToken) return res.status(401).json({ success: false, message: "Not Authorized" }); @@ -15,12 +15,12 @@ async function authValidator(req, res, next) { // success: true, // message: "Log Out Successfull", // }); - + const decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN); - + if (decoded && ("id" in decoded) && decoded.id !== null && ("email" in decoded)) { const vaEmail = decoded.email; - + const vaUser = await VAModel.findOne({ "userDetails.email": vaEmail }) if (!vaUser) { return res.status(401).json({ success: false, message: "Not Authorized" }); @@ -28,7 +28,7 @@ async function authValidator(req, res, next) { if (!("accessToken" in vaUser) || !vaUser.accessToken) { return res.status(401).json({ success: false, message: "Not Authorized" }); } - + if (vaUser.accessToken === accessToken) { req.body.vaId = vaUser._id req.body.vaEmail = vaEmail @@ -36,7 +36,7 @@ async function authValidator(req, res, next) { } else { return res.status(401).json({ success: false, message: "Not Authorized" }); } - + } } diff --git a/src/server.js b/src/server.js index 53d0c95..89dbe86 100644 --- a/src/server.js +++ b/src/server.js @@ -60,6 +60,57 @@ app.get("/client/forgot-password", (req, res) => { res.render("forgot_password") }) +// Client Dashboard Routes with new file names +app.get('/client/dashboard', async (req, res) => { + try { + const dashboardData = await dashboardService.getClientDashboard(req.user.id); + res.render('client/dashboard-dashboard', dashboardData); + } catch (error) { + console.error('Dashboard error:', error); + res.status(500).render('client/dashboard-dashboard', { error: 'Failed to load dashboard' }); + } +}); + +app.get('/client/virtual-assistants', async (req, res) => { + try { + const vaData = await virtualAssistantService.getClientVAs(req.user.id); + res.render('client/dashboard-virtual-assistants', vaData); + } catch (error) { + console.error('Virtual assistants error:', error); + res.status(500).render('client/dashboard-virtual-assistants', { error: 'Failed to load virtual assistants' }); + } +}); + +// app.get('/client/timesheets', async (req, res) => { +// try { +// const timesheetData = await timesheetService.getClientTimesheets(req.user.id); +// res.render('client/dashboard-timesheets', timesheetData); +// } catch (error) { +// console.error('Timesheets error:', error); +// res.status(500).render('client/dashboard-timesheets', { error: 'Failed to load timesheets' }); +// } +// }); + +// app.get('/client/invoices', async (req, res) => { +// try { +// const invoiceData = await invoiceService.getClientInvoices(req.user.id); +// res.render('client/dashboard-invoices', invoiceData); +// } catch (error) { +// console.error('Invoices error:', error); +// res.status(500).render('client/dashboard-invoices', { error: 'Failed to load invoices' }); +// } +// }); + +// app.get('/client/referrals', async (req, res) => { +// try { +// const referralData = await referralService.getClientReferrals(req.user.id); +// res.render('client/dashboard-referrals', referralData); +// } catch (error) { +// console.error('Referrals error:', error); +// res.status(500).render('client/dashboard-referrals', { error: 'Failed to load referrals' }); +// } +// }); + app.get("/live/:projectId", (req, res) => { const { projectId } = req.params; res.render("va_live_tracker", { title: "VA Live Tracker", projectId }) diff --git a/src/services/salesforce.service.js b/src/services/salesforce.service.js new file mode 100644 index 0000000..3f310ad --- /dev/null +++ b/src/services/salesforce.service.js @@ -0,0 +1,78 @@ +import axios from "axios"; +import logger from "../config/logger.js"; + +class SalesforceService { + + async registerClientInSalesforce(clientData, _id) { + try { + const url = `${process.env.SF_DOCTRACKER_API_URL}/services/apexrest/docva/api/client`; + + // Extract only the required data for Salesforce + const salesforcePayload = this.prepareSalesforcePayload(clientData, _id); + + logger.info(`SalesforceService - Sending client data to Salesforce: ${clientData.userDetails.email}`); + + const response = await axios.post(url, salesforcePayload, { + headers: { + 'Content-Type': 'application/json', + }, + timeout: 30000, // 30 seconds timeout + }); + + logger.info(`SalesforceService - Client registered successfully in Salesforce: ${clientData.userDetails.email}`); + return response.data; + } catch (error) { + logger.error(`SalesforceService - Error registering client in Salesforce: ${error.message}`); + + if (error.response) { + // Salesforce returned an error response + logger.error(`SalesforceService - Salesforce API error: ${error.response.status} - ${JSON.stringify(error.response.data)}`); + throw new Error(`Salesforce API error: ${error.response.status} - ${error.response.data.message || 'Unknown error'}`); + } else if (error.request) { + // Request was made but no response received + logger.error('SalesforceService - No response received from Salesforce API'); + throw new Error('Salesforce API is unreachable'); + } else { + // Something else happened + throw error; + } + } + } + + prepareSalesforcePayload(clientData, _id) { + const { userDetails } = clientData; + + return { + _id: _id, + userDetails: { + name: userDetails.name, + email: userDetails.email.toLowerCase(), + address: userDetails.address || null, + city: userDetails.city || null, + state: userDetails.state || null, + zip: userDetails.zip || null, + billingName: userDetails.billingName || null, + billingAddress: userDetails.billingAddress || null, + billingCity: userDetails.billingCity || null, + billingState: userDetails.billingState || null, + billingZip: userDetails.billingZip || null, + billingEmail: userDetails.billingEmail || null, + position: userDetails.position || null, + signatoryName: userDetails.signatoryName || null, + signatoryEmail: userDetails.signatoryEmail || null, + paymentMethod: userDetails.paymentMethod || null, + bankName: userDetails.bankName || null, + accountNumber: userDetails.accountNumber || null, + routingNumber: userDetails.routingNumber || null, + accountType: userDetails.accountType || null, + accountHolderName: userDetails.accountHolderName || null, + cardNumber: userDetails.cardNumber || null, + expiryDate: userDetails.expiryDate || null, + securityCode: userDetails.securityCode || null, + cardZip: userDetails.cardZip || null, + } + }; + } +} + +export default SalesforceService; \ No newline at end of file diff --git a/src/services/vaClient.service.js b/src/services/vaClient.service.js index e54f0ec..0feaa4c 100644 --- a/src/services/vaClient.service.js +++ b/src/services/vaClient.service.js @@ -1,8 +1,10 @@ import logger from "../config/logger.js"; +import SalesforceService from "./salesforce.service.js"; class VaClientService { constructor(vaClientRepository) { this.clientRepository = vaClientRepository; + this.salesforceService = new SalesforceService(); } async registerClient(data) { @@ -15,6 +17,9 @@ class VaClientService { // Sanitize and prepare data const clientData = this.sanitizeClientData(data); + + // Send data to Salesforce first + await this.salesforceService.registerClientInSalesforce(data, clientData._id); const response = await this.clientRepository.create(clientData); return response; @@ -87,19 +92,17 @@ class VaClientService { position: userDetails.position || null, signatoryName: userDetails.signatoryName || null, signatoryEmail: userDetails.signatoryEmail || null, - paymentMethod: userDetails.paymentMethod || null, - bankName: userDetails.bankName || null, - accountNumber: userDetails.accountNumber || null, - routingNumber: userDetails.routingNumber || null, - accountType: userDetails.accountType || null, - accountHolderName: userDetails.accountHolderName || null, - cardNumber: userDetails.cardNumber || null, - expiryDate: userDetails.expiryDate || null, - securityCode: userDetails.securityCode || null, - cardZip: userDetails.cardZip || null, - }, - vaIds: payload.vaIds || [], - projectIds: payload.projectIds || [], + paymentMethod: null, + bankName: null, + accountNumber: null, + routingNumber: null, + accountType: null, + accountHolderName: null, + cardNumber: null, + expiryDate: null, + securityCode: null, + cardZip: null, + } }; } } diff --git a/views/auth/reset-password-error.ejs b/views/auth/reset-password-error.ejs index 2af6b7d..2a61feb 100644 --- a/views/auth/reset-password-error.ejs +++ b/views/auth/reset-password-error.ejs @@ -12,8 +12,17 @@
- Request New Reset Link - Back to Login + <% + let forgotPasswordUrl = `${process.env.FRONTEND_URL}/forgot-password`; + let loginUrl = `${process.env.FRONTEND_URL}/login`; + + if (userType === 'client') { + forgotPasswordUrl = '/client/forgot-password'; + loginUrl = '/client/login'; + } + %> + Request New Reset Link + Back to Login
diff --git a/views/auth/reset-password-form.ejs b/views/auth/reset-password-form.ejs index 355262a..8ca134f 100644 --- a/views/auth/reset-password-form.ejs +++ b/views/auth/reset-password-form.ejs @@ -33,8 +33,13 @@ - - Back to Login + <% + let backToLoginUrl = `${process.env.FRONTEND_URL}/login`; + if (userType === 'client') { + backToLoginUrl = '/client/login'; + } + %> + Back to Login diff --git a/views/auth/reset-password-success.ejs b/views/auth/reset-password-success.ejs index 8c2c788..67676f3 100644 --- a/views/auth/reset-password-success.ejs +++ b/views/auth/reset-password-success.ejs @@ -12,7 +12,13 @@
- Go to Login + <% + let loginUrl = `${process.env.FRONTEND_URL}/login`; + if (userType === 'client') { + loginUrl = '/client/login'; + } + %> + Go to Login
diff --git a/views/client/dashboard-dashboard.ejs b/views/client/dashboard-dashboard.ejs new file mode 100644 index 0000000..c05be5a --- /dev/null +++ b/views/client/dashboard-dashboard.ejs @@ -0,0 +1,292 @@ + + + + + + DocVA Client | Dashboard + + + + +
+ +
+ +
+
+ +
+
+
+
+

Active Virtual Assistants

+
0
+
+
+ +
+
+
+ +
+
+
+

Total Billed Hours

+
0
+
+
+ +
+
+
+ +
+
+
+

Unpaid Invoices

+
$0.00
+
+
+ +
+
+
+ +
+
+
+

Outstanding Balance

+
$0.00
+
+
+ +
+
+
+
+ + +
+ +
+
+

Recent Timesheets

+ View All +
+
+ +
+
+ + +
+
+

Recent Invoices

+ View All +
+
+ +
+
+
+
+ +
+
+
+ + + + + \ No newline at end of file diff --git a/views/client/dashboard-invoices.ejs b/views/client/dashboard-invoices.ejs new file mode 100644 index 0000000..e69de29 diff --git a/views/client/dashboard-referrals.ejs b/views/client/dashboard-referrals.ejs new file mode 100644 index 0000000..e69de29 diff --git a/views/client/dashboard-timesheets.ejs b/views/client/dashboard-timesheets.ejs new file mode 100644 index 0000000..e69de29 diff --git a/views/client/dashboard-virtual-assistants.ejs b/views/client/dashboard-virtual-assistants.ejs new file mode 100644 index 0000000..ea6321c --- /dev/null +++ b/views/client/dashboard-virtual-assistants.ejs @@ -0,0 +1,286 @@ + + + + + + DocVA Client | Dashboard + + + + +
+ +
+ +
+
+ + + +
+
+
+
+

Active VAs

+
0
+
+
+ +
+
+
+ +
+
+
+

Unbilled Hours

+
0
+
+
+ +
+
+
+ +
+
+
+

Billed Hours

+
0
+
+
+ +
+
+
+ +
+
+
+

Billed Amount

+
$0.00
+
+
+ +
+
+
+
+ + +
+
+ + + + + + + + + + + + + + + +
StatusVirtual AssistantProjectUnbilled HoursBilled HoursBilled TipsBilled Amount
+
+
+
+
+
+
+ + + + + + \ No newline at end of file diff --git a/views/client_login.ejs b/views/client_login.ejs index 399c259..457ba08 100644 --- a/views/client_login.ejs +++ b/views/client_login.ejs @@ -88,7 +88,7 @@ } .login-card h2 { - margin-bottom: 25px; + margin-bottom: 15px; color: var(--color-muted); font-weight: 500; font-size: 0.95em; @@ -212,9 +212,7 @@ @@ -247,6 +245,7 @@ } setTheme(getSystemTheme()); + checkAuthentication(); document.querySelector("form").addEventListener("submit", async (e) => { e.preventDefault(); // prevent page reload @@ -276,10 +275,11 @@ if (!res.ok) { throw new Error(data.message || "Login failed"); } - - if (data.token) { + + if (data.accessToken) { // 🟢 Save token - localStorage.setItem("authToken", data.token); + localStorage.setItem("knoptpq", data.accessToken); + localStorage.setItem("refreshToken", data.refreshToken); // 🟢 Redirect window.location.href = "/client/dashboard"; @@ -291,6 +291,43 @@ alert(err.message || "Something went wrong while logging in"); } }); + + // Check if user is already authenticated and redirect to dashboard + async function checkAuthentication() { + const authToken = localStorage.getItem("knoptpq"); + + if (!authToken) { + return false; // No token found + } + + try { + // Validate the token with the server + const response = await fetch("/api/v1/client/validate-token", { + method: "GET", + headers: { + "Authorization": `Bearer ${authToken}`, + "Content-Type": "application/json" + } + }); + + if (response.ok) { + // Token is valid, redirect to dashboard + window.location.href = "/client/dashboard"; + return true; + } else { + // Token is invalid, clear localStorage + localStorage.removeItem("knoptpq"); + localStorage.removeItem("refreshToken"); + return false; + } + } catch (error) { + console.error("Token validation error:", error); + // If validation fails, clear tokens to be safe + localStorage.removeItem("knoptpq"); + localStorage.removeItem("refreshToken"); + return false; + } + } diff --git a/views/forgot_password.ejs b/views/forgot_password.ejs index c9e1ff6..434bdcd 100644 --- a/views/forgot_password.ejs +++ b/views/forgot_password.ejs @@ -117,12 +117,12 @@
- +

DocTracker

Reset your password

- Go back to login + Go back to login
@@ -134,7 +134,7 @@ function applyTheme(isDark) { const theme = isDark ? "dark" : "light"; document.documentElement.setAttribute("data-theme", theme); - logo.src = isDark ? "/public/images/logo-white-vertical.svg" : "/public/images/logo-colored-vertical.svg"; + // logo.src = isDark ? "/public/images/logo-white-vertical.svg" : "/public/images/logo-colored-vertical.svg"; } // Initial set @@ -174,7 +174,9 @@ const data = await response.json(); if (response.ok) { - window.location.href = "/client/login"; // Redirect to reset password page + if(confirm('Please Check your mail inbox for password reset link')) { + window.location.href = "/client/login"; // Redirect to reset password page + } } else { alert("❌ " + (data.message || "Failed to send reset link.")); } From f9b8cd886a78d1b2acfa4d3035531d263c051500 Mon Sep 17 00:00:00 2001 From: Aakash Date: Fri, 14 Nov 2025 19:48:45 +0530 Subject: [PATCH 4/4] Fixed Adjustment timesheet issues --- src/controllers/va/vaLog/vaLogWorkReport.js | 50 ++++++++++++++++++--- src/models/vaModel.js | 8 ++++ 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/src/controllers/va/vaLog/vaLogWorkReport.js b/src/controllers/va/vaLog/vaLogWorkReport.js index fd9344f..8064a39 100644 --- a/src/controllers/va/vaLog/vaLogWorkReport.js +++ b/src/controllers/va/vaLog/vaLogWorkReport.js @@ -1,6 +1,24 @@ import jwt from "jsonwebtoken"; import { VAModel, ProjectModel, LoggedModel } from "../../../models/vaModel.js"; +// Helper function to parse date string in DD-MM-YYYY format +function parseDate(dateString) { + if (!dateString) return null; + + const parts = dateString.split('-'); + if (parts.length !== 3) return null; + + const day = parseInt(parts[0], 10); + const month = parseInt(parts[1], 10) - 1; // Months are 0-indexed in JavaScript + const year = parseInt(parts[2], 10); + + // Validate date components + if (isNaN(day) || isNaN(month) || isNaN(year)) return null; + if (day < 1 || day > 31 || month < 0 || month > 11 || year < 1900) return null; + + return new Date(year, month, day); +} + // Getting logged hours in a specific project of VA async function va_log_work_report_callback(req, res) { try { @@ -27,15 +45,27 @@ async function va_log_work_report_callback(req, res) { const payload = req.body || {}; const { fromDate, toDate } = payload; - const start = fromDate ? new Date(fromDate) : new Date(); - const end = toDate ? new Date(toDate) : new Date(); + let start = fromDate ? parseDate(fromDate) : new Date(); + let end = toDate ? parseDate(toDate) : new Date(); - if (!fromDate && !toDate) { - start.setHours(0, 0, 0, 0); - end.setHours(23, 59, 59, 999); + if ((fromDate && (!start || isNaN(start.getTime()))) || (toDate && (!end || isNaN(end.getTime())))) { + return res.status(400).json({ + success: false, + message: "Invalid Date format. Use DD-MM-YYYY" + }); } - const logs = await LoggedModel.find({ + if (!fromDate && !toDate) { + // If no dates provided, default to today + start.setHours(0, 0, 0, 0); + end.setHours(23, 59, 59, 999); + } else { + // If dates are provided, set proper time boundaries + if (fromDate) start.setHours(0, 0, 0, 0); + if (toDate) end.setHours(23, 59, 59, 999); + } + + let logs = await LoggedModel.find({ vaId, time: { $gte: start, $lte: end }, }) @@ -58,6 +88,13 @@ async function va_log_work_report_callback(req, res) { let lastLogin = null; let lastProjectId = null; + const hasAdjustmentLogs = logs.some(log => log.isAdjustment === true); + + if(hasAdjustmentLogs) { + logs = logs.filter(log => log.isAdjustment === true); + } + + // ⏱️ Calculate work duration for (const log of logs) { const time = new Date(log.time); @@ -109,6 +146,7 @@ async function va_log_work_report_callback(req, res) { }); } catch (error) { + console.log(error) return res.status(500).send("Internal Server Error"); } } diff --git a/src/models/vaModel.js b/src/models/vaModel.js index 3f6a423..5bb7bac 100644 --- a/src/models/vaModel.js +++ b/src/models/vaModel.js @@ -237,6 +237,14 @@ const loggedSchema = new Schema({ projectId: { type: Types.ObjectId, ref: "Project" + }, + isAdjustment: { + type: Boolean, + default: false + }, + adjustmentId: { + type: Types.ObjectId, + ref: "AdjustedHours" } })