From 9c736dea02774eade165671fa1cd86731464644c Mon Sep 17 00:00:00 2001
From: Aakash
Date: Mon, 24 Nov 2025 19:49:28 +0530
Subject: [PATCH 1/7] 24-11-2025
---
views/client/dashboard-dashboard.ejs | 2 +-
views/client/dashboard-virtual-assistants.ejs | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/views/client/dashboard-dashboard.ejs b/views/client/dashboard-dashboard.ejs
index 0e55e21..10eb82e 100644
--- a/views/client/dashboard-dashboard.ejs
+++ b/views/client/dashboard-dashboard.ejs
@@ -242,7 +242,7 @@
// Format time function
function formatTime(seconds) {
- if (!seconds) return 'Not working';
+ if (!seconds) return 'No Record Found';
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
diff --git a/views/client/dashboard-virtual-assistants.ejs b/views/client/dashboard-virtual-assistants.ejs
index 972052a..773cbbf 100644
--- a/views/client/dashboard-virtual-assistants.ejs
+++ b/views/client/dashboard-virtual-assistants.ejs
@@ -242,7 +242,7 @@
// Format time function
function formatTime(seconds) {
- if (!seconds) return 'Not working';
+ if (!seconds) return 'No Record Found';
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
From 39010ee61c48d5b09638069141723eb0ca2ce2ef Mon Sep 17 00:00:00 2001
From: Aakash
Date: Tue, 25 Nov 2025 19:39:48 +0530
Subject: [PATCH 2/7] Added delete VA and project route, fixed tracker api
issues
---
public/js/va-client-register.js | 12 +-
.../project/deleteProjectSf.controller.js | 97 ++++++++++++++
.../va/vaLog/vaLogTrackWork.controller.js | 3 +-
.../va/vaProfile/vaDelete.controller.js | 92 ++++++++++++++
.../va/vaProjects/vaProjectLog.controller.js | 120 +++++++++++++++---
src/models/vaModel.js | 3 +
src/routes/v1/index.js | 2 +
src/routes/v1/va.routes.js | 2 +
8 files changed, 306 insertions(+), 25 deletions(-)
create mode 100644 src/controllers/project/deleteProjectSf.controller.js
create mode 100644 src/controllers/va/vaProfile/vaDelete.controller.js
diff --git a/public/js/va-client-register.js b/public/js/va-client-register.js
index a9d6788..7c330b2 100644
--- a/public/js/va-client-register.js
+++ b/public/js/va-client-register.js
@@ -693,12 +693,12 @@ async function submitForm() {
let cardNumber = document.getElementById('card-number');
let expiryDate = document.getElementById('expiry-date');
let securityCode = document.getElementById('security-code');
- let cardZip = document.getElementById('card-zip').value;
- let bankName = document.getElementById('bank-name').value;
- let accountNumber = document.getElementById('account-number').value;
- let routingNumber = document.getElementById('routing-number').value;
- let accountType = document.getElementById('account-type').value;
- let accountHolderName = document.getElementById('account-holder-name').value;
+ let cardZip = document.getElementById('card-zip');
+ let bankName = document.getElementById('bank-name');
+ let accountNumber = document.getElementById('account-number');
+ let routingNumber = document.getElementById('routing-number');
+ let accountType = document.getElementById('account-type');
+ let accountHolderName = document.getElementById('account-holder-name');
if(cardNumber.value) {
formData.userDetails.cardNumber = cardNumber.value;
diff --git a/src/controllers/project/deleteProjectSf.controller.js b/src/controllers/project/deleteProjectSf.controller.js
new file mode 100644
index 0000000..c3c6e39
--- /dev/null
+++ b/src/controllers/project/deleteProjectSf.controller.js
@@ -0,0 +1,97 @@
+import { ProjectModel, VAModel, ClientModel } from "../../models/vaModel.js";
+import { Types } from "mongoose";
+
+/**
+ * Delete project and all related data (without transaction)
+ */
+async function delete_project_sf(req, res) {
+ try {
+ const { projectId } = req.params;
+
+ // Validate projectId
+ if (!projectId) {
+ return res.status(400).json({
+ success: false,
+ message: "Project ID is required"
+ });
+ }
+
+ let projectObjectId;
+ try {
+ projectObjectId = new Types.ObjectId(projectId);
+ } catch (error) {
+ return res.status(400).json({
+ success: false,
+ message: "Invalid Project ID format"
+ });
+ }
+
+ // Check if project exists
+ const existingProject = await ProjectModel.findById(projectObjectId);
+ if (!existingProject) {
+ return res.status(404).json({
+ success: false,
+ message: "Project not found"
+ });
+ }
+
+ const clientId = existingProject.clientId;
+ const vaAssigned = existingProject.vaAssigned || [];
+ const projectName = existingProject.name;
+
+ console.log(`Deleting project: ${projectName} (${projectId})`);
+
+ const deletionSummary = {};
+
+ // 1. Remove project from Client's projectIds array
+ if (clientId) {
+ await ClientModel.findByIdAndUpdate(
+ clientId,
+ { $pull: { projectIds: projectObjectId } }
+ );
+ deletionSummary.clientUpdated = true;
+ console.log(`Removed project from client`);
+ }
+
+ // 2. Remove project from all assigned VAs' projectIds arrays
+ if (vaAssigned.length > 0) {
+ const vaResult = await VAModel.updateMany(
+ { _id: { $in: vaAssigned } },
+ { $pull: { projectIds: projectObjectId } }
+ );
+ deletionSummary.vasUpdated = vaResult.modifiedCount;
+ console.log(`Removed project from ${deletionSummary.vasUpdated} VAs`);
+ } else {
+ deletionSummary.vasUpdated = 0;
+ }
+
+ // 6. Finally delete the project
+ await ProjectModel.findByIdAndDelete(projectObjectId);
+
+ console.log(`✅ Successfully deleted project: ${projectName} (${projectId})`);
+
+ res.status(200).json({
+ success: true,
+ message: 'Project and all related data deleted successfully',
+ data: {
+ projectId: projectId,
+ projectName: projectName,
+ clientId: clientId,
+ deletedAt: new Date().toISOString()
+ }
+ });
+
+ } catch (error) {
+ console.error("delete_project_sf error:", error);
+
+ return res.status(500).json({
+ success: false,
+ message: "Failed to delete project",
+ error: process.env.NODE_ENV === 'production'
+ ? 'Internal server error'
+ : error.message
+ });
+ }
+}
+
+export { delete_project_sf };
\ No newline at end of file
diff --git a/src/controllers/va/vaLog/vaLogTrackWork.controller.js b/src/controllers/va/vaLog/vaLogTrackWork.controller.js
index ddf5d22..8333dfd 100644
--- a/src/controllers/va/vaLog/vaLogTrackWork.controller.js
+++ b/src/controllers/va/vaLog/vaLogTrackWork.controller.js
@@ -147,7 +147,7 @@ async function va_log_track_work_callback(req, res) {
const vaId = vaUser._id;
const payload = req.body || {};
- const { projectId, logId, title, appName } = payload;
+ const { projectId, logId, title, appName, url = null } = payload;
const project = await ProjectModel.findById(projectId);
if (!project) return res.status(400).json({ success: false, message: "Project not found" });
@@ -190,6 +190,7 @@ async function va_log_track_work_callback(req, res) {
logId,
title,
appName,
+ url,
from: Date.now(),
});
diff --git a/src/controllers/va/vaProfile/vaDelete.controller.js b/src/controllers/va/vaProfile/vaDelete.controller.js
new file mode 100644
index 0000000..8b71c12
--- /dev/null
+++ b/src/controllers/va/vaProfile/vaDelete.controller.js
@@ -0,0 +1,92 @@
+import { VAModel, ClientModel, ProjectModel, LoggedModel, WorkTrackModel } from "../../../models/vaModel.js";
+
+// Delete VA and all related data
+async function va_remove_function(req, res) {
+ try {
+ const vaId = req.params.id;
+
+ // Validate VA ID
+ if (!vaId) {
+ return res.status(400).json({
+ success: false,
+ message: "VA ID is required"
+ });
+ }
+
+ // Find the VA to get clientIds and projectIds before deletion
+ const vaUser = await VAModel.findById(vaId);
+ if (!vaUser) {
+ return res.status(404).json({
+ success: false,
+ message: "VA not found"
+ });
+ }
+
+ const clientIds = vaUser.clientIds || [];
+ const projectIds = vaUser.projectIds || [];
+ const vaEmail = vaUser.userDetails?.email;
+
+ console.log(`Deleting VA ${vaId} - Affected: ${clientIds.length} clients, ${projectIds.length} projects`);
+
+ // 1. Remove VA from all Clients
+ let clientsUpdated = 0;
+ if (clientIds.length > 0) {
+ const clientResult = await ClientModel.updateMany(
+ { _id: { $in: clientIds } },
+ { $pull: { vaIds: vaId } }
+ );
+ clientsUpdated = clientResult.modifiedCount;
+ console.log(`Removed VA from ${clientsUpdated} clients`);
+ }
+
+ // 2. Remove VA from all Projects
+ let projectsUpdated = 0;
+ if (projectIds.length > 0) {
+ const projectResult = await ProjectModel.updateMany(
+ { _id: { $in: projectIds } },
+ { $pull: { vaAssigned: vaId } }
+ );
+ projectsUpdated = projectResult.modifiedCount;
+ console.log(`Removed VA from ${projectsUpdated} projects`);
+ }
+
+ // 3. Delete all WorkTrack records for this VA
+ const workTrackResult = await WorkTrackModel.deleteMany({ vaId: vaId });
+ console.log(`Deleted ${workTrackResult.deletedCount} work track records`);
+
+ // 4. Delete all Logged records for this VA
+ const loggedResult = await LoggedModel.deleteMany({ vaId: vaId });
+ console.log(`Deleted ${loggedResult.deletedCount} log records`);
+
+ // 5. Finally delete the VA
+ await VAModel.findByIdAndDelete(vaId);
+
+ console.log(`Successfully deleted VA: ${vaId} and all related data`);
+
+ res.status(200).json({
+ success: true,
+ message: 'Virtual Assistant and all related data deleted successfully',
+ data: {
+ vaId: vaId,
+ vaName: `${vaUser.userDetails?.firstName || ''} ${vaUser.userDetails?.lastName || ''}`.trim() || 'Unknown VA',
+ vaEmail: vaEmail,
+ clientsUpdated: clientsUpdated,
+ projectsUpdated: projectsUpdated,
+ workTracksDeleted: workTrackResult.deletedCount,
+ logsDeleted: loggedResult.deletedCount,
+ deletedAt: new Date().toISOString()
+ }
+ });
+
+ } catch (error) {
+ console.error('Error deleting VA:', error);
+
+ res.status(500).json({
+ success: false,
+ message: 'Failed to delete Virtual Assistant',
+ error: 'Internal server error'
+ });
+ }
+}
+
+export { va_remove_function };
\ No newline at end of file
diff --git a/src/controllers/va/vaProjects/vaProjectLog.controller.js b/src/controllers/va/vaProjects/vaProjectLog.controller.js
index 1f62636..07ca354 100644
--- a/src/controllers/va/vaProjects/vaProjectLog.controller.js
+++ b/src/controllers/va/vaProjects/vaProjectLog.controller.js
@@ -1,12 +1,12 @@
import { LoggedModel, WorkTrackModel, ProjectModel } from "../../../models/vaModel.js";
-// Helper function to get start and end of day
+// Helper function to get start and end of day in UTC
function getDayRange(date = new Date()) {
const start = new Date(date);
- start.setHours(0, 0, 0, 0);
+ start.setUTCHours(0, 0, 0, 0);
const end = new Date(date);
- end.setHours(23, 59, 59, 999);
+ end.setUTCHours(23, 59, 59, 999);
return { start, end };
}
@@ -25,10 +25,53 @@ function formatHoursToReadable(hours) {
}
}
-// function to close open work tracks and create logout
-async function closePreviousSession(vaId, lastLog) {
+// Helper function to parse ISO time string
+function parseISOTimeString(timeString) {
+ if (!timeString) return new Date();
+
try {
- const currentTime = new Date();
+ // Parse ISO string directly
+ const parsedDate = new Date(timeString);
+
+ // Check if the date is valid
+ if (isNaN(parsedDate.getTime())) {
+ throw new Error('Invalid date');
+ }
+ parsedDate.setMilliseconds(0);
+
+ console.log("pasdes date" + parsedDate)
+ return parsedDate;
+ } catch (error) {
+ console.error('Error parsing ISO time string:', error);
+ return new Date();
+ }
+}
+
+function setMillisecondsToZero(date) {
+ const newDate = new Date(date);
+ newDate.setMilliseconds(0);
+ return newDate;
+}
+
+// Helper function to check if time difference is less than threshold
+function isTimeWithinThreshold(receivedTime, currentTime, thresholdSeconds = 10) {
+ const diffMs = Math.abs(currentTime - receivedTime);
+ const diffSeconds = diffMs / 1000;
+ console.log(`Time comparison - Received: ${receivedTime.toISOString()}, Current: ${currentTime.toISOString()}, Diff: ${diffSeconds}s`);
+ return diffSeconds <= thresholdSeconds;
+}
+
+// Helper function to get current UTC time
+function getCurrentUTCTime() {
+ const now = new Date();
+ now.setMilliseconds(0);
+ return now;
+}
+
+// function to close open work tracks and create logout
+async function closePreviousSession(vaId, lastLog, logTime = new Date()) {
+ try {
+ const currentTime = logTime;
// Find all open work tracks for this VA and project (where to is not set or totalTime is 0)
const openWorkTracks = await WorkTrackModel.find({
@@ -120,18 +163,44 @@ async function closePreviousSession(vaId, lastLog) {
async function va_project_log_callback(req, res) {
try {
const payload = req.body || {};
- const { vaId, vaEmail, projectId, type } = payload;
+ const { vaId, vaEmail, projectId, type, time: receivedTimeString } = payload;
+ console.log("Received payload:", payload);
- if (!vaId, !vaEmail, !projectId, !type) return res.status(400).send("Missing Props");
+ if (!vaId || !vaEmail || !projectId || !type) {
+ return res.status(400).send("Missing required properties");
+ }
+
+ // Determine the log time based on received time (both in UTC)
+ const currentTime = getCurrentUTCTime(); // Current UTC time
+ let logTime = currentTime;
+
+ if (receivedTimeString) {
+ // Parse the received ISO time string
+ const receivedTime = parseISOTimeString(receivedTimeString);
+
+ // Check if the difference is less than 10 seconds
+ if (isTimeWithinThreshold(receivedTime, currentTime, 10)) {
+ logTime = receivedTime;
+ console.log(`✅ Using received ISO time: ${receivedTimeString}, difference is within 10 seconds`);
+ console.log(`✅ Parsed as: ${receivedTime.toISOString()}`);
+ } else {
+ logTime = currentTime;
+ console.log(`⚠️ Using current UTC time, received time difference exceeds 10 seconds.`);
+ console.log(`Received: ${receivedTimeString} (parsed: ${receivedTime.toISOString()})`);
+ console.log(`Current: ${currentTime.toISOString()}`);
+ }
+ } else {
+ console.log(`ℹ️ No time received, using current UTC time: ${currentTime.toISOString()}`);
+ }
const project = await ProjectModel.findById(projectId);
if (!project) {
return res.status(400).json({ success: false, message: "Project not found" });
}
- // For LOGIN requests, check daily hours
+ // For LOGIN requests, check daily hours (using UTC day range)
if (type === "LOGIN") {
- const { start, end } = getDayRange();
+ const { start, end } = getDayRange(); // UTC day range
// Get today's total hours
const todayWorkEntries = await WorkTrackModel.find({
vaId,
@@ -163,7 +232,7 @@ async function va_project_log_callback(req, res) {
// check last workModel login time and make a logout request then create login
// console.log(`User already logged in. Closing previous session...`);
- const closeResult = await closePreviousSession(vaId, lastLog);
+ const closeResult = await closePreviousSession(vaId, lastLog, logTime);
// console.log(`Previous session closed: ${closeResult.closedWorkTracks} work tracks, logout log: ${closeResult.logoutLogId}`);
@@ -172,7 +241,8 @@ async function va_project_log_callback(req, res) {
loggedType: "LOGIN",
vaId,
vaEmail,
- projectId
+ projectId,
+ time: logTime
});
return res.status(201).send({
@@ -182,13 +252,21 @@ async function va_project_log_callback(req, res) {
previousSession: {
closedWorkTracks: closeResult.closedWorkTracks,
logoutLogId: closeResult.logoutLogId
- }
+ },
+ timeUsed: receivedTimeString ? 'received' : 'current',
+ actualTime: createdLog.time
});
}
}
- const createdLog = await LoggedModel.create({ loggedType: type, vaId, vaEmail, projectId })
-// console.log(createdLog)
+ const createdLog = await LoggedModel.create({
+ loggedType: type,
+ vaId,
+ vaEmail,
+ projectId,
+ time: logTime
+ });
+
try {
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
const sfData = {
@@ -218,7 +296,14 @@ async function va_project_log_callback(req, res) {
const result = await response.json();
// console.log("✅ Successfully synced VA data to Salesforce:", result);
- res.status(201).json({ success: true, message: `${type} Successfully`, logId: createdLog._id, SyncSalesforce: result });
+ res.status(201).json({
+ success: true,
+ message: `${type} Successfully`,
+ logId: createdLog._id,
+ SyncSalesforce: result,
+ timeUsed: receivedTimeString ? 'received' : 'current',
+ actualTime: createdLog.time
+ });
} catch (error) {
console.error("Failed to sync with Salesforce:", error.message);
console.log(error)
@@ -229,9 +314,8 @@ async function va_project_log_callback(req, res) {
throw salesforceError;
}
- // return res.status(201).send({ success: true, message: `${type} Successfully`, logId: createdLog._id });
-
} catch (error) {
+ console.error("Error in va_project_log_callback:", error);
return res.status(500).send("Internal Server Error");
}
}
diff --git a/src/models/vaModel.js b/src/models/vaModel.js
index b33241f..b16190f 100644
--- a/src/models/vaModel.js
+++ b/src/models/vaModel.js
@@ -320,6 +320,9 @@ const vaWorkTrackSchema = new Schema({
appName: {
type: String,
},
+ url: {
+ type: String,
+ },
from: {
type: Date,
default: Date.now,
diff --git a/src/routes/v1/index.js b/src/routes/v1/index.js
index a426f60..4cd658a 100644
--- a/src/routes/v1/index.js
+++ b/src/routes/v1/index.js
@@ -12,6 +12,7 @@ import { process_reset } from "../../controllers/passwordReset.controllers.js";
import { project_add_sf } from "../../controllers/project/projectAddSf.controller.js"
import { update_project_sf } from "../../controllers/project/projectUpdateSf.controller.js"
import { authValidator } from "../../middlewares/auth.middleware.js";
+import { delete_project_sf } from "../../controllers/project/deleteProjectSf.controller.js"
const v1Routes = express.Router()
@@ -29,6 +30,7 @@ v1Routes.use("/client", clientRoutes);
v1Routes.post("/project", authValidator, project_add_sf);
v1Routes.patch("/project/:projectId", authValidator, update_project_sf);
+v1Routes.delete("/project/:projectId", authValidator, delete_project_sf);
v1Routes.post("/password/reset", process_reset);
diff --git a/src/routes/v1/va.routes.js b/src/routes/v1/va.routes.js
index 087e684..fa8549d 100644
--- a/src/routes/v1/va.routes.js
+++ b/src/routes/v1/va.routes.js
@@ -26,6 +26,7 @@ import { va_verify_email } from "../../controllers/va/vaAuth/vaVerifyEmail.contr
import { va_leave_apply_function } from "../../controllers/va/vaLeaves/vaLeavesApply.controller.js";
import { va_leave_list_function } from "../../controllers/va/vaLeaves/vaLeaveList.controller.js"
import { va_leave_manage_function } from "../../controllers/va/vaLeaves/vaLeaveManage.controller.js";
+import { va_remove_function } from "../../controllers/va/vaProfile/vaDelete.controller.js";
vaRoutes.get("/", va_callback); // VA Callback for getting active VA
@@ -42,6 +43,7 @@ vaRoutes.patch("/profile", authValidator, va_profile_update_callback); // Update
vaRoutes.get("/:userName", va_username_callback); // VA Username Callback
+vaRoutes.delete("/:id", va_remove_function); // 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
From 16a62670e7f916d2521626ba0196eae2d71fe033 Mon Sep 17 00:00:00 2001
From: Aakash
Date: Wed, 26 Nov 2025 19:38:08 +0530
Subject: [PATCH 3/7] Added delete route form project, va, and client -
Updated leaves and time adjustment api
---
public/js/va-register.js | 369 +++++++++++++-----
src/controllers/client/vaClient.controller.js | 107 ++++-
.../project/projectAddSf.controller.js | 32 +-
.../va/vaLeaves/vaLeavesApply.controller.js | 195 +++++++--
.../vaProfile/vaProfileUpdate.controller.js | 118 ++++--
.../va/vaProfile/vaRegister.controller.js | 1 +
.../vaProjects/vaProjectReport.controller.js | 41 +-
.../vaTimesheetAdjustment.controller.js | 131 ++++++-
...vaTimesheetAdjustmentSummary.controller.js | 47 ++-
src/middlewares/auth.middleware.js | 2 +
src/models/vaModel.js | 19 +
src/repository/vaClient.repository.js | 25 +-
src/routes/v1/client.routes.js | 4 +-
src/routes/v1/va.routes.js | 4 +-
src/services/vaClient.service.js | 138 ++++++-
views/va_register.ejs | 30 +-
16 files changed, 1002 insertions(+), 261 deletions(-)
diff --git a/public/js/va-register.js b/public/js/va-register.js
index fea6594..ef2e9ac 100644
--- a/public/js/va-register.js
+++ b/public/js/va-register.js
@@ -9,65 +9,113 @@
// Countries and states data
const countries = [
- { code: 'US', name: 'United States' },
- { code: 'CA', name: 'Canada' },
- { code: 'GB', name: 'United Kingdom' },
- { code: 'AU', name: 'Australia' },
- { code: 'IN', name: 'India' },
- { code: 'DE', name: 'Germany' },
- { code: 'FR', name: 'France' },
- { code: 'BR', name: 'Brazil' },
- { code: 'JP', name: 'Japan' },
- { code: 'CN', name: 'China' }
+ { code: 'US', name: 'United States' },
+ { code: 'CA', name: 'Canada' },
+ { code: 'GB', name: 'United Kingdom' },
+ { code: 'AU', name: 'Australia' },
+ { code: 'IN', name: 'India' },
+ { code: 'DE', name: 'Germany' },
+ { code: 'FR', name: 'France' },
+ { code: 'BR', name: 'Brazil' },
+ { code: 'JP', name: 'Japan' },
+ { code: 'CN', name: 'China' },
+ { code: 'MX', name: 'Mexico' },
+ { code: 'ES', name: 'Spain' },
+ { code: 'IT', name: 'Italy' },
+ { code: 'NL', name: 'Netherlands' },
+ { code: 'SE', name: 'Sweden' },
+ { code: 'NO', name: 'Norway' },
+ { code: 'DK', name: 'Denmark' },
+ { code: 'FI', name: 'Finland' },
+ { code: 'BE', name: 'Belgium' },
+ { code: 'CH', name: 'Switzerland' },
+ { code: 'AT', name: 'Austria' },
+ { code: 'PT', name: 'Portugal' },
+ { code: 'IE', name: 'Ireland' },
+ { code: 'PL', name: 'Poland' },
+ { code: 'CZ', name: 'Czech Republic' },
+ { code: 'HU', name: 'Hungary' },
+ { code: 'RO', name: 'Romania' },
+ { code: 'GR', name: 'Greece' },
+ { code: 'TR', name: 'Turkey' },
+ { code: 'SA', name: 'Saudi Arabia' },
+ { code: 'AE', name: 'United Arab Emirates' },
+ { code: 'ZA', name: 'South Africa' },
+ { code: 'NG', name: 'Nigeria' },
+ { code: 'EG', name: 'Egypt' },
+ { code: 'KE', name: 'Kenya' },
+ { code: 'AR', name: 'Argentina' },
+ { code: 'CO', name: 'Colombia' },
+ { code: 'PE', name: 'Peru' },
+ { code: 'CL', name: 'Chile' },
+ { code: 'VE', name: 'Venezuela' },
+ { code: 'KR', name: 'South Korea' },
+ { code: 'SG', name: 'Singapore' },
+ { code: 'MY', name: 'Malaysia' },
+ { code: 'TH', name: 'Thailand' },
+ { code: 'ID', name: 'Indonesia' },
+ { code: 'PH', name: 'Philippines' },
+ { code: 'VN', name: 'Vietnam' },
+ { code: 'PK', name: 'Pakistan' },
+ { code: 'BD', name: 'Bangladesh' },
+ { code: 'LK', name: 'Sri Lanka' },
+ { code: 'NP', name: 'Nepal' },
+ { code: 'RU', name: 'Russia' },
+ { code: 'UA', name: 'Ukraine' },
+ { code: 'IL', name: 'Israel' },
+ { code: 'NZ', name: 'New Zealand' },
+ { code: 'HK', name: 'Hong Kong' },
+ { code: 'TW', name: 'Taiwan' }
];
- const states = {
- 'US': [
- { code: 'CA', name: 'California' }, { code: 'TX', name: 'Texas' }, { code: 'FL', name: 'Florida' },
- { code: 'NY', name: 'New York' }, { code: 'IL', name: 'Illinois' }, { code: 'PA', name: 'Pennsylvania' },
- { code: 'OH', name: 'Ohio' }, { code: 'GA', name: 'Georgia' }, { code: 'NC', name: 'North Carolina' },
- { code: 'MI', name: 'Michigan' }
- ],
- 'CA': [
- { code: 'ON', name: 'Ontario' }, { code: 'QC', name: 'Quebec' }, { code: 'BC', name: 'British Columbia' },
- { code: 'AB', name: 'Alberta' }, { code: 'MB', name: 'Manitoba' }, { code: 'SK', name: 'Saskatchewan' }
- ],
- 'GB': [
- { code: 'ENG', name: 'England' }, { code: 'SCT', name: 'Scotland' }, { code: 'WLS', name: 'Wales' },
- { code: 'NIR', name: 'Northern Ireland' }
- ],
- 'AU': [
- { code: 'NSW', name: 'New South Wales' }, { code: 'VIC', name: 'Victoria' }, { code: 'QLD', name: 'Queensland' },
- { code: 'WA', name: 'Western Australia' }, { code: 'SA', name: 'South Australia' }, { code: 'TAS', name: 'Tasmania' }
- ],
- 'IN': [
- { code: 'MH', name: 'Maharashtra' }, { code: 'TN', name: 'Tamil Nadu' }, { code: 'UP', name: 'Uttar Pradesh' },
- { code: 'KA', name: 'Karnataka' }, { code: 'GJ', name: 'Gujarat' }, { code: 'RJ', name: 'Rajasthan' }
- ],
- 'DE': [
- { code: 'BW', name: 'Baden-Württemberg' }, { code: 'BY', name: 'Bavaria' }, { code: 'BE', name: 'Berlin' },
- { code: 'HB', name: 'Bremen' }, { code: 'HH', name: 'Hamburg' }, { code: 'HE', name: 'Hesse' }
- ],
- 'FR': [
- { code: 'ARA', name: 'Auvergne-Rhône-Alpes' }, { code: 'BFC', name: 'Bourgogne-Franche-Comté' },
- { code: 'BRE', name: 'Brittany' }, { code: 'CVL', name: 'Centre-Val de Loire' },
- { code: 'COR', name: 'Corsica' }, { code: 'GES', name: 'Grand Est' }
- ],
- 'BR': [
- { code: 'SP', name: 'São Paulo' }, { code: 'RJ', name: 'Rio de Janeiro' }, { code: 'MG', name: 'Minas Gerais' },
- { code: 'BA', name: 'Bahia' }, { code: 'RS', name: 'Rio Grande do Sul' }, { code: 'PR', name: 'Paraná' }
- ],
- 'JP': [
- { code: 'TK', name: 'Tokyo' }, { code: 'OS', name: 'Osaka' }, { code: 'KY', name: 'Kyoto' },
- { code: 'HK', name: 'Hokkaido' }, { code: 'OK', name: 'Okinawa' }, { code: 'KN', name: 'Kanagawa' }
- ],
- 'CN': [
- { code: 'BJ', name: 'Beijing' }, { code: 'SH', name: 'Shanghai' }, { code: 'TJ', name: 'Tianjin' },
- { code: 'CQ', name: 'Chongqing' }, { code: 'GD', name: 'Guangdong' }, { code: 'JS', name: 'Jiangsu' }
- ]
- };
+ // const states = {
+ // 'US': [
+ // { code: 'CA', name: 'California' }, { code: 'TX', name: 'Texas' }, { code: 'FL', name: 'Florida' },
+ // { code: 'NY', name: 'New York' }, { code: 'IL', name: 'Illinois' }, { code: 'PA', name: 'Pennsylvania' },
+ // { code: 'OH', name: 'Ohio' }, { code: 'GA', name: 'Georgia' }, { code: 'NC', name: 'North Carolina' },
+ // { code: 'MI', name: 'Michigan' }
+ // ],
+ // 'CA': [
+ // { code: 'ON', name: 'Ontario' }, { code: 'QC', name: 'Quebec' }, { code: 'BC', name: 'British Columbia' },
+ // { code: 'AB', name: 'Alberta' }, { code: 'MB', name: 'Manitoba' }, { code: 'SK', name: 'Saskatchewan' }
+ // ],
+ // 'GB': [
+ // { code: 'ENG', name: 'England' }, { code: 'SCT', name: 'Scotland' }, { code: 'WLS', name: 'Wales' },
+ // { code: 'NIR', name: 'Northern Ireland' }
+ // ],
+ // 'AU': [
+ // { code: 'NSW', name: 'New South Wales' }, { code: 'VIC', name: 'Victoria' }, { code: 'QLD', name: 'Queensland' },
+ // { code: 'WA', name: 'Western Australia' }, { code: 'SA', name: 'South Australia' }, { code: 'TAS', name: 'Tasmania' }
+ // ],
+ // 'IN': [
+ // { code: 'MH', name: 'Maharashtra' }, { code: 'TN', name: 'Tamil Nadu' }, { code: 'UP', name: 'Uttar Pradesh' },
+ // { code: 'KA', name: 'Karnataka' }, { code: 'GJ', name: 'Gujarat' }, { code: 'RJ', name: 'Rajasthan' }
+ // ],
+ // 'DE': [
+ // { code: 'BW', name: 'Baden-Württemberg' }, { code: 'BY', name: 'Bavaria' }, { code: 'BE', name: 'Berlin' },
+ // { code: 'HB', name: 'Bremen' }, { code: 'HH', name: 'Hamburg' }, { code: 'HE', name: 'Hesse' }
+ // ],
+ // 'FR': [
+ // { code: 'ARA', name: 'Auvergne-Rhône-Alpes' }, { code: 'BFC', name: 'Bourgogne-Franche-Comté' },
+ // { code: 'BRE', name: 'Brittany' }, { code: 'CVL', name: 'Centre-Val de Loire' },
+ // { code: 'COR', name: 'Corsica' }, { code: 'GES', name: 'Grand Est' }
+ // ],
+ // 'BR': [
+ // { code: 'SP', name: 'São Paulo' }, { code: 'RJ', name: 'Rio de Janeiro' }, { code: 'MG', name: 'Minas Gerais' },
+ // { code: 'BA', name: 'Bahia' }, { code: 'RS', name: 'Rio Grande do Sul' }, { code: 'PR', name: 'Paraná' }
+ // ],
+ // 'JP': [
+ // { code: 'TK', name: 'Tokyo' }, { code: 'OS', name: 'Osaka' }, { code: 'KY', name: 'Kyoto' },
+ // { code: 'HK', name: 'Hokkaido' }, { code: 'OK', name: 'Okinawa' }, { code: 'KN', name: 'Kanagawa' }
+ // ],
+ // 'CN': [
+ // { code: 'BJ', name: 'Beijing' }, { code: 'SH', name: 'Shanghai' }, { code: 'TJ', name: 'Tianjin' },
+ // { code: 'CQ', name: 'Chongqing' }, { code: 'GD', name: 'Guangdong' }, { code: 'JS', name: 'Jiangsu' }
+ // ]
+ // };
// Initialize the form
+
document.addEventListener('DOMContentLoaded', function() {
// Populate country dropdown
const countrySelect = document.getElementById('country');
@@ -80,32 +128,34 @@
countrySelect.appendChild(option);
});
+ autoDetectCountry();
+
// Add event listener for country change
- countrySelect.addEventListener('change', function() {
- const countryCode = this.value;
- const stateSelect = document.getElementById('state');
+ // countrySelect.addEventListener('change', function() {
+ // const countryCode = this.value;
+ // const stateSelect = document.getElementById('state');
- // Show loading
- document.getElementById('state-loading').style.display = 'block';
+ // // Show loading
+ // document.getElementById('state-loading').style.display = 'block';
- // Clear current states
- stateSelect.innerHTML = '';
+ // // Clear current states
+ // stateSelect.innerHTML = '';
- // Simulate API delay
- setTimeout(() => {
- if (countryCode && states[countryCode]) {
- states[countryCode].forEach(state => {
- const option = document.createElement('option');
- option.value = state.code;
- option.textContent = state.name;
- stateSelect.appendChild(option);
- });
- }
+ // // Simulate API delay
+ // setTimeout(() => {
+ // if (countryCode && states[countryCode]) {
+ // states[countryCode].forEach(state => {
+ // const option = document.createElement('option');
+ // option.value = state.code;
+ // option.textContent = state.name;
+ // stateSelect.appendChild(option);
+ // });
+ // }
- // Hide loading
- document.getElementById('state-loading').style.display = 'none';
- }, 500);
- });
+ // // Hide loading
+ // document.getElementById('state-loading').style.display = 'none';
+ // }, 500);
+ // });
// Add input event listeners to handle label animation
@@ -209,6 +259,102 @@
});
});
+ async function autoDetectCountry() {
+ const countrySelect = document.getElementById('country');
+ // const loadingElement = document.getElementById('country-loading');
+
+ try {
+ // loadingElement.style.display = 'block';
+
+ // Using ipapi.co API (free tier available)
+ const response = await fetch('https://ipapi.co/json/');
+
+ if (!response.ok) {
+ throw new Error('Geolocation service unavailable');
+ }
+
+ const data = await response.json();
+
+ if (data && data.country_code) {
+ const detectedCountryCode = data.country_code;
+ const detectedCountryName = data.country_name;
+
+ // Find if the detected country is in our list
+ const countryExists = countries.find(country =>
+ country.code === detectedCountryCode
+ );
+
+ if (countryExists) {
+ // Set the country in dropdown
+ countrySelect.value = detectedCountryCode;
+
+ // Trigger the change event to update UI
+ countrySelect.dispatchEvent(new Event('change'));
+
+ // Show success message
+ console.log(`Auto-detected country: ${detectedCountryName}`);
+ } else {
+ console.log('Detected country not in supported list');
+ }
+ }
+ } catch (error) {
+ console.log('Could not auto-detect country:', error.message);
+ // Fallback: Use browser's geolocation API as backup
+ fallbackGeolocation();
+ } finally {
+ // loadingElement.style.display = 'none';
+ }
+ }
+
+ // Fallback geolocation using browser's navigator.geolocation
+ function fallbackGeolocation() {
+ if (!navigator.geolocation) {
+ console.log('Geolocation is not supported by this browser');
+ return;
+ }
+
+ navigator.geolocation.getCurrentPosition(
+ async function(position) {
+ try {
+ const { latitude, longitude } = position.coords;
+
+ // Use OpenStreetMap Nominatim API for reverse geocoding
+ const response = await fetch(
+ `https://nominatim.openstreetmap.org/reverse?format=json&lat=${latitude}&lon=${longitude}`
+ );
+
+ if (response.ok) {
+ const data = await response.json();
+ if (data && data.address && data.address.country_code) {
+ const countryCode = data.address.country_code.toUpperCase();
+ const countrySelect = document.getElementById('country');
+
+ const countryExists = countries.find(country =>
+ country.code === countryCode
+ );
+
+ if (countryExists) {
+ countrySelect.value = countryCode;
+ countrySelect.dispatchEvent(new Event('change'));
+ console.log(`Fallback geolocation detected: ${data.address.country}`);
+ }
+ }
+ }
+ } catch (error) {
+ console.log('Fallback geolocation failed:', error);
+ }
+ },
+ function(error) {
+ console.log('Browser geolocation error:', error.message);
+ },
+ {
+ enableHighAccuracy: false,
+ timeout: 10000,
+ maximumAge: 600000 // 10 minutes
+ }
+ );
+ }
+
// Function to navigate to any step (for tab navigation)
function goToStep(step) {
// Only allow navigation to completed steps or the next step
@@ -469,20 +615,29 @@
showError('Please select a payment method', null, step);
isValid = false;
} else {
- const paymentMethod = selectedPayment.getAttribute('onclick').match(/'([^']+)'/)[1];
- if (paymentMethod === 'payoneer') {
- const payoneerAccount = document.getElementById('payoneerAccount').value;
- if (!payoneerAccount) {
- showError('Please enter your Payoneer account number', 'payoneerAccount', step);
- isValid = false;
- }
- } else if (paymentMethod === 'veem') {
- const veemAccount = document.getElementById('veemAccount').value;
- if (!veemAccount) {
- showError('Please enter your Veem account number', 'veemAccount', step);
- isValid = false;
- }
- }
+ const paymentMethod = selectedPayment.getAttribute('onclick').match(/'([^']+)'/)[1];
+ if (paymentMethod === 'payoneer') {
+ // const payoneerAccount = document.getElementById('payoneerAccount').value;
+ // if (!payoneerAccount) {
+ // showError('Please enter your Payoneer account number', 'payoneerAccount', step);
+ // isValid = false;
+ // }
+ } else if (paymentMethod === 'veem') {
+ const veemAccount = document.getElementById('veemAccount').value;
+ if (!veemAccount) {
+ showError('Please enter your Veem account number', 'veemAccount', step);
+ isValid = false;
+ }
+ } else if (paymentMethod === 'wise') {
+ const wiseEmail = document.getElementById('wiseEmail').value;
+ if (!wiseEmail) {
+ showError('Please enter your Wise email address', 'wiseEmail', step);
+ isValid = false;
+ } else if (!validateEmail(wiseEmail)) {
+ showError('Please enter a valid email address for Wise', 'wiseEmail', step);
+ isValid = false;
+ }
+ }
}
}
@@ -547,12 +702,15 @@
const paymentMethod = selectedPayment.getAttribute('onclick').match(/'([^']+)'/)[1];
formData.userDetails.paymentMethod = paymentMethod;
- if (document.getElementById('payoneerAccount').value) {
- formData.userDetails.payoneerAccount = document.getElementById('payoneerAccount').value;
- }
+ // if (document.getElementById('payoneerAccount').value) {
+ // formData.userDetails.payoneerAccount = document.getElementById('payoneerAccount').value;
+ // }
if (document.getElementById('veemAccount').value) {
formData.userDetails.veemAccount = document.getElementById('veemAccount').value;
}
+ if (paymentMethod === 'wise') {
+ formData.userDetails.wiseEmail = document.getElementById('wiseEmail').value;
+ }
}
break;
}
@@ -575,12 +733,35 @@
// Show appropriate payment details
if (method === 'payoneer') {
- document.getElementById('payoneer-details').style.display = 'block';
+ // document.getElementById('payoneer-details').style.display = 'block';
} else if (method === 'veem') {
document.getElementById('veem-details').style.display = 'block';
+ } else if (method === 'wise') {
+ document.getElementById('wise-details').style.display = 'block';
}
}
+ function validateWiseEmail(input) {
+ const wiseError = document.getElementById('wise-email-error');
+ const value = input.value.trim();
+
+ // Remove any validation styling first
+ input.style.border = '';
+ wiseError.style.display = 'none';
+
+ if (value.length > 0) {
+ // Check if it's a valid email
+ if (!validateEmail(value)) {
+ wiseError.textContent = 'Please enter a valid email address for Wise';
+ wiseError.style.display = 'block';
+ input.style.border = '1px solid #ef4444';
+ return false;
+ }
+ }
+
+ return true;
+ }
+
// Function to generate a strong password
function generatePassword() {
const length = 12;
diff --git a/src/controllers/client/vaClient.controller.js b/src/controllers/client/vaClient.controller.js
index 700b238..8f23662 100644
--- a/src/controllers/client/vaClient.controller.js
+++ b/src/controllers/client/vaClient.controller.js
@@ -76,7 +76,112 @@ async function get_client_by_email(req, res) {
}
}
+async function delete_client(req, res) {
+ logger.info("ClientController - Delete Client Hit ...");
+
+ try {
+ const { clientId } = req.params;
+
+ if (!clientId) {
+ return res.status(400).json({
+ success: false,
+ message: "Client ID is required"
+ });
+ }
+
+ const deletedClient = await vaClientService.deleteClient(clientId);
+
+ if (!deletedClient) {
+ return res.status(404).json({
+ success: false,
+ message: "Client not found"
+ });
+ }
+
+ logger.info(`ClientController - Client deleted successfully: ${clientId}`);
+ return res.status(200).json({
+ success: true,
+ message: "Client deleted successfully",
+ data: {
+ clientId: clientId,
+ clientName: deletedClient.userDetails?.name || 'Unknown Client',
+ deletedAt: new Date().toISOString()
+ }
+ });
+ } catch (error) {
+ logger.error(`ClientController - Delete error: ${error.message}`);
+
+ if (error.message === "Client not found") {
+ return res.status(404).json({
+ success: false,
+ message: error.message
+ });
+ }
+
+ return res.status(500).json({
+ success: false,
+ message: "Internal Server Error"
+ });
+ }
+}
+
+async function update_client(req, res) {
+ logger.info("ClientController - Update Client Hit ...");
+
+ try {
+ const { clientId } = req.params;
+ const payload = req.body || {};
+ const { userDetails } = payload;
+
+ if (!clientId) {
+ return res.status(400).json({
+ success: false,
+ message: "Client ID is required"
+ });
+ }
+
+ if (!userDetails) {
+ return res.status(400).json({
+ success: false,
+ message: "Missing userDetails in request body"
+ });
+ }
+
+ const updatedClient = await vaClientService.updateClient(clientId, payload);
+
+ logger.info(`ClientController - Client updated successfully: ${clientId}`);
+ return res.status(200).json({
+ success: true,
+ message: "Client updated successfully",
+ data: updatedClient
+ });
+ } catch (error) {
+ logger.error(`ClientController - Update error: ${error.message}`);
+
+ if (error.message === "Client not found") {
+ return res.status(404).json({
+ success: false,
+ message: error.message
+ });
+ }
+
+ if (error.message.includes("Email already exists")) {
+ return res.status(409).json({
+ success: false,
+ message: error.message
+ });
+ }
+
+ return res.status(500).json({
+ success: false,
+ message: "Internal Server Error"
+ });
+ }
+}
+
export {
va_register_client,
- get_client_by_email
+ get_client_by_email,
+ delete_client,
+ update_client
};
\ No newline at end of file
diff --git a/src/controllers/project/projectAddSf.controller.js b/src/controllers/project/projectAddSf.controller.js
index cc08b15..cabe634 100644
--- a/src/controllers/project/projectAddSf.controller.js
+++ b/src/controllers/project/projectAddSf.controller.js
@@ -81,36 +81,8 @@ async function project_add_sf(req, res) {
console.warn('Project created but relationship update failed:', relationshipUpdate.message);
}
- // Send data to Salesforce API after successful registration
- try {
- const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/`;
-
- const response = await fetch(salesforceUrl, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- 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 });
- // res.status(201).json({ success: true, data: created, relationships: relationshipUpdate });
- } 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;
- }
+ res.status(201).json({ success: true, data: created, relationships: relationshipUpdate });
+
} catch (error) {
console.error("project_add_sf error:", error);
return res.status(500).json({ success: false, message: "Internal Server Error" });
diff --git a/src/controllers/va/vaLeaves/vaLeavesApply.controller.js b/src/controllers/va/vaLeaves/vaLeavesApply.controller.js
index b7e3fde..5076441 100644
--- a/src/controllers/va/vaLeaves/vaLeavesApply.controller.js
+++ b/src/controllers/va/vaLeaves/vaLeavesApply.controller.js
@@ -131,6 +131,7 @@ async function va_leave_apply_function(req, res) {
});
}
+ const processedComment = processHTMLComment(comment);
// Create leave request
const leaveRequest = new LeavesModel({
vaId,
@@ -138,62 +139,62 @@ async function va_leave_apply_function(req, res) {
dateTo: endDate,
days: totalDays.toString(),
leaveType,
- comment,
+ comment: processedComment.processedHTML,
+ attachments: processedComment.extractedImages,
status: "PENDING"
});
await leaveRequest.save();
try {
- const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/timesheet-adjustment`;
- const sfData = {
- "id": adjustmentRequest._id,
- "va": {
- "id": vaId
- },
- "project": {
- "id": projectId
- },
- "date": adjustmentDate.toISOString().split('T')[0],
- "loginTime": loginDateTime.toISOString(),
- "logoutTime": logoutDateTime.toISOString(),
- "comment": comment,
- "status": "PENDING",
- "rejectionReason": "",
- "adjustedHours": formatTime(adjustedSeconds)
- }
+ // const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/timesheet-adjustment`;
+ // const sfData = {
+ // "id": leaveRequest._id,
+ // "va": {
+ // "id": vaId
+ // },
+ // "dateFrom": startDate.toISOString().split('T')[0],
+ // "dateTo": endDate.toISOString().split('T')[0],
+ // "totalDays": totalDays,
+ // "leaveType": leaveType,
+ // "comment": processedComment.plainText, // Send plain text to Salesforce
+ // "attachments": processedComment.extractedImages, // Send extracted images info
+ // "status": "PENDING",
+ // "rejectionReason": ""
+ // }
- 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({"data": sfData})
- });
+ // 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({"data": sfData})
+ // });
- if (!response.ok) {
- const errorText = await response.text();
- throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
- }
+ // 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);
+ // const result = await response.json();
+ // // console.log("✅ Successfully synced VA data to Salesforce:", result);
return res.status(201).json({
success: true,
- message: "Adjustment request submitted successfully",
+ message: "Leave request submitted successfully",
data: {
- adjustmentId: adjustmentRequest._id,
- date: adjustmentDate.toISOString().split('T')[0],
- loginTime: loginDateTime.toISOString(),
- logoutTime: logoutDateTime.toISOString(),
- adjustedHours: formatTime(adjustedSeconds),
- comment: comment,
+ leaveId: leaveRequest._id,
+ dateFrom: startDate.toISOString().split('T')[0],
+ dateTo: endDate.toISOString().split('T')[0],
+ totalDays: totalDays,
+ leaveType,
+ comment: processedComment.processedHTML,
+ attachments: processedComment.extractedImages,
status: "PENDING"
},
- SyncSalesforce: result
+ // SyncSalesforce: result
});
} catch (error) {
console.error("Failed to sync with Salesforce:", error.message);
@@ -238,4 +239,116 @@ async function va_leave_apply_function(req, res) {
}
}
+function processHTMLComment(htmlComment) {
+ try {
+ let processedHTML = htmlComment;
+ const extractedImages = [];
+ let plainText = '';
+
+ // Extract plain text from HTML for Salesforce
+ plainText = extractPlainTextFromHTML(htmlComment);
+
+ // Process base64 images in HTML
+ const imageProcessingResult = extractAndProcessBase64Images(htmlComment);
+ processedHTML = imageProcessingResult.processedHTML;
+ extractedImages.push(...imageProcessingResult.extractedImages);
+
+ return {
+ processedHTML,
+ plainText,
+ extractedImages
+ };
+ } catch (error) {
+ console.error("Error processing HTML comment:", error);
+ // Return original content if processing fails
+ return {
+ processedHTML: htmlComment,
+ plainText: extractPlainTextFromHTML(htmlComment),
+ extractedImages: []
+ };
+ }
+}
+
+// Helper function to extract plain text from HTML
+function extractPlainTextFromHTML(html) {
+ try {
+ // Remove HTML tags and decode HTML entities
+ let plainText = html
+ .replace(/<[^>]*>/g, ' ') // Replace tags with spaces
+ .replace(/\s+/g, ' ') // Collapse multiple spaces
+ .trim();
+
+ // Decode common HTML entities
+ plainText = plainText
+ .replace(/ /g, ' ')
+ .replace(/&/g, '&')
+ .replace(/</g, '<')
+ .replace(/>/g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, "'")
+ .replace(/'/g, "'")
+ .replace(///g, '/');
+
+ return plainText;
+ } catch (error) {
+ console.error("Error extracting plain text from HTML:", error);
+ return html.replace(/<[^>]*>/g, ' ').trim();
+ }
+}
+
+// Helper function to extract and process base64 images from HTML
+function extractAndProcessBase64Images(html) {
+ const extractedImages = [];
+ let processedHTML = html;
+
+ try {
+ // Regex to find base64 images in HTML (various formats)
+ const base64Regex = /
]+src="data:image\/([a-zA-Z0-9+]+);base64,([^"]+)"[^>]*>/gi;
+
+ let match;
+ let imageIndex = 0;
+
+ while ((match = base64Regex.exec(html)) !== null) {
+ const fullMatch = match[0];
+ const imageType = match[1]; // png, jpeg, gif, etc.
+ const base64Data = match[2];
+
+ // Create a unique filename
+ const timestamp = Date.now();
+ const filename = `leave_image_${timestamp}_${imageIndex}.${imageType}`;
+
+ // Calculate approximate file size
+ const size = Math.floor((base64Data.length * 3) / 4); // Base64 size calculation
+
+ // Store image information
+ extractedImages.push({
+ filename: filename,
+ mimeType: `image/${imageType}`,
+ base64Data: base64Data,
+ size: size,
+ originalHTML: fullMatch, // Store original img tag for reference
+ index: imageIndex
+ });
+
+ // Replace base64 with a reference in HTML
+ const replacementHTML = `
`;
+
+ processedHTML = processedHTML.replace(fullMatch, replacementHTML);
+
+ imageIndex++;
+ }
+
+ return {
+ processedHTML,
+ extractedImages
+ };
+ } catch (error) {
+ console.error("Error processing base64 images:", error);
+ return {
+ processedHTML: html,
+ extractedImages: []
+ };
+ }
+}
+
export { va_leave_apply_function };
\ No newline at end of file
diff --git a/src/controllers/va/vaProfile/vaProfileUpdate.controller.js b/src/controllers/va/vaProfile/vaProfileUpdate.controller.js
index 66a78b3..0e8082f 100644
--- a/src/controllers/va/vaProfile/vaProfileUpdate.controller.js
+++ b/src/controllers/va/vaProfile/vaProfileUpdate.controller.js
@@ -5,28 +5,61 @@ import { VAModel } from "../../../models/vaModel.js";
// Register a new VA
async function va_profile_update_callback(req, res) {
try {
+ const { sf_token } = req.headers.sf_token;
const accessToken = req.headers.token;
- if (!accessToken) {
- return res.status(401).json({ success: false, message: "Missing access token" });
- }
-
- let decoded;
- try {
- decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN);
- } catch (err) {
- return res.status(401).json({ success: false, message: "Invalid or expired token" });
- }
-
- if (!decoded?.email) {
- return res.status(400).json({ success: false, message: "Invalid token payload" });
- }
-
- const vaUser = await VAModel.findOne({ "userDetails.email": decoded.email });
- if (!vaUser) {
- return res.status(404).json({ success: false, message: "VA not found" });
+ let vaUser;
+ let authSource = 'regular';
+
+ if(sf_token) {
+ try {
+ const sfDecoded = jwt.verify(sf_token, process.env.JWT_SECRET_SF_TOKEN);
+
+ if (!sfDecoded?.email && !sfDecoded?.vaId) {
+ return res.status(400).json({ success: false, message: "Invalid Salesforce token payload" });
+ }
+
+ // Find user by email or VA ID from Salesforce token
+ if (sfDecoded.email) {
+ vaUser = await VAModel.findOne({ "userDetails.email": sfDecoded.email });
+ } else if (sfDecoded.vaId) {
+ vaUser = await VAModel.findById(sfDecoded.vaId);
+ }
+
+ if (!vaUser) {
+ return res.status(404).json({ success: false, message: "VA not found with Salesforce token" });
+ }
+
+ authSource = 'salesforce';
+
+ } catch (sfError) {
+ return res.status(401).json({ success: false, message: "Invalid or expired Salesforce token" });
+ }
+ } else {
+ try {
+ if (!accessToken) {
+ return res.status(401).json({ success: false, message: "Missing access token" });
+ }
+
+ const decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN);
+
+ if (!decoded?.email) {
+ return res.status(400).json({ success: false, message: "Invalid token payload" });
+ }
+
+ vaUser = await VAModel.findOne({ "userDetails.email": decoded.email });
+ if (!vaUser) {
+ return res.status(404).json({ success: false, message: "VA not found" });
+ }
+
+ authSource = 'regular';
+
+ } catch (tokenError) {
+ return res.status(401).json({ success: false, message: "Invalid or expired access token" });
+ }
}
+
const payload = req.body || {};
const { userDetails } = payload;
@@ -34,23 +67,36 @@ async function va_profile_update_callback(req, res) {
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",
+ "city", "state", "country", "phone", "skype",
+ "paymentMethod", "payoneerAccount", "veemAccount"
+ ];
- // // Update allowed fields only
- // allowedFields.forEach(field => {
- // if (userDetails[field] !== undefined) {
- // vaUser.userDetails[field] = userDetails[field];
- // }
- // });
+ let hasUpdates = false;
+ const updatedFields = [];
+
+ // Update allowed fields if provided
+ allowedFields.forEach(field => {
+ if (userDetails[field] !== undefined && userDetails[field] !== null && userDetails[field] !== '') {
+ vaUser.userDetails[field] = userDetails[field];
+ hasUpdates = true;
+ updatedFields.push(field);
+ }
+ });
/* ---------------------- PASSWORD UPDATE ---------------------- */
+ let passwordUpdated = false;
if (userDetails.password || userDetails.cPassword || userDetails.oldPassword) {
const { oldPassword, password, cPassword } = userDetails;
+ if (authSource === 'salesforce') {
+ return res.status(403).json({
+ success: false,
+ message: "Password changes require regular authentication",
+ });
+ }
+
if (!oldPassword || !password || !cPassword) {
return res.status(400).json({
success: false,
@@ -78,11 +124,16 @@ async function va_profile_update_callback(req, res) {
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(password, saltRounds);
vaUser.userDetails.password = hashedPassword;
- } else {
+ hasUpdates = true;
+ passwordUpdated = true;
+ updatedFields.push('password');
+ }
+
+ if (!hasUpdates) {
return res.status(400).json({
- success: false,
- message: "Data not found",
- });
+ success: false,
+ message: "No valid fields to update",
+ });
}
await vaUser.save();
@@ -95,6 +146,7 @@ async function va_profile_update_callback(req, res) {
success: true,
message: "Profile updated successfully",
data: updatedUser,
+ updatedFields: updatedFields,
});
} catch (error) {
return res.status(500).json({ success: false, message: "Internal Server Error" });
diff --git a/src/controllers/va/vaProfile/vaRegister.controller.js b/src/controllers/va/vaProfile/vaRegister.controller.js
index e88cecf..5746119 100644
--- a/src/controllers/va/vaProfile/vaRegister.controller.js
+++ b/src/controllers/va/vaProfile/vaRegister.controller.js
@@ -36,6 +36,7 @@ async function va_register(req, res) {
paymentMethod: userDetails.paymentMethod || null,
payoneerAccount: userDetails.payoneerAccount || null,
veemAccount: userDetails.veemAccount || null,
+ wiseEmail: userDetails.wiseEmail || null,
phone: userDetails.phone || null,
skype: userDetails.skype || null,
},
diff --git a/src/controllers/va/vaProjects/vaProjectReport.controller.js b/src/controllers/va/vaProjects/vaProjectReport.controller.js
index 2b2bbc6..13c7a34 100644
--- a/src/controllers/va/vaProjects/vaProjectReport.controller.js
+++ b/src/controllers/va/vaProjects/vaProjectReport.controller.js
@@ -11,23 +11,36 @@ 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 sf_token = req.headers.sf_token;
+ let vaId;
+ let vaUser;
+ let vaEmail;
+ if(sf_token) {
+ vaId = req.body.vaId;
+ vaUser = await VAModel.findById(vaId);
+ if (!vaUser) {
+ return res.status(404).json({ success: false, message: "VA not found" });
+ }
+ vaEmail = vaUser.userDetails.email
+ } else {
+ 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 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" });
- }
+ vaEmail = decoded.email;
+ vaUser = await VAModel.findOne({ "userDetails.email": vaEmail });
+ if (!vaUser) {
+ return res.status(404).json({ success: false, message: "VA not found" });
+ }
- const vaId = vaUser._id;
+ vaId = vaUser._id;
+ }
const { projectId, from, to } = req.params;
if (!projectId) {
diff --git a/src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustment.controller.js b/src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustment.controller.js
index 6931442..a252386 100644
--- a/src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustment.controller.js
+++ b/src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustment.controller.js
@@ -59,14 +59,6 @@ async function va_timesheet_adjustment_function(req, res) {
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(year, month, day, hours, minutes, seconds);
const combined = new Date(dateObj);
combined.setHours(hours, minutes, seconds, 0);
@@ -120,13 +112,16 @@ async function va_timesheet_adjustment_function(req, res) {
});
}
+ const processedComment = processHTMLComment(comment);
+
const adjustmentRequest = new AdjustedHoursModel({
vaId,
projectId,
date: startOfDay,
loginTime: loginDateTime,
logoutTime: logoutDateTime,
- comment,
+ comment: processedComment.processedHTML,
+ attachments: processedComment.extractedImages,
status: "PENDING"
});
@@ -153,7 +148,8 @@ async function va_timesheet_adjustment_function(req, res) {
"date": adjustmentDate.toISOString().split('T')[0],
"loginTime": loginDateTime.toISOString(),
"logoutTime": logoutDateTime.toISOString(),
- "comment": comment,
+ "comment": processedComment.plainText, // Send plain text to Salesforce
+ "attachments": processedComment.extractedImages, // Send extracted images info
"status": "PENDING",
"rejectionReason": "",
"adjustedHours": formatTime(adjustedSeconds)
@@ -186,7 +182,8 @@ async function va_timesheet_adjustment_function(req, res) {
loginTime: loginDateTime.toISOString(),
logoutTime: logoutDateTime.toISOString(),
adjustedHours: formatTime(adjustedSeconds),
- comment: comment,
+ comment: processedComment.processedHTML,
+ attachments: processedComment.extractedImages,
status: "PENDING"
},
SyncSalesforce: result
@@ -221,4 +218,116 @@ async function va_timesheet_adjustment_function(req, res) {
}
}
+function processHTMLComment(htmlComment) {
+ try {
+ let processedHTML = htmlComment;
+ const extractedImages = [];
+ let plainText = '';
+
+ // Extract plain text from HTML for Salesforce
+ plainText = extractPlainTextFromHTML(htmlComment);
+
+ // Process base64 images in HTML
+ const imageProcessingResult = extractAndProcessBase64Images(htmlComment);
+ processedHTML = imageProcessingResult.processedHTML;
+ extractedImages.push(...imageProcessingResult.extractedImages);
+
+ return {
+ processedHTML,
+ plainText,
+ extractedImages
+ };
+ } catch (error) {
+ console.error("Error processing HTML comment:", error);
+ // Return original content if processing fails
+ return {
+ processedHTML: htmlComment,
+ plainText: extractPlainTextFromHTML(htmlComment),
+ extractedImages: []
+ };
+ }
+}
+
+// Helper function to extract plain text from HTML
+function extractPlainTextFromHTML(html) {
+ try {
+ // Remove HTML tags and decode HTML entities
+ let plainText = html
+ .replace(/<[^>]*>/g, ' ') // Replace tags with spaces
+ .replace(/\s+/g, ' ') // Collapse multiple spaces
+ .trim();
+
+ // Decode common HTML entities
+ plainText = plainText
+ .replace(/ /g, ' ')
+ .replace(/&/g, '&')
+ .replace(/</g, '<')
+ .replace(/>/g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, "'")
+ .replace(/'/g, "'")
+ .replace(///g, '/');
+
+ return plainText;
+ } catch (error) {
+ console.error("Error extracting plain text from HTML:", error);
+ return html.replace(/<[^>]*>/g, ' ').trim();
+ }
+}
+
+// Helper function to extract and process base64 images from HTML
+function extractAndProcessBase64Images(html) {
+ const extractedImages = [];
+ let processedHTML = html;
+
+ try {
+ // Regex to find base64 images in HTML (various formats)
+ const base64Regex = /
]+src="data:image\/([a-zA-Z0-9+]+);base64,([^"]+)"[^>]*>/gi;
+
+ let match;
+ let imageIndex = 0;
+
+ while ((match = base64Regex.exec(html)) !== null) {
+ const fullMatch = match[0];
+ const imageType = match[1]; // png, jpeg, gif, etc.
+ const base64Data = match[2];
+
+ // Create a unique filename
+ const timestamp = Date.now();
+ const filename = `adjustment_image_${timestamp}_${imageIndex}.${imageType}`;
+
+ // Calculate approximate file size
+ const size = Math.floor((base64Data.length * 3) / 4); // Base64 size calculation
+
+ // Store image information
+ extractedImages.push({
+ filename: filename,
+ mimeType: `image/${imageType}`,
+ base64Data: base64Data,
+ size: size,
+ originalHTML: fullMatch, // Store original img tag for reference
+ index: imageIndex
+ });
+
+ // Replace base64 with a reference in HTML
+ const replacementHTML = `
`;
+
+ processedHTML = processedHTML.replace(fullMatch, replacementHTML);
+
+ imageIndex++;
+ }
+
+ return {
+ processedHTML,
+ extractedImages
+ };
+ } catch (error) {
+ console.error("Error processing base64 images:", error);
+ return {
+ processedHTML: html,
+ extractedImages: []
+ };
+ }
+}
+
export { va_timesheet_adjustment_function };
\ No newline at end of file
diff --git a/src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustmentSummary.controller.js b/src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustmentSummary.controller.js
index e0d2096..94020f2 100644
--- a/src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustmentSummary.controller.js
+++ b/src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustmentSummary.controller.js
@@ -3,24 +3,37 @@ import { VAModel, ProjectModel, LoggedModel } from "../../../models/vaModel.js";
async function va_timesheet_summary_function(req, res) {
try {
- const accessToken = req.headers.token;
-
- if (!accessToken) {
- return res.status(401).json({ success: false, message: "Missing token" });
+ const sf_token = req.headers.sf_token;
+ let vaId;
+ let vaUser;
+ let vaEmail;
+ if(sf_token) {
+ vaId = req.body.vaId;
+ vaUser = await VAModel.findById(vaId);
+ if (!vaUser) {
+ return res.status(404).json({ success: false, message: "VA not found" });
+ }
+ vaEmail = vaUser.userDetails.email
+ } else {
+ 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" });
+ }
+
+ vaEmail = decoded.email;
+ vaUser = await VAModel.findOne({ "userDetails.email": vaEmail });
+ if (!vaUser) {
+ return res.status(404).json({ success: false, message: "VA not found" });
+ }
+
+ vaId = vaUser._id;
}
-
- 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;
if (!projectId || !date) {
diff --git a/src/middlewares/auth.middleware.js b/src/middlewares/auth.middleware.js
index ee688f6..49cf229 100644
--- a/src/middlewares/auth.middleware.js
+++ b/src/middlewares/auth.middleware.js
@@ -6,6 +6,8 @@ async function authValidator(req, res, next) {
try {
const sf_token = req.headers.sf_token;
if (sf_token) {
+ const { vaId } = req.params;
+ if(vaId) req.body.vaId = vaId;
next();
} else {
const accessToken = req.headers.token;
diff --git a/src/models/vaModel.js b/src/models/vaModel.js
index b16190f..ee1ef66 100644
--- a/src/models/vaModel.js
+++ b/src/models/vaModel.js
@@ -46,6 +46,9 @@ const userDetailsSchema = new Schema({
veemAccount: {
type: String,
},
+ wiseEmail: {
+ type: String,
+ },
phone: {
type: String,
},
@@ -404,6 +407,14 @@ const adjustedHoursSchema = new Schema({
type: String,
required: true
},
+ attachments: [{
+ filename: String,
+ mimeType: String,
+ base64Data: String,
+ size: Number,
+ originalHTML: String,
+ index: Number
+ }],
status: {
type: String,
enum: ["PENDING", "APPROVED", "REJECTED"],
@@ -453,6 +464,14 @@ const leavesSchema = new Schema({
type: String,
required: true
},
+ attachments: [{
+ filename: String,
+ mimeType: String,
+ base64Data: String,
+ size: Number,
+ originalHTML: String,
+ index: Number
+ }],
leaveType: {
type: String,
enum: ["SICK", "PTO"],
diff --git a/src/repository/vaClient.repository.js b/src/repository/vaClient.repository.js
index d087d4c..15aea74 100644
--- a/src/repository/vaClient.repository.js
+++ b/src/repository/vaClient.repository.js
@@ -31,10 +31,27 @@ class VaClientRepository {
async update(id, data) {
try {
+ // Convert nested userDetails object to dot notation
+ const updateObj = {};
+
+ // Handle userDetails fields with dot notation
+ if (data.userDetails) {
+ Object.keys(data.userDetails).forEach(key => {
+ updateObj[`userDetails.${key}`] = data.userDetails[key];
+ });
+ }
+
+ // Handle other top-level fields if any
+ Object.keys(data).forEach(key => {
+ if (key !== 'userDetails') {
+ updateObj[key] = data[key];
+ }
+ });
+
const response = await ClientModel.findByIdAndUpdate(
id,
- { $set: { ...data } },
- { new: true }
+ { $set: updateObj },
+ { new: true, runValidators: true }
);
return response;
} catch (error) {
@@ -42,9 +59,9 @@ class VaClientRepository {
}
}
- async delete(id) {
+ async delete(id, options = {}) {
try {
- const response = await ClientModel.findByIdAndDelete(id);
+ const response = await ClientModel.findByIdAndDelete(id, options);
return response;
} catch (error) {
throw error;
diff --git a/src/routes/v1/client.routes.js b/src/routes/v1/client.routes.js
index 066ccc3..cad13c1 100644
--- a/src/routes/v1/client.routes.js
+++ b/src/routes/v1/client.routes.js
@@ -1,7 +1,7 @@
import express from "express";
const clientRoutes = express.Router();
-import { va_register_client } from "../../controllers/client/vaClient.controller.js";
+import { va_register_client, delete_client, update_client } from "../../controllers/client/vaClient.controller.js";
// import { va_clients_callback } from "../../controllers/va/vaClients/vaClients.controller.js";
import { va_clients_projects_callback } from "../../controllers/va/vaClients/vaClientProject.controller.js";
import { va_login_client } from "../../controllers/client/vaClientLogin.controller.js";
@@ -21,6 +21,8 @@ clientRoutes.get("/refreshToken", client_refresh_token_callback); // Client Refr
clientRoutes.post("/forgot-password", client_request_password_reset);
clientRoutes.post("/verify-client-email", client_verify_email);
clientRoutes.post("/logout", clientAuthValidator, client_logout_callback);
+clientRoutes.patch("/:clientId", update_client);
+clientRoutes.delete("/:clientId", delete_client);
// clientRoutes.get("/:clientID?", va_clients_callback); // VA Clients Callback for getting all VA's or specific client by ID (NC)
clientRoutes.get("/:clientId/projects/:projectId", va_clients_projects_callback); // VA Clients Projects Callback (NC)
diff --git a/src/routes/v1/va.routes.js b/src/routes/v1/va.routes.js
index fa8549d..98cc2b9 100644
--- a/src/routes/v1/va.routes.js
+++ b/src/routes/v1/va.routes.js
@@ -46,7 +46,7 @@ vaRoutes.get("/:userName", va_username_callback); // VA Username Callback
vaRoutes.delete("/:id", va_remove_function); // 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
+vaRoutes.get("/timesheet/summary/:projectId/:date/:vaId?", authValidator, va_timesheet_summary_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);
@@ -67,7 +67,7 @@ vaRoutes.get("/search", va_filter_search);
// GET Windows Tab Logged Data for VA IN Between Login and LOGOUT
-vaRoutes.get("/report/project/:projectId/:from?/:to?", authValidator, va_projects_report_function);
+vaRoutes.get("/report/project/:projectId/:from?/:to?/:vaId?", authValidator, va_projects_report_function);
// Getting Project List assigned to VA should be ACTIVE and add total logged hours added by VA
vaRoutes.get("/list/projects/:projectId?/:from?/:to?", authValidator, va_projects_log_list_function);
diff --git a/src/services/vaClient.service.js b/src/services/vaClient.service.js
index b6c971b..c2a518e 100644
--- a/src/services/vaClient.service.js
+++ b/src/services/vaClient.service.js
@@ -1,5 +1,6 @@
import logger from "../config/logger.js";
import SalesforceService from "./salesforce.service.js";
+import { VAModel, ProjectModel, LoggedModel, WorkTrackModel } from "../models/vaModel.js";
class VaClientService {
constructor(vaClientRepository) {
@@ -53,20 +54,143 @@ class VaClientService {
}
async updateClient(id, data) {
- try {
- const response = await this.clientRepository.update(id, data);
- if (!response) throw new Error("Client not found");
- return response;
+ try {
+ // Check if client exists
+ const existingClient = await this.clientRepository.findById(id);
+ if (!existingClient) {
+ throw new Error("Client not found");
+ }
+
+ const { userDetails } = data;
+
+ if (!userDetails) {
+ throw new Error("No user details provided for update");
+ }
+
+ // Prepare update data
+ const updateData = {
+ userDetails: {}
+ };
+
+ // Define allowed fields for update (excluding password)
+ const allowedFields = [
+ "name", "address", "city", "state", "zip",
+ "billingName", "billingEmail", "billingAddress", "billingCity",
+ "billingState", "billingZip", "position", "signatoryName",
+ "signatoryEmail", "paymentMethod", "bankName", "accountNumber",
+ "routingNumber", "accountType", "accountHolderName", "cardNumber",
+ "expiryDate", "securityCode", "cardZip"
+ ];
+
+ let hasUpdates = false;
+ const updatedFields = [];
+
+ // Process allowed fields
+ allowedFields.forEach(field => {
+ if (userDetails[field] !== undefined && userDetails[field] !== null && userDetails[field] !== '') {
+ updateData.userDetails[field] = userDetails[field];
+ hasUpdates = true;
+ updatedFields.push(field);
+ }
+ });
+
+ // Handle email update separately (needs uniqueness check)
+ if (userDetails.email && userDetails.email !== existingClient.userDetails.email) {
+ // Check if new email already exists
+ const emailExists = await this.clientRepository.findByEmail(userDetails.email);
+ if (emailExists && emailExists._id.toString() !== id) {
+ throw new Error("Email already exists");
+ }
+ updateData.userDetails.email = userDetails.email.toLowerCase();
+ hasUpdates = true;
+ updatedFields.push('email');
+ }
+
+ // Check if any fields were actually updated
+ if (!hasUpdates) {
+ throw new Error("No valid fields to update");
+ }
+
+ // Update the client
+ const updatedClient = await this.clientRepository.update(id, updateData);
+
+ // Notify Salesforce about the update
+ try {
+ await this.salesforceService.updateClientInSalesforce(id, data);
+ logger.info(`Salesforce notified about client update: ${id}`);
+ } catch (sfError) {
+ logger.warn(`Salesforce notification failed for client update: ${sfError.message}`);
+ // Don't throw error - client update should proceed even if Salesforce fails
+ }
+
+ logger.info(`Client updated successfully: ${id}, Fields: ${updatedFields.join(', ')}`);
+
+ return {
+ ...updatedClient.toObject(),
+ updateSummary: {
+ updatedFields
+ }
+ };
+
} catch (error) {
logger.error("Client Service - updateClient error: " + error.message);
throw error;
}
}
- async deleteClient(id) {
+ async deleteClient(clientId) {
try {
- const response = await this.clientRepository.delete(id);
- return response;
+ // Check if client exists
+ const existingClient = await this.clientRepository.findById(clientId);
+ if (!existingClient) {
+ throw new Error("Client not found");
+ }
+
+ const clientName = existingClient.userDetails?.name;
+ const vaIds = existingClient.vaIds || [];
+ const projectIds = existingClient.projectIds || [];
+
+ logger.info(`Deleting client: ${clientName} (${clientId})`);
+
+ const deletionSummary = {};
+
+ // 1. Remove client from all VAs
+ if (vaIds.length > 0) {
+ const vaResult = await VAModel.updateMany(
+ { _id: { $in: vaIds } },
+ { $pull: { clientIds: clientId } }
+ );
+ deletionSummary.vasUpdated = vaResult.modifiedCount;
+ }
+
+ // 2. Delete all projects and associated data
+ if (projectIds.length > 0) {
+ // Remove project references from VAs
+ await VAModel.updateMany(
+ { projectIds: { $in: projectIds } },
+ { $pull: { projectIds: { $in: projectIds } } }
+ );
+
+ // Delete work tracks and logs
+ await WorkTrackModel.deleteMany({ projectId: { $in: projectIds } });
+ await LoggedModel.deleteMany({ projectId: { $in: projectIds } });
+
+ // Delete projects
+ const projectResult = await ProjectModel.deleteMany({ _id: { $in: projectIds } });
+ deletionSummary.projectsDeleted = projectResult.deletedCount;
+ }
+
+ // 3. Delete the client
+ const deletedClient = await this.clientRepository.delete(clientId);
+
+
+ logger.info(`✅ Successfully deleted client: ${clientName} (${clientId})`);
+
+ return {
+ ...deletedClient.toObject(),
+ deletionSummary
+ };
+
} catch (error) {
logger.error("Client Service - deleteClient error: " + error.message);
throw error;
diff --git a/views/va_register.ejs b/views/va_register.ejs
index 3bf2223..34841f9 100644
--- a/views/va_register.ejs
+++ b/views/va_register.ejs
@@ -266,17 +266,19 @@
+
@@ -327,7 +329,7 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/views/client/dashboard-profile.ejs b/views/client/dashboard-profile.ejs
new file mode 100644
index 0000000..cc2ea96
--- /dev/null
+++ b/views/client/dashboard-profile.ejs
@@ -0,0 +1,685 @@
+
+
+