Compare commits

..

No commits in common. "d0bb922da1196069617c40f7e8561ddff9afe78d" and "142f4f77cd58f615e50419845170797d2828196b" have entirely different histories.

34 changed files with 821 additions and 3614 deletions

File diff suppressed because it is too large Load Diff

View File

@ -5,52 +5,9 @@
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
} }
:root {
--bg-primary: #ffffff;
--bg-secondary: #f8fafc;
--bg-card: #ffffff;
--bg-sidebar: linear-gradient(to bottom, #f4f4f5, #d0f9f9, #cac5f7);
--text-primary: #333333;
--text-secondary: #6b7280;
--text-muted: #9ca3af;
--border-color: #e5e7eb;
--border-dark: #374151;
--accent-color: #000000;
--accent-light: #070065;
--success-color: #10b981;
--error-color: #dc2626;
--warning-bg: #fff3cd;
--warning-border: #ffeaa7;
--warning-text: #856404;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 4px 6px rgba(0, 0, 0, 0.05);
}
[data-theme="dark"] {
--bg-primary: #121212;
--bg-secondary: #1e1e1e;
--bg-card: #1e1e1e;
--bg-sidebar: linear-gradient(to bottom, #2d2d2d, #1a3a3a, #2a264d);
--text-primary: #f5f5f5;
--text-secondary: #d1d5db;
--text-muted: #9ca3af;
--border-color: #374151;
--border-dark: #4b5563;
--accent-color: #ffffff;
--accent-light: #6366f1;
--success-color: #10b981;
--error-color: #ef4444;
--warning-bg: #422006;
--warning-border: #854d0e;
--warning-text: #fef3c7;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
--shadow-lg: 0 4px 6px rgba(0, 0, 0, 0.2);
}
body { body {
background-color: var(--bg-primary); background-color: #fff;
color: var(--text-primary); color: #333;
transition: background-color 0.3s ease, color 0.3s ease;
} }
.container { .container {

View File

@ -693,12 +693,12 @@ async function submitForm() {
let cardNumber = document.getElementById('card-number'); let cardNumber = document.getElementById('card-number');
let expiryDate = document.getElementById('expiry-date'); let expiryDate = document.getElementById('expiry-date');
let securityCode = document.getElementById('security-code'); let securityCode = document.getElementById('security-code');
let cardZip = document.getElementById('card-zip'); let cardZip = document.getElementById('card-zip').value;
let bankName = document.getElementById('bank-name'); let bankName = document.getElementById('bank-name').value;
let accountNumber = document.getElementById('account-number'); let accountNumber = document.getElementById('account-number').value;
let routingNumber = document.getElementById('routing-number'); let routingNumber = document.getElementById('routing-number').value;
let accountType = document.getElementById('account-type'); let accountType = document.getElementById('account-type').value;
let accountHolderName = document.getElementById('account-holder-name'); let accountHolderName = document.getElementById('account-holder-name').value;
if(cardNumber.value) { if(cardNumber.value) {
formData.userDetails.cardNumber = cardNumber.value; formData.userDetails.cardNumber = cardNumber.value;

View File

@ -18,104 +18,56 @@
{ code: 'FR', name: 'France' }, { code: 'FR', name: 'France' },
{ code: 'BR', name: 'Brazil' }, { code: 'BR', name: 'Brazil' },
{ code: 'JP', name: 'Japan' }, { code: 'JP', name: 'Japan' },
{ code: 'CN', name: 'China' }, { code: 'CN', name: 'China' }
{ code: 'MX', name: 'Mexico' },
{ code: 'ES', name: 'Spain' },
{ code: 'IT', name: 'Italy' },
{ code: 'NL', name: 'Netherlands' },
{ code: 'SE', name: 'Sweden' },
{ code: 'NO', name: 'Norway' },
{ code: 'DK', name: 'Denmark' },
{ code: 'FI', name: 'Finland' },
{ code: 'BE', name: 'Belgium' },
{ code: 'CH', name: 'Switzerland' },
{ code: 'AT', name: 'Austria' },
{ code: 'PT', name: 'Portugal' },
{ code: 'IE', name: 'Ireland' },
{ code: 'PL', name: 'Poland' },
{ code: 'CZ', name: 'Czech Republic' },
{ code: 'HU', name: 'Hungary' },
{ code: 'RO', name: 'Romania' },
{ code: 'GR', name: 'Greece' },
{ code: 'TR', name: 'Turkey' },
{ code: 'SA', name: 'Saudi Arabia' },
{ code: 'AE', name: 'United Arab Emirates' },
{ code: 'ZA', name: 'South Africa' },
{ code: 'NG', name: 'Nigeria' },
{ code: 'EG', name: 'Egypt' },
{ code: 'KE', name: 'Kenya' },
{ code: 'AR', name: 'Argentina' },
{ code: 'CO', name: 'Colombia' },
{ code: 'PE', name: 'Peru' },
{ code: 'CL', name: 'Chile' },
{ code: 'VE', name: 'Venezuela' },
{ code: 'KR', name: 'South Korea' },
{ code: 'SG', name: 'Singapore' },
{ code: 'MY', name: 'Malaysia' },
{ code: 'TH', name: 'Thailand' },
{ code: 'ID', name: 'Indonesia' },
{ code: 'PH', name: 'Philippines' },
{ code: 'VN', name: 'Vietnam' },
{ code: 'PK', name: 'Pakistan' },
{ code: 'BD', name: 'Bangladesh' },
{ code: 'LK', name: 'Sri Lanka' },
{ code: 'NP', name: 'Nepal' },
{ code: 'RU', name: 'Russia' },
{ code: 'UA', name: 'Ukraine' },
{ code: 'IL', name: 'Israel' },
{ code: 'NZ', name: 'New Zealand' },
{ code: 'HK', name: 'Hong Kong' },
{ code: 'TW', name: 'Taiwan' }
]; ];
// const states = { const states = {
// 'US': [ 'US': [
// { code: 'CA', name: 'California' }, { code: 'TX', name: 'Texas' }, { code: 'FL', name: 'Florida' }, { code: 'CA', name: 'California' }, { code: 'TX', name: 'Texas' }, { code: 'FL', name: 'Florida' },
// { code: 'NY', name: 'New York' }, { code: 'IL', name: 'Illinois' }, { code: 'PA', name: 'Pennsylvania' }, { code: 'NY', name: 'New York' }, { code: 'IL', name: 'Illinois' }, { code: 'PA', name: 'Pennsylvania' },
// { code: 'OH', name: 'Ohio' }, { code: 'GA', name: 'Georgia' }, { code: 'NC', name: 'North Carolina' }, { code: 'OH', name: 'Ohio' }, { code: 'GA', name: 'Georgia' }, { code: 'NC', name: 'North Carolina' },
// { code: 'MI', name: 'Michigan' } { code: 'MI', name: 'Michigan' }
// ], ],
// 'CA': [ 'CA': [
// { code: 'ON', name: 'Ontario' }, { code: 'QC', name: 'Quebec' }, { code: 'BC', name: 'British Columbia' }, { code: 'ON', name: 'Ontario' }, { code: 'QC', name: 'Quebec' }, { code: 'BC', name: 'British Columbia' },
// { code: 'AB', name: 'Alberta' }, { code: 'MB', name: 'Manitoba' }, { code: 'SK', name: 'Saskatchewan' } { code: 'AB', name: 'Alberta' }, { code: 'MB', name: 'Manitoba' }, { code: 'SK', name: 'Saskatchewan' }
// ], ],
// 'GB': [ 'GB': [
// { code: 'ENG', name: 'England' }, { code: 'SCT', name: 'Scotland' }, { code: 'WLS', name: 'Wales' }, { code: 'ENG', name: 'England' }, { code: 'SCT', name: 'Scotland' }, { code: 'WLS', name: 'Wales' },
// { code: 'NIR', name: 'Northern Ireland' } { code: 'NIR', name: 'Northern Ireland' }
// ], ],
// 'AU': [ 'AU': [
// { code: 'NSW', name: 'New South Wales' }, { code: 'VIC', name: 'Victoria' }, { code: 'QLD', name: 'Queensland' }, { code: 'NSW', name: 'New South Wales' }, { code: 'VIC', name: 'Victoria' }, { code: 'QLD', name: 'Queensland' },
// { code: 'WA', name: 'Western Australia' }, { code: 'SA', name: 'South Australia' }, { code: 'TAS', name: 'Tasmania' } { code: 'WA', name: 'Western Australia' }, { code: 'SA', name: 'South Australia' }, { code: 'TAS', name: 'Tasmania' }
// ], ],
// 'IN': [ 'IN': [
// { code: 'MH', name: 'Maharashtra' }, { code: 'TN', name: 'Tamil Nadu' }, { code: 'UP', name: 'Uttar Pradesh' }, { code: 'MH', name: 'Maharashtra' }, { code: 'TN', name: 'Tamil Nadu' }, { code: 'UP', name: 'Uttar Pradesh' },
// { code: 'KA', name: 'Karnataka' }, { code: 'GJ', name: 'Gujarat' }, { code: 'RJ', name: 'Rajasthan' } { code: 'KA', name: 'Karnataka' }, { code: 'GJ', name: 'Gujarat' }, { code: 'RJ', name: 'Rajasthan' }
// ], ],
// 'DE': [ 'DE': [
// { code: 'BW', name: 'Baden-Württemberg' }, { code: 'BY', name: 'Bavaria' }, { code: 'BE', name: 'Berlin' }, { code: 'BW', name: 'Baden-Württemberg' }, { code: 'BY', name: 'Bavaria' }, { code: 'BE', name: 'Berlin' },
// { code: 'HB', name: 'Bremen' }, { code: 'HH', name: 'Hamburg' }, { code: 'HE', name: 'Hesse' } { code: 'HB', name: 'Bremen' }, { code: 'HH', name: 'Hamburg' }, { code: 'HE', name: 'Hesse' }
// ], ],
// 'FR': [ 'FR': [
// { code: 'ARA', name: 'Auvergne-Rhône-Alpes' }, { code: 'BFC', name: 'Bourgogne-Franche-Comté' }, { code: 'ARA', name: 'Auvergne-Rhône-Alpes' }, { code: 'BFC', name: 'Bourgogne-Franche-Comté' },
// { code: 'BRE', name: 'Brittany' }, { code: 'CVL', name: 'Centre-Val de Loire' }, { code: 'BRE', name: 'Brittany' }, { code: 'CVL', name: 'Centre-Val de Loire' },
// { code: 'COR', name: 'Corsica' }, { code: 'GES', name: 'Grand Est' } { code: 'COR', name: 'Corsica' }, { code: 'GES', name: 'Grand Est' }
// ], ],
// 'BR': [ 'BR': [
// { code: 'SP', name: 'São Paulo' }, { code: 'RJ', name: 'Rio de Janeiro' }, { code: 'MG', name: 'Minas Gerais' }, { code: 'SP', name: 'São Paulo' }, { code: 'RJ', name: 'Rio de Janeiro' }, { code: 'MG', name: 'Minas Gerais' },
// { code: 'BA', name: 'Bahia' }, { code: 'RS', name: 'Rio Grande do Sul' }, { code: 'PR', name: 'Paraná' } { code: 'BA', name: 'Bahia' }, { code: 'RS', name: 'Rio Grande do Sul' }, { code: 'PR', name: 'Paraná' }
// ], ],
// 'JP': [ 'JP': [
// { code: 'TK', name: 'Tokyo' }, { code: 'OS', name: 'Osaka' }, { code: 'KY', name: 'Kyoto' }, { code: 'TK', name: 'Tokyo' }, { code: 'OS', name: 'Osaka' }, { code: 'KY', name: 'Kyoto' },
// { code: 'HK', name: 'Hokkaido' }, { code: 'OK', name: 'Okinawa' }, { code: 'KN', name: 'Kanagawa' } { code: 'HK', name: 'Hokkaido' }, { code: 'OK', name: 'Okinawa' }, { code: 'KN', name: 'Kanagawa' }
// ], ],
// 'CN': [ 'CN': [
// { code: 'BJ', name: 'Beijing' }, { code: 'SH', name: 'Shanghai' }, { code: 'TJ', name: 'Tianjin' }, { code: 'BJ', name: 'Beijing' }, { code: 'SH', name: 'Shanghai' }, { code: 'TJ', name: 'Tianjin' },
// { code: 'CQ', name: 'Chongqing' }, { code: 'GD', name: 'Guangdong' }, { code: 'JS', name: 'Jiangsu' } { code: 'CQ', name: 'Chongqing' }, { code: 'GD', name: 'Guangdong' }, { code: 'JS', name: 'Jiangsu' }
// ] ]
// }; };
// Initialize the form // Initialize the form
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Populate country dropdown // Populate country dropdown
const countrySelect = document.getElementById('country'); const countrySelect = document.getElementById('country');
@ -128,34 +80,32 @@
countrySelect.appendChild(option); countrySelect.appendChild(option);
}); });
autoDetectCountry();
// Add event listener for country change // Add event listener for country change
// countrySelect.addEventListener('change', function() { countrySelect.addEventListener('change', function() {
// const countryCode = this.value; const countryCode = this.value;
// const stateSelect = document.getElementById('state'); const stateSelect = document.getElementById('state');
// // Show loading // Show loading
// document.getElementById('state-loading').style.display = 'block'; document.getElementById('state-loading').style.display = 'block';
// // Clear current states // Clear current states
// stateSelect.innerHTML = '<option value="" disabled selected></option>'; stateSelect.innerHTML = '<option value="" disabled selected></option>';
// // Simulate API delay // Simulate API delay
// setTimeout(() => { setTimeout(() => {
// if (countryCode && states[countryCode]) { if (countryCode && states[countryCode]) {
// states[countryCode].forEach(state => { states[countryCode].forEach(state => {
// const option = document.createElement('option'); const option = document.createElement('option');
// option.value = state.code; option.value = state.code;
// option.textContent = state.name; option.textContent = state.name;
// stateSelect.appendChild(option); stateSelect.appendChild(option);
// }); });
// } }
// // Hide loading // Hide loading
// document.getElementById('state-loading').style.display = 'none'; document.getElementById('state-loading').style.display = 'none';
// }, 500); }, 500);
// }); });
// Add input event listeners to handle label animation // Add input event listeners to handle label animation
@ -259,102 +209,6 @@
}); });
}); });
async function autoDetectCountry() {
const countrySelect = document.getElementById('country');
// const loadingElement = document.getElementById('country-loading');
try {
// loadingElement.style.display = 'block';
// Using ipapi.co API (free tier available)
const response = await fetch('https://ipapi.co/json/');
if (!response.ok) {
throw new Error('Geolocation service unavailable');
}
const data = await response.json();
if (data && data.country_code) {
const detectedCountryCode = data.country_code;
const detectedCountryName = data.country_name;
// Find if the detected country is in our list
const countryExists = countries.find(country =>
country.code === detectedCountryCode
);
if (countryExists) {
// Set the country in dropdown
countrySelect.value = detectedCountryCode;
// Trigger the change event to update UI
countrySelect.dispatchEvent(new Event('change'));
// Show success message
console.log(`Auto-detected country: ${detectedCountryName}`);
} else {
console.log('Detected country not in supported list');
}
}
} catch (error) {
console.log('Could not auto-detect country:', error.message);
// Fallback: Use browser's geolocation API as backup
fallbackGeolocation();
} finally {
// loadingElement.style.display = 'none';
}
}
// Fallback geolocation using browser's navigator.geolocation
function fallbackGeolocation() {
if (!navigator.geolocation) {
console.log('Geolocation is not supported by this browser');
return;
}
navigator.geolocation.getCurrentPosition(
async function(position) {
try {
const { latitude, longitude } = position.coords;
// Use OpenStreetMap Nominatim API for reverse geocoding
const response = await fetch(
`https://nominatim.openstreetmap.org/reverse?format=json&lat=${latitude}&lon=${longitude}`
);
if (response.ok) {
const data = await response.json();
if (data && data.address && data.address.country_code) {
const countryCode = data.address.country_code.toUpperCase();
const countrySelect = document.getElementById('country');
const countryExists = countries.find(country =>
country.code === countryCode
);
if (countryExists) {
countrySelect.value = countryCode;
countrySelect.dispatchEvent(new Event('change'));
console.log(`Fallback geolocation detected: ${data.address.country}`);
}
}
}
} catch (error) {
console.log('Fallback geolocation failed:', error);
}
},
function(error) {
console.log('Browser geolocation error:', error.message);
},
{
enableHighAccuracy: false,
timeout: 10000,
maximumAge: 600000 // 10 minutes
}
);
}
// Function to navigate to any step (for tab navigation) // Function to navigate to any step (for tab navigation)
function goToStep(step) { function goToStep(step) {
// Only allow navigation to completed steps or the next step // Only allow navigation to completed steps or the next step
@ -617,26 +471,17 @@
} 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) {
// showError('Please enter your Payoneer account number', 'payoneerAccount', step); 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) {
showError('Please enter your Veem account number', 'veemAccount', step); showError('Please enter your Veem account number', 'veemAccount', step);
isValid = false; isValid = false;
} }
} else if (paymentMethod === 'wise') {
const wiseEmail = document.getElementById('wiseEmail').value;
if (!wiseEmail) {
showError('Please enter your Wise email address', 'wiseEmail', step);
isValid = false;
} else if (!validateEmail(wiseEmail)) {
showError('Please enter a valid email address for Wise', 'wiseEmail', step);
isValid = false;
}
} }
} }
} }
@ -691,11 +536,10 @@
formData.userDetails.city = document.getElementById('city').value; formData.userDetails.city = document.getElementById('city').value;
formData.userDetails.state = document.getElementById('state').value; formData.userDetails.state = document.getElementById('state').value;
formData.userDetails.country = document.getElementById('country').value; formData.userDetails.country = document.getElementById('country').value;
formData.status = document.getElementById('status').value;
break; break;
case 3: case 3:
formData.userDetails.phone = document.getElementById('phone').value; formData.userDetails.phone = document.getElementById('phone').value;
// formData.userDetails.skype = document.getElementById('skype').value; formData.userDetails.skype = document.getElementById('skype').value;
break; break;
case 4: case 4:
const selectedPayment = document.querySelector('.payment-option.selected'); const selectedPayment = document.querySelector('.payment-option.selected');
@ -703,15 +547,12 @@
const paymentMethod = selectedPayment.getAttribute('onclick').match(/'([^']+)'/)[1]; const paymentMethod = selectedPayment.getAttribute('onclick').match(/'([^']+)'/)[1];
formData.userDetails.paymentMethod = paymentMethod; formData.userDetails.paymentMethod = paymentMethod;
// if (document.getElementById('payoneerAccount').value) { if (document.getElementById('payoneerAccount').value) {
// formData.userDetails.payoneerAccount = document.getElementById('payoneerAccount').value; formData.userDetails.payoneerAccount = document.getElementById('payoneerAccount').value;
// } }
if (document.getElementById('veemAccount').value) { if (document.getElementById('veemAccount').value) {
formData.userDetails.veemAccount = document.getElementById('veemAccount').value; formData.userDetails.veemAccount = document.getElementById('veemAccount').value;
} }
if (paymentMethod === 'wise') {
formData.userDetails.wiseEmail = document.getElementById('wiseEmail').value;
}
} }
break; break;
} }
@ -734,35 +575,12 @@
// Show appropriate payment details // Show appropriate payment details
if (method === 'payoneer') { if (method === 'payoneer') {
// document.getElementById('payoneer-details').style.display = 'block'; document.getElementById('payoneer-details').style.display = 'block';
} else if (method === 'veem') { } else if (method === 'veem') {
document.getElementById('veem-details').style.display = 'block'; document.getElementById('veem-details').style.display = 'block';
} else if (method === 'wise') {
document.getElementById('wise-details').style.display = 'block';
} }
} }
function validateWiseEmail(input) {
const wiseError = document.getElementById('wise-email-error');
const value = input.value.trim();
// Remove any validation styling first
input.style.border = '';
wiseError.style.display = 'none';
if (value.length > 0) {
// Check if it's a valid email
if (!validateEmail(value)) {
wiseError.textContent = 'Please enter a valid email address for Wise';
wiseError.style.display = 'block';
input.style.border = '1px solid #ef4444';
return false;
}
}
return true;
}
// Function to generate a strong password // Function to generate a strong password
function generatePassword() { function generatePassword() {
const length = 12; const length = 12;
@ -1151,7 +969,7 @@ function validateSkypeId(input) {
if (value && !/^[a-zA-Z][a-zA-Z0-9\.,\-_]+$/.test(value)) { if (value && !/^[a-zA-Z][a-zA-Z0-9\.,\-_]+$/.test(value)) {
input.style.border = '1px solid #ef4444'; input.style.border = '1px solid #ef4444';
showError('Invalid Skype ID', 'skype', 3); showError('Skype ID must start with a letter and can only contain letters, numbers, dots, hyphens, and underscores', 'skype', 3);
return false; return false;
} else { } else {
input.style.border = ''; input.style.border = '';

View File

@ -68,7 +68,12 @@ async function client_dashboard(req, res) {
$cond: [{ $eq: ["$loggedType", "LOGIN"] }, 1, 0] $cond: [{ $eq: ["$loggedType", "LOGIN"] }, 1, 0]
} }
}, },
lastActivity: { $first: "$time" } lastActivity: { $first: "$time" },
projectsWorked: {
$addToSet: "$projectId"
},
// Get all login logs for this VA
allLogs: { $push: "$$ROOT" }
} }
}, },
{ {
@ -80,12 +85,22 @@ async function client_dashboard(req, res) {
const activeVaIds = agg.map((a) => a._id).filter(Boolean); const activeVaIds = agg.map((a) => a._id).filter(Boolean);
// Get active VA details
const activeVAs = await VAModel.find({
_id: { $in: activeVaIds },
clientIds: clientID
})
.select('userDetails.firstName userDetails.lastName userDetails.email userDetails.phone status createdAt projectIds')
.lean()
.exec();
// Create mapping for last log and activity data // Create mapping for last log and activity data
const activityByVa = agg.reduce((acc, a) => { const activityByVa = agg.reduce((acc, a) => {
acc[String(a._id)] = { acc[String(a._id)] = {
lastLog: a.lastLog, lastLog: a.lastLog,
totalLogins: a.totalLogins, totalLogins: a.totalLogins,
lastActivity: a.lastActivity lastActivity: a.lastActivity,
projectsWorked: a.projectsWorked || []
}; };
return acc; return acc;
}, {}); }, {});
@ -111,14 +126,14 @@ async function client_dashboard(req, res) {
logsByVa[vaId].push(log); logsByVa[vaId].push(log);
}); });
// Calculate total worked time for each VA // Calculate total worked time for each VA using the same logic as va_log_work_report_callback
for (const [vaId, logs] of Object.entries(logsByVa)) { for (const [vaId, logs] of Object.entries(logsByVa)) {
let totalWorkSeconds = 0; let totalWorkSeconds = 0;
// Group logs by date (YYYY-MM-DD format) // Group logs by date (YYYY-MM-DD format)
const logsByDate = {}; const logsByDate = {};
logs.forEach(log => { logs.forEach(log => {
const date = new Date(log.time).toISOString().split('T')[0]; const date = new Date(log.time).toISOString().split('T')[0]; // YYYY-MM-DD
if (!logsByDate[date]) { if (!logsByDate[date]) {
logsByDate[date] = []; logsByDate[date] = [];
} }
@ -132,6 +147,7 @@ async function client_dashboard(req, res) {
let dayWorkSeconds = 0; let dayWorkSeconds = 0;
let lastLogin = null; let lastLogin = null;
let lastProjectId = null;
// If day has adjustment logs, only process adjustment logs // If day has adjustment logs, only process adjustment logs
// If no adjustment logs, process all logs for that day // If no adjustment logs, process all logs for that day
@ -148,10 +164,14 @@ async function client_dashboard(req, res) {
if (log.loggedType === "LOGIN") { if (log.loggedType === "LOGIN") {
lastLogin = time; lastLogin = time;
lastProjectId = log.projectId?.toString() || null;
} else if (log.loggedType === "LOGOUT" && lastLogin) { } else if (log.loggedType === "LOGOUT" && lastLogin) {
const diffSec = (time - lastLogin) / 1000; const diffSec = (time - lastLogin) / 1000;
dayWorkSeconds += diffSec; dayWorkSeconds += diffSec;
// reset after each pair
lastLogin = null; lastLogin = null;
lastProjectId = null;
} }
} }
@ -164,93 +184,25 @@ async function client_dashboard(req, res) {
}; };
} }
// NEW: Calculate project-wise work time for each VA
const projectWorkTimeByVa = {};
for (const [vaId, logs] of Object.entries(logsByVa)) {
const projectWorkTime = {};
// Group logs by project
const logsByProject = {};
logs.forEach(log => {
const projectId = log.projectId?.toString();
if (projectId) {
if (!logsByProject[projectId]) {
logsByProject[projectId] = [];
}
logsByProject[projectId].push(log);
}
});
// Calculate work time per project
for (const [projectId, projectLogs] of Object.entries(logsByProject)) {
let projectWorkSeconds = 0;
// Group logs by date
const logsByDate = {};
projectLogs.forEach(log => {
const date = new Date(log.time).toISOString().split('T')[0];
if (!logsByDate[date]) {
logsByDate[date] = [];
}
logsByDate[date].push(log);
});
// Process each day separately
for (const [date, dayLogs] of Object.entries(logsByDate)) {
const hasAdjustmentLogs = dayLogs.some(log => log.isAdjustment === true);
let dayWorkSeconds = 0;
let lastLogin = null;
const logsToProcess = hasAdjustmentLogs
? dayLogs.filter(log => log.isAdjustment === true)
: dayLogs;
logsToProcess.sort((a, b) => new Date(a.time) - new Date(b.time));
for (const log of logsToProcess) {
const time = new Date(log.time);
if (log.loggedType === "LOGIN") {
lastLogin = time;
} else if (log.loggedType === "LOGOUT" && lastLogin) {
const diffSec = (time - lastLogin) / 1000;
dayWorkSeconds += diffSec;
lastLogin = null;
}
}
projectWorkSeconds += dayWorkSeconds;
}
projectWorkTime[projectId] = {
totalSeconds: projectWorkSeconds,
formatted: formatTime(projectWorkSeconds),
projectName: projectMap[projectId]?.name || 'Unknown Project'
};
}
projectWorkTimeByVa[vaId] = projectWorkTime;
}
// Calculate session duration for currently active VAs // Calculate session duration for currently active VAs
const currentTime = new Date(); const currentTime = new Date();
// Format all assigned VAs - Only include data needed for frontend // Format all assigned VAs
const allVAsFormatted = allAssignedVAs.map((v) => { const allVAsFormatted = allAssignedVAs.map((v) => {
const activity = activityByVa[String(v._id)] || {}; const activity = activityByVa[String(v._id)] || {};
const workedTime = totalWorkedTimeByVa[String(v._id)] || { totalSeconds: 0, formatted: '00:00:00' }; const workedTime = totalWorkedTimeByVa[String(v._id)] || { totalSeconds: 0, formatted: '00:00:00' };
const lastLog = activity.lastLog; const lastLog = activity.lastLog;
const projectWorkTime = projectWorkTimeByVa[String(v._id)] || {};
// Calculate current session duration if still logged in // Calculate current session duration if still logged in
let currentSessionDuration = null; let currentSessionDuration = null;
let isCurrentlyActive = false; let isCurrentlyActive = false;
let currentSessionFormatted = null;
let currentProjectDetails = null; let currentProjectDetails = null;
if (lastLog && lastLog.loggedType === "LOGIN") { if (lastLog && lastLog.loggedType === "LOGIN") {
const loginTime = new Date(lastLog.time); const loginTime = new Date(lastLog.time);
currentSessionDuration = Math.floor((currentTime - loginTime) / 1000); currentSessionDuration = Math.floor((currentTime - loginTime) / 1000);
currentSessionFormatted = formatTime(currentSessionDuration);
isCurrentlyActive = true; isCurrentlyActive = true;
// Consider VA inactive if last login was more than 24 hours ago // Consider VA inactive if last login was more than 24 hours ago
@ -271,29 +223,6 @@ async function client_dashboard(req, res) {
// Combine first and last name // Combine first and last name
const fullName = `${v.userDetails?.firstName || ''} ${v.userDetails?.lastName || ''}`.trim() || 'Unknown'; const fullName = `${v.userDetails?.firstName || ''} ${v.userDetails?.lastName || ''}`.trim() || 'Unknown';
// Filter: Only include projects that belong to this client
const clientProjectsAssigned = (v.projectIds || []).filter(projectId =>
clientProjectIds.some(clientProjectId =>
clientProjectId.toString() === projectId.toString()
)
);
// Format project work time for popup
const projectsWithWorkTime = clientProjectsAssigned.map(projectId => {
const projectIdStr = projectId.toString();
const workData = projectWorkTime[projectIdStr] || { totalSeconds: 0, formatted: '00:00:00' };
const project = projectMap[projectIdStr];
return {
id: projectId,
name: project?.name || 'Unknown Project',
totalWorkedTime: workData.formatted,
totalWorkedSeconds: workData.totalSeconds,
isCurrentlyWorking: currentProjectDetails?.id?.toString() === projectIdStr,
hourlyRate: project?.hourlyRate || 0
};
});
return { return {
id: v._id, id: v._id,
name: fullName, name: fullName,
@ -302,18 +231,99 @@ async function client_dashboard(req, res) {
status: v.status || 'active', status: v.status || 'active',
isCurrentlyActive: isCurrentlyActive, isCurrentlyActive: isCurrentlyActive,
lastLoginTime: lastLog?.time || null, lastLoginTime: lastLog?.time || null,
lastActivity: activity.lastActivity,
currentSessionDuration: currentSessionDuration, currentSessionDuration: currentSessionDuration,
currentSessionFormatted: currentSessionFormatted,
totalWorkedTime: workedTime.formatted, totalWorkedTime: workedTime.formatted,
totalWorkedSeconds: workedTime.totalSeconds, totalWorkedSeconds: workedTime.totalSeconds,
totalLogins: activity.totalLogins || 0,
lastProject: currentProjectDetails, lastProject: currentProjectDetails,
lastProjectId: lastLog?.projectId || null, lastProjectId: lastLog?.projectId || null,
projectsAssigned: clientProjectsAssigned.length, projectsWorked: activity.projectsWorked || [],
// Detailed project information for popup projectsAssigned: v.projectIds?.length || 0,
projects: projectsWithWorkTime,
vaSince: v.createdAt vaSince: v.createdAt
}; };
}); });
// Format active VAs (currently working)
const activeVAsFormatted = activeVAs.map((v) => {
const activity = activityByVa[String(v._id)] || {};
const workedTime = totalWorkedTimeByVa[String(v._id)] || { totalSeconds: 0, formatted: '00:00:00' };
const lastLog = activity.lastLog;
let currentSessionDuration = null;
let currentSessionFormatted = null;
let currentProjectDetails = null;
if (lastLog && lastLog.loggedType === "LOGIN") {
const loginTime = new Date(lastLog.time);
currentSessionDuration = Math.floor((currentTime - loginTime) / 1000);
currentSessionFormatted = formatTime(currentSessionDuration);
// Get current project details
if (lastLog.projectId && projectMap[lastLog.projectId.toString()]) {
currentProjectDetails = {
id: lastLog.projectId,
name: projectMap[lastLog.projectId.toString()].name
};
}
}
// Combine first and last name
const fullName = `${v.userDetails?.firstName || ''} ${v.userDetails?.lastName || ''}`.trim() || 'Unknown';
return {
id: v._id,
name: fullName,
email: v.userDetails?.email,
phone: v.userDetails?.phone,
status: v.status || 'active',
lastLoginTime: lastLog?.time || null,
currentSessionDuration: currentSessionDuration,
currentSessionFormatted: currentSessionFormatted,
totalWorkedTime: workedTime.formatted,
totalWorkedSeconds: workedTime.totalSeconds,
totalLogins: activity.totalLogins || 0,
currentProject: currentProjectDetails,
currentProjectId: lastLog?.projectId || null,
projectsAssigned: v.projectIds?.length || 0,
vaSince: v.createdAt
};
});
// Format projects with additional data
const projectsFormatted = clientProjects.map((p) => {
// Find VAs currently working on this project
const activeVAsOnProject = allVAsFormatted.filter(va =>
va.isCurrentlyActive && va.lastProjectId &&
va.lastProjectId.toString() === p._id.toString()
);
// Find all VAs assigned to this project
const assignedVAs = allAssignedVAs.filter(va =>
va.projectIds && va.projectIds.some(projectId =>
projectId.toString() === p._id.toString()
)
);
return {
id: p._id,
name: p.name,
hourlyRate: p.hourlyRate,
totalLoggedHours: p.totalLoggedHours || 0,
dedicatedHours: p.dedicatedHours || 0,
vaAssigned: assignedVAs.length,
vaCurrentlyWorking: activeVAsOnProject.length,
activeVAs: activeVAsOnProject.map(va => ({
id: va.id,
name: va.name,
currentSession: va.currentSessionFormatted,
lastLoginTime: va.lastLoginTime
})),
createdAt: p.createdAt
};
});
// Calculate dashboard statistics // Calculate dashboard statistics
const totalHoursLogged = clientProjects.reduce((sum, p) => sum + (p.totalLoggedHours || 0), 0); const totalHoursLogged = clientProjects.reduce((sum, p) => sum + (p.totalLoggedHours || 0), 0);
const totalDedicatedHours = clientProjects.reduce((sum, p) => sum + (p.dedicatedHours || 0), 0); const totalDedicatedHours = clientProjects.reduce((sum, p) => sum + (p.dedicatedHours || 0), 0);
@ -324,22 +334,45 @@ async function client_dashboard(req, res) {
// Calculate total worked time across all VAs // Calculate total worked time across all VAs
const totalWorkedTimeAllVAs = allVAsFormatted.reduce((total, va) => total + (va.totalWorkedSeconds || 0), 0); const totalWorkedTimeAllVAs = allVAsFormatted.reduce((total, va) => total + (va.totalWorkedSeconds || 0), 0);
// Calculate projects with active workers
const projectsWithActiveWorkers = projectsFormatted.filter(p => p.vaCurrentlyWorking > 0).length;
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
data: { data: {
// Only send data that frontend actually uses // All assigned VAs with their status
allVAs: allVAsFormatted allVAs: {
active: currentlyActiveVAs,
inactive: inactiveVAs,
total: allVAsFormatted
},
// Currently working VAs (active session)
activeVAs: activeVAsFormatted,
// All client projects
projects: projectsFormatted
}, },
summary: { summary: {
totalVAs: allAssignedVAs.length, totalVAs: allAssignedVAs.length,
activeVAs: currentlyActiveVAs.length, activeVAs: currentlyActiveVAs.length,
inactiveVAs: inactiveVAs.length, inactiveVAs: inactiveVAs.length,
totalProjects: clientProjects.length, totalProjects: clientProjects.length,
projectsWithActiveWorkers: projectsWithActiveWorkers,
totalHoursLogged: totalHoursLogged, totalHoursLogged: totalHoursLogged,
totalDedicatedHours: totalDedicatedHours, totalDedicatedHours: totalDedicatedHours,
totalWorkedTime: formatTime(totalWorkedTimeAllVAs), totalWorkedTime: formatTime(totalWorkedTimeAllVAs),
utilizationRate: totalDedicatedHours > 0 ? utilizationRate: totalDedicatedHours > 0 ?
((totalHoursLogged / totalDedicatedHours) * 100).toFixed(2) + '%' : '0%' ((totalHoursLogged / totalDedicatedHours) * 100).toFixed(2) + '%' : '0%',
lastUpdated: currentTime.toISOString()
},
counts: {
vas: {
total: allAssignedVAs.length,
active: currentlyActiveVAs.length,
inactive: inactiveVAs.length
},
projects: clientProjects.length,
activeSessions: activeVAsFormatted.length,
projectsWithActivity: projectsWithActiveWorkers
} }
}); });
@ -353,7 +386,7 @@ async function client_dashboard(req, res) {
} }
} }
// Helper function to format time // Helper function to format time (same as in va_log_work_report_callback)
function formatTime(seconds) { function formatTime(seconds) {
const h = Math.floor(seconds / 3600); const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60); const m = Math.floor((seconds % 3600) / 60);

View File

@ -1,7 +1,6 @@
import VaClientRepository from "../../repository/vaClient.repository.js"; import VaClientRepository from "../../repository/vaClient.repository.js";
import VaClientService from "../../services/vaClient.service.js"; import VaClientService from "../../services/vaClient.service.js";
import logger from "../../config/logger.js"; import logger from "../../config/logger.js";
import jwt from "jsonwebtoken";
const vaClientService = new VaClientService(new VaClientRepository()); const vaClientService = new VaClientService(new VaClientRepository());
@ -52,9 +51,9 @@ async function va_register_client(req, res) {
} }
// Additional controller methods can be added here // Additional controller methods can be added here
async function get_client_profile(req, res) { async function get_client_by_email(req, res) {
try { try {
const email = req.body.clientEmail; const { email } = req.params;
const client = await vaClientService.getClientByEmail(email); const client = await vaClientService.getClientByEmail(email);
if (!client) { if (!client) {
@ -77,233 +76,7 @@ async function get_client_profile(req, res) {
} }
} }
async function delete_client(req, res) {
logger.info("ClientController - Delete Client Hit ...");
try {
const { clientId } = req.params;
if (!clientId) {
return res.status(400).json({
success: false,
message: "Client ID is required"
});
}
const deletedClient = await vaClientService.deleteClient(clientId);
if (!deletedClient) {
return res.status(404).json({
success: false,
message: "Client not found"
});
}
logger.info(`ClientController - Client deleted successfully: ${clientId}`);
return res.status(200).json({
success: true,
message: "Client deleted successfully",
data: {
clientId: clientId,
clientName: deletedClient.userDetails?.name || 'Unknown Client',
deletedAt: new Date().toISOString()
}
});
} catch (error) {
logger.error(`ClientController - Delete error: ${error.message}`);
if (error.message === "Client not found") {
return res.status(404).json({
success: false,
message: error.message
});
}
return res.status(500).json({
success: false,
message: "Internal Server Error"
});
}
}
async function update_client(req, res) {
logger.info("ClientController - Update Client Hit ...");
try {
const { clientId } = req.params;
const payload = req.body || {};
const { userDetails } = payload;
if (!clientId) {
return res.status(400).json({
success: false,
message: "Client ID is required"
});
}
if (!userDetails) {
return res.status(400).json({
success: false,
message: "Missing userDetails in request body"
});
}
const updatedClient = await vaClientService.updateClient(clientId, payload);
logger.info(`ClientController - Client updated successfully: ${clientId}`);
return res.status(200).json({
success: true,
message: "Client updated successfully",
data: updatedClient
});
} catch (error) {
logger.error(`ClientController - Update error: ${error.message}`);
if (error.message === "Client not found") {
return res.status(404).json({
success: false,
message: error.message
});
}
if (error.message.includes("Email already exists")) {
return res.status(409).json({
success: false,
message: error.message
});
}
return res.status(500).json({
success: false,
message: "Internal Server Error"
});
}
}
async function get_client_dashboard_profile(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;
// Use the new method that returns only necessary fields
const client = await vaClientService.getClientProfileByEmail(clientEmail);
if (!client) {
return res.status(404).json({
success: false,
message: "Client not found"
});
}
// Format the response to only include userDetails
const profileData = {
name: client.userDetails?.name || '',
email: client.userDetails?.email || '',
address: client.userDetails?.address || '',
city: client.userDetails?.city || '',
state: client.userDetails?.state || '',
zip: client.userDetails?.zip || '',
position: client.userDetails?.position || ''
};
return res.status(200).json({
success: true,
data: profileData
});
} catch (error) {
logger.error(`ClientController - Get dashboard profile error: ${error.message}`);
return res.status(500).json({
success: false,
message: "Internal Server Error"
});
}
}
async function update_client_password(req, res) {
logger.info("ClientController - Update Client Password Hit ...");
console.log("Update Client Password - Request Body:", req.body);
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 payload = req.body || {};
const { userDetails } = payload;
// Get client by email to get the ID
const client = await vaClientService.getClientByEmail(clientEmail);
if (!client) {
return res.status(404).json({
success: false,
message: "Client not found"
});
}
const clientId = client._id;
// Call service to update password
const result = await vaClientService.updateClientPassword(clientId, userDetails);
logger.info(`ClientController - Client password updated successfully: ${clientEmail}`);
return res.status(200).json({
success: true,
message: "Password updated successfully",
data: result
});
} catch (error) {
logger.error(`ClientController - Update password error: ${error.message}`);
if (error.message === "Client not found") {
return res.status(404).json({
success: false,
message: error.message
});
}
if (error.message.includes("Current password is incorrect") ||
error.message.includes("New passwords do not match") ||
error.message.includes("All password fields are required")) {
return res.status(400).json({
success: false,
message: error.message
});
}
return res.status(500).json({
success: false,
message: "Internal Server Error"
});
}
}
export { export {
va_register_client, va_register_client,
get_client_profile, get_client_by_email
delete_client,
update_client,
get_client_dashboard_profile,
update_client_password
}; };

View File

@ -1,97 +0,0 @@
import { ProjectModel, VAModel, ClientModel } from "../../models/vaModel.js";
import { Types } from "mongoose";
/**
* Delete project and all related data (without transaction)
*/
async function delete_project_sf(req, res) {
try {
const { projectId } = req.params;
// Validate projectId
if (!projectId) {
return res.status(400).json({
success: false,
message: "Project ID is required"
});
}
let projectObjectId;
try {
projectObjectId = new Types.ObjectId(projectId);
} catch (error) {
return res.status(400).json({
success: false,
message: "Invalid Project ID format"
});
}
// Check if project exists
const existingProject = await ProjectModel.findById(projectObjectId);
if (!existingProject) {
return res.status(404).json({
success: false,
message: "Project not found"
});
}
const clientId = existingProject.clientId;
const vaAssigned = existingProject.vaAssigned || [];
const projectName = existingProject.name;
console.log(`Deleting project: ${projectName} (${projectId})`);
const deletionSummary = {};
// 1. Remove project from Client's projectIds array
if (clientId) {
await ClientModel.findByIdAndUpdate(
clientId,
{ $pull: { projectIds: projectObjectId } }
);
deletionSummary.clientUpdated = true;
console.log(`Removed project from client`);
}
// 2. Remove project from all assigned VAs' projectIds arrays
if (vaAssigned.length > 0) {
const vaResult = await VAModel.updateMany(
{ _id: { $in: vaAssigned } },
{ $pull: { projectIds: projectObjectId } }
);
deletionSummary.vasUpdated = vaResult.modifiedCount;
console.log(`Removed project from ${deletionSummary.vasUpdated} VAs`);
} else {
deletionSummary.vasUpdated = 0;
}
// 6. Finally delete the project
await ProjectModel.findByIdAndDelete(projectObjectId);
console.log(`✅ Successfully deleted project: ${projectName} (${projectId})`);
res.status(200).json({
success: true,
message: 'Project and all related data deleted successfully',
data: {
projectId: projectId,
projectName: projectName,
clientId: clientId,
deletedAt: new Date().toISOString()
}
});
} catch (error) {
console.error("delete_project_sf error:", error);
return res.status(500).json({
success: false,
message: "Failed to delete project",
error: process.env.NODE_ENV === 'production'
? 'Internal server error'
: error.message
});
}
}
export { delete_project_sf };

View File

@ -5,7 +5,7 @@ import { updateEntityRelationships } from "../../helper/updateEntityRelationship
async function project_add_sf(req, res) { async function project_add_sf(req, res) {
try { try {
const payload = req.body|| {}; const payload = req.body|| {};
const { name, hourlyRate, clientId, vaAssigned, dedicatedHours, vaWorkingScheduleStart, vaWorkingScheduleEnd, isFlexibleAllowedPt, vaWorkingDelayAllowedStart, vaWorkingDelayAllowedEnd } = payload; const { name, hourlyRate, clientId, vaAssigned, dedicatedHours } = payload;
// Validate required fields // Validate required fields
if (!name) { if (!name) {
@ -65,11 +65,6 @@ async function project_add_sf(req, res) {
clientId: clientObjectId, clientId: clientObjectId,
vaAssigned: vaAssignedArray, vaAssigned: vaAssignedArray,
dedicatedHours: dedicatedHours ? parseFloat(dedicatedHours) : 0, dedicatedHours: dedicatedHours ? parseFloat(dedicatedHours) : 0,
vaWorkingScheduleStart: vaWorkingScheduleStart || null,
vaWorkingScheduleEnd: vaWorkingScheduleEnd || null,
isFlexibleAllowedPt: isFlexibleAllowedPt || true,
vaWorkingDelayAllowedStart: vaWorkingDelayAllowedStart || null,
vaWorkingDelayAllowedEnd: vaWorkingDelayAllowedEnd || null
}; };
const created = await ProjectModel.create(toCreate); const created = await ProjectModel.create(toCreate);
@ -86,8 +81,36 @@ async function project_add_sf(req, res) {
console.warn('Project created but relationship update failed:', relationshipUpdate.message); console.warn('Project created but relationship update failed:', relationshipUpdate.message);
} }
res.status(201).json({ success: true, data: created, relationships: relationshipUpdate }); // Send data to Salesforce API after successful registration
try {
// const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/`;
// const response = await fetch(salesforceUrl, {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json',
// },
// body: JSON.stringify(created)
// });
// 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, data: created, SyncSalesforce: result });
res.status(201).json({ success: true, data: created, relationships: relationshipUpdate });
} catch (error) {
console.error("❌ Failed to sync with Salesforce:", error.message);
// Create a custom error to distinguish Salesforce errors
const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
salesforceError.isSalesforceError = true;
throw salesforceError;
}
} catch (error) { } catch (error) {
console.error("project_add_sf error:", error); console.error("project_add_sf 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

@ -16,12 +16,7 @@ async function update_project_sf(req, res) {
vaAssigned, vaAssigned,
dedicatedHours, dedicatedHours,
status, status,
totalLoggedHours , totalLoggedHours
vaWorkingScheduleStart,
vaWorkingScheduleEnd,
isFlexibleAllowedPt,
vaWorkingDelayAllowedStart,
vaWorkingDelayAllowedEnd
} = payload; } = payload;
// Validate projectId // Validate projectId
@ -93,27 +88,6 @@ async function update_project_sf(req, res) {
updateData.status = status; updateData.status = status;
} }
// Update VA working schedule if provided
if (vaWorkingScheduleStart !== undefined) {
updateData.vaWorkingScheduleStart = vaWorkingScheduleStart || null;
}
if (vaWorkingScheduleEnd !== undefined) {
updateData.vaWorkingScheduleEnd = vaWorkingScheduleEnd || null;
}
if (isFlexibleAllowedPt !== undefined) {
updateData.isFlexibleAllowedPt = isFlexibleAllowedPt;
}
if (vaWorkingDelayAllowedStart !== undefined) {
updateData.vaWorkingDelayAllowedStart = vaWorkingDelayAllowedStart || null;
}
if (vaWorkingDelayAllowedEnd !== undefined) {
updateData.vaWorkingDelayAllowedEnd = vaWorkingDelayAllowedEnd || null;
}
// Handle VA assignments - this requires relationship updates // Handle VA assignments - this requires relationship updates
if (vaAssigned !== undefined) { if (vaAssigned !== undefined) {
let newVaAssignedArray = []; let newVaAssignedArray = [];

View File

@ -131,7 +131,6 @@ async function va_leave_apply_function(req, res) {
}); });
} }
const processedComment = processHTMLComment(comment);
// Create leave request // Create leave request
const leaveRequest = new LeavesModel({ const leaveRequest = new LeavesModel({
vaId, vaId,
@ -139,62 +138,62 @@ async function va_leave_apply_function(req, res) {
dateTo: endDate, dateTo: endDate,
days: totalDays.toString(), days: totalDays.toString(),
leaveType, leaveType,
comment: processedComment.processedHTML, comment,
attachments: processedComment.extractedImages,
status: "PENDING" status: "PENDING"
}); });
await leaveRequest.save(); await leaveRequest.save();
try { try {
// const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/timesheet-adjustment`; const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/timesheet-adjustment`;
// const sfData = { const sfData = {
// "id": leaveRequest._id, "id": adjustmentRequest._id,
// "va": { "va": {
// "id": vaId "id": vaId
// }, },
// "dateFrom": startDate.toISOString().split('T')[0], "project": {
// "dateTo": endDate.toISOString().split('T')[0], "id": projectId
// "totalDays": totalDays, },
// "leaveType": leaveType, "date": adjustmentDate.toISOString().split('T')[0],
// "comment": processedComment.plainText, // Send plain text to Salesforce "loginTime": loginDateTime.toISOString(),
// "attachments": processedComment.extractedImages, // Send extracted images info "logoutTime": logoutDateTime.toISOString(),
// "status": "PENDING", "comment": comment,
// "rejectionReason": "" "status": "PENDING",
// } "rejectionReason": "",
"adjustedHours": formatTime(adjustedSeconds)
}
// const response = await fetch(salesforceUrl, { const response = await fetch(salesforceUrl, {
// method: 'POST', method: 'POST',
// headers: { headers: {
// 'Content-Type': 'application/json', 'Content-Type': 'application/json',
// // Add any required headers for Salesforce authentication // Add any required headers for Salesforce authentication
// // 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN // 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
// }, },
// body: JSON.stringify({"data": sfData}) body: JSON.stringify({"data": sfData})
// }); });
// if (!response.ok) { if (!response.ok) {
// const errorText = await response.text(); const errorText = await response.text();
// throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`); throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
// } }
// const result = await response.json(); const result = await response.json();
// // console.log("✅ Successfully synced VA data to Salesforce:", result); // console.log("✅ Successfully synced VA data to Salesforce:", result);
return res.status(201).json({ return res.status(201).json({
success: true, success: true,
message: "Leave request submitted successfully", message: "Adjustment request submitted successfully",
data: { data: {
leaveId: leaveRequest._id, adjustmentId: adjustmentRequest._id,
dateFrom: startDate.toISOString().split('T')[0], date: adjustmentDate.toISOString().split('T')[0],
dateTo: endDate.toISOString().split('T')[0], loginTime: loginDateTime.toISOString(),
totalDays: totalDays, logoutTime: logoutDateTime.toISOString(),
leaveType, adjustedHours: formatTime(adjustedSeconds),
comment: processedComment.processedHTML, comment: comment,
// attachments: processedComment.extractedImages,
status: "PENDING" status: "PENDING"
}, },
// SyncSalesforce: result SyncSalesforce: result
}); });
} catch (error) { } catch (error) {
console.error("Failed to sync with Salesforce:", error.message); console.error("Failed to sync with Salesforce:", error.message);
@ -239,116 +238,4 @@ async function va_leave_apply_function(req, res) {
} }
} }
function processHTMLComment(htmlComment) {
try {
let processedHTML = htmlComment;
const extractedImages = [];
let plainText = '';
// Extract plain text from HTML for Salesforce
plainText = extractPlainTextFromHTML(htmlComment);
// Process base64 images in HTML
const imageProcessingResult = extractAndProcessBase64Images(htmlComment);
processedHTML = imageProcessingResult.processedHTML;
extractedImages.push(...imageProcessingResult.extractedImages);
return {
processedHTML,
plainText,
extractedImages
};
} catch (error) {
console.error("Error processing HTML comment:", error);
// Return original content if processing fails
return {
processedHTML: htmlComment,
plainText: extractPlainTextFromHTML(htmlComment),
extractedImages: []
};
}
}
// Helper function to extract plain text from HTML
function extractPlainTextFromHTML(html) {
try {
// Remove HTML tags and decode HTML entities
let plainText = html
.replace(/<[^>]*>/g, ' ') // Replace tags with spaces
.replace(/\s+/g, ' ') // Collapse multiple spaces
.trim();
// Decode common HTML entities
plainText = plainText
.replace(/&nbsp;/g, ' ')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.replace(/&#x27;/g, "'")
.replace(/&#x2F;/g, '/');
return plainText;
} catch (error) {
console.error("Error extracting plain text from HTML:", error);
return html.replace(/<[^>]*>/g, ' ').trim();
}
}
// Helper function to extract and process base64 images from HTML
function extractAndProcessBase64Images(html) {
const extractedImages = [];
let processedHTML = html;
try {
// Regex to find base64 images in HTML (various formats)
const base64Regex = /<img[^>]+src="data:image\/([a-zA-Z0-9+]+);base64,([^"]+)"[^>]*>/gi;
let match;
let imageIndex = 0;
while ((match = base64Regex.exec(html)) !== null) {
const fullMatch = match[0];
const imageType = match[1]; // png, jpeg, gif, etc.
const base64Data = match[2];
// Create a unique filename
const timestamp = Date.now();
const filename = `leave_image_${timestamp}_${imageIndex}.${imageType}`;
// Calculate approximate file size
const size = Math.floor((base64Data.length * 3) / 4); // Base64 size calculation
// Store image information
extractedImages.push({
filename: filename,
mimeType: `image/${imageType}`,
base64Data: base64Data,
size: size,
originalHTML: fullMatch, // Store original img tag for reference
index: imageIndex
});
// Replace base64 with a reference in HTML
const replacementHTML = `<img src="attachment://${filename}" alt="Leave evidence ${imageIndex + 1}" data-filename="${filename}" data-image-index="${imageIndex}" class="leave-image">`;
processedHTML = processedHTML.replace(fullMatch, replacementHTML);
imageIndex++;
}
return {
processedHTML,
extractedImages
};
} catch (error) {
console.error("Error processing base64 images:", error);
return {
processedHTML: html,
extractedImages: []
};
}
}
export { va_leave_apply_function }; export { va_leave_apply_function };

View File

@ -147,7 +147,7 @@ async function va_log_track_work_callback(req, res) {
const vaId = vaUser._id; const vaId = vaUser._id;
const payload = req.body || {}; const payload = req.body || {};
const { projectId, logId, title, appName, url = null } = payload; const { projectId, logId, title, appName } = payload;
const project = await ProjectModel.findById(projectId); const project = await ProjectModel.findById(projectId);
if (!project) return res.status(400).json({ success: false, message: "Project not found" }); if (!project) return res.status(400).json({ success: false, message: "Project not found" });
@ -190,7 +190,6 @@ async function va_log_track_work_callback(req, res) {
logId, logId,
title, title,
appName, appName,
url,
from: Date.now(), from: Date.now(),
}); });

View File

@ -1,46 +0,0 @@
import { VAModel } from "../../../models/vaModel.js";
async function va_payslip_data_function(req, res) {
try {
const vaId = req.params.vaId;
const vaUser = await VAModel.findById(vaId);
if (!vaUser) {
return res.status(404).json({ success: false, message: "VA not found" });
}
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-payslip?vaId=${vaId}`;
const response = await fetch(salesforceUrl, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
// Add any required headers for Salesforce authentication
// 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
},
});
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,
data: result.data
});
} 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;
}
}
export { va_payslip_data_function };

View File

@ -1,74 +0,0 @@
async function va_payroll_schedule_function(req, res) {
try {
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/payroll-schedule`;
const response = await fetch(salesforceUrl, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
// Add any required headers for Salesforce authentication
// 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
},
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
}
const result = await response.json();
// Get current date
const currentDate = new Date();
// Sort all data by ScheduleDate in ascending order
const sortedData = [...result.data].sort((a, b) => {
return new Date(a.ScheduleDate) - new Date(b.ScheduleDate);
});
// Find the index of current schedule (where current date falls between periodStart and periodEnd)
let currentIndex = -1;
for (let i = 0; i < sortedData.length; i++) {
const item = sortedData[i];
const periodStart = new Date(item.periodStart);
const periodEnd = new Date(item.periodEnd);
if (currentDate >= periodStart && currentDate <= periodEnd) {
currentIndex = i;
break;
}
}
// If no current schedule found, use the schedule that just passed or is upcoming
if (currentIndex === -1) {
// Find the first schedule where periodEnd is in future
currentIndex = sortedData.findIndex(item => new Date(item.periodEnd) >= currentDate);
// If all are in past, use the last one
if (currentIndex === -1) {
currentIndex = sortedData.length - 1;
}
}
// Calculate the start index (2nd last schedule from current)
const startIndex = Math.max(0, currentIndex - 2);
// Get the filtered data from 2nd last to all future schedules
const filteredData = sortedData.slice(startIndex);
res.status(201).json({
success: true,
data: filteredData
});
} 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;
}
}
export { va_payroll_schedule_function };

