Compare commits
No commits in common. "4ed62b25e823f6082edca6e0a46be399b26b9b28" and "3a6e86d6daa6298a5b54f1c37083289712d37c88" have entirely different histories.
4ed62b25e8
...
3a6e86d6da
|
|
@ -6,7 +6,6 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "nodemon --inspect src/server.js",
|
"start": "nodemon --inspect src/server.js",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"deploy": "curl -X POST http://35.95.209.161:9000/hooks/deploy-va-middleware",
|
|
||||||
"build": "npx webpack --mode production",
|
"build": "npx webpack --mode production",
|
||||||
"dev": "npx webpack --mode development"
|
"dev": "npx webpack --mode development"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -577,7 +577,7 @@ body {
|
||||||
.data-table tbody tr {
|
.data-table tbody tr {
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
transition: background-color 0.2s;
|
transition: background-color 0.2s;
|
||||||
/* cursor: pointer; */
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-table tbody tr:hover {
|
.data-table tbody tr:hover {
|
||||||
|
|
@ -1689,23 +1689,3 @@ tr[data-vaid]:hover {
|
||||||
[data-theme="dark"] .loading {
|
[data-theme="dark"] .loading {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.va-action {
|
|
||||||
text-decoration: none;
|
|
||||||
color: #fff;
|
|
||||||
background-color: #2563eb;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
text-align: center;
|
|
||||||
border-radius: 5px;
|
|
||||||
transform: scale(0.96);
|
|
||||||
transition: all 0.2s ease-in-out;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.va-info {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.va-action:hover {
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
|
|
@ -767,7 +767,7 @@ async function submitForm() {
|
||||||
nextStep(6);
|
nextStep(6);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.href = '/';
|
window.location.href = '/client/login';
|
||||||
}, 1000);
|
}, 1000);
|
||||||
} else {
|
} else {
|
||||||
// Handle server errors
|
// Handle server errors
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ async function client_dashboard_projects(req, res) {
|
||||||
|
|
||||||
// STEP 3: Get all VAs assigned to these projects
|
// STEP 3: Get all VAs assigned to these projects
|
||||||
const allAssignedVAs = await VAModel.find({
|
const allAssignedVAs = await VAModel.find({
|
||||||
_id: { $in: allProjectVaIds.vaId }
|
_id: { $in: allProjectVaIds }
|
||||||
})
|
})
|
||||||
.select('userDetails.firstName userDetails.lastName userDetails.email status')
|
.select('userDetails.firstName userDetails.lastName userDetails.email status')
|
||||||
.lean()
|
.lean()
|
||||||
|
|
|
||||||
|
|
@ -2,22 +2,9 @@ import VaClientRepository from "../../repository/vaClient.repository.js";
|
||||||
import VaClientService from "../../services/vaClient.service.js";
|
import VaClientService from "../../services/vaClient.service.js";
|
||||||
import logger from "../../config/logger.js";
|
import logger from "../../config/logger.js";
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
import PasswordResetService from "../../services/passwordReset.service.js";
|
|
||||||
import PasswordResetRepository from "../../repository/passwordReset.repository.js";
|
|
||||||
|
|
||||||
|
|
||||||
const vaClientService = new VaClientService(new VaClientRepository());
|
const vaClientService = new VaClientService(new VaClientRepository());
|
||||||
|
|
||||||
const passwordResetService = new PasswordResetService(
|
|
||||||
new PasswordResetRepository(),
|
|
||||||
// new EmailService({
|
|
||||||
// host: process.env.SMTP_HOST,
|
|
||||||
// port: process.env.SMTP_PORT,
|
|
||||||
// user: process.env.SMTP_USER,
|
|
||||||
// pass: process.env.SMTP_PASS,
|
|
||||||
// from: process.env.EMAIL_FROM,
|
|
||||||
// })
|
|
||||||
);
|
|
||||||
|
|
||||||
async function va_register_client(req, res) {
|
async function va_register_client(req, res) {
|
||||||
logger.info("ClientController - Register Client Hit ...");
|
logger.info("ClientController - Register Client Hit ...");
|
||||||
|
|
@ -35,9 +22,6 @@ async function va_register_client(req, res) {
|
||||||
const created = await vaClientService.registerClient(payload);
|
const created = await vaClientService.registerClient(payload);
|
||||||
|
|
||||||
logger.info(`ClientController - Client registered successfully: ${userDetails.email}`);
|
logger.info(`ClientController - Client registered successfully: ${userDetails.email}`);
|
||||||
await passwordResetService.requestPasswordReset(userDetails.email, 'client');
|
|
||||||
logger.info(`Client password reset requested for: ${userDetails.email}`);
|
|
||||||
|
|
||||||
return res.status(201).json({
|
return res.status(201).json({
|
||||||
success: true,
|
success: true,
|
||||||
data: created,
|
data: created,
|
||||||
|
|
@ -191,7 +175,7 @@ async function update_client(req, res) {
|
||||||
|
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: error.message
|
message: "Internal Server Error"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -300,8 +284,8 @@ async function update_client_password(req, res) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error.message.includes("Current password is incorrect") ||
|
if (error.message.includes("Current password is incorrect") ||
|
||||||
error.message.includes("New passwords do not match") ||
|
error.message.includes("New passwords do not match") ||
|
||||||
error.message.includes("All password fields are required")) {
|
error.message.includes("All password fields are required")) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: error.message
|
message: error.message
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import bcrypt from "bcrypt";
|
import bcrypt from "bcrypt";
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
import { ClientModel, VAModel } from "../../models/vaModel.js";
|
import { ClientModel } from "../../models/vaModel.js";
|
||||||
import { va_timesheet_adjustment_function } from "../va/vaTimesheetAdjustment/vaTimesheetAdjustment.controller.js";
|
|
||||||
|
|
||||||
// Login VA
|
// Login VA
|
||||||
async function va_login_client(req, res) {
|
async function va_login_client(req, res) {
|
||||||
|
|
@ -14,17 +13,8 @@ async function va_login_client(req, res) {
|
||||||
return res.status(400).json({ success: false, message: "Please provide email and password" });
|
return res.status(400).json({ success: false, message: "Please provide email and password" });
|
||||||
}
|
}
|
||||||
|
|
||||||
let isVa = false;
|
|
||||||
|
|
||||||
// Find user by email
|
// Find user by email
|
||||||
let user = await ClientModel.findOne({ "userDetails.email": userDetails.email });
|
const user = await ClientModel.findOne({ "userDetails.email": userDetails.email });
|
||||||
const vaUser = await VAModel.findOne({ "userDetails.email": userDetails.email });
|
|
||||||
|
|
||||||
if (vaUser) {
|
|
||||||
isVa = true
|
|
||||||
user = vaUser
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return res.status(404).json({ success: false, message: "Invalid credentials" });
|
return res.status(404).json({ success: false, message: "Invalid credentials" });
|
||||||
}
|
}
|
||||||
|
|
@ -59,7 +49,6 @@ async function va_login_client(req, res) {
|
||||||
id: user._id,
|
id: user._id,
|
||||||
email: user.userDetails.email,
|
email: user.userDetails.email,
|
||||||
name: user.userDetails.firstName,
|
name: user.userDetails.firstName,
|
||||||
userType: isVa ? "VA" : "CLIENT",
|
|
||||||
},
|
},
|
||||||
process.env.JWT_SECRET_REFRESH_TOKEN || "DocTrackerSecretKeyRefreshToken",
|
process.env.JWT_SECRET_REFRESH_TOKEN || "DocTrackerSecretKeyRefreshToken",
|
||||||
{ expiresIn: "30d" }
|
{ expiresIn: "30d" }
|
||||||
|
|
@ -80,7 +69,6 @@ async function va_login_client(req, res) {
|
||||||
name: user.userDetails.firstName,
|
name: user.userDetails.firstName,
|
||||||
email: user.userDetails.email,
|
email: user.userDetails.email,
|
||||||
},
|
},
|
||||||
userType: isVa ? "VA" : "CLIENT",
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("va_client_login_register error:", error);
|
console.error("va_client_login_register error:", error);
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ async function delete_project_sf(req, res) {
|
||||||
// 2. Remove project from all assigned VAs' projectIds arrays
|
// 2. Remove project from all assigned VAs' projectIds arrays
|
||||||
if (vaAssigned.length > 0) {
|
if (vaAssigned.length > 0) {
|
||||||
const vaResult = await VAModel.updateMany(
|
const vaResult = await VAModel.updateMany(
|
||||||
{ _id: { $in: vaAssigned.vaId } },
|
{ _id: { $in: vaAssigned } },
|
||||||
{ $pull: { projectIds: projectObjectId } }
|
{ $pull: { projectIds: projectObjectId } }
|
||||||
);
|
);
|
||||||
deletionSummary.vasUpdated = vaResult.modifiedCount;
|
deletionSummary.vasUpdated = vaResult.modifiedCount;
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ import { updateEntityRelationships } from "../../helper/updateEntityRelationship
|
||||||
// Register a new VA
|
// Register a new VA
|
||||||
async function project_add_sf(req, res) {
|
async function project_add_sf(req, res) {
|
||||||
try {
|
try {
|
||||||
const payload = req.body || {};
|
const payload = req.body|| {};
|
||||||
const { name, hourlyRate, clientId, vaAssigned, dedicatedHours, vaWorkingScheduleStart, vaWorkingScheduleEnd, isFlexibleAllowed, vaWorkingDelayAllowedStart, vaWorkingDelayAllowedEnd } = payload;
|
const { name, hourlyRate, clientId, vaAssigned, dedicatedHours, vaWorkingScheduleStart, vaWorkingScheduleEnd, isFlexibleAllowedPt, vaWorkingDelayAllowedStart, vaWorkingDelayAllowedEnd } = payload;
|
||||||
|
|
||||||
// Validate required fields
|
// Validate required fields
|
||||||
if (!name) {
|
if (!name) {
|
||||||
|
|
@ -34,55 +34,46 @@ async function project_add_sf(req, res) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate and convert vaAssigned to array of ObjectIds
|
// Validate and convert vaAssigned to array of ObjectIds
|
||||||
// let vaAssignedArray = [];
|
let vaAssignedArray = [];
|
||||||
// if (vaAssigned) {
|
if (vaAssigned) {
|
||||||
// if (Array.isArray(vaAssigned)) {
|
if (Array.isArray(vaAssigned)) {
|
||||||
// try {
|
try {
|
||||||
// vaAssignedArray = vaAssigned.map(vaId => {
|
vaAssignedArray = vaAssigned.map(vaId => new Types.ObjectId(vaId));
|
||||||
// if(typeof vaId === 'object' && "isFlexibleAllowed" in vaId){
|
} catch (error) {
|
||||||
// // Handle Error form SF Side for now - That vaId is coming as object
|
return res.status(400).json({
|
||||||
// return new Types.ObjectId(vaId.vaId)
|
success: false,
|
||||||
// } else {
|
message: "Invalid VA ID format in vaAssigned array"
|
||||||
// return new Types.ObjectId(vaId)
|
});
|
||||||
// }
|
}
|
||||||
// });
|
} else if (typeof vaAssigned === 'string') {
|
||||||
// } catch (error) {
|
// Handle single VA ID as string
|
||||||
// return res.status(400).json({
|
try {
|
||||||
// success: false,
|
vaAssignedArray = [new Types.ObjectId(vaAssigned)];
|
||||||
// message: "Invalid VA ID format in vaAssigned array"
|
} catch (error) {
|
||||||
// });
|
return res.status(400).json({
|
||||||
// }
|
success: false,
|
||||||
// } else if (typeof vaAssigned === 'string') {
|
message: "Invalid VA ID format"
|
||||||
// // Handle single VA ID as string
|
});
|
||||||
// try {
|
}
|
||||||
// vaAssignedArray = [new Types.ObjectId(vaAssigned)];
|
}
|
||||||
// } catch (error) {
|
}
|
||||||
// return res.status(400).json({
|
|
||||||
// success: false,
|
|
||||||
// message: "Invalid VA ID format"
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// sanitize/construct allowed fields
|
// sanitize/construct allowed fields
|
||||||
const toCreate = {
|
const toCreate = {
|
||||||
name: name.trim(),
|
name: name.trim(),
|
||||||
hourlyRate: hourlyRate || '0',
|
hourlyRate: hourlyRate || '0',
|
||||||
clientId: clientObjectId,
|
clientId: clientObjectId,
|
||||||
vaAssigned: vaAssigned,
|
vaAssigned: vaAssignedArray,
|
||||||
dedicatedHours: dedicatedHours ? parseFloat(dedicatedHours) : 0,
|
dedicatedHours: dedicatedHours ? parseFloat(dedicatedHours) : 0,
|
||||||
vaWorkingScheduleStart: vaWorkingScheduleStart || null,
|
vaWorkingScheduleStart: vaWorkingScheduleStart || null,
|
||||||
vaWorkingScheduleEnd: vaWorkingScheduleEnd || null,
|
vaWorkingScheduleEnd: vaWorkingScheduleEnd || null,
|
||||||
isFlexibleAllowed: isFlexibleAllowed || true,
|
isFlexibleAllowedPt: isFlexibleAllowedPt || true,
|
||||||
vaWorkingDelayAllowedStart: vaWorkingDelayAllowedStart || null,
|
vaWorkingDelayAllowedStart: vaWorkingDelayAllowedStart || null,
|
||||||
vaWorkingDelayAllowedEnd: vaWorkingDelayAllowedEnd || null
|
vaWorkingDelayAllowedEnd: vaWorkingDelayAllowedEnd || null
|
||||||
};
|
};
|
||||||
|
|
||||||
const created = await ProjectModel.create(toCreate);
|
const created = await ProjectModel.create(toCreate);
|
||||||
|
|
||||||
const vaAssignedArray = vaAssigned ? vaAssigned.map(vaDtl => vaDtl.vaId) : [];
|
|
||||||
|
|
||||||
const relationshipUpdate = await updateEntityRelationships({
|
const relationshipUpdate = await updateEntityRelationships({
|
||||||
project: {
|
project: {
|
||||||
projectId: created._id,
|
projectId: created._id,
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,10 @@ async function update_project_sf(req, res) {
|
||||||
vaAssigned,
|
vaAssigned,
|
||||||
dedicatedHours,
|
dedicatedHours,
|
||||||
status,
|
status,
|
||||||
totalLoggedHours,
|
totalLoggedHours ,
|
||||||
vaWorkingScheduleStart,
|
vaWorkingScheduleStart,
|
||||||
vaWorkingScheduleEnd,
|
vaWorkingScheduleEnd,
|
||||||
isFlexibleAllowed,
|
isFlexibleAllowedPt,
|
||||||
vaWorkingDelayAllowedStart,
|
vaWorkingDelayAllowedStart,
|
||||||
vaWorkingDelayAllowedEnd
|
vaWorkingDelayAllowedEnd
|
||||||
} = payload;
|
} = payload;
|
||||||
|
|
@ -102,8 +102,8 @@ async function update_project_sf(req, res) {
|
||||||
updateData.vaWorkingScheduleEnd = vaWorkingScheduleEnd || null;
|
updateData.vaWorkingScheduleEnd = vaWorkingScheduleEnd || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isFlexibleAllowed !== undefined) {
|
if (isFlexibleAllowedPt !== undefined) {
|
||||||
updateData.isFlexibleAllowed = isFlexibleAllowed;
|
updateData.isFlexibleAllowedPt = isFlexibleAllowedPt;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (vaWorkingDelayAllowedStart !== undefined) {
|
if (vaWorkingDelayAllowedStart !== undefined) {
|
||||||
|
|
@ -114,26 +114,15 @@ async function update_project_sf(req, res) {
|
||||||
updateData.vaWorkingDelayAllowedEnd = vaWorkingDelayAllowedEnd || null;
|
updateData.vaWorkingDelayAllowedEnd = vaWorkingDelayAllowedEnd || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (vaAssigned !== undefined && vaAssigned !== null && Array.isArray(vaAssigned)) {
|
|
||||||
updateData.vaAssigned = vaAssigned;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle VA assignments - this requires relationship updates
|
// Handle VA assignments - this requires relationship updates
|
||||||
if (vaAssigned !== undefined) {
|
if (vaAssigned !== undefined) {
|
||||||
let newVaAssignedArray = updateData.vaAssigned.map(vaDtl => vaDtl.vaId);
|
let newVaAssignedArray = [];
|
||||||
|
|
||||||
// Parse and validate VA IDs
|
// Parse and validate VA IDs
|
||||||
if (vaAssigned && vaAssigned.length > 0) {
|
if (vaAssigned && vaAssigned.length > 0) {
|
||||||
if (Array.isArray(vaAssigned)) {
|
if (Array.isArray(vaAssigned)) {
|
||||||
try {
|
try {
|
||||||
newVaAssignedArray = vaAssigned.map(vaId => {
|
newVaAssignedArray = vaAssigned.map(vaId => new Types.ObjectId(vaId));
|
||||||
if (typeof vaId === 'object' && "isFlexibleAllowed" in vaId) {
|
|
||||||
// Handle Error form SF Side for now - That vaId is coming as object
|
|
||||||
return new Types.ObjectId(vaId.vaId)
|
|
||||||
} else {
|
|
||||||
return new Types.ObjectId(vaId)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
|
|
@ -152,14 +141,14 @@ async function update_project_sf(req, res) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateData.vaAssigned = newVaAssignedArray;
|
updateData.vaAssigned = newVaAssignedArray;
|
||||||
|
|
||||||
// Calculate VAs to add and remove for relationship updates
|
// Calculate VAs to add and remove for relationship updates
|
||||||
const currentVaIds = existingProject.vaAssigned.map(va => va.vaId.toString());
|
const currentVaIds = existingProject.vaAssigned.map(va => va.toString());
|
||||||
const newVaIds = newVaAssignedArray.map(va => va.toString());
|
const newVaIds = newVaAssignedArray.map(va => va.toString());
|
||||||
|
|
||||||
const vaToAdd = newVaAssignedArray.filter(va => !currentVaIds.includes(va.toString()));
|
const vaToAdd = newVaAssignedArray.filter(va => !currentVaIds.includes(va.toString()));
|
||||||
const vaToRemove = existingProject.vaAssigned.filter(va => !newVaIds.includes(va.vaId.toString()));
|
const vaToRemove = existingProject.vaAssigned.filter(va => !newVaIds.includes(va.toString()));
|
||||||
|
|
||||||
// Update relationships for added VAs
|
// Update relationships for added VAs
|
||||||
if (vaToAdd.length > 0) {
|
if (vaToAdd.length > 0) {
|
||||||
|
|
@ -198,7 +187,7 @@ async function update_project_sf(req, res) {
|
||||||
updateData,
|
updateData,
|
||||||
{ new: true, runValidators: true }
|
{ new: true, runValidators: true }
|
||||||
).populate('clientId', 'userDetails.name userDetails.email')
|
).populate('clientId', 'userDetails.name userDetails.email')
|
||||||
.populate('vaAssigned', 'userDetails.firstName userDetails.lastName userDetails.email');
|
.populate('vaAssigned', 'userDetails.firstName userDetails.lastName userDetails.email');
|
||||||
|
|
||||||
// If VA assignments were changed but relationship update failed, log warning
|
// If VA assignments were changed but relationship update failed, log warning
|
||||||
if (vaAssigned !== undefined) {
|
if (vaAssigned !== undefined) {
|
||||||
|
|
|
||||||
|
|
@ -147,57 +147,55 @@ async function va_leave_apply_function(req, res) {
|
||||||
await leaveRequest.save();
|
await leaveRequest.save();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/leave-request`;
|
// const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/timesheet-adjustment`;
|
||||||
const sfData = {
|
// const sfData = {
|
||||||
"leaveId": leaveRequest._id.toString(),
|
// "id": leaveRequest._id,
|
||||||
// "va": {
|
// "va": {
|
||||||
// "id": vaId
|
// "id": vaId
|
||||||
// },
|
// },
|
||||||
"va": vaId.toString(),
|
// "dateFrom": startDate.toISOString().split('T')[0],
|
||||||
"dateFrom": startDate.toISOString().split('T')[0],
|
// "dateTo": endDate.toISOString().split('T')[0],
|
||||||
"dateTo": endDate.toISOString().split('T')[0],
|
// "totalDays": totalDays,
|
||||||
"totalDays": totalDays,
|
// "leaveType": leaveType,
|
||||||
"leaveType": leaveType,
|
// "comment": processedComment.plainText, // Send plain text to Salesforce
|
||||||
"comment": processedComment.plainText !== "" ? processedComment.plainText : "-", // Send plain text to Salesforce
|
// "attachments": processedComment.extractedImages, // Send extracted images info
|
||||||
"attachments": processedComment.extractedImages, // Send extracted images info
|
// "status": "PENDING",
|
||||||
"status": "PENDING",
|
// "rejectionReason": ""
|
||||||
"rejectionReason": ""
|
// }
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(salesforceUrl, {
|
// const response = await fetch(salesforceUrl, {
|
||||||
method: 'POST',
|
// method: 'POST',
|
||||||
headers: {
|
// headers: {
|
||||||
'Content-Type': 'application/json',
|
// 'Content-Type': 'application/json',
|
||||||
// Add any required headers for Salesforce authentication
|
// // Add any required headers for Salesforce authentication
|
||||||
// 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
|
// // 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
|
||||||
},
|
// },
|
||||||
body: JSON.stringify({ "data": sfData })
|
// body: JSON.stringify({"data": sfData})
|
||||||
});
|
// });
|
||||||
|
|
||||||
if (!response.ok) {
|
// if (!response.ok) {
|
||||||
const errorText = await response.text();
|
// const errorText = await response.text();
|
||||||
await LeavesModel.findByIdAndDelete(leaveRequest._id)
|
// throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
|
||||||
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
|
// }
|
||||||
}
|
|
||||||
|
|
||||||
// const result = await response.json();
|
// const result = await response.json();
|
||||||
// console.log("✅ Successfully synced VA data to Salesforce:", result);
|
// // console.log("✅ Successfully synced VA data to Salesforce:", result);
|
||||||
|
|
||||||
// return res.status(201).json({
|
return res.status(201).json({
|
||||||
// success: true,
|
success: true,
|
||||||
// message: "Leave request submitted successfully",
|
message: "Leave request submitted successfully",
|
||||||
// data: {
|
data: {
|
||||||
// leaveId: leaveRequest._id,
|
leaveId: leaveRequest._id,
|
||||||
// dateFrom: startDate.toISOString().split('T')[0],
|
dateFrom: startDate.toISOString().split('T')[0],
|
||||||
// dateTo: endDate.toISOString().split('T')[0],
|
dateTo: endDate.toISOString().split('T')[0],
|
||||||
// totalDays: totalDays,
|
totalDays: totalDays,
|
||||||
// leaveType,
|
leaveType,
|
||||||
// comment: processedComment.processedHTML,
|
comment: processedComment.processedHTML,
|
||||||
// // attachments: processedComment.extractedImages,
|
// attachments: processedComment.extractedImages,
|
||||||
// status: "PENDING"
|
status: "PENDING"
|
||||||
// },
|
},
|
||||||
// // SyncSalesforce: result
|
// SyncSalesforce: result
|
||||||
// });
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to sync with Salesforce:", error.message);
|
console.error("Failed to sync with Salesforce:", error.message);
|
||||||
console.log(error)
|
console.log(error)
|
||||||
|
|
@ -208,19 +206,19 @@ async function va_leave_apply_function(req, res) {
|
||||||
throw salesforceError;
|
throw salesforceError;
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(201).json({
|
// return res.status(201).json({
|
||||||
success: true,
|
// success: true,
|
||||||
message: "Leave request submitted successfully",
|
// message: "Leave request submitted successfully",
|
||||||
data: {
|
// data: {
|
||||||
leaveId: leaveRequest._id,
|
// leaveId: leaveRequest._id,
|
||||||
dateFrom: startDate.toISOString().split('T')[0],
|
// dateFrom: startDate.toISOString().split('T')[0],
|
||||||
dateTo: endDate.toISOString().split('T')[0],
|
// dateTo: endDate.toISOString().split('T')[0],
|
||||||
totalDays: totalDays,
|
// totalDays: totalDays,
|
||||||
leaveType,
|
// leaveType,
|
||||||
comment,
|
// comment,
|
||||||
status: "PENDING"
|
// status: "PENDING"
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Leave apply error:", error);
|
console.error("Leave apply error:", error);
|
||||||
|
|
|
||||||
|
|
@ -69,13 +69,13 @@ async function checkDailyHoursAndLogout(vaId, vaEmail, projectId, project, curre
|
||||||
try {
|
try {
|
||||||
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
|
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
|
||||||
const sfDataSync = {
|
const sfDataSync = {
|
||||||
"_id": logoutLog._id,
|
"_id": logoutLog._id,
|
||||||
"loggedType": "LOGOUT",
|
"loggedType": "LOGOUT",
|
||||||
"vaId": vaId,
|
"vaId": vaId,
|
||||||
"vaEmail": vaEmail,
|
"vaEmail": vaEmail,
|
||||||
"projectId": projectId,
|
"projectId": projectId,
|
||||||
"time": logoutLog.time,
|
"time": logoutLog.time,
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(salesforceUrl, {
|
const response = await fetch(salesforceUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
@ -147,12 +147,7 @@ async function va_log_track_work_callback(req, res) {
|
||||||
|
|
||||||
const vaId = vaUser._id;
|
const vaId = vaUser._id;
|
||||||
const payload = req.body || {};
|
const payload = req.body || {};
|
||||||
|
const { projectId, logId, title, appName, url = null } = payload;
|
||||||
let { projectId, logId, title, appName, url = null } = payload;
|
|
||||||
|
|
||||||
if (title === "") {
|
|
||||||
title = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
const project = await ProjectModel.findById(projectId);
|
const project = await ProjectModel.findById(projectId);
|
||||||
if (!project) return res.status(400).json({ success: false, message: "Project not found" });
|
if (!project) return res.status(400).json({ success: false, message: "Project not found" });
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ async function va_payslip_data_function(req, res) {
|
||||||
return res.status(404).json({ success: false, message: "VA not found" });
|
return res.status(404).json({ success: false, message: "VA not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-payslip/${vaId}`;
|
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-payslip?vaId=${vaId}`;
|
||||||
|
|
||||||
const response = await fetch(salesforceUrl, {
|
const response = await fetch(salesforceUrl, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ async function va_remove_function(req, res) {
|
||||||
if (projectIds.length > 0) {
|
if (projectIds.length > 0) {
|
||||||
const projectResult = await ProjectModel.updateMany(
|
const projectResult = await ProjectModel.updateMany(
|
||||||
{ _id: { $in: projectIds } },
|
{ _id: { $in: projectIds } },
|
||||||
{ $pull: { "vaAssigned.vaId": vaId } }
|
{ $pull: { vaAssigned: vaId } }
|
||||||
);
|
);
|
||||||
projectsUpdated = projectResult.modifiedCount;
|
projectsUpdated = projectResult.modifiedCount;
|
||||||
console.log(`Removed VA from ${projectsUpdated} projects`);
|
console.log(`Removed VA from ${projectsUpdated} projects`);
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { VAModel } from "../../../models/vaModel.js";
|
||||||
// Register a new VA
|
// Register a new VA
|
||||||
async function va_profile_update_callback(req, res) {
|
async function va_profile_update_callback(req, res) {
|
||||||
try {
|
try {
|
||||||
const sf_token = req.headers["x-auth-token"];
|
const sf_token = req.headers.sf_token;
|
||||||
const accessToken = req.headers.token;
|
const accessToken = req.headers.token;
|
||||||
|
|
||||||
let vaUser;
|
let vaUser;
|
||||||
|
|
@ -68,7 +68,7 @@ async function va_profile_update_callback(req, res) {
|
||||||
const allowedFields = [
|
const allowedFields = [
|
||||||
"firstName", "lastName", "dob", "age", "address",
|
"firstName", "lastName", "dob", "age", "address",
|
||||||
"city", "state", "country", "phone", "skype",
|
"city", "state", "country", "phone", "skype",
|
||||||
"paymentMethod", "payoneerAccount", "veemAccount", "wiseEmail"
|
"paymentMethod", "payoneerAccount", "veemAccount"
|
||||||
];
|
];
|
||||||
|
|
||||||
let hasUpdates = false;
|
let hasUpdates = false;
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ async function va_register(req, res) {
|
||||||
res.status(201).json({ success: true, data: created, SyncSalesforce: result });
|
res.status(201).json({ success: true, data: created, SyncSalesforce: result });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("❌ Failed to sync with Salesforce:", error.message);
|
console.error("❌ Failed to sync with Salesforce:", error.message);
|
||||||
await VAModel.findOneAndDelete({_id: created.userDetails._id});
|
|
||||||
// Create a custom error to distinguish Salesforce errors
|
// Create a custom error to distinguish Salesforce errors
|
||||||
const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
|
const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
|
||||||
salesforceError.isSalesforceError = true;
|
salesforceError.isSalesforceError = true;
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,7 @@ function getCurrentUTCTime() {
|
||||||
// console.log(`Schedule Check - Current: ${currentMinutes}, Start: ${scheduleStartMinutes}, End: ${scheduleEndMinutes}, Allowed Start: ${allowedStartMinutes}, Allowed End: ${allowedEndMinutes}`);
|
// console.log(`Schedule Check - Current: ${currentMinutes}, Start: ${scheduleStartMinutes}, End: ${scheduleEndMinutes}, Allowed Start: ${allowedStartMinutes}, Allowed End: ${allowedEndMinutes}`);
|
||||||
|
|
||||||
// // Check for PARTTIME with flexible schedule
|
// // Check for PARTTIME with flexible schedule
|
||||||
// if (vaStatus === "PARTTIME" && project.isFlexibleAllowed) {
|
// if (vaStatus === "PARTTIME" && project.isFlexibleAllowedPt) {
|
||||||
// return { isCompliant: true, warnings };
|
// return { isCompliant: true, warnings };
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
|
@ -366,7 +366,7 @@ async function va_project_log_callback(req, res) {
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
// console.log("✅ Successfully synced VA data to Salesforce:", result);
|
// console.log("✅ Successfully synced VA data to Salesforce:", result);
|
||||||
|
|
||||||
return res.status(201).json({
|
res.status(201).json({
|
||||||
success: true,
|
success: true,
|
||||||
message: `${type} Successfully`,
|
message: `${type} Successfully`,
|
||||||
logId: createdLog._id,
|
logId: createdLog._id,
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ async function va_projects_log_list_function(req, res) {
|
||||||
|
|
||||||
console.log("VA ID:", vaId.toString());
|
console.log("VA ID:", vaId.toString());
|
||||||
|
|
||||||
projectList = await ProjectModel.find({ "vaAssigned.vaId": vaUser._id });
|
projectList = await ProjectModel.find({ vaAssigned: vaUser._id });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!projectList.length) {
|
if (!projectList.length) {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ function formatHMS(ms) {
|
||||||
|
|
||||||
async function va_projects_report_function(req, res) {
|
async function va_projects_report_function(req, res) {
|
||||||
try {
|
try {
|
||||||
const sf_token = req.headers["x-auth-token"];
|
const sf_token = req.headers.sf_token;
|
||||||
let vaId;
|
let vaId;
|
||||||
let vaUser;
|
let vaUser;
|
||||||
let vaEmail;
|
let vaEmail;
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,15 @@
|
||||||
import mongoose from "mongoose";
|
|
||||||
import { AdjustedHoursModel } from "../../../models/vaModel.js";
|
import { AdjustedHoursModel } from "../../../models/vaModel.js";
|
||||||
|
|
||||||
// Get all adjustment requests (with filtering options)
|
// Get all adjustment requests (with filtering options)
|
||||||
async function get_adjustment_requests_list(req, res) {
|
async function get_adjustment_requests_list(req, res) {
|
||||||
try {
|
try {
|
||||||
const { status, projectId, vaId } = req.query;
|
const { status, projectId, vaId } = req.params;
|
||||||
|
|
||||||
// Build filter object
|
// Build filter object
|
||||||
const filter = {};
|
const filter = {};
|
||||||
// if (status) filter.status = status;
|
if (status) filter.status = status;
|
||||||
// if (projectId) filter.projectId = projectId;
|
if (projectId) filter.projectId = projectId;
|
||||||
// if (vaId) filter.vaId = vaId;
|
if (vaId) filter.vaId = vaId;
|
||||||
|
|
||||||
if (status && status !== "undefined") filter.status = status;
|
|
||||||
if (projectId && mongoose.Types.ObjectId.isValid(projectId)) {
|
|
||||||
filter.projectId = new mongoose.Types.ObjectId(projectId);
|
|
||||||
}
|
|
||||||
if (vaId && mongoose.Types.ObjectId.isValid(vaId)) {
|
|
||||||
filter.vaId = new mongoose.Types.ObjectId(vaId);
|
|
||||||
}
|
|
||||||
|
|
||||||
const adjustments = await AdjustedHoursModel.find(filter)
|
const adjustments = await AdjustedHoursModel.find(filter)
|
||||||
.sort({ createdAt: -1 })
|
.sort({ createdAt: -1 })
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ async function va_timesheet_manage_adjustment_request(req, res) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action === "APPROVE") {
|
if (action === "approve") {
|
||||||
// Create new log entries in LoggedModel
|
// Create new log entries in LoggedModel
|
||||||
const loginLog = new LoggedModel({
|
const loginLog = new LoggedModel({
|
||||||
loggedType: "LOGIN",
|
loggedType: "LOGIN",
|
||||||
|
|
@ -78,7 +78,7 @@ async function va_timesheet_manage_adjustment_request(req, res) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
} else if (action === "REJECT") {
|
} else if (action === "reject") {
|
||||||
// Update adjustment record as rejected
|
// Update adjustment record as rejected
|
||||||
adjustment.status = "REJECTED";
|
adjustment.status = "REJECTED";
|
||||||
adjustment.reviewedAt = new Date();
|
adjustment.reviewedAt = new Date();
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ async function va_timesheet_adjustment_function(req, res) {
|
||||||
// Validate project exists and VA is assigned to it
|
// Validate project exists and VA is assigned to it
|
||||||
const project = await ProjectModel.findOne({
|
const project = await ProjectModel.findOne({
|
||||||
_id: projectId,
|
_id: projectId,
|
||||||
"vaAssigned.vaId": vaId
|
vaAssigned: vaId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!project) {
|
if (!project) {
|
||||||
|
|
@ -138,17 +138,17 @@ async function va_timesheet_adjustment_function(req, res) {
|
||||||
try {
|
try {
|
||||||
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/timesheet-adjustment`;
|
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/timesheet-adjustment`;
|
||||||
const sfData = {
|
const sfData = {
|
||||||
"id": adjustmentRequest._id.toString(),
|
"id": adjustmentRequest._id,
|
||||||
"va": {
|
"va": {
|
||||||
"id": vaId.toString()
|
"id": vaId
|
||||||
},
|
},
|
||||||
"project": {
|
"project": {
|
||||||
"id": projectId.toString()
|
"id": projectId
|
||||||
},
|
},
|
||||||
"date": adjustmentDate.toISOString().split('T')[0],
|
"date": adjustmentDate.toISOString().split('T')[0],
|
||||||
"loginTime": loginDateTime.toISOString(),
|
"loginTime": loginDateTime.toISOString(),
|
||||||
"logoutTime": logoutDateTime.toISOString(),
|
"logoutTime": logoutDateTime.toISOString(),
|
||||||
"comment": processedComment.plainText !== "" ? processedComment.plainText : "-", // Send plain text to Salesforce
|
"comment": processedComment.plainText, // Send plain text to Salesforce
|
||||||
"attachments": processedComment.extractedImages, // Send extracted images info
|
"attachments": processedComment.extractedImages, // Send extracted images info
|
||||||
"status": "PENDING",
|
"status": "PENDING",
|
||||||
"rejectionReason": "",
|
"rejectionReason": "",
|
||||||
|
|
@ -167,7 +167,6 @@ async function va_timesheet_adjustment_function(req, res) {
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorText = await response.text();
|
const errorText = await response.text();
|
||||||
await AdjustedHoursModel.findByIdAndDelete(adjustmentRequest._id)
|
|
||||||
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
|
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { VAModel, ProjectModel, LoggedModel } from "../../../models/vaModel.js";
|
||||||
|
|
||||||
async function va_timesheet_summary_function(req, res) {
|
async function va_timesheet_summary_function(req, res) {
|
||||||
try {
|
try {
|
||||||
const sf_token = req.headers["x-auth-token"];
|
const sf_token = req.headers.sf_token;
|
||||||
let vaId;
|
let vaId;
|
||||||
let vaUser;
|
let vaUser;
|
||||||
let vaEmail;
|
let vaEmail;
|
||||||
|
|
@ -43,7 +43,7 @@ async function va_timesheet_summary_function(req, res) {
|
||||||
// Validate project exists and VA is assigned to it
|
// Validate project exists and VA is assigned to it
|
||||||
const project = await ProjectModel.findOne({
|
const project = await ProjectModel.findOne({
|
||||||
_id: projectId,
|
_id: projectId,
|
||||||
"vaAssigned.vaId": vaId
|
vaAssigned: vaId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!project) {
|
if (!project) {
|
||||||
|
|
|
||||||
|
|
@ -1,143 +0,0 @@
|
||||||
import { VAModel, TipsModel, ClientModel } from "../../../models/vaModel.js";
|
|
||||||
|
|
||||||
// Approve or reject leave
|
|
||||||
async function va_tips_manage_function(req, res) {
|
|
||||||
|
|
||||||
const { clientId, amount, description, vaId } = req.body;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const vaUser = await VAModel.findById(vaId);
|
|
||||||
if (!vaUser) {
|
|
||||||
return res.status(404).json({ success: false, message: "Virtual Assistant not found" });
|
|
||||||
}
|
|
||||||
const clUser = await ClientModel.findById(clientId);
|
|
||||||
if (!clUser) {
|
|
||||||
return res.status(404).json({ success: false, message: "Client not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create new tip entry
|
|
||||||
const newTip = {
|
|
||||||
clientId,
|
|
||||||
status: "Unbilled",
|
|
||||||
amount: parseFloat(amount).toFixed(2),
|
|
||||||
description: description || "",
|
|
||||||
vaName: vaUser.userDetails.firstName ? vaUser.userDetails.firstName : "" + " " + vaUser.userDetails.lastName ? vaUser.userDetails.lastName : "",
|
|
||||||
vaId,
|
|
||||||
};
|
|
||||||
|
|
||||||
const createdTip = await TipsModel.create(newTip);
|
|
||||||
|
|
||||||
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-tips`;
|
|
||||||
|
|
||||||
const sfData = {
|
|
||||||
"id": createdTip._id.toString(),
|
|
||||||
"vaId": createdTip.vaId.toString(),
|
|
||||||
"status": "Unbilled",
|
|
||||||
"clientId": createdTip.clientId.toString(),
|
|
||||||
"comment": createdTip.description || "",
|
|
||||||
"tipAmount": Number(parseFloat(createdTip.amount).toFixed(2)), // Convert to cents
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(salesforceUrl, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
// Add any required headers for Salesforce authentication
|
|
||||||
// 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ ...sfData })
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
console.log("✅ Successfully synced Tip data to Salesforce");
|
|
||||||
return res.status(201).json({ success: true, data: createdTip });
|
|
||||||
} else {
|
|
||||||
TipsModel.findByIdAndDelete(createdTip._id);
|
|
||||||
const errorText = await response.text();
|
|
||||||
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
res.status(500).json({ success: false, message: "Internal server error" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function va_tips_get(req, res) {
|
|
||||||
|
|
||||||
const { clientId } = req.body;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const clUser = await ClientModel.findById(clientId);
|
|
||||||
if (!clUser) {
|
|
||||||
return res.status(404).json({ success: false, message: "Client not found" });
|
|
||||||
}
|
|
||||||
const data = await TipsModel.find({ clientId: clientId, status: "Unbilled" });
|
|
||||||
return res.status(200).json({ success: true, data: data });
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
res.status(500).json({ success: false, message: "Internal server error" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function va_tips_update(req, res) {
|
|
||||||
const { tipId } = req.params;
|
|
||||||
const { update } = req.body;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const tipData = await TipsModel.findById(tipId);
|
|
||||||
if (!tipData) {
|
|
||||||
return res.status(404).json({ success: false, message: "Tip not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
await TipsModel.updateOne({ _id: tipId }, update);
|
|
||||||
return res.status(200).json({ success: true, message: "Tip updated successfully" });
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
res.status(500).json({ success: false, message: "Internal server error" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function va_tips_delete(req, res) {
|
|
||||||
const { tipId } = req.params;
|
|
||||||
|
|
||||||
try {
|
|
||||||
|
|
||||||
|
|
||||||
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-tips`;
|
|
||||||
|
|
||||||
// const sfData = {
|
|
||||||
// "id": createdTip._id.toString(),
|
|
||||||
// "vaId": createdTip.vaId.toString(),
|
|
||||||
// "status": "Unbilled",
|
|
||||||
// "clientId": createdTip.clientId.toString(),
|
|
||||||
// "comment": createdTip.description,
|
|
||||||
// "tipAmount": Number(parseFloat(createdTip.amount).toFixed(2)), // Convert to cents
|
|
||||||
// }
|
|
||||||
|
|
||||||
const response = await fetch(salesforceUrl, {
|
|
||||||
method: 'DELETE',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
// Add any required headers for Salesforce authentication
|
|
||||||
// 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ "id": tipId})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
await TipsModel.findOneAndDelete({ _id: tipId });
|
|
||||||
return res.status(200).json({ success: true, message: "Tip updated successfully" });
|
|
||||||
} else {
|
|
||||||
const errorText = await response.text();
|
|
||||||
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
res.status(500).json({ success: false, message: "Internal server error" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export { va_tips_manage_function, va_tips_get, va_tips_update, va_tips_delete };
|
|
||||||
|
|
@ -4,7 +4,7 @@ import logger from "../config/logger.js";
|
||||||
|
|
||||||
async function authValidator(req, res, next) {
|
async function authValidator(req, res, next) {
|
||||||
try {
|
try {
|
||||||
const sf_token = req.headers["x-auth-token"];
|
const sf_token = req.headers.sf_token;
|
||||||
if (sf_token) {
|
if (sf_token) {
|
||||||
const { vaId } = req.params;
|
const { vaId } = req.params;
|
||||||
if(vaId) req.body.vaId = vaId;
|
if(vaId) req.body.vaId = vaId;
|
||||||
|
|
@ -50,15 +50,6 @@ async function authValidator(req, res, next) {
|
||||||
|
|
||||||
async function clientAuthValidator(req, res, next) {
|
async function clientAuthValidator(req, res, next) {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
const sf_token = req.headers["x-auth-token"];
|
|
||||||
if (sf_token) {
|
|
||||||
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
|
|
||||||
if(sf_token) return;
|
|
||||||
|
|
||||||
const accessToken = req.headers.token;
|
const accessToken = req.headers.token;
|
||||||
|
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
|
|
|
||||||
|
|
@ -89,16 +89,9 @@ const projectSchema = new Schema({
|
||||||
},
|
},
|
||||||
vaAssigned: [
|
vaAssigned: [
|
||||||
{
|
{
|
||||||
_id: false,
|
type: Types.ObjectId,
|
||||||
vaId: {
|
ref: "VA",
|
||||||
type: Types.ObjectId,
|
},
|
||||||
ref: "VA",
|
|
||||||
},
|
|
||||||
isFlexibleAllowed: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
dedicatedHours: {
|
dedicatedHours: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|
@ -117,7 +110,7 @@ const projectSchema = new Schema({
|
||||||
type: String,
|
type: String,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
isFlexibleAllowed: {
|
isFlexibleAllowedPt: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
|
@ -370,36 +363,36 @@ const vaWorkTrackSchema = new Schema({
|
||||||
)
|
)
|
||||||
|
|
||||||
const passwordResetSchema = new mongoose.Schema({
|
const passwordResetSchema = new mongoose.Schema({
|
||||||
email: {
|
email: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
lowercase: true,
|
lowercase: true,
|
||||||
index: true
|
index: true
|
||||||
},
|
},
|
||||||
token: {
|
token: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
unique: true,
|
unique: true,
|
||||||
index: true
|
index: true
|
||||||
},
|
},
|
||||||
userType: {
|
userType: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
enum: ['va', 'client'],
|
enum: ['va', 'client'],
|
||||||
index: true
|
index: true
|
||||||
},
|
},
|
||||||
expiresAt: {
|
expiresAt: {
|
||||||
type: Date,
|
type: Date,
|
||||||
required: true,
|
required: true,
|
||||||
index: true,
|
index: true,
|
||||||
expires: 3600 // TTL index for 1 hour
|
expires: 3600 // TTL index for 1 hour
|
||||||
},
|
},
|
||||||
used: {
|
used: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
timestamps: true
|
timestamps: true
|
||||||
});
|
});
|
||||||
|
|
||||||
passwordResetSchema.index({ email: 1, used: 1 });
|
passwordResetSchema.index({ email: 1, used: 1 });
|
||||||
|
|
@ -518,37 +511,6 @@ const leavesSchema = new Schema({
|
||||||
}
|
}
|
||||||
}, { timestamps: true });
|
}, { timestamps: true });
|
||||||
|
|
||||||
|
|
||||||
/* ---------------------- Tips Schema ---------------------- */
|
|
||||||
const tipsSchema = new Schema({
|
|
||||||
clientId: {
|
|
||||||
type: Types.ObjectId,
|
|
||||||
ref: "Client"
|
|
||||||
},
|
|
||||||
vaId: {
|
|
||||||
type: Types.ObjectId,
|
|
||||||
ref: "VA"
|
|
||||||
},
|
|
||||||
vaName: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
type: String,
|
|
||||||
enum: ["Unbilled", "Billed"],
|
|
||||||
default: "Unbilled",
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
amount: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
type: String,
|
|
||||||
}
|
|
||||||
}, { timestamps: true })
|
|
||||||
|
|
||||||
|
|
||||||
/* ---------------------- Model Exports ---------------------- */
|
/* ---------------------- Model Exports ---------------------- */
|
||||||
const VAModel = model("VA", vaSchema);
|
const VAModel = model("VA", vaSchema);
|
||||||
const ClientModel = model("Client", clientSchema);
|
const ClientModel = model("Client", clientSchema);
|
||||||
|
|
@ -558,6 +520,5 @@ const WorkTrackModel = model("TrackWork", vaWorkTrackSchema);
|
||||||
const PasswordResetModel = model("PasswordReset", passwordResetSchema);
|
const PasswordResetModel = model("PasswordReset", passwordResetSchema);
|
||||||
const AdjustedHoursModel = model("AdjustedHours", adjustedHoursSchema);
|
const AdjustedHoursModel = model("AdjustedHours", adjustedHoursSchema);
|
||||||
const LeavesModel = model("Leaves", leavesSchema);
|
const LeavesModel = model("Leaves", leavesSchema);
|
||||||
const TipsModel = model("Tips", tipsSchema);
|
|
||||||
|
|
||||||
export { VAModel, ClientModel, ProjectModel, LoggedModel, WorkTrackModel, PasswordResetModel, AdjustedHoursModel, LeavesModel, TipsModel };
|
export { VAModel, ClientModel, ProjectModel, LoggedModel, WorkTrackModel, PasswordResetModel, AdjustedHoursModel, LeavesModel };
|
||||||
|
|
@ -13,7 +13,6 @@ import { client_dashboard_va_log } from "../../controllers/client/dashboard/clie
|
||||||
import { client_verify_email } from "../../controllers/client/vaClientEmailVerify.controller.js";
|
import { client_verify_email } from "../../controllers/client/vaClientEmailVerify.controller.js";
|
||||||
import { client_dashboard_projects } from "../../controllers/client/dashboard/clientDashboardProjects.controller.js";
|
import { client_dashboard_projects } from "../../controllers/client/dashboard/clientDashboardProjects.controller.js";
|
||||||
import { client_logout_callback } from "../../controllers/client/dashboard/clientLogout.controller.js";
|
import { client_logout_callback } from "../../controllers/client/dashboard/clientLogout.controller.js";
|
||||||
import { va_tips_delete, va_tips_get, va_tips_manage_function, va_tips_update } from "../../controllers/va/vaTips/vaTipsManage.controller.js";
|
|
||||||
|
|
||||||
// clients routes
|
// clients routes
|
||||||
clientRoutes.post("/register", va_register_client); // Register a new VA
|
clientRoutes.post("/register", va_register_client); // Register a new VA
|
||||||
|
|
@ -23,14 +22,11 @@ clientRoutes.post("/forgot-password", client_request_password_reset);
|
||||||
clientRoutes.post("/verify-client-email", client_verify_email);
|
clientRoutes.post("/verify-client-email", client_verify_email);
|
||||||
clientRoutes.post("/logout", clientAuthValidator, client_logout_callback);
|
clientRoutes.post("/logout", clientAuthValidator, client_logout_callback);
|
||||||
|
|
||||||
clientRoutes.post("/tips", clientAuthValidator, va_tips_manage_function);
|
|
||||||
clientRoutes.get("/tips", clientAuthValidator, va_tips_get);
|
|
||||||
clientRoutes.patch("/tips/:tipId", clientAuthValidator, va_tips_update);
|
|
||||||
clientRoutes.delete("/tips/:tipId", clientAuthValidator, va_tips_delete);
|
|
||||||
|
|
||||||
// clientRoutes.get("/:clientID?", va_clients_callback); // VA Clients Callback for getting all VA's or specific client by ID (NC)
|
// clientRoutes.get("/:clientID?", va_clients_callback); // VA Clients Callback for getting all VA's or specific client by ID (NC)
|
||||||
clientRoutes.get("/:clientId/projects/:projectId", va_clients_projects_callback); // VA Clients Projects Callback (NC)
|
clientRoutes.get("/:clientId/projects/:projectId", va_clients_projects_callback); // VA Clients Projects Callback (NC)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// dashboard API's
|
// dashboard API's
|
||||||
clientRoutes.get("/dashboard", clientAuthValidator, client_dashboard); // VA Clients Projects Callback
|
clientRoutes.get("/dashboard", clientAuthValidator, client_dashboard); // VA Clients Projects Callback
|
||||||
clientRoutes.post("/dashboard/va-report", clientAuthValidator, client_dashboard_va_log); // VA details
|
clientRoutes.post("/dashboard/va-report", clientAuthValidator, client_dashboard_va_log); // VA details
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ import { va_remove_function } from "../../controllers/va/vaProfile/vaDelete.cont
|
||||||
import { va_payroll_schedule_function } from "../../controllers/va/vaPayroll/vaPayrollSchedule.controller.js";
|
import { va_payroll_schedule_function } from "../../controllers/va/vaPayroll/vaPayrollSchedule.controller.js";
|
||||||
import { va_payslip_data_function } from "../../controllers/va/vaPayroll/vaPaySlip.controller.js";
|
import { va_payslip_data_function } from "../../controllers/va/vaPayroll/vaPaySlip.controller.js";
|
||||||
|
|
||||||
vaRoutes.get("/", va_callback); // VA Callback for getting active VA list
|
vaRoutes.get("/", va_callback); // VA Callback for getting active VA
|
||||||
|
|
||||||
vaRoutes.post("/login", va_login_callback); // VA Callback for login VA
|
vaRoutes.post("/login", va_login_callback); // VA Callback for login VA
|
||||||
vaRoutes.get("/logout", authValidator, va_logout_callback); // VA Callback for login VA
|
vaRoutes.get("/logout", authValidator, va_logout_callback); // VA Callback for login VA
|
||||||
|
|
@ -47,9 +47,7 @@ vaRoutes.delete("/profile/:id", va_remove_function); // VA delete Callback
|
||||||
// timesheet adjustment routes
|
// timesheet adjustment routes
|
||||||
vaRoutes.get("/timesheet/summary/:projectId/:date/:vaId?", authValidator, va_timesheet_summary_function); // Get Timesheet Adjustment for VA on specific project and date
|
vaRoutes.get("/timesheet/summary/:projectId/:date/:vaId?", authValidator, va_timesheet_summary_function); // Get Timesheet Adjustment for VA on specific project and date
|
||||||
vaRoutes.post("/timesheet/adjustment/request", authValidator, va_timesheet_adjustment_function); // Timesheet Adjustment for VA
|
vaRoutes.post("/timesheet/adjustment/request", authValidator, va_timesheet_adjustment_function); // Timesheet Adjustment for VA
|
||||||
vaRoutes.get("/timesheet/adjustments/list/:projectId?/:vaId?/:status?", get_adjustment_requests_list); // show all adjustment list to approve and reject
|
// vaRoutes.get("/timesheet/adjustments/list/:status?/:projectId?/:vaId?", get_adjustment_requests_list); // show all adjustment list to approve and reject
|
||||||
|
|
||||||
|
|
||||||
vaRoutes.patch("/timesheet/adjustments/:adjustmentId", va_timesheet_manage_adjustment_request);
|
vaRoutes.patch("/timesheet/adjustments/:adjustmentId", va_timesheet_manage_adjustment_request);
|
||||||
|
|
||||||
// Leaves routes
|
// Leaves routes
|
||||||
|
|
@ -58,7 +56,7 @@ vaRoutes.get("/leaves/status/:status", authValidator, va_leave_list_function); /
|
||||||
vaRoutes.get("/leaves/type/:leaveType", authValidator, va_leave_list_function); // Filter by leave type
|
vaRoutes.get("/leaves/type/:leaveType", authValidator, va_leave_list_function); // Filter by leave type
|
||||||
vaRoutes.get("/leaves/status/:status/type/:leaveType", authValidator, va_leave_list_function); // leaves Filter
|
vaRoutes.get("/leaves/status/:status/type/:leaveType", authValidator, va_leave_list_function); // leaves Filter
|
||||||
vaRoutes.post("/leave/request", authValidator, va_leave_apply_function); // Leave for VA
|
vaRoutes.post("/leave/request", authValidator, va_leave_apply_function); // Leave for VA
|
||||||
vaRoutes.patch("/leave/:leaveId", authValidator, va_leave_manage_function);
|
vaRoutes.patch("/leave/:leaveId", va_leave_manage_function);
|
||||||
|
|
||||||
// timesheet adjustment routes
|
// timesheet adjustment routes
|
||||||
|
|
||||||
|
|
@ -86,6 +84,9 @@ vaRoutes.get("/log/project/:projectId/:from?/:to?", authValidator, va_log_callba
|
||||||
vaRoutes.get("/payrollSchedule", authValidator, va_payroll_schedule_function); // VA Payroll Schedule
|
vaRoutes.get("/payrollSchedule", authValidator, va_payroll_schedule_function); // VA Payroll Schedule
|
||||||
vaRoutes.get("/paySlip/:vaId", authValidator, va_payslip_data_function); // VA Payslip
|
vaRoutes.get("/paySlip/:vaId", authValidator, va_payslip_data_function); // VA Payslip
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
vaRoutes.get("/:userName", va_username_callback); // VA Username Callback
|
vaRoutes.get("/:userName", va_username_callback); // VA Username Callback
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
113
src/server.js
113
src/server.js
|
|
@ -121,7 +121,6 @@ app.get('/health', (req, res) => {
|
||||||
app.get("/client/login", (req, res) => {
|
app.get("/client/login", (req, res) => {
|
||||||
res.render("client_login")
|
res.render("client_login")
|
||||||
})
|
})
|
||||||
|
|
||||||
app.get("/client/forgot-password", (req, res) => {
|
app.get("/client/forgot-password", (req, res) => {
|
||||||
res.render("forgot_password")
|
res.render("forgot_password")
|
||||||
})
|
})
|
||||||
|
|
@ -300,8 +299,6 @@ io.use(async (socket, next) => {
|
||||||
if (clientUser) {
|
if (clientUser) {
|
||||||
socket.data.role = "client";
|
socket.data.role = "client";
|
||||||
socket.data.clientId = clientUser._id.toString();
|
socket.data.clientId = clientUser._id.toString();
|
||||||
socket.data.vaId = vaId.toString();
|
|
||||||
socket.data.projectId = projectId;
|
|
||||||
|
|
||||||
// Get all VA IDs assigned to this client
|
// Get all VA IDs assigned to this client
|
||||||
socket.data.allowedVAs = clientData.vaAssigned.map((va) => va.toString());
|
socket.data.allowedVAs = clientData.vaAssigned.map((va) => va.toString());
|
||||||
|
|
@ -322,7 +319,7 @@ io.on("connection", (socket) => {
|
||||||
console.log("User connected:", socket.id);
|
console.log("User connected:", socket.id);
|
||||||
|
|
||||||
// Register role and ID (client or VA)
|
// Register role and ID (client or VA)
|
||||||
socket.on("register", async ({ role, id, projectId = null, clientId = null }) => {
|
socket.on("register", async ({ role, id }) => {
|
||||||
if (role === "va") {
|
if (role === "va") {
|
||||||
socket.data.role = "va";
|
socket.data.role = "va";
|
||||||
socket.data.vaId = id;
|
socket.data.vaId = id;
|
||||||
|
|
@ -331,35 +328,23 @@ io.on("connection", (socket) => {
|
||||||
|
|
||||||
if (role === "client") {
|
if (role === "client") {
|
||||||
socket.data.role = "client";
|
socket.data.role = "client";
|
||||||
socket.data.clientId = clientId;
|
socket.data.clientId = id;
|
||||||
|
|
||||||
// 🧩 Get allowed VAs for this client
|
// 🧩 Get allowed VAs for this client
|
||||||
const client = await ClientModel.findById(clientId);
|
const client = await ClientModel.findById(id)
|
||||||
|
.populate("projectIds") // to access their projects
|
||||||
const a = JSON.stringify(client);
|
.populate("vaIds"); // optional if you track directly
|
||||||
console.log(a, "client data for allowed VAs");
|
|
||||||
if (client.projectIds.length !== 0 && client.projectIds.includes(projectId)) {
|
|
||||||
const projectVAIds = new Set();
|
|
||||||
|
|
||||||
const populated = await ProjectModel.findById(projectId).populate("vaAssigned");
|
|
||||||
populated.vaAssigned.forEach((va) => projectVAIds.add(va.vaId.toString()));
|
|
||||||
|
|
||||||
socket.data.allowedVAs = Array.from(projectVAIds);
|
|
||||||
console.log(`Client ${clientId} can view VAs:`, socket.data.allowedVAs);
|
|
||||||
|
|
||||||
io.to(vaSocketRespectToClient.get(`${clientId}-${id}-${projectId}-va`)).emit("startSendAppsDetails", { projId: projectId });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect all VA IDs from their projects
|
// Collect all VA IDs from their projects
|
||||||
// const projectVAIds = new Set();
|
const projectVAIds = new Set();
|
||||||
// for (const project of client.projectIds) {
|
for (const project of client.projectIds) {
|
||||||
// const populated = await ProjectModel.findById(project._id).populate("vaAssigned");
|
const populated = await ProjectModel.findById(project._id).populate("vaAssigned");
|
||||||
// populated.vaAssigned.forEach((va) => projectVAIds.add(va._id.toString()));
|
populated.vaAssigned.forEach((va) => projectVAIds.add(va._id.toString()));
|
||||||
// }
|
}
|
||||||
|
|
||||||
// // Store allowed VA IDs in socket memory
|
// Store allowed VA IDs in socket memory
|
||||||
// socket.data.allowedVAs = Array.from(projectVAIds);
|
socket.data.allowedVAs = Array.from(projectVAIds);
|
||||||
// console.log(`Client ${clientId} can view VAs:`, socket.data.allowedVAs);
|
console.log(`Client ${id} can view VAs:`, socket.data.allowedVAs);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -415,7 +400,7 @@ io.on("connection", (socket) => {
|
||||||
socket.to(vaSocketRespectToClient.get(`${clientId}-${vaId}-${projectId}-va`)).emit("appToCapture", { appTitle });
|
socket.to(vaSocketRespectToClient.get(`${clientId}-${vaId}-${projectId}-va`)).emit("appToCapture", { appTitle });
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("requestVisibleApps", async ({ token }) => {
|
socket.on("getVisibleApps", async ({ token, apps }) => {
|
||||||
try {
|
try {
|
||||||
// Verify token
|
// Verify token
|
||||||
const decoded = jwt.verify(token, process.env.JWT_SECRET_ACCESS_TOKEN);
|
const decoded = jwt.verify(token, process.env.JWT_SECRET_ACCESS_TOKEN);
|
||||||
|
|
@ -431,35 +416,6 @@ io.on("connection", (socket) => {
|
||||||
const vaId = vaUser._id.toString();
|
const vaId = vaUser._id.toString();
|
||||||
console.log("🟢 Received Apps from VA:", vaId);
|
console.log("🟢 Received Apps from VA:", vaId);
|
||||||
|
|
||||||
// Broadcast image only to allowed clients
|
|
||||||
for (let [_, clientSocket] of io.sockets.sockets) {
|
|
||||||
if (
|
|
||||||
clientSocket.data.role === "client" &&
|
|
||||||
clientSocket.data.allowedVAs?.includes(vaId)
|
|
||||||
) {
|
|
||||||
clientSocket.emit("captureAppsTracker");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("❌ Token verification failed:", err.message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("getVisibleApps", async ({ token, apps }) => {
|
|
||||||
try {
|
|
||||||
// Verify token
|
|
||||||
const decoded = jwt.verify(token, process.env.JWT_SECRET_ACCESS_TOKEN);
|
|
||||||
const vaEmail = decoded.email;
|
|
||||||
|
|
||||||
// Find VA in DB
|
|
||||||
const vaUser = await VAModel.findOne({ "userDetails.email": vaEmail });
|
|
||||||
if (!vaUser) {
|
|
||||||
console.log("❌ Unauthorized VA");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const vaId = vaUser._id.toString();
|
|
||||||
|
|
||||||
// Broadcast image only to allowed clients
|
// Broadcast image only to allowed clients
|
||||||
for (let [_, clientSocket] of io.sockets.sockets) {
|
for (let [_, clientSocket] of io.sockets.sockets) {
|
||||||
if (
|
if (
|
||||||
|
|
@ -475,53 +431,18 @@ io.on("connection", (socket) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// socket.on('disconnect', () => {
|
socket.on('disconnect', () => {
|
||||||
// console.log('a user Disconnected');
|
console.log('a user Disconnected');
|
||||||
// });
|
|
||||||
|
|
||||||
// When client leaves or closes tab
|
|
||||||
socket.on("disconnect", () => {
|
|
||||||
if (socket.data.role === "client") {
|
|
||||||
const { clientId, vaId, projectId } = socket.data;
|
|
||||||
|
|
||||||
const vaSocketId = vaSocketRespectToClient.get(
|
|
||||||
`${clientId}-${vaId}-${projectId}-va`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (vaSocketId) {
|
|
||||||
// io.to(vaSocketId).emit("stopSendAppsDetails");
|
|
||||||
io.to(vaSocketRespectToClient.get(`${clientId}-${vaId}-${projectId}-va`)).emit("stopSendAppsDetails", { projId: projectId });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get("/", (req, res) => {
|
|
||||||
res.render("client_login")
|
|
||||||
})
|
|
||||||
|
|
||||||
app.get("/forgot-password", (req, res) => {
|
|
||||||
res.render("forgot_password")
|
|
||||||
})
|
|
||||||
|
|
||||||
app.get("/heath", () => {
|
|
||||||
if (Math.round(Math.random()) === 1) {
|
|
||||||
return res.status(200).json({ "success": true, "message": "Server is Healthy" })
|
|
||||||
} else {
|
|
||||||
return res.status(403).json({ "success": false, "message": "Server is not Healthy" })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
app.use((req, res) => {
|
app.use((req, res) => {
|
||||||
res.status(404).render("not_found", {
|
res.status(404).render("not_found", {
|
||||||
title: "404 - Page Not Found"
|
title: "404 - Page Not Found"
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
server.listen(PORT, "0.0.0.0", () => {
|
server.listen(PORT, () => {
|
||||||
mongoose
|
mongoose
|
||||||
.connect(
|
.connect(
|
||||||
process.env.MONGO_URI,
|
process.env.MONGO_URI,
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ class PasswordResetService {
|
||||||
logger.info(`Password reset token created for ${userType}: ${email}`);
|
logger.info(`Password reset token created for ${userType}: ${email}`);
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Email sent successfully. Please check your inbox."
|
message: "If the email exists, a reset link has been sent"
|
||||||
};
|
};
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
const convertToObjectId = (id) => {
|
|
||||||
return mongoose.Types.ObjectId.isValid(id) ? new mongoose.Types.ObjectId(id) : id;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default convertToObjectId;
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
|
|
||||||
if (userType === 'client') {
|
if (userType === 'client') {
|
||||||
forgotPasswordUrl = '/client/forgot-password';
|
forgotPasswordUrl = '/client/forgot-password';
|
||||||
loginUrl = '/';
|
loginUrl = '/client/login';
|
||||||
}
|
}
|
||||||
%>
|
%>
|
||||||
<a href="<%= forgotPasswordUrl %>" class="btn btn-primary" style="margin-bottom:8px;">Request New Reset Link</a>
|
<a href="<%= forgotPasswordUrl %>" class="btn btn-primary" style="margin-bottom:8px;">Request New Reset Link</a>
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@
|
||||||
<%
|
<%
|
||||||
let backToLoginUrl = `${process.env.FRONTEND_URL}/login`;
|
let backToLoginUrl = `${process.env.FRONTEND_URL}/login`;
|
||||||
if (userType === 'client') {
|
if (userType === 'client') {
|
||||||
backToLoginUrl = '/';
|
backToLoginUrl = '/client/login';
|
||||||
}
|
}
|
||||||
%>
|
%>
|
||||||
<a href="<%= backToLoginUrl %>" class="small-link">Back to Login</a>
|
<a href="<%= backToLoginUrl %>" class="small-link">Back to Login</a>
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
<%
|
<%
|
||||||
let loginUrl = `${process.env.FRONTEND_URL}/login`;
|
let loginUrl = `${process.env.FRONTEND_URL}/login`;
|
||||||
if (userType === 'client') {
|
if (userType === 'client') {
|
||||||
loginUrl = '/';
|
loginUrl = '/client/login';
|
||||||
}
|
}
|
||||||
%>
|
%>
|
||||||
<a href="<%= loginUrl %>" class="btn btn-primary">Go to Login</a>
|
<a href="<%= loginUrl %>" class="btn btn-primary">Go to Login</a>
|
||||||
|
|
|
||||||
|
|
@ -252,7 +252,7 @@
|
||||||
async function fetchWithAuth(url, options = {}) {
|
async function fetchWithAuth(url, options = {}) {
|
||||||
const token = getAuthToken();
|
const token = getAuthToken();
|
||||||
if (!token) {
|
if (!token) {
|
||||||
window.location.href = '/';
|
window.location.href = '/client/login';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -268,7 +268,7 @@
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
localStorage.removeItem('knoptpq');
|
localStorage.removeItem('knoptpq');
|
||||||
localStorage.removeItem('refreshToken');
|
localStorage.removeItem('refreshToken');
|
||||||
window.location.href = '/';
|
window.location.href = '/client/login';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -287,7 +287,7 @@
|
||||||
// Clear local storage and redirect
|
// Clear local storage and redirect
|
||||||
localStorage.removeItem('knoptpq');
|
localStorage.removeItem('knoptpq');
|
||||||
localStorage.removeItem('refreshToken');
|
localStorage.removeItem('refreshToken');
|
||||||
window.location.href = '/';
|
window.location.href = '/client/login';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en" data-theme="light">
|
<html lang="en" data-theme="light">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
@ -8,21 +7,6 @@
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||||
<link rel="stylesheet" href="/public/css/dashboard.css">
|
<link rel="stylesheet" href="/public/css/dashboard.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<style>
|
|
||||||
.void-btn-a {
|
|
||||||
background: tomato;
|
|
||||||
text-decoration: none;
|
|
||||||
color: #ffffff !important;
|
|
||||||
border: none;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
border-radius: 5px;
|
|
||||||
font-weight: bold;
|
|
||||||
text-decoration: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<body class="dashboard-body">
|
<body class="dashboard-body">
|
||||||
<div id="root">
|
<div id="root">
|
||||||
<nav class="dashboard-header">
|
<nav class="dashboard-header">
|
||||||
|
|
@ -171,8 +155,9 @@
|
||||||
<form class="modal-form" id="tipForm">
|
<form class="modal-form" id="tipForm">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Select Virtual Assistant</label>
|
<label class="form-label">Select Virtual Assistant</label>
|
||||||
<select name="vaForTip" class="form-input" id="vaForTip" required>
|
<select name="vaForTip" class="form-input select-field" id="vaForTip" required>
|
||||||
<option value="">-- Select Virtual Assistant --</option>
|
<option value="">-- Select Virtual Assistant --</option>
|
||||||
|
<option value="va 1">VA 1</option>
|
||||||
<!-- Options will be populated dynamically -->
|
<!-- Options will be populated dynamically -->
|
||||||
</select>
|
</select>
|
||||||
<div class="error-message" id="vaForTipError"></div>
|
<div class="error-message" id="vaForTipError"></div>
|
||||||
|
|
@ -184,12 +169,12 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Tip Description</label>
|
<label class="form-label">Tip Description</label>
|
||||||
<textarea required="true" name="vaTipDescription" class="form-input" id="vaTipDescription"></textarea>
|
<textarea name="vaTipDescription" class="form-input" id="vaTipDescription"></textarea>
|
||||||
<div class="error-message" id="vaTipDescriptionError"></div>
|
<div class="error-message" id="vaTipDescriptionError"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
<button type="button" class="cancel-btn" onclick="closeTipModal()">Cancel</button>
|
<button type="button" class="cancel-btn" onclick="closeTipModal()">Cancel</button>
|
||||||
<button type="submit" class="save-btn" id="submitTipBtn">Submit Tip</button>
|
<button type="submit" class="save-btn" id="savePasswordBtn">Submit Tip</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -209,8 +194,7 @@
|
||||||
<i class="fas fa-sign-out-alt"></i>
|
<i class="fas fa-sign-out-alt"></i>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="logout-title">Are you sure you want to logout?</h3>
|
<h3 class="logout-title">Are you sure you want to logout?</h3>
|
||||||
<p class="logout-message">You will be logged out of your account and redirected to the login page.
|
<p class="logout-message">You will be logged out of your account and redirected to the login page.</p>
|
||||||
</p>
|
|
||||||
<div class="logout-actions">
|
<div class="logout-actions">
|
||||||
<button class="logout-cancel-btn" onclick="closeLogoutModal()">Cancel</button>
|
<button class="logout-cancel-btn" onclick="closeLogoutModal()">Cancel</button>
|
||||||
<button class="logout-confirm-btn" id="confirmLogoutBtn">Yes, Logout</button>
|
<button class="logout-confirm-btn" id="confirmLogoutBtn">Yes, Logout</button>
|
||||||
|
|
@ -219,330 +203,148 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
// Theme management
|
// Theme management
|
||||||
function setTheme(theme) {
|
function setTheme(theme) {
|
||||||
document.documentElement.setAttribute('data-theme', theme);
|
document.documentElement.setAttribute('data-theme', theme);
|
||||||
localStorage.setItem('theme', theme);
|
localStorage.setItem('theme', theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSystemTheme() {
|
||||||
|
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize theme
|
||||||
|
const savedTheme = getSystemTheme();
|
||||||
|
setTheme(savedTheme);
|
||||||
|
|
||||||
|
// API base URL
|
||||||
|
const API_BASE = '/api/v1/client';
|
||||||
|
|
||||||
|
// Auth token management
|
||||||
|
function getAuthToken() {
|
||||||
|
return localStorage.getItem('knoptpq');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchWithAuth(url, options = {}) {
|
||||||
|
const token = getAuthToken();
|
||||||
|
if (!token) {
|
||||||
|
window.location.href = '/client/login';
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSystemTheme() {
|
const response = await fetch(url, {
|
||||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
...options,
|
||||||
}
|
headers: {
|
||||||
|
'token': `${token}`,
|
||||||
// Initialize theme
|
'Content-Type': 'application/json',
|
||||||
const savedTheme = getSystemTheme();
|
...options.headers,
|
||||||
setTheme(savedTheme);
|
|
||||||
|
|
||||||
// API base URL
|
|
||||||
const API_BASE = '/api/v1/client';
|
|
||||||
|
|
||||||
// Auth token management
|
|
||||||
function getAuthToken() {
|
|
||||||
return localStorage.getItem('knoptpq');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchWithAuth(url, options = {}) {
|
|
||||||
const token = getAuthToken();
|
|
||||||
if (!token) {
|
|
||||||
window.location.href = '/';
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(url, {
|
|
||||||
...options,
|
|
||||||
headers: {
|
|
||||||
'token': `${token}`,
|
|
||||||
'type': 'client',
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'ngrok-skip-browser-warning': 69420,
|
|
||||||
...options.headers,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.status === 401) {
|
|
||||||
localStorage.removeItem('knoptpq');
|
|
||||||
localStorage.removeItem('refreshToken');
|
|
||||||
window.location.href = '/';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleLogout() {
|
|
||||||
try {
|
|
||||||
// Call logout API
|
|
||||||
await fetchWithAuth(`${API_BASE}/logout`, {
|
|
||||||
method: 'POST'
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Logout API error:', error);
|
|
||||||
} finally {
|
|
||||||
// Clear local storage and redirect
|
|
||||||
localStorage.removeItem('knoptpq');
|
|
||||||
localStorage.removeItem('refreshToken');
|
|
||||||
window.location.href = '/';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logout Modal Functions
|
|
||||||
function openLogoutModal() {
|
|
||||||
const modal = document.getElementById('logoutModal');
|
|
||||||
modal.style.display = 'flex';
|
|
||||||
document.body.style.overflow = 'hidden';
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeLogoutModal() {
|
|
||||||
const modal = document.getElementById('logoutModal');
|
|
||||||
modal.style.display = 'none';
|
|
||||||
document.body.style.overflow = 'auto';
|
|
||||||
}
|
|
||||||
|
|
||||||
function openTipModal() {
|
|
||||||
const modal = document.getElementById('tipModal');
|
|
||||||
modal.style.display = 'flex';
|
|
||||||
document.body.style.overflow = 'hidden';
|
|
||||||
|
|
||||||
// Clear form
|
|
||||||
document.getElementById('tipForm').reset();
|
|
||||||
clearErrorMessages();
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeTipModal() {
|
|
||||||
const modal = document.getElementById('tipModal');
|
|
||||||
modal.style.display = 'none';
|
|
||||||
document.body.style.overflow = 'auto';
|
|
||||||
|
|
||||||
// Clear form and errors
|
|
||||||
document.getElementById('tipForm').reset();
|
|
||||||
clearErrorMessages();
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearErrorMessages() {
|
|
||||||
document.getElementById('vaForTipError').textContent = '';
|
|
||||||
document.getElementById('vaTipAmountError').textContent = '';
|
|
||||||
document.getElementById('vaTipDescriptionError').textContent = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
|
||||||
|
|
||||||
// Mobile menu toggle
|
|
||||||
const menuToggle = document.querySelector('.mobile-menu-button');
|
|
||||||
const sidebar = document.querySelector('.dashboard-sidebar');
|
|
||||||
menuToggle.addEventListener('click', () => {
|
|
||||||
sidebar.classList.toggle('active');
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('tipVaAddBtn').addEventListener('click', openTipModal);
|
|
||||||
|
|
||||||
document.getElementById('tipModal').addEventListener('click', function (e) {
|
|
||||||
if (e.target === this) {
|
|
||||||
closeTipModal();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close modal with escape key
|
|
||||||
document.addEventListener('keydown', function (e) {
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
closeLogoutModal();
|
|
||||||
closeTipModal();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('logoutModal').addEventListener('click', function (e) {
|
|
||||||
if (e.target === this) {
|
|
||||||
closeLogoutModal();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Logout button - opens confirmation modal instead of directly logging out
|
|
||||||
document.getElementById('logoutButton').addEventListener('click', openLogoutModal);
|
|
||||||
|
|
||||||
// Confirm logout button
|
|
||||||
document.getElementById('confirmLogoutBtn').addEventListener('click', handleLogout);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ------------------------------
|
if (response.status === 401) {
|
||||||
// TIP FORM SUBMISSION HANDLER
|
localStorage.removeItem('knoptpq');
|
||||||
// ------------------------------
|
localStorage.removeItem('refreshToken');
|
||||||
|
window.location.href = '/client/login';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
document.getElementById('tipForm').addEventListener('submit', async function (e) {
|
return response;
|
||||||
e.preventDefault();
|
}
|
||||||
|
|
||||||
clearErrorMessages();
|
async function handleLogout() {
|
||||||
|
try {
|
||||||
|
// Call logout API
|
||||||
|
await fetchWithAuth(`${API_BASE}/logout`, {
|
||||||
|
method: 'POST'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Logout API error:', error);
|
||||||
|
} finally {
|
||||||
|
// Clear local storage and redirect
|
||||||
|
localStorage.removeItem('knoptpq');
|
||||||
|
localStorage.removeItem('refreshToken');
|
||||||
|
window.location.href = '/client/login';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const vaId = document.getElementById('vaForTip').value;
|
// Logout Modal Functions
|
||||||
const amount = document.getElementById('vaTipAmount').value.trim();
|
function openLogoutModal() {
|
||||||
const description = document.getElementById('vaTipDescription').value.trim();
|
const modal = document.getElementById('logoutModal');
|
||||||
|
modal.style.display = 'flex';
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
}
|
||||||
|
|
||||||
let isValid = true;
|
function closeLogoutModal() {
|
||||||
|
const modal = document.getElementById('logoutModal');
|
||||||
|
modal.style.display = 'none';
|
||||||
|
document.body.style.overflow = 'auto';
|
||||||
|
}
|
||||||
|
|
||||||
// Validation
|
function openTipModal() {
|
||||||
if (!vaId) {
|
const modal = document.getElementById('tipModal');
|
||||||
document.getElementById('vaForTipError').textContent = "Please select a virtual assistant.";
|
modal.style.display = 'flex';
|
||||||
isValid = false;
|
document.body.style.overflow = 'hidden';
|
||||||
}
|
|
||||||
|
|
||||||
if (!amount || isNaN(amount) || Number(amount) <= 0) {
|
// Clear form
|
||||||
document.getElementById('vaTipAmountError').textContent = "Enter a valid tip amount.";
|
document.getElementById('tipForm').reset();
|
||||||
isValid = false;
|
clearErrorMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isValid) return;
|
function closeTipModal() {
|
||||||
|
const modal = document.getElementById('tipModal');
|
||||||
|
modal.style.display = 'none';
|
||||||
|
document.body.style.overflow = 'auto';
|
||||||
|
|
||||||
// Prepare API body
|
// Clear form and errors
|
||||||
const payload = {
|
document.getElementById('tipForm').reset();
|
||||||
vaId,
|
clearErrorMessages();
|
||||||
amount: Number(amount),
|
}
|
||||||
description
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
function clearErrorMessages() {
|
||||||
document.getElementById('submitTipBtn').disabled = true;
|
document.getElementById('vaForTipError').textContent = '';
|
||||||
document.getElementById('submitTipBtn').textContent = "Submitting...";
|
document.getElementById('vaTipAmountError').textContent = '';
|
||||||
|
document.getElementById('vaTipDescriptionError').textContent = '';
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetchWithAuth(`/api/v1/client/tips`, {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(payload)
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
// Mobile menu toggle
|
||||||
|
const menuToggle = document.querySelector('.mobile-menu-button');
|
||||||
|
const sidebar = document.querySelector('.dashboard-sidebar');
|
||||||
|
menuToggle.addEventListener('click', () => {
|
||||||
|
sidebar.classList.toggle('active');
|
||||||
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
document.getElementById('tipVaAddBtn').addEventListener('click', openTipModal);
|
||||||
alert(data.message || "Failed to submit tip.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
alert("Tip submitted successfully!");
|
document.getElementById('tipModal').addEventListener('click', function(e) {
|
||||||
|
if (e.target === this) {
|
||||||
closeTipModal();
|
closeTipModal();
|
||||||
|
|
||||||
// Optionally refresh page or reload tips list
|
|
||||||
window.location.reload();
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Tip submit error:", err);
|
|
||||||
alert("Something went wrong while submitting tip.");
|
|
||||||
} finally {
|
|
||||||
document.getElementById('submitTipBtn').disabled = false;
|
|
||||||
document.getElementById('submitTipBtn').textContent = "Submit Tip";
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Close modal with escape key
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('keydown', function(e) {
|
||||||
// Load VAs for tip selection
|
if (e.key === 'Escape') {
|
||||||
async function loadVAs() {
|
closeLogoutModal();
|
||||||
try {
|
closeTipModal();
|
||||||
const response = await fetchWithAuth(`${API_BASE}/dashboard`);
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
console.error("Failed to load VAs:", data.message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const vaSelect = document.getElementById('vaForTip');
|
|
||||||
data.data.allVAs.forEach(va => {
|
|
||||||
const option = document.createElement('option');
|
|
||||||
option.value = va.id;
|
|
||||||
option.textContent = va.name;
|
|
||||||
vaSelect.appendChild(option);
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error loading VAs:", error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
loadVAs();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.getElementById('logoutModal').addEventListener('click', function(e) {
|
||||||
// Load VAs for tip selection
|
if (e.target === this) {
|
||||||
async function loadTips() {
|
closeLogoutModal();
|
||||||
try {
|
|
||||||
const response = await fetchWithAuth(`${API_BASE}/tips`);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
console.error("Failed to load VAs:", data.message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
const vaSelect = document.getElementById('vaTipTableBody');
|
|
||||||
data.data.forEach(tip => {
|
|
||||||
const row = document.createElement('tr');
|
|
||||||
const cellVoid = document.createElement('td');
|
|
||||||
const cellStatus = document.createElement('td');
|
|
||||||
const cellName = document.createElement('td');
|
|
||||||
const cellDescription = document.createElement('td');
|
|
||||||
const cellDate = document.createElement('td');
|
|
||||||
const cellAmount = document.createElement('td');
|
|
||||||
|
|
||||||
cellVoid.innerHTML = `<button id='voidBtn' class='void-btn-a' data-id='${tip._id}'>Void</button>`;
|
|
||||||
cellVoid.classList.add('void-btn');
|
|
||||||
|
|
||||||
cellStatus.textContent = tip.status;
|
|
||||||
cellName.textContent = tip.vaName;
|
|
||||||
cellDescription.textContent = tip.description;
|
|
||||||
cellDate.textContent = tip.createdAt ? new Date(tip.createdAt).toLocaleDateString() : '';
|
|
||||||
cellAmount.textContent = `$${parseFloat(tip.amount).toFixed(2)}`;
|
|
||||||
|
|
||||||
row.appendChild(cellVoid);
|
|
||||||
row.appendChild(cellStatus);
|
|
||||||
row.appendChild(cellName);
|
|
||||||
row.appendChild(cellDescription);
|
|
||||||
row.appendChild(cellDate);
|
|
||||||
row.appendChild(cellAmount);
|
|
||||||
|
|
||||||
vaSelect.appendChild(row);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelectorAll('.void-btn-a').forEach(button => {
|
|
||||||
button.addEventListener('click', async function () {
|
|
||||||
const tipId = this.getAttribute('data-id');
|
|
||||||
if (confirm('Are you sure you want to void this tip?')) {
|
|
||||||
try {
|
|
||||||
const response = await fetchWithAuth(`${API_BASE}/tips/${tipId}`, {
|
|
||||||
method: 'DELETE'
|
|
||||||
});
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
alert(result.message || 'Failed to void tip.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
alert('Tip voided successfully!');
|
|
||||||
window.location.reload();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error voiding tip:', error);
|
|
||||||
alert('Something went wrong while voiding the tip.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error loading VAs:", error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
loadTips();
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Logout button - opens confirmation modal instead of directly logging out
|
||||||
|
document.getElementById('logoutButton').addEventListener('click', openLogoutModal);
|
||||||
|
|
||||||
|
// Confirm logout button
|
||||||
|
document.getElementById('confirmLogoutBtn').addEventListener('click', handleLogout);
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
@ -234,7 +234,7 @@
|
||||||
async function fetchWithAuth(url, options = {}) {
|
async function fetchWithAuth(url, options = {}) {
|
||||||
const token = getAuthToken();
|
const token = getAuthToken();
|
||||||
if (!token) {
|
if (!token) {
|
||||||
window.location.href = '/';
|
window.location.href = '/client/login';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -250,7 +250,7 @@
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
localStorage.removeItem('knoptpq');
|
localStorage.removeItem('knoptpq');
|
||||||
localStorage.removeItem('refreshToken');
|
localStorage.removeItem('refreshToken');
|
||||||
window.location.href = '/';
|
window.location.href = '/client/login';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -265,7 +265,7 @@
|
||||||
} finally {
|
} finally {
|
||||||
localStorage.removeItem('knoptpq');
|
localStorage.removeItem('knoptpq');
|
||||||
localStorage.removeItem('refreshToken');
|
localStorage.removeItem('refreshToken');
|
||||||
window.location.href = '/';
|
window.location.href = '/client/login';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -216,7 +216,7 @@
|
||||||
async function fetchWithAuth(url, options = {}) {
|
async function fetchWithAuth(url, options = {}) {
|
||||||
const token = getAuthToken();
|
const token = getAuthToken();
|
||||||
if (!token) {
|
if (!token) {
|
||||||
window.location.href = '/';
|
window.location.href = '/client/login';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -232,7 +232,7 @@
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
localStorage.removeItem('knoptpq');
|
localStorage.removeItem('knoptpq');
|
||||||
localStorage.removeItem('refreshToken');
|
localStorage.removeItem('refreshToken');
|
||||||
window.location.href = '/';
|
window.location.href = '/client/login';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -251,7 +251,7 @@
|
||||||
// Clear local storage and redirect
|
// Clear local storage and redirect
|
||||||
localStorage.removeItem('knoptpq');
|
localStorage.removeItem('knoptpq');
|
||||||
localStorage.removeItem('refreshToken');
|
localStorage.removeItem('refreshToken');
|
||||||
window.location.href = '/';
|
window.location.href = '/client/login';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en" data-theme="light">
|
<html lang="en" data-theme="light">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
@ -8,7 +7,6 @@
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||||
<link rel="stylesheet" href="/public/css/dashboard.css">
|
<link rel="stylesheet" href="/public/css/dashboard.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="dashboard-body">
|
<body class="dashboard-body">
|
||||||
<div id="root">
|
<div id="root">
|
||||||
<nav class="dashboard-header">
|
<nav class="dashboard-header">
|
||||||
|
|
@ -124,8 +122,7 @@
|
||||||
<!-- Virtual Assistants List -->
|
<!-- Virtual Assistants List -->
|
||||||
<div class="content-section">
|
<div class="content-section">
|
||||||
<div class="section-header" style="justify-content: space-between;align-items: center;">
|
<div class="section-header" style="justify-content: space-between;align-items: center;">
|
||||||
<h2 class="section-title">Virtual Assistants <span class="total-count" id="vaListCount">0
|
<h2 class="section-title">Virtual Assistants <span class="total-count" id="vaListCount">0 VAs</span></h2>
|
||||||
VAs</span></h2>
|
|
||||||
<a href="/client/virtual-assistants/manage-tips" class="btn btn-primary">Manage Tips</a>
|
<a href="/client/virtual-assistants/manage-tips" class="btn btn-primary">Manage Tips</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
|
|
@ -158,8 +155,6 @@
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- VA Modal Start -->
|
|
||||||
<div id="vaModal" class="modal-overlay">
|
<div id="vaModal" class="modal-overlay">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header" style="display: flex;align-items: center;justify-content: space-between;">
|
<div class="modal-header" style="display: flex;align-items: center;justify-content: space-between;">
|
||||||
|
|
@ -209,62 +204,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- VA Modal Start -->
|
|
||||||
|
|
||||||
<!-- Log Modal Start -->
|
|
||||||
<div id="vaModal" class="modal-overlay">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header" style="display: flex;align-items: center;justify-content: space-between;">
|
|
||||||
<h2 class="modal-title" id="modalVAName">VA Details</h2>
|
|
||||||
<button class="modal-close" onclick="closeVAModal()">
|
|
||||||
<i class="fas fa-times"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<!-- Projects Section -->
|
|
||||||
<div class="va-details-section">
|
|
||||||
<!-- <h3 class="va-section-title">
|
|
||||||
<i class="fas fa-briefcase"></i>
|
|
||||||
Projects
|
|
||||||
<span class="va-count-badge" id="projectsCountBadge">0</span>
|
|
||||||
</h3> -->
|
|
||||||
|
|
||||||
<div class="va-table-container">
|
|
||||||
<table class="va-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>#</th>
|
|
||||||
<th>Title</th>
|
|
||||||
<th>Time</th>
|
|
||||||
<th>Duration</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="modalProjectsTableBody">
|
|
||||||
<!-- Project details will be loaded here -->
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<div id="modalNoProjects" class="no-vas-message" style="display: none;">
|
|
||||||
<i class="fas fa-briefcase"></i>
|
|
||||||
<h4>No Logs Found</h4>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Action Button -->
|
|
||||||
<div class="modal-actions" id="modalActions" style="display: none;">
|
|
||||||
<a href="#" class="view-session-btn" id="viewSessionBtn" target="_blank">
|
|
||||||
<i class="fas fa-eye"></i>
|
|
||||||
View Live Session
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Log Modal End -->
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div id="logoutModal" class="modal-overlay">
|
<div id="logoutModal" class="modal-overlay">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
|
|
@ -279,8 +218,7 @@
|
||||||
<i class="fas fa-sign-out-alt"></i>
|
<i class="fas fa-sign-out-alt"></i>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="logout-title">Are you sure you want to logout?</h3>
|
<h3 class="logout-title">Are you sure you want to logout?</h3>
|
||||||
<p class="logout-message">You will be logged out of your account and redirected to the login page.
|
<p class="logout-message">You will be logged out of your account and redirected to the login page.</p>
|
||||||
</p>
|
|
||||||
<div class="logout-actions">
|
<div class="logout-actions">
|
||||||
<button class="logout-cancel-btn" onclick="closeLogoutModal()">Cancel</button>
|
<button class="logout-cancel-btn" onclick="closeLogoutModal()">Cancel</button>
|
||||||
<button class="logout-confirm-btn" id="confirmLogoutBtn">Yes, Logout</button>
|
<button class="logout-confirm-btn" id="confirmLogoutBtn">Yes, Logout</button>
|
||||||
|
|
@ -289,137 +227,137 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
// Theme management
|
// Theme management
|
||||||
function setTheme(theme) {
|
function setTheme(theme) {
|
||||||
document.documentElement.setAttribute('data-theme', theme);
|
document.documentElement.setAttribute('data-theme', theme);
|
||||||
localStorage.setItem('theme', theme);
|
localStorage.setItem('theme', theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSystemTheme() {
|
||||||
|
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize theme
|
||||||
|
const savedTheme = getSystemTheme();
|
||||||
|
setTheme(savedTheme);
|
||||||
|
|
||||||
|
// API base URL
|
||||||
|
const API_BASE = '/api/v1/client';
|
||||||
|
|
||||||
|
// Auth token management
|
||||||
|
function getAuthToken() {
|
||||||
|
return localStorage.getItem('knoptpq');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchWithAuth(url, options = {}) {
|
||||||
|
const token = getAuthToken();
|
||||||
|
if (!token) {
|
||||||
|
window.location.href = '/client/login';
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSystemTheme() {
|
const response = await fetch(url, {
|
||||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
...options,
|
||||||
}
|
headers: {
|
||||||
|
'token': `${token}`,
|
||||||
// Initialize theme
|
'Content-Type': 'application/json',
|
||||||
const savedTheme = getSystemTheme();
|
...options.headers,
|
||||||
setTheme(savedTheme);
|
|
||||||
|
|
||||||
// API base URL
|
|
||||||
const API_BASE = '/api/v1/client';
|
|
||||||
|
|
||||||
// Auth token management
|
|
||||||
function getAuthToken() {
|
|
||||||
return localStorage.getItem('knoptpq');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchWithAuth(url, options = {}) {
|
|
||||||
const token = getAuthToken();
|
|
||||||
if (!token) {
|
|
||||||
window.location.href = '/';
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const response = await fetch(url, {
|
if (response.status === 401) {
|
||||||
...options,
|
localStorage.removeItem('knoptpq');
|
||||||
headers: {
|
localStorage.removeItem('refreshToken');
|
||||||
'token': `${token}`,
|
window.location.href = '/client/login';
|
||||||
'Content-Type': 'application/json',
|
return;
|
||||||
...options.headers,
|
}
|
||||||
}
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleLogout() {
|
||||||
|
try {
|
||||||
|
// Call logout API
|
||||||
|
await fetchWithAuth(`${API_BASE}/logout`, {
|
||||||
|
method: 'POST'
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Logout API error:', error);
|
||||||
|
} finally {
|
||||||
|
// Clear local storage and redirect
|
||||||
|
localStorage.removeItem('knoptpq');
|
||||||
|
localStorage.removeItem('refreshToken');
|
||||||
|
window.location.href = '/client/login';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (response.status === 401) {
|
// Format time function
|
||||||
localStorage.removeItem('knoptpq');
|
function formatTime(seconds) {
|
||||||
localStorage.removeItem('refreshToken');
|
if (!seconds) return 'No Record Found';
|
||||||
window.location.href = '/';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
const hours = Math.floor(seconds / 3600);
|
||||||
|
const minutes = Math.floor((seconds % 3600) / 60);
|
||||||
|
|
||||||
|
if (hours > 0) {
|
||||||
|
return `${hours}h ${minutes}m`;
|
||||||
|
} else if (minutes > 0) {
|
||||||
|
return `${minutes}m`;
|
||||||
|
} else {
|
||||||
|
return 'Just started';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const vaModal = document.getElementById('vaModal');
|
||||||
|
|
||||||
|
function openVAModal(va) {
|
||||||
|
// Set basic VA info
|
||||||
|
document.getElementById('modalVAName').textContent = va.name;
|
||||||
|
|
||||||
|
// Set projects
|
||||||
|
const projects = va.projects || [];
|
||||||
|
document.getElementById('projectsCountBadge').textContent = projects.length;
|
||||||
|
|
||||||
|
const projectsTableBody = document.getElementById('modalProjectsTableBody');
|
||||||
|
const noProjectsMessage = document.getElementById('modalNoProjects');
|
||||||
|
|
||||||
|
if (projects.length > 0) {
|
||||||
|
projectsTableBody.innerHTML = projects.map(project => createProjectTableRow(project)).join('');
|
||||||
|
projectsTableBody.style.display = '';
|
||||||
|
noProjectsMessage.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
projectsTableBody.innerHTML = '';
|
||||||
|
projectsTableBody.style.display = 'none';
|
||||||
|
noProjectsMessage.style.display = 'block';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleLogout() {
|
// Set action button
|
||||||
try {
|
const modalActions = document.getElementById('modalActions');
|
||||||
// Call logout API
|
const viewSessionBtn = document.getElementById('viewSessionBtn');
|
||||||
await fetchWithAuth(`${API_BASE}/logout`, {
|
|
||||||
method: 'POST'
|
if (va.isCurrentlyActive && va.lastProjectId) {
|
||||||
});
|
viewSessionBtn.href = `/live/${va.lastProjectId}/${va.id}`;
|
||||||
} catch (error) {
|
modalActions.style.display = 'block';
|
||||||
console.error('Logout API error:', error);
|
} else {
|
||||||
} finally {
|
modalActions.style.display = 'none';
|
||||||
// Clear local storage and redirect
|
|
||||||
localStorage.removeItem('knoptpq');
|
|
||||||
localStorage.removeItem('refreshToken');
|
|
||||||
window.location.href = '/';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format time function
|
// Show modal
|
||||||
function formatTime(seconds) {
|
vaModal.style.display = 'flex';
|
||||||
if (!seconds) return '-';
|
document.body.style.overflow = 'hidden';
|
||||||
|
}
|
||||||
|
|
||||||
const hours = Math.floor(seconds / 3600);
|
function closeVAModal() {
|
||||||
const minutes = Math.floor((seconds % 3600) / 60);
|
vaModal.style.display = 'none';
|
||||||
|
document.body.style.overflow = 'auto';
|
||||||
|
}
|
||||||
|
|
||||||
if (hours > 0) {
|
// Create project table row for modal
|
||||||
return `${hours}h ${minutes}m`;
|
function createProjectTableRow(project) {
|
||||||
} else if (minutes > 0) {
|
const statusClass = project.isCurrentlyWorking ? 'active' : 'inactive';
|
||||||
return `${minutes}m`;
|
const statusText = project.isCurrentlyWorking ? 'Working' : 'Not Active';
|
||||||
} else {
|
|
||||||
return 'Just started';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const vaModal = document.getElementById('vaModal');
|
return `
|
||||||
|
|
||||||
function openVAModal(va) {
|
|
||||||
// Set basic VA info
|
|
||||||
document.getElementById('modalVAName').textContent = va.name;
|
|
||||||
|
|
||||||
// Set projects
|
|
||||||
const projects = va.projects || [];
|
|
||||||
document.getElementById('projectsCountBadge').textContent = projects.length;
|
|
||||||
|
|
||||||
const projectsTableBody = document.getElementById('modalProjectsTableBody');
|
|
||||||
const noProjectsMessage = document.getElementById('modalNoProjects');
|
|
||||||
|
|
||||||
if (projects.length > 0) {
|
|
||||||
projectsTableBody.innerHTML = projects.map(project => createProjectTableRow(project)).join('');
|
|
||||||
projectsTableBody.style.display = '';
|
|
||||||
noProjectsMessage.style.display = 'none';
|
|
||||||
} else {
|
|
||||||
projectsTableBody.innerHTML = '';
|
|
||||||
projectsTableBody.style.display = 'none';
|
|
||||||
noProjectsMessage.style.display = 'block';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set action button
|
|
||||||
const modalActions = document.getElementById('modalActions');
|
|
||||||
const viewSessionBtn = document.getElementById('viewSessionBtn');
|
|
||||||
|
|
||||||
if (va.isCurrentlyActive && va.lastProjectId) {
|
|
||||||
viewSessionBtn.href = `/live/${va.lastProjectId}/${va.id}`;
|
|
||||||
modalActions.style.display = 'block';
|
|
||||||
} else {
|
|
||||||
modalActions.style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show modal
|
|
||||||
vaModal.style.display = 'flex';
|
|
||||||
document.body.style.overflow = 'hidden';
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeVAModal() {
|
|
||||||
vaModal.style.display = 'none';
|
|
||||||
document.body.style.overflow = 'auto';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create project table row for modal
|
|
||||||
function createProjectTableRow(project) {
|
|
||||||
const statusClass = project.isCurrentlyWorking ? 'active' : 'inactive';
|
|
||||||
const statusText = project.isCurrentlyWorking ? 'Working' : 'Not Active';
|
|
||||||
|
|
||||||
return `
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<div class="project-info">
|
<div class="project-info">
|
||||||
|
|
@ -439,18 +377,18 @@
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create VA table row for main table
|
// Create VA table row for main table
|
||||||
function createVATableRow(va) {
|
function createVATableRow(va) {
|
||||||
const statusClass = va.isCurrentlyActive ? 'active' : 'inactive';
|
const statusClass = va.isCurrentlyActive ? 'active' : 'inactive';
|
||||||
const statusText = va.isCurrentlyActive ? 'Active' : 'Inactive';
|
const statusText = va.isCurrentlyActive ? 'Active' : 'Inactive';
|
||||||
const activeProject = va?.lastProject?.name ? va.lastProject.name : '-';
|
const activeProject = va?.lastProject?.name ? va.lastProject.name : '-';
|
||||||
const workedTime = formatTime(va.currentSessionDuration);
|
const workedTime = formatTime(va.currentSessionDuration);
|
||||||
const projectsAssigned = va.projectsAssigned || 0;
|
const projectsAssigned = va.projectsAssigned || 0;
|
||||||
const viewLink = va.isCurrentlyActive ? `<a href="/live/${va.lastProjectId}/${va.id}" class="view-link va-action" data-va-id="${va.id}" target="_blank">Live Tracking</a>` : '<p class="va-logs va-action" data-va-id="${va.id}" target="_blank">Logs</p>';
|
const viewLink = va.isCurrentlyActive ? `<a href="/live/${va.lastProjectId}/${va.id}" class="view-link" data-va-id="${va.id}" target="_blank">Live Tracking</a>` : '';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<tr class="va-row" data-va-id="${va.id}">
|
<tr class="va-row" data-va-id="${va.id}">
|
||||||
<td>
|
<td>
|
||||||
<div class="status-indicator">
|
<div class="status-indicator">
|
||||||
|
|
@ -467,7 +405,7 @@
|
||||||
<span class="projects-count">${activeProject}</span>
|
<span class="projects-count">${activeProject}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="projects-count">${projectsAssigned}</span>
|
<span class="projects-count">${projectsAssigned} Projects</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="worked-time">${workedTime}</span>
|
<span class="worked-time">${workedTime}</span>
|
||||||
|
|
@ -477,161 +415,144 @@
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load dashboard data
|
// Load dashboard data
|
||||||
async function loadDashboardData() {
|
async function loadDashboardData() {
|
||||||
try {
|
try {
|
||||||
const response = await fetchWithAuth(`${API_BASE}/dashboard`);
|
const response = await fetchWithAuth(`${API_BASE}/dashboard`);
|
||||||
if (!response) return;
|
if (!response) return;
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const data = result.data;
|
const data = result.data;
|
||||||
const summary = result.summary;
|
const summary = result.summary;
|
||||||
|
|
||||||
// Update stats cards
|
// Update stats cards
|
||||||
document.getElementById('activeVaCount').textContent = summary.activeVAs || 0;
|
document.getElementById('activeVaCount').textContent = summary.activeVAs || 0;
|
||||||
document.getElementById('totalVaCount').textContent = summary.totalVAs || 0;
|
document.getElementById('totalVaCount').textContent = summary.totalVAs || 0;
|
||||||
document.getElementById('totalProjects').textContent = summary.totalProjects || 0;
|
document.getElementById('totalProjects').textContent = summary.totalProjects || 0;
|
||||||
|
|
||||||
// Update VA list count
|
// Update VA list count
|
||||||
document.getElementById('vaListCount').textContent = `${summary.totalVAs || 0} VAs`;
|
document.getElementById('vaListCount').textContent = `${summary.totalVAs || 0} VAs`;
|
||||||
|
|
||||||
// Update VA table
|
// Update VA table
|
||||||
const vaTableBody = document.getElementById('vaTableBody');
|
const vaTableBody = document.getElementById('vaTableBody');
|
||||||
const vaEmptyState = document.getElementById('vaEmptyState');
|
const vaEmptyState = document.getElementById('vaEmptyState');
|
||||||
const allVAs = data.allVAs || [];
|
const allVAs = data.allVAs || [];
|
||||||
|
|
||||||
if (allVAs.length > 0) {
|
if (allVAs.length > 0) {
|
||||||
vaTableBody.innerHTML = allVAs.map(va => createVATableRow(va)).join('');
|
vaTableBody.innerHTML = allVAs.map(va => createVATableRow(va)).join('');
|
||||||
vaTableBody.style.display = '';
|
vaTableBody.style.display = '';
|
||||||
vaEmptyState.style.display = 'none';
|
vaEmptyState.style.display = 'none';
|
||||||
|
|
||||||
// Store VA data globally for modal access
|
// Store VA data globally for modal access
|
||||||
window.vaData = allVAs;
|
window.vaData = allVAs;
|
||||||
|
|
||||||
// Add click event listeners to VA rows
|
// Add click event listeners to VA rows
|
||||||
document.querySelectorAll('.va-info').forEach((row, index) => {
|
document.querySelectorAll('.va-row').forEach((row, index) => {
|
||||||
row.addEventListener('click', function (e) {
|
row.addEventListener('click', function(e) {
|
||||||
// Don't open modal if clicking on view link
|
// Don't open modal if clicking on view link
|
||||||
if (!e.target.closest('.view-link')) {
|
if (!e.target.closest('.view-link')) {
|
||||||
const va = allVAs[index];
|
const va = allVAs[index];
|
||||||
openVAModal(va);
|
openVAModal(va);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
});
|
});
|
||||||
} else {
|
});
|
||||||
vaTableBody.innerHTML = '';
|
|
||||||
vaTableBody.style.display = 'none';
|
|
||||||
vaEmptyState.style.display = 'block';
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
console.error('Failed to load dashboard data:', result.message);
|
vaTableBody.innerHTML = '';
|
||||||
showError('Failed to load dashboard data');
|
vaTableBody.style.display = 'none';
|
||||||
|
vaEmptyState.style.display = 'block';
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} else {
|
||||||
console.error('Error loading dashboard data:', error);
|
console.error('Failed to load dashboard data:', result.message);
|
||||||
showError('Error loading dashboard data');
|
showError('Failed to load dashboard data');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading dashboard data:', error);
|
||||||
|
showError('Error loading dashboard data');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Show error message
|
// Show error message
|
||||||
function showError(message) {
|
function showError(message) {
|
||||||
const vaTableBody = document.getElementById('vaTableBody');
|
const vaTableBody = document.getElementById('vaTableBody');
|
||||||
const vaEmptyState = document.getElementById('vaEmptyState');
|
const vaEmptyState = document.getElementById('vaEmptyState');
|
||||||
|
|
||||||
vaTableBody.style.display = 'none';
|
vaTableBody.style.display = 'none';
|
||||||
vaEmptyState.innerHTML = `
|
vaEmptyState.innerHTML = `
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<i class="fas fa-exclamation-triangle"></i>
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
<h3>Error</h3>
|
<h3>Error</h3>
|
||||||
<p>${message}</p>
|
<p>${message}</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
vaEmptyState.style.display = 'block';
|
vaEmptyState.style.display = 'block';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close modal when clicking outside
|
// Close modal when clicking outside
|
||||||
vaModal.addEventListener('click', (e) => {
|
vaModal.addEventListener('click', (e) => {
|
||||||
if (e.target === vaModal) {
|
if (e.target === vaModal) {
|
||||||
closeVAModal();
|
closeVAModal();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close modal with escape key
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Escape' && vaModal.style.display === 'flex') {
|
||||||
|
closeVAModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Optional: Auto-refresh data every 30 seconds
|
||||||
|
setInterval(loadDashboardData, 30000);
|
||||||
|
|
||||||
|
// Logout Modal Functions
|
||||||
|
function openLogoutModal() {
|
||||||
|
const modal = document.getElementById('logoutModal');
|
||||||
|
modal.style.display = 'flex';
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeLogoutModal() {
|
||||||
|
const modal = document.getElementById('logoutModal');
|
||||||
|
modal.style.display = 'none';
|
||||||
|
document.body.style.overflow = 'auto';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Load dashboard data
|
||||||
|
loadDashboardData();
|
||||||
|
|
||||||
|
// Mobile menu toggle
|
||||||
|
const menuToggle = document.querySelector('.mobile-menu-button');
|
||||||
|
const sidebar = document.querySelector('.dashboard-sidebar');
|
||||||
|
menuToggle.addEventListener('click', () => {
|
||||||
|
sidebar.classList.toggle('active');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close modal with escape key
|
// Close modal with escape key
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener('keydown', function(e) {
|
||||||
if (e.key === 'Escape' && vaModal.style.display === 'flex') {
|
if (e.key === 'Escape') {
|
||||||
closeVAModal();
|
closeLogoutModal();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Optional: Auto-refresh data every 30 seconds
|
document.getElementById('logoutModal').addEventListener('click', function(e) {
|
||||||
setInterval(loadDashboardData, 30000);
|
if (e.target === this) {
|
||||||
|
closeLogoutModal();
|
||||||
// Logout Modal Functions
|
|
||||||
function openLogoutModal() {
|
|
||||||
const modal = document.getElementById('logoutModal');
|
|
||||||
modal.style.display = 'flex';
|
|
||||||
document.body.style.overflow = 'hidden';
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeLogoutModal() {
|
|
||||||
const modal = document.getElementById('logoutModal');
|
|
||||||
modal.style.display = 'none';
|
|
||||||
document.body.style.overflow = 'auto';
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleVaLogsClick = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
cosnole.log("handleVaLogsClick Called")
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
|
||||||
// Load dashboard data
|
|
||||||
loadDashboardData();
|
|
||||||
|
|
||||||
// Mobile menu toggle
|
|
||||||
const menuToggle = document.querySelector('.mobile-menu-button');
|
|
||||||
const sidebar = document.querySelector('.dashboard-sidebar');
|
|
||||||
menuToggle.addEventListener('click', () => {
|
|
||||||
sidebar.classList.toggle('active');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close modal with escape key
|
|
||||||
document.addEventListener('keydown', function (e) {
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
closeLogoutModal();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('logoutModal').addEventListener('click', function (e) {
|
|
||||||
if (e.target === this) {
|
|
||||||
closeLogoutModal();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Logout button - opens confirmation modal instead of directly logging out
|
|
||||||
document.getElementById('logoutButton').addEventListener('click', openLogoutModal);
|
|
||||||
|
|
||||||
// Confirm logout button
|
|
||||||
document.getElementById('confirmLogoutBtn').addEventListener('click', handleLogout);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const allVaLogsBtn = document.querySelectorAll(".va-logs");
|
|
||||||
for (const va of allVaLogsBtn) {
|
|
||||||
va.addEventListener("click", handleVaLogsClick)
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Logout button - opens confirmation modal instead of directly logging out
|
||||||
|
document.getElementById('logoutButton').addEventListener('click', openLogoutModal);
|
||||||
|
|
||||||
|
// Confirm logout button
|
||||||
|
document.getElementById('confirmLogoutBtn').addEventListener('click', handleLogout);
|
||||||
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
@ -222,7 +222,7 @@
|
||||||
<form>
|
<form>
|
||||||
<input type="email" name="email" placeholder="Enter your email address *" required />
|
<input type="email" name="email" placeholder="Enter your email address *" required />
|
||||||
<input type="password" name="password" placeholder="Enter your password *" required />
|
<input type="password" name="password" placeholder="Enter your password *" required />
|
||||||
<a href="/forgot-password" class="forgot">Forgot password?</a>
|
<a href="/client/forgot-password" class="forgot">Forgot password?</a>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary">Log in using password</button>
|
<button type="submit" class="btn btn-primary">Log in using password</button>
|
||||||
|
|
||||||
|
|
@ -282,12 +282,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
if(data.userType === "CLIENT") {
|
// 🟢 Save token
|
||||||
localStorage.setItem("knoptpq", data.accessToken);
|
localStorage.setItem("knoptpq", data.accessToken);
|
||||||
window.location.href = "/client/dashboard";
|
// localStorage.setItem("refreshToken", data.refreshToken);
|
||||||
} else {
|
|
||||||
window.location.href = "/download-tracker";
|
// 🟢 Redirect
|
||||||
}
|
window.location.href = "/client/dashboard";
|
||||||
} else {
|
} else {
|
||||||
alert("No token received. Please try again.");
|
alert("No token received. Please try again.");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -122,7 +122,7 @@
|
||||||
<h2>Reset your password</h2>
|
<h2>Reset your password</h2>
|
||||||
<input type="email" placeholder="Enter your email address*" required />
|
<input type="email" placeholder="Enter your email address*" required />
|
||||||
<button type="submit">Send password reset code</button>
|
<button type="submit">Send password reset code</button>
|
||||||
<a href="/">Go back to login</a>
|
<a href="/client/login">Go back to login</a>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -181,7 +181,7 @@
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
if(response.success) {
|
if(response.success) {
|
||||||
if(confirm('Please Check your mail inbox for password reset link')) {
|
if(confirm('Please Check your mail inbox for password reset link')) {
|
||||||
window.location.href = "/"; // Redirect to reset password page
|
window.location.href = "/client/login"; // Redirect to reset password page
|
||||||
// console.log(response)
|
// console.log(response)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,13 @@
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<title>Live Screen Feed</title>
|
<title>Live Screen Feed</title>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
|
@ -163,7 +163,7 @@
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<!-- Mobile Toggle Button -->
|
<!-- Mobile Toggle Button -->
|
||||||
<div class="mobile-toggle" id="toggleBtn">
|
<div class="mobile-toggle" id="toggleBtn">
|
||||||
<span></span>
|
<span></span>
|
||||||
<span></span>
|
<span></span>
|
||||||
|
|
@ -207,8 +207,6 @@
|
||||||
clientId: clientId
|
clientId: clientId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const sidebar = document.getElementById("sidebar");
|
const sidebar = document.getElementById("sidebar");
|
||||||
const toggleBtn = document.getElementById("toggleBtn");
|
const toggleBtn = document.getElementById("toggleBtn");
|
||||||
const overlay = document.getElementById("overlay");
|
const overlay = document.getElementById("overlay");
|
||||||
|
|
@ -231,12 +229,8 @@
|
||||||
overlay.classList.remove("show");
|
overlay.classList.remove("show");
|
||||||
};
|
};
|
||||||
|
|
||||||
socket.on("connected", () => {
|
|
||||||
socket.emit("requestVisibleApps", { token });
|
|
||||||
});
|
|
||||||
|
|
||||||
/* Receive visible apps */
|
/* Receive visible apps */
|
||||||
socket.on("receiveVisibleApps", ({ apps, vaId: senderVaId }) => {
|
socket.on("receiveVisibleApps", ({apps, vaId: senderVaId}) => {
|
||||||
if (vaId !== senderVaId) return;
|
if (vaId !== senderVaId) return;
|
||||||
|
|
||||||
appsList.innerHTML = "";
|
appsList.innerHTML = "";
|
||||||
|
|
@ -275,7 +269,7 @@
|
||||||
socket.on("authError", () => {
|
socket.on("authError", () => {
|
||||||
socket.disconnect();
|
socket.disconnect();
|
||||||
localStorage.removeItem("knoptpq");
|
localStorage.removeItem("knoptpq");
|
||||||
window.location.href = "/";
|
window.location.href = "/client/login";
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Receive images */
|
/* Receive images */
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,6 @@
|
||||||
select option:disabled {
|
select option:disabled {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
|
||||||
background-color: white;
|
|
||||||
color: #2d2d2d;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue