Compare commits

..

23 Commits

Author SHA1 Message Date
Aakash Mittal dc085787a3 Fixed issues and bugs 2025-11-21 16:38:31 +05:30
Aakash Mittal bbc9793614 Merge branch 'master' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker-middleware into backend 2025-11-21 15:40:22 +05:30
Aakash Mittal 5eef945f50 Added leave manage route 2025-11-21 15:40:11 +05:30
=bhagwanSingh 0b9d6f801e Bug fixed client & VA not updating on adding project with respected Ids 2025-11-21 15:04:11 +05:30
Aakash Mittal a48a919b41 Merge branch 'master' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker-middleware into backend 2025-11-21 12:56:18 +05:30
Aakash Mittal c1cf57e0f4 added leave route 2025-11-21 12:56:14 +05:30
=bhagwanSingh db51e213f1 Big Fix 2025-11-21 12:21:50 +05:30
=bhagwanSingh d49ba5a8b1 20-11 2025-11-20 12:55:25 +05:30
=bhagwanSingh e1fc8d9d13 Merge branch 'master' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker-middleware 2025-11-20 12:54:28 +05:30
=bhagwanSingh e9a2ace3db scoketUpdate 2025-11-20 12:39:05 +05:30
=bhagwanSingh 36bdc24b54 Client Register code update 2025-11-19 17:21:20 +05:30
=bhagwanSingh acb1061de5 Merge branch 'master' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker-middleware 2025-11-18 11:24:56 +05:30
=bhagwanSingh e9ff5ec612 Updating LiveTracker 2025-11-18 11:16:58 +05:30
=bhagwanSingh 8254fab09a Merge branch 'master' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker-middleware 2025-11-17 15:58:58 +05:30
=bhagwanSingh b2fee65156 a 2025-11-17 15:58:20 +05:30
=bhagwanSingh 7e7a6423b1 Update forgot_password.ejs 2025-11-15 19:18:50 +05:30
=bhagwanSingh 682393a82c Merge branch 'master' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker-middleware 2025-11-15 19:15:30 +05:30
=bhagwanSingh 7fe3836f73 15-11 2025-11-15 19:09:38 +05:30
=bhagwanSingh ea574004ce socket and client login and forgot password update 2025-11-13 17:56:49 +05:30
=bhagwanSingh 79eff1a6e3 Merge branch 'master' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker-middleware 2025-11-12 20:07:48 +05:30
=bhagwanSingh 6358564131 Socket Update and Client Login UI 2025-11-12 20:05:09 +05:30
=bhagwanSingh 47f8d96a02 Merge branch 'master' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker-middleware 2025-11-12 15:39:16 +05:30
=bhagwanSingh 5bfc6b8bd5 Socket Setup 2025-11-12 15:36:26 +05:30
24 changed files with 1028 additions and 364 deletions

View File

@ -80,7 +80,6 @@ body {
padding: 0 1.5rem;
gap: 1rem;
width: 100%;
max-width: 1024px;
flex-direction: row;
align-items: center;
justify-content: space-between;
@ -188,7 +187,10 @@ body {
width: 16rem;
background-color: var(--bg-card);
border-right: 1px solid var(--border-color);
display: none;
/* display: none; */
}
.dashboard-sidebar.active {
display: block;
}
@media (min-width: 768px) {
@ -196,6 +198,20 @@ body {
display: block;
}
}
@media (max-width: 768px) {
.dashboard-sidebar {
position: absolute;
left: 0;
bottom: 0;
z-index: 99;
top: 0;
transform: translateX(-100%);
transition: .3s ease;
}
.dashboard-sidebar.active {
transform: translateX(0);
}
}
.sidebar-header {
padding: 1.5rem;

BIN
public/images/downloads.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 504 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 27 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 782 B

BIN
public/images/windows.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 B

View File

@ -42,11 +42,54 @@ async function closePreviousSession(vaId, lastLog) {
});
// console.log(`Created logout log: ${logoutLog._id}`);
// try {
// const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
// const sfDataSync = {
// "_id": logoutLog._id,
// "loggedType": "LOGOUT",
// "vaId": vaId,
// "vaEmail": lastLog.vaEmail,
// "projectId": lastLog.projectId,
// "time": logoutLog.time,
// }
// const response = await fetch(salesforceUrl, {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json',
// // Add any required headers for Salesforce authentication
// // 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
// },
// body: JSON.stringify(sfDataSync)
// });
// if (!response.ok) {
// const errorText = await response.text();
// throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
// }
// const result = await response.json();
// // console.log("✅ Successfully synced VA data to Salesforce:", result);
// return {
// closedWorkTracks: openWorkTracks.length,
// logoutLogId: logoutLog._id,
// 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 {
closedWorkTracks: openWorkTracks.length,
logoutLogId: logoutLog._id
};
closedWorkTracks: openWorkTracks.length,
logoutLogId: logoutLog._id,
};
} catch (error) {
console.error("Error closing previous session:", error);

View File

@ -0,0 +1,101 @@
import jwt from "jsonwebtoken";
import { VAModel, LeavesModel } from "../../../models/vaModel.js";
// Get all leave lists applied by VA
async function va_leave_list_function(req, res) {
try {
const accessToken = req.headers.token;
if (!accessToken) {
return res.status(401).json({ success: false, message: "Missing token" });
}
const decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN);
if (!decoded || !decoded.email) {
return res.status(401).json({ success: false, message: "Invalid token" });
}
const vaEmail = decoded.email;
const vaUser = await VAModel.findOne({ "userDetails.email": vaEmail });
if (!vaUser) {
return res.status(404).json({ success: false, message: "VA not found" });
}
const vaId = vaUser._id;
// Get parameters from route params (they are optional)
const { status, type } = req.params;
// Get sorting from query params (optional)
const {
sortBy = 'createdAt',
sortOrder = 'desc'
} = req.query;
// Build filter object
const filter = { vaId };
// Handle status filter from params
if (status && status !== 'ALL') {
filter.status = status.toUpperCase();
}
// Handle type filter from params (using 'type' instead of 'leaveType')
if (type && type !== 'ALL') {
filter.leaveType = type.toUpperCase();
}
// Validate sort parameters
const validSortFields = ['createdAt', 'dateFrom', 'dateTo', 'status', 'leaveType'];
const sortField = validSortFields.includes(sortBy) ? sortBy : 'createdAt';
const sortDirection = sortOrder.toLowerCase() === 'asc' ? 1 : -1;
// Get all leaves without pagination
const leaves = await LeavesModel.find(filter)
.sort({ [sortField]: sortDirection })
.select('-__v')
.lean();
// Format the response data
const formattedLeaves = leaves.map(leave => ({
leaveId: leave._id,
dateFrom: leave.dateFrom.toISOString().split('T')[0],
dateTo: leave.dateTo.toISOString().split('T')[0],
totalDays: parseInt(leave.days),
leaveType: leave.leaveType,
comment: leave.comment,
status: leave.status,
appliedOn: leave.createdAt,
reviewedAt: leave.reviewedAt || null,
rejectionReason: leave.rejectionReason || null
}));
return res.status(200).json({
success: true,
message: "Leave list retrieved successfully",
data: {
leaves: formattedLeaves,
totalCount: formattedLeaves.length
}
});
} catch (error) {
console.error("Leave list error:", error);
if (error.name === 'JsonWebTokenError') {
return res.status(401).json({ success: false, message: "Invalid token" });
}
if (error.name === 'TokenExpiredError') {
return res.status(401).json({ success: false, message: "Token expired" });
}
return res.status(500).json({
success: false,
message: "Internal Server Error",
error: error.message
});
}
}
export { va_leave_list_function };

View File

@ -0,0 +1,114 @@
import jwt from "jsonwebtoken";
import { VAModel, LeavesModel } from "../../../models/vaModel.js";
// Approve or reject leave
async function va_leave_manage_function(req, res) {
try {
const { leaveId } = req.params;
const { action, rejectionReason } = req.body;
if (!leaveId) {
return res.status(400).json({
success: false,
message: "Leave ID is required"
});
}
if (!action || !['APPROVE', 'REJECT'].includes(action.toUpperCase())) {
return res.status(400).json({
success: false,
message: "Action is required and must be either 'APPROVE' or 'REJECT'"
});
}
// If rejecting, rejectionReason is required
if (action.toUpperCase() === 'REJECT' && !rejectionReason) {
return res.status(400).json({
success: false,
message: "Rejection reason is required when rejecting a leave"
});
}
// Find the leave request
const leaveRequest = await LeavesModel.findById(leaveId);
if (!leaveRequest) {
return res.status(404).json({
success: false,
message: "Leave request not found"
});
}
// Check if leave is already processed
if (leaveRequest.status !== 'PENDING') {
return res.status(400).json({
success: false,
message: `Leave request is already ${leaveRequest.status.toLowerCase()}`
});
}
// Prepare update data
const updateData = {
status: action.toUpperCase() === 'APPROVE' ? 'APPROVED' : 'REJECTED',
reviewedAt: new Date()
};
// Add rejection reason if rejecting
if (action.toUpperCase() === 'REJECT') {
updateData.rejectionReason = rejectionReason;
}
// Update the leave request
const updatedLeave = await LeavesModel.findByIdAndUpdate(
leaveId,
updateData,
{ new: true } // Return updated document
).select('-__v').lean();
// Format the response
const formattedLeave = {
leaveId: updatedLeave._id,
dateFrom: updatedLeave.dateFrom.toISOString().split('T')[0],
dateTo: updatedLeave.dateTo.toISOString().split('T')[0],
totalDays: parseInt(updatedLeave.days),
leaveType: updatedLeave.leaveType,
comment: updatedLeave.comment,
status: updatedLeave.status,
appliedOn: updatedLeave.createdAt,
reviewedAt: updatedLeave.reviewedAt,
rejectionReason: updatedLeave.rejectionReason || null
};
return res.status(200).json({
success: true,
message: `Leave request ${action.toLowerCase()}d successfully`,
data: formattedLeave
});
} catch (error) {
console.error("Leave approve/reject error:", error);
if (error.name === 'JsonWebTokenError') {
return res.status(401).json({ success: false, message: "Invalid token" });
}
if (error.name === 'TokenExpiredError') {
return res.status(401).json({ success: false, message: "Token expired" });
}
if (error.name === 'CastError') {
return res.status(400).json({
success: false,
message: "Invalid leave ID format"
});
}
return res.status(500).json({
success: false,
message: "Internal Server Error",
error: error.message
});
}
}
export { va_leave_manage_function };

View File

@ -0,0 +1,241 @@
import jwt from "jsonwebtoken";
import { VAModel, LeavesModel } from "../../../models/vaModel.js";
// Apply for leave
async function va_leave_apply_function(req, res) {
try {
const accessToken = req.headers.token;
if (!accessToken) {
return res.status(401).json({ success: false, message: "Missing token" });
}
const decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN);
if (!decoded || !decoded.email) {
return res.status(401).json({ success: false, message: "Invalid token" });
}
const vaEmail = decoded.email;
const vaUser = await VAModel.findOne({ "userDetails.email": vaEmail });
if (!vaUser) {
return res.status(404).json({ success: false, message: "VA not found" });
}
const vaId = vaUser._id;
const { dateFrom, dateTo, leaveType, comment } = req.body;
if (!dateFrom || !dateTo || !leaveType || !comment) {
return res.status(400).json({
success: false,
message: "dateFrom, dateTo, leaveType, and comment are required"
});
}
// Validate date formats
let startDate, endDate;
try {
startDate = new Date(dateFrom);
endDate = new Date(dateTo);
// Set to start of day for accurate calculation
startDate.setHours(0, 0, 0, 0);
endDate.setHours(0, 0, 0, 0);
if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
return res.status(400).json({
success: false,
message: "Invalid date format. Use valid date format (YYYY-MM-DD)"
});
}
} catch (error) {
return res.status(400).json({
success: false,
message: "Invalid date format"
});
}
// Ensure start date is before or equal to end date
if (startDate > endDate) {
return res.status(400).json({
success: false,
message: "Start date must be before or equal to end date"
});
}
// Ensure dates are not in the past
const today = new Date();
today.setHours(0, 0, 0, 0);
if (startDate < today) {
return res.status(400).json({
success: false,
message: "Cannot apply for leave for past dates"
});
}
// Validate leave type
const validLeaveTypes = ["SICK", "PTO"];
if (!validLeaveTypes.includes(leaveType)) {
return res.status(400).json({
success: false,
message: "Invalid leave type. Must be SICK or PTO"
});
}
// Calculate total days (inclusive of both start and end dates)
const timeDifference = endDate.getTime() - startDate.getTime();
const totalDays = Math.floor(timeDifference / (1000 * 60 * 60 * 24)) + 1; // +1 to include both start and end dates
if (totalDays <= 0) {
return res.status(400).json({
success: false,
message: "Invalid date range"
});
}
// Check for overlapping leave requests
const existingLeave = await LeavesModel.findOne({
vaId,
status: "PENDING",
$or: [
{
dateFrom: { $lte: endDate },
dateTo: { $gte: startDate }
}
]
});
if (existingLeave) {
return res.status(400).json({
success: false,
message: "You already have a pending leave request that overlaps with these dates"
});
}
// Check for overlapping approved leaves
const existingApprovedLeave = await LeavesModel.findOne({
vaId,
status: "APPROVED",
$or: [
{
dateFrom: { $lte: endDate },
dateTo: { $gte: startDate }
}
]
});
if (existingApprovedLeave) {
return res.status(400).json({
success: false,
message: "You already have an approved leave that overlaps with these dates"
});
}
// Create leave request
const leaveRequest = new LeavesModel({
vaId,
dateFrom: startDate,
dateTo: endDate,
days: totalDays.toString(),
leaveType,
comment,
status: "PENDING"
});
await leaveRequest.save();
// try {
// const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/timesheet-adjustment`;
// const sfData = {
// "id": adjustmentRequest._id,
// "va": {
// "id": vaId
// },
// "project": {
// "id": projectId
// },
// "date": adjustmentDate.toISOString().split('T')[0],
// "loginTime": loginDateTime.toISOString(),
// "logoutTime": logoutDateTime.toISOString(),
// "comment": comment,
// "status": "PENDING",
// "rejectionReason": "",
// "adjustedHours": formatTime(adjustedSeconds)
// }
// const response = await fetch(salesforceUrl, {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json',
// // Add any required headers for Salesforce authentication
// // 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
// },
// body: JSON.stringify({"data": sfData})
// });
// if (!response.ok) {
// const errorText = await response.text();
// throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
// }
// const result = await response.json();
// // console.log("✅ Successfully synced VA data to Salesforce:", result);
// return res.status(201).json({
// success: true,
// message: "Adjustment request submitted successfully",
// data: {
// adjustmentId: adjustmentRequest._id,
// date: adjustmentDate.toISOString().split('T')[0],
// loginTime: loginDateTime.toISOString(),
// logoutTime: logoutDateTime.toISOString(),
// adjustedHours: formatTime(adjustedSeconds),
// comment: comment,
// 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) {
console.error("Leave apply error:", error);
if (error.name === 'JsonWebTokenError') {
return res.status(401).json({ success: false, message: "Invalid token" });
}
if (error.name === 'TokenExpiredError') {
return res.status(401).json({ success: false, message: "Token expired" });
}
return res.status(500).json({
success: false,
message: "Internal Server Error",
error: error.message
});
}
}
export { va_leave_apply_function };

View File

