Compare commits
No commits in common. "dc085787a341d1d36ef81909b46867442f454186" and "aecb4af2ac01ccedbc4ffe4e6ae65b7c3c6ea973" have entirely different histories.
dc085787a3
...
aecb4af2ac
|
|
@ -80,6 +80,7 @@ body {
|
||||||
padding: 0 1.5rem;
|
padding: 0 1.5rem;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
max-width: 1024px;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
@ -187,10 +188,7 @@ body {
|
||||||
width: 16rem;
|
width: 16rem;
|
||||||
background-color: var(--bg-card);
|
background-color: var(--bg-card);
|
||||||
border-right: 1px solid var(--border-color);
|
border-right: 1px solid var(--border-color);
|
||||||
/* display: none; */
|
display: none;
|
||||||
}
|
|
||||||
.dashboard-sidebar.active {
|
|
||||||
display: block;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
|
|
@ -198,20 +196,6 @@ body {
|
||||||
display: block;
|
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 {
|
.sidebar-header {
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 504 B |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 27 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 27 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 782 B |
Binary file not shown.
|
Before Width: | Height: | Size: 255 B |
|
|
@ -42,53 +42,10 @@ async function closePreviousSession(vaId, lastLog) {
|
||||||
});
|
});
|
||||||
|
|
||||||
// console.log(`Created logout log: ${logoutLog._id}`);
|
// 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 {
|
return {
|
||||||
closedWorkTracks: openWorkTracks.length,
|
closedWorkTracks: openWorkTracks.length,
|
||||||
logoutLogId: logoutLog._id,
|
logoutLogId: logoutLog._id
|
||||||
};
|
};
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -1,101 +0,0 @@
|
||||||
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 };
|
|
||||||
|
|
@ -1,114 +0,0 @@
|
||||||
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 };
|
|
||||||
|
|
@ -1,241 +0,0 @@
|
||||||
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 };
|
|
||||||
|
|
@ -66,51 +66,12 @@ async function checkDailyHoursAndLogout(vaId, vaEmail, projectId, project, curre
|
||||||
projectId
|
projectId
|
||||||
});
|
});
|
||||||
|
|
||||||
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 {
|
return {
|
||||||
shouldLogout: true,
|
shouldLogout: true,
|
||||||
totalTodayHours: totalTodayHours,
|
totalTodayHours: totalTodayHours,
|
||||||
dedicatedHours: project.dedicatedHours,
|
dedicatedHours: project.dedicatedHours,
|
||||||
logoutLogId: logoutLog._id,
|
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 {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -50,8 +50,7 @@ async function va_register(req, res) {
|
||||||
// Send data to Salesforce API after successful registration
|
// Send data to Salesforce API after successful registration
|
||||||
try {
|
try {
|
||||||
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/virtualassistant`;
|
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, {
|
const response = await fetch(salesforceUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
|
|
|
||||||
|
|
@ -66,49 +66,11 @@ async function closePreviousSession(vaId, lastLog) {
|
||||||
});
|
});
|
||||||
|
|
||||||
// console.log(`Created logout log: ${logoutLog._id}`);
|
// 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 {
|
return {
|
||||||
closedWorkTracks: openWorkTracks.length,
|
closedWorkTracks: openWorkTracks.length,
|
||||||
logoutLogId: logoutLog._id,
|
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) {
|
} catch (error) {
|
||||||
console.error("Error closing previous session:", error);
|
console.error("Error closing previous session:", error);
|
||||||
|
|
@ -189,47 +151,47 @@ async function va_project_log_callback(req, res) {
|
||||||
|
|
||||||
const createdLog = await LoggedModel.create({ loggedType: type, vaId, vaEmail, projectId })
|
const createdLog = await LoggedModel.create({ loggedType: type, vaId, vaEmail, projectId })
|
||||||
// console.log(createdLog)
|
// console.log(createdLog)
|
||||||
// try {
|
try {
|
||||||
// const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
|
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
|
||||||
// const sfData = {
|
const sfData = {
|
||||||
// "_id": createdLog._id,
|
"_id": createdLog._id,
|
||||||
// "loggedType": type,
|
"loggedType": type,
|
||||||
// "vaId": vaId,
|
"vaId": vaId,
|
||||||
// "vaEmail": vaEmail,
|
"vaEmail": vaEmail,
|
||||||
// "projectId": projectId,
|
"projectId": projectId,
|
||||||
// "time": createdLog.time,
|
"time": createdLog.time,
|
||||||
// }
|
}
|
||||||
|
|
||||||
// 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(sfData)
|
body: JSON.stringify(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}`);
|
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);
|
||||||
|
|
||||||
// res.status(201).json({ success: true, message: `${type} Successfully`, logId: createdLog._id, SyncSalesforce: result });
|
res.status(201).json({ success: true, message: `${type} Successfully`, logId: createdLog._id, SyncSalesforce: result });
|
||||||
// } catch (error) {
|
} catch (error) {
|
||||||
// console.error("Failed to sync with Salesforce:", error.message);
|
console.error("Failed to sync with Salesforce:", error.message);
|
||||||
// console.log(error)
|
console.log(error)
|
||||||
|
|
||||||
// // 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;
|
||||||
// throw salesforceError;
|
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) {
|
} catch (error) {
|
||||||
return res.status(500).send("Internal Server Error");
|
return res.status(500).send("Internal Server Error");
|
||||||
|
|
|
||||||
|
|
@ -140,43 +140,6 @@ async function va_timesheet_adjustment_function(req, res) {
|
||||||
return `${h}h ${m}m ${s}s`;
|
return `${h}h ${m}m ${s}s`;
|
||||||
};
|
};
|
||||||
|
|
||||||
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({
|
return res.status(201).json({
|
||||||
success: true,
|
success: true,
|
||||||
message: "Adjustment request submitted successfully",
|
message: "Adjustment request submitted successfully",
|
||||||
|
|
@ -188,32 +151,8 @@ async function va_timesheet_adjustment_function(req, res) {
|
||||||
adjustedHours: formatTime(adjustedSeconds),
|
adjustedHours: formatTime(adjustedSeconds),
|
||||||
comment: comment,
|
comment: comment,
|
||||||
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: "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) {
|
} catch (error) {
|
||||||
console.error("Timesheet adjustment error:", error);
|
console.error("Timesheet adjustment error:", error);
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,7 @@ async function updateEntityRelationships(data) {
|
||||||
clientId,
|
clientId,
|
||||||
{
|
{
|
||||||
$addToSet: {
|
$addToSet: {
|
||||||
projectIds: projectId,
|
projectIds: projectId
|
||||||
vaIds: vaId
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ new: true }
|
{ new: true }
|
||||||
|
|
@ -38,46 +37,21 @@ async function updateEntityRelationships(data) {
|
||||||
{ _id: { $in: vaId } },
|
{ _id: { $in: vaId } },
|
||||||
{
|
{
|
||||||
$addToSet: {
|
$addToSet: {
|
||||||
projectIds: projectId,
|
projectIds: projectId
|
||||||
clientIds: clientId
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
results.vaUpdate = `Updated ${vaUpdateResult.modifiedCount} VAs`;
|
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 = {};
|
const projectUpdateData = {};
|
||||||
if (clientId) {
|
if (clientId) {
|
||||||
projectUpdateData.clientId = clientId;
|
projectUpdateData.clientId = clientId;
|
||||||
}
|
}
|
||||||
|
if (vaId && Array.isArray(vaId)) {
|
||||||
|
projectUpdateData.vaAssigned = vaId;
|
||||||
|
}
|
||||||
|
|
||||||
if (Object.keys(projectUpdateData).length > 0) {
|
if (Object.keys(projectUpdateData).length > 0) {
|
||||||
const projectUpdate = await ProjectModel.findByIdAndUpdate(
|
const projectUpdate = await ProjectModel.findByIdAndUpdate(
|
||||||
|
|
@ -228,46 +202,16 @@ async function removeEntityRelationships(data) {
|
||||||
if (clientId) {
|
if (clientId) {
|
||||||
await ClientModel.findByIdAndUpdate(
|
await ClientModel.findByIdAndUpdate(
|
||||||
clientId,
|
clientId,
|
||||||
{$pull: {
|
{ $pull: { projectIds: projectId } }
|
||||||
projectIds: projectId,
|
|
||||||
vaIds: vaId
|
|
||||||
}}
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove projectId from VAs
|
// Remove projectId from VAs
|
||||||
if (vaId && Array.isArray(vaId)) {
|
if (vaId && Array.isArray(vaId)) {
|
||||||
// await VAModel.updateMany(
|
await VAModel.updateMany(
|
||||||
// { _id: { $in: vaId } },
|
{ _id: { $in: vaId } },
|
||||||
// { $pull: {
|
{ $pull: { projectIds: projectId } }
|
||||||
// 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';
|
results.projectRemoval = 'Project relationships removed successfully';
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ const projectSchema = new Schema({
|
||||||
],
|
],
|
||||||
dedicatedHours: {
|
dedicatedHours: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "0"
|
default: 0
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|
@ -166,7 +166,6 @@ const clientDetailsSchema = new Schema({
|
||||||
enum: ["CHECKING", "SAVINGS"],
|
enum: ["CHECKING", "SAVINGS"],
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
|
|
||||||
accountHolderName: {
|
accountHolderName: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
|
|
@ -424,51 +423,6 @@ const adjustedHoursSchema = new Schema({
|
||||||
}]
|
}]
|
||||||
}, { timestamps: true });
|
}, { 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 ---------------------- */
|
/* ---------------------- Model Exports ---------------------- */
|
||||||
const VAModel = model("VA", vaSchema);
|
const VAModel = model("VA", vaSchema);
|
||||||
const ClientModel = model("Client", clientSchema);
|
const ClientModel = model("Client", clientSchema);
|
||||||
|
|
@ -477,6 +431,5 @@ const LoggedModel = model("TrackLog", loggedSchema);
|
||||||
const WorkTrackModel = model("TrackWork", vaWorkTrackSchema);
|
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);
|
|
||||||
|
|
||||||
export { VAModel, ClientModel, ProjectModel, LoggedModel, WorkTrackModel, PasswordResetModel, AdjustedHoursModel, LeavesModel };
|
export { VAModel, ClientModel, ProjectModel, LoggedModel, WorkTrackModel, PasswordResetModel, AdjustedHoursModel };
|
||||||
|
|
@ -23,9 +23,6 @@ import { va_timesheet_adjustment_function } from "../../controllers/va/vaTimeshe
|
||||||
import { get_adjustment_requests_list } from "../../controllers/va/vaTimesheetAdjustment/vaAllTimesheetAdjustmentReuqest.controller.js"
|
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_timesheet_manage_adjustment_request } from "../../controllers/va/vaTimesheetAdjustment/vaManageTimesheetAdjustment.controller.js"
|
||||||
import { va_verify_email } from "../../controllers/va/vaAuth/vaVerifyEmail.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
|
vaRoutes.get("/", va_callback); // VA Callback for getting active VA
|
||||||
|
|
||||||
|
|
@ -46,17 +43,9 @@ vaRoutes.get("/:userName", va_username_callback); // VA Username Callback
|
||||||
// timesheet adjustment routes
|
// timesheet adjustment routes
|
||||||
vaRoutes.get("/timesheet/summary/:projectId/:date", authValidator, va_timesheet_summary_function); // Get Timesheet Adjustment for VA on specific project and date
|
vaRoutes.get("/timesheet/summary/:projectId/:date", 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/:status?/:projectId?/:vaId?", get_adjustment_requests_list); // show all adjustment list to approve and reject
|
||||||
vaRoutes.patch("/timesheet/adjustments/:adjustmentId", va_timesheet_manage_adjustment_request);
|
vaRoutes.patch("/timesheet/adjustments/:adjustmentId", va_timesheet_manage_adjustment_request);
|
||||||
|
|
||||||
// Leaves routes
|
|
||||||
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
|
// timesheet adjustment routes
|
||||||
|
|
||||||
// Example: GET /va/search?name=john&email=@example.com&from=2025-01-01&to=2025-01-31 (NC)
|
// Example: GET /va/search?name=john&email=@example.com&from=2025-01-01&to=2025-01-31 (NC)
|
||||||
|
|
|
||||||
157
src/server.js
157
src/server.js
|
|
@ -13,32 +13,16 @@ import cors from 'cors';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
|
||||||
import apiRoutes from './routes/index.js'
|
import apiRoutes from './routes/index.js'
|
||||||
|
|
||||||
|
|
||||||
import { render_reset_form } from "./controllers/passwordReset.controllers.js";
|
import { render_reset_form } from "./controllers/passwordReset.controllers.js";
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
|
|
||||||
const server = new createServer(app);
|
const server = new createServer(app);
|
||||||
const io = new Server(server);
|
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) => {
|
app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
|
||||||
console.log("⚠️ Certificate error:", url);
|
console.log("⚠️ Certificate error:", url);
|
||||||
|
|
||||||
|
|
@ -109,18 +93,13 @@ app.get('/varegister/:sf_token?', (req, res) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/clientregister/: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);
|
|
||||||
|
|
||||||
if (decoded) {
|
// portalId: RECORDid => 8990928342309
|
||||||
|
// email
|
||||||
|
|
||||||
|
// const { sf_token } = useParams();
|
||||||
|
|
||||||
res.render('va_client_register', { title: "Client Registration" });
|
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);
|
app.get('/auth/password/reset-form', render_reset_form);
|
||||||
|
|
@ -159,28 +138,35 @@ app.get('/client/virtual-assistants', async (req, res) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get("/live/:projectId/:vaId", async (req, res) => {
|
// app.get('/client/timesheets', async (req, res) => {
|
||||||
const { projectId, vaId } = req.params;
|
// 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' });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
if (!projectId || !vaId) {
|
// app.get('/client/invoices', async (req, res) => {
|
||||||
return res.status(400).send({ "message": "Page not found" });
|
// 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' });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
let clientId = null;
|
// app.get('/client/referrals', async (req, res) => {
|
||||||
try {
|
// try {
|
||||||
const clientRes = await ProjectModel.findById(projectId);
|
// const referralData = await referralService.getClientReferrals(req.user.id);
|
||||||
if (clientRes) {
|
// res.render('client/dashboard-referrals', referralData);
|
||||||
clientId = clientRes.clientId.toString();
|
// } catch (error) {
|
||||||
}
|
// console.error('Referrals error:', error);
|
||||||
} catch (err) {
|
// res.status(500).render('client/dashboard-referrals', { error: 'Failed to load referrals' });
|
||||||
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) => {
|
app.get("/live/:projectId/:vaId", async (req, res) => {
|
||||||
const { projectId, vaId } = req.params;
|
const { projectId, vaId } = req.params;
|
||||||
|
|
@ -207,17 +193,16 @@ app.get("/live/:projectId/:vaId", async (req, res) => {
|
||||||
|
|
||||||
// Handling Socket
|
// Handling Socket
|
||||||
import { VAModel, ProjectModel, ClientModel } from './models/vaModel.js';
|
import { VAModel, ProjectModel, ClientModel } from './models/vaModel.js';
|
||||||
|
import { ObjectId } from 'mongodb/lib/bson.js';
|
||||||
|
|
||||||
|
|
||||||
const vaSocketRespectToClient = new Map();
|
const vaSocketRespectToClient = new Map();
|
||||||
|
|
||||||
// Handling Socket
|
|
||||||
io.use(async (socket, next) => {
|
io.use(async (socket, next) => {
|
||||||
try {
|
try {
|
||||||
const token = socket.handshake.auth?.token;
|
const token = socket.handshake.auth?.token;
|
||||||
const projectId = socket.handshake.auth?.projectId;
|
const projectId = socket.handshake.auth?.projectId;
|
||||||
const userType = socket.handshake.auth?.userType;
|
const userType = socket.handshake.auth?.userType;
|
||||||
const vaId = socket.handshake.auth?.vaId;
|
|
||||||
|
|
||||||
|
|
||||||
console.log("🔐 Received token:", token);
|
console.log("🔐 Received token:", token);
|
||||||
|
|
||||||
|
|
@ -256,7 +241,7 @@ io.use(async (socket, next) => {
|
||||||
|
|
||||||
console.log(a, b, c, "vaProjClientId vs clientData.clientId");
|
console.log(a, b, c, "vaProjClientId vs clientData.clientId");
|
||||||
|
|
||||||
vaSocketRespectToClient.set(`${vaProjClientId.toString()}-${vaUser._id.toString()}-${projectId.toString()}-va`, socket.id)
|
vaSocketRespectToClient.set(`${vaProjClientId.toString()}-${vaUser._id.toString()}`, socket.id)
|
||||||
|
|
||||||
console.log("✅ Authenticated VA:", vaUser.userDetails.email);
|
console.log("✅ Authenticated VA:", vaUser.userDetails.email);
|
||||||
return next();
|
return next();
|
||||||
|
|
@ -283,7 +268,6 @@ io.use(async (socket, next) => {
|
||||||
|
|
||||||
// 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());
|
||||||
vaSocketRespectToClient.set(`${vaProjClientId.toString()}-${vaId.toString()}-${projectId.toString()}-client`, socket.id)
|
|
||||||
|
|
||||||
// console.log("✅ Authenticated Client:", clientUser.userDetails.email);
|
// console.log("✅ Authenticated Client:", clientUser.userDetails.email);
|
||||||
return next();
|
return next();
|
||||||
|
|
@ -296,6 +280,8 @@ io.use(async (socket, next) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
io.on("connection", (socket) => {
|
io.on("connection", (socket) => {
|
||||||
console.log("User connected:", socket.id);
|
console.log("User connected:", socket.id);
|
||||||
|
|
||||||
|
|
@ -330,7 +316,7 @@ io.on("connection", (socket) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// When VA sends live image
|
// When VA sends live image
|
||||||
socket.on("sendImage", async ({ token, image, title, projectId }) => {
|
socket.on("sendImage", async ({ token, image, title }) => {
|
||||||
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);
|
||||||
|
|
@ -343,42 +329,30 @@ io.on("connection", (socket) => {
|
||||||
return;
|
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 vaId = vaUser._id.toString();
|
||||||
const clientVaUniqueId = `${clientId}-${vaId}-${projectId}-client`;
|
|
||||||
const socketId = vaSocketRespectToClient.get(clientVaUniqueId);
|
|
||||||
|
|
||||||
console.log("🟢 Received image from VA:", vaId);
|
console.log("🟢 Received image from VA:", vaId);
|
||||||
|
|
||||||
// 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 (
|
||||||
// clientSocket.data.role === "client" &&
|
clientSocket.data.role === "client" &&
|
||||||
// clientSocket.data.allowedVAs?.includes(vaId)
|
clientSocket.data.allowedVAs?.includes(vaId)
|
||||||
// ) {
|
) {
|
||||||
// clientSocket.emit("receiveImage", image, title);
|
clientSocket.emit("receiveImage", image, title);
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
// Can be like clientId-projectId-vaId to uniquely identify
|
|
||||||
socket.to(socketId).emit("receiveImage", image, title);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("❌ Token verification failed:", err.message);
|
console.error("❌ Token verification failed:", err.message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("selectApp", ({ appTitle, clientId, vaId, projectId }) => {
|
socket.on("selectApp", ({ appTitle, clientId }) => {
|
||||||
console.log(`VA selected app: ${appTitle}`);
|
console.log(`VA selected app: ${appTitle}`);
|
||||||
|
|
||||||
console.log(vaSocketRespectToClient.get(`${clientId}-${vaId}-${projectId}-va`), "vaSocketRespectToClient[clientId]")
|
const sssrrrr = vaSocketRespectToClient.get(`${clientId}-${socket.data.vaId}`);
|
||||||
|
console.log(vaSocketRespectToClient.get(`${clientId}-${socket.data.vaId}`), sssrrrr, "vaSocketRespectToClient[clientId]")
|
||||||
// You can forward this info to the respective Electron client
|
// You can forward this info to the respective Electron client
|
||||||
socket.to(vaSocketRespectToClient.get(`${clientId}-${vaId}-${projectId}-va`)).emit("appToCapture", { appTitle });
|
socket.to(vaSocketRespectToClient.get(`${clientId}-${socket.data.vaId}`)).emit("appToCapture", { appTitle });
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("getVisibleApps", async ({ token, apps }) => {
|
socket.on("getVisibleApps", async ({ token, apps }) => {
|
||||||
|
|
@ -403,7 +377,7 @@ io.on("connection", (socket) => {
|
||||||
clientSocket.data.role === "client" &&
|
clientSocket.data.role === "client" &&
|
||||||
clientSocket.data.allowedVAs?.includes(vaId)
|
clientSocket.data.allowedVAs?.includes(vaId)
|
||||||
) {
|
) {
|
||||||
clientSocket.emit("receiveVisibleApps", { apps, vaId });
|
clientSocket.emit("receiveVisibleApps", apps);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -417,6 +391,35 @@ 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) => {
|
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"
|
||||||
|
|
|
||||||
|
|
@ -29,35 +29,4 @@ 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();
|
testPaths();
|
||||||
Binary file not shown.
|
|
@ -148,7 +148,8 @@
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Virtual Assistant</th>
|
<th>Virtual Assistant</th>
|
||||||
<th>Projects Assigned</th>
|
<th>Projects Assigned</th>
|
||||||
<th>Worked Time</th>
|
<th>Working from</th>
|
||||||
|
<th>Total Worked</th>
|
||||||
<th>Action</th>
|
<th>Action</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
@ -221,7 +222,7 @@
|
||||||
|
|
||||||
// Format time function
|
// Format time function
|
||||||
function formatTime(seconds) {
|
function formatTime(seconds) {
|
||||||
if (!seconds) return 'Not working';
|
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);
|
||||||
|
|
@ -230,6 +231,8 @@
|
||||||
return `${hours}h ${minutes}m`;
|
return `${hours}h ${minutes}m`;
|
||||||
} else if (minutes > 0) {
|
} else if (minutes > 0) {
|
||||||
return `${minutes}m`;
|
return `${minutes}m`;
|
||||||
|
} else if (seconds > 0) {
|
||||||
|
return `${seconds}s`;
|
||||||
} else {
|
} else {
|
||||||
return 'Just started';
|
return 'Just started';
|
||||||
}
|
}
|
||||||
|
|
@ -240,6 +243,7 @@
|
||||||
const statusClass = va.isCurrentlyActive ? 'active' : 'inactive';
|
const statusClass = va.isCurrentlyActive ? 'active' : 'inactive';
|
||||||
const statusText = va.isCurrentlyActive ? 'Active' : 'Inactive';
|
const statusText = va.isCurrentlyActive ? 'Active' : 'Inactive';
|
||||||
const workedTime = formatTime(va.currentSessionDuration);
|
const workedTime = formatTime(va.currentSessionDuration);
|
||||||
|
const totalWorkedTime = formatTime(va.totalWorkedSeconds);
|
||||||
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">
|
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>
|
<i class="fas fa-eye"></i>
|
||||||
|
|
@ -247,7 +251,7 @@
|
||||||
</a>` : '';
|
</a>` : '';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<tr>
|
<tr data-vaid="${va.id}">
|
||||||
<td>
|
<td>
|
||||||
<div class="status-indicator">
|
<div class="status-indicator">
|
||||||
<div class="status-dot ${statusClass}"></div>
|
<div class="status-dot ${statusClass}"></div>
|
||||||
|
|
@ -266,6 +270,9 @@
|
||||||
<td>
|
<td>
|
||||||
<span class="worked-time">${workedTime}</span>
|
<span class="worked-time">${workedTime}</span>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="worked-time">${totalWorkedTime}</span>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
${viewLink}
|
${viewLink}
|
||||||
</td>
|
</td>
|
||||||
|
|
@ -303,15 +310,34 @@
|
||||||
vaTableBody.style.display = '';
|
vaTableBody.style.display = '';
|
||||||
vaEmptyState.style.display = 'none';
|
vaEmptyState.style.display = 'none';
|
||||||
|
|
||||||
// Add event listeners to view links
|
// Add event listeners to table rows
|
||||||
document.querySelectorAll('.view-link').forEach(link => {
|
document.querySelectorAll('tr[data-vaid]').forEach(row => {
|
||||||
link.addEventListener('click', function(e) {
|
row.addEventListener('click', async function(e) {
|
||||||
e.preventDefault();
|
// Don't trigger if click is on view link
|
||||||
const vaId = this.getAttribute('data-va-id');
|
if (e.target.closest('.view-link')) {
|
||||||
// You can update this link as needed
|
return;
|
||||||
window.location.href = `/client/virtual-assistants/${vaId}`;
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
vaTableBody.innerHTML = '';
|
vaTableBody.innerHTML = '';
|
||||||
vaTableBody.style.display = 'none';
|
vaTableBody.style.display = 'none';
|
||||||
|
|
@ -351,14 +377,126 @@
|
||||||
// Optional: Auto-refresh data every 30 seconds
|
// Optional: Auto-refresh data every 30 seconds
|
||||||
setInterval(loadDashboardData, 30000);
|
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 || []);
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
// Create project details HTML
|
||||||
var menuToggle = document.querySelector('.mobile-menu-button');
|
const projectsHTML = projectDetails.length > 0
|
||||||
var sidebar = document.querySelector('.dashboard-sidebar');
|
? projectDetails.map(project => `
|
||||||
menuToggle.addEventListener('click', () => {
|
<div class="project-item">
|
||||||
sidebar.classList.toggle('active')
|
<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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -211,7 +211,6 @@
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<div class="logo">
|
<div class="logo">
|
||||||
<img src="/public/images/logo-white-vertical.svg" alt="DocVA Logo" />
|
<img src="/public/images/logo-white-vertical.svg" alt="DocVA Logo" />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -334,3 +333,5 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
|
@ -21,7 +21,6 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* SIDEBAR */
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
width: 280px;
|
width: 280px;
|
||||||
background: #181818;
|
background: #181818;
|
||||||
|
|
@ -82,6 +81,7 @@
|
||||||
#liveImage {
|
#liveImage {
|
||||||
width: 90%;
|
width: 90%;
|
||||||
max-width: 900px;
|
max-width: 900px;
|
||||||
|
border: 2px solid #555;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
border: 2px solid #444;
|
border: 2px solid #444;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
|
|
@ -188,15 +188,12 @@
|
||||||
<script>
|
<script>
|
||||||
const vaId = "<%= vaId %>";
|
const vaId = "<%= vaId %>";
|
||||||
const clientId = "<%= clientId %>";
|
const clientId = "<%= clientId %>";
|
||||||
const projectId = "<%= projectId %>";
|
|
||||||
const token = localStorage.getItem("knoptpq");
|
const token = localStorage.getItem("knoptpq");
|
||||||
|
|
||||||
const socket = io({
|
const socket = io({
|
||||||
auth: {
|
auth: {
|
||||||
token: token,
|
token: token,
|
||||||
userType: "client",
|
userType: "client",
|
||||||
projectId: projectId,
|
projectId: "<%= projectId %>"
|
||||||
vaId: vaId
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -230,9 +227,7 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Receive visible apps */
|
/* Receive visible apps */
|
||||||
socket.on("receiveVisibleApps", ({apps, vaId: senderVaId}) => {
|
socket.on("receiveVisibleApps", (apps) => {
|
||||||
if (vaId !== senderVaId) return;
|
|
||||||
|
|
||||||
appsList.innerHTML = "";
|
appsList.innerHTML = "";
|
||||||
|
|
||||||
if (!apps?.length) {
|
if (!apps?.length) {
|
||||||
|
|
@ -255,7 +250,7 @@
|
||||||
placeholder.style.display = "none";
|
placeholder.style.display = "none";
|
||||||
liveImage.style.display = "none";
|
liveImage.style.display = "none";
|
||||||
|
|
||||||
socket.emit("selectApp", { appTitle: app.name, clientId, vaId, projectId });
|
socket.emit("selectApp", { appTitle: app.name, clientId, vaId });
|
||||||
|
|
||||||
// Close sidebar on mobile
|
// Close sidebar on mobile
|
||||||
sidebar.classList.remove("open");
|
sidebar.classList.remove("open");
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue