Compare commits

..

No commits in common. "4ed62b25e823f6082edca6e0a46be399b26b9b28" and "3a6e86d6daa6298a5b54f1c37083289712d37c88" have entirely different histories.

43 changed files with 816 additions and 1468 deletions

View File

View File

@ -6,7 +6,6 @@
"scripts": { "scripts": {
"start": "nodemon --inspect src/server.js", "start": "nodemon --inspect src/server.js",
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"deploy": "curl -X POST http://35.95.209.161:9000/hooks/deploy-va-middleware",
"build": "npx webpack --mode production", "build": "npx webpack --mode production",
"dev": "npx webpack --mode development" "dev": "npx webpack --mode development"
}, },

View File

@ -577,7 +577,7 @@ body {
.data-table tbody tr { .data-table tbody tr {
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
transition: background-color 0.2s; transition: background-color 0.2s;
/* cursor: pointer; */ cursor: pointer;
} }
.data-table tbody tr:hover { .data-table tbody tr:hover {
@ -1689,23 +1689,3 @@ tr[data-vaid]:hover {
[data-theme="dark"] .loading { [data-theme="dark"] .loading {
opacity: 0.5; opacity: 0.5;
} }
.va-action {
text-decoration: none;
color: #fff;
background-color: #2563eb;
padding: 0.5rem 1rem;
text-align: center;
border-radius: 5px;
transform: scale(0.96);
transition: all 0.2s ease-in-out;
cursor: pointer;
}
.va-info {
cursor: pointer;
}
.va-action:hover {
transform: scale(1);
}

View File

@ -767,7 +767,7 @@ async function submitForm() {
nextStep(6); nextStep(6);
setTimeout(() => { setTimeout(() => {
window.location.href = '/'; window.location.href = '/client/login';
}, 1000); }, 1000);
} else { } else {
// Handle server errors // Handle server errors

View File

@ -58,7 +58,7 @@ async function client_dashboard_projects(req, res) {
// STEP 3: Get all VAs assigned to these projects // STEP 3: Get all VAs assigned to these projects
const allAssignedVAs = await VAModel.find({ const allAssignedVAs = await VAModel.find({
_id: { $in: allProjectVaIds.vaId } _id: { $in: allProjectVaIds }
}) })
.select('userDetails.firstName userDetails.lastName userDetails.email status') .select('userDetails.firstName userDetails.lastName userDetails.email status')
.lean() .lean()

View File

@ -2,22 +2,9 @@ import VaClientRepository from "../../repository/vaClient.repository.js";
import VaClientService from "../../services/vaClient.service.js"; import VaClientService from "../../services/vaClient.service.js";
import logger from "../../config/logger.js"; import logger from "../../config/logger.js";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import PasswordResetService from "../../services/passwordReset.service.js";
import PasswordResetRepository from "../../repository/passwordReset.repository.js";
const vaClientService = new VaClientService(new VaClientRepository()); const vaClientService = new VaClientService(new VaClientRepository());
const passwordResetService = new PasswordResetService(
new PasswordResetRepository(),
// new EmailService({
// host: process.env.SMTP_HOST,
// port: process.env.SMTP_PORT,
// user: process.env.SMTP_USER,
// pass: process.env.SMTP_PASS,
// from: process.env.EMAIL_FROM,
// })
);
async function va_register_client(req, res) { async function va_register_client(req, res) {
logger.info("ClientController - Register Client Hit ..."); logger.info("ClientController - Register Client Hit ...");
@ -35,9 +22,6 @@ async function va_register_client(req, res) {
const created = await vaClientService.registerClient(payload); const created = await vaClientService.registerClient(payload);
logger.info(`ClientController - Client registered successfully: ${userDetails.email}`); logger.info(`ClientController - Client registered successfully: ${userDetails.email}`);
await passwordResetService.requestPasswordReset(userDetails.email, 'client');
logger.info(`Client password reset requested for: ${userDetails.email}`);
return res.status(201).json({ return res.status(201).json({
success: true, success: true,
data: created, data: created,
@ -191,7 +175,7 @@ async function update_client(req, res) {
return res.status(500).json({ return res.status(500).json({
success: false, success: false,
message: error.message message: "Internal Server Error"
}); });
} }
} }
@ -300,8 +284,8 @@ async function update_client_password(req, res) {
} }
if (error.message.includes("Current password is incorrect") || if (error.message.includes("Current password is incorrect") ||
error.message.includes("New passwords do not match") || error.message.includes("New passwords do not match") ||
error.message.includes("All password fields are required")) { error.message.includes("All password fields are required")) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: error.message message: error.message

View File

@ -1,7 +1,6 @@
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { ClientModel, VAModel } from "../../models/vaModel.js"; import { ClientModel } from "../../models/vaModel.js";
import { va_timesheet_adjustment_function } from "../va/vaTimesheetAdjustment/vaTimesheetAdjustment.controller.js";
// Login VA // Login VA
async function va_login_client(req, res) { async function va_login_client(req, res) {
@ -14,17 +13,8 @@ async function va_login_client(req, res) {
return res.status(400).json({ success: false, message: "Please provide email and password" }); return res.status(400).json({ success: false, message: "Please provide email and password" });
} }
let isVa = false;
// Find user by email // Find user by email
let user = await ClientModel.findOne({ "userDetails.email": userDetails.email }); const user = await ClientModel.findOne({ "userDetails.email": userDetails.email });
const vaUser = await VAModel.findOne({ "userDetails.email": userDetails.email });
if (vaUser) {
isVa = true
user = vaUser
}
if (!user) { if (!user) {
return res.status(404).json({ success: false, message: "Invalid credentials" }); return res.status(404).json({ success: false, message: "Invalid credentials" });
} }
@ -59,7 +49,6 @@ async function va_login_client(req, res) {
id: user._id, id: user._id,
email: user.userDetails.email, email: user.userDetails.email,
name: user.userDetails.firstName, name: user.userDetails.firstName,
userType: isVa ? "VA" : "CLIENT",
}, },
process.env.JWT_SECRET_REFRESH_TOKEN || "DocTrackerSecretKeyRefreshToken", process.env.JWT_SECRET_REFRESH_TOKEN || "DocTrackerSecretKeyRefreshToken",
{ expiresIn: "30d" } { expiresIn: "30d" }
@ -80,7 +69,6 @@ async function va_login_client(req, res) {
name: user.userDetails.firstName, name: user.userDetails.firstName,
email: user.userDetails.email, email: user.userDetails.email,
}, },
userType: isVa ? "VA" : "CLIENT",
}); });
} catch (error) { } catch (error) {
console.error("va_client_login_register error:", error); console.error("va_client_login_register error:", error);

View File

@ -56,7 +56,7 @@ async function delete_project_sf(req, res) {
// 2. Remove project from all assigned VAs' projectIds arrays // 2. Remove project from all assigned VAs' projectIds arrays
if (vaAssigned.length > 0) { if (vaAssigned.length > 0) {
const vaResult = await VAModel.updateMany( const vaResult = await VAModel.updateMany(
{ _id: { $in: vaAssigned.vaId } }, { _id: { $in: vaAssigned } },
{ $pull: { projectIds: projectObjectId } } { $pull: { projectIds: projectObjectId } }
); );
deletionSummary.vasUpdated = vaResult.modifiedCount; deletionSummary.vasUpdated = vaResult.modifiedCount;

View File

@ -4,8 +4,8 @@ import { updateEntityRelationships } from "../../helper/updateEntityRelationship
// Register a new VA // Register a new VA
async function project_add_sf(req, res) { async function project_add_sf(req, res) {
try { try {
const payload = req.body || {}; const payload = req.body|| {};
const { name, hourlyRate, clientId, vaAssigned, dedicatedHours, vaWorkingScheduleStart, vaWorkingScheduleEnd, isFlexibleAllowed, vaWorkingDelayAllowedStart, vaWorkingDelayAllowedEnd } = payload; const { name, hourlyRate, clientId, vaAssigned, dedicatedHours, vaWorkingScheduleStart, vaWorkingScheduleEnd, isFlexibleAllowedPt, vaWorkingDelayAllowedStart, vaWorkingDelayAllowedEnd } = payload;
// Validate required fields // Validate required fields
if (!name) { if (!name) {
@ -34,55 +34,46 @@ async function project_add_sf(req, res) {
} }
// Validate and convert vaAssigned to array of ObjectIds // Validate and convert vaAssigned to array of ObjectIds
// let vaAssignedArray = []; let vaAssignedArray = [];
// if (vaAssigned) { if (vaAssigned) {
// if (Array.isArray(vaAssigned)) { if (Array.isArray(vaAssigned)) {
// try { try {
// vaAssignedArray = vaAssigned.map(vaId => { vaAssignedArray = vaAssigned.map(vaId => new Types.ObjectId(vaId));
// if(typeof vaId === 'object' && "isFlexibleAllowed" in vaId){ } catch (error) {
// // Handle Error form SF Side for now - That vaId is coming as object return res.status(400).json({
// return new Types.ObjectId(vaId.vaId) success: false,
// } else { message: "Invalid VA ID format in vaAssigned array"
// return new Types.ObjectId(vaId) });
// } }
// }); } else if (typeof vaAssigned === 'string') {
// } catch (error) { // Handle single VA ID as string
// return res.status(400).json({ try {
// success: false, vaAssignedArray = [new Types.ObjectId(vaAssigned)];
// message: "Invalid VA ID format in vaAssigned array" } catch (error) {
// }); return res.status(400).json({
// } success: false,
// } else if (typeof vaAssigned === 'string') { message: "Invalid VA ID format"
// // Handle single VA ID as string });
// try { }
// vaAssignedArray = [new Types.ObjectId(vaAssigned)]; }
// } catch (error) { }
// return res.status(400).json({
// success: false,
// message: "Invalid VA ID format"
// });
// }
// }
// }
// sanitize/construct allowed fields // sanitize/construct allowed fields
const toCreate = { const toCreate = {
name: name.trim(), name: name.trim(),
hourlyRate: hourlyRate || '0', hourlyRate: hourlyRate || '0',
clientId: clientObjectId, clientId: clientObjectId,
vaAssigned: vaAssigned, vaAssigned: vaAssignedArray,
dedicatedHours: dedicatedHours ? parseFloat(dedicatedHours) : 0, dedicatedHours: dedicatedHours ? parseFloat(dedicatedHours) : 0,
vaWorkingScheduleStart: vaWorkingScheduleStart || null, vaWorkingScheduleStart: vaWorkingScheduleStart || null,
vaWorkingScheduleEnd: vaWorkingScheduleEnd || null, vaWorkingScheduleEnd: vaWorkingScheduleEnd || null,
isFlexibleAllowed: isFlexibleAllowed || true, isFlexibleAllowedPt: isFlexibleAllowedPt || true,
vaWorkingDelayAllowedStart: vaWorkingDelayAllowedStart || null, vaWorkingDelayAllowedStart: vaWorkingDelayAllowedStart || null,
vaWorkingDelayAllowedEnd: vaWorkingDelayAllowedEnd || null vaWorkingDelayAllowedEnd: vaWorkingDelayAllowedEnd || null
}; };
const created = await ProjectModel.create(toCreate); const created = await ProjectModel.create(toCreate);
const vaAssignedArray = vaAssigned ? vaAssigned.map(vaDtl => vaDtl.vaId) : [];
const relationshipUpdate = await updateEntityRelationships({ const relationshipUpdate = await updateEntityRelationships({
project: { project: {
projectId: created._id, projectId: created._id,

View File

@ -16,10 +16,10 @@ async function update_project_sf(req, res) {
vaAssigned, vaAssigned,
dedicatedHours, dedicatedHours,
status, status,
totalLoggedHours, totalLoggedHours ,
vaWorkingScheduleStart, vaWorkingScheduleStart,
vaWorkingScheduleEnd, vaWorkingScheduleEnd,
isFlexibleAllowed, isFlexibleAllowedPt,
vaWorkingDelayAllowedStart, vaWorkingDelayAllowedStart,
vaWorkingDelayAllowedEnd vaWorkingDelayAllowedEnd
} = payload; } = payload;
@ -102,8 +102,8 @@ async function update_project_sf(req, res) {
updateData.vaWorkingScheduleEnd = vaWorkingScheduleEnd || null; updateData.vaWorkingScheduleEnd = vaWorkingScheduleEnd || null;
} }
if (isFlexibleAllowed !== undefined) { if (isFlexibleAllowedPt !== undefined) {
updateData.isFlexibleAllowed = isFlexibleAllowed; updateData.isFlexibleAllowedPt = isFlexibleAllowedPt;
} }
if (vaWorkingDelayAllowedStart !== undefined) { if (vaWorkingDelayAllowedStart !== undefined) {
@ -114,26 +114,15 @@ async function update_project_sf(req, res) {
updateData.vaWorkingDelayAllowedEnd = vaWorkingDelayAllowedEnd || null; updateData.vaWorkingDelayAllowedEnd = vaWorkingDelayAllowedEnd || null;
} }
if (vaAssigned !== undefined && vaAssigned !== null && Array.isArray(vaAssigned)) {
updateData.vaAssigned = vaAssigned;
}
// Handle VA assignments - this requires relationship updates // Handle VA assignments - this requires relationship updates
if (vaAssigned !== undefined) { if (vaAssigned !== undefined) {
let newVaAssignedArray = updateData.vaAssigned.map(vaDtl => vaDtl.vaId); let newVaAssignedArray = [];
// Parse and validate VA IDs // Parse and validate VA IDs
if (vaAssigned && vaAssigned.length > 0) { if (vaAssigned && vaAssigned.length > 0) {
if (Array.isArray(vaAssigned)) { if (Array.isArray(vaAssigned)) {
try { try {
newVaAssignedArray = vaAssigned.map(vaId => { newVaAssignedArray = vaAssigned.map(vaId => new Types.ObjectId(vaId));
if (typeof vaId === 'object' && "isFlexibleAllowed" in vaId) {
// Handle Error form SF Side for now - That vaId is coming as object
return new Types.ObjectId(vaId.vaId)
} else {
return new Types.ObjectId(vaId)
}
});
} catch (error) { } catch (error) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
@ -152,14 +141,14 @@ async function update_project_sf(req, res) {
} }
} }
// updateData.vaAssigned = newVaAssignedArray; updateData.vaAssigned = newVaAssignedArray;
// Calculate VAs to add and remove for relationship updates // Calculate VAs to add and remove for relationship updates
const currentVaIds = existingProject.vaAssigned.map(va => va.vaId.toString()); const currentVaIds = existingProject.vaAssigned.map(va => va.toString());
const newVaIds = newVaAssignedArray.map(va => va.toString()); const newVaIds = newVaAssignedArray.map(va => va.toString());
const vaToAdd = newVaAssignedArray.filter(va => !currentVaIds.includes(va.toString())); const vaToAdd = newVaAssignedArray.filter(va => !currentVaIds.includes(va.toString()));
const vaToRemove = existingProject.vaAssigned.filter(va => !newVaIds.includes(va.vaId.toString())); const vaToRemove = existingProject.vaAssigned.filter(va => !newVaIds.includes(va.toString()));
// Update relationships for added VAs // Update relationships for added VAs
if (vaToAdd.length > 0) { if (vaToAdd.length > 0) {
@ -198,7 +187,7 @@ async function update_project_sf(req, res) {
updateData, updateData,
{ new: true, runValidators: true } { new: true, runValidators: true }
).populate('clientId', 'userDetails.name userDetails.email') ).populate('clientId', 'userDetails.name userDetails.email')
.populate('vaAssigned', 'userDetails.firstName userDetails.lastName userDetails.email'); .populate('vaAssigned', 'userDetails.firstName userDetails.lastName userDetails.email');
// If VA assignments were changed but relationship update failed, log warning // If VA assignments were changed but relationship update failed, log warning
if (vaAssigned !== undefined) { if (vaAssigned !== undefined) {

View File

@ -147,57 +147,55 @@ async function va_leave_apply_function(req, res) {
await leaveRequest.save(); await leaveRequest.save();
try { try {
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/leave-request`; // const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/timesheet-adjustment`;
const sfData = { // const sfData = {
"leaveId": leaveRequest._id.toString(), // "id": leaveRequest._id,
// "va": { // "va": {
// "id": vaId // "id": vaId
// }, // },
"va": vaId.toString(), // "dateFrom": startDate.toISOString().split('T')[0],
"dateFrom": startDate.toISOString().split('T')[0], // "dateTo": endDate.toISOString().split('T')[0],
"dateTo": endDate.toISOString().split('T')[0], // "totalDays": totalDays,
"totalDays": totalDays, // "leaveType": leaveType,
"leaveType": leaveType, // "comment": processedComment.plainText, // Send plain text to Salesforce
"comment": processedComment.plainText !== "" ? processedComment.plainText : "-", // Send plain text to Salesforce // "attachments": processedComment.extractedImages, // Send extracted images info
"attachments": processedComment.extractedImages, // Send extracted images info // "status": "PENDING",
"status": "PENDING", // "rejectionReason": ""
"rejectionReason": "" // }
}
const response = await fetch(salesforceUrl, { // const response = await fetch(salesforceUrl, {
method: 'POST', // method: 'POST',
headers: { // headers: {
'Content-Type': 'application/json', // 'Content-Type': 'application/json',
// Add any required headers for Salesforce authentication // // Add any required headers for Salesforce authentication
// 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN // // 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
}, // },
body: JSON.stringify({ "data": sfData }) // body: JSON.stringify({"data": sfData})
}); // });
if (!response.ok) { // if (!response.ok) {
const errorText = await response.text(); // const errorText = await response.text();
await LeavesModel.findByIdAndDelete(leaveRequest._id) // throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`); // }
}
// const result = await response.json(); // const result = await response.json();
// console.log("✅ Successfully synced VA data to Salesforce:", result); // // console.log("✅ Successfully synced VA data to Salesforce:", result);
// return res.status(201).json({ return res.status(201).json({
// success: true, success: true,
// message: "Leave request submitted successfully", message: "Leave request submitted successfully",
// data: { data: {
// leaveId: leaveRequest._id, leaveId: leaveRequest._id,
// dateFrom: startDate.toISOString().split('T')[0], dateFrom: startDate.toISOString().split('T')[0],
// dateTo: endDate.toISOString().split('T')[0], dateTo: endDate.toISOString().split('T')[0],
// totalDays: totalDays, totalDays: totalDays,
// leaveType, leaveType,
// comment: processedComment.processedHTML, comment: processedComment.processedHTML,
// // attachments: processedComment.extractedImages, // attachments: processedComment.extractedImages,
// status: "PENDING" status: "PENDING"
// }, },
// // SyncSalesforce: result // SyncSalesforce: result
// }); });
} catch (error) { } catch (error) {
console.error("Failed to sync with Salesforce:", error.message); console.error("Failed to sync with Salesforce:", error.message);
console.log(error) console.log(error)
@ -208,19 +206,19 @@ async function va_leave_apply_function(req, res) {
throw salesforceError; throw salesforceError;
} }
return res.status(201).json({ // return res.status(201).json({
success: true, // success: true,
message: "Leave request submitted successfully", // message: "Leave request submitted successfully",
data: { // data: {
leaveId: leaveRequest._id, // leaveId: leaveRequest._id,
dateFrom: startDate.toISOString().split('T')[0], // dateFrom: startDate.toISOString().split('T')[0],
dateTo: endDate.toISOString().split('T')[0], // dateTo: endDate.toISOString().split('T')[0],
totalDays: totalDays, // totalDays: totalDays,
leaveType, // leaveType,
comment, // comment,
status: "PENDING" // status: "PENDING"
} // }
}); // });
} catch (error) { } catch (error) {
console.error("Leave apply error:", error); console.error("Leave apply error:", error);

View File

@ -69,13 +69,13 @@ async function checkDailyHoursAndLogout(vaId, vaEmail, projectId, project, curre
try { try {
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`; const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
const sfDataSync = { const sfDataSync = {
"_id": logoutLog._id, "_id": logoutLog._id,
"loggedType": "LOGOUT", "loggedType": "LOGOUT",
"vaId": vaId, "vaId": vaId,
"vaEmail": vaEmail, "vaEmail": vaEmail,
"projectId": projectId, "projectId": projectId,
"time": logoutLog.time, "time": logoutLog.time,
} }
const response = await fetch(salesforceUrl, { const response = await fetch(salesforceUrl, {
method: 'POST', method: 'POST',
@ -147,12 +147,7 @@ async function va_log_track_work_callback(req, res) {
const vaId = vaUser._id; const vaId = vaUser._id;
const payload = req.body || {}; const payload = req.body || {};
const { projectId, logId, title, appName, url = null } = payload;
let { projectId, logId, title, appName, url = null } = payload;
if (title === "") {
title = ""
}
const project = await ProjectModel.findById(projectId); const project = await ProjectModel.findById(projectId);
if (!project) return res.status(400).json({ success: false, message: "Project not found" }); if (!project) return res.status(400).json({ success: false, message: "Project not found" });

View File

@ -9,7 +9,7 @@ async function va_payslip_data_function(req, res) {
return res.status(404).json({ success: false, message: "VA not found" }); return res.status(404).json({ success: false, message: "VA not found" });
} }
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-payslip/${vaId}`; const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-payslip?vaId=${vaId}`;
const response = await fetch(salesforceUrl, { const response = await fetch(salesforceUrl, {
method: 'GET', method: 'GET',

View File

@ -44,7 +44,7 @@ async function va_remove_function(req, res) {
if (projectIds.length > 0) { if (projectIds.length > 0) {
const projectResult = await ProjectModel.updateMany( const projectResult = await ProjectModel.updateMany(
{ _id: { $in: projectIds } }, { _id: { $in: projectIds } },
{ $pull: { "vaAssigned.vaId": vaId } } { $pull: { vaAssigned: vaId } }
); );
projectsUpdated = projectResult.modifiedCount; projectsUpdated = projectResult.modifiedCount;
console.log(`Removed VA from ${projectsUpdated} projects`); console.log(`Removed VA from ${projectsUpdated} projects`);

View File

@ -5,7 +5,7 @@ import { VAModel } from "../../../models/vaModel.js";
// Register a new VA // Register a new VA
async function va_profile_update_callback(req, res) { async function va_profile_update_callback(req, res) {
try { try {
const sf_token = req.headers["x-auth-token"]; const sf_token = req.headers.sf_token;
const accessToken = req.headers.token; const accessToken = req.headers.token;
let vaUser; let vaUser;
@ -68,7 +68,7 @@ async function va_profile_update_callback(req, res) {
const allowedFields = [ const allowedFields = [
"firstName", "lastName", "dob", "age", "address", "firstName", "lastName", "dob", "age", "address",
"city", "state", "country", "phone", "skype", "city", "state", "country", "phone", "skype",
"paymentMethod", "payoneerAccount", "veemAccount", "wiseEmail" "paymentMethod", "payoneerAccount", "veemAccount"
]; ];
let hasUpdates = false; let hasUpdates = false;

View File

@ -74,7 +74,7 @@ async function va_register(req, res) {
res.status(201).json({ success: true, data: created, SyncSalesforce: result }); res.status(201).json({ success: true, data: created, SyncSalesforce: result });
} catch (error) { } catch (error) {
console.error("❌ Failed to sync with Salesforce:", error.message); console.error("❌ Failed to sync with Salesforce:", error.message);
await VAModel.findOneAndDelete({_id: created.userDetails._id});
// Create a custom error to distinguish Salesforce errors // Create a custom error to distinguish Salesforce errors
const salesforceError = new Error(`Salesforce sync failed: ${error.message}`); const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
salesforceError.isSalesforceError = true; salesforceError.isSalesforceError = true;

View File

@ -105,7 +105,7 @@ function getCurrentUTCTime() {
// console.log(`Schedule Check - Current: ${currentMinutes}, Start: ${scheduleStartMinutes}, End: ${scheduleEndMinutes}, Allowed Start: ${allowedStartMinutes}, Allowed End: ${allowedEndMinutes}`); // console.log(`Schedule Check - Current: ${currentMinutes}, Start: ${scheduleStartMinutes}, End: ${scheduleEndMinutes}, Allowed Start: ${allowedStartMinutes}, Allowed End: ${allowedEndMinutes}`);
// // Check for PARTTIME with flexible schedule // // Check for PARTTIME with flexible schedule
// if (vaStatus === "PARTTIME" && project.isFlexibleAllowed) { // if (vaStatus === "PARTTIME" && project.isFlexibleAllowedPt) {
// return { isCompliant: true, warnings }; // return { isCompliant: true, warnings };
// } // }
@ -366,7 +366,7 @@ async function va_project_log_callback(req, res) {
const result = await response.json(); const result = await response.json();
// console.log("✅ Successfully synced VA data to Salesforce:", result); // console.log("✅ Successfully synced VA data to Salesforce:", result);
return res.status(201).json({ res.status(201).json({
success: true, success: true,
message: `${type} Successfully`, message: `${type} Successfully`,
logId: createdLog._id, logId: createdLog._id,

View File

@ -72,7 +72,7 @@ async function va_projects_log_list_function(req, res) {
console.log("VA ID:", vaId.toString()); console.log("VA ID:", vaId.toString());
projectList = await ProjectModel.find({ "vaAssigned.vaId": vaUser._id }); projectList = await ProjectModel.find({ vaAssigned: vaUser._id });
} }
if (!projectList.length) { if (!projectList.length) {

View File

@ -11,7 +11,7 @@ function formatHMS(ms) {
async function va_projects_report_function(req, res) { async function va_projects_report_function(req, res) {
try { try {
const sf_token = req.headers["x-auth-token"]; const sf_token = req.headers.sf_token;
let vaId; let vaId;
let vaUser; let vaUser;
let vaEmail; let vaEmail;

View File

@ -1,24 +1,15 @@
import mongoose from "mongoose";
import { AdjustedHoursModel } from "../../../models/vaModel.js"; import { AdjustedHoursModel } from "../../../models/vaModel.js";
// Get all adjustment requests (with filtering options) // Get all adjustment requests (with filtering options)
async function get_adjustment_requests_list(req, res) { async function get_adjustment_requests_list(req, res) {
try { try {
const { status, projectId, vaId } = req.query; const { status, projectId, vaId } = req.params;
// Build filter object // Build filter object
const filter = {}; const filter = {};
// if (status) filter.status = status; if (status) filter.status = status;
// if (projectId) filter.projectId = projectId; if (projectId) filter.projectId = projectId;
// if (vaId) filter.vaId = vaId; if (vaId) filter.vaId = vaId;
if (status && status !== "undefined") filter.status = status;
if (projectId && mongoose.Types.ObjectId.isValid(projectId)) {
filter.projectId = new mongoose.Types.ObjectId(projectId);
}
if (vaId && mongoose.Types.ObjectId.isValid(vaId)) {
filter.vaId = new mongoose.Types.ObjectId(vaId);
}
const adjustments = await AdjustedHoursModel.find(filter) const adjustments = await AdjustedHoursModel.find(filter)
.sort({ createdAt: -1 }) .sort({ createdAt: -1 })

View File

@ -31,7 +31,7 @@ async function va_timesheet_manage_adjustment_request(req, res) {
}); });
} }
if (action === "APPROVE") { if (action === "approve") {
// Create new log entries in LoggedModel // Create new log entries in LoggedModel
const loginLog = new LoggedModel({ const loginLog = new LoggedModel({
loggedType: "LOGIN", loggedType: "LOGIN",
@ -78,7 +78,7 @@ async function va_timesheet_manage_adjustment_request(req, res) {
} }
}); });
} else if (action === "REJECT") { } else if (action === "reject") {
// Update adjustment record as rejected // Update adjustment record as rejected
adjustment.status = "REJECTED"; adjustment.status = "REJECTED";
adjustment.reviewedAt = new Date(); adjustment.reviewedAt = new Date();

View File

@ -34,7 +34,7 @@ async function va_timesheet_adjustment_function(req, res) {
// Validate project exists and VA is assigned to it // Validate project exists and VA is assigned to it
const project = await ProjectModel.findOne({ const project = await ProjectModel.findOne({
_id: projectId, _id: projectId,
"vaAssigned.vaId": vaId vaAssigned: vaId
}); });
if (!project) { if (!project) {
@ -138,17 +138,17 @@ async function va_timesheet_adjustment_function(req, res) {
try { try {
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/timesheet-adjustment`; const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/timesheet-adjustment`;
const sfData = { const sfData = {
"id": adjustmentRequest._id.toString(), "id": adjustmentRequest._id,
"va": { "va": {
"id": vaId.toString() "id": vaId
}, },
"project": { "project": {
"id": projectId.toString() "id": projectId
}, },
"date": adjustmentDate.toISOString().split('T')[0], "date": adjustmentDate.toISOString().split('T')[0],
"loginTime": loginDateTime.toISOString(), "loginTime": loginDateTime.toISOString(),
"logoutTime": logoutDateTime.toISOString(), "logoutTime": logoutDateTime.toISOString(),
"comment": processedComment.plainText !== "" ? processedComment.plainText : "-", // Send plain text to Salesforce "comment": processedComment.plainText, // Send plain text to Salesforce
"attachments": processedComment.extractedImages, // Send extracted images info "attachments": processedComment.extractedImages, // Send extracted images info
"status": "PENDING", "status": "PENDING",
"rejectionReason": "", "rejectionReason": "",
@ -167,7 +167,6 @@ async function va_timesheet_adjustment_function(req, res) {
if (!response.ok) { if (!response.ok) {
const errorText = await response.text(); const errorText = await response.text();
await AdjustedHoursModel.findByIdAndDelete(adjustmentRequest._id)
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`); throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
} }

View File

@ -3,7 +3,7 @@ import { VAModel, ProjectModel, LoggedModel } from "../../../models/vaModel.js";
async function va_timesheet_summary_function(req, res) { async function va_timesheet_summary_function(req, res) {
try { try {
const sf_token = req.headers["x-auth-token"]; const sf_token = req.headers.sf_token;
let vaId; let vaId;
let vaUser; let vaUser;
let vaEmail; let vaEmail;
@ -43,7 +43,7 @@ async function va_timesheet_summary_function(req, res) {
// Validate project exists and VA is assigned to it // Validate project exists and VA is assigned to it
const project = await ProjectModel.findOne({ const project = await ProjectModel.findOne({
_id: projectId, _id: projectId,
"vaAssigned.vaId": vaId vaAssigned: vaId
}); });
if (!project) { if (!project) {

View File

@ -1,143 +0,0 @@
import { VAModel, TipsModel, ClientModel } from "../../../models/vaModel.js";
// Approve or reject leave
async function va_tips_manage_function(req, res) {
const { clientId, amount, description, vaId } = req.body;
try {
const vaUser = await VAModel.findById(vaId);
if (!vaUser) {
return res.status(404).json({ success: false, message: "Virtual Assistant not found" });
}
const clUser = await ClientModel.findById(clientId);
if (!clUser) {
return res.status(404).json({ success: false, message: "Client not found" });
}
// Create new tip entry
const newTip = {
clientId,
status: "Unbilled",
amount: parseFloat(amount).toFixed(2),
description: description || "",
vaName: vaUser.userDetails.firstName ? vaUser.userDetails.firstName : "" + " " + vaUser.userDetails.lastName ? vaUser.userDetails.lastName : "",
vaId,
};
const createdTip = await TipsModel.create(newTip);
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-tips`;
const sfData = {
"id": createdTip._id.toString(),
"vaId": createdTip.vaId.toString(),
"status": "Unbilled",
"clientId": createdTip.clientId.toString(),
"comment": createdTip.description || "",
"tipAmount": Number(parseFloat(createdTip.amount).toFixed(2)), // Convert to cents
}
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) {
console.log("✅ Successfully synced Tip data to Salesforce");
return res.status(201).json({ success: true, data: createdTip });
} else {
TipsModel.findByIdAndDelete(createdTip._id);
const errorText = await response.text();
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
}
} catch (error) {
console.log(error);
res.status(500).json({ success: false, message: "Internal server error" });
}
}
async function va_tips_get(req, res) {
const { clientId } = req.body;
try {
const clUser = await ClientModel.findById(clientId);
if (!clUser) {
return res.status(404).json({ success: false, message: "Client not found" });
}
const data = await TipsModel.find({ clientId: clientId, status: "Unbilled" });
return res.status(200).json({ success: true, data: data });
} catch (error) {
console.log(error);
res.status(500).json({ success: false, message: "Internal server error" });
}
}
async function va_tips_update(req, res) {
const { tipId } = req.params;
const { update } = req.body;
try {
const tipData = await TipsModel.findById(tipId);
if (!tipData) {
return res.status(404).json({ success: false, message: "Tip not found" });
}
await TipsModel.updateOne({ _id: tipId }, update);
return res.status(200).json({ success: true, message: "Tip updated successfully" });
} catch (error) {
console.log(error);
res.status(500).json({ success: false, message: "Internal server error" });
}
}
async function va_tips_delete(req, res) {
const { tipId } = req.params;
try {
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-tips`;
// const sfData = {
// "id": createdTip._id.toString(),
// "vaId": createdTip.vaId.toString(),
// "status": "Unbilled",
// "clientId": createdTip.clientId.toString(),
// "comment": createdTip.description,
// "tipAmount": Number(parseFloat(createdTip.amount).toFixed(2)), // Convert to cents
// }
const response = await fetch(salesforceUrl, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
// Add any required headers for Salesforce authentication
// 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
},
body: JSON.stringify({ "id": tipId})
});
if (response.ok) {
await TipsModel.findOneAndDelete({ _id: tipId });
return res.status(200).json({ success: true, message: "Tip updated successfully" });
} else {
const errorText = await response.text();
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
}
} catch (error) {
console.log(error);
res.status(500).json({ success: false, message: "Internal server error" });
}
}
export { va_tips_manage_function, va_tips_get, va_tips_update, va_tips_delete };

View File

@ -4,7 +4,7 @@ import logger from "../config/logger.js";
async function authValidator(req, res, next) { async function authValidator(req, res, next) {
try { try {
const sf_token = req.headers["x-auth-token"]; const sf_token = req.headers.sf_token;
if (sf_token) { if (sf_token) {
const { vaId } = req.params; const { vaId } = req.params;
if(vaId) req.body.vaId = vaId; if(vaId) req.body.vaId = vaId;
@ -50,15 +50,6 @@ async function authValidator(req, res, next) {
async function clientAuthValidator(req, res, next) { async function clientAuthValidator(req, res, next) {
try { try {
const sf_token = req.headers["x-auth-token"];
if (sf_token) {
next();
}
if(sf_token) return;
const accessToken = req.headers.token; const accessToken = req.headers.token;
if (!accessToken) { if (!accessToken) {

View File

@ -89,16 +89,9 @@ const projectSchema = new Schema({
}, },
vaAssigned: [ vaAssigned: [
{ {
_id: false, type: Types.ObjectId,
vaId: { ref: "VA",
type: Types.ObjectId, },
ref: "VA",
},
isFlexibleAllowed: {
type: Boolean,
default: true
}
}
], ],
dedicatedHours: { dedicatedHours: {
type: String, type: String,
@ -117,7 +110,7 @@ const projectSchema = new Schema({
type: String, type: String,
default: null default: null
}, },
isFlexibleAllowed: { isFlexibleAllowedPt: {
type: Boolean, type: Boolean,
default: true default: true
}, },
@ -370,36 +363,36 @@ const vaWorkTrackSchema = new Schema({
) )
const passwordResetSchema = new mongoose.Schema({ const passwordResetSchema = new mongoose.Schema({
email: { email: {
type: String, type: String,
required: true, required: true,
lowercase: true, lowercase: true,
index: true index: true
}, },
token: { token: {
type: String, type: String,
required: true, required: true,
unique: true, unique: true,
index: true index: true
}, },
userType: { userType: {
type: String, type: String,
required: true, required: true,
enum: ['va', 'client'], enum: ['va', 'client'],
index: true index: true
}, },
expiresAt: { expiresAt: {
type: Date, type: Date,
required: true, required: true,
index: true, index: true,
expires: 3600 // TTL index for 1 hour expires: 3600 // TTL index for 1 hour
}, },
used: { used: {
type: Boolean, type: Boolean,
default: false default: false
} }
}, { }, {
timestamps: true timestamps: true
}); });
passwordResetSchema.index({ email: 1, used: 1 }); passwordResetSchema.index({ email: 1, used: 1 });
@ -518,37 +511,6 @@ const leavesSchema = new Schema({
} }
}, { timestamps: true }); }, { timestamps: true });
/* ---------------------- Tips Schema ---------------------- */
const tipsSchema = new Schema({
clientId: {
type: Types.ObjectId,
ref: "Client"
},
vaId: {
type: Types.ObjectId,
ref: "VA"
},
vaName: {
type: String,
required: true
},
status: {
type: String,
enum: ["Unbilled", "Billed"],
default: "Unbilled",
required: true
},
amount: {
type: String,
required: true
},
description: {
type: String,
}
}, { timestamps: true })
/* ---------------------- Model Exports ---------------------- */ /* ---------------------- Model Exports ---------------------- */
const VAModel = model("VA", vaSchema); const VAModel = model("VA", vaSchema);
const ClientModel = model("Client", clientSchema); const ClientModel = model("Client", clientSchema);
@ -558,6 +520,5 @@ const WorkTrackModel = model("TrackWork", vaWorkTrackSchema);
const PasswordResetModel = model("PasswordReset", passwordResetSchema); const PasswordResetModel = model("PasswordReset", passwordResetSchema);
const AdjustedHoursModel = model("AdjustedHours", adjustedHoursSchema); const AdjustedHoursModel = model("AdjustedHours", adjustedHoursSchema);
const LeavesModel = model("Leaves", leavesSchema); const LeavesModel = model("Leaves", leavesSchema);
const TipsModel = model("Tips", tipsSchema);
export { VAModel, ClientModel, ProjectModel, LoggedModel, WorkTrackModel, PasswordResetModel, AdjustedHoursModel, LeavesModel, TipsModel }; export { VAModel, ClientModel, ProjectModel, LoggedModel, WorkTrackModel, PasswordResetModel, AdjustedHoursModel, LeavesModel };

View File

@ -13,7 +13,6 @@ import { client_dashboard_va_log } from "../../controllers/client/dashboard/clie
import { client_verify_email } from "../../controllers/client/vaClientEmailVerify.controller.js"; import { client_verify_email } from "../../controllers/client/vaClientEmailVerify.controller.js";
import { client_dashboard_projects } from "../../controllers/client/dashboard/clientDashboardProjects.controller.js"; import { client_dashboard_projects } from "../../controllers/client/dashboard/clientDashboardProjects.controller.js";
import { client_logout_callback } from "../../controllers/client/dashboard/clientLogout.controller.js"; import { client_logout_callback } from "../../controllers/client/dashboard/clientLogout.controller.js";
import { va_tips_delete, va_tips_get, va_tips_manage_function, va_tips_update } from "../../controllers/va/vaTips/vaTipsManage.controller.js";
// clients routes // clients routes
clientRoutes.post("/register", va_register_client); // Register a new VA clientRoutes.post("/register", va_register_client); // Register a new VA
@ -23,14 +22,11 @@ clientRoutes.post("/forgot-password", client_request_password_reset);
clientRoutes.post("/verify-client-email", client_verify_email); clientRoutes.post("/verify-client-email", client_verify_email);
clientRoutes.post("/logout", clientAuthValidator, client_logout_callback); clientRoutes.post("/logout", clientAuthValidator, client_logout_callback);
clientRoutes.post("/tips", clientAuthValidator, va_tips_manage_function);
clientRoutes.get("/tips", clientAuthValidator, va_tips_get);
clientRoutes.patch("/tips/:tipId", clientAuthValidator, va_tips_update);
clientRoutes.delete("/tips/:tipId", clientAuthValidator, va_tips_delete);
// clientRoutes.get("/:clientID?", va_clients_callback); // VA Clients Callback for getting all VA's or specific client by ID (NC) // 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) clientRoutes.get("/:clientId/projects/:projectId", va_clients_projects_callback); // VA Clients Projects Callback (NC)
// dashboard API's // dashboard API's
clientRoutes.get("/dashboard", clientAuthValidator, client_dashboard); // 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.post("/dashboard/va-report", clientAuthValidator, client_dashboard_va_log); // VA details

View File

@ -30,7 +30,7 @@ import { va_remove_function } from "../../controllers/va/vaProfile/vaDelete.cont
import { va_payroll_schedule_function } from "../../controllers/va/vaPayroll/vaPayrollSchedule.controller.js"; import { va_payroll_schedule_function } from "../../controllers/va/vaPayroll/vaPayrollSchedule.controller.js";
import { va_payslip_data_function } from "../../controllers/va/vaPayroll/vaPaySlip.controller.js"; import { va_payslip_data_function } from "../../controllers/va/vaPayroll/vaPaySlip.controller.js";
vaRoutes.get("/", va_callback); // VA Callback for getting active VA list vaRoutes.get("/", va_callback); // VA Callback for getting active VA
vaRoutes.post("/login", va_login_callback); // VA Callback for login VA vaRoutes.post("/login", va_login_callback); // VA Callback for login VA
vaRoutes.get("/logout", authValidator, va_logout_callback); // VA Callback for login VA vaRoutes.get("/logout", authValidator, va_logout_callback); // VA Callback for login VA
@ -47,9 +47,7 @@ vaRoutes.delete("/profile/:id", va_remove_function); // VA delete Callback
// timesheet adjustment routes // timesheet adjustment routes
vaRoutes.get("/timesheet/summary/:projectId/:date/:vaId?", 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.post("/timesheet/adjustment/request", authValidator, va_timesheet_adjustment_function); // Timesheet Adjustment for VA
vaRoutes.get("/timesheet/adjustments/list/:projectId?/:vaId?/:status?", get_adjustment_requests_list); // show all adjustment list to approve and reject // 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); vaRoutes.patch("/timesheet/adjustments/:adjustmentId", va_timesheet_manage_adjustment_request);
// Leaves routes // Leaves routes
@ -58,7 +56,7 @@ vaRoutes.get("/leaves/status/:status", authValidator, va_leave_list_function); /
vaRoutes.get("/leaves/type/:leaveType", authValidator, va_leave_list_function); // Filter by leave type 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.get("/leaves/status/:status/type/:leaveType", authValidator, va_leave_list_function); // leaves Filter
vaRoutes.post("/leave/request", authValidator, va_leave_apply_function); // Leave for VA vaRoutes.post("/leave/request", authValidator, va_leave_apply_function); // Leave for VA
vaRoutes.patch("/leave/:leaveId", authValidator, va_leave_manage_function); vaRoutes.patch("/leave/:leaveId", va_leave_manage_function);
// timesheet adjustment routes // timesheet adjustment routes
@ -86,6 +84,9 @@ vaRoutes.get("/log/project/:projectId/:from?/:to?", authValidator, va_log_callba
vaRoutes.get("/payrollSchedule", authValidator, va_payroll_schedule_function); // VA Payroll Schedule vaRoutes.get("/payrollSchedule", authValidator, va_payroll_schedule_function); // VA Payroll Schedule
vaRoutes.get("/paySlip/:vaId", authValidator, va_payslip_data_function); // VA Payslip vaRoutes.get("/paySlip/:vaId", authValidator, va_payslip_data_function); // VA Payslip
vaRoutes.get("/:userName", va_username_callback); // VA Username Callback vaRoutes.get("/:userName", va_username_callback); // VA Username Callback

View File

@ -121,7 +121,6 @@ app.get('/health', (req, res) => {
app.get("/client/login", (req, res) => { app.get("/client/login", (req, res) => {
res.render("client_login") res.render("client_login")
}) })
app.get("/client/forgot-password", (req, res) => { app.get("/client/forgot-password", (req, res) => {
res.render("forgot_password") res.render("forgot_password")
}) })
@ -300,8 +299,6 @@ io.use(async (socket, next) => {
if (clientUser) { if (clientUser) {
socket.data.role = "client"; socket.data.role = "client";
socket.data.clientId = clientUser._id.toString(); socket.data.clientId = clientUser._id.toString();
socket.data.vaId = vaId.toString();
socket.data.projectId = projectId;
// Get all VA IDs assigned to this client // Get all VA IDs assigned to this client
socket.data.allowedVAs = clientData.vaAssigned.map((va) => va.toString()); socket.data.allowedVAs = clientData.vaAssigned.map((va) => va.toString());
@ -322,7 +319,7 @@ io.on("connection", (socket) => {
console.log("User connected:", socket.id); console.log("User connected:", socket.id);
// Register role and ID (client or VA) // Register role and ID (client or VA)
socket.on("register", async ({ role, id, projectId = null, clientId = null }) => { socket.on("register", async ({ role, id }) => {
if (role === "va") { if (role === "va") {
socket.data.role = "va"; socket.data.role = "va";
socket.data.vaId = id; socket.data.vaId = id;
@ -331,35 +328,23 @@ io.on("connection", (socket) => {
if (role === "client") { if (role === "client") {
socket.data.role = "client"; socket.data.role = "client";
socket.data.clientId = clientId; socket.data.clientId = id;
// 🧩 Get allowed VAs for this client // 🧩 Get allowed VAs for this client
const client = await ClientModel.findById(clientId); const client = await ClientModel.findById(id)
.populate("projectIds") // to access their projects
const a = JSON.stringify(client); .populate("vaIds"); // optional if you track directly
console.log(a, "client data for allowed VAs");
if (client.projectIds.length !== 0 && client.projectIds.includes(projectId)) {
const projectVAIds = new Set();
const populated = await ProjectModel.findById(projectId).populate("vaAssigned");
populated.vaAssigned.forEach((va) => projectVAIds.add(va.vaId.toString()));
socket.data.allowedVAs = Array.from(projectVAIds);
console.log(`Client ${clientId} can view VAs:`, socket.data.allowedVAs);
io.to(vaSocketRespectToClient.get(`${clientId}-${id}-${projectId}-va`)).emit("startSendAppsDetails", { projId: projectId });
}
// Collect all VA IDs from their projects // Collect all VA IDs from their projects
// const projectVAIds = new Set(); const projectVAIds = new Set();
// for (const project of client.projectIds) { for (const project of client.projectIds) {
// const populated = await ProjectModel.findById(project._id).populate("vaAssigned"); const populated = await ProjectModel.findById(project._id).populate("vaAssigned");
// populated.vaAssigned.forEach((va) => projectVAIds.add(va._id.toString())); populated.vaAssigned.forEach((va) => projectVAIds.add(va._id.toString()));
// } }
// // Store allowed VA IDs in socket memory // Store allowed VA IDs in socket memory
// socket.data.allowedVAs = Array.from(projectVAIds); socket.data.allowedVAs = Array.from(projectVAIds);
// console.log(`Client ${clientId} can view VAs:`, socket.data.allowedVAs); console.log(`Client ${id} can view VAs:`, socket.data.allowedVAs);
} }
}); });
@ -415,7 +400,7 @@ io.on("connection", (socket) => {
socket.to(vaSocketRespectToClient.get(`${clientId}-${vaId}-${projectId}-va`)).emit("appToCapture", { appTitle }); socket.to(vaSocketRespectToClient.get(`${clientId}-${vaId}-${projectId}-va`)).emit("appToCapture", { appTitle });
}); });
socket.on("requestVisibleApps", async ({ token }) => { socket.on("getVisibleApps", async ({ token, apps }) => {
try { try {
// Verify token // Verify token
const decoded = jwt.verify(token, process.env.JWT_SECRET_ACCESS_TOKEN); const decoded = jwt.verify(token, process.env.JWT_SECRET_ACCESS_TOKEN);
@ -431,35 +416,6 @@ io.on("connection", (socket) => {
const vaId = vaUser._id.toString(); const vaId = vaUser._id.toString();
console.log("🟢 Received Apps from VA:", vaId); console.log("🟢 Received Apps from VA:", vaId);
// Broadcast image only to allowed clients
for (let [_, clientSocket] of io.sockets.sockets) {
if (
clientSocket.data.role === "client" &&
clientSocket.data.allowedVAs?.includes(vaId)
) {
clientSocket.emit("captureAppsTracker");
}
}
} catch (error) {
console.error("❌ Token verification failed:", err.message);
}
});
socket.on("getVisibleApps", async ({ token, apps }) => {
try {
// Verify token
const decoded = jwt.verify(token, process.env.JWT_SECRET_ACCESS_TOKEN);
const vaEmail = decoded.email;
// Find VA in DB
const vaUser = await VAModel.findOne({ "userDetails.email": vaEmail });
if (!vaUser) {
console.log("❌ Unauthorized VA");
return;
}
const vaId = vaUser._id.toString();
// Broadcast image only to allowed clients // Broadcast image only to allowed clients
for (let [_, clientSocket] of io.sockets.sockets) { for (let [_, clientSocket] of io.sockets.sockets) {
if ( if (
@ -475,53 +431,18 @@ io.on("connection", (socket) => {
}); });
// socket.on('disconnect', () => { socket.on('disconnect', () => {
// console.log('a user Disconnected'); console.log('a user Disconnected');
// });
// When client leaves or closes tab
socket.on("disconnect", () => {
if (socket.data.role === "client") {
const { clientId, vaId, projectId } = socket.data;
const vaSocketId = vaSocketRespectToClient.get(
`${clientId}-${vaId}-${projectId}-va`
);
if (vaSocketId) {
// io.to(vaSocketId).emit("stopSendAppsDetails");
io.to(vaSocketRespectToClient.get(`${clientId}-${vaId}-${projectId}-va`)).emit("stopSendAppsDetails", { projId: projectId });
}
}
}); });
}); });
app.get("/", (req, res) => {
res.render("client_login")
})
app.get("/forgot-password", (req, res) => {
res.render("forgot_password")
})
app.get("/heath", () => {
if (Math.round(Math.random()) === 1) {
return res.status(200).json({ "success": true, "message": "Server is Healthy" })
} else {
return res.status(403).json({ "success": false, "message": "Server is not Healthy" })
}
})
app.use((req, res) => { app.use((req, res) => {
res.status(404).render("not_found", { res.status(404).render("not_found", {
title: "404 - Page Not Found" title: "404 - Page Not Found"
}); });
}); });
server.listen(PORT, "0.0.0.0", () => { server.listen(PORT, () => {
mongoose mongoose
.connect( .connect(
process.env.MONGO_URI, process.env.MONGO_URI,

View File

@ -63,7 +63,7 @@ class PasswordResetService {
logger.info(`Password reset token created for ${userType}: ${email}`); logger.info(`Password reset token created for ${userType}: ${email}`);
return { return {
success: true, success: true,
message: "Email sent successfully. Please check your inbox." message: "If the email exists, a reset link has been sent"
}; };
} catch (error) { } catch (error) {

View File

@ -1,5 +0,0 @@
const convertToObjectId = (id) => {
return mongoose.Types.ObjectId.isValid(id) ? new mongoose.Types.ObjectId(id) : id;
}
export default convertToObjectId;

View File

@ -18,7 +18,7 @@
if (userType === 'client') { if (userType === 'client') {
forgotPasswordUrl = '/client/forgot-password'; forgotPasswordUrl = '/client/forgot-password';
loginUrl = '/'; loginUrl = '/client/login';
} }
%> %>
<a href="<%= forgotPasswordUrl %>" class="btn btn-primary" style="margin-bottom:8px;">Request New Reset Link</a> <a href="<%= forgotPasswordUrl %>" class="btn btn-primary" style="margin-bottom:8px;">Request New Reset Link</a>

View File

@ -36,7 +36,7 @@
<% <%
let backToLoginUrl = `${process.env.FRONTEND_URL}/login`; let backToLoginUrl = `${process.env.FRONTEND_URL}/login`;
if (userType === 'client') { if (userType === 'client') {
backToLoginUrl = '/'; backToLoginUrl = '/client/login';
} }
%> %>
<a href="<%= backToLoginUrl %>" class="small-link">Back to Login</a> <a href="<%= backToLoginUrl %>" class="small-link">Back to Login</a>

View File

@ -15,7 +15,7 @@
<% <%
let loginUrl = `${process.env.FRONTEND_URL}/login`; let loginUrl = `${process.env.FRONTEND_URL}/login`;
if (userType === 'client') { if (userType === 'client') {
loginUrl = '/'; loginUrl = '/client/login';
} }
%> %>
<a href="<%= loginUrl %>" class="btn btn-primary">Go to Login</a> <a href="<%= loginUrl %>" class="btn btn-primary">Go to Login</a>

View File

@ -252,7 +252,7 @@
async function fetchWithAuth(url, options = {}) { async function fetchWithAuth(url, options = {}) {
const token = getAuthToken(); const token = getAuthToken();
if (!token) { if (!token) {
window.location.href = '/'; window.location.href = '/client/login';
return; return;
} }
@ -268,7 +268,7 @@
if (response.status === 401) { if (response.status === 401) {
localStorage.removeItem('knoptpq'); localStorage.removeItem('knoptpq');
localStorage.removeItem('refreshToken'); localStorage.removeItem('refreshToken');
window.location.href = '/'; window.location.href = '/client/login';
return; return;
} }
@ -287,7 +287,7 @@
// Clear local storage and redirect // Clear local storage and redirect
localStorage.removeItem('knoptpq'); localStorage.removeItem('knoptpq');
localStorage.removeItem('refreshToken'); localStorage.removeItem('refreshToken');
window.location.href = '/'; window.location.href = '/client/login';
} }
} }

View File

@ -1,6 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" data-theme="light"> <html lang="en" data-theme="light">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
@ -8,21 +7,6 @@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<link rel="stylesheet" href="/public/css/dashboard.css"> <link rel="stylesheet" href="/public/css/dashboard.css">
</head> </head>
<style>
.void-btn-a {
background: tomato;
text-decoration: none;
color: #ffffff !important;
border: none;
padding: 0.5rem 1rem;
border-radius: 5px;
font-weight: bold;
text-decoration: none;
cursor: pointer;
}
</style>
<body class="dashboard-body"> <body class="dashboard-body">
<div id="root"> <div id="root">
<nav class="dashboard-header"> <nav class="dashboard-header">
@ -171,8 +155,9 @@
<form class="modal-form" id="tipForm"> <form class="modal-form" id="tipForm">
<div class="form-group"> <div class="form-group">
<label class="form-label">Select Virtual Assistant</label> <label class="form-label">Select Virtual Assistant</label>
<select name="vaForTip" class="form-input" id="vaForTip" required> <select name="vaForTip" class="form-input select-field" id="vaForTip" required>
<option value="">-- Select Virtual Assistant --</option> <option value="">-- Select Virtual Assistant --</option>
<option value="va 1">VA 1</option>
<!-- Options will be populated dynamically --> <!-- Options will be populated dynamically -->
</select> </select>
<div class="error-message" id="vaForTipError"></div> <div class="error-message" id="vaForTipError"></div>
@ -184,12 +169,12 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Tip Description</label> <label class="form-label">Tip Description</label>
<textarea required="true" name="vaTipDescription" class="form-input" id="vaTipDescription"></textarea> <textarea name="vaTipDescription" class="form-input" id="vaTipDescription"></textarea>
<div class="error-message" id="vaTipDescriptionError"></div> <div class="error-message" id="vaTipDescriptionError"></div>
</div> </div>
<div class="modal-actions"> <div class="modal-actions">
<button type="button" class="cancel-btn" onclick="closeTipModal()">Cancel</button> <button type="button" class="cancel-btn" onclick="closeTipModal()">Cancel</button>
<button type="submit" class="save-btn" id="submitTipBtn">Submit Tip</button> <button type="submit" class="save-btn" id="savePasswordBtn">Submit Tip</button>
</div> </div>
</form> </form>
</div> </div>
@ -209,8 +194,7 @@
<i class="fas fa-sign-out-alt"></i> <i class="fas fa-sign-out-alt"></i>
</div> </div>
<h3 class="logout-title">Are you sure you want to logout?</h3> <h3 class="logout-title">Are you sure you want to logout?</h3>
<p class="logout-message">You will be logged out of your account and redirected to the login page. <p class="logout-message">You will be logged out of your account and redirected to the login page.</p>
</p>
<div class="logout-actions"> <div class="logout-actions">
<button class="logout-cancel-btn" onclick="closeLogoutModal()">Cancel</button> <button class="logout-cancel-btn" onclick="closeLogoutModal()">Cancel</button>
<button class="logout-confirm-btn" id="confirmLogoutBtn">Yes, Logout</button> <button class="logout-confirm-btn" id="confirmLogoutBtn">Yes, Logout</button>
@ -219,330 +203,148 @@
</div> </div>
</div> </div>
</div> </div>
<script> <script>
// Theme management // Theme management
function setTheme(theme) { function setTheme(theme) {
document.documentElement.setAttribute('data-theme', theme); document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme); localStorage.setItem('theme', theme);
}
function getSystemTheme() {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
// Initialize theme
const savedTheme = getSystemTheme();
setTheme(savedTheme);
// API base URL
const API_BASE = '/api/v1/client';
// Auth token management
function getAuthToken() {
return localStorage.getItem('knoptpq');
}
async function fetchWithAuth(url, options = {}) {
const token = getAuthToken();
if (!token) {
window.location.href = '/client/login';
return;
} }
function getSystemTheme() { const response = await fetch(url, {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; ...options,
} headers: {
'token': `${token}`,
// Initialize theme 'Content-Type': 'application/json',
const savedTheme = getSystemTheme(); ...options.headers,
setTheme(savedTheme);
// API base URL
const API_BASE = '/api/v1/client';
// Auth token management
function getAuthToken() {
return localStorage.getItem('knoptpq');
}
async function fetchWithAuth(url, options = {}) {
const token = getAuthToken();
if (!token) {
window.location.href = '/';
return;
} }
const response = await fetch(url, {
...options,
headers: {
'token': `${token}`,
'type': 'client',
'Content-Type': 'application/json',
'ngrok-skip-browser-warning': 69420,
...options.headers,
}
});
if (response.status === 401) {
localStorage.removeItem('knoptpq');
localStorage.removeItem('refreshToken');
window.location.href = '/';
return;
}
return response;
}
async function handleLogout() {
try {
// Call logout API
await fetchWithAuth(`${API_BASE}/logout`, {
method: 'POST'
});
} catch (error) {
console.error('Logout API error:', error);
} finally {
// Clear local storage and redirect
localStorage.removeItem('knoptpq');
localStorage.removeItem('refreshToken');
window.location.href = '/';
}
}
// Logout Modal Functions
function openLogoutModal() {
const modal = document.getElementById('logoutModal');
modal.style.display = 'flex';
document.body.style.overflow = 'hidden';
}
function closeLogoutModal() {
const modal = document.getElementById('logoutModal');
modal.style.display = 'none';
document.body.style.overflow = 'auto';
}
function openTipModal() {
const modal = document.getElementById('tipModal');
modal.style.display = 'flex';
document.body.style.overflow = 'hidden';
// Clear form
document.getElementById('tipForm').reset();
clearErrorMessages();
}
function closeTipModal() {
const modal = document.getElementById('tipModal');
modal.style.display = 'none';
document.body.style.overflow = 'auto';
// Clear form and errors
document.getElementById('tipForm').reset();
clearErrorMessages();
}
function clearErrorMessages() {
document.getElementById('vaForTipError').textContent = '';
document.getElementById('vaTipAmountError').textContent = '';
document.getElementById('vaTipDescriptionError').textContent = '';
}
document.addEventListener('DOMContentLoaded', function () {
// Mobile menu toggle
const menuToggle = document.querySelector('.mobile-menu-button');
const sidebar = document.querySelector('.dashboard-sidebar');
menuToggle.addEventListener('click', () => {
sidebar.classList.toggle('active');
});
document.getElementById('tipVaAddBtn').addEventListener('click', openTipModal);
document.getElementById('tipModal').addEventListener('click', function (e) {
if (e.target === this) {
closeTipModal();
}
});
// Close modal with escape key
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') {
closeLogoutModal();
closeTipModal();
}
});
document.getElementById('logoutModal').addEventListener('click', function (e) {
if (e.target === this) {
closeLogoutModal();
}
});
// Logout button - opens confirmation modal instead of directly logging out
document.getElementById('logoutButton').addEventListener('click', openLogoutModal);
// Confirm logout button
document.getElementById('confirmLogoutBtn').addEventListener('click', handleLogout);
}); });
// ------------------------------ if (response.status === 401) {
// TIP FORM SUBMISSION HANDLER localStorage.removeItem('knoptpq');
// ------------------------------ localStorage.removeItem('refreshToken');
window.location.href = '/client/login';
return;
}
document.getElementById('tipForm').addEventListener('submit', async function (e) { return response;
e.preventDefault(); }
clearErrorMessages(); async function handleLogout() {
try {
// Call logout API
await fetchWithAuth(`${API_BASE}/logout`, {
method: 'POST'
});
} catch (error) {
console.error('Logout API error:', error);
} finally {
// Clear local storage and redirect
localStorage.removeItem('knoptpq');
localStorage.removeItem('refreshToken');
window.location.href = '/client/login';
}
}
const vaId = document.getElementById('vaForTip').value; // Logout Modal Functions
const amount = document.getElementById('vaTipAmount').value.trim(); function openLogoutModal() {
const description = document.getElementById('vaTipDescription').value.trim(); const modal = document.getElementById('logoutModal');
modal.style.display = 'flex';
document.body.style.overflow = 'hidden';
}
let isValid = true; function closeLogoutModal() {
const modal = document.getElementById('logoutModal');
modal.style.display = 'none';
document.body.style.overflow = 'auto';
}
// Validation function openTipModal() {
if (!vaId) { const modal = document.getElementById('tipModal');
document.getElementById('vaForTipError').textContent = "Please select a virtual assistant."; modal.style.display = 'flex';
isValid = false; document.body.style.overflow = 'hidden';
}
if (!amount || isNaN(amount) || Number(amount) <= 0) { // Clear form
document.getElementById('vaTipAmountError').textContent = "Enter a valid tip amount."; document.getElementById('tipForm').reset();
isValid = false; clearErrorMessages();
} }
if (!isValid) return; function closeTipModal() {
const modal = document.getElementById('tipModal');
modal.style.display = 'none';
document.body.style.overflow = 'auto';
// Prepare API body // Clear form and errors
const payload = { document.getElementById('tipForm').reset();
vaId, clearErrorMessages();
amount: Number(amount), }
description
};
try { function clearErrorMessages() {
document.getElementById('submitTipBtn').disabled = true; document.getElementById('vaForTipError').textContent = '';
document.getElementById('submitTipBtn').textContent = "Submitting..."; document.getElementById('vaTipAmountError').textContent = '';
document.getElementById('vaTipDescriptionError').textContent = '';
}
const response = await fetchWithAuth(`/api/v1/client/tips`, { document.addEventListener('DOMContentLoaded', function() {
method: "POST",
body: JSON.stringify(payload)
});
const data = await response.json(); // Mobile menu toggle
const menuToggle = document.querySelector('.mobile-menu-button');
const sidebar = document.querySelector('.dashboard-sidebar');
menuToggle.addEventListener('click', () => {
sidebar.classList.toggle('active');
});
if (!response.ok) { document.getElementById('tipVaAddBtn').addEventListener('click', openTipModal);
alert(data.message || "Failed to submit tip.");
return;
}
alert("Tip submitted successfully!"); document.getElementById('tipModal').addEventListener('click', function(e) {
if (e.target === this) {
closeTipModal(); closeTipModal();
// Optionally refresh page or reload tips list
window.location.reload();
} catch (err) {
console.error("Tip submit error:", err);
alert("Something went wrong while submitting tip.");
} finally {
document.getElementById('submitTipBtn').disabled = false;
document.getElementById('submitTipBtn').textContent = "Submit Tip";
} }
}); });
// Close modal with escape key
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('keydown', function(e) {
// Load VAs for tip selection if (e.key === 'Escape') {
async function loadVAs() { closeLogoutModal();
try { closeTipModal();
const response = await fetchWithAuth(`${API_BASE}/dashboard`);
const data = await response.json();
if (!response.ok) {
console.error("Failed to load VAs:", data.message);
return;
}
const vaSelect = document.getElementById('vaForTip');
data.data.allVAs.forEach(va => {
const option = document.createElement('option');
option.value = va.id;
option.textContent = va.name;
vaSelect.appendChild(option);
});
} catch (error) {
console.error("Error loading VAs:", error);
}
} }
loadVAs();
}); });
document.addEventListener('DOMContentLoaded', function () { document.getElementById('logoutModal').addEventListener('click', function(e) {
// Load VAs for tip selection if (e.target === this) {
async function loadTips() { closeLogoutModal();
try {
const response = await fetchWithAuth(`${API_BASE}/tips`);
if (!response.ok) {
console.error("Failed to load VAs:", data.message);
return;
}
const data = await response.json();
const vaSelect = document.getElementById('vaTipTableBody');
data.data.forEach(tip => {
const row = document.createElement('tr');
const cellVoid = document.createElement('td');
const cellStatus = document.createElement('td');
const cellName = document.createElement('td');
const cellDescription = document.createElement('td');
const cellDate = document.createElement('td');
const cellAmount = document.createElement('td');
cellVoid.innerHTML = `<button id='voidBtn' class='void-btn-a' data-id='${tip._id}'>Void</button>`;
cellVoid.classList.add('void-btn');
cellStatus.textContent = tip.status;
cellName.textContent = tip.vaName;
cellDescription.textContent = tip.description;
cellDate.textContent = tip.createdAt ? new Date(tip.createdAt).toLocaleDateString() : '';
cellAmount.textContent = `$${parseFloat(tip.amount).toFixed(2)}`;
row.appendChild(cellVoid);
row.appendChild(cellStatus);
row.appendChild(cellName);
row.appendChild(cellDescription);
row.appendChild(cellDate);
row.appendChild(cellAmount);
vaSelect.appendChild(row);
});
document.querySelectorAll('.void-btn-a').forEach(button => {
button.addEventListener('click', async function () {
const tipId = this.getAttribute('data-id');
if (confirm('Are you sure you want to void this tip?')) {
try {
const response = await fetchWithAuth(`${API_BASE}/tips/${tipId}`, {
method: 'DELETE'
});
const result = await response.json();
if (!response.ok) {
alert(result.message || 'Failed to void tip.');
return;
}
alert('Tip voided successfully!');
window.location.reload();
} catch (error) {
console.error('Error voiding tip:', error);
alert('Something went wrong while voiding the tip.');
}
}
});
});
} catch (error) {
console.error("Error loading VAs:", error);
}
} }
loadTips();
}); });
// Logout button - opens confirmation modal instead of directly logging out
document.getElementById('logoutButton').addEventListener('click', openLogoutModal);
// Confirm logout button
document.getElementById('confirmLogoutBtn').addEventListener('click', handleLogout);
});
</script>
</script>
</body> </body>
</html> </html>

View File

@ -234,7 +234,7 @@
async function fetchWithAuth(url, options = {}) { async function fetchWithAuth(url, options = {}) {
const token = getAuthToken(); const token = getAuthToken();
if (!token) { if (!token) {
window.location.href = '/'; window.location.href = '/client/login';
return; return;
} }
@ -250,7 +250,7 @@
if (response.status === 401) { if (response.status === 401) {
localStorage.removeItem('knoptpq'); localStorage.removeItem('knoptpq');
localStorage.removeItem('refreshToken'); localStorage.removeItem('refreshToken');
window.location.href = '/'; window.location.href = '/client/login';
return; return;
} }
@ -265,7 +265,7 @@
} finally { } finally {
localStorage.removeItem('knoptpq'); localStorage.removeItem('knoptpq');
localStorage.removeItem('refreshToken'); localStorage.removeItem('refreshToken');
window.location.href = '/'; window.location.href = '/client/login';
} }
} }

View File

@ -216,7 +216,7 @@
async function fetchWithAuth(url, options = {}) { async function fetchWithAuth(url, options = {}) {
const token = getAuthToken(); const token = getAuthToken();
if (!token) { if (!token) {
window.location.href = '/'; window.location.href = '/client/login';
return; return;
} }
@ -232,7 +232,7 @@
if (response.status === 401) { if (response.status === 401) {
localStorage.removeItem('knoptpq'); localStorage.removeItem('knoptpq');
localStorage.removeItem('refreshToken'); localStorage.removeItem('refreshToken');
window.location.href = '/'; window.location.href = '/client/login';
return; return;
} }
@ -251,7 +251,7 @@
// Clear local storage and redirect // Clear local storage and redirect
localStorage.removeItem('knoptpq'); localStorage.removeItem('knoptpq');
localStorage.removeItem('refreshToken'); localStorage.removeItem('refreshToken');
window.location.href = '/'; window.location.href = '/client/login';
} }
} }

View File

@ -1,6 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" data-theme="light"> <html lang="en" data-theme="light">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
@ -8,7 +7,6 @@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<link rel="stylesheet" href="/public/css/dashboard.css"> <link rel="stylesheet" href="/public/css/dashboard.css">
</head> </head>
<body class="dashboard-body"> <body class="dashboard-body">
<div id="root"> <div id="root">
<nav class="dashboard-header"> <nav class="dashboard-header">
@ -124,8 +122,7 @@
<!-- Virtual Assistants List --> <!-- Virtual Assistants List -->
<div class="content-section"> <div class="content-section">
<div class="section-header" style="justify-content: space-between;align-items: center;"> <div class="section-header" style="justify-content: space-between;align-items: center;">
<h2 class="section-title">Virtual Assistants <span class="total-count" id="vaListCount">0 <h2 class="section-title">Virtual Assistants <span class="total-count" id="vaListCount">0 VAs</span></h2>
VAs</span></h2>
<a href="/client/virtual-assistants/manage-tips" class="btn btn-primary">Manage Tips</a> <a href="/client/virtual-assistants/manage-tips" class="btn btn-primary">Manage Tips</a>
</div> </div>
<div class="table-container"> <div class="table-container">
@ -158,8 +155,6 @@
</div> </div>
</main> </main>
</div> </div>
<!-- VA Modal Start -->
<div id="vaModal" class="modal-overlay"> <div id="vaModal" class="modal-overlay">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header" style="display: flex;align-items: center;justify-content: space-between;"> <div class="modal-header" style="display: flex;align-items: center;justify-content: space-between;">
@ -209,62 +204,6 @@
</div> </div>
</div> </div>
</div> </div>
<!-- VA Modal Start -->
<!-- Log Modal Start -->
<div id="vaModal" class="modal-overlay">
<div class="modal-content">
<div class="modal-header" style="display: flex;align-items: center;justify-content: space-between;">
<h2 class="modal-title" id="modalVAName">VA Details</h2>
<button class="modal-close" onclick="closeVAModal()">
<i class="fas fa-times"></i>
</button>
</div>
<div class="modal-body">
<!-- Projects Section -->
<div class="va-details-section">
<!-- <h3 class="va-section-title">
<i class="fas fa-briefcase"></i>
Projects
<span class="va-count-badge" id="projectsCountBadge">0</span>
</h3> -->
<div class="va-table-container">
<table class="va-table">
<thead>
<tr>
<th>#</th>
<th>Title</th>
<th>Time</th>
<th>Duration</th>
</tr>
</thead>
<tbody id="modalProjectsTableBody">
<!-- Project details will be loaded here -->
</tbody>
</table>
<div id="modalNoProjects" class="no-vas-message" style="display: none;">
<i class="fas fa-briefcase"></i>
<h4>No Logs Found</h4>
</div>
</div>
</div>
<!-- Action Button -->
<div class="modal-actions" id="modalActions" style="display: none;">
<a href="#" class="view-session-btn" id="viewSessionBtn" target="_blank">
<i class="fas fa-eye"></i>
View Live Session
</a>
</div>
</div>
</div>
</div>
<!-- Log Modal End -->
<div id="logoutModal" class="modal-overlay"> <div id="logoutModal" class="modal-overlay">
<div class="modal-content"> <div class="modal-content">
@ -279,8 +218,7 @@
<i class="fas fa-sign-out-alt"></i> <i class="fas fa-sign-out-alt"></i>
</div> </div>
<h3 class="logout-title">Are you sure you want to logout?</h3> <h3 class="logout-title">Are you sure you want to logout?</h3>
<p class="logout-message">You will be logged out of your account and redirected to the login page. <p class="logout-message">You will be logged out of your account and redirected to the login page.</p>
</p>
<div class="logout-actions"> <div class="logout-actions">
<button class="logout-cancel-btn" onclick="closeLogoutModal()">Cancel</button> <button class="logout-cancel-btn" onclick="closeLogoutModal()">Cancel</button>
<button class="logout-confirm-btn" id="confirmLogoutBtn">Yes, Logout</button> <button class="logout-confirm-btn" id="confirmLogoutBtn">Yes, Logout</button>
@ -289,137 +227,137 @@
</div> </div>
</div> </div>
</div> </div>
<script> <script>
// Theme management // Theme management
function setTheme(theme) { function setTheme(theme) {
document.documentElement.setAttribute('data-theme', theme); document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme); localStorage.setItem('theme', theme);
}
function getSystemTheme() {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
// Initialize theme
const savedTheme = getSystemTheme();
setTheme(savedTheme);
// API base URL
const API_BASE = '/api/v1/client';
// Auth token management
function getAuthToken() {
return localStorage.getItem('knoptpq');
}
async function fetchWithAuth(url, options = {}) {
const token = getAuthToken();
if (!token) {
window.location.href = '/client/login';
return;
} }
function getSystemTheme() { const response = await fetch(url, {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; ...options,
} headers: {
'token': `${token}`,
// Initialize theme 'Content-Type': 'application/json',
const savedTheme = getSystemTheme(); ...options.headers,
setTheme(savedTheme);
// API base URL
const API_BASE = '/api/v1/client';
// Auth token management
function getAuthToken() {
return localStorage.getItem('knoptpq');
}
async function fetchWithAuth(url, options = {}) {
const token = getAuthToken();
if (!token) {
window.location.href = '/';
return;
} }
});
const response = await fetch(url, { if (response.status === 401) {
...options, localStorage.removeItem('knoptpq');
headers: { localStorage.removeItem('refreshToken');
'token': `${token}`, window.location.href = '/client/login';
'Content-Type': 'application/json', return;
...options.headers, }
}
return response;
}
async function handleLogout() {
try {
// Call logout API
await fetchWithAuth(`${API_BASE}/logout`, {
method: 'POST'
}); });
} catch (error) {
console.error('Logout API error:', error);
} finally {
// Clear local storage and redirect
localStorage.removeItem('knoptpq');
localStorage.removeItem('refreshToken');
window.location.href = '/client/login';
}
}
if (response.status === 401) { // Format time function
localStorage.removeItem('knoptpq'); function formatTime(seconds) {
localStorage.removeItem('refreshToken'); if (!seconds) return 'No Record Found';
window.location.href = '/';
return;
}
return response; const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (hours > 0) {
return `${hours}h ${minutes}m`;
} else if (minutes > 0) {
return `${minutes}m`;
} else {
return 'Just started';
}
}
const vaModal = document.getElementById('vaModal');
function openVAModal(va) {
// Set basic VA info
document.getElementById('modalVAName').textContent = va.name;
// Set projects
const projects = va.projects || [];
document.getElementById('projectsCountBadge').textContent = projects.length;
const projectsTableBody = document.getElementById('modalProjectsTableBody');
const noProjectsMessage = document.getElementById('modalNoProjects');
if (projects.length > 0) {
projectsTableBody.innerHTML = projects.map(project => createProjectTableRow(project)).join('');
projectsTableBody.style.display = '';
noProjectsMessage.style.display = 'none';
} else {
projectsTableBody.innerHTML = '';
projectsTableBody.style.display = 'none';
noProjectsMessage.style.display = 'block';
} }
async function handleLogout() { // Set action button
try { const modalActions = document.getElementById('modalActions');
// Call logout API const viewSessionBtn = document.getElementById('viewSessionBtn');
await fetchWithAuth(`${API_BASE}/logout`, {
method: 'POST' if (va.isCurrentlyActive && va.lastProjectId) {
}); viewSessionBtn.href = `/live/${va.lastProjectId}/${va.id}`;
} catch (error) { modalActions.style.display = 'block';
console.error('Logout API error:', error); } else {
} finally { modalActions.style.display = 'none';
// Clear local storage and redirect
localStorage.removeItem('knoptpq');
localStorage.removeItem('refreshToken');
window.location.href = '/';
}
} }
// Format time function // Show modal
function formatTime(seconds) { vaModal.style.display = 'flex';
if (!seconds) return '-'; document.body.style.overflow = 'hidden';
}
const hours = Math.floor(seconds / 3600); function closeVAModal() {
const minutes = Math.floor((seconds % 3600) / 60); vaModal.style.display = 'none';
document.body.style.overflow = 'auto';
}
if (hours > 0) { // Create project table row for modal
return `${hours}h ${minutes}m`; function createProjectTableRow(project) {
} else if (minutes > 0) { const statusClass = project.isCurrentlyWorking ? 'active' : 'inactive';
return `${minutes}m`; const statusText = project.isCurrentlyWorking ? 'Working' : 'Not Active';
} else {
return 'Just started';
}
}
const vaModal = document.getElementById('vaModal'); return `
function openVAModal(va) {
// Set basic VA info
document.getElementById('modalVAName').textContent = va.name;
// Set projects
const projects = va.projects || [];
document.getElementById('projectsCountBadge').textContent = projects.length;
const projectsTableBody = document.getElementById('modalProjectsTableBody');
const noProjectsMessage = document.getElementById('modalNoProjects');
if (projects.length > 0) {
projectsTableBody.innerHTML = projects.map(project => createProjectTableRow(project)).join('');
projectsTableBody.style.display = '';
noProjectsMessage.style.display = 'none';
} else {
projectsTableBody.innerHTML = '';
projectsTableBody.style.display = 'none';
noProjectsMessage.style.display = 'block';
}
// Set action button
const modalActions = document.getElementById('modalActions');
const viewSessionBtn = document.getElementById('viewSessionBtn');
if (va.isCurrentlyActive && va.lastProjectId) {
viewSessionBtn.href = `/live/${va.lastProjectId}/${va.id}`;
modalActions.style.display = 'block';
} else {
modalActions.style.display = 'none';
}
// Show modal
vaModal.style.display = 'flex';
document.body.style.overflow = 'hidden';
}
function closeVAModal() {
vaModal.style.display = 'none';
document.body.style.overflow = 'auto';
}
// Create project table row for modal
function createProjectTableRow(project) {
const statusClass = project.isCurrentlyWorking ? 'active' : 'inactive';
const statusText = project.isCurrentlyWorking ? 'Working' : 'Not Active';
return `
<tr> <tr>
<td> <td>
<div class="project-info"> <div class="project-info">
@ -439,18 +377,18 @@
</td> </td>
</tr> </tr>
`; `;
} }
// Create VA table row for main table // Create VA table row for main table
function createVATableRow(va) { function createVATableRow(va) {
const statusClass = va.isCurrentlyActive ? 'active' : 'inactive'; const statusClass = va.isCurrentlyActive ? 'active' : 'inactive';
const statusText = va.isCurrentlyActive ? 'Active' : 'Inactive'; const statusText = va.isCurrentlyActive ? 'Active' : 'Inactive';
const activeProject = va?.lastProject?.name ? va.lastProject.name : '-'; const activeProject = va?.lastProject?.name ? va.lastProject.name : '-';
const workedTime = formatTime(va.currentSessionDuration); const workedTime = formatTime(va.currentSessionDuration);
const projectsAssigned = va.projectsAssigned || 0; const projectsAssigned = va.projectsAssigned || 0;
const viewLink = va.isCurrentlyActive ? `<a href="/live/${va.lastProjectId}/${va.id}" class="view-link va-action" data-va-id="${va.id}" target="_blank">Live Tracking</a>` : '<p class="va-logs va-action" data-va-id="${va.id}" target="_blank">Logs</p>'; const viewLink = va.isCurrentlyActive ? `<a href="/live/${va.lastProjectId}/${va.id}" class="view-link" data-va-id="${va.id}" target="_blank">Live Tracking</a>` : '';
return ` return `
<tr class="va-row" data-va-id="${va.id}"> <tr class="va-row" data-va-id="${va.id}">
<td> <td>
<div class="status-indicator"> <div class="status-indicator">
@ -467,7 +405,7 @@
<span class="projects-count">${activeProject}</span> <span class="projects-count">${activeProject}</span>
</td> </td>
<td> <td>
<span class="projects-count">${projectsAssigned}</span> <span class="projects-count">${projectsAssigned} Projects</span>
</td> </td>
<td> <td>
<span class="worked-time">${workedTime}</span> <span class="worked-time">${workedTime}</span>
@ -477,161 +415,144 @@
</td> </td>
</tr> </tr>
`; `;
} }
// Load dashboard data // Load dashboard data
async function loadDashboardData() { async function loadDashboardData() {
try { try {
const response = await fetchWithAuth(`${API_BASE}/dashboard`); const response = await fetchWithAuth(`${API_BASE}/dashboard`);
if (!response) return; if (!response) return;
const result = await response.json(); const result = await response.json();
if (result.success) { if (result.success) {
const data = result.data; const data = result.data;
const summary = result.summary; const summary = result.summary;
// Update stats cards // Update stats cards
document.getElementById('activeVaCount').textContent = summary.activeVAs || 0; document.getElementById('activeVaCount').textContent = summary.activeVAs || 0;
document.getElementById('totalVaCount').textContent = summary.totalVAs || 0; document.getElementById('totalVaCount').textContent = summary.totalVAs || 0;
document.getElementById('totalProjects').textContent = summary.totalProjects || 0; document.getElementById('totalProjects').textContent = summary.totalProjects || 0;
// Update VA list count // Update VA list count
document.getElementById('vaListCount').textContent = `${summary.totalVAs || 0} VAs`; document.getElementById('vaListCount').textContent = `${summary.totalVAs || 0} VAs`;
// Update VA table // Update VA table
const vaTableBody = document.getElementById('vaTableBody'); const vaTableBody = document.getElementById('vaTableBody');
const vaEmptyState = document.getElementById('vaEmptyState'); const vaEmptyState = document.getElementById('vaEmptyState');
const allVAs = data.allVAs || []; const allVAs = data.allVAs || [];
if (allVAs.length > 0) { if (allVAs.length > 0) {
vaTableBody.innerHTML = allVAs.map(va => createVATableRow(va)).join(''); vaTableBody.innerHTML = allVAs.map(va => createVATableRow(va)).join('');
vaTableBody.style.display = ''; vaTableBody.style.display = '';
vaEmptyState.style.display = 'none'; vaEmptyState.style.display = 'none';
// Store VA data globally for modal access // Store VA data globally for modal access
window.vaData = allVAs; window.vaData = allVAs;
// Add click event listeners to VA rows // Add click event listeners to VA rows
document.querySelectorAll('.va-info').forEach((row, index) => { document.querySelectorAll('.va-row').forEach((row, index) => {
row.addEventListener('click', function (e) { row.addEventListener('click', function(e) {
// Don't open modal if clicking on view link // Don't open modal if clicking on view link
if (!e.target.closest('.view-link')) { if (!e.target.closest('.view-link')) {
const va = allVAs[index]; const va = allVAs[index];
openVAModal(va); openVAModal(va);
} }
});
}); });
} else { });
vaTableBody.innerHTML = '';
vaTableBody.style.display = 'none';
vaEmptyState.style.display = 'block';
}
} else { } else {
console.error('Failed to load dashboard data:', result.message); vaTableBody.innerHTML = '';
showError('Failed to load dashboard data'); vaTableBody.style.display = 'none';
vaEmptyState.style.display = 'block';
} }
} catch (error) { } else {
console.error('Error loading dashboard data:', error); console.error('Failed to load dashboard data:', result.message);
showError('Error loading dashboard data'); showError('Failed to load dashboard data');
} }
} catch (error) {
console.error('Error loading dashboard data:', error);
showError('Error loading dashboard data');
} }
}
// Show error message // Show error message
function showError(message) { function showError(message) {
const vaTableBody = document.getElementById('vaTableBody'); const vaTableBody = document.getElementById('vaTableBody');
const vaEmptyState = document.getElementById('vaEmptyState'); const vaEmptyState = document.getElementById('vaEmptyState');
vaTableBody.style.display = 'none'; vaTableBody.style.display = 'none';
vaEmptyState.innerHTML = ` vaEmptyState.innerHTML = `
<div class="empty-state"> <div class="empty-state">
<i class="fas fa-exclamation-triangle"></i> <i class="fas fa-exclamation-triangle"></i>
<h3>Error</h3> <h3>Error</h3>
<p>${message}</p> <p>${message}</p>
</div> </div>
`; `;
vaEmptyState.style.display = 'block'; vaEmptyState.style.display = 'block';
} }
// Close modal when clicking outside // Close modal when clicking outside
vaModal.addEventListener('click', (e) => { vaModal.addEventListener('click', (e) => {
if (e.target === vaModal) { if (e.target === vaModal) {
closeVAModal(); closeVAModal();
} }
});
// Close modal with escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && vaModal.style.display === 'flex') {
closeVAModal();
}
});
// Optional: Auto-refresh data every 30 seconds
setInterval(loadDashboardData, 30000);
// Logout Modal Functions
function openLogoutModal() {
const modal = document.getElementById('logoutModal');
modal.style.display = 'flex';
document.body.style.overflow = 'hidden';
}
function closeLogoutModal() {
const modal = document.getElementById('logoutModal');
modal.style.display = 'none';
document.body.style.overflow = 'auto';
}
document.addEventListener('DOMContentLoaded', function() {
// Load dashboard data
loadDashboardData();
// Mobile menu toggle
const menuToggle = document.querySelector('.mobile-menu-button');
const sidebar = document.querySelector('.dashboard-sidebar');
menuToggle.addEventListener('click', () => {
sidebar.classList.toggle('active');
}); });
// Close modal with escape key // Close modal with escape key
document.addEventListener('keydown', (e) => { document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && vaModal.style.display === 'flex') { if (e.key === 'Escape') {
closeVAModal(); closeLogoutModal();
} }
}); });
// Optional: Auto-refresh data every 30 seconds document.getElementById('logoutModal').addEventListener('click', function(e) {
setInterval(loadDashboardData, 30000); if (e.target === this) {
closeLogoutModal();
// Logout Modal Functions
function openLogoutModal() {
const modal = document.getElementById('logoutModal');
modal.style.display = 'flex';
document.body.style.overflow = 'hidden';
}
function closeLogoutModal() {
const modal = document.getElementById('logoutModal');
modal.style.display = 'none';
document.body.style.overflow = 'auto';
}
const handleVaLogsClick = (e) => {
e.preventDefault();
cosnole.log("handleVaLogsClick Called")
}
document.addEventListener('DOMContentLoaded', function () {
// Load dashboard data
loadDashboardData();
// Mobile menu toggle
const menuToggle = document.querySelector('.mobile-menu-button');
const sidebar = document.querySelector('.dashboard-sidebar');
menuToggle.addEventListener('click', () => {
sidebar.classList.toggle('active');
});
// Close modal with escape key
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') {
closeLogoutModal();
}
});
document.getElementById('logoutModal').addEventListener('click', function (e) {
if (e.target === this) {
closeLogoutModal();
}
});
// Logout button - opens confirmation modal instead of directly logging out
document.getElementById('logoutButton').addEventListener('click', openLogoutModal);
// Confirm logout button
document.getElementById('confirmLogoutBtn').addEventListener('click', handleLogout);
const allVaLogsBtn = document.querySelectorAll(".va-logs");
for (const va of allVaLogsBtn) {
va.addEventListener("click", handleVaLogsClick)
} }
}); });
// Logout button - opens confirmation modal instead of directly logging out
document.getElementById('logoutButton').addEventListener('click', openLogoutModal);
// Confirm logout button
document.getElementById('confirmLogoutBtn').addEventListener('click', handleLogout);
});
</script> </script>
</body> </body>
</html> </html>

View File

@ -222,7 +222,7 @@
<form> <form>
<input type="email" name="email" placeholder="Enter your email address *" required /> <input type="email" name="email" placeholder="Enter your email address *" required />
<input type="password" name="password" placeholder="Enter your password *" required /> <input type="password" name="password" placeholder="Enter your password *" required />
<a href="/forgot-password" class="forgot">Forgot password?</a> <a href="/client/forgot-password" class="forgot">Forgot password?</a>
<button type="submit" class="btn btn-primary">Log in using password</button> <button type="submit" class="btn btn-primary">Log in using password</button>
@ -282,12 +282,12 @@
} }
if (data.success) { if (data.success) {
if(data.userType === "CLIENT") { // 🟢 Save token
localStorage.setItem("knoptpq", data.accessToken); localStorage.setItem("knoptpq", data.accessToken);
window.location.href = "/client/dashboard"; // localStorage.setItem("refreshToken", data.refreshToken);
} else {
window.location.href = "/download-tracker"; // 🟢 Redirect
} window.location.href = "/client/dashboard";
} else { } else {
alert("No token received. Please try again."); alert("No token received. Please try again.");
} }

View File

@ -122,7 +122,7 @@
<h2>Reset your password</h2> <h2>Reset your password</h2>
<input type="email" placeholder="Enter your email address*" required /> <input type="email" placeholder="Enter your email address*" required />
<button type="submit">Send password reset code</button> <button type="submit">Send password reset code</button>
<a href="/">Go back to login</a> <a href="/client/login">Go back to login</a>
</form> </form>
</div> </div>
@ -181,7 +181,7 @@
if (response.ok) { if (response.ok) {
if(response.success) { if(response.success) {
if(confirm('Please Check your mail inbox for password reset link')) { if(confirm('Please Check your mail inbox for password reset link')) {
window.location.href = "/"; // Redirect to reset password page window.location.href = "/client/login"; // Redirect to reset password page
// console.log(response) // console.log(response)
} }
} else { } else {

View File

@ -3,13 +3,13 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<title>Live Screen Feed</title> <title>Live Screen Feed</title>
<style> <style>
* { * {
box-sizing: border-box; box-sizing: border-box;
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
body { body {
@ -163,7 +163,7 @@
</head> </head>
<body> <body>
<!-- Mobile Toggle Button --> <!-- Mobile Toggle Button -->
<div class="mobile-toggle" id="toggleBtn"> <div class="mobile-toggle" id="toggleBtn">
<span></span> <span></span>
<span></span> <span></span>
@ -207,8 +207,6 @@
clientId: clientId clientId: clientId
}); });
const sidebar = document.getElementById("sidebar"); const sidebar = document.getElementById("sidebar");
const toggleBtn = document.getElementById("toggleBtn"); const toggleBtn = document.getElementById("toggleBtn");
const overlay = document.getElementById("overlay"); const overlay = document.getElementById("overlay");
@ -231,12 +229,8 @@
overlay.classList.remove("show"); overlay.classList.remove("show");
}; };
socket.on("connected", () => {
socket.emit("requestVisibleApps", { token });
});
/* Receive visible apps */ /* Receive visible apps */
socket.on("receiveVisibleApps", ({ apps, vaId: senderVaId }) => { socket.on("receiveVisibleApps", ({apps, vaId: senderVaId}) => {
if (vaId !== senderVaId) return; if (vaId !== senderVaId) return;
appsList.innerHTML = ""; appsList.innerHTML = "";
@ -275,7 +269,7 @@
socket.on("authError", () => { socket.on("authError", () => {
socket.disconnect(); socket.disconnect();
localStorage.removeItem("knoptpq"); localStorage.removeItem("knoptpq");
window.location.href = "/"; window.location.href = "/client/login";
}); });
/* Receive images */ /* Receive images */

View File

@ -11,11 +11,6 @@
select option:disabled { select option:disabled {
display: none; display: none;
} }
select {
background-color: white;
color: #2d2d2d;
}
</style> </style>
</head> </head>