@ -66,12 +66,51 @@ async function checkDailyHoursAndLogout(vaId, vaEmail, projectId, project, curre
projectId
});
return {
shouldLogout: true,
totalTodayHours: totalTodayHours,
dedicatedHours: project.dedicatedHours,
logoutLogId: logoutLog._id
};
try {
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
const sfDataSync = {
"_id": logoutLog._id,
"loggedType": "LOGOUT",
"vaId": vaId,
"vaEmail": vaEmail,
"projectId": projectId,
"time": logoutLog.time,
}
const response = await fetch(salesforceUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// Add any required headers for Salesforce authentication
// 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
},
body: JSON.stringify(sfDataSync)
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
}
const result = await response.json();
// console.log("✅ Successfully synced VA data to Salesforce:", result);
return {
shouldLogout: true,
totalTodayHours: totalTodayHours,
dedicatedHours: project.dedicatedHours,
logoutLogId: logoutLog._id,
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 {

View File

@ -50,7 +50,8 @@ async function va_register(req, res) {
// Send data to Salesforce API after successful registration
try {
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/virtualassistant`;
delete created.userDetails._id;
created.userDetails._id = created._id; // Include VA's MongoDB ID
const response = await fetch(salesforceUrl, {
method: 'POST',
headers: {

View File

@ -66,11 +66,49 @@ async function closePreviousSession(vaId, lastLog) {
});
// console.log(`Created logout log: ${logoutLog._id}`);
try {
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
const sfDataSync = {
"_id": logoutLog._id,
"loggedType": "LOGOUT",
"vaId": vaId,
"vaEmail": lastLog.vaEmail,
"projectId": lastLog.projectId,
"time": logoutLog.time,
}
return {
closedWorkTracks: openWorkTracks.length,
logoutLogId: logoutLog._id
};
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(sfDataSync)
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
}
const result = await response.json();
// console.log("✅ Successfully synced VA data to Salesforce:", result);
return {
closedWorkTracks: openWorkTracks.length,
logoutLogId: logoutLog._id,
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;
}
} catch (error) {
console.error("Error closing previous session:", error);
@ -151,47 +189,47 @@ async function va_project_log_callback(req, res) {
const createdLog = await LoggedModel.create({ loggedType: type, vaId, vaEmail, projectId })
// console.log(createdLog)
try {
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
const sfData = {
"_id": createdLog._id,
"loggedType": type,
"vaId": vaId,
"vaEmail": vaEmail,
"projectId": projectId,
"time": createdLog.time,
}
// try {
// const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
// const sfData = {
// "_id": createdLog._id,
// "loggedType": type,
// "vaId": vaId,
// "vaEmail": vaEmail,
// "projectId": projectId,
// "time": createdLog.time,
// }
const response = await fetch(salesforceUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// Add any required headers for Salesforce authentication
// 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
},
body: JSON.stringify(sfData)
});
// const response = await fetch(salesforceUrl, {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json',
// // Add any required headers for Salesforce authentication
// // 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
// },
// body: JSON.stringify(sfData)
// });
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
}
// if (!response.ok) {
// const errorText = await response.text();
// throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
// }
const result = await response.json();
// console.log("✅ Successfully synced VA data to Salesforce:", result);
// const result = await response.json();
// // console.log("✅ Successfully synced VA data to Salesforce:", result);
res.status(201).json({ success: true, message: `${type} Successfully`, logId: createdLog._id, SyncSalesforce: result });
} catch (error) {
console.error("Failed to sync with Salesforce:", error.message);
console.log(error)
// res.status(201).json({ success: true, message: `${type} Successfully`, logId: createdLog._id, 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;
}
// // 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).send({ success: true, message: `${type} Successfully`, logId: createdLog._id });
return res.status(201).send({ success: true, message: `${type} Successfully`, logId: createdLog._id });
} catch (error) {
return res.status(500).send("Internal Server Error");

View File

@ -140,19 +140,80 @@ async function va_timesheet_adjustment_function(req, res) {
return `${h}h ${m}m ${s}s`;
};
return res.status(201).json({
success: true,
message: "Adjustment request submitted successfully",
data: {
adjustmentId: adjustmentRequest._id,
date: adjustmentDate.toISOString().split('T')[0],
loginTime: loginDateTime.toISOString(),
logoutTime: logoutDateTime.toISOString(),
adjustedHours: formatTime(adjustedSeconds),
comment: comment,
status: "PENDING"
try {
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/timesheet-adjustment`;
const sfData = {
"id": adjustmentRequest._id,
"va": {
"id": vaId
},
"project": {
"id": projectId
},
"date": adjustmentDate.toISOString().split('T')[0],
"loginTime": loginDateTime.toISOString(),
"logoutTime": logoutDateTime.toISOString(),
"comment": comment,
"status": "PENDING",
"rejectionReason": "",
"adjustedHours": formatTime(adjustedSeconds)
}
});
const response = await fetch(salesforceUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// Add any required headers for Salesforce authentication
// 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
},
body: JSON.stringify({"data": sfData})
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
}
const result = await response.json();
// console.log("✅ Successfully synced VA data to Salesforce:", result);
return res.status(201).json({
success: true,
message: "Adjustment request submitted successfully",
data: {
adjustmentId: adjustmentRequest._id,
date: adjustmentDate.toISOString().split('T')[0],
loginTime: loginDateTime.toISOString(),
logoutTime: logoutDateTime.toISOString(),
adjustedHours: formatTime(adjustedSeconds),
comment: comment,
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: "Adjustment request submitted successfully",
// data: {
// adjustmentId: adjustmentRequest._id,
// date: adjustmentDate.toISOString().split('T')[0],
// loginTime: loginDateTime.toISOString(),
// logoutTime: logoutDateTime.toISOString(),
// adjustedHours: formatTime(adjustedSeconds),
// comment: comment,
// status: "PENDING"
// }
// });
} catch (error) {
console.error("Timesheet adjustment error:", error);

View File

@ -23,7 +23,8 @@ async function updateEntityRelationships(data) {
clientId,
{
$addToSet: {
projectIds: projectId
projectIds: projectId,
vaIds: vaId
}
},
{ new: true }
@ -37,21 +38,46 @@ async function updateEntityRelationships(data) {
{ _id: { $in: vaId } },
{
$addToSet: {
projectIds: projectId
projectIds: projectId,
clientIds: clientId
}
}
);
results.vaUpdate = `Updated ${vaUpdateResult.modifiedCount} VAs`;
}
// Update Project with clientId and vaId
// // Update Project with clientId and vaId
// const projectUpdateData = {};
// if (clientId) {
// projectUpdateData.clientId = clientId;
// }
// // 🐞🐞🐞 Here Duplicate Entry of Va is Created if SF pass same va multiple times in same req
// if (vaId && Array.isArray(vaId)) {
// projectUpdateData.vaAssigned = vaId;
// }
// if (Object.keys(projectUpdateData).length > 0) {
// const projectUpdate = await ProjectModel.findByIdAndUpdate(
// projectId,
// projectUpdateData,
// { new: true }
// );
// results.projectUpdate = projectUpdate ? 'Project updated successfully' : 'Project not found';
// }
if (vaId && Array.isArray(vaId)) {
await ProjectModel.findByIdAndUpdate(
projectId,
{ $addToSet: { vaAssigned: { $each: vaId } } }
);
}
// Update other fields normally
const projectUpdateData = {};
if (clientId) {
projectUpdateData.clientId = clientId;
}
if (vaId && Array.isArray(vaId)) {
projectUpdateData.vaAssigned = vaId;
}
if (Object.keys(projectUpdateData).length > 0) {
const projectUpdate = await ProjectModel.findByIdAndUpdate(
@ -202,16 +228,46 @@ async function removeEntityRelationships(data) {
if (clientId) {
await ClientModel.findByIdAndUpdate(
clientId,
{ $pull: { projectIds: projectId } }
{$pull: {
projectIds: projectId,
vaIds: vaId
}}
);
}
// Remove projectId from VAs
if (vaId && Array.isArray(vaId)) {
await VAModel.updateMany(
{ _id: { $in: vaId } },
{ $pull: { projectIds: projectId } }
);
// await VAModel.updateMany(
// { _id: { $in: vaId } },
// { $pull: {
// projectIds: projectId,
// // 🐞🐞🐞 Here before removing we need to check if VA is still assigned to other projects of the same client
// clientIds: clientId
// }}
// );
for (const va of vaId) {
// Check if VA is still assigned to other projects of the same client
const otherProjectsWithSameClient = await ProjectModel.countDocuments({
_id: { $ne: projectId },
clientId: clientId,
vaAssigned: va
});
// Prepare update operation
const updateOperation = {
$pull: { projectIds: projectId }
};
// Only remove clientId if VA is not assigned to any other projects from this client
if (!otherProjectsWithSameClient) {
updateOperation.$pull = {
...updateOperation.$pull,
clientIds: clientId
};
}
await VAModel.findByIdAndUpdate(va, updateOperation);
}
}
results.projectRemoval = 'Project relationships removed successfully';

View File

@ -92,7 +92,7 @@ const projectSchema = new Schema({
],
dedicatedHours: {
type: String,
default: 0
default: "0"
},
status: {
type: String,
@ -166,6 +166,7 @@ const clientDetailsSchema = new Schema({
enum: ["CHECKING", "SAVINGS"],
type: String,
},
accountHolderName: {
type: String,
},
@ -423,6 +424,51 @@ const adjustedHoursSchema = new Schema({
}]
}, { timestamps: true });
const leavesSchema = new Schema({
vaId: {
type: Types.ObjectId,
ref: "VA",
required: true
},
clientId: {
type: Types.ObjectId,
ref: "Client"
},
dateFrom: {
type: Date,
required: true
},
dateTo: {
type: Date,
required: true
},
days: {
type: String,
required: true
},
comment: {
type: String,
required: true
},
leaveType: {
type: String,
enum: ["SICK", "PTO"],
default: "SICK",
required: true
},
status: {
type: String,
enum: ["PENDING", "APPROVED", "REJECTED"],
default: "PENDING"
},
reviewedAt: {
type: Date
},
rejectionReason: {
type: String // If rejected, store the reason
}
}, { timestamps: true });
/* ---------------------- Model Exports ---------------------- */
const VAModel = model("VA", vaSchema);
const ClientModel = model("Client", clientSchema);
@ -431,5 +477,6 @@ const LoggedModel = model("TrackLog", loggedSchema);
const WorkTrackModel = model("TrackWork", vaWorkTrackSchema);
const PasswordResetModel = model("PasswordReset", passwordResetSchema);
const AdjustedHoursModel = model("AdjustedHours", adjustedHoursSchema);
const LeavesModel = model("Leaves", leavesSchema);
export { VAModel, ClientModel, ProjectModel, LoggedModel, WorkTrackModel, PasswordResetModel, AdjustedHoursModel };
export { VAModel, ClientModel, ProjectModel, LoggedModel, WorkTrackModel, PasswordResetModel, AdjustedHoursModel, LeavesModel };

View File

@ -23,6 +23,9 @@ import { va_timesheet_adjustment_function } from "../../controllers/va/vaTimeshe
import { get_adjustment_requests_list } from "../../controllers/va/vaTimesheetAdjustment/vaAllTimesheetAdjustmentReuqest.controller.js"
import { va_timesheet_manage_adjustment_request } from "../../controllers/va/vaTimesheetAdjustment/vaManageTimesheetAdjustment.controller.js"
import { va_verify_email } from "../../controllers/va/vaAuth/vaVerifyEmail.controller.js";
import { va_leave_apply_function } from "../../controllers/va/vaLeaves/vaLeavesApply.controller.js";
import { va_leave_list_function } from "../../controllers/va/vaLeaves/vaLeaveList.controller.js"
import { va_leave_manage_function } from "../../controllers/va/vaLeaves/vaLeaveManage.controller.js";
vaRoutes.get("/", va_callback); // VA Callback for getting active VA
@ -43,9 +46,17 @@ vaRoutes.get("/:userName", va_username_callback); // VA Username Callback
// timesheet adjustment routes
vaRoutes.get("/timesheet/summary/:projectId/:date", authValidator, va_timesheet_summary_function); // Get Timesheet Adjustment for VA on specific project and date
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/: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);
// Leaves routes
vaRoutes.get("/leaves/all", authValidator, va_leave_list_function); // Get all leaves
vaRoutes.get("/leaves/status/:status", authValidator, va_leave_list_function); // Leaves Filter by status
vaRoutes.get("/leaves/type/:leaveType", authValidator, va_leave_list_function); // Filter by leave type
vaRoutes.get("/leaves/status/:status/type/:leaveType", authValidator, va_leave_list_function); // leaves Filter
vaRoutes.post("/leave/request", authValidator, va_leave_apply_function); // Timesheet Adjustment for VA
vaRoutes.patch("/leave/:leaveId", va_leave_manage_function);
// timesheet adjustment routes
// Example: GET /va/search?name=john&email=@example.com&from=2025-01-01&to=2025-01-31 (NC)

View File

@ -13,16 +13,32 @@ import cors from 'cors';
import path from 'path';
import { fileURLToPath } from 'url';
import apiRoutes from './routes/index.js'
import { render_reset_form } from "./controllers/passwordReset.controllers.js";
import jwt from "jsonwebtoken";
const app = express();
const server = new createServer(app);
const io = new Server(server);
app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
console.log("⚠️ Certificate error:", url);
// Allow for ngrok only
if (url.includes("ngrok")) {
event.preventDefault();
callback(true); // approve the certificate
} else {
callback(false); // reject others
}
});
app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
console.log("⚠️ Certificate error:", url);
@ -93,13 +109,18 @@ app.get('/varegister/:sf_token?', (req, res) => {
});
app.get('/clientregister/:sf_token?', (req, res) => {
const { sf_token } = req.params;
try {
const decoded = jwt.verify(sf_token, process.env.JWT_SECRET_SF_TOKEN);
// portalId: RECORDid => 8990928342309
// email
// const { sf_token } = useParams();
res.render('va_client_register', { title: "Client Registration" });
if (decoded) {
res.render('va_client_register', { title: "Client Registration" });
} else {
res.render('not_found', { title: "Client Registration" });
}
} catch (error) {
res.render('not_found', { title: "Client Registration" });
}
});
app.get('/auth/password/reset-form', render_reset_form);
@ -138,35 +159,28 @@ app.get('/client/virtual-assistants', async (req, res) => {
}
});
// app.get('/client/timesheets', async (req, res) => {
// try {
// const timesheetData = await timesheetService.getClientTimesheets(req.user.id);
// res.render('client/dashboard-timesheets', timesheetData);
// } catch (error) {
// console.error('Timesheets error:', error);
// res.status(500).render('client/dashboard-timesheets', { error: 'Failed to load timesheets' });
// }
// });
app.get("/live/:projectId/:vaId", async (req, res) => {
const { projectId, vaId } = req.params;
// app.get('/client/invoices', async (req, res) => {
// try {
// const invoiceData = await invoiceService.getClientInvoices(req.user.id);
// res.render('client/dashboard-invoices', invoiceData);
// } catch (error) {
// console.error('Invoices error:', error);
// res.status(500).render('client/dashboard-invoices', { error: 'Failed to load invoices' });
// }
// });
if (!projectId || !vaId) {
return res.status(400).send({ "message": "Page not found" });
}
// app.get('/client/referrals', async (req, res) => {
// try {
// const referralData = await referralService.getClientReferrals(req.user.id);
// res.render('client/dashboard-referrals', referralData);
// } catch (error) {
// console.error('Referrals error:', error);
// res.status(500).render('client/dashboard-referrals', { error: 'Failed to load referrals' });
// }
// });
let clientId = null;
try {
const clientRes = await ProjectModel.findById(projectId);
if (clientRes) {
clientId = clientRes.clientId.toString();
}
} catch (err) {
console.error("Error fetching client for project:", err.message);
return;
}
if (!clientId) return res.status(400).send({ "message": "Page not found" });
res.render("va_live_tracker", { title: "VA Live Tracker", projectId, vaId, clientId })
})
app.get("/live/:projectId/:vaId", async (req, res) => {
const { projectId, vaId } = req.params;
@ -193,16 +207,17 @@ app.get("/live/:projectId/:vaId", async (req, res) => {
// Handling Socket
import { VAModel, ProjectModel, ClientModel } from './models/vaModel.js';
import { ObjectId } from 'mongodb/lib/bson.js';
const vaSocketRespectToClient = new Map();
// Handling Socket
io.use(async (socket, next) => {
try {
const token = socket.handshake.auth?.token;
const projectId = socket.handshake.auth?.projectId;
const userType = socket.handshake.auth?.userType;
const vaId = socket.handshake.auth?.vaId;
console.log("🔐 Received token:", token);
@ -216,7 +231,7 @@ io.use(async (socket, next) => {
return next(new Error("Invalid token payload"));
}
/* ---------------------- 1️⃣ Check if VA ---------------------- */
/* ---------------------- 1️⃣ Check if VA ---------------------- */
let vaUser = null;
let clientData = null;
@ -241,13 +256,13 @@ io.use(async (socket, next) => {
console.log(a, b, c, "vaProjClientId vs clientData.clientId");
vaSocketRespectToClient.set(`${vaProjClientId.toString()}-${vaUser._id.toString()}`, socket.id)
vaSocketRespectToClient.set(`${vaProjClientId.toString()}-${vaUser._id.toString()}-${projectId.toString()}-va`, socket.id)
console.log("✅ Authenticated VA:", vaUser.userDetails.email);
return next();
}
/* ---------------------- 2️⃣ Check if Client ---------------------- */
/* ---------------------- 2️⃣ Check if Client ---------------------- */
// const clientUser = await ClientModel.findOne({ "userDetails.email": decoded.email });
// if (clientUser) {
@ -268,6 +283,7 @@ io.use(async (socket, next) => {
// Get all VA IDs assigned to this client
socket.data.allowedVAs = clientData.vaAssigned.map((va) => va.toString());
vaSocketRespectToClient.set(`${vaProjClientId.toString()}-${vaId.toString()}-${projectId.toString()}-client`, socket.id)
// console.log("✅ Authenticated Client:", clientUser.userDetails.email);
return next();
@ -280,8 +296,6 @@ io.use(async (socket, next) => {
}
});
io.on("connection", (socket) => {
console.log("User connected:", socket.id);
@ -316,7 +330,7 @@ io.on("connection", (socket) => {
});
// When VA sends live image
socket.on("sendImage", async ({ token, image, title }) => {
socket.on("sendImage", async ({ token, image, title, projectId }) => {
try {
// Verify token
const decoded = jwt.verify(token, process.env.JWT_SECRET_ACCESS_TOKEN);
@ -329,30 +343,42 @@ io.on("connection", (socket) => {
return;
}
const project = await ProjectModel.findById(projectId);
if (!project) {
console.log("❌ Invalid Project ID");
return;
}
const clientId = project.clientId.toString();
const vaId = vaUser._id.toString();
const clientVaUniqueId = `${clientId}-${vaId}-${projectId}-client`;
const socketId = vaSocketRespectToClient.get(clientVaUniqueId);
console.log("🟢 Received image from VA:", vaId);
// Broadcast image only to allowed clients
for (let [_, clientSocket] of io.sockets.sockets) {
if (
clientSocket.data.role === "client" &&
clientSocket.data.allowedVAs?.includes(vaId)
) {
clientSocket.emit("receiveImage", image, title);
}
}
// for (let [_, clientSocket] of io.sockets.sockets) {
// if (
// clientSocket.data.role === "client" &&
// clientSocket.data.allowedVAs?.includes(vaId)
// ) {
// clientSocket.emit("receiveImage", image, title);
// }
// }
// Can be like clientId-projectId-vaId to uniquely identify
socket.to(socketId).emit("receiveImage", image, title);
} catch (err) {
console.error("❌ Token verification failed:", err.message);
}
});
socket.on("selectApp", ({ appTitle, clientId }) => {
socket.on("selectApp", ({ appTitle, clientId, vaId, projectId }) => {
console.log(`VA selected app: ${appTitle}`);
const sssrrrr = vaSocketRespectToClient.get(`${clientId}-${socket.data.vaId}`);
console.log(vaSocketRespectToClient.get(`${clientId}-${socket.data.vaId}`), sssrrrr, "vaSocketRespectToClient[clientId]")
console.log(vaSocketRespectToClient.get(`${clientId}-${vaId}-${projectId}-va`), "vaSocketRespectToClient[clientId]")
// You can forward this info to the respective Electron client
socket.to(vaSocketRespectToClient.get(`${clientId}-${socket.data.vaId}`)).emit("appToCapture", { appTitle });
socket.to(vaSocketRespectToClient.get(`${clientId}-${vaId}-${projectId}-va`)).emit("appToCapture", { appTitle });
});
socket.on("getVisibleApps", async ({ token, apps }) => {
@ -377,7 +403,7 @@ io.on("connection", (socket) => {
clientSocket.data.role === "client" &&
clientSocket.data.allowedVAs?.includes(vaId)
) {
clientSocket.emit("receiveVisibleApps", apps);
clientSocket.emit("receiveVisibleApps", { apps, vaId });
}
}
} catch (err) {
@ -391,39 +417,10 @@ io.on("connection", (socket) => {
});
});
// io.on('connection', (socket) => {
// socket.on("sendImage", (base64String) => {
// io.emit("receiveImage", base64String);
// });
// socket.on('disconnect', () => {
// console.log('a user Disconnected');
// });
// });
// Express App Listening
// app.listen(PORT, () => {
// mongoose
// .connect(
// process.env.MONGO_URI,
// )
// .then(() => console.log("mongodb connected"))
// .catch((err) => {
// console.log(err.message);
// process.exit(1);
// });
// console.log(`Express started on http://localhost:${PORT}`);
// });
app.use((req, res) => {
res.status(404).render("not_found", {
title: "404 - Page Not Found"
});
res.status(404).render("not_found", {
title: "404 - Page Not Found"
});
});
server.listen(PORT, () => {

View File

@ -29,4 +29,35 @@ async function testPaths() {
}
}
testPaths();// testVpnApi.js
import axios from "axios";
const apiUrl = "http://165.140.240.203:3000/api";
const apiKey = "hellovpn123";
const possiblePaths = [
"/", "/v1", "/servers", "/status", "/vpn", "/health", "/ping",
"/nodes", "/list", "/connect", "/config", "/create", "/createConfig",
"/check", "/organizations", "/users"
];
async function testPaths() {
for (const path of possiblePaths) {
try {
const res = await axios.get(`${apiUrl}${path}`, {
headers: { "x-api-key": apiKey },
timeout: 3000
});
console.log(`✅ ${path} → ${res.status} ${res.statusText}`);
console.log("Response:", res.data);
} catch (err) {
if (err.response) {
console.log(`❌ ${path} → ${err.response.status}`);
} else {
console.log(`⚠️ ${path} → No response`);
}
}
}
}
testPaths();

BIN
trackerExpress.zip Normal file

Binary file not shown.

View File

@ -148,8 +148,7 @@
<th>Status</th>
<th>Virtual Assistant</th>
<th>Projects Assigned</th>
<th>Working from</th>
<th>Total Worked</th>
<th>Worked Time</th>
<th>Action</th>
</tr>
</thead>
@ -222,7 +221,7 @@
// Format time function
function formatTime(seconds) {
if (!seconds) return '';
if (!seconds) return 'Not working';
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
@ -231,8 +230,6 @@
return `${hours}h ${minutes}m`;
} else if (minutes > 0) {
return `${minutes}m`;
} else if (seconds > 0) {
return `${seconds}s`;
} else {
return 'Just started';
}
@ -243,7 +240,6 @@
const statusClass = va.isCurrentlyActive ? 'active' : 'inactive';
const statusText = va.isCurrentlyActive ? 'Active' : 'Inactive';
const workedTime = formatTime(va.currentSessionDuration);
const totalWorkedTime = formatTime(va.totalWorkedSeconds);
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">
<i class="fas fa-eye"></i>
@ -251,7 +247,7 @@
</a>` : '';
return `
<tr data-vaid="${va.id}">
<tr>
<td>
<div class="status-indicator">
<div class="status-dot ${statusClass}"></div>
@ -270,9 +266,6 @@
<td>
<span class="worked-time">${workedTime}</span>
</td>
<td>
<span class="worked-time">${totalWorkedTime}</span>
</td>
<td>
${viewLink}
</td>
@ -310,34 +303,15 @@
vaTableBody.style.display = '';
vaEmptyState.style.display = 'none';
// Add event listeners to table rows
document.querySelectorAll('tr[data-vaid]').forEach(row => {
row.addEventListener('click', async function(e) {
// Don't trigger if click is on view link
if (e.target.closest('.view-link')) {
return;
}
const vaId = this.getAttribute('data-vaid');
const vaData = allVAs.find(va => va.id === vaId);
if (vaData) {
// Show loading state
const originalContent = this.innerHTML;
this.innerHTML = '<td colspan="6" style="text-align: center;"><i class="fas fa-spinner fa-spin"></i> Loading...</td>';
// Fetch additional VA details from /test API
const projectData = await fetchVADetails(vaId);
// Restore original content
this.innerHTML = originalContent;
// Show popup with VA details
showVAPopup(projectData);
}
// Add event listeners to view links
document.querySelectorAll('.view-link').forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
const vaId = this.getAttribute('data-va-id');
// You can update this link as needed
window.location.href = `/client/virtual-assistants/${vaId}`;
});
});
} else {
vaTableBody.innerHTML = '';
vaTableBody.style.display = 'none';
@ -377,126 +351,14 @@
// Optional: Auto-refresh data every 30 seconds
setInterval(loadDashboardData, 30000);
function createPopup(vaData) {
// Extract project details from the API response
// The API response is the project array, so use it directly
const projectDetails = Array.isArray(vaData) ? vaData : (vaData.projects || []);
// Create project details HTML
const projectsHTML = projectDetails.length > 0
? projectDetails.map(project => `
<div class="project-item">
<div class="project-header">
<span class="project-name">${project.projectName || 'Unknown Project'}</span>
<span class="project-time">${project.totalTime || '00:00:00'}</span>
</div>
<div class="project-id">ID: ${project.projectId || 'N/A'}</div>
</div>
`).join('')
: '<div class="no-projects">No project data available</div>';
return `
<div class="popup-overlay" id="vaPopup">
<div class="popup-content">
<div class="popup-header">
<h3>Project Work Details</h3>
<button class="popup-close" id="closePopup">
<i class="fas fa-times"></i>
</button>
</div>
<div class="popup-body">
<!-- Project Count -->
<div class="project-summary">
<div class="total-projects">
<i class="fas fa-folder-open"></i>
<span>Total Projects: ${projectDetails.length}</span>
</div>
</div>
<!-- Project Details List -->
<div class="projects-list">
${projectsHTML}
</div>
</div>
<div class="popup-footer">
<button class="btn btn-secondary" id="closePopupBtn">
<i class="fas fa-times"></i> Close
</button>
</div>
</div>
</div>
`;
}
// Show popup with VA details
function showVAPopup(projectData) {
console.log('Project Data:', projectData);
// Remove existing popup if any
const existingPopup = document.getElementById('vaPopup');
if (existingPopup) {
existingPopup.remove();
}
// Create and show new popup with only project data
const popupHTML = createPopup(projectData);
document.body.insertAdjacentHTML('beforeend', popupHTML);
// Add event listeners for closing popup
document.getElementById('closePopup').addEventListener('click', closePopup);
document.getElementById('closePopupBtn').addEventListener('click', closePopup);
// Close popup when clicking outside
document.getElementById('vaPopup').addEventListener('click', function(e) {
if (e.target === this) {
closePopup();
}
});
// Close popup with Escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closePopup();
}
});
}
// Fetch VA details for popup - now returns only project data
async function fetchVADetails(vaId) {
try {
const response = await fetchWithAuth(`${API_BASE}/dashboard/va-report`, {
method: 'POST',
body: JSON.stringify({ vaId: vaId }),
headers: {
'Content-Type': 'application/json'
}
});
if (!response) return;
const result = await response.json();
console.log('API Response:', result);
if (result.success) {
// Return the projects array directly
return result.projects || [];
} else {
console.error('Failed to fetch project details:', result.message);
return [];
}
} catch (error) {
console.error('Error fetching project details:', error);
return [];
}
}
// Close popup function
function closePopup() {
const popup = document.getElementById('vaPopup');
if (popup) {
popup.remove();
}
}
document.addEventListener('DOMContentLoaded', () => {
var menuToggle = document.querySelector('.mobile-menu-button');
var sidebar = document.querySelector('.dashboard-sidebar');
menuToggle.addEventListener('click', () => {
sidebar.classList.toggle('active')
})
})
</script>

View File

@ -211,6 +211,7 @@
</head>
<body>
<div class="logo">
<img src="/public/images/logo-white-vertical.svg" alt="DocVA Logo" />
</div>
@ -333,5 +334,3 @@
}
</script>
</body>
</html>

View File

@ -21,6 +21,7 @@
overflow: hidden;
}
/* SIDEBAR */
.sidebar {
width: 280px;
background: #181818;
@ -81,7 +82,6 @@
#liveImage {
width: 90%;
max-width: 900px;
border: 2px solid #555;
border-radius: 10px;
border: 2px solid #444;
object-fit: contain;
@ -185,49 +185,54 @@
</div>
<script src="/socket.io/socket.io.js"></script>
<script>
const vaId = "<%= vaId %>";
const clientId = "<%= clientId %>";
const token = localStorage.getItem("knoptpq");
const socket = io({
auth: {
token: token,
userType: "client",
projectId: "<%= projectId %>"
},
});
<script>
const vaId = "<%= vaId %>";
const clientId = "<%= clientId %>";
const projectId = "<%= projectId %>";
const token = localStorage.getItem("knoptpq");
socket.emit("register", {
role: "client",
id: vaId,
projectId: "<%= projectId %>",
clientId: clientId
});
const socket = io({
auth: {
token: token,
userType: "client",
projectId: projectId,
vaId: vaId
},
});
const sidebar = document.getElementById("sidebar");
const toggleBtn = document.getElementById("toggleBtn");
const overlay = document.getElementById("overlay");
socket.emit("register", {
role: "client",
id: vaId,
projectId: "<%= projectId %>",
clientId: clientId
});
const appsList = document.getElementById("appsList");
const liveImage = document.getElementById("liveImage");
const selectedAppTitle = document.getElementById("selectedAppTitle");
const placeholder = document.getElementById("placeholder");
const sidebar = document.getElementById("sidebar");
const toggleBtn = document.getElementById("toggleBtn");
const overlay = document.getElementById("overlay");
let currentApp = null;
const appsList = document.getElementById("appsList");
const liveImage = document.getElementById("liveImage");
const selectedAppTitle = document.getElementById("selectedAppTitle");
const placeholder = document.getElementById("placeholder");
/* MOBILE SIDEBAR TOGGLE */
toggleBtn.onclick = () => {
sidebar.classList.add("open");
overlay.classList.add("show");
};
let currentApp = null;
overlay.onclick = () => {
/* MOBILE SIDEBAR TOGGLE */
toggleBtn.onclick = () => {
sidebar.classList.add("open");
overlay.classList.add("show");
};
overlay.onclick = () => {
sidebar.classList.remove("open");
overlay.classList.remove("show");
};
/* Receive visible apps */
socket.on("receiveVisibleApps", (apps) => {
socket.on("receiveVisibleApps", ({apps, vaId: senderVaId}) => {
if (vaId !== senderVaId) return;
appsList.innerHTML = "";
if (!apps?.length) {
@ -250,7 +255,7 @@
placeholder.style.display = "none";
liveImage.style.display = "none";
socket.emit("selectApp", { appTitle: app.name, clientId, vaId });
socket.emit("selectApp", { appTitle: app.name, clientId, vaId, projectId });
// Close sidebar on mobile
sidebar.classList.remove("open");