View File

@ -1,92 +0,0 @@
import { VAModel, ClientModel, ProjectModel, LoggedModel, WorkTrackModel } from "../../../models/vaModel.js";
// Delete VA and all related data
async function va_remove_function(req, res) {
try {
const vaId = req.params.id;
// Validate VA ID
if (!vaId) {
return res.status(400).json({
success: false,
message: "VA ID is required"
});
}
// Find the VA to get clientIds and projectIds before deletion
const vaUser = await VAModel.findById(vaId);
if (!vaUser) {
return res.status(404).json({
success: false,
message: "VA not found"
});
}
const clientIds = vaUser.clientIds || [];
const projectIds = vaUser.projectIds || [];
const vaEmail = vaUser.userDetails?.email;
console.log(`Deleting VA ${vaId} - Affected: ${clientIds.length} clients, ${projectIds.length} projects`);
// 1. Remove VA from all Clients
let clientsUpdated = 0;
if (clientIds.length > 0) {
const clientResult = await ClientModel.updateMany(
{ _id: { $in: clientIds } },
{ $pull: { vaIds: vaId } }
);
clientsUpdated = clientResult.modifiedCount;
console.log(`Removed VA from ${clientsUpdated} clients`);
}
// 2. Remove VA from all Projects
let projectsUpdated = 0;
if (projectIds.length > 0) {
const projectResult = await ProjectModel.updateMany(
{ _id: { $in: projectIds } },
{ $pull: { vaAssigned: vaId } }
);
projectsUpdated = projectResult.modifiedCount;
console.log(`Removed VA from ${projectsUpdated} projects`);
}
// 3. Delete all WorkTrack records for this VA
const workTrackResult = await WorkTrackModel.deleteMany({ vaId: vaId });
console.log(`Deleted ${workTrackResult.deletedCount} work track records`);
// 4. Delete all Logged records for this VA
const loggedResult = await LoggedModel.deleteMany({ vaId: vaId });
console.log(`Deleted ${loggedResult.deletedCount} log records`);
// 5. Finally delete the VA
await VAModel.findByIdAndDelete(vaId);
console.log(`Successfully deleted VA: ${vaId} and all related data`);
res.status(200).json({
success: true,
message: 'Virtual Assistant and all related data deleted successfully',
data: {
vaId: vaId,
vaName: `${vaUser.userDetails?.firstName || ''} ${vaUser.userDetails?.lastName || ''}`.trim() || 'Unknown VA',
vaEmail: vaEmail,
clientsUpdated: clientsUpdated,
projectsUpdated: projectsUpdated,
workTracksDeleted: workTrackResult.deletedCount,
logsDeleted: loggedResult.deletedCount,
deletedAt: new Date().toISOString()
}
});
} catch (error) {
console.error('Error deleting VA:', error);
res.status(500).json({
success: false,
message: 'Failed to delete Virtual Assistant',
error: 'Internal server error'
});
}
}
export { va_remove_function };

