Compare commits

...

25 Commits

Author SHA1 Message Date
Aakash Mittal 4ed62b25e8 !5 dec 2025-12-15 11:50:07 +05:30
=bhagwanSingh 2e7731e704 Adding uv Ip on Listener 2025-12-12 14:21:55 +05:30
=bhagwanSingh 9a811e6f00 Password Reset on register 2025-12-12 12:30:40 +05:30
=bhagwanSingh fc821c70ca Test 2025-12-10 20:26:32 +05:30
=bhagwanSingh 6fd3da0837 Test 2025-12-10 20:24:38 +05:30
=bhagwanSingh 140b669bdb Project not showing Fixed 2025-12-10 14:35:53 +05:30
=bhagwanSingh 0ef8d60060 Bug Resolve 2025-12-09 19:15:26 +05:30
=bhagwanSingh bc98edcb4b a 09-Dec 2025-12-09 07:21:19 +05:30
=bhagwanSingh 00cb793a7c Va and project List API update 2025-12-05 13:29:04 +05:30
=bhagwanSingh 59561f211d VaSlip Update 2025-12-04 13:35:12 +05:30
=bhagwanSingh 4d617d3113 Client and VA Update and VA Tip 2025-12-04 11:46:42 +05:30
=bhagwanSingh 15f0f3d782 Socke, Time and leave adjustment update 2025-12-02 18:45:28 +05:30
=bhagwanSingh 7ac50fe720 Manage Timesheet update 2025-12-02 13:20:04 +05:30
=bhagwanSingh b085a88579 ENUM update 2025-12-02 13:01:09 +05:30
=bhagwanSingh 5ee97a7d44 Test auto deploy - Done 2025-12-02 12:32:58 +05:30
=bhagwanSingh c04264f054 Test auto deploy and VPN 2025-12-02 12:32:00 +05:30
=bhagwanSingh a7d770498c Leave API UCment 2025-12-01 17:25:16 +05:30
=bhagwanSingh 2ca5146846 Merge branch 'backend' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker-middleware - 01-12 2025-12-01 15:49:50 +05:30
=bhagwanSingh 6c8e85336c Test Deploy - Done 2025-12-01 14:04:23 +05:30
=bhagwanSingh 0c578d8121 Test Deploy 2025-12-01 14:02:17 +05:30
=bhagwanSingh d0bb922da1 Server Update 2025-12-01 13:52:55 +05:30
=bhagwanSingh 1baa6da4c7 Merge branch 'backend' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker-middleware - 30-11 2025-11-30 11:32:37 +05:30
=bhagwanSingh 3154a7e0d3 Merge branch 'backend' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker-middleware | 28-11 | 16:21 2025-11-28 16:21:40 +05:30
=bhagwanSingh 142f4f77cd 28-11 2025-11-28 16:19:19 +05:30
=bhagwanSingh c7f44c7754 Revert "Client register error - fix"
This reverts commit 384f75f5ef.
2025-11-24 18:56:44 +05:30
43 changed files with 1473 additions and 821 deletions

0
notePad-wg-cmd.txt Normal file
View File

View File

@ -6,6 +6,7 @@
"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,3 +1689,23 @@ 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 = '/client/login'; window.location.href = '/';
}, 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 } _id: { $in: allProjectVaIds.vaId }
}) })
.select('userDetails.firstName userDetails.lastName userDetails.email status') .select('userDetails.firstName userDetails.lastName userDetails.email status')
.lean() .lean()

View File

@ -2,9 +2,22 @@ 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 ...");
@ -22,6 +35,9 @@ 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,
@ -175,7 +191,7 @@ async function update_client(req, res) {
return res.status(500).json({ return res.status(500).json({
success: false, success: false,
message: "Internal Server Error" message: error.message
}); });
} }
} }

View File

@ -1,6 +1,7 @@
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { ClientModel } from "../../models/vaModel.js"; import { ClientModel, VAModel } 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) {
@ -13,8 +14,17 @@ 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
const user = await ClientModel.findOne({ "userDetails.email": userDetails.email }); let 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" });
} }
@ -49,6 +59,7 @@ 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" }
@ -69,6 +80,7 @@ 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 } }, { _id: { $in: vaAssigned.vaId } },
{ $pull: { projectIds: projectObjectId } } { $pull: { projectIds: projectObjectId } }
); );
deletionSummary.vasUpdated = vaResult.modifiedCount; deletionSummary.vasUpdated = vaResult.modifiedCount;

