From 4d757daf97a1f2988d7747442d041fed6d8fb6d4 Mon Sep 17 00:00:00 2001
From: Aakash
Date: Thu, 27 Nov 2025 19:58:33 +0530
Subject: [PATCH] Updated client dashboard
---
public/css/dashboard.css | 10 +
public/js/va-register.js | 5 +-
.../dashboard/clientDashboard.controller.js | 243 +++----
src/controllers/client/vaClient.controller.js | 130 +++-
.../project/projectAddSf.controller.js | 7 +-
.../project/projectUpdateSf.controller.js | 28 +-
.../va/vaLeaves/vaLeavesApply.controller.js | 2 +-
.../vaProfile/vaProfileUpdate.controller.js | 7 +-
.../va/vaProjects/vaProjectLog.controller.js | 179 ++++-
src/models/vaModel.js | 22 +-
src/repository/vaClient.repository.js | 46 +-
src/routes/v1/client.routes.js | 8 +-
src/routes/v1/va.routes.js | 2 +-
src/server.js | 12 +-
src/services/vaClient.service.js | 65 ++
views/client/dashboard-dashboard.ejs | 212 ++++--
views/client/dashboard-profile.ejs | 685 ++++++++++++++++++
views/client/dashboard-projects.ejs | 34 +-
views/client/dashboard-virtual-assistants.ejs | 203 +++++-
views/va_client_register.ejs | 8 +-
views/va_register.ejs | 15 +-
21 files changed, 1633 insertions(+), 290 deletions(-)
create mode 100644 views/client/dashboard-profile.ejs
diff --git a/public/css/dashboard.css b/public/css/dashboard.css
index d73ce47..c20f34e 100644
--- a/public/css/dashboard.css
+++ b/public/css/dashboard.css
@@ -826,6 +826,7 @@ tr[data-vaid]:hover {
cursor: pointer;
z-index: 10;
position: relative;
+ text-decoration: none;
}
/* Project Details Styles in Popup */
@@ -1299,4 +1300,13 @@ tr[data-vaid]:hover {
[data-theme="dark"] .status-badge.inactive {
background: #4b5563;
color: #d1d5db;
+}
+
+.modal-actions {
+ text-align: center;
+ margin-top: 20px;
+}
+
+.modal-actions a {
+ text-decoration: none;
}
\ No newline at end of file
diff --git a/public/js/va-register.js b/public/js/va-register.js
index ef2e9ac..7ce3f38 100644
--- a/public/js/va-register.js
+++ b/public/js/va-register.js
@@ -691,10 +691,11 @@
formData.userDetails.city = document.getElementById('city').value;
formData.userDetails.state = document.getElementById('state').value;
formData.userDetails.country = document.getElementById('country').value;
+ formData.status = document.getElementById('status').value;
break;
case 3:
formData.userDetails.phone = document.getElementById('phone').value;
- formData.userDetails.skype = document.getElementById('skype').value;
+ // formData.userDetails.skype = document.getElementById('skype').value;
break;
case 4:
const selectedPayment = document.querySelector('.payment-option.selected');
@@ -1150,7 +1151,7 @@ function validateSkypeId(input) {
if (value && !/^[a-zA-Z][a-zA-Z0-9\.,\-_]+$/.test(value)) {
input.style.border = '1px solid #ef4444';
- showError('Skype ID must start with a letter and can only contain letters, numbers, dots, hyphens, and underscores', 'skype', 3);
+ showError('Invalid Skype ID', 'skype', 3);
return false;
} else {
input.style.border = '';
diff --git a/src/controllers/client/dashboard/clientDashboard.controller.js b/src/controllers/client/dashboard/clientDashboard.controller.js
index cfd80bf..b13f92d 100644
--- a/src/controllers/client/dashboard/clientDashboard.controller.js
+++ b/src/controllers/client/dashboard/clientDashboard.controller.js
@@ -68,12 +68,7 @@ async function client_dashboard(req, res) {
$cond: [{ $eq: ["$loggedType", "LOGIN"] }, 1, 0]
}
},
- lastActivity: { $first: "$time" },
- projectsWorked: {
- $addToSet: "$projectId"
- },
- // Get all login logs for this VA
- allLogs: { $push: "$$ROOT" }
+ lastActivity: { $first: "$time" }
}
},
{
@@ -85,22 +80,12 @@ async function client_dashboard(req, res) {
const activeVaIds = agg.map((a) => a._id).filter(Boolean);
- // Get active VA details
- const activeVAs = await VAModel.find({
- _id: { $in: activeVaIds },
- clientIds: clientID
- })
- .select('userDetails.firstName userDetails.lastName userDetails.email userDetails.phone status createdAt projectIds')
- .lean()
- .exec();
-
// 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,
- projectsWorked: a.projectsWorked || []
+ lastActivity: a.lastActivity
};
return acc;
}, {});
@@ -126,14 +111,14 @@ async function client_dashboard(req, res) {
logsByVa[vaId].push(log);
});
- // Calculate total worked time for each VA using the same logic as va_log_work_report_callback
+ // Calculate total worked time for each VA
for (const [vaId, logs] of Object.entries(logsByVa)) {
let totalWorkSeconds = 0;
// Group logs by date (YYYY-MM-DD format)
const logsByDate = {};
logs.forEach(log => {
- const date = new Date(log.time).toISOString().split('T')[0]; // YYYY-MM-DD
+ const date = new Date(log.time).toISOString().split('T')[0];
if (!logsByDate[date]) {
logsByDate[date] = [];
}
@@ -147,7 +132,6 @@ async function client_dashboard(req, res) {
let dayWorkSeconds = 0;
let lastLogin = null;
- let lastProjectId = null;
// If day has adjustment logs, only process adjustment logs
// If no adjustment logs, process all logs for that day
@@ -164,14 +148,10 @@ async function client_dashboard(req, res) {
if (log.loggedType === "LOGIN") {
lastLogin = time;
- lastProjectId = log.projectId?.toString() || null;
} else if (log.loggedType === "LOGOUT" && lastLogin) {
const diffSec = (time - lastLogin) / 1000;
dayWorkSeconds += diffSec;
-
- // reset after each pair
lastLogin = null;
- lastProjectId = null;
}
}
@@ -184,25 +164,93 @@ async function client_dashboard(req, res) {
};
}
+ // NEW: Calculate project-wise work time for each VA
+ const projectWorkTimeByVa = {};
+
+ for (const [vaId, logs] of Object.entries(logsByVa)) {
+ const projectWorkTime = {};
+
+ // Group logs by project
+ const logsByProject = {};
+ logs.forEach(log => {
+ const projectId = log.projectId?.toString();
+ if (projectId) {
+ if (!logsByProject[projectId]) {
+ logsByProject[projectId] = [];
+ }
+ logsByProject[projectId].push(log);
+ }
+ });
+
+ // Calculate work time per project
+ for (const [projectId, projectLogs] of Object.entries(logsByProject)) {
+ let projectWorkSeconds = 0;
+
+ // Group logs by date
+ const logsByDate = {};
+ projectLogs.forEach(log => {
+ const date = new Date(log.time).toISOString().split('T')[0];
+ if (!logsByDate[date]) {
+ logsByDate[date] = [];
+ }
+ logsByDate[date].push(log);
+ });
+
+ // Process each day separately
+ for (const [date, dayLogs] of Object.entries(logsByDate)) {
+ const hasAdjustmentLogs = dayLogs.some(log => log.isAdjustment === true);
+ let dayWorkSeconds = 0;
+ let lastLogin = null;
+
+ const logsToProcess = hasAdjustmentLogs
+ ? dayLogs.filter(log => log.isAdjustment === true)
+ : dayLogs;
+
+ logsToProcess.sort((a, b) => new Date(a.time) - new Date(b.time));
+
+ for (const log of logsToProcess) {
+ const time = new Date(log.time);
+
+ if (log.loggedType === "LOGIN") {
+ lastLogin = time;
+ } else if (log.loggedType === "LOGOUT" && lastLogin) {
+ const diffSec = (time - lastLogin) / 1000;
+ dayWorkSeconds += diffSec;
+ lastLogin = null;
+ }
+ }
+
+ projectWorkSeconds += dayWorkSeconds;
+ }
+
+ projectWorkTime[projectId] = {
+ totalSeconds: projectWorkSeconds,
+ formatted: formatTime(projectWorkSeconds),
+ projectName: projectMap[projectId]?.name || 'Unknown Project'
+ };
+ }
+
+ projectWorkTimeByVa[vaId] = projectWorkTime;
+ }
+
// Calculate session duration for currently active VAs
const currentTime = new Date();
- // Format all assigned VAs
+ // Format all assigned VAs - Only include data needed for frontend
const allVAsFormatted = allAssignedVAs.map((v) => {
const activity = activityByVa[String(v._id)] || {};
const workedTime = totalWorkedTimeByVa[String(v._id)] || { totalSeconds: 0, formatted: '00:00:00' };
const lastLog = activity.lastLog;
+ const projectWorkTime = projectWorkTimeByVa[String(v._id)] || {};
// Calculate current session duration if still logged in
let currentSessionDuration = null;
let isCurrentlyActive = false;
- let currentSessionFormatted = null;
let currentProjectDetails = null;
if (lastLog && lastLog.loggedType === "LOGIN") {
const loginTime = new Date(lastLog.time);
currentSessionDuration = Math.floor((currentTime - loginTime) / 1000);
- currentSessionFormatted = formatTime(currentSessionDuration);
isCurrentlyActive = true;
// Consider VA inactive if last login was more than 24 hours ago
@@ -223,6 +271,29 @@ async function client_dashboard(req, res) {
// Combine first and last name
const fullName = `${v.userDetails?.firstName || ''} ${v.userDetails?.lastName || ''}`.trim() || 'Unknown';
+ // Filter: Only include projects that belong to this client
+ const clientProjectsAssigned = (v.projectIds || []).filter(projectId =>
+ clientProjectIds.some(clientProjectId =>
+ clientProjectId.toString() === projectId.toString()
+ )
+ );
+
+ // Format project work time for popup
+ const projectsWithWorkTime = clientProjectsAssigned.map(projectId => {
+ const projectIdStr = projectId.toString();
+ const workData = projectWorkTime[projectIdStr] || { totalSeconds: 0, formatted: '00:00:00' };
+ const project = projectMap[projectIdStr];
+
+ return {
+ id: projectId,
+ name: project?.name || 'Unknown Project',
+ totalWorkedTime: workData.formatted,
+ totalWorkedSeconds: workData.totalSeconds,
+ isCurrentlyWorking: currentProjectDetails?.id?.toString() === projectIdStr,
+ hourlyRate: project?.hourlyRate || 0
+ };
+ });
+
return {
id: v._id,
name: fullName,
@@ -231,99 +302,18 @@ async function client_dashboard(req, res) {
status: v.status || 'active',
isCurrentlyActive: isCurrentlyActive,
lastLoginTime: lastLog?.time || null,
- lastActivity: activity.lastActivity,
currentSessionDuration: currentSessionDuration,
- currentSessionFormatted: currentSessionFormatted,
totalWorkedTime: workedTime.formatted,
totalWorkedSeconds: workedTime.totalSeconds,
- totalLogins: activity.totalLogins || 0,
lastProject: currentProjectDetails,
lastProjectId: lastLog?.projectId || null,
- projectsWorked: activity.projectsWorked || [],
- projectsAssigned: v.projectIds?.length || 0,
+ projectsAssigned: clientProjectsAssigned.length,
+ // Detailed project information for popup
+ projects: projectsWithWorkTime,
vaSince: v.createdAt
};
});
- // Format active VAs (currently working)
- const activeVAsFormatted = activeVAs.map((v) => {
- const activity = activityByVa[String(v._id)] || {};
- const workedTime = totalWorkedTimeByVa[String(v._id)] || { totalSeconds: 0, formatted: '00:00:00' };
- const lastLog = activity.lastLog;
-
- let currentSessionDuration = null;
- let currentSessionFormatted = null;
- let currentProjectDetails = null;
-
- if (lastLog && lastLog.loggedType === "LOGIN") {
- const loginTime = new Date(lastLog.time);
- currentSessionDuration = Math.floor((currentTime - loginTime) / 1000);
- currentSessionFormatted = formatTime(currentSessionDuration);
-
- // Get current project details
- if (lastLog.projectId && projectMap[lastLog.projectId.toString()]) {
- currentProjectDetails = {
- id: lastLog.projectId,
- name: projectMap[lastLog.projectId.toString()].name
- };
- }
- }
-
- // Combine first and last name
- const fullName = `${v.userDetails?.firstName || ''} ${v.userDetails?.lastName || ''}`.trim() || 'Unknown';
-
- return {
- id: v._id,
- name: fullName,
- email: v.userDetails?.email,
- phone: v.userDetails?.phone,
- status: v.status || 'active',
- lastLoginTime: lastLog?.time || null,
- currentSessionDuration: currentSessionDuration,
- currentSessionFormatted: currentSessionFormatted,
- totalWorkedTime: workedTime.formatted,
- totalWorkedSeconds: workedTime.totalSeconds,
- totalLogins: activity.totalLogins || 0,
- currentProject: currentProjectDetails,
- currentProjectId: lastLog?.projectId || null,
- projectsAssigned: v.projectIds?.length || 0,
- vaSince: v.createdAt
- };
- });
-
- // Format projects with additional data
- const projectsFormatted = clientProjects.map((p) => {
- // Find VAs currently working on this project
- const activeVAsOnProject = allVAsFormatted.filter(va =>
- va.isCurrentlyActive && va.lastProjectId &&
- va.lastProjectId.toString() === p._id.toString()
- );
-
- // Find all VAs assigned to this project
- const assignedVAs = allAssignedVAs.filter(va =>
- va.projectIds && va.projectIds.some(projectId =>
- projectId.toString() === p._id.toString()
- )
- );
-
- return {
- id: p._id,
- name: p.name,
- hourlyRate: p.hourlyRate,
- totalLoggedHours: p.totalLoggedHours || 0,
- dedicatedHours: p.dedicatedHours || 0,
- vaAssigned: assignedVAs.length,
- vaCurrentlyWorking: activeVAsOnProject.length,
- activeVAs: activeVAsOnProject.map(va => ({
- id: va.id,
- name: va.name,
- currentSession: va.currentSessionFormatted,
- lastLoginTime: va.lastLoginTime
- })),
- createdAt: p.createdAt
- };
- });
-
// Calculate dashboard statistics
const totalHoursLogged = clientProjects.reduce((sum, p) => sum + (p.totalLoggedHours || 0), 0);
const totalDedicatedHours = clientProjects.reduce((sum, p) => sum + (p.dedicatedHours || 0), 0);
@@ -334,45 +324,22 @@ async function client_dashboard(req, res) {
// Calculate total worked time across all VAs
const totalWorkedTimeAllVAs = allVAsFormatted.reduce((total, va) => total + (va.totalWorkedSeconds || 0), 0);
- // Calculate projects with active workers
- const projectsWithActiveWorkers = projectsFormatted.filter(p => p.vaCurrentlyWorking > 0).length;
-
return res.status(200).json({
success: true,
data: {
- // All assigned VAs with their status
- allVAs: {
- active: currentlyActiveVAs,
- inactive: inactiveVAs,
- total: allVAsFormatted
- },
- // Currently working VAs (active session)
- activeVAs: activeVAsFormatted,
- // All client projects
- projects: projectsFormatted
+ // Only send data that frontend actually uses
+ allVAs: allVAsFormatted
},
summary: {
totalVAs: allAssignedVAs.length,
activeVAs: currentlyActiveVAs.length,
inactiveVAs: inactiveVAs.length,
totalProjects: clientProjects.length,
- projectsWithActiveWorkers: projectsWithActiveWorkers,
totalHoursLogged: totalHoursLogged,
totalDedicatedHours: totalDedicatedHours,
totalWorkedTime: formatTime(totalWorkedTimeAllVAs),
utilizationRate: totalDedicatedHours > 0 ?
- ((totalHoursLogged / totalDedicatedHours) * 100).toFixed(2) + '%' : '0%',
- lastUpdated: currentTime.toISOString()
- },
- counts: {
- vas: {
- total: allAssignedVAs.length,
- active: currentlyActiveVAs.length,
- inactive: inactiveVAs.length
- },
- projects: clientProjects.length,
- activeSessions: activeVAsFormatted.length,
- projectsWithActivity: projectsWithActiveWorkers
+ ((totalHoursLogged / totalDedicatedHours) * 100).toFixed(2) + '%' : '0%'
}
});
@@ -386,7 +353,7 @@ async function client_dashboard(req, res) {
}
}
-// Helper function to format time (same as in va_log_work_report_callback)
+// Helper function to format time
function formatTime(seconds) {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
diff --git a/src/controllers/client/vaClient.controller.js b/src/controllers/client/vaClient.controller.js
index 8f23662..1547e84 100644
--- a/src/controllers/client/vaClient.controller.js
+++ b/src/controllers/client/vaClient.controller.js
@@ -1,6 +1,7 @@
import VaClientRepository from "../../repository/vaClient.repository.js";
import VaClientService from "../../services/vaClient.service.js";
import logger from "../../config/logger.js";
+import jwt from "jsonwebtoken";
const vaClientService = new VaClientService(new VaClientRepository());
@@ -51,9 +52,9 @@ async function va_register_client(req, res) {
}
// Additional controller methods can be added here
-async function get_client_by_email(req, res) {
+async function get_client_profile(req, res) {
try {
- const { email } = req.params;
+ const email = req.body.clientEmail;
const client = await vaClientService.getClientByEmail(email);
if (!client) {
@@ -179,9 +180,130 @@ async function update_client(req, res) {
}
}
+async function get_client_dashboard_profile(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 clientEmail = decoded.email;
+
+ // Use the new method that returns only necessary fields
+ const client = await vaClientService.getClientProfileByEmail(clientEmail);
+
+ if (!client) {
+ return res.status(404).json({
+ success: false,
+ message: "Client not found"
+ });
+ }
+
+ // Format the response to only include userDetails
+ const profileData = {
+ name: client.userDetails?.name || '',
+ email: client.userDetails?.email || '',
+ address: client.userDetails?.address || '',
+ city: client.userDetails?.city || '',
+ state: client.userDetails?.state || '',
+ zip: client.userDetails?.zip || '',
+ position: client.userDetails?.position || ''
+ };
+
+ return res.status(200).json({
+ success: true,
+ data: profileData
+ });
+ } catch (error) {
+ logger.error(`ClientController - Get dashboard profile error: ${error.message}`);
+ return res.status(500).json({
+ success: false,
+ message: "Internal Server Error"
+ });
+ }
+}
+
+async function update_client_password(req, res) {
+ logger.info("ClientController - Update Client Password Hit ...");
+ console.log("Update Client Password - Request Body:", req.body);
+ 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 clientEmail = decoded.email;
+ const payload = req.body || {};
+ const { userDetails } = payload;
+
+ // Get client by email to get the ID
+ const client = await vaClientService.getClientByEmail(clientEmail);
+
+ if (!client) {
+ return res.status(404).json({
+ success: false,
+ message: "Client not found"
+ });
+ }
+
+ const clientId = client._id;
+
+ // Call service to update password
+ const result = await vaClientService.updateClientPassword(clientId, userDetails);
+
+ logger.info(`ClientController - Client password updated successfully: ${clientEmail}`);
+
+ return res.status(200).json({
+ success: true,
+ message: "Password updated successfully",
+ data: result
+ });
+
+ } catch (error) {
+ logger.error(`ClientController - Update password error: ${error.message}`);
+
+ if (error.message === "Client not found") {
+ return res.status(404).json({
+ success: false,
+ message: error.message
+ });
+ }
+
+ if (error.message.includes("Current password is incorrect") ||
+ error.message.includes("New passwords do not match") ||
+ error.message.includes("All password fields are required")) {
+ return res.status(400).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_profile,
delete_client,
- update_client
+ update_client,
+ get_client_dashboard_profile,
+ update_client_password
};
\ No newline at end of file
diff --git a/src/controllers/project/projectAddSf.controller.js b/src/controllers/project/projectAddSf.controller.js
index cabe634..ccad5d0 100644
--- a/src/controllers/project/projectAddSf.controller.js
+++ b/src/controllers/project/projectAddSf.controller.js
@@ -5,7 +5,7 @@ import { updateEntityRelationships } from "../../helper/updateEntityRelationship
async function project_add_sf(req, res) {
try {
const payload = req.body|| {};
- const { name, hourlyRate, clientId, vaAssigned, dedicatedHours } = payload;
+ const { name, hourlyRate, clientId, vaAssigned, dedicatedHours, vaWorkingScheduleStart, vaWorkingScheduleEnd, isFlexibleAllowedPt, vaWorkingDelayAllowedStart, vaWorkingDelayAllowedEnd } = payload;
// Validate required fields
if (!name) {
@@ -65,6 +65,11 @@ async function project_add_sf(req, res) {
clientId: clientObjectId,
vaAssigned: vaAssignedArray,
dedicatedHours: dedicatedHours ? parseFloat(dedicatedHours) : 0,
+ vaWorkingScheduleStart: vaWorkingScheduleStart || null,
+ vaWorkingScheduleEnd: vaWorkingScheduleEnd || null,
+ isFlexibleAllowedPt: isFlexibleAllowedPt || true,
+ vaWorkingDelayAllowedStart: vaWorkingDelayAllowedStart || null,
+ vaWorkingDelayAllowedEnd: vaWorkingDelayAllowedEnd || null
};
const created = await ProjectModel.create(toCreate);
diff --git a/src/controllers/project/projectUpdateSf.controller.js b/src/controllers/project/projectUpdateSf.controller.js
index 5035f29..62e3047 100644
--- a/src/controllers/project/projectUpdateSf.controller.js
+++ b/src/controllers/project/projectUpdateSf.controller.js
@@ -16,7 +16,12 @@ async function update_project_sf(req, res) {
vaAssigned,
dedicatedHours,
status,
- totalLoggedHours
+ totalLoggedHours ,
+ vaWorkingScheduleStart,
+ vaWorkingScheduleEnd,
+ isFlexibleAllowedPt,
+ vaWorkingDelayAllowedStart,
+ vaWorkingDelayAllowedEnd
} = payload;
// Validate projectId
@@ -88,6 +93,27 @@ async function update_project_sf(req, res) {
updateData.status = status;
}
+ // Update VA working schedule if provided
+ if (vaWorkingScheduleStart !== undefined) {
+ updateData.vaWorkingScheduleStart = vaWorkingScheduleStart || null;
+ }
+
+ if (vaWorkingScheduleEnd !== undefined) {
+ updateData.vaWorkingScheduleEnd = vaWorkingScheduleEnd || null;
+ }
+
+ if (isFlexibleAllowedPt !== undefined) {
+ updateData.isFlexibleAllowedPt = isFlexibleAllowedPt;
+ }
+
+ if (vaWorkingDelayAllowedStart !== undefined) {
+ updateData.vaWorkingDelayAllowedStart = vaWorkingDelayAllowedStart || null;
+ }
+
+ if (vaWorkingDelayAllowedEnd !== undefined) {
+ updateData.vaWorkingDelayAllowedEnd = vaWorkingDelayAllowedEnd || null;
+ }
+
// Handle VA assignments - this requires relationship updates
if (vaAssigned !== undefined) {
let newVaAssignedArray = [];
diff --git a/src/controllers/va/vaLeaves/vaLeavesApply.controller.js b/src/controllers/va/vaLeaves/vaLeavesApply.controller.js
index 5076441..35ba56d 100644
--- a/src/controllers/va/vaLeaves/vaLeavesApply.controller.js
+++ b/src/controllers/va/vaLeaves/vaLeavesApply.controller.js
@@ -191,7 +191,7 @@ async function va_leave_apply_function(req, res) {
totalDays: totalDays,
leaveType,
comment: processedComment.processedHTML,
- attachments: processedComment.extractedImages,
+ // attachments: processedComment.extractedImages,
status: "PENDING"
},
// SyncSalesforce: result
diff --git a/src/controllers/va/vaProfile/vaProfileUpdate.controller.js b/src/controllers/va/vaProfile/vaProfileUpdate.controller.js
index 0e8082f..780a7d8 100644
--- a/src/controllers/va/vaProfile/vaProfileUpdate.controller.js
+++ b/src/controllers/va/vaProfile/vaProfileUpdate.controller.js
@@ -61,7 +61,7 @@ async function va_profile_update_callback(req, res) {
const payload = req.body || {};
- const { userDetails } = payload;
+ const { userDetails, status } = payload;
if (!userDetails) {
return res.status(400).json({ success: false, message: "Missing userDetails" });
@@ -136,6 +136,11 @@ async function va_profile_update_callback(req, res) {
});
}
+ if (status) {
+ vaUser.status = status;
+ updatedFields.push('status');
+ }
+
await vaUser.save();
const updatedUser = await VAModel.findById(vaUser._id)
diff --git a/src/controllers/va/vaProjects/vaProjectLog.controller.js b/src/controllers/va/vaProjects/vaProjectLog.controller.js
index 07ca354..a4ab951 100644
--- a/src/controllers/va/vaProjects/vaProjectLog.controller.js
+++ b/src/controllers/va/vaProjects/vaProjectLog.controller.js
@@ -1,4 +1,4 @@
-import { LoggedModel, WorkTrackModel, ProjectModel } from "../../../models/vaModel.js";
+import { LoggedModel, WorkTrackModel, ProjectModel, VAModel } from "../../../models/vaModel.js";
// Helper function to get start and end of day in UTC
function getDayRange(date = new Date()) {
@@ -47,12 +47,6 @@ function parseISOTimeString(timeString) {
}
}
-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);
@@ -68,6 +62,89 @@ function getCurrentUTCTime() {
return now;
}
+// function parseTimeToMinutes(timeString) {
+// if (!timeString) return null;
+
+// const [hours, minutes] = timeString.split(':').map(Number);
+// return hours * 60 + (minutes || 0);
+// }
+
+// function parseDelayToMinutes(delayString) {
+// if (!delayString) return 0;
+
+// if (delayString.includes('h')) {
+// const hours = parseInt(delayString.replace('h', '').trim());
+// return hours * 60;
+// } else if (delayString.includes('m')) {
+// return parseInt(delayString.replace('m', '').trim());
+// }
+
+// return parseInt(delayString) || 0; // Default to minutes if no unit specified
+// }
+
+// function checkScheduleCompliance(currentTime, project, vaStatus, type) {
+// const warnings = [];
+
+// // If project doesn't have schedule or VA is PARTTIME with flexible allowed, no schedule restrictions
+// if (!project.vaWorkingScheduleStart || !project.vaWorkingScheduleEnd) {
+// return { isCompliant: true, warnings };
+// }
+
+// const currentDate = new Date(currentTime);
+// const currentMinutes = currentDate.getUTCHours() * 60 + currentDate.getUTCMinutes();
+
+// const scheduleStartMinutes = parseTimeToMinutes(project.vaWorkingScheduleStart);
+// const scheduleEndMinutes = parseTimeToMinutes(project.vaWorkingScheduleEnd);
+// const allowedStartDelay = parseDelayToMinutes(project.vaWorkingDelayAllowedStart);
+// const allowedEndDelay = parseDelayToMinutes(project.vaWorkingDelayAllowedEnd);
+
+// // Calculate allowed time ranges
+// const allowedStartMinutes = scheduleStartMinutes - allowedStartDelay;
+// const allowedEndMinutes = scheduleEndMinutes + allowedEndDelay;
+
+// console.log(`Schedule Check - Current: ${currentMinutes}, Start: ${scheduleStartMinutes}, End: ${scheduleEndMinutes}, Allowed Start: ${allowedStartMinutes}, Allowed End: ${allowedEndMinutes}`);
+
+// // Check for PARTTIME with flexible schedule
+// if (vaStatus === "PARTTIME" && project.isFlexibleAllowedPt) {
+// return { isCompliant: true, warnings };
+// }
+
+// // For all other cases, check schedule compliance
+// if (type === "LOGIN") {
+// // Check if login is too early
+// if (currentMinutes < allowedStartMinutes) {
+// warnings.push("early_login");
+// }
+
+// // Check if login is after shift end
+// if (currentMinutes > scheduleEndMinutes) {
+// warnings.push("working_after_shift_end");
+// }
+
+// // Check if login is within allowed range
+// // const isWithinAllowedRange = currentMinutes >= allowedStartMinutes && currentMinutes <= allowedEndMinutes;
+
+// return {
+// isCompliant: true, // Allow login regardless of time
+// warnings
+// };
+
+// } else if (type === "LOGOUT") {
+// // Check if logout is too late
+// if (currentMinutes > allowedEndMinutes) {
+// warnings.push("late_logout");
+// }
+
+// // For logout, we're generally more permissive - allow logout even if late
+// return {
+// isCompliant: true, // Allow logout regardless of time
+// warnings
+// };
+// }
+
+// return { isCompliant: true, warnings };
+// }
+
// function to close open work tracks and create logout
async function closePreviousSession(vaId, lastLog, logTime = new Date()) {
try {
@@ -164,7 +241,7 @@ async function va_project_log_callback(req, res) {
try {
const payload = req.body || {};
const { vaId, vaEmail, projectId, type, time: receivedTimeString } = payload;
- console.log("Received payload:", payload);
+ // console.log("Received payload:", payload);
if (!vaId || !vaEmail || !projectId || !type) {
return res.status(400).send("Missing required properties");
@@ -189,8 +266,6 @@ async function va_project_log_callback(req, res) {
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);
@@ -198,6 +273,22 @@ async function va_project_log_callback(req, res) {
return res.status(400).json({ success: false, message: "Project not found" });
}
+ const vaUser = await VAModel.findById(vaId);
+ if (!vaUser) {
+ return res.status(400).json({ success: false, message: "VA not found" });
+ }
+
+ // Check schedule compliance
+ // const scheduleCheck = checkScheduleCompliance(logTime, project, vaUser.status, type);
+ // console.log(`Schedule compliance check:`, scheduleCheck);
+ // if (!scheduleCheck.isCompliant) {
+ // return res.status(400).json({
+ // success: false,
+ // message: `Cannot ${type.toLowerCase()} - outside of allowed schedule time`,
+ // warnings: scheduleCheck.warnings
+ // });
+ // }
+
// For LOGIN requests, check daily hours (using UTC day range)
if (type === "LOGIN") {
const { start, end } = getDayRange(); // UTC day range
@@ -218,7 +309,8 @@ async function va_project_log_callback(req, res) {
if (project.dedicatedHours > 0 && totalTodayHours >= project.dedicatedHours) {
return res.status(400).json({
success: false,
- message: `Cannot login - daily hours already completed (${formatHoursToReadable(totalTodayHours)} / ${formatHoursToReadable(project.dedicatedHours)})`
+ message: `Cannot login - daily hours already completed (${formatHoursToReadable(totalTodayHours)} / ${formatHoursToReadable(project.dedicatedHours)})`,
+ // warnings: scheduleCheck.warnings
});
}
@@ -245,17 +337,57 @@ async function va_project_log_callback(req, res) {
time: logTime
});
- return res.status(201).send({
- success: true,
- message: `Login Successful (previous session closed)`,
- logId: createdLog._id,
- previousSession: {
- closedWorkTracks: closeResult.closedWorkTracks,
- logoutLogId: closeResult.logoutLogId
- },
- timeUsed: receivedTimeString ? 'received' : 'current',
- actualTime: createdLog.time
- });
+ try {
+ const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
+ const sfData = {
+ "_id": createdLog._id,
+ "loggedType": "LOGIN",
+ "vaId": vaId,
+ "vaEmail": vaEmail,
+ "projectId": projectId,
+ "time": createdLog.time,
+ }
+
+ 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(sfData)
+ });
+
+ 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,
+ message: `${type} Successfully`,
+ logId: createdLog._id,
+ SyncSalesforce: result,
+ previousSession: {
+ closedWorkTracks: closeResult.closedWorkTracks,
+ logoutLogId: closeResult.logoutLogId
+ },
+ timeUsed: receivedTimeString ? 'received' : 'current',
+ actualTime: createdLog.time,
+ // warnings: scheduleCheck.warnings
+ });
+ } catch (error) {
+ console.error("Failed to sync with Salesforce:", error.message);
+ console.log(error)
+
+ // Create a custom error to distinguish Salesforce errors
+ const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
+ salesforceError.isSalesforceError = true;
+ throw salesforceError;
+ }
}
}
@@ -302,7 +434,8 @@ async function va_project_log_callback(req, res) {
logId: createdLog._id,
SyncSalesforce: result,
timeUsed: receivedTimeString ? 'received' : 'current',
- actualTime: createdLog.time
+ actualTime: createdLog.time,
+ // warnings: scheduleCheck.warnings
});
} catch (error) {
console.error("Failed to sync with Salesforce:", error.message);
diff --git a/src/models/vaModel.js b/src/models/vaModel.js
index ee1ef66..e6794e1 100644
--- a/src/models/vaModel.js
+++ b/src/models/vaModel.js
@@ -101,7 +101,27 @@ const projectSchema = new Schema({
type: String,
enum: ["ACTIVE", "INACTIVE"],
default: "ACTIVE"
- }
+ },
+ vaWorkingScheduleStart: {
+ type: String,
+ default: null
+ },
+ vaWorkingScheduleEnd: {
+ type: String,
+ default: null
+ },
+ isFlexibleAllowedPt: {
+ type: Boolean,
+ default: true
+ },
+ vaWorkingDelayAllowedStart: {
+ type: String,
+ default: '5m'
+ },
+ vaWorkingDelayAllowedEnd: {
+ type: String,
+ default: '5m'
+ },
});
/* ---------------------- Client Details Schema ---------------------- */
diff --git a/src/repository/vaClient.repository.js b/src/repository/vaClient.repository.js
index 15aea74..15a170f 100644
--- a/src/repository/vaClient.repository.js
+++ b/src/repository/vaClient.repository.js
@@ -13,7 +13,7 @@ class VaClientRepository {
async findByEmail(email) {
try {
- const response = await ClientModel.findOne({ "userDetails.email": email.toLowerCase() });
+ const response = await ClientModel.findOne({ "userDetails.email": email });
return response;
} catch (error) {
throw error;
@@ -59,6 +59,50 @@ class VaClientRepository {
}
}
+ async findProfileByEmail(email) {
+ try {
+ const response = await ClientModel.findOne(
+ { "userDetails.email": email },
+ {
+ // Only specify the fields you want to include
+ "userDetails.name": 1,
+ "userDetails.email": 1,
+ "userDetails.address": 1,
+ "userDetails.city": 1,
+ "userDetails.state": 1,
+ "userDetails.zip": 1,
+ "userDetails.position": 1
+ // Don't include any exclusion fields (0) when using inclusion (1)
+ }
+ ).lean(); // Use lean() for better performance since we don't need mongoose document
+
+ return response;
+ } catch (error) {
+ throw error;
+ }
+ }
+
+ // Alternative: If you need to be more explicit about excluding sensitive fields
+ async findProfileByEmailExplicit(email) {
+ try {
+ const response = await ClientModel.findOne(
+ { "userDetails.email": email }
+ ).select({
+ 'userDetails.name': 1,
+ 'userDetails.email': 1,
+ 'userDetails.address': 1,
+ 'userDetails.city': 1,
+ 'userDetails.state': 1,
+ 'userDetails.zip': 1,
+ 'userDetails.position': 1
+ }).lean();
+
+ return response;
+ } catch (error) {
+ throw error;
+ }
+ }
+
async delete(id, options = {}) {
try {
const response = await ClientModel.findByIdAndDelete(id, options);
diff --git a/src/routes/v1/client.routes.js b/src/routes/v1/client.routes.js
index cad13c1..9e2c6e6 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, delete_client, update_client } from "../../controllers/client/vaClient.controller.js";
+import { va_register_client, delete_client, update_client, get_client_dashboard_profile, update_client_password } 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,8 +21,6 @@ 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)
@@ -33,6 +31,10 @@ clientRoutes.get("/:clientId/projects/:projectId", va_clients_projects_callback)
clientRoutes.get("/dashboard", clientAuthValidator, client_dashboard); // VA Clients Projects Callback
clientRoutes.post("/dashboard/va-report", clientAuthValidator, client_dashboard_va_log); // VA details
clientRoutes.get("/dashboard-projects", clientAuthValidator, client_dashboard_projects); // VA Clients Projects Callback
+clientRoutes.get("/profile", clientAuthValidator, get_client_dashboard_profile);
+clientRoutes.patch("/profile", clientAuthValidator, update_client_password);
+clientRoutes.patch("/:clientId", update_client);
+clientRoutes.delete("/:clientId", delete_client);
diff --git a/src/routes/v1/va.routes.js b/src/routes/v1/va.routes.js
index 98cc2b9..c80794d 100644
--- a/src/routes/v1/va.routes.js
+++ b/src/routes/v1/va.routes.js
@@ -56,7 +56,7 @@ vaRoutes.get("/leaves/all", authValidator, va_leave_list_function); // Get all l
vaRoutes.get("/leaves/status/:status", authValidator, va_leave_list_function); // Leaves Filter by status
vaRoutes.get("/leaves/type/:leaveType", authValidator, va_leave_list_function); // Filter by leave type
vaRoutes.get("/leaves/status/:status/type/:leaveType", authValidator, va_leave_list_function); // leaves Filter
-vaRoutes.post("/leave/request", authValidator, va_leave_apply_function); // Timesheet Adjustment for VA
+vaRoutes.post("/leave/request", authValidator, va_leave_apply_function); // Leave for VA
vaRoutes.patch("/leave/:leaveId", va_leave_manage_function);
// timesheet adjustment routes
diff --git a/src/server.js b/src/server.js
index 5e99b37..0a295d7 100644
--- a/src/server.js
+++ b/src/server.js
@@ -154,7 +154,17 @@ app.get('/client/projects', async (req, res) => {
res.render('client/dashboard-projects');
} catch (error) {
console.error('Projects error:', error);
- res.status(500).render('client/dashboard-projects', { error: 'Failed to load virtual assistants' });
+ res.status(500).render('client/dashboard-projects', { error: 'Failed to load projects' });
+ }
+});
+
+app.get('/client/profile', async (req, res) => {
+ try {
+ // const vaData = await virtualAssistantService.getClientVAs(req.user.id);
+ res.render('client/dashboard-profile');
+ } catch (error) {
+ console.error('Projects error:', error);
+ res.status(500).render('client/dashboard-profile', { error: 'Failed to load profile' });
}
});
diff --git a/src/services/vaClient.service.js b/src/services/vaClient.service.js
index c2a518e..ea5e297 100644
--- a/src/services/vaClient.service.js
+++ b/src/services/vaClient.service.js
@@ -1,6 +1,7 @@
import logger from "../config/logger.js";
import SalesforceService from "./salesforce.service.js";
import { VAModel, ProjectModel, LoggedModel, WorkTrackModel } from "../models/vaModel.js";
+import bcrypt from "bcrypt";
class VaClientService {
constructor(vaClientRepository) {
@@ -197,6 +198,70 @@ class VaClientService {
}
}
+ async getClientProfileByEmail(email) {
+ try {
+ const response = await this.clientRepository.findProfileByEmail(email);
+ return response;
+ } catch (error) {
+ logger.error("Client Service - getClientProfileByEmail error: " + error.message);
+ throw error;
+ }
+ }
+
+ async updateClientPassword(id, passwordData) {
+ try {
+ // Check if client exists
+ const existingClient = await this.clientRepository.findById(id);
+ if (!existingClient) {
+ throw new Error("Client not found");
+ }
+
+ const { currentPassword, newPassword, confirmPassword } = passwordData;
+
+ // Validate required fields
+ if (!currentPassword || !newPassword || !confirmPassword) {
+ throw new Error("All password fields are required");
+ }
+
+ // Validate password match
+ if (newPassword !== confirmPassword) {
+ throw new Error("New passwords do not match");
+ }
+
+ // Verify current password
+ const isMatch = await bcrypt.compare(currentPassword, existingClient.userDetails.password);
+ if (!isMatch) {
+ throw new Error("Current password is incorrect");
+ }
+
+ // Hash new password
+ const saltRounds = 10;
+ const hashedPassword = await bcrypt.hash(newPassword, saltRounds);
+
+ // Prepare update data
+ const updateData = {
+ userDetails: {
+ password: hashedPassword
+ }
+ };
+
+ // Update the client password
+ const updatedClient = await this.clientRepository.update(id, updateData);
+
+ logger.info(`Client password updated successfully: ${id}`);
+
+ return {
+ success: true,
+ message: "Password updated successfully",
+ clientId: id
+ };
+
+ } catch (error) {
+ logger.error("Client Service - updateClientPassword error: " + error.message);
+ throw error;
+ }
+ }
+
// Helper method to sanitize data
sanitizeClientData(payload) {
const { userDetails } = payload;
diff --git a/views/client/dashboard-dashboard.ejs b/views/client/dashboard-dashboard.ejs
index 10eb82e..a845ea3 100644
--- a/views/client/dashboard-dashboard.ejs
+++ b/views/client/dashboard-dashboard.ejs
@@ -34,16 +34,6 @@
Projects
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+