View File

@ -5,96 +5,52 @@ import { VAModel } from "../../../models/vaModel.js";
// Register a new VA // Register a new VA
async function va_profile_update_callback(req, res) { async function va_profile_update_callback(req, res) {
try { try {
const sf_token = req.headers.sf_token;
const accessToken = req.headers.token; const accessToken = req.headers.token;
let vaUser;
let authSource = 'regular';
if(sf_token) {
try {
const sfDecoded = jwt.verify(sf_token, process.env.JWT_SECRET_SF_TOKEN);
if (!sfDecoded?.email && !sfDecoded?.portalId) {
return res.status(400).json({ success: false, message: "Invalid Salesforce token payload" });
}
// Find user by email or VA ID from Salesforce token
if (sfDecoded.portalId) {
vaUser = await VAModel.findById(sfDecoded.portalId);
} else if (sfDecoded.email) {
vaUser = await VAModel.findOne({ "userDetails.email": sfDecoded.email });
}
if (!vaUser) {
return res.status(404).json({ success: false, message: "VA not found with Salesforce token" });
}
authSource = 'salesforce';
} catch (sfError) {
return res.status(401).json({ success: false, message: "Invalid or expired Salesforce token" });
}
} else {
try {
if (!accessToken) { if (!accessToken) {
return res.status(401).json({ success: false, message: "Missing access token" }); return res.status(401).json({ success: false, message: "Missing access token" });
} }
const decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN); let decoded;
try {
decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN);
} catch (err) {
return res.status(401).json({ success: false, message: "Invalid or expired token" });
}
if (!decoded?.email) { if (!decoded?.email) {
return res.status(400).json({ success: false, message: "Invalid token payload" }); return res.status(400).json({ success: false, message: "Invalid token payload" });
} }
vaUser = await VAModel.findOne({ "userDetails.email": decoded.email }); const vaUser = await VAModel.findOne({ "userDetails.email": decoded.email });
if (!vaUser) { if (!vaUser) {
return res.status(404).json({ success: false, message: "VA not found" }); return res.status(404).json({ success: false, message: "VA not found" });
} }
authSource = 'regular';
} catch (tokenError) {
return res.status(401).json({ success: false, message: "Invalid or expired access token" });
}
}
const payload = req.body || {}; const payload = req.body || {};
const { userDetails, status } = payload; const { userDetails } = payload;
if (!userDetails) { if (!userDetails) {
return res.status(400).json({ success: false, message: "Missing userDetails" }); return res.status(400).json({ success: false, message: "Missing userDetails" });
} }
const allowedFields = [ // Allowed fields to update
"firstName", "lastName", "dob", "age", "address", // const allowedFields = [
"city", "state", "country", "phone", "skype", // "firstName", "lastName", "dob", "age", "address",
"paymentMethod", "payoneerAccount", "veemAccount" // "bankName", "accountHolderName", "accountNumber", "routingNumber"
]; // ];
let hasUpdates = false; // // Update allowed fields only
const updatedFields = []; // allowedFields.forEach(field => {
// if (userDetails[field] !== undefined) {
// Update allowed fields if provided // vaUser.userDetails[field] = userDetails[field];
allowedFields.forEach(field => { // }
if (userDetails[field] !== undefined && userDetails[field] !== null && userDetails[field] !== '') { // });
vaUser.userDetails[field] = userDetails[field];
hasUpdates = true;
updatedFields.push(field);
}
});
/* ---------------------- PASSWORD UPDATE ---------------------- */ /* ---------------------- PASSWORD UPDATE ---------------------- */
let passwordUpdated = false;
if (userDetails.password || userDetails.cPassword || userDetails.oldPassword) { if (userDetails.password || userDetails.cPassword || userDetails.oldPassword) {
const { oldPassword, password, cPassword } = userDetails; const { oldPassword, password, cPassword } = userDetails;
if (authSource === 'salesforce') {
return res.status(403).json({
success: false,
message: "Password changes require regular authentication",
});
}
if (!oldPassword || !password || !cPassword) { if (!oldPassword || !password || !cPassword) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
@ -122,23 +78,13 @@ async function va_profile_update_callback(req, res) {
const saltRounds = 10; const saltRounds = 10;
const hashedPassword = await bcrypt.hash(password, saltRounds); const hashedPassword = await bcrypt.hash(password, saltRounds);
vaUser.userDetails.password = hashedPassword; vaUser.userDetails.password = hashedPassword;
hasUpdates = true; } else {
passwordUpdated = true;
updatedFields.push('password');
}
if (!hasUpdates) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: "No valid fields to update", message: "Data not found",
}); });
} }
if (status) {
vaUser.status = status;
updatedFields.push('status');
}
await vaUser.save(); await vaUser.save();
const updatedUser = await VAModel.findById(vaUser._id) const updatedUser = await VAModel.findById(vaUser._id)
@ -149,7 +95,6 @@ async function va_profile_update_callback(req, res) {
success: true, success: true,
message: "Profile updated successfully", message: "Profile updated successfully",
data: updatedUser, data: updatedUser,
updatedFields: updatedFields,
}); });
} catch (error) { } catch (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

@ -36,7 +36,6 @@ async function va_register(req, res) {
paymentMethod: userDetails.paymentMethod || null, paymentMethod: userDetails.paymentMethod || null,
payoneerAccount: userDetails.payoneerAccount || null, payoneerAccount: userDetails.payoneerAccount || null,
veemAccount: userDetails.veemAccount || null, veemAccount: userDetails.veemAccount || null,
wiseEmail: userDetails.wiseEmail || null,
phone: userDetails.phone || null, phone: userDetails.phone || null,
skype: userDetails.skype || null, skype: userDetails.skype || null,
}, },

View File

