Compare commits

..

15 Commits

34 changed files with 3513 additions and 698 deletions

View File

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

View File

@ -476,3 +476,42 @@ body {
margin-bottom: 40px; margin-bottom: 40px;
} }
} }
.step-error-container {
background-color: #fef2f2;
border: 1px solid #fecaca;
border-radius: 8px;
padding: 12px 16px;
margin-bottom: 20px;
animation: slideDown 0.3s ease-out;
}
.error-message {
color: #dc2626;
font-size: 14px;
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
}
.error-message::before {
content: "⚠";
font-size: 16px;
}
.field-error {
border: 1px solid #dc2626 !important;
background-color: #fef2f2;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -109,7 +109,22 @@
// Add input event listeners to handle label animation // Add input event listeners to handle label animation
document.querySelectorAll('.input-field').forEach(input => { document.querySelectorAll('.input-field:not(.dob)').forEach(input => {
setTimeout(() => {
if (input.value) {
input.nextElementSibling.style.top = '4px';
input.nextElementSibling.style.fontSize = '12px';
input.nextElementSibling.style.transform = 'none';
}
}, 100);
input.addEventListener('animationstart', function() {
if (this.value) {
this.nextElementSibling.style.top = '4px';
this.nextElementSibling.style.fontSize = '12px';
this.nextElementSibling.style.transform = 'none';
}
});
input.addEventListener('input', function() { input.addEventListener('input', function() {
if (this.value) { if (this.value) {
this.nextElementSibling.style.top = '4px'; this.nextElementSibling.style.top = '4px';
@ -139,6 +154,13 @@
// Handle select field label animation // Handle select field label animation
document.querySelectorAll('.select-field').forEach(select => { document.querySelectorAll('.select-field').forEach(select => {
setTimeout(() => {
if (select.value) {
select.nextElementSibling.style.top = '4px';
select.nextElementSibling.style.fontSize = '12px';
select.nextElementSibling.style.transform = 'none';
}
}, 100);
select.addEventListener('change', function() { select.addEventListener('change', function() {
if (this.value) { if (this.value) {
this.nextElementSibling.style.top = '4px'; this.nextElementSibling.style.top = '4px';
@ -228,10 +250,14 @@
return maxStep; return maxStep;
} }
async function handleNextStep(step) {
await nextStep(step);
}
// Function to navigate to next step // Function to navigate to next step
function nextStep(step) { async function nextStep(step) {
// Validate current step before proceeding // Validate current step before proceeding
if (!validateStep(currentStep)) { if (!(await validateStep(currentStep))) {
return; return;
} }
@ -306,10 +332,14 @@
} }
// Function to validate current step // Function to validate current step
function validateStep(step) { async function validateStep(step) {
let isValid = true; let isValid = true;
const form = document.getElementById(getFormId(step)); const form = document.getElementById(getFormId(step));
// Hide any previous errors for this step
hideError(step);
clearFieldErrors();
if (form) { if (form) {
// For account setup step // For account setup step
if (step === 1) { if (step === 1) {
@ -319,21 +349,31 @@
// Email validation // Email validation
if (!validateEmail(email)) { if (!validateEmail(email)) {
alert('Please enter a valid email address'); showError('Please enter a valid email address', 'email', step);
isValid = false; isValid = false;
} else {
// Email verification - wait for the result
const isEmailVerified = await verifyEmail(email);
if (!isEmailVerified) {
showError('Email already registered. Please use a different email address.', 'email', step);
isValid = false;
}
} }
// Password validation // Password validation
if (password.length < 8) { if (password.length < 8) {
alert('Password must be at least 8 characters long'); showError('Password must be at least 8 characters long', 'password', step);
isValid = false; isValid = false;
} }
if (password !== confirmPassword) { if (password !== confirmPassword) {
document.getElementById('password-match-error').style.display = 'block'; document.getElementById('password-match-error').style.display = 'block';
document.getElementById('confirmPassword').classList.add('field-error');
showError('Passwords do not match', 'confirmPassword', step);
isValid = false; isValid = false;
} else { } else {
document.getElementById('password-match-error').style.display = 'none'; document.getElementById('password-match-error').style.display = 'none';
document.getElementById('confirmPassword').classList.remove('field-error');
} }
} }
@ -342,15 +382,18 @@
const firstName = document.getElementById('firstName').value; const firstName = document.getElementById('firstName').value;
if (!firstName) { if (!firstName) {
document.getElementById('profile-incomplete').style.display = 'block'; document.getElementById('profile-incomplete').style.display = 'block';
document.getElementById('firstName').classList.add('field-error');
showError('First name is required', 'firstName', step);
isValid = false; isValid = false;
} else { } else {
document.getElementById('profile-incomplete').style.display = 'none'; document.getElementById('profile-incomplete').style.display = 'none';
document.getElementById('firstName').classList.remove('field-error');
} }
// Age validation // Age validation
const age = document.getElementById('age').value; const age = document.getElementById('age').value;
if (age && (age < 18 || age > 100)) { if (age && (age < 18 || age > 100)) {
alert('You must be 18 years or older to register'); showError('You age must be 18 years or older to register', 'dob', step);
isValid = false; isValid = false;
} }
@ -359,12 +402,12 @@
const state = document.getElementById('state').value; const state = document.getElementById('state').value;
if (!country) { if (!country) {
alert('Please select your country'); showError('Please select your country', 'country', step);
isValid = false; isValid = false;
} }
if (!state) { if (!state) {
alert('Please select your state/region'); showError('Please select your state/region', 'state', step);
isValid = false; isValid = false;
} }
@ -373,9 +416,40 @@
requiredFields.forEach(field => { requiredFields.forEach(field => {
if (!field.value.trim()) { if (!field.value.trim()) {
isValid = false; isValid = false;
field.style.border = '1px solid #ef4444'; field.classList.add('field-error');
if (isValid === false) {
showError(`Please fill in ${field.labels[0]?.textContent || 'this field'}`, field.id, step);
}
} else { } else {
field.style.border = ''; field.classList.remove('field-error');
}
});
}
if (step === 3) {
// Phone number validation
const phone = document.getElementById('phone').value;
if (!phone) {
showError('Please enter your phone number', 'phone', step);
isValid = false;
} else if (!validatePhoneNumberStrict(phone)) {
document.getElementById('phone-error').style.display = 'block';
document.getElementById('phone').classList.add('field-error');
showError('Please enter a valid phone number (10-15 digits)', 'phone', step);
isValid = false;
} else {
document.getElementById('phone-error').style.display = 'none';
document.getElementById('phone').classList.remove('field-error');
}
// Validate other required fields in step 3
const requiredFields = form.querySelectorAll('[required]');
requiredFields.forEach(field => {
if (!field.value.trim()) {
isValid = false;
field.classList.add('field-error');
} else {
field.classList.remove('field-error');
} }
}); });
} }
@ -384,20 +458,20 @@
if (step === 4) { if (step === 4) {
const selectedPayment = document.querySelector('.payment-option.selected'); const selectedPayment = document.querySelector('.payment-option.selected');
if (!selectedPayment) { if (!selectedPayment) {
alert('Please select a payment method'); showError('Please select a payment method', null, step);
isValid = false; isValid = false;
} else { } else {
const paymentMethod = selectedPayment.getAttribute('onclick').match(/'([^']+)'/)[1]; const paymentMethod = selectedPayment.getAttribute('onclick').match(/'([^']+)'/)[1];
if (paymentMethod === 'payoneer') { if (paymentMethod === 'payoneer') {
const payoneerAccount = document.getElementById('payoneerAccount').value; const payoneerAccount = document.getElementById('payoneerAccount').value;
if (!payoneerAccount) { if (!payoneerAccount) {
alert('Please enter your Payoneer account number'); showError('Please enter your Payoneer account number', 'payoneerAccount', step);
isValid = false; isValid = false;
} }
} else if (paymentMethod === 'veem') { } else if (paymentMethod === 'veem') {
const veemAccount = document.getElementById('veemAccount').value; const veemAccount = document.getElementById('veemAccount').value;
if (!veemAccount) { if (!veemAccount) {
alert('Please enter your Veem account number'); showError('Please enter your Veem account number', 'veemAccount', step);
isValid = false; isValid = false;
} }
} }
@ -410,9 +484,9 @@
requiredFields.forEach(field => { requiredFields.forEach(field => {
if (!field.value.trim()) { if (!field.value.trim()) {
isValid = false; isValid = false;
field.style.border = '1px solid #ef4444'; field.classList.add('field-error');
} else { } else {
field.style.border = ''; field.classList.remove('field-error');
} }
}); });
} }
@ -598,12 +672,12 @@
}, 1000); }, 1000);
} else { } else {
// Handle server errors // Handle server errors
alert('There was an error submitting your registration. Please try again.'); showError('There was an error submitting your registration. Please try again.', null, 4);
} }
} catch (error) { } catch (error) {
// Handle network errors // Handle network errors
console.error('Error:', error); console.error('Error:', error);
alert('There was a network error. Please check your connection and try again.'); showError('There was a network error. Please check your connection and try again.', null, 4);
} finally { } finally {
// Restore button state // Restore button state
submitBtn.textContent = originalText; submitBtn.textContent = originalText;
@ -611,3 +685,220 @@
} }
} }
async function verifyEmail(email) {
try {
const response = await fetch('/api/v1/va/verify-va-email', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({"email": email})
});
if (response.ok) {
return false;
} else {
return true;
}
} catch (error) {
// Handle network errors
console.error('Error:', error);
showError('There was a network error. Please check your connection and try again.', null, 1);
return false;
}
}
// Function to calculate age from date of birth
function calculateAge(dob) {
if (!dob) return '';
const birthDate = new Date(dob);
const today = new Date();
let age = today.getFullYear() - birthDate.getFullYear();
const monthDiff = today.getMonth() - birthDate.getMonth();
// Adjust age if birthday hasn't occurred this year yet
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
age--;
}
return age;
}
// Function to handle date of birth change and update age
function handleDOBChange() {
const dob = document.getElementById('dob').value;
const ageInput = document.getElementById('age');
if (dob) {
const age = calculateAge(dob);
ageInput.value = age;
// Trigger input event to update the label position
ageInput.dispatchEvent(new Event('input'));
// Validate age
if (age < 18) {
// alert('You must be 18 years or older to register');
showError('You Age must be 18 years or older to register', 'dob',);
ageInput.style.border = '1px solid #ef4444';
} else if (age > 100) {
// alert('Please enter a valid age');
showError('Please enter a valid age', 'dob');
ageInput.style.border = '1px solid #ef4444';
} else {
ageInput.style.border = '';
}
} else {
// Clear age if DOB is cleared
ageInput.value = '';
ageInput.dispatchEvent(new Event('input'));
}
}
// Function to handle age manual input and update DOB (optional)
function handleAgeChange() {
const age = document.getElementById('age').value;
const dobInput = document.getElementById('dob');
if (age && age >= 18 && age <= 100) {
// Calculate approximate DOB based on age
const today = new Date();
const birthYear = today.getFullYear() - age;
const approximateDOB = `${birthYear}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
// You can set this as the DOB or just leave it for reference
// dobInput.value = approximateDOB;
// dobInput.dispatchEvent(new Event('change'));
}
}
// Function to validate phone number in real-time
function validatePhoneNumber(input) {
const phoneError = document.getElementById('phone-error');
const value = input.value;
// Remove any non-digit characters
const digitsOnly = value.replace(/\D/g, '');
// Update the input value with digits only
if (value !== digitsOnly) {
input.value = digitsOnly;
}
// Validate length
if (digitsOnly.length > 0 && (digitsOnly.length < 10 || digitsOnly.length > 15)) {
phoneError.style.display = 'block';
input.style.border = '1px solid #ef4444';
return false;
} else {
phoneError.style.display = 'none';
input.style.border = '';
return true;
}
}
// Function to validate phone number on form submission
function validatePhoneNumberStrict(phone) {
const phoneRegex = /^[0-9]{10,15}$/;
return phoneRegex.test(phone);
}
// Function to validate Payoneer account number
function validatePayoneerAccount(input) {
const payoneerError = document.getElementById('payoneer-error');
const value = input.value.trim();
// Payoneer typically uses email format or alphanumeric IDs
// Adjust validation rules based on Payoneer's actual format requirements
if (value.length > 0 && (value.length < 5 || value.length > 20)) {
payoneerError.textContent = 'Payoneer account must be 5-20 characters';
payoneerError.style.display = 'block';
input.style.border = '1px solid #ef4445';
return false;
} else {
payoneerError.style.display = 'none';
input.style.border = '';
return true;
}
}
// Function to validate Veem account number
function validateVeemAccount(input) {
const veemError = document.getElementById('veem-error');
const value = input.value.trim();
// Veem account numbers are typically longer alphanumeric strings
if (value.length > 0 && (value.length < 8 || value.length > 25)) {
veemError.textContent = 'Veem account must be 8-25 characters';
veemError.style.display = 'block';
input.style.border = '1px solid #ef4445';
return false;
} else {
veemError.style.display = 'none';
input.style.border = '';
return true;
}
}
function showError(message, fieldId = null, step = null) {
// Determine the current step if not provided
const currentStep = step || getCurrentStep();
const errorContainer = document.getElementById(`step-${currentStep}-error`);
const errorMessage = document.getElementById(`step-${currentStep}-error-message`);
if (errorContainer && errorMessage) {
errorMessage.textContent = message;
errorContainer.style.display = 'block';
// Scroll to error container
errorContainer.scrollIntoView({ behavior: 'smooth', block: 'center' });
// Highlight specific field if provided
if (fieldId) {
const field = document.getElementById(fieldId);
if (field) {
field.classList.add('field-error');
// Remove error after 5 seconds
setTimeout(() => {
field.classList.remove('field-error');
}, 5000);
}
}
// Auto-hide error after 8 seconds
setTimeout(() => {
hideError(currentStep);
}, 8000);
}
}
function hideError(step = null) {
if (step) {
// Hide specific step error
const errorContainer = document.getElementById(`step-${step}-error`);
if (errorContainer) {
errorContainer.style.display = 'none';
}
} else {
// Hide all step errors
for (let i = 1; i <= 5; i++) {
const errorContainer = document.getElementById(`step-${i}-error`);
if (errorContainer) {
errorContainer.style.display = 'none';
}
}
}
}
function clearFieldErrors() {
document.querySelectorAll('.field-error').forEach(field => {
field.classList.remove('field-error');
});
}
function getCurrentStep() {
return currentStep;
}

View File

@ -0,0 +1,100 @@
import { ClientModel, ProjectModel, VAModel } from "../../../models/vaModel.js";
import jwt from "jsonwebtoken";
function formatHoursToReadable(hours) {
const wholeHours = Math.floor(hours);
const decimalMinutes = (hours - wholeHours) * 60;
const wholeMinutes = Math.round(decimalMinutes);
if (wholeHours > 0 && wholeMinutes > 0) {
return `${wholeHours}h ${wholeMinutes}m`;
} else if (wholeHours > 0) {
return `${wholeHours}h`;
} else {
return `${wholeMinutes}m`;
}
}
// Getting client dashboard data - all VAs and projects
async function client_dashboard_projects(req, res) {
try {
const accessToken = req.headers.token;
if (!accessToken) {
return res.status(401).json({ success: false, message: "Missing token" });
}
const decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN);
if (!decoded || !decoded.email) {
return res.status(401).json({ success: false, message: "Invalid token" });
}
const clientEmail = decoded.email;
const clientUser = await ClientModel.findOne({ "userDetails.email": clientEmail });
if (!clientUser) {
return res.status(404).json({ success: false, message: "Client not found" });
}
// Get all VA IDs and Project IDs assigned to this client
const clientVaIds = clientUser.vaIds || [];
const clientProjectIds = clientUser.projectIds || [];
// Get all VAs assigned to this client (with required fields only)
const allAssignedVAs = await VAModel.find({
_id: { $in: clientVaIds }
})
.select('userDetails.firstName userDetails.lastName userDetails.email status')
.lean()
.exec();
// Get All Projects of client with required fields
const clientProjects = await ProjectModel.find({
_id: { $in: clientProjectIds }
})
.select('name hourlyRate vaAssigned dedicatedHours status')
.lean()
.exec();
// Format VAs with only required fields
const vasFormatted = allAssignedVAs.map((va) => ({
id: va._id,
firstName: va.userDetails?.firstName || '',
lastName: va.userDetails?.lastName || '',
email: va.userDetails?.email || '',
status: va.status || ''
}));
// Format projects with only required fields
const projectsFormatted = clientProjects.map((project) => ({
id: project._id,
name: project.name,
hourlyRate: project.hourlyRate,
vaAssigned: project.vaAssigned.length || 0,
dedicatedHours: formatHoursToReadable(project.dedicatedHours) || 0,
status: project.status || ''
}));
return res.status(200).json({
success: true,
data: {
vas: vasFormatted,
projects: projectsFormatted
},
counts: {
totalVAs: vasFormatted.length,
totalProjects: projectsFormatted.length
}
});
} catch (error) {
console.error("Client dashboard error:", error);
return res.status(500).json({
success: false,
message: "Internal Server Error",
error: error.message
});
}
}
export { client_dashboard_projects };

View File

@ -0,0 +1,32 @@
import { ClientModel } from "../../models/vaModel.js";
// Verify VA Email
async function client_verify_email(req, res) {
try {
// const payload = req.body ? JSON.parse(req.body) : {};
const payload = req.body || {};
const { email } = payload;
if (!email) {
return res.status(400).json({ success: false, message: "Please provide email" });
}
// Find user by email
const user = await ClientModel.findOne({ "userDetails.email": email });
if (!user) {
return res.status(404).json({ success: false, message: "Email not found" });
}
// Return accessToken and user info
return res.status(200).json({
success: true,
email: user.userDetails?.email,
message: "Email ID is already registerd"
});
} catch (error) {
console.error("va_register error:", error);
return res.status(500).json({ success: false, message: "Internal Server Error" });
}
}
export { client_verify_email };

View File

@ -71,7 +71,7 @@ async function va_login_client(req, res) {
}, },
}); });
} catch (error) { } catch (error) {
console.error("va_register error:", error); console.error("va_client_login_register error:", error);
return res.status(500).json({ success: false, message: "Internal Server Error" }); return res.status(500).json({ success: false, message: "Internal Server Error" });
} }
} }

View File

@ -1,6 +1,101 @@
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { VAModel, LoggedModel } from "../../../models/vaModel.js"; import { VAModel, LoggedModel, WorkTrackModel } from "../../../models/vaModel.js";
// function to close open work tracks and create logout
async function closePreviousSession(vaId, lastLog) {
try {
const currentTime = new Date();
// Find all open work tracks for this VA and project (where to is not set or totalTime is 0)
const openWorkTracks = await WorkTrackModel.find({
vaId,
logId: lastLog._id,
$or: [
{ to: { $exists: false } },
{ totalTime: 0 },
{ to: null }
]
});
// console.log(`Found ${openWorkTracks.length} open work tracks to close`);
// Close all open work tracks
for (const workTrack of openWorkTracks) {
const fromTime = new Date(workTrack.from);
const totalTimeMs = currentTime - fromTime;
// Update the work track with end time and total time
workTrack.to = currentTime;
workTrack.totalTime = totalTimeMs;
await workTrack.save();
// console.log(`Closed work track ${workTrack._id} with duration: ${totalTimeMs}ms`);
}
// Create a logout log entry
const logoutLog = await LoggedModel.create({
loggedType: "LOGOUT",
vaId,
vaEmail: lastLog.vaEmail, // Use email from previous log
projectId: lastLog.projectId,
time: currentTime // Use same timestamp for consistent logging
});
// console.log(`Created logout log: ${logoutLog._id}`);
// try {
// const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
// const sfDataSync = {
// "_id": logoutLog._id,
// "loggedType": "LOGOUT",
// "vaId": vaId,
// "vaEmail": lastLog.vaEmail,
// "projectId": lastLog.projectId,
// "time": logoutLog.time,
// }
// const response = await fetch(salesforceUrl, {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json',
// // Add any required headers for Salesforce authentication
// // 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
// },
// body: JSON.stringify(sfDataSync)
// });
// if (!response.ok) {
// const errorText = await response.text();
// throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
// }
// const result = await response.json();
// // console.log("✅ Successfully synced VA data to Salesforce:", result);
// return {
// closedWorkTracks: openWorkTracks.length,
// logoutLogId: logoutLog._id,
// SyncSalesforce: result
// };
// } catch (error) {
// console.error("Failed to sync with Salesforce:", error.message);
// console.log(error)
// // Create a custom error to distinguish Salesforce errors
// const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
// salesforceError.isSalesforceError = true;
// throw salesforceError;
// }
return {
closedWorkTracks: openWorkTracks.length,
logoutLogId: logoutLog._id,
};
} catch (error) {
console.error("Error closing previous session:", error);
throw error;
}
}
// Login VA // Login VA
async function va_login_callback(req, res) { async function va_login_callback(req, res) {
@ -32,34 +127,20 @@ async function va_login_callback(req, res) {
return res.status(401).json({ success: false, message: "Invalid password" }); return res.status(401).json({ success: false, message: "Invalid password" });
} }
// also on main login when user try to login into app then logout project and workModel track then allow login on same timestamp
// Check if VA has any active sessions (LOGIN without LOGOUT) // Check if VA has any active sessions (LOGIN without LOGOUT)
const activeSessions = await LoggedModel.find({ const activeSessions = await LoggedModel.find({
vaId: user._id, vaId: user._id
loggedType: "LOGIN"
}).sort({ time: -1 }); }).sort({ time: -1 });
let closeResult = [];
if (activeSessions.length > 0) { if (activeSessions.length > 0) {
// Get the latest login to check if there's no corresponding logout // Get the latest login to check if there's no corresponding logout
const latestLogin = activeSessions[0]; const latestLog = activeSessions[0];
closeResult = await closePreviousSession(user._id, latestLog);
// Check if there's a logout after this login
const correspondingLogout = await LoggedModel.findOne({
vaId: user._id,
loggedType: "LOGOUT",
time: { $gt: latestLogin.time }
});
if (!correspondingLogout) {
// VA is still logged in to a project
const projectInfo = await LoggedModel.findById(latestLogin._id)
.populate('projectId', 'name')
.lean();
return res.status(400).json({
success: false,
message: "You are still logged in to a project. Please logout first before logging in again."
});
}
} }
// Generate JWT accessToken with 24h expiry // Generate JWT accessToken with 24h expiry
@ -88,18 +169,40 @@ async function va_login_callback(req, res) {
$set: { accessToken, refreshToken } $set: { accessToken, refreshToken }
}) })
let responseData = [];
if (closeResult) {
responseData = {
success: true,
message: "Login Successfull",
accessToken,
refreshToken,
user: {
id: user._id,
name: user.userDetails.firstName,
email: user.userDetails.email,
},
previousSession: {
closedWorkTracks: closeResult.closedWorkTracks,
logoutLogId: closeResult.logoutLogId
}
}
} else {
responseData = {
success: true,
message: "Login Successfull",
accessToken,
refreshToken,
user: {
id: user._id,
name: user.userDetails.firstName,
email: user.userDetails.email,
}
}
}
// Return accessToken and user info // Return accessToken and user info
return res.status(200).json({ return res.status(200).json(responseData);
success: true,
message: "Login Successfull",
accessToken,
refreshToken,
user: {
id: user._id,
name: user.userDetails.firstName,
email: user.userDetails.email,
},
});
} catch (error) { } catch (error) {
console.error("va_register error:", error); console.error("va_register error:", error);
return res.status(500).json({ success: false, message: "Internal Server Error" }); return res.status(500).json({ success: false, message: "Internal Server Error" });

View File

@ -0,0 +1,32 @@
import { VAModel } from "../../../models/vaModel.js";
// Verify VA Email
async function va_verify_email(req, res) {
try {
// const payload = req.body ? JSON.parse(req.body) : {};
const payload = req.body || {};
const { email } = payload;
if (!email) {
return res.status(400).json({ success: false, message: "Please provide email" });
}
// Find user by email
const user = await VAModel.findOne({ "userDetails.email": email });
if (!user) {
return res.status(404).json({ success: false, message: "Invalid Email ID" });
}
// Return accessToken and user info
return res.status(200).json({
success: true,
email: user.userDetails?.email,
message: "Email verified successfully"
});
} catch (error) {
console.error("va_register error:", error);
return res.status(500).json({ success: false, message: "Internal Server Error" });
}
}
export { va_verify_email };

View File

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

View File

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

View File

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

View File

@ -1,6 +1,130 @@
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { WorkTrackModel, VAModel, ProjectModel, LoggedModel } from "../../../models/vaModel.js"; import { WorkTrackModel, VAModel, ProjectModel, LoggedModel } from "../../../models/vaModel.js";
// Helper function to get start and end of day
function getDayRange(date = new Date()) {
const start = new Date(date);
start.setHours(0, 0, 0, 0);
const end = new Date(date);
end.setHours(23, 59, 59, 999);
return { start, end };
}
function formatHoursToReadable(hours) {
const wholeHours = Math.floor(hours);
const decimalMinutes = (hours - wholeHours) * 60;
const wholeMinutes = Math.round(decimalMinutes);
if (wholeHours > 0 && wholeMinutes > 0) {
return `${wholeHours}h ${wholeMinutes}m`;
} else if (wholeHours > 0) {
return `${wholeHours}h`;
} else {
return `${wholeMinutes}m`;
}
}
// Helper function to check daily hours and create logout log if needed
async function checkDailyHoursAndLogout(vaId, vaEmail, projectId, project, currentSessionId = null, additionalTimeMs = 0) {
try {
const today = new Date();
const { start, end } = getDayRange(today);
// Get today's work entries for this VA and project
// Exclude the current session if it's already in the database
const query = {
vaId,
projectId,
from: { $gte: start, $lte: end }
};
if (currentSessionId) {
query._id = { $ne: currentSessionId };
}
const todayWorkEntries = await WorkTrackModel.find(query);
// Calculate total hours worked today
let totalTodayMs = todayWorkEntries.reduce((total, entry) => {
return total + (entry.totalTime || 0);
}, 0);
// Add the additional time from current session if provided
totalTodayMs += additionalTimeMs;
const totalTodayHours = totalTodayMs / (1000 * 60 * 60);
// Check if daily dedicated hours are completed
if (project.dedicatedHours > 0 && totalTodayHours >= project.dedicatedHours) {
// Create automatic logout log
const logoutLog = await LoggedModel.create({
loggedType: "LOGOUT",
vaId,
vaEmail,
projectId
});
try {
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
const sfDataSync = {
"_id": logoutLog._id,
"loggedType": "LOGOUT",
"vaId": vaId,
"vaEmail": vaEmail,
"projectId": projectId,
"time": logoutLog.time,
}
const response = await fetch(salesforceUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// Add any required headers for Salesforce authentication
// 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
},
body: JSON.stringify(sfDataSync)
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
}
const result = await response.json();
// console.log("✅ Successfully synced VA data to Salesforce:", result);
return {
shouldLogout: true,
totalTodayHours: totalTodayHours,
dedicatedHours: project.dedicatedHours,
logoutLogId: logoutLog._id,
SyncSalesforce: result
};
} catch (error) {
console.error("Failed to sync with Salesforce:", error.message);
console.log(error)
// Create a custom error to distinguish Salesforce errors
const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
salesforceError.isSalesforceError = true;
throw salesforceError;
}
}
return {
shouldLogout: false,
totalTodayHours: totalTodayHours,
dedicatedHours: project.dedicatedHours
};
} catch (error) {
console.error("Error in checkDailyHoursAndLogout:", error);
return { shouldLogout: false, error: error.message };
}
}
// Getting logged hours in a specific project of VA // Getting logged hours in a specific project of VA
async function va_log_track_work_callback(req, res) { async function va_log_track_work_callback(req, res) {
try { try {
@ -50,6 +174,16 @@ async function va_log_track_work_callback(req, res) {
// Handle POST → create entry // Handle POST → create entry
if (req.method === "POST") { if (req.method === "POST") {
const dailyCheck = await checkDailyHoursAndLogout(vaId, vaEmail, projectId, project);
if (dailyCheck.shouldLogout) {
return res.status(400).json({
success: false,
message: `Cannot start work - daily hours completed (${formatHoursToReadable(dailyCheck.totalTodayHours)} / ${formatHoursToReadable(dailyCheck.dedicatedHours)})`,
autoLogout: true
});
}
const createdTrack = await WorkTrackModel.create({ const createdTrack = await WorkTrackModel.create({
vaId, vaId,
projectId, projectId,
@ -63,6 +197,11 @@ async function va_log_track_work_callback(req, res) {
success: true, success: true,
message: "Work tracking started", message: "Work tracking started",
data: createdTrack, data: createdTrack,
dailyHours: {
todayTotal: formatHoursToReadable(dailyCheck.totalTodayHours),
dailyLimit: formatHoursToReadable(dailyCheck.dedicatedHours),
remaining: formatHoursToReadable(dailyCheck.dedicatedHours - dailyCheck.totalTodayHours)
}
}); });
} }
@ -89,11 +228,14 @@ async function va_log_track_work_callback(req, res) {
const formattedTotalTime = `${hours}h ${minutes}m ${seconds}s`; const formattedTotalTime = `${hours}h ${minutes}m ${seconds}s`;
// Check if daily dedicated hours are completed after this work session
const dailyCheck = await checkDailyHoursAndLogout(vaId, vaEmail, projectId, project, id, totalTimeMs);
workEntry.to = toTime; workEntry.to = toTime;
workEntry.totalTime = totalTimeMs; // store as number of minutes workEntry.totalTime = totalTimeMs; // store as number of minutes
await workEntry.save(); await workEntry.save();
return res.status(200).json({ const response = {
success: true, success: true,
message: "Work tracking stopped", message: "Work tracking stopped",
data: { data: {
@ -101,9 +243,24 @@ async function va_log_track_work_callback(req, res) {
from: workEntry.from, from: workEntry.from,
to: workEntry.to, to: workEntry.to,
formattedTotalTime, formattedTotalTime,
totalTimeMs totalTimeMs,
sessionHours: totalTimeMs / (1000 * 60 * 60)
}, },
}); dailyHours: {
todayTotal: formatHoursToReadable(dailyCheck.totalTodayHours),
dailyLimit: formatHoursToReadable(dailyCheck.dedicatedHours),
remaining: formatHoursToReadable(Math.max(0, dailyCheck.dedicatedHours - dailyCheck.totalTodayHours))
}
};
// If auto-logout was triggered, add that info to response
if (dailyCheck.shouldLogout) {
response.autoLogout = true;
response.message = `Work tracking stopped. Auto-logout created - daily hours completed (${formatHoursToReadable(dailyCheck.totalTodayHours)} / ${formatHoursToReadable(dailyCheck.dedicatedHours)})`;
response.logoutLogId = dailyCheck.logoutLogId;
}
return res.status(200).json(response);
} }
// Fallback for unsupported methods // Fallback for unsupported methods

View File

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

View File

@ -1,4 +1,120 @@
import { LoggedModel } from "../../../models/vaModel.js"; import { LoggedModel, WorkTrackModel, ProjectModel } from "../../../models/vaModel.js";
// Helper function to get start and end of day
function getDayRange(date = new Date()) {
const start = new Date(date);
start.setHours(0, 0, 0, 0);
const end = new Date(date);
end.setHours(23, 59, 59, 999);
return { start, end };
}
function formatHoursToReadable(hours) {
const wholeHours = Math.floor(hours);
const decimalMinutes = (hours - wholeHours) * 60;
const wholeMinutes = Math.round(decimalMinutes);
if (wholeHours > 0 && wholeMinutes > 0) {
return `${wholeHours}h ${wholeMinutes}m`;
} else if (wholeHours > 0) {
return `${wholeHours}h`;
} else {
return `${wholeMinutes}m`;
}
}
// function to close open work tracks and create logout
async function closePreviousSession(vaId, lastLog) {
try {
const currentTime = new Date();
// Find all open work tracks for this VA and project (where to is not set or totalTime is 0)
const openWorkTracks = await WorkTrackModel.find({
vaId,
logId: lastLog._id,
$or: [
{ to: { $exists: false } },
{ totalTime: 0 },
{ to: null }
]
});
// console.log(`Found ${openWorkTracks.length} open work tracks to close`);
// Close all open work tracks
for (const workTrack of openWorkTracks) {
const fromTime = new Date(workTrack.from);
const totalTimeMs = currentTime - fromTime;
// Update the work track with end time and total time
workTrack.to = currentTime;
workTrack.totalTime = totalTimeMs;
await workTrack.save();
// console.log(`Closed work track ${workTrack._id} with duration: ${totalTimeMs}ms`);
}
// Create a logout log entry
const logoutLog = await LoggedModel.create({
loggedType: "LOGOUT",
vaId,
vaEmail: lastLog.vaEmail, // Use email from previous log
projectId: lastLog.projectId,
time: currentTime // Use same timestamp for consistent logging
});
// console.log(`Created logout log: ${logoutLog._id}`);
try {
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
const sfDataSync = {
"_id": logoutLog._id,
"loggedType": "LOGOUT",
"vaId": vaId,
"vaEmail": lastLog.vaEmail,
"projectId": lastLog.projectId,
"time": logoutLog.time,
}
const response = await fetch(salesforceUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// Add any required headers for Salesforce authentication
// 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
},
body: JSON.stringify(sfDataSync)
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
}
const result = await response.json();
// console.log("✅ Successfully synced VA data to Salesforce:", result);
return {
closedWorkTracks: openWorkTracks.length,
logoutLogId: logoutLog._id,
SyncSalesforce: result
};
} catch (error) {
console.error("Failed to sync with Salesforce:", error.message);
console.log(error)
// Create a custom error to distinguish Salesforce errors
const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
salesforceError.isSalesforceError = true;
throw salesforceError;
}
} catch (error) {
console.error("Error closing previous session:", error);
throw error;
}
}
// Getting logged hours in a specific project of VA // Getting logged hours in a specific project of VA
async function va_project_log_callback(req, res) { async function va_project_log_callback(req, res) {
@ -7,9 +123,114 @@ async function va_project_log_callback(req, res) {
const { vaId, vaEmail, projectId, type } = payload; const { vaId, vaEmail, projectId, type } = payload;
if (!vaId, !vaEmail, !projectId, !type) return res.status(400).send("Missing Props"); if (!vaId, !vaEmail, !projectId, !type) return res.status(400).send("Missing Props");
const project = await ProjectModel.findById(projectId);
if (!project) {
return res.status(400).json({ success: false, message: "Project not found" });
}
// For LOGIN requests, check daily hours
if (type === "LOGIN") {
const { start, end } = getDayRange();
// Get today's total hours
const todayWorkEntries = await WorkTrackModel.find({
vaId,
projectId,
from: { $gte: start, $lte: end }
});
const totalTodayMs = todayWorkEntries.reduce((total, entry) => {
return total + (entry.totalTime || 0);
}, 0);
const totalTodayHours = totalTodayMs / (1000 * 60 * 60);
// Check if daily hours are already completed
if (project.dedicatedHours > 0 && totalTodayHours >= project.dedicatedHours) {
return res.status(400).json({
success: false,
message: `Cannot login - daily hours already completed (${formatHoursToReadable(totalTodayHours)} / ${formatHoursToReadable(project.dedicatedHours)})`
});
}
// Check last log entry
const lastLog = await LoggedModel.findOne({ vaId })
.sort({ time: -1 })
.lean();
// Prevent login if last action was login (not logout)
if (lastLog && lastLog.loggedType === "LOGIN") {
// check last workModel login time and make a logout request then create login
// console.log(`User already logged in. Closing previous session...`);
const closeResult = await closePreviousSession(vaId, lastLog);
// console.log(`Previous session closed: ${closeResult.closedWorkTracks} work tracks, logout log: ${closeResult.logoutLogId}`);
// Now proceed to create new login
const createdLog = await LoggedModel.create({
loggedType: "LOGIN",
vaId,
vaEmail,
projectId
});
return res.status(201).send({
success: true,
message: `Login Successful (previous session closed)`,
logId: createdLog._id,
previousSession: {
closedWorkTracks: closeResult.closedWorkTracks,
logoutLogId: closeResult.logoutLogId
}
});
}
}
const createdLog = await LoggedModel.create({ loggedType: type, vaId, vaEmail, projectId }) const createdLog = await LoggedModel.create({ loggedType: type, vaId, vaEmail, projectId })
// console.log(createdLog)
// try {
// const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
// const sfData = {
// "_id": createdLog._id,
// "loggedType": type,
// "vaId": vaId,
// "vaEmail": vaEmail,
// "projectId": projectId,
// "time": createdLog.time,
// }
// const response = await fetch(salesforceUrl, {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json',
// // Add any required headers for Salesforce authentication
// // 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
// },
// body: JSON.stringify(sfData)
// });
// if (!response.ok) {
// const errorText = await response.text();
// throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
// }
// const result = await response.json();
// // console.log("✅ Successfully synced VA data to Salesforce:", result);
// res.status(201).json({ success: true, message: `${type} Successfully`, logId: createdLog._id, SyncSalesforce: result });
// } catch (error) {
// console.error("Failed to sync with Salesforce:", error.message);
// console.log(error)
// // Create a custom error to distinguish Salesforce errors
// const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
// salesforceError.isSalesforceError = true;
// throw salesforceError;
// }
return res.status(201).send({ success: true, message: `${type} Successfully`, logId: createdLog._id }); return res.status(201).send({ success: true, message: `${type} Successfully`, logId: createdLog._id });
} catch (error) { } catch (error) {
return res.status(500).send("Internal Server Error"); return res.status(500).send("Internal Server Error");
} }

View File

@ -9,6 +9,21 @@ function formatHMS(ms) {
return `${h}h ${m}m ${s}s`; return `${h}h ${m}m ${s}s`;
} }
function formatHoursToReadable(hours) {
const wholeHours = Math.floor(hours);
const decimalMinutes = (hours - wholeHours) * 60;
const wholeMinutes = Math.round(decimalMinutes);
if (wholeHours > 0 && wholeMinutes > 0) {
return `${wholeHours}h ${wholeMinutes}m`;
} else if (wholeHours > 0) {
return `${wholeHours}h`;
} else {
return `${wholeMinutes}m`;
}
}
async function va_projects_log_list_function(req, res) { async function va_projects_log_list_function(req, res) {
try { try {
const { projectId, from, to } = req.params; const { projectId, from, to } = req.params;
@ -101,7 +116,9 @@ async function va_projects_log_list_function(req, res) {
activityStatus, activityStatus,
totalWorkTime: formatHMS(totalWorkMs), totalWorkTime: formatHMS(totalWorkMs),
totalWorkMs, totalWorkMs,
totalLogs: logs.length dedicatedHours: formatHoursToReadable(project?.dedicatedHours),
totalLogs: logs.length,
status: project?.status
}); });
} }

View File

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

View File

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

View File

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

View File

@ -9,13 +9,16 @@ import { client_refresh_token_callback } from "../../controllers/client/clientRe
import { client_request_password_reset } from "../../controllers/passwordReset.controllers.js"; import { client_request_password_reset } from "../../controllers/passwordReset.controllers.js";
import { clientAuthValidator } from "../../middlewares/auth.middleware.js"; import { clientAuthValidator } from "../../middlewares/auth.middleware.js";
import { client_dashboard } from "./../../controllers/client/dashboard/clientDashboard.controller.js"; import { client_dashboard } from "./../../controllers/client/dashboard/clientDashboard.controller.js";
import { client_dashboard_va_log } from "../../controllers/client/dashboard/clientDashboardVaLog.controller.js" import { client_dashboard_va_log } from "../../controllers/client/dashboard/clientDashboardVaLog.controller.js";
import { client_verify_email } from "../../controllers/client/vaClientEmailVerify.controller.js";
import { client_dashboard_projects } from "../../controllers/client/dashboard/clientDashboardProjects.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
clientRoutes.post("/login", va_login_client); // Login Client clientRoutes.post("/login", va_login_client); // Login Client
clientRoutes.get("/refreshToken", client_refresh_token_callback); // Client Refresh Token clientRoutes.get("/refreshToken", client_refresh_token_callback); // Client Refresh Token
clientRoutes.post("/forgot-password", client_request_password_reset); clientRoutes.post("/forgot-password", client_request_password_reset);
clientRoutes.post("/verify-client-email", client_verify_email);
// 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)
@ -24,7 +27,8 @@ clientRoutes.get("/:clientId/projects/:projectId", va_clients_projects_callback)
// 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 Clients Projects Callback clientRoutes.post("/dashboard/va-report", clientAuthValidator, client_dashboard_va_log); // VA details
clientRoutes.get("/dashboard-projects", clientAuthValidator, client_dashboard_projects); // VA Clients Projects Callback

View File

@ -22,6 +22,10 @@ import { va_timesheet_summary_function } from "../../controllers/va/vaTimesheetA
import { va_timesheet_adjustment_function } from "../../controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustment.controller.js"; import { va_timesheet_adjustment_function } from "../../controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustment.controller.js";
import { get_adjustment_requests_list } from "../../controllers/va/vaTimesheetAdjustment/vaAllTimesheetAdjustmentReuqest.controller.js" import { get_adjustment_requests_list } from "../../controllers/va/vaTimesheetAdjustment/vaAllTimesheetAdjustmentReuqest.controller.js"
import { va_timesheet_manage_adjustment_request } from "../../controllers/va/vaTimesheetAdjustment/vaManageTimesheetAdjustment.controller.js" import { va_timesheet_manage_adjustment_request } from "../../controllers/va/vaTimesheetAdjustment/vaManageTimesheetAdjustment.controller.js"
import { va_verify_email } from "../../controllers/va/vaAuth/vaVerifyEmail.controller.js";
import { va_leave_apply_function } from "../../controllers/va/vaLeaves/vaLeavesApply.controller.js";
import { va_leave_list_function } from "../../controllers/va/vaLeaves/vaLeaveList.controller.js"
import { va_leave_manage_function } from "../../controllers/va/vaLeaves/vaLeaveManage.controller.js";
vaRoutes.get("/", va_callback); // VA Callback for getting active VA vaRoutes.get("/", va_callback); // VA Callback for getting active VA
@ -30,6 +34,7 @@ vaRoutes.get("/logout", authValidator, va_logout_callback); // VA Callback for l
vaRoutes.post("/refreshToken", va_refresh_token_callback); // VA Callback for refresh token vaRoutes.post("/refreshToken", va_refresh_token_callback); // VA Callback for refresh token
vaRoutes.post("/register", va_register); // Register a new VA vaRoutes.post("/register", va_register); // Register a new VA
vaRoutes.post("/forgot-password", va_request_password_reset); vaRoutes.post("/forgot-password", va_request_password_reset);
vaRoutes.post("/verify-va-email", va_verify_email);
vaRoutes.get("/profile", authValidator, va_profile_callback); // Get VA Profile vaRoutes.get("/profile", authValidator, va_profile_callback); // Get VA Profile
vaRoutes.patch("/profile", authValidator, va_profile_update_callback); // Update VA Profile vaRoutes.patch("/profile", authValidator, va_profile_update_callback); // Update VA Profile
@ -41,9 +46,17 @@ vaRoutes.get("/:userName", va_username_callback); // VA Username Callback
// timesheet adjustment routes // timesheet adjustment routes
vaRoutes.get("/timesheet/summary/:projectId/:date", authValidator, va_timesheet_summary_function); // Get Timesheet Adjustment for VA on specific project and date vaRoutes.get("/timesheet/summary/:projectId/:date", authValidator, va_timesheet_summary_function); // Get Timesheet Adjustment for VA on specific project and date
vaRoutes.post("/timesheet/adjustment/request", authValidator, va_timesheet_adjustment_function); // Timesheet Adjustment for VA vaRoutes.post("/timesheet/adjustment/request", authValidator, va_timesheet_adjustment_function); // Timesheet Adjustment for VA
vaRoutes.get("/timesheet/adjustments/list/:status?/:projectId?/:vaId?", get_adjustment_requests_list); // show all adjustment list to approve and reject // vaRoutes.get("/timesheet/adjustments/list/:status?/:projectId?/:vaId?", get_adjustment_requests_list); // show all adjustment list to approve and reject
vaRoutes.patch("/timesheet/adjustments/:adjustmentId", va_timesheet_manage_adjustment_request); vaRoutes.patch("/timesheet/adjustments/:adjustmentId", va_timesheet_manage_adjustment_request);
// Leaves routes
vaRoutes.get("/leaves/all", authValidator, va_leave_list_function); // Get all leaves
vaRoutes.get("/leaves/status/:status", authValidator, va_leave_list_function); // Leaves Filter by status
vaRoutes.get("/leaves/type/:leaveType", authValidator, va_leave_list_function); // Filter by leave type
vaRoutes.get("/leaves/status/:status/type/:leaveType", authValidator, va_leave_list_function); // leaves Filter
vaRoutes.post("/leave/request", authValidator, va_leave_apply_function); // Timesheet Adjustment for VA
vaRoutes.patch("/leave/:leaveId", va_leave_manage_function);
// timesheet adjustment routes // timesheet adjustment routes
// Example: GET /va/search?name=john&email=@example.com&from=2025-01-01&to=2025-01-31 (NC) // Example: GET /va/search?name=john&email=@example.com&from=2025-01-01&to=2025-01-31 (NC)

View File

@ -13,16 +13,32 @@ import cors from 'cors';
import path from 'path'; import path from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import apiRoutes from './routes/index.js' import apiRoutes from './routes/index.js'
import { render_reset_form } from "./controllers/passwordReset.controllers.js"; import { render_reset_form } from "./controllers/passwordReset.controllers.js";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
const app = express(); const app = express();
const server = new createServer(app); const server = new createServer(app);
const io = new Server(server); const io = new Server(server);
app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
console.log("⚠️ Certificate error:", url);
// Allow for ngrok only
if (url.includes("ngrok")) {
event.preventDefault();
callback(true); // approve the certificate
} else {
callback(false); // reject others
}
});
app.on('certificate-error', (event, webContents, url, error, certificate, callback) => { app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
console.log("⚠️ Certificate error:", url); console.log("⚠️ Certificate error:", url);
@ -70,7 +86,6 @@ app.get("/download-file", (req, res) => {
} }
}); });
app.get('/varegister/:sf_token?', (req, res) => { app.get('/varegister/:sf_token?', (req, res) => {
const { sf_token } = req.params; const { sf_token } = req.params;
@ -94,10 +109,18 @@ app.get('/varegister/:sf_token?', (req, res) => {
}); });
app.get('/clientregister/:sf_token?', (req, res) => { app.get('/clientregister/:sf_token?', (req, res) => {
const { sf_token } = req.params;
try {
const decoded = jwt.verify(sf_token, process.env.JWT_SECRET_SF_TOKEN);
// const { sf_token } = useParams(); if (decoded) {
res.render('va_client_register', { title: "Client Registration" });
res.render('va_client_register', { title: "Client Registration" }); } else {
res.render('not_found', { title: "Client Registration" });
}
} catch (error) {
res.render('not_found', { title: "Client Registration" });
}
}); });
app.get('/auth/password/reset-form', render_reset_form); app.get('/auth/password/reset-form', render_reset_form);
@ -136,35 +159,38 @@ app.get('/client/virtual-assistants', async (req, res) => {
} }
}); });
// app.get('/client/timesheets', async (req, res) => { app.get('/client/projects', async (req, res) => {
// try { try {
// const timesheetData = await timesheetService.getClientTimesheets(req.user.id); // const vaData = await virtualAssistantService.getClientVAs(req.user.id);
// res.render('client/dashboard-timesheets', timesheetData); res.render('client/dashboard-projects');
// } catch (error) { } catch (error) {
// console.error('Timesheets error:', error); console.error('Projects error:', error);
// res.status(500).render('client/dashboard-timesheets', { error: 'Failed to load timesheets' }); res.status(500).render('client/dashboard-projects', { error: 'Failed to load virtual assistants' });
// } }
// }); });
// app.get('/client/invoices', async (req, res) => { app.get("/live/:projectId/:vaId", async (req, res) => {
// try { const { projectId, vaId } = req.params;
// const invoiceData = await invoiceService.getClientInvoices(req.user.id);
// res.render('client/dashboard-invoices', invoiceData);
// } catch (error) {
// console.error('Invoices error:', error);
// res.status(500).render('client/dashboard-invoices', { error: 'Failed to load invoices' });
// }
// });
// app.get('/client/referrals', async (req, res) => { if (!projectId || !vaId) {
// try { return res.status(400).send({ "message": "Page not found" });
// const referralData = await referralService.getClientReferrals(req.user.id); }
// res.render('client/dashboard-referrals', referralData);
// } catch (error) { let clientId = null;
// console.error('Referrals error:', error); try {
// res.status(500).render('client/dashboard-referrals', { error: 'Failed to load referrals' }); const clientRes = await ProjectModel.findById(projectId);
// } if (clientRes) {
// }); clientId = clientRes.clientId.toString();
}
} catch (err) {
console.error("Error fetching client for project:", err.message);
return;
}
if (!clientId) return res.status(400).send({ "message": "Page not found" });
res.render("va_live_tracker", { title: "VA Live Tracker", projectId, vaId, clientId })
})
app.get("/live/:projectId/:vaId", async (req, res) => { app.get("/live/:projectId/:vaId", async (req, res) => {
const { projectId, vaId } = req.params; const { projectId, vaId } = req.params;
@ -191,241 +217,17 @@ app.get("/live/:projectId/:vaId", async (req, res) => {
// Handling Socket // Handling Socket
import { VAModel, ProjectModel, ClientModel } from './models/vaModel.js'; import { VAModel, ProjectModel, ClientModel } from './models/vaModel.js';
import { ObjectId } from 'mongodb/lib/bson.js';
const vaSocketRespectToClient = new Map(); const vaSocketRespectToClient = new Map();
io.use(async (socket, next) => {
try {
const token = socket.handshake.auth?.token;
const projectId = socket.handshake.auth?.projectId;
const userType = socket.handshake.auth?.userType;
console.log("🔐 Received token:", token);
if (!token) {
return next(new Error("Not Authorized: Missing Token"));
}
const decoded = jwt.verify(token, process.env.JWT_SECRET_ACCESS_TOKEN);
if (!decoded?.email) {
return next(new Error("Invalid token payload"));
}
/* ---------------------- 1⃣ Check if VA ---------------------- */
let vaUser = null;
let clientData = null;
if (userType === "va") {
vaUser = await VAModel.findOne({ "userDetails.email": decoded.email });
clientData = await ProjectModel.findById(projectId).populate("clientId");
} else if (userType === "client") {
clientData = await ProjectModel.findById(projectId).populate("clientId");
}
if (clientData == null) return next(new Error("Invalid Project ID"));
const vaProjClientId = clientData.clientId._id;
if (vaUser && vaUser.accessToken === token) {
socket.data.role = "va";
socket.data.vaId = vaUser._id.toString();
socket.data.vaEmail = decoded.email;
const a = vaProjClientId.toString();
const b = clientData.clientId._id.toString();
const c = JSON.stringify(clientData);
console.log(a, b, c, "vaProjClientId vs clientData.clientId");
vaSocketRespectToClient.set(vaProjClientId.toString(), socket.id)
console.log("✅ Authenticated VA:", vaUser.userDetails.email);
return next();
}
/* ---------------------- 2⃣ Check if Client ---------------------- */
// const clientUser = await ClientModel.findOne({ "userDetails.email": decoded.email });
// if (clientUser) {
// socket.data.role = "client";
// socket.data.clientId = clientUser._id.toString();
// // Get all VA IDs assigned to this client
// socket.data.allowedVAs = clientUser.vaIds.map((va) => va.toString());
// console.log("✅ Authenticated Client:", clientUser.userDetails.email);
// return next();
// }
const clientUser = clientData.clientId;
if (clientUser) {
socket.data.role = "client";
socket.data.clientId = clientUser._id.toString();
// Get all VA IDs assigned to this client
socket.data.allowedVAs = clientData.vaAssigned.map((va) => va.toString());
// console.log("✅ Authenticated Client:", clientUser.userDetails.email);
return next();
}
return next(new Error("Not Authorized: Invalid User"));
} catch (error) {
console.error("❌ Socket Auth Error:", error.message);
next(new Error("Authentication failed: " + error.message));
}
});
io.on("connection", (socket) => {
console.log("User connected:", socket.id);
// Register role and ID (client or VA)
socket.on("register", async ({ role, id, projectId = null, clientId = null }) => {
if (role === "va") {
socket.data.role = "va";
socket.data.vaId = id;
console.log(`VA registered: ${id}`);
}
if (role === "client") {
socket.data.role = "client";
socket.data.vaId = id;
socket.data.projectId = projectId;
socket.data.clientId = null
if (!projectId || !clientId) {
console.log("projectId is missing in register payload");
return;
}
socket.data.clientId = clientId;
// 🧩 Get allowed VAs for this client
const client = await ClientModel.findById(clientId)
.populate("projectIds") // to access their projects
.populate("vaIds"); // optional if you track directly
// Collect all VA IDs from their projects
const projectVAIds = new Set();
for (const project of client.projectIds) {
const populated = await ProjectModel.findById(project._id).populate("vaAssigned");
populated.vaAssigned.forEach((va) => projectVAIds.add(va._id.toString()));
}
// Store allowed VA IDs in socket memory
socket.data.allowedVAs = Array.from(projectVAIds);
console.log(`Client ${id} can view VAs:`, socket.data.allowedVAs);
}
});
// When VA sends live image
socket.on("sendImage", async ({ token, image, title }) => {
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();
console.log("🟢 Received image from VA:", vaId);
// Broadcast image only to allowed clients
for (let [_, clientSocket] of io.sockets.sockets) {
if (
clientSocket.data.role === "client" &&
clientSocket.data.allowedVAs?.includes(vaId)
) {
clientSocket.emit("receiveImage", image, title);
}
}
} catch (err) {
console.error("❌ Token verification failed:", err.message);
}
});
socket.on("selectApp", ({ appTitle, clientId }) => {
console.log(`VA selected app: ${appTitle}`);
const sssrrrr = vaSocketRespectToClient.get(clientId);
console.log(vaSocketRespectToClient.get(clientId), sssrrrr, "vaSocketRespectToClient[clientId]")
// You can forward this info to the respective Electron client
socket.to(vaSocketRespectToClient.get(clientId)).emit("appToCapture", { appTitle });
});
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();
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("receiveVisibleApps", apps);
}
}
} catch (err) {
console.error("❌ Token verification failed:", err.message);
}
});
socket.on('disconnect', () => {
console.log('a user Disconnected');
});
});
// io.on('connection', (socket) => {
// socket.on("sendImage", (base64String) => {
// io.emit("receiveImage", base64String);
// });
// socket.on('disconnect', () => {
// console.log('a user Disconnected');
// });
// });
app.get("/client/login", (req, res) => {
res.render("client_login")
})
app.get("/client/forgot-password", (req, res) => {
res.render("forgot_password")
})
// Handling Socket // Handling Socket
io.use(async (socket, next) => { io.use(async (socket, next) => {
try { try {
const token = socket.handshake.auth?.token; const token = socket.handshake.auth?.token;
const projectId = socket.handshake.auth?.projectId; const projectId = socket.handshake.auth?.projectId;
const userType = socket.handshake.auth?.userType; const userType = socket.handshake.auth?.userType;
const vaId = socket.handshake.auth?.vaId;
console.log("🔐 Received token:", token); console.log("🔐 Received token:", token);
@ -464,7 +266,7 @@ io.use(async (socket, next) => {
console.log(a, b, c, "vaProjClientId vs clientData.clientId"); console.log(a, b, c, "vaProjClientId vs clientData.clientId");
vaSocketRespectToClient.set(`${vaProjClientId.toString()}-${vaUser._id.toString()}`, socket.id) vaSocketRespectToClient.set(`${vaProjClientId.toString()}-${vaUser._id.toString()}-${projectId.toString()}-va`, socket.id)
console.log("✅ Authenticated VA:", vaUser.userDetails.email); console.log("✅ Authenticated VA:", vaUser.userDetails.email);
return next(); return next();
@ -491,6 +293,7 @@ io.use(async (socket, next) => {
// Get all VA IDs assigned to this client // Get all VA IDs assigned to this client
socket.data.allowedVAs = clientData.vaAssigned.map((va) => va.toString()); socket.data.allowedVAs = clientData.vaAssigned.map((va) => va.toString());
vaSocketRespectToClient.set(`${vaProjClientId.toString()}-${vaId.toString()}-${projectId.toString()}-client`, socket.id)
// console.log("✅ Authenticated Client:", clientUser.userDetails.email); // console.log("✅ Authenticated Client:", clientUser.userDetails.email);
return next(); return next();
@ -503,8 +306,6 @@ io.use(async (socket, next) => {
} }
}); });
io.on("connection", (socket) => { io.on("connection", (socket) => {
console.log("User connected:", socket.id); console.log("User connected:", socket.id);
@ -539,7 +340,7 @@ io.on("connection", (socket) => {
}); });
// When VA sends live image // When VA sends live image
socket.on("sendImage", async ({ token, image, title }) => { socket.on("sendImage", async ({ token, image, title, projectId }) => {
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);
@ -552,30 +353,42 @@ io.on("connection", (socket) => {
return; return;
} }
const project = await ProjectModel.findById(projectId);
if (!project) {
console.log("❌ Invalid Project ID");
return;
}
const clientId = project.clientId.toString();
const vaId = vaUser._id.toString(); const vaId = vaUser._id.toString();
const clientVaUniqueId = `${clientId}-${vaId}-${projectId}-client`;
const socketId = vaSocketRespectToClient.get(clientVaUniqueId);
console.log("🟢 Received image from VA:", vaId); console.log("🟢 Received image from VA:", vaId);
// Broadcast image only to allowed clients // Broadcast image only to allowed clients
for (let [_, clientSocket] of io.sockets.sockets) { // for (let [_, clientSocket] of io.sockets.sockets) {
if ( // if (
clientSocket.data.role === "client" && // clientSocket.data.role === "client" &&
clientSocket.data.allowedVAs?.includes(vaId) // clientSocket.data.allowedVAs?.includes(vaId)
) { // ) {
clientSocket.emit("receiveImage", image, title); // clientSocket.emit("receiveImage", image, title);
} // }
} // }
// Can be like clientId-projectId-vaId to uniquely identify
socket.to(socketId).emit("receiveImage", image, title);
} catch (err) { } catch (err) {
console.error("❌ Token verification failed:", err.message); console.error("❌ Token verification failed:", err.message);
} }
}); });
socket.on("selectApp", ({ appTitle, clientId }) => { socket.on("selectApp", ({ appTitle, clientId, vaId, projectId }) => {
console.log(`VA selected app: ${appTitle}`); console.log(`VA selected app: ${appTitle}`);
const sssrrrr = vaSocketRespectToClient.get(`${clientId}-${socket.data.vaId}`); console.log(vaSocketRespectToClient.get(`${clientId}-${vaId}-${projectId}-va`), "vaSocketRespectToClient[clientId]")
console.log(vaSocketRespectToClient.get(`${clientId}-${socket.data.vaId}`), sssrrrr, "vaSocketRespectToClient[clientId]")
// You can forward this info to the respective Electron client // You can forward this info to the respective Electron client
socket.to(vaSocketRespectToClient.get(`${clientId}-${socket.data.vaId}`)).emit("appToCapture", { appTitle }); socket.to(vaSocketRespectToClient.get(`${clientId}-${vaId}-${projectId}-va`)).emit("appToCapture", { appTitle });
}); });
socket.on("getVisibleApps", async ({ token, apps }) => { socket.on("getVisibleApps", async ({ token, apps }) => {
@ -600,7 +413,7 @@ io.on("connection", (socket) => {
clientSocket.data.role === "client" && clientSocket.data.role === "client" &&
clientSocket.data.allowedVAs?.includes(vaId) clientSocket.data.allowedVAs?.includes(vaId)
) { ) {
clientSocket.emit("receiveVisibleApps", apps); clientSocket.emit("receiveVisibleApps", { apps, vaId });
} }
} }
} catch (err) { } catch (err) {
@ -614,42 +427,10 @@ io.on("connection", (socket) => {
}); });
}); });
// io.on('connection', (socket) => {
// socket.on("sendImage", (base64String) => {
// io.emit("receiveImage", base64String);
// });
// socket.on('disconnect', () => {
// console.log('a user Disconnected');
// });
// });
// Express App Listening
// app.listen(PORT, () => {
// mongoose
// .connect(
// process.env.MONGO_URI,
// )
// .then(() => console.log("mongodb connected"))
// .catch((err) => {
// console.log(err.message);
// process.exit(1);
// });
// console.log(`Express started on http://localhost:${PORT}`);
// });
app.use((req, res) => { app.use((req, res) => {
res.status(404).render("not_found", { res.status(404).render("not_found", {
title: "404 - Page Not Found" title: "404 - Page Not Found"
}); });
}); });
server.listen(PORT, () => { server.listen(PORT, () => {

View File

@ -19,8 +19,8 @@ class PasswordResetService {
if (!user) { if (!user) {
logger.info(`Password reset requested for non-existent ${userType}: ${email}`); logger.info(`Password reset requested for non-existent ${userType}: ${email}`);
return { return {
success: true, success: false,
message: "If the email exists, a reset link has been sent" message: "Email not recognized. Please sign up to continue."
}; };
} }

View File

@ -43,8 +43,8 @@ class SalesforceService {
const { userDetails } = clientData; const { userDetails } = clientData;
return { return {
_id: _id,
userDetails: { userDetails: {
_id: _id,
name: userDetails.name, name: userDetails.name,
email: userDetails.email.toLowerCase(), email: userDetails.email.toLowerCase(),
address: userDetails.address || null, address: userDetails.address || null,

View File

@ -17,11 +17,13 @@ class VaClientService {
// Sanitize and prepare data // Sanitize and prepare data
const clientData = this.sanitizeClientData(data); const clientData = this.sanitizeClientData(data);
// Send data to Salesforce first
await this.salesforceService.registerClientInSalesforce(data, clientData._id);
const response = await this.clientRepository.create(clientData); const response = await this.clientRepository.create(clientData);
if(response) {
await this.salesforceService.registerClientInSalesforce(data, response._id);
}
// Send data to Salesforce first
return response; return response;
} catch (error) { } catch (error) {
logger.error("Client Service - registerClient error: " + error.message); logger.error("Client Service - registerClient error: " + error.message);

View File

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

View File

@ -0,0 +1,378 @@
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DocVA Client | Dashboard</title>
<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">
</head>
<body class="dashboard-body">
<div id="root">
<nav class="dashboard-header">
<div class="header-content">
<div class="header-logo">
<div class="logo-container">
<h2 class="sidebar-title">DocVA Client Portal</h2>
</div>
</div>
<div class="header-nav">
<ul class="nav-list">
<li class="nav-item">
<a href="/client/dashboard" class="nav-button">
<i class="fas fa-chart-bar"></i>Dashboard
</a>
</li>
<li class="nav-item">
<a href="/client/virtual-assistants" class="nav-button">
<i class="fas fa-users"></i>Virtual Assistants
</a>
</li>
<li class="nav-item">
<a href="/client/projects" class="nav-button active">
<i class="fas fa-file-invoice"></i>Projects
</a>
</li>
<!-- <li class="nav-item">
<a href="/client/timesheets" class="nav-button">
<i class="fas fa-clock"></i>Timesheets
</a>
</li>
<li class="nav-item">
<a href="/client/referrals" class="nav-button">
<i class="fas fa-share-alt"></i>Referrals
</a>
</li> -->
</ul>
<div class="user-avatar">TT</div>
<button class="mobile-menu-button">
<i class="fas fa-bars"></i>
</button>
</div>
</div>
</nav>
<div class="dashboard-container">
<aside class="dashboard-sidebar">
<!-- <div class="sidebar-header">
<h2 class="sidebar-title">DocVA Client Portal</h2>
</div> -->
<nav class="sidebar-nav">
<ul class="sidebar-list">
<li class="sidebar-item">
<a href="/client/dashboard" class="sidebar-link">
<i class="fas fa-chart-bar sidebar-icon"></i>
<span>Dashboard</span>
</a>
</li>
<li class="sidebar-item">
<a href="/client/virtual-assistants" class="sidebar-link">
<i class="fas fa-users sidebar-icon"></i>
<span>Virtual Assistants</span>
</a>
</li>
<li class="sidebar-item">
<a href="/client/projects" class="sidebar-link active">
<i class="fas fa-file-invoice sidebar-icon"></i>
<span>Projects</span>
</a>
</li>
<!-- <li class="sidebar-item">
<a href="/client/invoices" class="sidebar-link">
<i class="fas fa-file-invoice sidebar-icon"></i>
<span>Invoices</span>
</a>
</li>
<li class="sidebar-item">
<a href="/client/referrals" class="sidebar-link">
<i class="fas fa-share-alt sidebar-icon"></i>
<span>Referrals</span>
</a>
</li> -->
</ul>
</nav>
</aside>
<main class="dashboard-main">
<div class="dashboard-content">
<!-- Projects List -->
<div class="content-section">
<div class="section-header">
<h2 class="section-title">Projects <span class="total-count" id="projectsCount">0 Projects</span></h2>
</div>
<div class="table-container">
<div class="table-wrapper">
<table class="data-table">
<thead>
<tr>
<th>Name</th>
<th>Hourly Rate</th>
<th>Total VA Assigned</th>
<th>Dedicated Hours</th>
<th>Status</th>
</tr>
</thead>
<tbody id="projectsTableBody">
<!-- Data will be loaded here -->
</tbody>
</table>
</div>
<div id="projectsEmptyState" style="display: none;">
<div class="empty-state">
<i class="fas fa-briefcase"></i>
<h3>No Projects</h3>
<p>You don't have any projects yet.</p>
</div>
</div>
</div>
</div>
<br><br>
<!-- Virtual Assistants List -->
<div class="content-section">
<div class="section-header">
<h2 class="section-title">Virtual Assistants <span class="total-count" id="vasCount">0 VAs</span></h2>
</div>
<div class="table-container">
<div class="table-wrapper">
<table class="data-table">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Status</th>
</tr>
</thead>
<tbody id="vaTableBody">
<!-- Data will be loaded here -->
</tbody>
</table>
</div>
<div id="vaEmptyState" style="display: none;">
<div class="empty-state">
<i class="fas fa-users"></i>
<h3>No Virtual Assistants</h3>
<p>You don't have any virtual assistants assigned yet.</p>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
</div>
<script>
// Theme management
function setTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
}
function getSystemTheme() {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
// Initialize theme
const savedTheme = localStorage.getItem('theme') || 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;
}
const response = await fetch(url, {
...options,
headers: {
'token': `${token}`,
'Content-Type': 'application/json',
...options.headers,
}
});
if (response.status === 401) {
localStorage.removeItem('knoptpq');
localStorage.removeItem('refreshToken');
window.location.href = '/client/login';
return;
}
return response;
}
// Create Project table row HTML
function createProjectTableRow(project) {
const statusClass = project.status === 'active' ? 'active' : 'inactive';
const statusText = project.status === 'active' ? 'Active' : 'Inactive';
const hourlyRate = project.hourlyRate ? `$${project.hourlyRate}/hr` : '-';
const dedicatedHours = project.dedicatedHours ? `${project.dedicatedHours}` : '-';
return `
<tr>
<td>
<div class="project-info">
<div class="project-name">${project.name || 'Unnamed Project'}</div>
</div>
</td>
<td>
<span class="hourly-rate">${hourlyRate}</span>
</td>
<td>
<span class="va-count">${project.vaAssigned || 0}</span>
</td>
<td>
<span class="dedicated-hours">${dedicatedHours}</span>
</td>
<td>
<div class="status-indicator">
<div class="status-dot ${statusClass}"></div>
<span class="status-text ${statusClass}">${statusText}</span>
</div>
</td>
</tr>
`;
}
// Create VA table row HTML
function createVATableRow(va) {
const statusClass = va.status === 'active' ? 'active' : 'inactive';
const statusText = va.status === 'active' ? 'Active' : 'Inactive';
const fullName = `${va.firstName || ''} ${va.lastName || ''}`.trim() || 'Unknown';
return `
<tr>
<td>
<div class="va-info">
<div class="va-name">${fullName}</div>
</div>
</td>
<td>
<span class="va-email">${va.email || 'N/A'}</span>
</td>
<td>
<div class="status-indicator">
<div class="status-dot ${statusClass}"></div>
<span class="status-text ${statusClass}">${statusText}</span>
</div>
</td>
</tr>
`;
}
// Load dashboard data
async function loadDashboardData() {
try {
const response = await fetchWithAuth(`${API_BASE}/dashboard-projects`);
if (!response) return;
const result = await response.json();
if (result.success) {
const data = result.data;
const counts = result.counts;
// Update counts
document.getElementById('projectsCount').textContent = `${counts.totalProjects || 0} Projects`;
document.getElementById('vasCount').textContent = `${counts.totalVAs || 0} VAs`;
// Update Projects table
const projectsTableBody = document.getElementById('projectsTableBody');
const projectsEmptyState = document.getElementById('projectsEmptyState');
const projects = data.projects || [];
if (projects.length > 0) {
projectsTableBody.innerHTML = projects.map(project => createProjectTableRow(project)).join('');
projectsTableBody.style.display = '';
projectsEmptyState.style.display = 'none';
} else {
projectsTableBody.innerHTML = '';
projectsTableBody.style.display = 'none';
projectsEmptyState.style.display = 'block';
}
// Update VA table
const vaTableBody = document.getElementById('vaTableBody');
const vaEmptyState = document.getElementById('vaEmptyState');
const vas = data.vas || [];
if (vas.length > 0) {
vaTableBody.innerHTML = vas.map(va => createVATableRow(va)).join('');
vaTableBody.style.display = '';
vaEmptyState.style.display = 'none';
} else {
vaTableBody.innerHTML = '';
vaTableBody.style.display = 'none';
vaEmptyState.style.display = 'block';
}
} else {
console.error('Failed to load dashboard data:', result.message);
showError('Failed to load dashboard data');
}
} catch (error) {
console.error('Error loading dashboard data:', error);
showError('Error loading dashboard data');
}
}
// Show error message
function showError(message) {
const projectsTableBody = document.getElementById('projectsTableBody');
const projectsEmptyState = document.getElementById('projectsEmptyState');
const vaTableBody = document.getElementById('vaTableBody');
const vaEmptyState = document.getElementById('vaEmptyState');
// Hide tables
projectsTableBody.style.display = 'none';
vaTableBody.style.display = 'none';
// Show error states
projectsEmptyState.innerHTML = `
<div class="empty-state">
<i class="fas fa-exclamation-triangle"></i>
<h3>Error</h3>
<p>${message}</p>
</div>
`;
projectsEmptyState.style.display = 'block';
vaEmptyState.innerHTML = `
<div class="empty-state">
<i class="fas fa-exclamation-triangle"></i>
<h3>Error</h3>
<p>${message}</p>
</div>
`;
vaEmptyState.style.display = 'block';
}
// Load data when page loads
document.addEventListener('DOMContentLoaded', loadDashboardData);
// Optional: Auto-refresh data every 30 seconds
setInterval(loadDashboardData, 30000);
document.addEventListener('DOMContentLoaded', () => {
var menuToggle = document.querySelector('.mobile-menu-button');
var sidebar = document.querySelector('.dashboard-sidebar');
menuToggle.addEventListener('click', () => {
sidebar.classList.toggle('active')
})
})
</script>
</body>
</html>

View File

@ -29,12 +29,12 @@
<i class="fas fa-users"></i>Virtual Assistants <i class="fas fa-users"></i>Virtual Assistants
</a> </a>
</li> </li>
<!-- <li class="nav-item"> <li class="nav-item">
<a href="/client/invoices" class="nav-button"> <a href="/client/projects" class="nav-button">
<i class="fas fa-file-invoice"></i>Invoices <i class="fas fa-file-invoice"></i>Projects
</a> </a>
</li> </li>
<li class="nav-item"> <!-- <li class="nav-item">
<a href="/client/timesheets" class="nav-button"> <a href="/client/timesheets" class="nav-button">
<i class="fas fa-clock"></i>Timesheets <i class="fas fa-clock"></i>Timesheets
</a> </a>
@ -73,13 +73,13 @@
<span>Virtual Assistants</span> <span>Virtual Assistants</span>
</a> </a>
</li> </li>
<!-- <li class="sidebar-item"> <li class="sidebar-item">
<a href="/client/timesheets" class="sidebar-link"> <a href="/client/projects" class="sidebar-link">
<i class="fas fa-clock sidebar-icon"></i> <i class="fas fa-file-invoice sidebar-icon"></i>
<span>Timesheets</span> <span>Projects</span>
</a> </a>
</li> </li>
<li class="sidebar-item"> <!-- <li class="sidebar-item">
<a href="/client/invoices" class="sidebar-link"> <a href="/client/invoices" class="sidebar-link">
<i class="fas fa-file-invoice sidebar-icon"></i> <i class="fas fa-file-invoice sidebar-icon"></i>
<span>Invoices</span> <span>Invoices</span>
@ -148,6 +148,7 @@
<th>Status</th> <th>Status</th>
<th>Virtual Assistant</th> <th>Virtual Assistant</th>
<th>Projects Assigned</th> <th>Projects Assigned</th>
<th>Active Projects</th>
<th>Worked Time</th> <th>Worked Time</th>
<th>Action</th> <th>Action</th>
</tr> </tr>
@ -239,6 +240,7 @@
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 workedTime = formatTime(va.currentSessionDuration); const workedTime = formatTime(va.currentSessionDuration);
const projectsAssigned = va.projectsAssigned || 0; const projectsAssigned = va.projectsAssigned || 0;
const viewLink = va.isCurrentlyActive ? `<a href="/live/${va.lastProjectId}/${va.id}" class="view-link" data-va-id="${va.id}" target="_blank"> const viewLink = va.isCurrentlyActive ? `<a href="/live/${va.lastProjectId}/${va.id}" class="view-link" data-va-id="${va.id}" target="_blank">
@ -260,6 +262,9 @@
<div class="va-email">${va.email || 'N/A'}</div> <div class="va-email">${va.email || 'N/A'}</div>
</div> </div>
</td> </td>
<td>
<span class="projects-count">${activeProject}</span>
</td>
<td> <td>
<span class="projects-count">${projectsAssigned} Projects</span> <span class="projects-count">${projectsAssigned} Projects</span>
</td> </td>
@ -352,6 +357,15 @@
setInterval(loadDashboardData, 30000); setInterval(loadDashboardData, 30000);
document.addEventListener('DOMContentLoaded', () => {
var menuToggle = document.querySelector('.mobile-menu-button');
var sidebar = document.querySelector('.dashboard-sidebar');
menuToggle.addEventListener('click', () => {
sidebar.classList.toggle('active')
})
})
</script> </script>
</body> </body>
</html> </html>

View File

@ -277,10 +277,10 @@
throw new Error(data.message || "Login failed"); throw new Error(data.message || "Login failed");
} }
if (data.accessToken) { if (data.success) {
// 🟢 Save token // 🟢 Save token
localStorage.setItem("knoptpq", data.accessToken); localStorage.setItem("knoptpq", data.accessToken);
localStorage.setItem("refreshToken", data.refreshToken); // localStorage.setItem("refreshToken", data.refreshToken);
// 🟢 Redirect // 🟢 Redirect
window.location.href = "/client/dashboard"; window.location.href = "/client/dashboard";

View File

@ -127,6 +127,11 @@
</div> </div>
<script> <script>
function validateEmail(email) {
const emailRegex = /^[a-zA-Z0-9](?:[a-zA-Z0-9]*[._-]?[a-zA-Z0-9]+)*@[a-zA-Z0-9](?:[a-zA-Z0-9]*[-]?[a-zA-Z0-9]+)*\.[a-zA-Z]{2,}$/;
return emailRegex.test(email);
}
(function () { (function () {
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)"); const prefersDark = window.matchMedia("(prefers-color-scheme: dark)");
const logo = document.getElementById("logo"); const logo = document.getElementById("logo");
@ -154,8 +159,8 @@
e.preventDefault(); e.preventDefault();
const email = emailInput.value.trim(); const email = emailInput.value.trim();
if (!email) { if (!email || !validateEmail(email)) {
alert("Please enter your email address."); alert("Please enter valid email address.");
return; return;
} }
@ -174,8 +179,13 @@
const data = await response.json(); const data = await response.json();
if (response.ok) { if (response.ok) {
if(confirm('Please Check your mail inbox for password reset link')) { if(response.success) {
window.location.href = "/client/login"; // Redirect to reset password page if(confirm('Please Check your mail inbox for password reset link')) {
window.location.href = "/client/login"; // Redirect to reset password page
// console.log(response)
}
} else {
alert(data.message || "Failed to send reset link.");
} }
} else { } else {
alert("❌ " + (data.message || "Failed to send reset link.")); alert("❌ " + (data.message || "Failed to send reset link."));

View File

@ -92,12 +92,18 @@
<!-- Step 1: Create Account --> <!-- Step 1: Create Account -->
<div class="form-step active" id="step-1"> <div class="form-step active" id="step-1">
<h2 class="form-title">Create Your Account</h2> <h2 class="form-title">Create Your Account</h2>
<div class="step-error-container" id="step-1-error" style="display: none;">
<div class="error-message" id="step-1-error-message"></div>
</div>
<form id="account-form"> <form id="account-form">
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
<input type="text" id="company-name" class="input-field" placeholder=" " required> <input type="text" id="company-name" class="input-field" placeholder=" " oninput="validateCompanyName(this)" required>
<label for="company-name" class="input-label">Company / Client Name *</label> <label for="company-name" class="input-label">Company / Client Name *</label>
</div> </div>
<div class="invalid-feedback" id="company-name-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
Company name should only contain letters, numbers, spaces, hyphens, apostrophes, ampersands, and periods
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
@ -107,7 +113,7 @@
</div> </div>
<div class="form-actions"> <div class="form-actions">
<div></div> <!-- Empty div for spacing --> <div></div> <!-- Empty div for spacing -->
<button type="button" class="btn btn-primary" onclick="nextStep(2)">Continue</button> <button type="button" class="btn btn-primary" onclick="handleNextStep(2)">Continue</button>
</div> </div>
</form> </form>
</div> </div>
@ -115,6 +121,9 @@
<!-- Step 2: Business Information --> <!-- Step 2: Business Information -->
<div class="form-step" id="step-2"> <div class="form-step" id="step-2">
<h2 class="form-title">Business Information</h2> <h2 class="form-title">Business Information</h2>
<div class="step-error-container" id="step-2-error" style="display: none;">
<div class="error-message" id="step-2-error-message"></div>
</div>
<form id="business-form"> <form id="business-form">
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
@ -124,9 +133,12 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
<input type="text" id="city" class="input-field" placeholder=" " required> <input type="text" id="city" class="input-field" placeholder=" " oninput="validateCity(this)" onkeypress="return preventInvalidCityChars(event)" required>
<label for="city" class="input-label">City *</label> <label for="city" class="input-label">City *</label>
</div> </div>
<div class="invalid-feedback" id="city-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
City name should contain letters and cannot be only numbers or special characters
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
@ -141,13 +153,16 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
<input type="text" id="zip" class="input-field" placeholder=" " required> <input type="text" id="zip" class="input-field" placeholder=" " oninput="validateZipCode(this)" onkeypress="return preventNonNumericZip(event)" maxlength="10" required>
<label for="zip" class="input-label">ZIP *</label> <label for="zip" class="input-label">ZIP *</label>
</div> </div>
<div class="invalid-feedback" id="zip-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
ZIP code should contain only numbers
</div>
</div> </div>
<div class="form-actions"> <div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="prevStep(1)">Back</button> <button type="button" class="btn btn-secondary" onclick="prevStep(1)">Back</button>
<button type="button" class="btn btn-primary" onclick="nextStep(3)">Continue</button> <button type="button" class="btn btn-primary" onclick="handleNextStep(3)">Continue</button>
</div> </div>
</form> </form>
</div> </div>
@ -155,6 +170,9 @@
<!-- Step 3: Billing Address --> <!-- Step 3: Billing Address -->
<div class="form-step" id="step-3"> <div class="form-step" id="step-3">
<h2 class="form-title">Billing Address</h2> <h2 class="form-title">Billing Address</h2>
<div class="step-error-container" id="step-3-error" style="display: none;">
<div class="error-message" id="step-3-error-message"></div>
</div>
<form id="billing-form"> <form id="billing-form">
<div class="checkbox-group"> <div class="checkbox-group">
<input type="checkbox" id="use-business-address"> <input type="checkbox" id="use-business-address">
@ -174,9 +192,12 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
<input type="text" id="billing-city" class="input-field" placeholder=" " required> <input type="text" id="billing-city" class="input-field" placeholder=" " oninput="validateCity(this)" onkeypress="return preventInvalidCityChars(event)" required>
<label for="billing-city" class="input-label">City *</label> <label for="billing-city" class="input-label">City *</label>
</div> </div>
<div class="invalid-feedback" id="billing-city-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
City name should contain letters and cannot be only numbers or special characters
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
@ -190,9 +211,12 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
<input type="text" id="billing-zip" class="input-field" placeholder=" " required> <input type="text" id="billing-zip" class="input-field" placeholder=" " oninput="validateZipCode(this)" onkeypress="return preventNonNumericZip(event)" maxlength="10" required>
<label for="billing-zip" class="input-label">ZIP *</label> <label for="billing-zip" class="input-label">ZIP *</label>
</div> </div>
<div class="invalid-feedback" id="billing-zip-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
ZIP code should contain only numbers
</div>
</div> </div>
<div class="checkbox-group"> <div class="checkbox-group">
<input type="checkbox" id="use-account-email"> <input type="checkbox" id="use-account-email">
@ -206,7 +230,7 @@
</div> </div>
<div class="form-actions"> <div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="prevStep(2)">Back</button> <button type="button" class="btn btn-secondary" onclick="prevStep(2)">Back</button>
<button type="button" class="btn btn-primary" onclick="nextStep(4)">Continue</button> <button type="button" class="btn btn-primary" onclick="handleNextStep(4)">Continue</button>
</div> </div>
</form> </form>
</div> </div>
@ -214,21 +238,30 @@
<!-- Step 4: Authorized Signatory --> <!-- Step 4: Authorized Signatory -->
<div class="form-step" id="step-4"> <div class="form-step" id="step-4">
<h2 class="form-title">Authorized Signatory</h2> <h2 class="form-title">Authorized Signatory</h2>
<div class="step-error-container" id="step-4-error" style="display: none;">
<div class="error-message" id="step-4-error-message"></div>
</div>
<p style="margin-bottom: 20px; color: #6b7280; font-size: 14px;"> <p style="margin-bottom: 20px; color: #6b7280; font-size: 14px;">
We will use the details below in the agreement that we will send to your prior to entering the project. We will use the details below in the agreement that we will send to your prior to entering the project.
</p> </p>
<form id="signatory-form"> <form id="signatory-form">
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
<input type="text" id="position" class="input-field" placeholder=" " required> <input type="text" id="position" class="input-field" placeholder=" " oninput="validatePosition(this)" onkeypress="return preventInvalidPositionChars(event)" required>
<label for="position" class="input-label">Position (Owner / CEO / Manager / etc.) *</label> <label for="position" class="input-label">Position (Owner / CEO / Manager / etc.) *</label>
</div> </div>
<div class="invalid-feedback" id="position-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
Position should only contain letters, spaces, and common punctuation
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
<input type="text" id="signatory-name" class="input-field" placeholder=" " required> <input type="text" id="signatory-name" class="input-field" placeholder=" " oninput="validateSignatoryName(this)" onkeypress="return preventNumbers(event)" required>
<label for="signatory-name" class="input-label">Name *</label> <label for="signatory-name" class="input-label">Name *</label>
</div> </div>
<div class="invalid-feedback" id="signatory-name-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
Name should not contain numbers
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
@ -238,7 +271,7 @@
</div> </div>
<div class="form-actions"> <div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="prevStep(3)">Back</button> <button type="button" class="btn btn-secondary" onclick="prevStep(3)">Back</button>
<button type="button" class="btn btn-primary" onclick="nextStep(5)">Continue</button> <button type="button" class="btn btn-primary" onclick="handleNextStep(5)">Continue</button>
</div> </div>
</form> </form>
</div> </div>
@ -246,6 +279,9 @@
<!-- Step 5: Payment Details --> <!-- Step 5: Payment Details -->
<div class="form-step" id="step-5"> <div class="form-step" id="step-5">
<h2 class="form-title">Payment Details</h2> <h2 class="form-title">Payment Details</h2>
<div class="step-error-container" id="step-5-error" style="display: none;">
<div class="error-message" id="step-5-error-message"></div>
</div>
<form id="payment-form"> <form id="payment-form">
<div class="payment-option" onclick="selectPaymentOption('card')"> <div class="payment-option" onclick="selectPaymentOption('card')">
<h4>Credit or Debit Card</h4> <h4>Credit or Debit Card</h4>
@ -267,16 +303,22 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
<input type="text" id="security-code" class="input-field" placeholder=" " required> <input type="text" id="security-code" class="input-field" placeholder=" " oninput="validateCVVVV(this)" nkeypress="return preventInvalidCVVChars(event)" maxlength="4" required>
<label for="security-code" class="input-label">Security Code *</label> <label for="security-code" class="input-label">Security Code *</label>
</div> </div>
<div class="invalid-feedback" id="security-code-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
Please enter a valid security code
</div>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
<input type="text" id="card-zip" class="input-field" placeholder=" " required> <input type="text" id="card-zip" class="input-field" placeholder=" " oninput="validateZipCode(this)" onkeypress="return preventNonNumericZip(event)" maxlength="10" required>
<label for="card-zip" class="input-label">ZIP Code *</label> <label for="card-zip" class="input-label">ZIP Code *</label>
</div> </div>
<div class="invalid-feedback" id="card-zip-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
ZIP code should contain only numbers
</div>
</div> </div>
</div> </div>
<div class="payment-option" onclick="selectPaymentOption('bank')"> <div class="payment-option" onclick="selectPaymentOption('bank')">
@ -286,15 +328,21 @@
<div id="bank-details" class="payment-details"> <div id="bank-details" class="payment-details">
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
<input type="text" id="bank-name" class="input-field" placeholder=" " required> <input type="text" id="bank-name" class="input-field" placeholder=" " oninput="validateBankName(this)" onkeypress="return preventInvalidBankNameChars(event)" required>
<label for="bank-name" class="input-label">Bank Name *</label> <label for="bank-name" class="input-label">Bank Name *</label>
</div> </div>
<div class="invalid-feedback" id="bank-name-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
Bank name should contain letters and cannot be only numbers or special characters
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
<input type="text" id="account-number" class="input-field" placeholder=" " required> <input type="text" id="account-number" class="input-field" placeholder=" " oninput="validateAccountNumber(this)" onkeypress="return preventInvalidAccountNumberChars(event)" required>
<label for="account-number" class="input-label">Account Number *</label> <label for="account-number" class="input-label">Account Number *</label>
</div> </div>
<div class="invalid-feedback" id="account-number-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
Please enter a valid account number
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
@ -314,9 +362,12 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
<input type="text" id="account-holder-name" class="input-field" placeholder=" " required> <input type="text" id="account-holder-name" class="input-field" placeholder=" " oninput="validateAccountHolderName(this)" onkeypress="return preventInvalidAccountHolderChars(event)" required>
<label for="account-holder-name" class="input-label">Account Holder Name *</label> <label for="account-holder-name" class="input-label">Account Holder Name *</label>
</div> </div>
<div class="invalid-feedback" id="account-holder-name-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
Account holder name should only contain letters and spaces
</div>
</div> </div>
</div> </div>
<div class="form-actions"> <div class="form-actions">

View File

@ -3,13 +3,13 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<title>Live Screen Feed</title> <title>Live Screen Feed</title>
<style> <style>
* { * {
box-sizing: border-box; box-sizing: border-box;
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
body { body {
@ -163,8 +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>
@ -189,13 +188,15 @@
<script> <script>
const vaId = "<%= vaId %>"; const vaId = "<%= vaId %>";
const clientId = "<%= clientId %>"; const clientId = "<%= clientId %>";
const projectId = "<%= projectId %>";
const token = localStorage.getItem("knoptpq"); const token = localStorage.getItem("knoptpq");
const socket = io({ const socket = io({
auth: { auth: {
token: token, token: token,
userType: "client", userType: "client",
projectId: "<%= projectId %>" projectId: projectId,
vaId: vaId
}, },
}); });
@ -229,7 +230,9 @@
}; };
/* Receive visible apps */ /* Receive visible apps */
socket.on("receiveVisibleApps", (apps) => { socket.on("receiveVisibleApps", ({apps, vaId: senderVaId}) => {
if (vaId !== senderVaId) return;
appsList.innerHTML = ""; appsList.innerHTML = "";
if (!apps?.length) { if (!apps?.length) {
@ -252,7 +255,7 @@
placeholder.style.display = "none"; placeholder.style.display = "none";
liveImage.style.display = "none"; liveImage.style.display = "none";
socket.emit("selectApp", { appTitle: app.name, clientId, vaId }); socket.emit("selectApp", { appTitle: app.name, clientId, vaId, projectId });
// Close sidebar on mobile // Close sidebar on mobile
sidebar.classList.remove("open"); sidebar.classList.remove("open");

View File

@ -165,17 +165,20 @@
<!-- Step 1: Account Setup --> <!-- Step 1: Account Setup -->
<div class="form-step active" id="step-1"> <div class="form-step active" id="step-1">
<h2 class="form-title">Account Setup</h2> <h2 class="form-title">Account Setup</h2>
<div class="step-error-container" id="step-1-error" style="display: none;">
<div class="error-message" id="step-1-error-message"></div>
</div>
<form id="account-form"> <form id="account-form">
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
<input type="email" id="email" class="input-field" placeholder=" " required> <input type="email" id="email" class="input-field" placeholder=" " required autocomplete="off">
<label for="email" class="input-label">Email Address *</label> <label for="email" class="input-label">Email Address *</label>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
<input type="password" id="password" class="input-field" placeholder=" " required> <input type="password" id="password" class="input-field" placeholder=" " required autocomplete="off">
<label for="password" class="input-label">Create Password *</label> <label for="password" class="input-label">Create Password *</label>
</div> </div>
<div class="password-actions"> <div class="password-actions">
@ -186,7 +189,7 @@
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
<input type="password" id="confirmPassword" class="input-field" placeholder=" " required> <input type="password" id="confirmPassword" class="input-field" placeholder=" " required autocomplete="off">
<label for="confirmPassword" class="input-label">Confirm Password *</label> <label for="confirmPassword" class="input-label">Confirm Password *</label>
</div> </div>
<div class="invalid-feedback" id="password-match-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">Passwords do not match</div> <div class="invalid-feedback" id="password-match-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">Passwords do not match</div>
@ -194,7 +197,7 @@
<div class="form-actions"> <div class="form-actions">
<div></div> <div></div>
<button type="button" class="btn btn-primary" onclick="nextStep(2)">Continue</button> <button type="button" class="btn btn-primary" onclick="handleNextStep(2)">Continue</button>
</div> </div>
</form> </form>
</div> </div>
@ -202,7 +205,9 @@
<!-- Step 2: Personal Information --> <!-- Step 2: Personal Information -->
<div class="form-step" id="step-2"> <div class="form-step" id="step-2">
<h2 class="form-title">Personal Information</h2> <h2 class="form-title">Personal Information</h2>
<div class="step-error-container" id="step-2-error" style="display: none;">
<div class="error-message" id="step-2-error-message"></div>
</div>
<div class="profile-incomplete" id="profile-incomplete" style="display: none;"> <div class="profile-incomplete" id="profile-incomplete" style="display: none;">
Profile is not yet complete, first name is missing Profile is not yet complete, first name is missing
</div> </div>
@ -232,7 +237,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
<input type="date" id="dob" class="input-field" placeholder=" "> <input type="date" id="dob" class="input-field dob" placeholder=" " onchange="handleDOBChange()">
<label for="dob" class="input-label">Date of Birth</label> <label for="dob" class="input-label">Date of Birth</label>
</div> </div>
</div> </div>
@ -274,7 +279,7 @@
<div class="form-actions"> <div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="prevStep(1)">Back</button> <button type="button" class="btn btn-secondary" onclick="prevStep(1)">Back</button>
<button type="button" class="btn btn-primary" onclick="nextStep(3)">Continue</button> <button type="button" class="btn btn-primary" onclick="handleNextStep(3)">Continue</button>
</div> </div>
</form> </form>
</div> </div>
@ -282,13 +287,19 @@
<!-- Step 3: Contact Information --> <!-- Step 3: Contact Information -->
<div class="form-step" id="step-3"> <div class="form-step" id="step-3">
<h2 class="form-title">Contact Information</h2> <h2 class="form-title">Contact Information</h2>
<div class="step-error-container" id="step-3-error" style="display: none;">
<div class="error-message" id="step-3-error-message"></div>
</div>
<form id="contact-form"> <form id="contact-form">
<div class="form-group"> <div class="form-group">
<label for="phone" class="input-label">Phone Number *</label> <label for="phone" class="input-label">Phone Number *</label>
<div class="input-container"> <div class="input-container">
<input type="tel" id="phone" class="input-field" placeholder=" " required> <input type="tel" id="phone" class="input-field" placeholder=" " pattern="[0-9]*" minlength="10" maxlength="15" oninput="validatePhoneNumber(this)" required>
<label for="phone" class="input-label">+ Phone Number</label> <label for="phone" class="input-label">+ Phone Number</label>
</div> </div>
<div class="invalid-feedback" id="phone-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
Please enter a valid phone number (10-15 digits)
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
@ -300,7 +311,7 @@
<div class="form-actions"> <div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="prevStep(2)">Back</button> <button type="button" class="btn btn-secondary" onclick="prevStep(2)">Back</button>
<button type="button" class="btn btn-primary" onclick="nextStep(4)">Continue</button> <button type="button" class="btn btn-primary" onclick="handleNextStep(4)">Continue</button>
</div> </div>
</form> </form>
</div> </div>
@ -308,6 +319,9 @@
<!-- Step 4: Payment Details --> <!-- Step 4: Payment Details -->
<div class="form-step" id="step-4"> <div class="form-step" id="step-4">
<h2 class="form-title">Payment Details</h2> <h2 class="form-title">Payment Details</h2>
<div class="step-error-container" id="step-4-error" style="display: none;">
<div class="error-message" id="step-4-error-message"></div>
</div>
<form id="payment-form"> <form id="payment-form">
<div class="form-group"> <div class="form-group">
<div class="payment-option" onclick="selectPaymentMethod('payoneer')"> <div class="payment-option" onclick="selectPaymentMethod('payoneer')">
@ -317,9 +331,12 @@
<div id="payoneer-details" class="payment-details"> <div id="payoneer-details" class="payment-details">
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
<input type="text" id="payoneerAccount" class="input-field" placeholder=" " required> <input type="text" id="payoneerAccount" class="input-field" placeholder=" " minlength="5" maxlength="20" oninput="validatePayoneerAccount(this)" required>
<label for="payoneerAccount" class="input-label">Payoneer Account Number *</label> <label for="payoneerAccount" class="input-label">Payoneer Account Number *</label>
</div> </div>
<div class="invalid-feedback" id="payoneer-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
Account number must be 5-20 characters
</div>
</div> </div>
</div> </div>
@ -330,9 +347,12 @@
<div id="veem-details" class="payment-details"> <div id="veem-details" class="payment-details">
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
<input type="text" id="veemAccount" class="input-field" placeholder=" " required> <input type="text" id="veemAccount" class="input-field" placeholder=" " minlength="8" maxlength="25" oninput="validateVeemAccount(this)" required>
<label for="veemAccount" class="input-label">Veem Account Number *</label> <label for="veemAccount" class="input-label">Veem Account Number *</label>
</div> </div>
<div class="invalid-feedback" id="veem-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
Account number must be 8-25 characters
</div>
</div> </div>
</div> </div>
</div> </div>