View File

@ -5,7 +5,7 @@ import { updateEntityRelationships } from "../../helper/updateEntityRelationship
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, isFlexibleAllowedPt, vaWorkingDelayAllowedStart, vaWorkingDelayAllowedEnd } = payload; const { name, hourlyRate, clientId, vaAssigned, dedicatedHours, vaWorkingScheduleStart, vaWorkingScheduleEnd, isFlexibleAllowed, vaWorkingDelayAllowedStart, vaWorkingDelayAllowedEnd } = payload;
// Validate required fields // Validate required fields
if (!name) { if (!name) {
@ -34,46 +34,55 @@ 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 => new Types.ObjectId(vaId)); // vaAssignedArray = vaAssigned.map(vaId => {
} catch (error) { // if(typeof vaId === 'object' && "isFlexibleAllowed" in vaId){
return res.status(400).json({ // // Handle Error form SF Side for now - That vaId is coming as object
success: false, // return new Types.ObjectId(vaId.vaId)
message: "Invalid VA ID format in vaAssigned array" // } else {
}); // return new Types.ObjectId(vaId)
} // }
} else if (typeof vaAssigned === 'string') { // });
// Handle single VA ID as string // } catch (error) {
try { // return res.status(400).json({
vaAssignedArray = [new Types.ObjectId(vaAssigned)]; // success: false,
} catch (error) { // message: "Invalid VA ID format in vaAssigned array"
return res.status(400).json({ // });
success: false, // }
message: "Invalid VA ID format" // } else if (typeof vaAssigned === 'string') {
}); // // 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: vaAssignedArray, vaAssigned: vaAssigned,
dedicatedHours: dedicatedHours ? parseFloat(dedicatedHours) : 0, dedicatedHours: dedicatedHours ? parseFloat(dedicatedHours) : 0,
vaWorkingScheduleStart: vaWorkingScheduleStart || null, vaWorkingScheduleStart: vaWorkingScheduleStart || null,
vaWorkingScheduleEnd: vaWorkingScheduleEnd || null, vaWorkingScheduleEnd: vaWorkingScheduleEnd || null,
isFlexibleAllowedPt: isFlexibleAllowedPt || true, isFlexibleAllowed: isFlexibleAllowed || 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

@ -19,7 +19,7 @@ async function update_project_sf(req, res) {
totalLoggedHours, totalLoggedHours,
vaWorkingScheduleStart, vaWorkingScheduleStart,
vaWorkingScheduleEnd, vaWorkingScheduleEnd,
isFlexibleAllowedPt, isFlexibleAllowed,
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 (isFlexibleAllowedPt !== undefined) { if (isFlexibleAllowed !== undefined) {
updateData.isFlexibleAllowedPt = isFlexibleAllowedPt; updateData.isFlexibleAllowed = isFlexibleAllowed;
} }
if (vaWorkingDelayAllowedStart !== undefined) { if (vaWorkingDelayAllowedStart !== undefined) {
@ -114,15 +114,26 @@ 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 = []; let newVaAssignedArray = updateData.vaAssigned.map(vaDtl => vaDtl.vaId);
// 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 => new Types.ObjectId(vaId)); newVaAssignedArray = vaAssigned.map(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,
@ -141,14 +152,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.toString()); const currentVaIds = existingProject.vaAssigned.map(va => va.vaId.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.toString())); const vaToRemove = existingProject.vaAssigned.filter(va => !newVaIds.includes(va.vaId.toString()));
// Update relationships for added VAs // Update relationships for added VAs
if (vaToAdd.length > 0) { if (vaToAdd.length > 0) {

View File

@ -147,64 +147,41 @@ 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/timesheet-adjustment`; const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/leave-request`;
// const sfData = { const sfData = {
// "id": leaveRequest._id, "leaveId": leaveRequest._id.toString(),
// "va": { // "va": {
// "id": vaId // "id": vaId
// }, // },
// "dateFrom": startDate.toISOString().split('T')[0], "va": vaId.toString(),
// "dateTo": endDate.toISOString().split('T')[0], "dateFrom": startDate.toISOString().split('T')[0],
// "totalDays": totalDays, "dateTo": endDate.toISOString().split('T')[0],
// "leaveType": leaveType, "totalDays": totalDays,
// "comment": processedComment.plainText, // Send plain text to Salesforce "leaveType": leaveType,
// "attachments": processedComment.extractedImages, // Send extracted images info "comment": processedComment.plainText !== "" ? processedComment.plainText : "-", // Send plain text to Salesforce
// "status": "PENDING", "attachments": processedComment.extractedImages, // Send extracted images info
// "rejectionReason": "" "status": "PENDING",
// } "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();
// throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`); await LeavesModel.findByIdAndDelete(leaveRequest._id)
// } 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({
success: true,
message: "Leave request submitted successfully",
data: {
leaveId: leaveRequest._id,
dateFrom: startDate.toISOString().split('T')[0],
dateTo: endDate.toISOString().split('T')[0],
totalDays: totalDays,
leaveType,
comment: processedComment.processedHTML,
// attachments: processedComment.extractedImages,
status: "PENDING"
},
// SyncSalesforce: result
});
} catch (error) {
console.error("Failed to sync with Salesforce:", error.message);
console.log(error)
// Create a custom error to distinguish Salesforce errors
const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
salesforceError.isSalesforceError = true;
throw salesforceError;
}
// return res.status(201).json({ // return res.status(201).json({
// success: true, // success: true,
@ -215,10 +192,35 @@ async function va_leave_apply_function(req, res) {
// dateTo: endDate.toISOString().split('T')[0], // dateTo: endDate.toISOString().split('T')[0],
// totalDays: totalDays, // totalDays: totalDays,
// leaveType, // leaveType,
// comment, // comment: processedComment.processedHTML,
// // attachments: processedComment.extractedImages,
// status: "PENDING" // status: "PENDING"
// } // },
// // SyncSalesforce: result
// }); // });
} catch (error) {
console.error("Failed to sync with Salesforce:", error.message);
console.log(error)
// Create a custom error to distinguish Salesforce errors
const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
salesforceError.isSalesforceError = true;
throw salesforceError;
}
return res.status(201).json({
success: true,
message: "Leave request submitted successfully",
data: {
leaveId: leaveRequest._id,
dateFrom: startDate.toISOString().split('T')[0],
dateTo: endDate.toISOString().split('T')[0],
totalDays: totalDays,
leaveType,
comment,
status: "PENDING"
}
});
} catch (error) { } catch (error) {
console.error("Leave apply error:", error); console.error("Leave apply error:", error);

View File

@ -147,7 +147,12 @@ 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=${vaId}`; const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-payslip/${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 } } { $pull: { "vaAssigned.vaId": 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.sf_token; const sf_token = req.headers["x-auth-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" "paymentMethod", "payoneerAccount", "veemAccount", "wiseEmail"
]; ];
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.isFlexibleAllowedPt) { // if (vaStatus === "PARTTIME" && project.isFlexibleAllowed) {
// 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);
res.status(201).json({ return 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: vaUser._id }); projectList = await ProjectModel.find({ "vaAssigned.vaId": 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.sf_token; const sf_token = req.headers["x-auth-token"];
let vaId; let vaId;
let vaUser; let vaUser;
let vaEmail; let vaEmail;

View File

@ -1,15 +1,24 @@
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.params; const { status, projectId, vaId } = req.query;
// 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 "vaAssigned.vaId": 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, "id": adjustmentRequest._id.toString(),
"va": { "va": {
"id": vaId "id": vaId.toString()
}, },
"project": { "project": {
"id": projectId "id": projectId.toString()
}, },
"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, // 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": "",
@ -167,6 +167,7 @@ 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.sf_token; const sf_token = req.headers["x-auth-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 "vaAssigned.vaId": vaId
}); });
if (!project) { if (!project) {

View File

@ -0,0 +1,143 @@
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.sf_token; const sf_token = req.headers["x-auth-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,6 +50,15 @@ 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,9 +89,16 @@ const projectSchema = new Schema({
}, },
vaAssigned: [ vaAssigned: [
{ {
_id: false,
vaId: {
type: Types.ObjectId, type: Types.ObjectId,
ref: "VA", ref: "VA",
}, },
isFlexibleAllowed: {
type: Boolean,
default: true
}
}
], ],
dedicatedHours: { dedicatedHours: {
type: String, type: String,
@ -110,7 +117,7 @@ const projectSchema = new Schema({
type: String, type: String,
default: null default: null
}, },
isFlexibleAllowedPt: { isFlexibleAllowed: {
type: Boolean, type: Boolean,
default: true default: true
}, },
@ -511,6 +518,37 @@ 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);
@ -520,5 +558,6 @@ 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 }; export { VAModel, ClientModel, ProjectModel, LoggedModel, WorkTrackModel, PasswordResetModel, AdjustedHoursModel, LeavesModel, TipsModel };

View File

@ -13,6 +13,7 @@ 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
@ -22,11 +23,14 @@ 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 vaRoutes.get("/", va_callback); // VA Callback for getting active VA list
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,7 +47,9 @@ 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/:status?/:projectId?/:vaId?", get_adjustment_requests_list); // show all adjustment list to approve and reject vaRoutes.get("/timesheet/adjustments/list/:projectId?/:vaId?/:status?", 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
@ -56,7 +58,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", va_leave_manage_function); vaRoutes.patch("/leave/:leaveId", authValidator, va_leave_manage_function);
// timesheet adjustment routes // timesheet adjustment routes
@ -84,9 +86,6 @@ 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,6 +121,7 @@ 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")
}) })
@ -299,6 +300,8 @@ 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());
@ -319,7 +322,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 }) => { socket.on("register", async ({ role, id, projectId = null, clientId = null }) => {
if (role === "va") { if (role === "va") {
socket.data.role = "va"; socket.data.role = "va";
socket.data.vaId = id; socket.data.vaId = id;
@ -328,23 +331,35 @@ io.on("connection", (socket) => {
if (role === "client") { if (role === "client") {
socket.data.role = "client"; socket.data.role = "client";
socket.data.clientId = id; socket.data.clientId = clientId;
// 🧩 Get allowed VAs for this client // 🧩 Get allowed VAs for this client
const client = await ClientModel.findById(id) const client = await ClientModel.findById(clientId);
.populate("projectIds") // to access their projects
.populate("vaIds"); // optional if you track directly
// Collect all VA IDs from their projects const a = JSON.stringify(client);
console.log(a, "client data for allowed VAs");
if (client.projectIds.length !== 0 && client.projectIds.includes(projectId)) {
const projectVAIds = new Set(); const projectVAIds = new Set();
for (const project of client.projectIds) {
const populated = await ProjectModel.findById(project._id).populate("vaAssigned"); const populated = await ProjectModel.findById(projectId).populate("vaAssigned");
populated.vaAssigned.forEach((va) => projectVAIds.add(va._id.toString())); 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 });
} }
// Store allowed VA IDs in socket memory // Collect all VA IDs from their projects
socket.data.allowedVAs = Array.from(projectVAIds); // const projectVAIds = new Set();
console.log(`Client ${id} can view VAs:`, socket.data.allowedVAs); // for (const project of client.projectIds) {
// const populated = await ProjectModel.findById(project._id).populate("vaAssigned");
// populated.vaAssigned.forEach((va) => projectVAIds.add(va._id.toString()));
// }
// // Store allowed VA IDs in socket memory
// socket.data.allowedVAs = Array.from(projectVAIds);
// console.log(`Client ${clientId} can view VAs:`, socket.data.allowedVAs);
} }
}); });
@ -400,7 +415,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("getVisibleApps", async ({ token, apps }) => { socket.on("requestVisibleApps", async ({ token }) => {
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);
@ -416,6 +431,35 @@ 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 (
@ -431,18 +475,53 @@ 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, () => { server.listen(PORT, "0.0.0.0", () => {
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: "If the email exists, a reset link has been sent" message: "Email sent successfully. Please check your inbox."
}; };
} catch (error) { } catch (error) {

View File

@ -0,0 +1,5 @@
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 = '/client/login'; loginUrl = '/';
} }
%> %>
<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 = '/client/login'; backToLoginUrl = '/';
} }
%> %>
<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 = '/client/login'; loginUrl = '/';
} }
%> %>
<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 = '/client/login'; window.location.href = '/';
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 = '/client/login'; window.location.href = '/';
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 = '/client/login'; window.location.href = '/';
} }
} }

View File

@ -1,5 +1,6 @@
<!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">
@ -7,6 +8,21 @@
<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">
@ -155,9 +171,8 @@
<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 select-field" id="vaForTip" required> <select name="vaForTip" class="form-input" 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>
@ -169,12 +184,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 name="vaTipDescription" class="form-input" id="vaTipDescription"></textarea> <textarea required="true" 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="savePasswordBtn">Submit Tip</button> <button type="submit" class="save-btn" id="submitTipBtn">Submit Tip</button>
</div> </div>
</form> </form>
</div> </div>
@ -194,7 +209,8 @@
<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> <p class="logout-message">You will be logged out of your account and redirected to the login page.
</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>
@ -229,7 +245,7 @@
async function fetchWithAuth(url, options = {}) { async function fetchWithAuth(url, options = {}) {
const token = getAuthToken(); const token = getAuthToken();
if (!token) { if (!token) {
window.location.href = '/client/login'; window.location.href = '/';
return; return;
} }
@ -237,7 +253,9 @@
...options, ...options,
headers: { headers: {
'token': `${token}`, 'token': `${token}`,
'type': 'client',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'ngrok-skip-browser-warning': 69420,
...options.headers, ...options.headers,
} }
}); });
@ -245,7 +263,7 @@
if (response.status === 401) { if (response.status === 401) {
localStorage.removeItem('knoptpq'); localStorage.removeItem('knoptpq');
localStorage.removeItem('refreshToken'); localStorage.removeItem('refreshToken');
window.location.href = '/client/login'; window.location.href = '/';
return; return;
} }
@ -264,7 +282,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 = '/client/login'; window.location.href = '/';
} }
} }
@ -345,6 +363,186 @@
document.getElementById('confirmLogoutBtn').addEventListener('click', handleLogout); document.getElementById('confirmLogoutBtn').addEventListener('click', handleLogout);
}); });
// ------------------------------
// TIP FORM SUBMISSION HANDLER
// ------------------------------
document.getElementById('tipForm').addEventListener('submit', async function (e) {
e.preventDefault();
clearErrorMessages();
const vaId = document.getElementById('vaForTip').value;
const amount = document.getElementById('vaTipAmount').value.trim();
const description = document.getElementById('vaTipDescription').value.trim();
let isValid = true;
// Validation
if (!vaId) {
document.getElementById('vaForTipError').textContent = "Please select a virtual assistant.";
isValid = false;
}
if (!amount || isNaN(amount) || Number(amount) <= 0) {
document.getElementById('vaTipAmountError').textContent = "Enter a valid tip amount.";
isValid = false;
}
if (!isValid) return;
// Prepare API body
const payload = {
vaId,
amount: Number(amount),
description
};
try {
document.getElementById('submitTipBtn').disabled = true;
document.getElementById('submitTipBtn').textContent = "Submitting...";
const response = await fetchWithAuth(`/api/v1/client/tips`, {
method: "POST",
body: JSON.stringify(payload)
});
const data = await response.json();
if (!response.ok) {
alert(data.message || "Failed to submit tip.");
return;
}
alert("Tip submitted successfully!");
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";
}
});
document.addEventListener('DOMContentLoaded', function () {
// Load VAs for tip selection
async function loadVAs() {
try {
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 () {
// Load VAs for tip selection
async function loadTips() {
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();
});
</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 = '/client/login'; window.location.href = '/';
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 = '/client/login'; window.location.href = '/';
return; return;
} }
@ -265,7 +265,7 @@
} finally { } finally {
localStorage.removeItem('knoptpq'); localStorage.removeItem('knoptpq');
localStorage.removeItem('refreshToken'); localStorage.removeItem('refreshToken');
window.location.href = '/client/login'; window.location.href = '/';
} }
} }

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 = '/client/login'; window.location.href = '/';
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 = '/client/login'; window.location.href = '/';
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 = '/client/login'; window.location.href = '/';
} }
} }

View File

@ -1,5 +1,6 @@
<!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">
@ -7,6 +8,7 @@
<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">
@ -122,7 +124,8 @@
<!-- 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 VAs</span></h2> <h2 class="section-title">Virtual Assistants <span class="total-count" id="vaListCount">0
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">
@ -155,6 +158,8 @@
</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;">
@ -204,6 +209,62 @@
</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">
@ -218,7 +279,8 @@
<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> <p class="logout-message">You will be logged out of your account and redirected to the login page.
</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>
@ -253,7 +315,7 @@
async function fetchWithAuth(url, options = {}) { async function fetchWithAuth(url, options = {}) {
const token = getAuthToken(); const token = getAuthToken();
if (!token) { if (!token) {
window.location.href = '/client/login'; window.location.href = '/';
return; return;
} }
@ -269,7 +331,7 @@
if (response.status === 401) { if (response.status === 401) {
localStorage.removeItem('knoptpq'); localStorage.removeItem('knoptpq');
localStorage.removeItem('refreshToken'); localStorage.removeItem('refreshToken');
window.location.href = '/client/login'; window.location.href = '/';
return; return;
} }
@ -288,13 +350,13 @@
// 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 = '/client/login'; window.location.href = '/';
} }
} }
// Format time function // Format time function
function formatTime(seconds) { function formatTime(seconds) {
if (!seconds) return 'No Record Found'; if (!seconds) return '-';
const hours = Math.floor(seconds / 3600); const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60); const minutes = Math.floor((seconds % 3600) / 60);
@ -386,7 +448,7 @@
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" data-va-id="${va.id}" target="_blank">Live Tracking</a>` : ''; 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>';
return ` return `
<tr class="va-row" data-va-id="${va.id}"> <tr class="va-row" data-va-id="${va.id}">
@ -405,7 +467,7 @@
<span class="projects-count">${activeProject}</span> <span class="projects-count">${activeProject}</span>
</td> </td>
<td> <td>
<span class="projects-count">${projectsAssigned} Projects</span> <span class="projects-count">${projectsAssigned}</span>
</td> </td>
<td> <td>
<span class="worked-time">${workedTime}</span> <span class="worked-time">${workedTime}</span>
@ -450,7 +512,7 @@
window.vaData = allVAs; window.vaData = allVAs;
// Add click event listeners to VA rows // Add click event listeners to VA rows
document.querySelectorAll('.va-row').forEach((row, index) => { document.querySelectorAll('.va-info').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')) {
@ -522,6 +584,12 @@
document.body.style.overflow = 'auto'; document.body.style.overflow = 'auto';
} }
const handleVaLogsClick = (e) => {
e.preventDefault();
cosnole.log("handleVaLogsClick Called")
}
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
// Load dashboard data // Load dashboard data
loadDashboardData(); loadDashboardData();
@ -551,8 +619,19 @@
// Confirm logout button // Confirm logout button
document.getElementById('confirmLogoutBtn').addEventListener('click', handleLogout); document.getElementById('confirmLogoutBtn').addEventListener('click', handleLogout);
const allVaLogsBtn = document.querySelectorAll(".va-logs");
for (const va of allVaLogsBtn) {
va.addEventListener("click", handleVaLogsClick)
}
}); });
</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="/client/forgot-password" class="forgot">Forgot password?</a> <a href="/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) {
// 🟢 Save token if(data.userType === "CLIENT") {
localStorage.setItem("knoptpq", data.accessToken); localStorage.setItem("knoptpq", data.accessToken);
// localStorage.setItem("refreshToken", data.refreshToken);
// 🟢 Redirect
window.location.href = "/client/dashboard"; window.location.href = "/client/dashboard";
} else {
window.location.href = "/download-tracker";
}
} 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="/client/login">Go back to login</a> <a href="/">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 = "/client/login"; // Redirect to reset password page window.location.href = "/"; // Redirect to reset password page
// console.log(response) // console.log(response)
} }
} else { } else {

View File

@ -207,6 +207,8 @@
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");
@ -229,6 +231,10 @@
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;
@ -269,7 +275,7 @@
socket.on("authError", () => { socket.on("authError", () => {
socket.disconnect(); socket.disconnect();
localStorage.removeItem("knoptpq"); localStorage.removeItem("knoptpq");
window.location.href = "/client/login"; window.location.href = "/";
}); });
/* Receive images */ /* Receive images */

View File

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