@ -1,12 +1,12 @@
import { LoggedModel, WorkTrackModel, ProjectModel, VAModel } from "../../../models/vaModel.js"; import { LoggedModel, WorkTrackModel, ProjectModel } from "../../../models/vaModel.js";
// Helper function to get start and end of day in UTC // Helper function to get start and end of day
function getDayRange(date = new Date()) { function getDayRange(date = new Date()) {
const start = new Date(date); const start = new Date(date);
start.setUTCHours(0, 0, 0, 0); start.setHours(0, 0, 0, 0);
const end = new Date(date); const end = new Date(date);
end.setUTCHours(23, 59, 59, 999); end.setHours(23, 59, 59, 999);
return { start, end }; return { start, end };
} }
@ -25,130 +25,10 @@ function formatHoursToReadable(hours) {
} }
} }
// Helper function to parse ISO time string
function parseISOTimeString(timeString) {
if (!timeString) return new Date();
try {
// Parse ISO string directly
const parsedDate = new Date(timeString);
// Check if the date is valid
if (isNaN(parsedDate.getTime())) {
throw new Error('Invalid date');
}
parsedDate.setMilliseconds(0);
console.log("pasdes date" + parsedDate)
return parsedDate;
} catch (error) {
console.error('Error parsing ISO time string:', error);
return new Date();
}
}
// Helper function to check if time difference is less than threshold
function isTimeWithinThreshold(receivedTime, currentTime, thresholdSeconds = 10) {
const diffMs = Math.abs(currentTime - receivedTime);
const diffSeconds = diffMs / 1000;
console.log(`Time comparison - Received: ${receivedTime.toISOString()}, Current: ${currentTime.toISOString()}, Diff: ${diffSeconds}s`);
return diffSeconds <= thresholdSeconds;
}
// Helper function to get current UTC time
function getCurrentUTCTime() {
const now = new Date();
now.setMilliseconds(0);
return now;
}
// function parseTimeToMinutes(timeString) {
// if (!timeString) return null;
// const [hours, minutes] = timeString.split(':').map(Number);
// return hours * 60 + (minutes || 0);
// }
// function parseDelayToMinutes(delayString) {
// if (!delayString) return 0;
// if (delayString.includes('h')) {
// const hours = parseInt(delayString.replace('h', '').trim());
// return hours * 60;
// } else if (delayString.includes('m')) {
// return parseInt(delayString.replace('m', '').trim());
// }
// return parseInt(delayString) || 0; // Default to minutes if no unit specified
// }
// function checkScheduleCompliance(currentTime, project, vaStatus, type) {
// const warnings = [];
// // If project doesn't have schedule or VA is PARTTIME with flexible allowed, no schedule restrictions
// if (!project.vaWorkingScheduleStart || !project.vaWorkingScheduleEnd) {
// return { isCompliant: true, warnings };
// }
// const currentDate = new Date(currentTime);
// const currentMinutes = currentDate.getUTCHours() * 60 + currentDate.getUTCMinutes();
// const scheduleStartMinutes = parseTimeToMinutes(project.vaWorkingScheduleStart);
// const scheduleEndMinutes = parseTimeToMinutes(project.vaWorkingScheduleEnd);
// const allowedStartDelay = parseDelayToMinutes(project.vaWorkingDelayAllowedStart);
// const allowedEndDelay = parseDelayToMinutes(project.vaWorkingDelayAllowedEnd);
// // Calculate allowed time ranges
// const allowedStartMinutes = scheduleStartMinutes - allowedStartDelay;
// const allowedEndMinutes = scheduleEndMinutes + allowedEndDelay;
// console.log(`Schedule Check - Current: ${currentMinutes}, Start: ${scheduleStartMinutes}, End: ${scheduleEndMinutes}, Allowed Start: ${allowedStartMinutes}, Allowed End: ${allowedEndMinutes}`);
// // Check for PARTTIME with flexible schedule
// if (vaStatus === "PARTTIME" && project.isFlexibleAllowedPt) {
// return { isCompliant: true, warnings };
// }
// // For all other cases, check schedule compliance
// if (type === "LOGIN") {
// // Check if login is too early
// if (currentMinutes < allowedStartMinutes) {
// warnings.push("early_login");
// }
// // Check if login is after shift end
// if (currentMinutes > scheduleEndMinutes) {
// warnings.push("working_after_shift_end");
// }
// // Check if login is within allowed range
// // const isWithinAllowedRange = currentMinutes >= allowedStartMinutes && currentMinutes <= allowedEndMinutes;
// return {
// isCompliant: true, // Allow login regardless of time
// warnings
// };
// } else if (type === "LOGOUT") {
// // Check if logout is too late
// if (currentMinutes > allowedEndMinutes) {
// warnings.push("late_logout");
// }
// // For logout, we're generally more permissive - allow logout even if late
// return {
// isCompliant: true, // Allow logout regardless of time
// warnings
// };
// }
// return { isCompliant: true, warnings };
// }
// function to close open work tracks and create logout // function to close open work tracks and create logout
async function closePreviousSession(vaId, lastLog, logTime = new Date()) { async function closePreviousSession(vaId, lastLog) {
try { try {
const currentTime = logTime; const currentTime = new Date();
// Find all open work tracks for this VA and project (where to is not set or totalTime is 0) // Find all open work tracks for this VA and project (where to is not set or totalTime is 0)
const openWorkTracks = await WorkTrackModel.find({ const openWorkTracks = await WorkTrackModel.find({
@ -240,58 +120,18 @@ async function closePreviousSession(vaId, lastLog, logTime = new Date()) {
async function va_project_log_callback(req, res) { async function va_project_log_callback(req, res) {
try { try {
const payload = req.body || {}; const payload = req.body || {};
const { vaId, vaEmail, projectId, type, time: receivedTimeString } = payload; const { vaId, vaEmail, projectId, type } = payload;
// console.log("Received payload:", payload);
if (!vaId || !vaEmail || !projectId || !type) { if (!vaId, !vaEmail, !projectId, !type) return res.status(400).send("Missing Props");
return res.status(400).send("Missing required properties");
}
// Determine the log time based on received time (both in UTC)
const currentTime = getCurrentUTCTime(); // Current UTC time
let logTime = currentTime;
if (receivedTimeString) {
// Parse the received ISO time string
const receivedTime = parseISOTimeString(receivedTimeString);
// Check if the difference is less than 10 seconds
if (isTimeWithinThreshold(receivedTime, currentTime, 10)) {
logTime = receivedTime;
console.log(`✅ Using received ISO time: ${receivedTimeString}, difference is within 10 seconds`);
console.log(`✅ Parsed as: ${receivedTime.toISOString()}`);
} else {
logTime = currentTime;
console.log(`⚠️ Using current UTC time, received time difference exceeds 10 seconds.`);
console.log(`Received: ${receivedTimeString} (parsed: ${receivedTime.toISOString()})`);
console.log(`Current: ${currentTime.toISOString()}`);
}
}
const project = await ProjectModel.findById(projectId); const project = await ProjectModel.findById(projectId);
if (!project) { if (!project) {
return res.status(400).json({ success: false, message: "Project not found" }); return res.status(400).json({ success: false, message: "Project not found" });
} }
const vaUser = await VAModel.findById(vaId); // For LOGIN requests, check daily hours
if (!vaUser) {
return res.status(400).json({ success: false, message: "VA not found" });
}
// Check schedule compliance
// const scheduleCheck = checkScheduleCompliance(logTime, project, vaUser.status, type);
// console.log(`Schedule compliance check:`, scheduleCheck);
// if (!scheduleCheck.isCompliant) {
// return res.status(400).json({
// success: false,
// message: `Cannot ${type.toLowerCase()} - outside of allowed schedule time`,
// warnings: scheduleCheck.warnings
// });
// }
// For LOGIN requests, check daily hours (using UTC day range)
if (type === "LOGIN") { if (type === "LOGIN") {
const { start, end } = getDayRange(); // UTC day range const { start, end } = getDayRange();
// Get today's total hours // Get today's total hours
const todayWorkEntries = await WorkTrackModel.find({ const todayWorkEntries = await WorkTrackModel.find({
vaId, vaId,
@ -309,8 +149,7 @@ async function va_project_log_callback(req, res) {
if (project.dedicatedHours > 0 && totalTodayHours >= project.dedicatedHours) { if (project.dedicatedHours > 0 && totalTodayHours >= project.dedicatedHours) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: `Cannot login - daily hours already completed (${formatHoursToReadable(totalTodayHours)} / ${formatHoursToReadable(project.dedicatedHours)})`, message: `Cannot login - daily hours already completed (${formatHoursToReadable(totalTodayHours)} / ${formatHoursToReadable(project.dedicatedHours)})`
// warnings: scheduleCheck.warnings
}); });
} }
@ -324,7 +163,7 @@ async function va_project_log_callback(req, res) {
// check last workModel login time and make a logout request then create login // check last workModel login time and make a logout request then create login
// console.log(`User already logged in. Closing previous session...`); // console.log(`User already logged in. Closing previous session...`);
const closeResult = await closePreviousSession(vaId, lastLog, logTime); const closeResult = await closePreviousSession(vaId, lastLog);
// console.log(`Previous session closed: ${closeResult.closedWorkTracks} work tracks, logout log: ${closeResult.logoutLogId}`); // console.log(`Previous session closed: ${closeResult.closedWorkTracks} work tracks, logout log: ${closeResult.logoutLogId}`);
@ -333,72 +172,23 @@ async function va_project_log_callback(req, res) {
loggedType: "LOGIN", loggedType: "LOGIN",
vaId, vaId,
vaEmail, vaEmail,
projectId, projectId
time: logTime
}); });
try { return res.status(201).send({
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
const sfData = {
"_id": createdLog._id,
"loggedType": "LOGIN",
"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, success: true,
message: `${type} Successfully`, message: `Login Successful (previous session closed)`,
logId: createdLog._id, logId: createdLog._id,
SyncSalesforce: result,
previousSession: { previousSession: {
closedWorkTracks: closeResult.closedWorkTracks, closedWorkTracks: closeResult.closedWorkTracks,
logoutLogId: closeResult.logoutLogId logoutLogId: closeResult.logoutLogId
}, }
timeUsed: receivedTimeString ? 'received' : 'current',
actualTime: createdLog.time,
// warnings: scheduleCheck.warnings
}); });
} 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;
}
} }
} }
const createdLog = await LoggedModel.create({ const createdLog = await LoggedModel.create({ loggedType: type, vaId, vaEmail, projectId })
loggedType: type, // console.log(createdLog)
vaId,
vaEmail,
projectId,
time: logTime
});
try { try {
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`; const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
const sfData = { const sfData = {
@ -428,15 +218,7 @@ async function va_project_log_callback(req, res) {
const result = await response.json(); const result = await response.json();
// console.log("✅ Successfully synced VA data to Salesforce:", result); // console.log("✅ Successfully synced VA data to Salesforce:", result);
res.status(201).json({ res.status(201).json({ success: true, message: `${type} Successfully`, logId: createdLog._id, SyncSalesforce: result });
success: true,
message: `${type} Successfully`,
logId: createdLog._id,
SyncSalesforce: result,
timeUsed: receivedTimeString ? 'received' : 'current',
actualTime: createdLog.time,
// warnings: scheduleCheck.warnings
});
} catch (error) { } catch (error) {
console.error("Failed to sync with Salesforce:", error.message); console.error("Failed to sync with Salesforce:", error.message);
console.log(error) console.log(error)
@ -447,8 +229,9 @@ async function va_project_log_callback(req, res) {
throw salesforceError; throw salesforceError;
} }
// return res.status(201).send({ success: true, message: `${type} Successfully`, logId: createdLog._id });
} catch (error) { } catch (error) {
console.error("Error in va_project_log_callback:", error);
return res.status(500).send("Internal Server Error"); return res.status(500).send("Internal Server Error");
} }
} }

View File

@ -11,18 +11,6 @@ function formatHMS(ms) {
async function va_projects_report_function(req, res) { async function va_projects_report_function(req, res) {
try { try {
const sf_token = req.headers.sf_token;
let vaId;
let vaUser;
let vaEmail;
if(sf_token) {
vaId = req.body.vaId;
vaUser = await VAModel.findById(vaId);
if (!vaUser) {
return res.status(404).json({ success: false, message: "VA not found" });
}
vaEmail = vaUser.userDetails.email
} else {
const accessToken = req.headers.token; const accessToken = req.headers.token;
if (!accessToken) { if (!accessToken) {
return res.status(401).json({ success: false, message: "Missing token" }); return res.status(401).json({ success: false, message: "Missing token" });
@ -33,14 +21,13 @@ async function va_projects_report_function(req, res) {
return res.status(401).json({ success: false, message: "Invalid token" }); return res.status(401).json({ success: false, message: "Invalid token" });
} }
vaEmail = decoded.email; const vaEmail = decoded.email;
vaUser = await VAModel.findOne({ "userDetails.email": vaEmail }); const vaUser = await VAModel.findOne({ "userDetails.email": vaEmail });
if (!vaUser) { if (!vaUser) {
return res.status(404).json({ success: false, message: "VA not found" }); return res.status(404).json({ success: false, message: "VA not found" });
} }
vaId = vaUser._id; const vaId = vaUser._id;
}
const { projectId, from, to } = req.params; const { projectId, from, to } = req.params;
if (!projectId) { if (!projectId) {

View File

@ -59,6 +59,14 @@ async function va_timesheet_adjustment_function(req, res) {
const combineDateTime = (dateObj, timeStr) => { const combineDateTime = (dateObj, timeStr) => {
const [hours, minutes, seconds] = timeStr.split(':').map(Number); const [hours, minutes, seconds] = timeStr.split(':').map(Number);
// // Extract date components from the Date object
// const year = dateObj.getFullYear();
// const month = dateObj.getMonth();
// const day = dateObj.getDate();
// // Create UTC date directly
// return new Date(year, month, day, hours, minutes, seconds);
const combined = new Date(dateObj); const combined = new Date(dateObj);
combined.setHours(hours, minutes, seconds, 0); combined.setHours(hours, minutes, seconds, 0);
@ -112,16 +120,13 @@ async function va_timesheet_adjustment_function(req, res) {
}); });
} }
const processedComment = processHTMLComment(comment);
const adjustmentRequest = new AdjustedHoursModel({ const adjustmentRequest = new AdjustedHoursModel({
vaId, vaId,
projectId, projectId,
date: startOfDay, date: startOfDay,
loginTime: loginDateTime, loginTime: loginDateTime,
logoutTime: logoutDateTime, logoutTime: logoutDateTime,
comment: processedComment.processedHTML, comment,
attachments: processedComment.extractedImages,
status: "PENDING" status: "PENDING"
}); });
@ -148,8 +153,7 @@ async function va_timesheet_adjustment_function(req, res) {
"date": adjustmentDate.toISOString().split('T')[0], "date": adjustmentDate.toISOString().split('T')[0],
"loginTime": loginDateTime.toISOString(), "loginTime": loginDateTime.toISOString(),
"logoutTime": logoutDateTime.toISOString(), "logoutTime": logoutDateTime.toISOString(),
"comment": processedComment.plainText, // Send plain text to Salesforce "comment": comment,
"attachments": processedComment.extractedImages, // Send extracted images info
"status": "PENDING", "status": "PENDING",
"rejectionReason": "", "rejectionReason": "",
"adjustedHours": formatTime(adjustedSeconds) "adjustedHours": formatTime(adjustedSeconds)
@ -182,8 +186,7 @@ async function va_timesheet_adjustment_function(req, res) {
loginTime: loginDateTime.toISOString(), loginTime: loginDateTime.toISOString(),
logoutTime: logoutDateTime.toISOString(), logoutTime: logoutDateTime.toISOString(),
adjustedHours: formatTime(adjustedSeconds), adjustedHours: formatTime(adjustedSeconds),
comment: processedComment.processedHTML, comment: comment,
attachments: processedComment.extractedImages,
status: "PENDING" status: "PENDING"
}, },
SyncSalesforce: result SyncSalesforce: result
@ -218,116 +221,4 @@ async function va_timesheet_adjustment_function(req, res) {
} }
} }
function processHTMLComment(htmlComment) {
try {
let processedHTML = htmlComment;
const extractedImages = [];
let plainText = '';
// Extract plain text from HTML for Salesforce
plainText = extractPlainTextFromHTML(htmlComment);
// Process base64 images in HTML
const imageProcessingResult = extractAndProcessBase64Images(htmlComment);
processedHTML = imageProcessingResult.processedHTML;
extractedImages.push(...imageProcessingResult.extractedImages);
return {
processedHTML,
plainText,
extractedImages
};
} catch (error) {
console.error("Error processing HTML comment:", error);
// Return original content if processing fails
return {
processedHTML: htmlComment,
plainText: extractPlainTextFromHTML(htmlComment),
extractedImages: []
};
}
}
// Helper function to extract plain text from HTML
function extractPlainTextFromHTML(html) {
try {
// Remove HTML tags and decode HTML entities
let plainText = html
.replace(/<[^>]*>/g, ' ') // Replace tags with spaces
.replace(/\s+/g, ' ') // Collapse multiple spaces
.trim();
// Decode common HTML entities
plainText = plainText
.replace(/&nbsp;/g, ' ')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.replace(/&#x27;/g, "'")
.replace(/&#x2F;/g, '/');
return plainText;
} catch (error) {
console.error("Error extracting plain text from HTML:", error);
return html.replace(/<[^>]*>/g, ' ').trim();
}
}
// Helper function to extract and process base64 images from HTML
function extractAndProcessBase64Images(html) {
const extractedImages = [];
let processedHTML = html;
try {
// Regex to find base64 images in HTML (various formats)
const base64Regex = /<img[^>]+src="data:image\/([a-zA-Z0-9+]+);base64,([^"]+)"[^>]*>/gi;
let match;
let imageIndex = 0;
while ((match = base64Regex.exec(html)) !== null) {
const fullMatch = match[0];
const imageType = match[1]; // png, jpeg, gif, etc.
const base64Data = match[2];
// Create a unique filename
const timestamp = Date.now();
const filename = `adjustment_image_${timestamp}_${imageIndex}.${imageType}`;
// Calculate approximate file size
const size = Math.floor((base64Data.length * 3) / 4); // Base64 size calculation
// Store image information
extractedImages.push({
filename: filename,
mimeType: `image/${imageType}`,
base64Data: base64Data,
size: size,
originalHTML: fullMatch, // Store original img tag for reference
index: imageIndex
});
// Replace base64 with a reference in HTML
const replacementHTML = `<img src="attachment://${filename}" alt="Adjustment evidence ${imageIndex + 1}" data-filename="${filename}" data-image-index="${imageIndex}" class="adjustment-image">`;
processedHTML = processedHTML.replace(fullMatch, replacementHTML);
imageIndex++;
}
return {
processedHTML,
extractedImages
};
} catch (error) {
console.error("Error processing base64 images:", error);
return {
processedHTML: html,
extractedImages: []
};
}
}
export { va_timesheet_adjustment_function }; export { va_timesheet_adjustment_function };

View File

@ -3,18 +3,6 @@ import { VAModel, ProjectModel, LoggedModel } from "../../../models/vaModel.js";
async function va_timesheet_summary_function(req, res) { async function va_timesheet_summary_function(req, res) {
try { try {
const sf_token = req.headers.sf_token;
let vaId;
let vaUser;
let vaEmail;
if(sf_token) {
vaId = req.body.vaId;
vaUser = await VAModel.findById(vaId);
if (!vaUser) {
return res.status(404).json({ success: false, message: "VA not found" });
}
vaEmail = vaUser.userDetails.email
} else {
const accessToken = req.headers.token; const accessToken = req.headers.token;
if (!accessToken) { if (!accessToken) {
@ -26,14 +14,13 @@ async function va_timesheet_summary_function(req, res) {
return res.status(401).json({ success: false, message: "Invalid token" }); return res.status(401).json({ success: false, message: "Invalid token" });
} }
vaEmail = decoded.email; const vaEmail = decoded.email;
vaUser = await VAModel.findOne({ "userDetails.email": vaEmail }); const vaUser = await VAModel.findOne({ "userDetails.email": vaEmail });
if (!vaUser) { if (!vaUser) {
return res.status(404).json({ success: false, message: "VA not found" }); return res.status(404).json({ success: false, message: "VA not found" });
} }
vaId = vaUser._id; const vaId = vaUser._id;
}
const { projectId, date } = req.params; const { projectId, date } = req.params;
if (!projectId || !date) { if (!projectId || !date) {

View File

@ -6,8 +6,6 @@ async function authValidator(req, res, next) {
try { try {
const sf_token = req.headers.sf_token; const sf_token = req.headers.sf_token;
if (sf_token) { if (sf_token) {
const { vaId } = req.params;
if(vaId) req.body.vaId = vaId;
next(); next();
} else { } else {
const accessToken = req.headers.token; const accessToken = req.headers.token;

View File

@ -46,9 +46,6 @@ const userDetailsSchema = new Schema({
veemAccount: { veemAccount: {
type: String, type: String,
}, },
wiseEmail: {
type: String,
},
phone: { phone: {
type: String, type: String,
}, },
@ -101,27 +98,7 @@ const projectSchema = new Schema({
type: String, type: String,
enum: ["ACTIVE", "INACTIVE"], enum: ["ACTIVE", "INACTIVE"],
default: "ACTIVE" default: "ACTIVE"
}, }
vaWorkingScheduleStart: {
type: String,
default: null
},
vaWorkingScheduleEnd: {
type: String,
default: null
},
isFlexibleAllowedPt: {
type: Boolean,
default: true
},
vaWorkingDelayAllowedStart: {
type: String,
default: '5m'
},
vaWorkingDelayAllowedEnd: {
type: String,
default: '5m'
},
}); });
/* ---------------------- Client Details Schema ---------------------- */ /* ---------------------- Client Details Schema ---------------------- */
@ -343,9 +320,6 @@ const vaWorkTrackSchema = new Schema({
appName: { appName: {
type: String, type: String,
}, },
url: {
type: String,
},
from: { from: {
type: Date, type: Date,
default: Date.now, default: Date.now,
@ -427,14 +401,6 @@ const adjustedHoursSchema = new Schema({
type: String, type: String,
required: true required: true
}, },
attachments: [{
filename: String,
mimeType: String,
base64Data: String,
size: Number,
originalHTML: String,
index: Number
}],
status: { status: {
type: String, type: String,
enum: ["PENDING", "APPROVED", "REJECTED"], enum: ["PENDING", "APPROVED", "REJECTED"],
@ -484,14 +450,6 @@ const leavesSchema = new Schema({
type: String, type: String,
required: true required: true
}, },
attachments: [{
filename: String,
mimeType: String,
base64Data: String,
size: Number,
originalHTML: String,
index: Number
}],
leaveType: { leaveType: {
type: String, type: String,
enum: ["SICK", "PTO"], enum: ["SICK", "PTO"],

View File

@ -13,7 +13,7 @@ class VaClientRepository {
async findByEmail(email) { async findByEmail(email) {
try { try {
const response = await ClientModel.findOne({ "userDetails.email": email }); const response = await ClientModel.findOne({ "userDetails.email": email.toLowerCase() });
return response; return response;
} catch (error) { } catch (error) {
throw error; throw error;
@ -31,27 +31,10 @@ class VaClientRepository {
async update(id, data) { async update(id, data) {
try { try {
// Convert nested userDetails object to dot notation
const updateObj = {};
// Handle userDetails fields with dot notation
if (data.userDetails) {
Object.keys(data.userDetails).forEach(key => {
updateObj[`userDetails.${key}`] = data.userDetails[key];
});
}
// Handle other top-level fields if any
Object.keys(data).forEach(key => {
if (key !== 'userDetails') {
updateObj[key] = data[key];
}
});
const response = await ClientModel.findByIdAndUpdate( const response = await ClientModel.findByIdAndUpdate(
id, id,
{ $set: updateObj }, { $set: { ...data } },
{ new: true, runValidators: true } { new: true }
); );
return response; return response;
} catch (error) { } catch (error) {
@ -59,53 +42,9 @@ class VaClientRepository {
} }
} }
async findProfileByEmail(email) { async delete(id) {
try { try {
const response = await ClientModel.findOne( const response = await ClientModel.findByIdAndDelete(id);
{ "userDetails.email": email },
{
// Only specify the fields you want to include
"userDetails.name": 1,
"userDetails.email": 1,
"userDetails.address": 1,
"userDetails.city": 1,
"userDetails.state": 1,
"userDetails.zip": 1,
"userDetails.position": 1
// Don't include any exclusion fields (0) when using inclusion (1)
}
).lean(); // Use lean() for better performance since we don't need mongoose document
return response;
} catch (error) {
throw error;
}
}
// Alternative: If you need to be more explicit about excluding sensitive fields
async findProfileByEmailExplicit(email) {
try {
const response = await ClientModel.findOne(
{ "userDetails.email": email }
).select({
'userDetails.name': 1,
'userDetails.email': 1,
'userDetails.address': 1,
'userDetails.city': 1,
'userDetails.state': 1,
'userDetails.zip': 1,
'userDetails.position': 1
}).lean();
return response;
} catch (error) {
throw error;
}
}
async delete(id, options = {}) {
try {
const response = await ClientModel.findByIdAndDelete(id, options);
return response; return response;
} catch (error) { } catch (error) {
throw error; throw error;

View File

@ -1,7 +1,7 @@
import express from "express"; import express from "express";
const clientRoutes = express.Router(); const clientRoutes = express.Router();
import { va_register_client, delete_client, update_client, get_client_dashboard_profile, update_client_password } from "../../controllers/client/vaClient.controller.js"; import { va_register_client } from "../../controllers/client/vaClient.controller.js";
// import { va_clients_callback } from "../../controllers/va/vaClients/vaClients.controller.js"; // import { va_clients_callback } from "../../controllers/va/vaClients/vaClients.controller.js";
import { va_clients_projects_callback } from "../../controllers/va/vaClients/vaClientProject.controller.js"; import { va_clients_projects_callback } from "../../controllers/va/vaClients/vaClientProject.controller.js";
import { va_login_client } from "../../controllers/client/vaClientLogin.controller.js"; import { va_login_client } from "../../controllers/client/vaClientLogin.controller.js";
@ -31,10 +31,6 @@ clientRoutes.get("/:clientId/projects/:projectId", va_clients_projects_callback)
clientRoutes.get("/dashboard", clientAuthValidator, client_dashboard); // VA Clients Projects Callback clientRoutes.get("/dashboard", clientAuthValidator, client_dashboard); // VA Clients Projects Callback
clientRoutes.post("/dashboard/va-report", clientAuthValidator, client_dashboard_va_log); // VA details clientRoutes.post("/dashboard/va-report", clientAuthValidator, client_dashboard_va_log); // VA details
clientRoutes.get("/dashboard-projects", clientAuthValidator, client_dashboard_projects); // VA Clients Projects Callback clientRoutes.get("/dashboard-projects", clientAuthValidator, client_dashboard_projects); // VA Clients Projects Callback
clientRoutes.get("/profile", clientAuthValidator, get_client_dashboard_profile);
clientRoutes.patch("/profile", clientAuthValidator, update_client_password);
clientRoutes.patch("/:clientId", update_client);
clientRoutes.delete("/:clientId", delete_client);

View File

@ -12,7 +12,6 @@ import { process_reset } from "../../controllers/passwordReset.controllers.js";
import { project_add_sf } from "../../controllers/project/projectAddSf.controller.js" import { project_add_sf } from "../../controllers/project/projectAddSf.controller.js"
import { update_project_sf } from "../../controllers/project/projectUpdateSf.controller.js" import { update_project_sf } from "../../controllers/project/projectUpdateSf.controller.js"
import { authValidator } from "../../middlewares/auth.middleware.js"; import { authValidator } from "../../middlewares/auth.middleware.js";
import { delete_project_sf } from "../../controllers/project/deleteProjectSf.controller.js"
const v1Routes = express.Router() const v1Routes = express.Router()
@ -30,7 +29,6 @@ v1Routes.use("/client", clientRoutes);
v1Routes.post("/project", authValidator, project_add_sf); v1Routes.post("/project", authValidator, project_add_sf);
v1Routes.patch("/project/:projectId", authValidator, update_project_sf); v1Routes.patch("/project/:projectId", authValidator, update_project_sf);
v1Routes.delete("/project/:projectId", authValidator, delete_project_sf);
v1Routes.post("/password/reset", process_reset); v1Routes.post("/password/reset", process_reset);

View File

@ -26,9 +26,6 @@ import { va_verify_email } from "../../controllers/va/vaAuth/vaVerifyEmail.contr
import { va_leave_apply_function } from "../../controllers/va/vaLeaves/vaLeavesApply.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_list_function } from "../../controllers/va/vaLeaves/vaLeaveList.controller.js"
import { va_leave_manage_function } from "../../controllers/va/vaLeaves/vaLeaveManage.controller.js"; import { va_leave_manage_function } from "../../controllers/va/vaLeaves/vaLeaveManage.controller.js";
import { va_remove_function } from "../../controllers/va/vaProfile/vaDelete.controller.js";
import { va_payroll_schedule_function } from "../../controllers/va/vaPayroll/vaPayrollSchedule.controller.js";
import { va_payslip_data_function } from "../../controllers/va/vaPayroll/vaPaySlip.controller.js";
vaRoutes.get("/", va_callback); // VA Callback for getting active VA vaRoutes.get("/", va_callback); // VA Callback for getting active VA
@ -42,10 +39,12 @@ 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
vaRoutes.delete("/profile/:id", va_remove_function); // VA delete Callback
vaRoutes.get("/:userName", va_username_callback); // VA Username Callback
// timesheet adjustment routes // timesheet adjustment routes
vaRoutes.get("/timesheet/summary/:projectId/:date/:vaId?", authValidator, va_timesheet_summary_function); // Get Timesheet Adjustment for VA on specific project and date vaRoutes.get("/timesheet/summary/:projectId/:date", 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);
@ -55,7 +54,7 @@ vaRoutes.get("/leaves/all", authValidator, va_leave_list_function); // Get all l
vaRoutes.get("/leaves/status/:status", authValidator, va_leave_list_function); // Leaves Filter by status 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/type/:leaveType", authValidator, va_leave_list_function); // Filter by leave type
vaRoutes.get("/leaves/status/:status/type/:leaveType", authValidator, va_leave_list_function); // leaves Filter vaRoutes.get("/leaves/status/:status/type/:leaveType", authValidator, va_leave_list_function); // leaves Filter
vaRoutes.post("/leave/request", authValidator, va_leave_apply_function); // Leave for VA vaRoutes.post("/leave/request", authValidator, va_leave_apply_function); // Timesheet Adjustment for VA
vaRoutes.patch("/leave/:leaveId", va_leave_manage_function); vaRoutes.patch("/leave/:leaveId", va_leave_manage_function);
// timesheet adjustment routes // timesheet adjustment routes
@ -66,7 +65,7 @@ vaRoutes.get("/search", va_filter_search);
// GET Windows Tab Logged Data for VA IN Between Login and LOGOUT // GET Windows Tab Logged Data for VA IN Between Login and LOGOUT
vaRoutes.get("/report/project/:projectId/:from?/:to?/:vaId?", authValidator, va_projects_report_function); vaRoutes.get("/report/project/:projectId/:from?/:to?", authValidator, va_projects_report_function);
// Getting Project List assigned to VA should be ACTIVE and add total logged hours added by VA // Getting Project List assigned to VA should be ACTIVE and add total logged hours added by VA
vaRoutes.get("/list/projects/:projectId?/:from?/:to?", authValidator, va_projects_log_list_function); vaRoutes.get("/list/projects/:projectId?/:from?/:to?", authValidator, va_projects_log_list_function);
@ -80,14 +79,6 @@ vaRoutes.get("/log/project/:projectId/:from?/:to?", authValidator, va_log_callba
// VA filter search: supports query params `name`, `email`, `from`, `to` // VA filter search: supports query params `name`, `email`, `from`, `to`
// Payroll routes
vaRoutes.get("/payrollSchedule", authValidator, va_payroll_schedule_function); // VA Payroll Schedule
vaRoutes.get("/paySlip/:vaId", authValidator, va_payslip_data_function); // VA Payslip
vaRoutes.get("/:userName", va_username_callback); // VA Username Callback
export default vaRoutes; export default vaRoutes;

View File

@ -154,17 +154,7 @@ app.get('/client/projects', async (req, res) => {
res.render('client/dashboard-projects'); res.render('client/dashboard-projects');
} catch (error) { } catch (error) {
console.error('Projects error:', error); console.error('Projects error:', error);
res.status(500).render('client/dashboard-projects', { error: 'Failed to load projects' }); res.status(500).render('client/dashboard-projects', { error: 'Failed to load virtual assistants' });
}
});
app.get('/client/profile', async (req, res) => {
try {
// const vaData = await virtualAssistantService.getClientVAs(req.user.id);
res.render('client/dashboard-profile');
} catch (error) {
console.error('Projects error:', error);
res.status(500).render('client/dashboard-profile', { error: 'Failed to load profile' });
} }
}); });
@ -451,27 +441,9 @@ io.on("connection", (socket) => {
}); });
// socket.on('disconnect', () => { socket.on('disconnect', () => {
// console.log('a user Disconnected'); console.log('a user Disconnected');
// });
// When client leaves or closes tab
socket.on("disconnect", () => {
if (socket.data.role === "client") {
const { clientId, vaId, projectId } = socket.data;
const vaSocketId = vaSocketRespectToClient.get(
`${clientId}-${vaId}-${projectId}-va`
);
if (vaSocketId) {
io.to(vaSocketId).emit("stopSendAppsDetails");
}
}
console.log("❌ Client disconnected");
}); });
}); });
app.use((req, res) => { app.use((req, res) => {

View File

@ -1,7 +1,5 @@
import logger from "../config/logger.js"; import logger from "../config/logger.js";
import SalesforceService from "./salesforce.service.js"; import SalesforceService from "./salesforce.service.js";
import { VAModel, ProjectModel, LoggedModel, WorkTrackModel } from "../models/vaModel.js";
import bcrypt from "bcrypt";
class VaClientService { class VaClientService {
constructor(vaClientRepository) { constructor(vaClientRepository) {
@ -56,208 +54,21 @@ class VaClientService {
async updateClient(id, data) { async updateClient(id, data) {
try { try {
// Check if client exists const response = await this.clientRepository.update(id, data);
const existingClient = await this.clientRepository.findById(id); if (!response) throw new Error("Client not found");
if (!existingClient) { return response;
throw new Error("Client not found");
}
const { userDetails } = data;
if (!userDetails) {
throw new Error("No user details provided for update");
}
// Prepare update data
const updateData = {
userDetails: {}
};
// Define allowed fields for update (excluding password)
const allowedFields = [
"name", "address", "city", "state", "zip",
"billingName", "billingEmail", "billingAddress", "billingCity",
"billingState", "billingZip", "position", "signatoryName",
"signatoryEmail", "paymentMethod", "bankName", "accountNumber",
"routingNumber", "accountType", "accountHolderName", "cardNumber",
"expiryDate", "securityCode", "cardZip"
];
let hasUpdates = false;
const updatedFields = [];
// Process allowed fields
allowedFields.forEach(field => {
if (userDetails[field] !== undefined && userDetails[field] !== null && userDetails[field] !== '') {
updateData.userDetails[field] = userDetails[field];
hasUpdates = true;
updatedFields.push(field);
}
});
// Handle email update separately (needs uniqueness check)
if (userDetails.email && userDetails.email !== existingClient.userDetails.email) {
// Check if new email already exists
const emailExists = await this.clientRepository.findByEmail(userDetails.email);
if (emailExists && emailExists._id.toString() !== id) {
throw new Error("Email already exists");
}
updateData.userDetails.email = userDetails.email.toLowerCase();
hasUpdates = true;
updatedFields.push('email');
}
// Check if any fields were actually updated
if (!hasUpdates) {
throw new Error("No valid fields to update");
}
// Update the client
const updatedClient = await this.clientRepository.update(id, updateData);
// Notify Salesforce about the update
try {
await this.salesforceService.updateClientInSalesforce(id, data);
logger.info(`Salesforce notified about client update: ${id}`);
} catch (sfError) {
logger.warn(`Salesforce notification failed for client update: ${sfError.message}`);
// Don't throw error - client update should proceed even if Salesforce fails
}
logger.info(`Client updated successfully: ${id}, Fields: ${updatedFields.join(', ')}`);
return {
...updatedClient.toObject(),
updateSummary: {
updatedFields
}
};
} catch (error) { } catch (error) {
logger.error("Client Service - updateClient error: " + error.message); logger.error("Client Service - updateClient error: " + error.message);
throw error; throw error;
} }
} }
async deleteClient(clientId) { async deleteClient(id) {
try { try {
// Check if client exists const response = await this.clientRepository.delete(id);
const existingClient = await this.clientRepository.findById(clientId);
if (!existingClient) {
throw new Error("Client not found");
}
const clientName = existingClient.userDetails?.name;
const vaIds = existingClient.vaIds || [];
const projectIds = existingClient.projectIds || [];
logger.info(`Deleting client: ${clientName} (${clientId})`);
const deletionSummary = {};
// 1. Remove client from all VAs
if (vaIds.length > 0) {
const vaResult = await VAModel.updateMany(
{ _id: { $in: vaIds } },
{ $pull: { clientIds: clientId } }
);
deletionSummary.vasUpdated = vaResult.modifiedCount;
}
// 2. Delete all projects and associated data
if (projectIds.length > 0) {
// Remove project references from VAs
await VAModel.updateMany(
{ projectIds: { $in: projectIds } },
{ $pull: { projectIds: { $in: projectIds } } }
);
// Delete work tracks and logs
await WorkTrackModel.deleteMany({ projectId: { $in: projectIds } });
await LoggedModel.deleteMany({ projectId: { $in: projectIds } });
// Delete projects
const projectResult = await ProjectModel.deleteMany({ _id: { $in: projectIds } });
deletionSummary.projectsDeleted = projectResult.deletedCount;
}
// 3. Delete the client
const deletedClient = await this.clientRepository.delete(clientId);
logger.info(`✅ Successfully deleted client: ${clientName} (${clientId})`);
return {
...deletedClient.toObject(),
deletionSummary
};
} catch (error) {
logger.error("Client Service - deleteClient error: " + error.message);
throw error;
}
}
async getClientProfileByEmail(email) {
try {
const response = await this.clientRepository.findProfileByEmail(email);
return response; return response;
} catch (error) { } catch (error) {
logger.error("Client Service - getClientProfileByEmail error: " + error.message); logger.error("Client Service - deleteClient error: " + error.message);
throw error;
}
}
async updateClientPassword(id, passwordData) {
try {
// Check if client exists
const existingClient = await this.clientRepository.findById(id);
if (!existingClient) {
throw new Error("Client not found");
}
const { currentPassword, newPassword, confirmPassword } = passwordData;
// Validate required fields
if (!currentPassword || !newPassword || !confirmPassword) {
throw new Error("All password fields are required");
}
// Validate password match
if (newPassword !== confirmPassword) {
throw new Error("New passwords do not match");
}
// Verify current password
const isMatch = await bcrypt.compare(currentPassword, existingClient.userDetails.password);
if (!isMatch) {
throw new Error("Current password is incorrect");
}
// Hash new password
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(newPassword, saltRounds);
// Prepare update data
const updateData = {
userDetails: {
password: hashedPassword
}
};
// Update the client password
const updatedClient = await this.clientRepository.update(id, updateData);
logger.info(`Client password updated successfully: ${id}`);
return {
success: true,
message: "Password updated successfully",
clientId: id
};
} catch (error) {
logger.error("Client Service - updateClientPassword error: " + error.message);
throw error; throw error;
} }
} }

View File

@ -34,6 +34,16 @@
<i class="fas fa-file-invoice"></i>Projects <i class="fas fa-file-invoice"></i>Projects
</a> </a>
</li> </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> </ul>
<button class="mobile-menu-button"> <button class="mobile-menu-button">
@ -44,14 +54,11 @@
</nav> </nav>
<div class="dashboard-container"> <div class="dashboard-container">
<aside class="dashboard-sidebar"> <aside class="dashboard-sidebar">
<!-- <div class="sidebar-header">
<h2 class="sidebar-title">DocVA Client Portal</h2>
</div> -->
<nav class="sidebar-nav"> <nav class="sidebar-nav">
<ul class="sidebar-list"> <ul class="sidebar-list">
<li class="sidebar-item">
<a href="/client/profile" class="sidebar-link">
<i class="fas fa-user sidebar-icon"></i>
<span>Profile</span>
</a>
</li>
<li class="sidebar-item"> <li class="sidebar-item">
<a href="/client/dashboard" class="sidebar-link active"> <a href="/client/dashboard" class="sidebar-link active">
<i class="fas fa-chart-bar sidebar-icon"></i> <i class="fas fa-chart-bar sidebar-icon"></i>
@ -70,6 +77,18 @@
<span>Projects</span> <span>Projects</span>
</a> </a>
</li> </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> -->
<li class="sidebar-item"> <li class="sidebar-item">
<a href="javascript:void(0);" class="sidebar-link" id="logoutButton"> <a href="javascript:void(0);" class="sidebar-link" id="logoutButton">
<i class="fas fa-sign-out-alt sidebar-icon"></i> <i class="fas fa-sign-out-alt sidebar-icon"></i>
@ -123,6 +142,7 @@
<div class="content-section"> <div class="content-section">
<div class="section-header"> <div class="section-header">
<h2 class="section-title">Virtual Assistants <span class="total-count" id="vaListCount">0 VAs</span></h2> <h2 class="section-title">Virtual Assistants <span class="total-count" id="vaListCount">0 VAs</span></h2>
</div> </div>
<div class="table-container"> <div class="table-container">
<div class="table-wrapper"> <div class="table-wrapper">
@ -131,9 +151,9 @@
<tr> <tr>
<th>Status</th> <th>Status</th>
<th>Virtual Assistant</th> <th>Virtual Assistant</th>
<th>Active Project</th> <th>Projects Assigned</th>
<th>Total Projects</th> <th>Active Projects</th>
<th>Today Logged Hours</th> <th>Worked Time</th>
<th>Action</th> <th>Action</th>
</tr> </tr>
</thead> </thead>
@ -154,77 +174,6 @@
</div> </div>
</main> </main>
</div> </div>
<div id="vaModal" class="modal-overlay">
<div class="modal-content">
<div class="modal-header" style="display: flex;align-items: center;justify-content: space-between;">
<h2 class="modal-title" id="modalVAName">VA Details</h2>
<button class="modal-close" onclick="closeVAModal()">
<i class="fas fa-times"></i>
</button>
</div>
<div class="modal-body">
<!-- Projects Section -->
<div class="va-details-section">
<h3 class="va-section-title">
<i class="fas fa-briefcase"></i>
Projects
<span class="va-count-badge" id="projectsCountBadge">0</span>
</h3>
<div class="va-table-container">
<table class="va-table">
<thead>
<tr>
<th>Project Name</th>
<th>Hourly Rate</th>
<th>Worked Time</th>
<th>Status</th>
</tr>
</thead>
<tbody id="modalProjectsTableBody">
<!-- Project details will be loaded here -->
</tbody>
</table>
<div id="modalNoProjects" class="no-vas-message" style="display: none;">
<i class="fas fa-briefcase"></i>
<h4>No Projects</h4>
<p>This VA is not assigned to any projects.</p>
</div>
</div>
</div>
<!-- Action Button -->
<div class="modal-actions" id="modalActions" style="display: none;">
<a href="#" class="view-session-btn" id="viewSessionBtn" target="_blank">
<i class="fas fa-eye"></i>
View Live Session
</a>
</div>
</div>
</div>
</div>
<div id="logoutModal" class="modal-overlay">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title">Confirm Logout</h2>
<button class="modal-close" onclick="closeLogoutModal()">
<i class="fas fa-times"></i>
</button>
</div>
<div class="logout-modal-body">
<div class="logout-icon">
<i class="fas fa-sign-out-alt"></i>
</div>
<h3 class="logout-title">Are you sure you want to logout?</h3>
<p class="logout-message">You will be logged out of your account and redirected to the login page.</p>
<div class="logout-actions">
<button class="logout-cancel-btn" onclick="closeLogoutModal()">Cancel</button>
<button class="logout-confirm-btn" id="confirmLogoutBtn">Yes, Logout</button>
</div>
</div>
</div>
</div>
</div> </div>
<script> <script>
// Theme management // Theme management
@ -238,7 +187,7 @@
} }
// Initialize theme // Initialize theme
const savedTheme = getSystemTheme(); const savedTheme = localStorage.getItem('theme') || getSystemTheme();
setTheme(savedTheme); setTheme(savedTheme);
// API base URL // API base URL
@ -293,7 +242,7 @@
// Format time function // Format time function
function formatTime(seconds) { function formatTime(seconds) {
if (!seconds) return 'No Record Found'; 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);
@ -307,88 +256,20 @@
} }
} }
const vaModal = document.getElementById('vaModal'); // Create VA card HTML
function openVAModal(va) {
// Set basic VA info
document.getElementById('modalVAName').textContent = va.name;
// Set projects
const projects = va.projects || [];
document.getElementById('projectsCountBadge').textContent = projects.length;
const projectsTableBody = document.getElementById('modalProjectsTableBody');
const noProjectsMessage = document.getElementById('modalNoProjects');
if (projects.length > 0) {
projectsTableBody.innerHTML = projects.map(project => createProjectTableRow(project)).join('');
projectsTableBody.style.display = '';
noProjectsMessage.style.display = 'none';
} else {
projectsTableBody.innerHTML = '';
projectsTableBody.style.display = 'none';
noProjectsMessage.style.display = 'block';
}
// Set action button
const modalActions = document.getElementById('modalActions');
const viewSessionBtn = document.getElementById('viewSessionBtn');
if (va.isCurrentlyActive && va.lastProjectId) {
viewSessionBtn.href = `/live/${va.lastProjectId}/${va.id}`;
modalActions.style.display = 'block';
} else {
modalActions.style.display = 'none';
}
// Show modal
vaModal.style.display = 'flex';
document.body.style.overflow = 'hidden';
}
function closeVAModal() {
vaModal.style.display = 'none';
document.body.style.overflow = 'auto';
}
// Create project table row for modal
function createProjectTableRow(project) {
const statusClass = project.isCurrentlyWorking ? 'active' : 'inactive';
const statusText = project.isCurrentlyWorking ? 'Working' : 'Not Active';
return `
<tr>
<td>
<div class="project-info">
<div class="project-name">${project.name}</div>
</div>
</td>
<td>
<span class="hourly-rate">$${project.hourlyRate}/hr</span>
</td>
<td>
<span class="worked-time">${project.totalWorkedTime}</span>
</td>
<td>
<div class="status-badge ${statusClass}">
<span class="status-text">${statusText}</span>
</div>
</td>
</tr>
`;
}
// Create VA table row for main table
function createVATableRow(va) { function createVATableRow(va) {
const statusClass = va.isCurrentlyActive ? 'active' : 'inactive'; const statusClass = va.isCurrentlyActive ? 'active' : 'inactive';
const statusText = va.isCurrentlyActive ? 'Active' : 'Inactive'; const statusText = va.isCurrentlyActive ? 'Active' : 'Inactive';
const activeProject = va?.lastProject?.name ? va.lastProject.name : '-'; const activeProject = va?.lastProject?.name ? va.lastProject.name : '-';
const workedTime = formatTime(va.currentSessionDuration); const workedTime = formatTime(va.currentSessionDuration);
const projectsAssigned = va.projectsAssigned || 0; const projectsAssigned = va.projectsAssigned || 0;
const viewLink = va.isCurrentlyActive ? `<a href="/live/${va.lastProjectId}/${va.id}" class="view-link" data-va-id="${va.id}" target="_blank">Live Tracking</a>` : ''; 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>
View
</a>` : '';
return ` return `
<tr class="va-row" data-va-id="${va.id}"> <tr>
<td> <td>
<div class="status-indicator"> <div class="status-indicator">
<span class="status-text ${statusClass}">${statusText}</span> <span class="status-text ${statusClass}">${statusText}</span>
@ -422,6 +303,7 @@
const response = await fetchWithAuth(`${API_BASE}/dashboard`); const response = await fetchWithAuth(`${API_BASE}/dashboard`);
if (!response) return; if (!response) return;
const result = await response.json(); const result = await response.json();
// console.log(data)
if (result.success) { if (result.success) {
const data = result.data; const data = result.data;
@ -438,26 +320,22 @@
// Update VA table // Update VA table
const vaTableBody = document.getElementById('vaTableBody'); const vaTableBody = document.getElementById('vaTableBody');
const vaEmptyState = document.getElementById('vaEmptyState'); const vaEmptyState = document.getElementById('vaEmptyState');
const allVAs = data.allVAs || []; const allVAs = data.allVAs?.total || [];
if (allVAs.length > 0) { if (allVAs.length > 0) {
vaTableBody.innerHTML = allVAs.map(va => createVATableRow(va)).join(''); vaTableBody.innerHTML = allVAs.map(va => createVATableRow(va)).join('');
vaTableBody.style.display = ''; vaTableBody.style.display = '';
vaEmptyState.style.display = 'none'; vaEmptyState.style.display = 'none';
// Store VA data globally for modal access // Add event listeners to view links
window.vaData = allVAs; // document.querySelectorAll('.view-link').forEach(link => {
// link.addEventListener('click', function(e) {
// Add click event listeners to VA rows // e.preventDefault();
document.querySelectorAll('.va-row').forEach((row, index) => { // const vaId = this.getAttribute('data-va-id');
row.addEventListener('click', function(e) { // // You can update this link as needed
// Don't open modal if clicking on view link // window.location.href = `/client/virtual-assistants/${vaId}`;
if (!e.target.closest('.view-link')) { // });
const va = allVAs[index]; // });
openVAModal(va);
}
});
});
} else { } else {
vaTableBody.innerHTML = ''; vaTableBody.innerHTML = '';
vaTableBody.style.display = 'none'; vaTableBody.style.display = 'none';
@ -491,35 +369,9 @@
vaEmptyState.style.display = 'block'; vaEmptyState.style.display = 'block';
} }
// Close modal when clicking outside
vaModal.addEventListener('click', (e) => {
if (e.target === vaModal) {
closeVAModal();
}
});
// Close modal with escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && vaModal.style.display === 'flex') {
closeVAModal();
}
});
// Optional: Auto-refresh data every 30 seconds // Optional: Auto-refresh data every 30 seconds
setInterval(loadDashboardData, 30000); setInterval(loadDashboardData, 30000);
// Logout Modal Functions
function openLogoutModal() {
const modal = document.getElementById('logoutModal');
modal.style.display = 'flex';
document.body.style.overflow = 'hidden';
}
function closeLogoutModal() {
const modal = document.getElementById('logoutModal');
modal.style.display = 'none';
document.body.style.overflow = 'auto';
}
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Load dashboard data // Load dashboard data
@ -532,25 +384,11 @@
sidebar.classList.toggle('active'); sidebar.classList.toggle('active');
}); });
// Close modal with escape key // Logout button
document.addEventListener('keydown', function(e) { const logoutButton = document.getElementById('logoutButton');
if (e.key === 'Escape') { logoutButton.addEventListener('click', handleLogout);
closeLogoutModal();
}
}); });
document.getElementById('logoutModal').addEventListener('click', function(e) {
if (e.target === this) {
closeLogoutModal();
}
});
// Logout button - opens confirmation modal instead of directly logging out
document.getElementById('logoutButton').addEventListener('click', openLogoutModal);
// Confirm logout button
document.getElementById('confirmLogoutBtn').addEventListener('click', handleLogout);
});
</script> </script>
</body> </body>

View File

@ -1,478 +0,0 @@
<!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 | Profile</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">
<i class="fas fa-file-invoice"></i>Projects
</a>
</li>
</ul>
<button class="mobile-menu-button">
<i class="fas fa-bars"></i>
</button>
</div>
</div>
</nav>
<div class="dashboard-container">
<aside class="dashboard-sidebar">
<nav class="sidebar-nav">
<ul class="sidebar-list">
<li class="sidebar-item">
<a href="/client/profile" class="sidebar-link active">
<i class="fas fa-user sidebar-icon"></i>
<span>Profile</span>
</a>
</li>
<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">
<i class="fas fa-file-invoice sidebar-icon"></i>
<span>Projects</span>
</a>
</li>
<li class="sidebar-item">
<a href="javascript:void(0);" class="sidebar-link" id="logoutButton">
<i class="fas fa-sign-out-alt sidebar-icon"></i>
<span>Logout</span>
</a>
</li>
</ul>
</nav>
</aside>
<main class="dashboard-main">
<div class="dashboard-content">
<div class="profile-container">
<div class="profile-header">
<h1 class="profile-title">Client Profile</h1>
</div>
<div class="profile-sections">
<!-- Personal Information -->
<div class="profile-section">
<div class="section-header">
<i class="fas fa-user section-icon"></i>
<h2 class="section-title">Personal Information</h2>
</div>
<div class="form-grid">
<div class="form-group">
<label class="form-label">Full Name</label>
<input type="text" class="form-input" id="name" readonly>
</div>
<div class="form-group">
<label class="form-label">Email</label>
<input type="email" class="form-input" id="email" readonly>
</div>
<div class="form-group">
<label class="form-label">Position</label>
<input type="text" class="form-input" id="position" readonly>
</div>
</div>
</div>
<!-- Address Information -->
<div class="profile-section">
<div class="section-header">
<i class="fas fa-home section-icon"></i>
<h2 class="section-title">Address Information</h2>
</div>
<div class="form-grid">
<div class="form-group">
<label class="form-label">Address</label>
<input type="text" class="form-input" id="address" readonly>
</div>
<div class="form-group">
<label class="form-label">City</label>
<input type="text" class="form-input" id="city" readonly>
</div>
<div class="form-group">
<label class="form-label">State</label>
<input type="text" class="form-input" id="state" readonly>
</div>
<div class="form-group">
<label class="form-label">ZIP Code</label>
<input type="text" class="form-input" id="zip" readonly>
</div>
</div>
</div>
<!-- Password Change Section -->
<div class="profile-section">
<div class="password-section">
<button class="change-password-btn" id="changePasswordBtn">
<i class="fas fa-lock"></i>
Change Password
</button>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
<!-- Change Password Modal -->
<div id="passwordModal" class="modal-overlay">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title">Change Password</h2>
<button class="modal-close" onclick="closePasswordModal()">
<i class="fas fa-times"></i>
</button>
</div>
<div class="modal-body">
<form class="modal-form" id="passwordForm">
<div class="form-group">
<label class="form-label">Current Password</label>
<input type="password" class="form-input" id="currentPassword" required>
<div class="error-message" id="currentPasswordError"></div>
</div>
<div class="form-group">
<label class="form-label">New Password</label>
<input type="password" class="form-input" id="newPassword" required>
<div class="error-message" id="newPasswordError"></div>
</div>
<div class="form-group">
<label class="form-label">Confirm New Password</label>
<input type="password" class="form-input" id="confirmPassword" required>
<div class="error-message" id="confirmPasswordError"></div>
</div>
<div class="modal-actions">
<button type="button" class="cancel-btn" onclick="closePasswordModal()">Cancel</button>
<button type="submit" class="save-btn" id="savePasswordBtn">Change Password</button>
</div>
</form>
</div>
</div>
</div>
<div id="logoutModal" class="modal-overlay">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title">Confirm Logout</h2>
<button class="modal-close" onclick="closeLogoutModal()">
<i class="fas fa-times"></i>
</button>
</div>
<div class="logout-modal-body">
<div class="logout-icon">
<i class="fas fa-sign-out-alt"></i>
</div>
<h3 class="logout-title">Are you sure you want to logout?</h3>
<p class="logout-message">You will be logged out of your account and redirected to the login page.</p>
<div class="logout-actions">
<button class="logout-cancel-btn" onclick="closeLogoutModal()">Cancel</button>
<button class="logout-confirm-btn" id="confirmLogoutBtn">Yes, Logout</button>
</div>
</div>
</div>
</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 = 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;
}
async function handleLogout() {
try {
await fetchWithAuth(`${API_BASE}/logout`, { method: 'POST' });
} catch (error) {
console.error('Logout API error:', error);
} finally {
localStorage.removeItem('knoptpq');
localStorage.removeItem('refreshToken');
window.location.href = '/client/login';
}
}
// Format empty fields
function formatEmptyFields() {
const inputs = document.querySelectorAll('.form-input');
inputs.forEach(input => {
if (!input.value.trim()) {
input.value = 'Not provided';
input.classList.add('empty-field');
} else {
input.classList.remove('empty-field');
}
});
}
// Load profile data
async function loadProfileData() {
try {
const response = await fetchWithAuth(`${API_BASE}/profile`);
if (!response) return;
const result = await response.json();
if (result.success) {
const data = result.data;
// Populate only personal and address information
const fieldsToShow = ['name', 'email', 'position', 'address', 'city', 'state', 'zip'];
fieldsToShow.forEach(key => {
const element = document.getElementById(key);
if (element && data[key]) {
element.value = data[key];
}
});
formatEmptyFields();
} else {
console.error('Failed to load profile data:', result.message);
alert('Failed to load profile data');
}
} catch (error) {
console.error('Error loading profile data:', error);
alert('Error loading profile data');
}
}
// Password Modal Functions
function openPasswordModal() {
const modal = document.getElementById('passwordModal');
modal.style.display = 'flex';
document.body.style.overflow = 'hidden';
// Clear form
document.getElementById('passwordForm').reset();
clearErrorMessages();
}
function closePasswordModal() {
const modal = document.getElementById('passwordModal');
modal.style.display = 'none';
document.body.style.overflow = 'auto';
// Clear form and errors
document.getElementById('passwordForm').reset();
clearErrorMessages();
}
function clearErrorMessages() {
document.getElementById('currentPasswordError').textContent = '';
document.getElementById('newPasswordError').textContent = '';
document.getElementById('confirmPasswordError').textContent = '';
}
// Change password
async function changePassword(event) {
event.preventDefault();
const currentPassword = document.getElementById('currentPassword').value;
const newPassword = document.getElementById('newPassword').value;
const confirmPassword = document.getElementById('confirmPassword').value;
// Reset errors
clearErrorMessages();
let hasError = false;
if (!currentPassword) {
document.getElementById('currentPasswordError').textContent = 'Current password is required';
hasError = true;
}
if (!newPassword) {
document.getElementById('newPasswordError').textContent = 'New password is required';
hasError = true;
} else if (newPassword.length < 6) {
document.getElementById('newPasswordError').textContent = 'Password must be at least 6 characters';
hasError = true;
}
if (!confirmPassword) {
document.getElementById('confirmPasswordError').textContent = 'Please confirm your password';
hasError = true;
} else if (newPassword !== confirmPassword) {
document.getElementById('confirmPasswordError').textContent = 'Passwords do not match';
hasError = true;
}
if (hasError) return;
try {
const saveBtn = document.getElementById('savePasswordBtn');
saveBtn.disabled = true;
saveBtn.textContent = 'Changing Password...';
const response = await fetchWithAuth(`${API_BASE}/profile`, {
method: 'PATCH',
body: JSON.stringify({
userDetails: {
currentPassword,
newPassword,
confirmPassword
}
})
});
if (!response) {
saveBtn.disabled = false;
saveBtn.textContent = 'Change Password';
return;
}
const result = await response.json();
if (result.success) {
alert('Password changed successfully!');
closePasswordModal();
} else {
alert(result.message || 'Failed to change password');
}
} catch (error) {
console.error('Error changing password:', error);
alert('Error changing password');
} finally {
const saveBtn = document.getElementById('savePasswordBtn');
saveBtn.disabled = false;
saveBtn.textContent = 'Change Password';
}
}
// Logout Modal Functions
function openLogoutModal() {
const modal = document.getElementById('logoutModal');
modal.style.display = 'flex';
document.body.style.overflow = 'hidden';
}
function closeLogoutModal() {
const modal = document.getElementById('logoutModal');
modal.style.display = 'none';
document.body.style.overflow = 'auto';
}
// Event listeners
document.addEventListener('DOMContentLoaded', function() {
loadProfileData();
// Change password button
document.getElementById('changePasswordBtn').addEventListener('click', openPasswordModal);
// Password form submission
document.getElementById('passwordForm').addEventListener('submit', changePassword);
// Close modal when clicking outside
document.getElementById('passwordModal').addEventListener('click', function(e) {
if (e.target === this) {
closePasswordModal();
}
});
// Close modal with escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closePasswordModal();
closeLogoutModal();
}
});
document.getElementById('logoutModal').addEventListener('click', function(e) {
if (e.target === this) {
closeLogoutModal();
}
});
// Logout button - opens confirmation modal instead of directly logging out
document.getElementById('logoutButton').addEventListener('click', openLogoutModal);
// Confirm logout button
document.getElementById('confirmLogoutBtn').addEventListener('click', handleLogout);
// Mobile menu toggle
const menuToggle = document.querySelector('.mobile-menu-button');
const sidebar = document.querySelector('.dashboard-sidebar');
menuToggle.addEventListener('click', () => {
sidebar.classList.toggle('active');
});
});
</script>
</body>
</html>

View File

@ -60,12 +60,6 @@
</div> --> </div> -->
<nav class="sidebar-nav"> <nav class="sidebar-nav">
<ul class="sidebar-list"> <ul class="sidebar-list">
<li class="sidebar-item">
<a href="/client/profile" class="sidebar-link">
<i class="fas fa-user sidebar-icon"></i>
<span>Profile</span>
</a>
</li>
<li class="sidebar-item"> <li class="sidebar-item">
<a href="/client/dashboard" class="sidebar-link"> <a href="/client/dashboard" class="sidebar-link">
<i class="fas fa-chart-bar sidebar-icon"></i> <i class="fas fa-chart-bar sidebar-icon"></i>
@ -84,6 +78,18 @@
<span>Projects</span> <span>Projects</span>
</a> </a>
</li> </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> -->
<li class="sidebar-item"> <li class="sidebar-item">
<a href="javascript:void(0);" class="sidebar-link" id="logoutButton"> <a href="javascript:void(0);" class="sidebar-link" id="logoutButton">
<i class="fas fa-sign-out-alt sidebar-icon"></i> <i class="fas fa-sign-out-alt sidebar-icon"></i>
@ -109,6 +115,7 @@
<th>Hourly Rate</th> <th>Hourly Rate</th>
<th>Total VA Assigned</th> <th>Total VA Assigned</th>
<th>Dedicated Hours</th> <th>Dedicated Hours</th>
<th>Status</th>
</tr> </tr>
</thead> </thead>
<tbody id="projectsTableBody"> <tbody id="projectsTableBody">
@ -130,11 +137,8 @@
</div> </div>
<div id="projectModal" class="modal-overlay"> <div id="projectModal" class="modal-overlay">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header" style="display: flex;align-items: center;justify-content: space-between;"> <div class="modal-header">
<h2 class="modal-title" id="modalProjectName">Project Details</h2> <h2 class="modal-title" id="modalProjectName">Project Details</h2>
<button class="modal-close" id="modalClose">
<i class="fas fa-times"></i>
</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="va-details-section"> <div class="va-details-section">
@ -167,28 +171,6 @@
</div> </div>
</div> </div>
</div> </div>
<div id="logoutModal" class="modal-overlay">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title">Confirm Logout</h2>
<button class="modal-close" onclick="closeLogoutModal()">
<i class="fas fa-times"></i>
</button>
</div>
<div class="logout-modal-body">
<div class="logout-icon">
<i class="fas fa-sign-out-alt"></i>
</div>
<h3 class="logout-title">Are you sure you want to logout?</h3>
<p class="logout-message">You will be logged out of your account and redirected to the login page.</p>
<div class="logout-actions">
<button class="logout-cancel-btn" onclick="closeLogoutModal()">Cancel</button>
<button class="logout-confirm-btn" id="confirmLogoutBtn">Yes, Logout</button>
</div>
</div>
</div>
</div>
</div> </div>
<script> <script>
// Theme management // Theme management
@ -202,7 +184,7 @@
} }
// Initialize theme // Initialize theme
const savedTheme = getSystemTheme(); const savedTheme = localStorage.getItem('theme') || getSystemTheme();
setTheme(savedTheme); setTheme(savedTheme);
// API base URL // API base URL
@ -370,6 +352,12 @@
<td> <td>
<span class="dedicated-hours">${dedicatedHours}</span> <span class="dedicated-hours">${dedicatedHours}</span>
</td> </td>
<td>
<div class="status-badge ${statusClass}">
<div class="status-dot ${statusClass}"></div>
<span class="status-text">${statusText}</span>
</div>
</td>
</tr> </tr>
`; `;
} }
@ -457,18 +445,6 @@
// Optional: Auto-refresh data every 30 seconds // Optional: Auto-refresh data every 30 seconds
setInterval(loadDashboardData, 30000); setInterval(loadDashboardData, 30000);
// Logout Modal Functions
function openLogoutModal() {
const modal = document.getElementById('logoutModal');
modal.style.display = 'flex';
document.body.style.overflow = 'hidden';
}
function closeLogoutModal() {
const modal = document.getElementById('logoutModal');
modal.style.display = 'none';
document.body.style.overflow = 'auto';
}
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Load dashboard data // Load dashboard data
@ -481,28 +457,11 @@
sidebar.classList.toggle('active'); sidebar.classList.toggle('active');
}); });
// Close modal with escape key // Logout button
document.addEventListener('keydown', function(e) { const logoutButton = document.getElementById('logoutButton');
if (e.key === 'Escape') { logoutButton.addEventListener('click', handleLogout);
closeLogoutModal();
}
}); });
document.getElementById('logoutModal').addEventListener('click', function(e) {
if (e.target === this) {
closeLogoutModal();
}
});
// Logout button - opens confirmation modal instead of directly logging out
document.getElementById('logoutButton').addEventListener('click', openLogoutModal);
// Confirm logout button
document.getElementById('confirmLogoutBtn').addEventListener('click', handleLogout);
});
modalClose.addEventListener('click', closeProjectModal);
</script> </script>
</body> </body>

View File

@ -34,6 +34,16 @@
<i class="fas fa-file-invoice"></i>Projects <i class="fas fa-file-invoice"></i>Projects
</a> </a>
</li> </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> </ul>
<button class="mobile-menu-button"> <button class="mobile-menu-button">
@ -44,14 +54,11 @@
</nav> </nav>
<div class="dashboard-container"> <div class="dashboard-container">
<aside class="dashboard-sidebar"> <aside class="dashboard-sidebar">
<!-- <div class="sidebar-header">
<h2 class="sidebar-title">DocVA Client Portal</h2>
</div> -->
<nav class="sidebar-nav"> <nav class="sidebar-nav">
<ul class="sidebar-list"> <ul class="sidebar-list">
<li class="sidebar-item">
<a href="/client/profile" class="sidebar-link">
<i class="fas fa-user sidebar-icon"></i>
<span>Profile</span>
</a>
</li>
<li class="sidebar-item"> <li class="sidebar-item">
<a href="/client/dashboard" class="sidebar-link"> <a href="/client/dashboard" class="sidebar-link">
<i class="fas fa-chart-bar sidebar-icon"></i> <i class="fas fa-chart-bar sidebar-icon"></i>
@ -70,6 +77,18 @@
<span>Projects</span> <span>Projects</span>
</a> </a>
</li> </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> -->
<li class="sidebar-item"> <li class="sidebar-item">
<a href="javascript:void(0);" class="sidebar-link" id="logoutButton"> <a href="javascript:void(0);" class="sidebar-link" id="logoutButton">
<i class="fas fa-sign-out-alt sidebar-icon"></i> <i class="fas fa-sign-out-alt sidebar-icon"></i>
@ -123,6 +142,7 @@
<div class="content-section"> <div class="content-section">
<div class="section-header"> <div class="section-header">
<h2 class="section-title">Virtual Assistants <span class="total-count" id="vaListCount">0 VAs</span></h2> <h2 class="section-title">Virtual Assistants <span class="total-count" id="vaListCount">0 VAs</span></h2>
</div> </div>
<div class="table-container"> <div class="table-container">
<div class="table-wrapper"> <div class="table-wrapper">
@ -131,9 +151,9 @@
<tr> <tr>
<th>Status</th> <th>Status</th>
<th>Virtual Assistant</th> <th>Virtual Assistant</th>
<th>Active Project</th> <th>Projects Assigned</th>
<th>Total Projects</th> <th>Active Projects</th>
<th>Today Logged Hours</th> <th>Worked Time</th>
<th>Action</th> <th>Action</th>
</tr> </tr>
</thead> </thead>
@ -154,77 +174,6 @@
</div> </div>
</main> </main>
</div> </div>
<div id="vaModal" class="modal-overlay">
<div class="modal-content">
<div class="modal-header" style="display: flex;align-items: center;justify-content: space-between;">
<h2 class="modal-title" id="modalVAName">VA Details</h2>
<button class="modal-close" onclick="closeVAModal()">
<i class="fas fa-times"></i>
</button>
</div>
<div class="modal-body">
<!-- Projects Section -->
<div class="va-details-section">
<h3 class="va-section-title">
<i class="fas fa-briefcase"></i>
Projects
<span class="va-count-badge" id="projectsCountBadge">0</span>
</h3>
<div class="va-table-container">
<table class="va-table">
<thead>
<tr>
<th>Project Name</th>
<th>Hourly Rate</th>
<th>Worked Time</th>
<th>Status</th>
</tr>
</thead>
<tbody id="modalProjectsTableBody">
<!-- Project details will be loaded here -->
</tbody>
</table>
<div id="modalNoProjects" class="no-vas-message" style="display: none;">
<i class="fas fa-briefcase"></i>
<h4>No Projects</h4>
<p>This VA is not assigned to any projects.</p>
</div>
</div>
</div>
<!-- Action Button -->
<div class="modal-actions" id="modalActions" style="display: none;">
<a href="#" class="view-session-btn" id="viewSessionBtn" target="_blank">
<i class="fas fa-eye"></i>
View Live Session
</a>
</div>
</div>
</div>
</div>
<div id="logoutModal" class="modal-overlay">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title">Confirm Logout</h2>
<button class="modal-close" onclick="closeLogoutModal()">
<i class="fas fa-times"></i>
</button>
</div>
<div class="logout-modal-body">
<div class="logout-icon">
<i class="fas fa-sign-out-alt"></i>
</div>
<h3 class="logout-title">Are you sure you want to logout?</h3>
<p class="logout-message">You will be logged out of your account and redirected to the login page.</p>
<div class="logout-actions">
<button class="logout-cancel-btn" onclick="closeLogoutModal()">Cancel</button>
<button class="logout-confirm-btn" id="confirmLogoutBtn">Yes, Logout</button>
</div>
</div>
</div>
</div>
</div> </div>
<script> <script>
// Theme management // Theme management
@ -238,7 +187,7 @@
} }
// Initialize theme // Initialize theme
const savedTheme = getSystemTheme(); const savedTheme = localStorage.getItem('theme') || getSystemTheme();
setTheme(savedTheme); setTheme(savedTheme);
// API base URL // API base URL
@ -293,7 +242,7 @@
// Format time function // Format time function
function formatTime(seconds) { function formatTime(seconds) {
if (!seconds) return 'No Record Found'; 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);
@ -307,88 +256,20 @@
} }
} }
const vaModal = document.getElementById('vaModal'); // Create VA card HTML
function openVAModal(va) {
// Set basic VA info
document.getElementById('modalVAName').textContent = va.name;
// Set projects
const projects = va.projects || [];
document.getElementById('projectsCountBadge').textContent = projects.length;
const projectsTableBody = document.getElementById('modalProjectsTableBody');
const noProjectsMessage = document.getElementById('modalNoProjects');
if (projects.length > 0) {
projectsTableBody.innerHTML = projects.map(project => createProjectTableRow(project)).join('');
projectsTableBody.style.display = '';
noProjectsMessage.style.display = 'none';
} else {
projectsTableBody.innerHTML = '';
projectsTableBody.style.display = 'none';
noProjectsMessage.style.display = 'block';
}
// Set action button
const modalActions = document.getElementById('modalActions');
const viewSessionBtn = document.getElementById('viewSessionBtn');
if (va.isCurrentlyActive && va.lastProjectId) {
viewSessionBtn.href = `/live/${va.lastProjectId}/${va.id}`;
modalActions.style.display = 'block';
} else {
modalActions.style.display = 'none';
}
// Show modal
vaModal.style.display = 'flex';
document.body.style.overflow = 'hidden';
}
function closeVAModal() {
vaModal.style.display = 'none';
document.body.style.overflow = 'auto';
}
// Create project table row for modal
function createProjectTableRow(project) {
const statusClass = project.isCurrentlyWorking ? 'active' : 'inactive';
const statusText = project.isCurrentlyWorking ? 'Working' : 'Not Active';
return `
<tr>
<td>
<div class="project-info">
<div class="project-name">${project.name}</div>
</div>
</td>
<td>
<span class="hourly-rate">$${project.hourlyRate}/hr</span>
</td>
<td>
<span class="worked-time">${project.totalWorkedTime}</span>
</td>
<td>
<div class="status-badge ${statusClass}">
<span class="status-text">${statusText}</span>
</div>
</td>
</tr>
`;
}
// Create VA table row for main table
function createVATableRow(va) { function createVATableRow(va) {
const statusClass = va.isCurrentlyActive ? 'active' : 'inactive'; const statusClass = va.isCurrentlyActive ? 'active' : 'inactive';
const statusText = va.isCurrentlyActive ? 'Active' : 'Inactive'; const statusText = va.isCurrentlyActive ? 'Active' : 'Inactive';
const activeProject = va?.lastProject?.name ? va.lastProject.name : '-'; const activeProject = va?.lastProject?.name ? va.lastProject.name : '-';
const workedTime = formatTime(va.currentSessionDuration); const workedTime = formatTime(va.currentSessionDuration);
const projectsAssigned = va.projectsAssigned || 0; const projectsAssigned = va.projectsAssigned || 0;
const viewLink = va.isCurrentlyActive ? `<a href="/live/${va.lastProjectId}/${va.id}" class="view-link" data-va-id="${va.id}" target="_blank">Live Tracking</a>` : ''; 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>
View
</a>` : '';
return ` return `
<tr class="va-row" data-va-id="${va.id}"> <tr>
<td> <td>
<div class="status-indicator"> <div class="status-indicator">
<span class="status-text ${statusClass}">${statusText}</span> <span class="status-text ${statusClass}">${statusText}</span>
@ -422,6 +303,7 @@
const response = await fetchWithAuth(`${API_BASE}/dashboard`); const response = await fetchWithAuth(`${API_BASE}/dashboard`);
if (!response) return; if (!response) return;
const result = await response.json(); const result = await response.json();
// console.log(data)
if (result.success) { if (result.success) {
const data = result.data; const data = result.data;
@ -438,26 +320,12 @@
// Update VA table // Update VA table
const vaTableBody = document.getElementById('vaTableBody'); const vaTableBody = document.getElementById('vaTableBody');
const vaEmptyState = document.getElementById('vaEmptyState'); const vaEmptyState = document.getElementById('vaEmptyState');
const allVAs = data.allVAs || []; const allVAs = data.allVAs?.total || [];
if (allVAs.length > 0) { if (allVAs.length > 0) {
vaTableBody.innerHTML = allVAs.map(va => createVATableRow(va)).join(''); vaTableBody.innerHTML = allVAs.map(va => createVATableRow(va)).join('');
vaTableBody.style.display = ''; vaTableBody.style.display = '';
vaEmptyState.style.display = 'none'; vaEmptyState.style.display = 'none';
// Store VA data globally for modal access
window.vaData = allVAs;
// Add click event listeners to VA rows
document.querySelectorAll('.va-row').forEach((row, index) => {
row.addEventListener('click', function(e) {
// Don't open modal if clicking on view link
if (!e.target.closest('.view-link')) {
const va = allVAs[index];
openVAModal(va);
}
});
});
} else { } else {
vaTableBody.innerHTML = ''; vaTableBody.innerHTML = '';
vaTableBody.style.display = 'none'; vaTableBody.style.display = 'none';
@ -491,36 +359,9 @@
vaEmptyState.style.display = 'block'; vaEmptyState.style.display = 'block';
} }
// Close modal when clicking outside
vaModal.addEventListener('click', (e) => {
if (e.target === vaModal) {
closeVAModal();
}
});
// Close modal with escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && vaModal.style.display === 'flex') {
closeVAModal();
}
});
// Optional: Auto-refresh data every 30 seconds // Optional: Auto-refresh data every 30 seconds
setInterval(loadDashboardData, 30000); setInterval(loadDashboardData, 30000);
// Logout Modal Functions
function openLogoutModal() {
const modal = document.getElementById('logoutModal');
modal.style.display = 'flex';
document.body.style.overflow = 'hidden';
}
function closeLogoutModal() {
const modal = document.getElementById('logoutModal');
modal.style.display = 'none';
document.body.style.overflow = 'auto';
}
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Load dashboard data // Load dashboard data
loadDashboardData(); loadDashboardData();
@ -532,25 +373,11 @@
sidebar.classList.toggle('active'); sidebar.classList.toggle('active');
}); });
// Close modal with escape key // Logout button
document.addEventListener('keydown', function(e) { const logoutButton = document.getElementById('logoutButton');
if (e.key === 'Escape') { logoutButton.addEventListener('click', handleLogout);
closeLogoutModal();
}
}); });
document.getElementById('logoutModal').addEventListener('click', function(e) {
if (e.target === this) {
closeLogoutModal();
}
});
// Logout button - opens confirmation modal instead of directly logging out
document.getElementById('logoutButton').addEventListener('click', openLogoutModal);
// Confirm logout button
document.getElementById('confirmLogoutBtn').addEventListener('click', handleLogout);
});
</script> </script>
</body> </body>

View File

@ -144,7 +144,7 @@
<div class="input-container"> <div class="input-container">
<!-- <input type="text" id="state" class="input-field" placeholder=" " required> --> <!-- <input type="text" id="state" class="input-field" placeholder=" " required> -->
<select id="state" class="select-field" required> <select id="state" class="select-field" required>
<option value="" disable selected style="display: none"></option> <option value=""></option>
<!-- States will be populated by JavaScript --> <!-- States will be populated by JavaScript -->
</select> </select>
<label for="state" class="input-label">State *</label> <label for="state" class="input-label">State *</label>
@ -157,7 +157,7 @@
<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;"> <div class="invalid-feedback" id="zip-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
Invalid Zip code ZIP code should contain only numbers
</div> </div>
</div> </div>
<div class="form-actions"> <div class="form-actions">
@ -203,7 +203,7 @@
<div class="input-container"> <div class="input-container">
<!-- <input type="text" id="billing-state" class="input-field" placeholder=" " required> --> <!-- <input type="text" id="billing-state" class="input-field" placeholder=" " required> -->
<select id="billing-state" class="select-field" required> <select id="billing-state" class="select-field" required>
<option value="" disable selected style="display: none"></option> <option value=""></option>
<!-- States will be populated by JavaScript --> <!-- States will be populated by JavaScript -->
</select> </select>
<label for="billing-state" class="input-label">State *</label> <label for="billing-state" class="input-label">State *</label>
@ -353,7 +353,7 @@
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
<select id="account-type" class="select-field" required> <select id="account-type" class="select-field" required>
<option value="" disable selected style="display: none"></option> <option value=""></option>
<option value="checking">Checking</option> <option value="checking">Checking</option>
<option value="savings">Savings</option> <option value="savings">Savings</option>
</select> </select>
@ -390,19 +390,5 @@
</div> </div>
</div> </div>
<script src="/public/js/va-client-register.js"></script> <script src="/public/js/va-client-register.js"></script>
<script>
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 = getSystemTheme();
setTheme(savedTheme);
</script>
</body> </body>
</html> </html>

View File

@ -11,6 +11,87 @@
select option:disabled { select option:disabled {
display: none; display: none;
} }
/* body {
background-color: #f8f9fa;
font-family: 'Segoe UI', sans-serif;
}
.card-registration {
border: none;
border-radius: 12px;
overflow: hidden;
}
.card-registration img {
height: 100%;
object-fit: cover;
}
.card-body {
padding: 3rem !important;
}
h3 {
font-weight: 600;
color: #343a40;
margin-bottom: 2rem;
}
.form-outline {
position: relative;
}
.form-control-lg {
border-radius: 8px;
padding: 0.75rem 1rem;
font-size: 1rem;
}
.form-label {
font-weight: 500;
color: #495057;
margin-bottom: 0.5rem;
}
.btn-lg {
border-radius: 8px;
padding: 0.7rem 2rem;
font-size: 1rem;
}
.btn-warning {
background-color: #ffc107;
border: none;
font-weight: 600;
}
.btn-warning:hover {
background-color: #e0a800;
}
.btn-light {
border: 1px solid #ccc;
font-weight: 500;
}
h5 {
font-weight: 600;
color: #495057;
border-bottom: 2px solid #ffc107;
display: inline-block;
padding-bottom: 0.25rem;
margin-top: 2rem;
}
@media (max-width: 991px) {
.card-body {
padding: 2rem !important;
}
.card-registration img {
display: none;
}
} */
</style> </style>
</head> </head>
@ -185,28 +266,17 @@
<option value="" disabled selected></option> <option value="" disabled selected></option>
</select> </select>
<label for="country" class="input-label">Country *</label> <label for="country" class="input-label">Country *</label>
<!-- <div class="location-loading" id="country-loading" style="display: none;">Detecting your location...</div> -->
</div> </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="state" class="input-field" placeholder=" " required> <select id="state" class="select-field" required onchange="handleChangeSelect(this)">
<!-- <select id="state" class="select-field" required onchange="handleChangeSelect(this)"> <!-- <option value=""></option> -->
<option value=""></option>
</select> -->
<label for="state" class="input-label">State / Region *</label>
<!-- <div class="location-loading" id="state-loading">Loading states...</div> -->
</div>
</div>
<div class="form-group">
<div class="input-container">
<select id="status" class="select-field" required>
<option value="FULLTIME" selected>Full Time</option>
<option value="PARTTIME">Part Time</option>
</select> </select>
<label for="status" class="input-label">Status *</label> <label for="state" class="input-label">State / Region *</label>
<div class="location-loading" id="state-loading">Loading states...</div>
</div> </div>
</div> </div>
@ -228,19 +298,19 @@
<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=" " pattern="[0-9]*" minlength="10" maxlength="15" oninput="validatePhoneNumber(this)" 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;"> <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) Please enter a valid phone number (10-15 digits)
</div> </div>
</div> </div>
<!-- <div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
<input type="text" id="skype" class="input-field" placeholder=" " oninput="validateSkypeId(this)"> <input type="text" id="skype" class="input-field" placeholder=" " oninput="validateSkypeId(this)">
<label for="skype" class="input-label">Skype ID</label> <label for="skype" class="input-label">Skype ID</label>
</div> </div>
</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>
@ -257,7 +327,7 @@
</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')">
<h4>Payoneer</h4> <h4>Payoneer</h4>
<p>Receive payments via Payoneer</p> <p>Receive payments via Payoneer</p>
</div> </div>
@ -271,7 +341,7 @@
Account number must be 5-20 characters Account number must be 5-20 characters
</div> </div>
</div> </div>
</div> --> </div>
<div class="payment-option" onclick="selectPaymentMethod('veem')"> <div class="payment-option" onclick="selectPaymentMethod('veem')">
<h4>Veem</h4> <h4>Veem</h4>
@ -288,22 +358,6 @@
</div> </div>
</div> </div>
</div> </div>
<div class="payment-option" onclick="selectPaymentMethod('wise')">
<h4>Wise</h4>
<p>Receive payments via Wise</p>
</div>
<div id="wise-details" class="payment-details">
<div class="form-group">
<div class="input-container">
<input type="email" id="wiseEmail" class="input-field" placeholder=" " oninput="validateWiseEmail(this)" required>
<label for="wiseEmail" class="input-label">Wise Email Address *</label>
</div>
<div class="invalid-feedback" id="wise-email-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
Please enter a valid email address for Wise
</div>
</div>
</div>
</div> </div>
<div class="form-actions"> <div class="form-actions">
@ -326,19 +380,5 @@
</div> </div>
</div> </div>
<script src="/public/js/va-register.js"></script> <script src="/public/js/va-register.js"></script>
<script>
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 = getSystemTheme();
setTheme(savedTheme);
</script>
</body> </body>
</html> </html>