Compare commits

..

11 Commits

34 changed files with 3613 additions and 820 deletions

File diff suppressed because it is too large Load Diff

View File

@ -5,9 +5,52 @@
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 {
background-color: #fff;
color: #333;
background-color: var(--bg-primary);
color: var(--text-primary);
transition: background-color 0.3s ease, color 0.3s ease;
}
.container {

View File

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

View File

@ -18,56 +18,104 @@
{ code: 'FR', name: 'France' },
{ code: 'BR', name: 'Brazil' },
{ 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 = {
'US': [
{ 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: 'OH', name: 'Ohio' }, { code: 'GA', name: 'Georgia' }, { code: 'NC', name: 'North Carolina' },
{ code: 'MI', name: 'Michigan' }
],
'CA': [
{ 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' }
],
'GB': [
{ code: 'ENG', name: 'England' }, { code: 'SCT', name: 'Scotland' }, { code: 'WLS', name: 'Wales' },
{ code: 'NIR', name: 'Northern Ireland' }
],
'AU': [
{ 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' }
],
'IN': [
{ 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' }
],
'DE': [
{ 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' }
],
'FR': [
{ 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: 'COR', name: 'Corsica' }, { code: 'GES', name: 'Grand Est' }
],
'BR': [
{ 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á' }
],
'JP': [
{ code: 'TK', name: 'Tokyo' }, { code: 'OS', name: 'Osaka' }, { code: 'KY', name: 'Kyoto' },
{ code: 'HK', name: 'Hokkaido' }, { code: 'OK', name: 'Okinawa' }, { code: 'KN', name: 'Kanagawa' }
],
'CN': [
{ code: 'BJ', name: 'Beijing' }, { code: 'SH', name: 'Shanghai' }, { code: 'TJ', name: 'Tianjin' },
{ code: 'CQ', name: 'Chongqing' }, { code: 'GD', name: 'Guangdong' }, { code: 'JS', name: 'Jiangsu' }
]
};
// const states = {
// 'US': [
// { 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: 'OH', name: 'Ohio' }, { code: 'GA', name: 'Georgia' }, { code: 'NC', name: 'North Carolina' },
// { code: 'MI', name: 'Michigan' }
// ],
// 'CA': [
// { 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' }
// ],
// 'GB': [
// { code: 'ENG', name: 'England' }, { code: 'SCT', name: 'Scotland' }, { code: 'WLS', name: 'Wales' },
// { code: 'NIR', name: 'Northern Ireland' }
// ],
// 'AU': [
// { 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' }
// ],
// 'IN': [
// { 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' }
// ],
// 'DE': [
// { 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' }
// ],
// 'FR': [
// { 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: 'COR', name: 'Corsica' }, { code: 'GES', name: 'Grand Est' }
// ],
// 'BR': [
// { 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á' }
// ],
// 'JP': [
// { code: 'TK', name: 'Tokyo' }, { code: 'OS', name: 'Osaka' }, { code: 'KY', name: 'Kyoto' },
// { code: 'HK', name: 'Hokkaido' }, { code: 'OK', name: 'Okinawa' }, { code: 'KN', name: 'Kanagawa' }
// ],
// 'CN': [
// { code: 'BJ', name: 'Beijing' }, { code: 'SH', name: 'Shanghai' }, { code: 'TJ', name: 'Tianjin' },
// { code: 'CQ', name: 'Chongqing' }, { code: 'GD', name: 'Guangdong' }, { code: 'JS', name: 'Jiangsu' }
// ]
// };
// Initialize the form
document.addEventListener('DOMContentLoaded', function() {
// Populate country dropdown
const countrySelect = document.getElementById('country');
@ -80,32 +128,34 @@
countrySelect.appendChild(option);
});
autoDetectCountry();
// Add event listener for country change
countrySelect.addEventListener('change', function() {
const countryCode = this.value;
const stateSelect = document.getElementById('state');
// countrySelect.addEventListener('change', function() {
// const countryCode = this.value;
// const stateSelect = document.getElementById('state');
// Show loading
document.getElementById('state-loading').style.display = 'block';
// // Show loading
// document.getElementById('state-loading').style.display = 'block';
// Clear current states
stateSelect.innerHTML = '<option value="" disabled selected></option>';
// // Clear current states
// stateSelect.innerHTML = '<option value="" disabled selected></option>';
// Simulate API delay
setTimeout(() => {
if (countryCode && states[countryCode]) {
states[countryCode].forEach(state => {
const option = document.createElement('option');
option.value = state.code;
option.textContent = state.name;
stateSelect.appendChild(option);
});
}
// // Simulate API delay
// setTimeout(() => {
// if (countryCode && states[countryCode]) {
// states[countryCode].forEach(state => {
// const option = document.createElement('option');
// option.value = state.code;
// option.textContent = state.name;
// stateSelect.appendChild(option);
// });
// }
// Hide loading
document.getElementById('state-loading').style.display = 'none';
}, 500);
});
// // Hide loading
// document.getElementById('state-loading').style.display = 'none';
// }, 500);
// });
// Add input event listeners to handle label animation
@ -209,6 +259,102 @@
});
});
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 goToStep(step) {
// Only allow navigation to completed steps or the next step
@ -471,17 +617,26 @@
} else {
const paymentMethod = selectedPayment.getAttribute('onclick').match(/'([^']+)'/)[1];
if (paymentMethod === 'payoneer') {
const payoneerAccount = document.getElementById('payoneerAccount').value;
if (!payoneerAccount) {
showError('Please enter your Payoneer account number', 'payoneerAccount', step);
isValid = false;
}
// const payoneerAccount = document.getElementById('payoneerAccount').value;
// if (!payoneerAccount) {
// showError('Please enter your Payoneer account number', 'payoneerAccount', step);
// isValid = false;
// }
} else if (paymentMethod === 'veem') {
const veemAccount = document.getElementById('veemAccount').value;
if (!veemAccount) {
showError('Please enter your Veem account number', 'veemAccount', step);
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;
}
}
}
}
@ -536,10 +691,11 @@
formData.userDetails.city = document.getElementById('city').value;
formData.userDetails.state = document.getElementById('state').value;
formData.userDetails.country = document.getElementById('country').value;
formData.status = document.getElementById('status').value;
break;
case 3:
formData.userDetails.phone = document.getElementById('phone').value;
formData.userDetails.skype = document.getElementById('skype').value;
// formData.userDetails.skype = document.getElementById('skype').value;
break;
case 4:
const selectedPayment = document.querySelector('.payment-option.selected');
@ -547,12 +703,15 @@
const paymentMethod = selectedPayment.getAttribute('onclick').match(/'([^']+)'/)[1];
formData.userDetails.paymentMethod = paymentMethod;
if (document.getElementById('payoneerAccount').value) {
formData.userDetails.payoneerAccount = document.getElementById('payoneerAccount').value;
}
// if (document.getElementById('payoneerAccount').value) {
// formData.userDetails.payoneerAccount = document.getElementById('payoneerAccount').value;
// }
if (document.getElementById('veemAccount').value) {
formData.userDetails.veemAccount = document.getElementById('veemAccount').value;
}
if (paymentMethod === 'wise') {
formData.userDetails.wiseEmail = document.getElementById('wiseEmail').value;
}
}
break;
}
@ -575,12 +734,35 @@
// Show appropriate payment details
if (method === 'payoneer') {
document.getElementById('payoneer-details').style.display = 'block';
// document.getElementById('payoneer-details').style.display = 'block';
} else if (method === 'veem') {
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 generatePassword() {
const length = 12;
@ -969,7 +1151,7 @@ function validateSkypeId(input) {
if (value && !/^[a-zA-Z][a-zA-Z0-9\.,\-_]+$/.test(value)) {
input.style.border = '1px solid #ef4444';
showError('Skype ID must start with a letter and can only contain letters, numbers, dots, hyphens, and underscores', 'skype', 3);
showError('Invalid Skype ID', 'skype', 3);
return false;
} else {
input.style.border = '';

View File

@ -68,12 +68,7 @@ async function client_dashboard(req, res) {
$cond: [{ $eq: ["$loggedType", "LOGIN"] }, 1, 0]
}
},
lastActivity: { $first: "$time" },
projectsWorked: {
$addToSet: "$projectId"
},
// Get all login logs for this VA
allLogs: { $push: "$$ROOT" }
lastActivity: { $first: "$time" }
}
},
{
@ -85,22 +80,12 @@ async function client_dashboard(req, res) {
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
const activityByVa = agg.reduce((acc, a) => {
acc[String(a._id)] = {
lastLog: a.lastLog,
totalLogins: a.totalLogins,
lastActivity: a.lastActivity,
projectsWorked: a.projectsWorked || []
lastActivity: a.lastActivity
};
return acc;
}, {});
@ -126,14 +111,14 @@ async function client_dashboard(req, res) {
logsByVa[vaId].push(log);
});
// Calculate total worked time for each VA using the same logic as va_log_work_report_callback
// Calculate total worked time for each VA
for (const [vaId, logs] of Object.entries(logsByVa)) {
let totalWorkSeconds = 0;
// Group logs by date (YYYY-MM-DD format)
const logsByDate = {};
logs.forEach(log => {
const date = new Date(log.time).toISOString().split('T')[0]; // YYYY-MM-DD
const date = new Date(log.time).toISOString().split('T')[0];
if (!logsByDate[date]) {
logsByDate[date] = [];
}
@ -147,7 +132,6 @@ async function client_dashboard(req, res) {
let dayWorkSeconds = 0;
let lastLogin = null;
let lastProjectId = null;
// If day has adjustment logs, only process adjustment logs
// If no adjustment logs, process all logs for that day
@ -164,14 +148,10 @@ async function client_dashboard(req, res) {
if (log.loggedType === "LOGIN") {
lastLogin = time;
lastProjectId = log.projectId?.toString() || null;
} else if (log.loggedType === "LOGOUT" && lastLogin) {
const diffSec = (time - lastLogin) / 1000;
dayWorkSeconds += diffSec;
// reset after each pair
lastLogin = null;
lastProjectId = null;
}
}
@ -184,25 +164,93 @@ 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
const currentTime = new Date();
// Format all assigned VAs
// Format all assigned VAs - Only include data needed for frontend
const allVAsFormatted = allAssignedVAs.map((v) => {
const activity = activityByVa[String(v._id)] || {};
const workedTime = totalWorkedTimeByVa[String(v._id)] || { totalSeconds: 0, formatted: '00:00:00' };
const lastLog = activity.lastLog;
const projectWorkTime = projectWorkTimeByVa[String(v._id)] || {};
// Calculate current session duration if still logged in
let currentSessionDuration = null;
let isCurrentlyActive = false;
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);
isCurrentlyActive = true;
// Consider VA inactive if last login was more than 24 hours ago
@ -223,6 +271,29 @@ async function client_dashboard(req, res) {
// Combine first and last name
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 {
id: v._id,
name: fullName,
@ -231,99 +302,18 @@ async function client_dashboard(req, res) {
status: v.status || 'active',
isCurrentlyActive: isCurrentlyActive,
lastLoginTime: lastLog?.time || null,
lastActivity: activity.lastActivity,
currentSessionDuration: currentSessionDuration,
currentSessionFormatted: currentSessionFormatted,
totalWorkedTime: workedTime.formatted,
totalWorkedSeconds: workedTime.totalSeconds,
totalLogins: activity.totalLogins || 0,
lastProject: currentProjectDetails,
lastProjectId: lastLog?.projectId || null,
projectsWorked: activity.projectsWorked || [],
projectsAssigned: v.projectIds?.length || 0,
projectsAssigned: clientProjectsAssigned.length,
// Detailed project information for popup
projects: projectsWithWorkTime,
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
const totalHoursLogged = clientProjects.reduce((sum, p) => sum + (p.totalLoggedHours || 0), 0);
const totalDedicatedHours = clientProjects.reduce((sum, p) => sum + (p.dedicatedHours || 0), 0);
@ -334,45 +324,22 @@ async function client_dashboard(req, res) {
// Calculate total worked time across all VAs
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({
success: true,
data: {
// All assigned VAs with their status
allVAs: {
active: currentlyActiveVAs,
inactive: inactiveVAs,
total: allVAsFormatted
},
// Currently working VAs (active session)
activeVAs: activeVAsFormatted,
// All client projects
projects: projectsFormatted
// Only send data that frontend actually uses
allVAs: allVAsFormatted
},
summary: {
totalVAs: allAssignedVAs.length,
activeVAs: currentlyActiveVAs.length,
inactiveVAs: inactiveVAs.length,
totalProjects: clientProjects.length,
projectsWithActiveWorkers: projectsWithActiveWorkers,
totalHoursLogged: totalHoursLogged,
totalDedicatedHours: totalDedicatedHours,
totalWorkedTime: formatTime(totalWorkedTimeAllVAs),
utilizationRate: totalDedicatedHours > 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
((totalHoursLogged / totalDedicatedHours) * 100).toFixed(2) + '%' : '0%'
}
});
@ -386,7 +353,7 @@ async function client_dashboard(req, res) {
}
}
// Helper function to format time (same as in va_log_work_report_callback)
// Helper function to format time
function formatTime(seconds) {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);

View File

@ -1,6 +1,7 @@
import VaClientRepository from "../../repository/vaClient.repository.js";
import VaClientService from "../../services/vaClient.service.js";
import logger from "../../config/logger.js";
import jwt from "jsonwebtoken";
const vaClientService = new VaClientService(new VaClientRepository());
@ -51,9 +52,9 @@ async function va_register_client(req, res) {
}
// Additional controller methods can be added here
async function get_client_by_email(req, res) {
async function get_client_profile(req, res) {
try {
const { email } = req.params;
const email = req.body.clientEmail;
const client = await vaClientService.getClientByEmail(email);
if (!client) {
@ -76,7 +77,233 @@ async function get_client_by_email(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 {
va_register_client,
get_client_by_email
get_client_profile,
delete_client,
update_client,
get_client_dashboard_profile,
update_client_password
};

View File

@ -0,0 +1,97 @@
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) {
try {
const payload = req.body|| {};
const { name, hourlyRate, clientId, vaAssigned, dedicatedHours } = payload;
const { name, hourlyRate, clientId, vaAssigned, dedicatedHours, vaWorkingScheduleStart, vaWorkingScheduleEnd, isFlexibleAllowedPt, vaWorkingDelayAllowedStart, vaWorkingDelayAllowedEnd } = payload;
// Validate required fields
if (!name) {
@ -65,6 +65,11 @@ async function project_add_sf(req, res) {
clientId: clientObjectId,
vaAssigned: vaAssignedArray,
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);
@ -81,36 +86,8 @@ async function project_add_sf(req, res) {
console.warn('Project created but relationship update failed:', relationshipUpdate.message);
}
// 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) {
console.error("project_add_sf error:", error);
return res.status(500).json({ success: false, message: "Internal Server Error" });

View File

@ -16,7 +16,12 @@ async function update_project_sf(req, res) {
vaAssigned,
dedicatedHours,
status,
totalLoggedHours
totalLoggedHours ,
vaWorkingScheduleStart,
vaWorkingScheduleEnd,
isFlexibleAllowedPt,
vaWorkingDelayAllowedStart,
vaWorkingDelayAllowedEnd
} = payload;
// Validate projectId
@ -88,6 +93,27 @@ async function update_project_sf(req, res) {
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
if (vaAssigned !== undefined) {
let newVaAssignedArray = [];

View File

@ -131,6 +131,7 @@ async function va_leave_apply_function(req, res) {
});
}
const processedComment = processHTMLComment(comment);
// Create leave request
const leaveRequest = new LeavesModel({
vaId,
@ -138,62 +139,62 @@ async function va_leave_apply_function(req, res) {
dateTo: endDate,
days: totalDays.toString(),
leaveType,
comment,
comment: processedComment.processedHTML,
attachments: processedComment.extractedImages,
status: "PENDING"
});
await leaveRequest.save();
try {
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/timesheet-adjustment`;
const sfData = {
"id": adjustmentRequest._id,
"va": {
"id": vaId
},
"project": {
"id": projectId
},
"date": adjustmentDate.toISOString().split('T')[0],
"loginTime": loginDateTime.toISOString(),
"logoutTime": logoutDateTime.toISOString(),
"comment": comment,
"status": "PENDING",
"rejectionReason": "",
"adjustedHours": formatTime(adjustedSeconds)
}
// const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/timesheet-adjustment`;
// const sfData = {
// "id": leaveRequest._id,
// "va": {
// "id": vaId
// },
// "dateFrom": startDate.toISOString().split('T')[0],
// "dateTo": endDate.toISOString().split('T')[0],
// "totalDays": totalDays,
// "leaveType": leaveType,
// "comment": processedComment.plainText, // Send plain text to Salesforce
// "attachments": processedComment.extractedImages, // Send extracted images info
// "status": "PENDING",
// "rejectionReason": ""
// }
const response = await fetch(salesforceUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// Add any required headers for Salesforce authentication
// 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
},
body: JSON.stringify({"data": sfData})
});
// const response = await fetch(salesforceUrl, {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json',
// // Add any required headers for Salesforce authentication
// // 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
// },
// body: JSON.stringify({"data": sfData})
// });
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
}
// if (!response.ok) {
// const errorText = await response.text();
// throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
// }
const result = await response.json();
// console.log("✅ Successfully synced VA data to Salesforce:", result);
// const result = await response.json();
// // console.log("✅ Successfully synced VA data to Salesforce:", result);
return res.status(201).json({
success: true,
message: "Adjustment request submitted successfully",
message: "Leave request submitted successfully",
data: {
adjustmentId: adjustmentRequest._id,
date: adjustmentDate.toISOString().split('T')[0],
loginTime: loginDateTime.toISOString(),
logoutTime: logoutDateTime.toISOString(),
adjustedHours: formatTime(adjustedSeconds),
comment: comment,
leaveId: leaveRequest._id,
dateFrom: startDate.toISOString().split('T')[0],
dateTo: endDate.toISOString().split('T')[0],
totalDays: totalDays,
leaveType,
comment: processedComment.processedHTML,
// attachments: processedComment.extractedImages,
status: "PENDING"
},
SyncSalesforce: result
// SyncSalesforce: result
});
} catch (error) {
console.error("Failed to sync with Salesforce:", error.message);
@ -238,4 +239,116 @@ 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 };

View File

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

View File

@ -0,0 +1,46 @@
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

@ -0,0 +1,74 @@
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

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

View File

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

View File

@ -1,12 +1,12 @@
import { LoggedModel, WorkTrackModel, ProjectModel } from "../../../models/vaModel.js";
import { LoggedModel, WorkTrackModel, ProjectModel, VAModel } from "../../../models/vaModel.js";
// Helper function to get start and end of day
// Helper function to get start and end of day in UTC
function getDayRange(date = new Date()) {
const start = new Date(date);
start.setHours(0, 0, 0, 0);
start.setUTCHours(0, 0, 0, 0);
const end = new Date(date);
end.setHours(23, 59, 59, 999);
end.setUTCHours(23, 59, 59, 999);
return { start, end };
}
@ -25,10 +25,130 @@ function formatHoursToReadable(hours) {
}
}
// function to close open work tracks and create logout
async function closePreviousSession(vaId, lastLog) {
// Helper function to parse ISO time string
function parseISOTimeString(timeString) {
if (!timeString) return new Date();
try {
const currentTime = new Date();
// 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
async function closePreviousSession(vaId, lastLog, logTime = new Date()) {
try {
const currentTime = logTime;
// Find all open work tracks for this VA and project (where to is not set or totalTime is 0)
const openWorkTracks = await WorkTrackModel.find({
@ -120,18 +240,58 @@ async function closePreviousSession(vaId, lastLog) {
async function va_project_log_callback(req, res) {
try {
const payload = req.body || {};
const { vaId, vaEmail, projectId, type } = payload;
const { vaId, vaEmail, projectId, type, time: receivedTimeString } = payload;
// console.log("Received payload:", payload);
if (!vaId, !vaEmail, !projectId, !type) return res.status(400).send("Missing Props");
if (!vaId || !vaEmail || !projectId || !type) {
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);
if (!project) {
return res.status(400).json({ success: false, message: "Project not found" });
}
// For LOGIN requests, check daily hours
const vaUser = await VAModel.findById(vaId);
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") {
const { start, end } = getDayRange();
const { start, end } = getDayRange(); // UTC day range
// Get today's total hours
const todayWorkEntries = await WorkTrackModel.find({
vaId,
@ -149,7 +309,8 @@ async function va_project_log_callback(req, res) {
if (project.dedicatedHours > 0 && totalTodayHours >= project.dedicatedHours) {
return res.status(400).json({
success: false,
message: `Cannot login - daily hours already completed (${formatHoursToReadable(totalTodayHours)} / ${formatHoursToReadable(project.dedicatedHours)})`
message: `Cannot login - daily hours already completed (${formatHoursToReadable(totalTodayHours)} / ${formatHoursToReadable(project.dedicatedHours)})`,
// warnings: scheduleCheck.warnings
});
}
@ -163,7 +324,7 @@ async function va_project_log_callback(req, res) {
// check last workModel login time and make a logout request then create login
// console.log(`User already logged in. Closing previous session...`);
const closeResult = await closePreviousSession(vaId, lastLog);
const closeResult = await closePreviousSession(vaId, lastLog, logTime);
// console.log(`Previous session closed: ${closeResult.closedWorkTracks} work tracks, logout log: ${closeResult.logoutLogId}`);
@ -172,23 +333,72 @@ async function va_project_log_callback(req, res) {
loggedType: "LOGIN",
vaId,
vaEmail,
projectId
projectId,
time: logTime
});
return res.status(201).send({
try {
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,
message: `Login Successful (previous session closed)`,
message: `${type} Successfully`,
logId: createdLog._id,
SyncSalesforce: result,
previousSession: {
closedWorkTracks: closeResult.closedWorkTracks,
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({ loggedType: type, vaId, vaEmail, projectId })
// console.log(createdLog)
const createdLog = await LoggedModel.create({
loggedType: type,
vaId,
vaEmail,
projectId,
time: logTime
});
try {
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
const sfData = {
@ -218,7 +428,15 @@ async function va_project_log_callback(req, res) {
const result = await response.json();
// console.log("✅ Successfully synced VA data to Salesforce:", result);
res.status(201).json({ success: true, message: `${type} Successfully`, logId: createdLog._id, SyncSalesforce: result });
res.status(201).json({
success: true,
message: `${type} Successfully`,
logId: createdLog._id,
SyncSalesforce: result,
timeUsed: receivedTimeString ? 'received' : 'current',
actualTime: createdLog.time,
// warnings: scheduleCheck.warnings
});
} catch (error) {
console.error("Failed to sync with Salesforce:", error.message);
console.log(error)
@ -229,9 +447,8 @@ async function va_project_log_callback(req, res) {
throw salesforceError;
}
// return res.status(201).send({ success: true, message: `${type} Successfully`, logId: createdLog._id });
} catch (error) {
console.error("Error in va_project_log_callback:", error);
return res.status(500).send("Internal Server Error");
}
}

View File

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

View File

@ -59,14 +59,6 @@ async function va_timesheet_adjustment_function(req, res) {
const combineDateTime = (dateObj, timeStr) => {
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);
combined.setHours(hours, minutes, seconds, 0);
@ -120,13 +112,16 @@ async function va_timesheet_adjustment_function(req, res) {
});
}
const processedComment = processHTMLComment(comment);
const adjustmentRequest = new AdjustedHoursModel({
vaId,
projectId,
date: startOfDay,
loginTime: loginDateTime,
logoutTime: logoutDateTime,
comment,
comment: processedComment.processedHTML,
attachments: processedComment.extractedImages,
status: "PENDING"
});
@ -153,7 +148,8 @@ async function va_timesheet_adjustment_function(req, res) {
"date": adjustmentDate.toISOString().split('T')[0],
"loginTime": loginDateTime.toISOString(),
"logoutTime": logoutDateTime.toISOString(),
"comment": comment,
"comment": processedComment.plainText, // Send plain text to Salesforce
"attachments": processedComment.extractedImages, // Send extracted images info
"status": "PENDING",
"rejectionReason": "",
"adjustedHours": formatTime(adjustedSeconds)
@ -186,7 +182,8 @@ async function va_timesheet_adjustment_function(req, res) {
loginTime: loginDateTime.toISOString(),
logoutTime: logoutDateTime.toISOString(),
adjustedHours: formatTime(adjustedSeconds),
comment: comment,
comment: processedComment.processedHTML,
attachments: processedComment.extractedImages,
status: "PENDING"
},
SyncSalesforce: result
@ -221,4 +218,116 @@ 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 };

View File

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

View File

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

View File

@ -46,6 +46,9 @@ const userDetailsSchema = new Schema({
veemAccount: {
type: String,
},
wiseEmail: {
type: String,
},
phone: {
type: String,
},
@ -98,7 +101,27 @@ const projectSchema = new Schema({
type: String,
enum: ["ACTIVE", "INACTIVE"],
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 ---------------------- */
@ -320,6 +343,9 @@ const vaWorkTrackSchema = new Schema({
appName: {
type: String,
},
url: {
type: String,
},
from: {
type: Date,
default: Date.now,
@ -401,6 +427,14 @@ const adjustedHoursSchema = new Schema({
type: String,
required: true
},
attachments: [{
filename: String,
mimeType: String,
base64Data: String,
size: Number,
originalHTML: String,
index: Number
}],
status: {
type: String,
enum: ["PENDING", "APPROVED", "REJECTED"],
@ -450,6 +484,14 @@ const leavesSchema = new Schema({
type: String,
required: true
},
attachments: [{
filename: String,
mimeType: String,
base64Data: String,
size: Number,
originalHTML: String,
index: Number
}],
leaveType: {
type: String,
enum: ["SICK", "PTO"],

View File

@ -13,7 +13,7 @@ class VaClientRepository {
async findByEmail(email) {
try {
const response = await ClientModel.findOne({ "userDetails.email": email.toLowerCase() });
const response = await ClientModel.findOne({ "userDetails.email": email });
return response;
} catch (error) {
throw error;
@ -31,10 +31,27 @@ class VaClientRepository {
async update(id, data) {
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(
id,
{ $set: { ...data } },
{ new: true }
{ $set: updateObj },
{ new: true, runValidators: true }
);
return response;
} catch (error) {
@ -42,9 +59,53 @@ class VaClientRepository {
}
}
async delete(id) {
async findProfileByEmail(email) {
try {
const response = await ClientModel.findByIdAndDelete(id);
const response = await ClientModel.findOne(
{ "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;
} catch (error) {
throw error;

View File

@ -1,7 +1,7 @@
import express from "express";
const clientRoutes = express.Router();
import { va_register_client } from "../../controllers/client/vaClient.controller.js";
import { va_register_client, delete_client, update_client, get_client_dashboard_profile, update_client_password } from "../../controllers/client/vaClient.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_login_client } from "../../controllers/client/vaClientLogin.controller.js";
@ -31,6 +31,10 @@ clientRoutes.get("/:clientId/projects/:projectId", 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.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,6 +12,7 @@ import { process_reset } from "../../controllers/passwordReset.controllers.js";
import { project_add_sf } from "../../controllers/project/projectAddSf.controller.js"
import { update_project_sf } from "../../controllers/project/projectUpdateSf.controller.js"
import { authValidator } from "../../middlewares/auth.middleware.js";
import { delete_project_sf } from "../../controllers/project/deleteProjectSf.controller.js"
const v1Routes = express.Router()
@ -29,6 +30,7 @@ v1Routes.use("/client", clientRoutes);
v1Routes.post("/project", authValidator, project_add_sf);
v1Routes.patch("/project/:projectId", authValidator, update_project_sf);
v1Routes.delete("/project/:projectId", authValidator, delete_project_sf);
v1Routes.post("/password/reset", process_reset);

View File

@ -26,6 +26,9 @@ 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_list_function } from "../../controllers/va/vaLeaves/vaLeaveList.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
@ -39,12 +42,10 @@ vaRoutes.post("/verify-va-email", va_verify_email);
vaRoutes.get("/profile", authValidator, va_profile_callback); // Get VA Profile
vaRoutes.patch("/profile", authValidator, va_profile_update_callback); // Update VA Profile
vaRoutes.get("/:userName", va_username_callback); // VA Username Callback
vaRoutes.delete("/profile/:id", va_remove_function); // VA delete Callback
// timesheet adjustment routes
vaRoutes.get("/timesheet/summary/:projectId/:date", authValidator, va_timesheet_summary_function); // Get Timesheet Adjustment for VA on specific project and date
vaRoutes.get("/timesheet/summary/:projectId/:date/:vaId?", authValidator, va_timesheet_summary_function); // Get Timesheet Adjustment for VA on specific project and date
vaRoutes.post("/timesheet/adjustment/request", authValidator, va_timesheet_adjustment_function); // Timesheet Adjustment for VA
// vaRoutes.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);
@ -54,7 +55,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/type/:leaveType", authValidator, va_leave_list_function); // Filter by leave type
vaRoutes.get("/leaves/status/:status/type/:leaveType", authValidator, va_leave_list_function); // leaves Filter
vaRoutes.post("/leave/request", authValidator, va_leave_apply_function); // Timesheet Adjustment for VA
vaRoutes.post("/leave/request", authValidator, va_leave_apply_function); // Leave for VA
vaRoutes.patch("/leave/:leaveId", va_leave_manage_function);
// timesheet adjustment routes
@ -65,7 +66,7 @@ vaRoutes.get("/search", va_filter_search);
// GET Windows Tab Logged Data for VA IN Between Login and LOGOUT
vaRoutes.get("/report/project/:projectId/:from?/:to?", authValidator, va_projects_report_function);
vaRoutes.get("/report/project/:projectId/:from?/:to?/:vaId?", authValidator, va_projects_report_function);
// 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);
@ -79,6 +80,14 @@ vaRoutes.get("/log/project/:projectId/:from?/:to?", authValidator, va_log_callba
// 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;

View File

@ -154,7 +154,17 @@ app.get('/client/projects', async (req, res) => {
res.render('client/dashboard-projects');
} catch (error) {
console.error('Projects error:', error);
res.status(500).render('client/dashboard-projects', { error: 'Failed to load virtual assistants' });
res.status(500).render('client/dashboard-projects', { error: 'Failed to load projects' });
}
});
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' });
}
});
@ -441,9 +451,27 @@ io.on("connection", (socket) => {
});
socket.on('disconnect', () => {
console.log('a user Disconnected');
// socket.on('disconnect', () => {
// 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) => {

View File

@ -1,5 +1,7 @@
import logger from "../config/logger.js";
import SalesforceService from "./salesforce.service.js";
import { VAModel, ProjectModel, LoggedModel, WorkTrackModel } from "../models/vaModel.js";
import bcrypt from "bcrypt";
class VaClientService {
constructor(vaClientRepository) {
@ -54,25 +56,212 @@ class VaClientService {
async updateClient(id, data) {
try {
const response = await this.clientRepository.update(id, data);
if (!response) throw new Error("Client not found");
return response;
// Check if client exists
const existingClient = await this.clientRepository.findById(id);
if (!existingClient) {
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) {
logger.error("Client Service - updateClient error: " + error.message);
throw error;
}
}
async deleteClient(id) {
async deleteClient(clientId) {
try {
const response = await this.clientRepository.delete(id);
return response;
// Check if client exists
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;
} catch (error) {
logger.error("Client Service - getClientProfileByEmail 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;
}
}
// Helper method to sanitize data
sanitizeClientData(payload) {
const { userDetails } = payload;

View File

@ -34,16 +34,6 @@
<i class="fas fa-file-invoice"></i>Projects
</a>
</li>
<!-- <li class="nav-item">
<a href="/client/timesheets" class="nav-button">
<i class="fas fa-clock"></i>Timesheets
</a>
</li>
<li class="nav-item">
<a href="/client/referrals" class="nav-button">
<i class="fas fa-share-alt"></i>Referrals
</a>
</li> -->
</ul>
<button class="mobile-menu-button">
@ -54,11 +44,14 @@
</nav>
<div class="dashboard-container">
<aside class="dashboard-sidebar">
<!-- <div class="sidebar-header">
<h2 class="sidebar-title">DocVA Client Portal</h2>
</div> -->
<nav class="sidebar-nav">
<ul class="sidebar-list">
<li class="sidebar-item">
<a href="/client/profile" class="sidebar-link">
<i class="fas fa-user sidebar-icon"></i>
<span>Profile</span>
</a>
</li>
<li class="sidebar-item">
<a href="/client/dashboard" class="sidebar-link active">
<i class="fas fa-chart-bar sidebar-icon"></i>
@ -77,18 +70,6 @@
<span>Projects</span>
</a>
</li>
<!-- <li class="sidebar-item">
<a href="/client/invoices" class="sidebar-link">
<i class="fas fa-file-invoice sidebar-icon"></i>
<span>Invoices</span>
</a>
</li>
<li class="sidebar-item">
<a href="/client/referrals" class="sidebar-link">
<i class="fas fa-share-alt sidebar-icon"></i>
<span>Referrals</span>
</a>
</li> -->
<li class="sidebar-item">
<a href="javascript:void(0);" class="sidebar-link" id="logoutButton">
<i class="fas fa-sign-out-alt sidebar-icon"></i>
@ -142,7 +123,6 @@
<div class="content-section">
<div class="section-header">
<h2 class="section-title">Virtual Assistants <span class="total-count" id="vaListCount">0 VAs</span></h2>
</div>
<div class="table-container">
<div class="table-wrapper">
@ -151,9 +131,9 @@
<tr>
<th>Status</th>
<th>Virtual Assistant</th>
<th>Projects Assigned</th>
<th>Active Projects</th>
<th>Worked Time</th>
<th>Active Project</th>
<th>Total Projects</th>
<th>Today Logged Hours</th>
<th>Action</th>
</tr>
</thead>
@ -174,6 +154,77 @@
</div>
</main>
</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>
<script>
// Theme management
@ -187,7 +238,7 @@
}
// Initialize theme
const savedTheme = localStorage.getItem('theme') || getSystemTheme();
const savedTheme = getSystemTheme();
setTheme(savedTheme);
// API base URL
@ -242,7 +293,7 @@
// Format time function
function formatTime(seconds) {
if (!seconds) return 'Not working';
if (!seconds) return 'No Record Found';
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
@ -256,20 +307,88 @@
}
}
// Create VA card HTML
const vaModal = document.getElementById('vaModal');
function openVAModal(va) {
// Set basic VA info
document.getElementById('modalVAName').textContent = va.name;
// Set projects
const projects = va.projects || [];
document.getElementById('projectsCountBadge').textContent = projects.length;
const projectsTableBody = document.getElementById('modalProjectsTableBody');
const noProjectsMessage = document.getElementById('modalNoProjects');
if (projects.length > 0) {
projectsTableBody.innerHTML = projects.map(project => createProjectTableRow(project)).join('');
projectsTableBody.style.display = '';
noProjectsMessage.style.display = 'none';
} else {
projectsTableBody.innerHTML = '';
projectsTableBody.style.display = 'none';
noProjectsMessage.style.display = 'block';
}
// 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) {
const statusClass = va.isCurrentlyActive ? 'active' : 'inactive';
const statusText = va.isCurrentlyActive ? 'Active' : 'Inactive';
const activeProject = va?.lastProject?.name ? va.lastProject.name : '-';
const workedTime = formatTime(va.currentSessionDuration);
const projectsAssigned = va.projectsAssigned || 0;
const viewLink = va.isCurrentlyActive ? `<a href="/live/${va.lastProjectId}/${va.id}" class="view-link" data-va-id="${va.id}" target="_blank">
<i class="fas fa-eye"></i>
View
</a>` : '';
const viewLink = va.isCurrentlyActive ? `<a href="/live/${va.lastProjectId}/${va.id}" class="view-link" data-va-id="${va.id}" target="_blank">Live Tracking</a>` : '';
return `
<tr>
<tr class="va-row" data-va-id="${va.id}">
<td>
<div class="status-indicator">
<span class="status-text ${statusClass}">${statusText}</span>
@ -303,7 +422,6 @@
const response = await fetchWithAuth(`${API_BASE}/dashboard`);
if (!response) return;
const result = await response.json();
// console.log(data)
if (result.success) {
const data = result.data;
@ -320,22 +438,26 @@
// Update VA table
const vaTableBody = document.getElementById('vaTableBody');
const vaEmptyState = document.getElementById('vaEmptyState');
const allVAs = data.allVAs?.total || [];
const allVAs = data.allVAs || [];
if (allVAs.length > 0) {
vaTableBody.innerHTML = allVAs.map(va => createVATableRow(va)).join('');
vaTableBody.style.display = '';
vaEmptyState.style.display = 'none';
// Add event listeners to view links
// document.querySelectorAll('.view-link').forEach(link => {
// link.addEventListener('click', function(e) {
// e.preventDefault();
// const vaId = this.getAttribute('data-va-id');
// // You can update this link as needed
// window.location.href = `/client/virtual-assistants/${vaId}`;
// });
// });
// 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 {
vaTableBody.innerHTML = '';
vaTableBody.style.display = 'none';
@ -369,9 +491,35 @@
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
setInterval(loadDashboardData, 30000);
// Logout Modal Functions
function openLogoutModal() {
const modal = document.getElementById('logoutModal');
modal.style.display = 'flex';
document.body.style.overflow = 'hidden';
}
function closeLogoutModal() {
const modal = document.getElementById('logoutModal');
modal.style.display = 'none';
document.body.style.overflow = 'auto';
}
document.addEventListener('DOMContentLoaded', function() {
// Load dashboard data
@ -384,11 +532,25 @@
sidebar.classList.toggle('active');
});
// Logout button
const logoutButton = document.getElementById('logoutButton');
logoutButton.addEventListener('click', handleLogout);
// Close modal with escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeLogoutModal();
}
});
document.getElementById('logoutModal').addEventListener('click', function(e) {
if (e.target === this) {
closeLogoutModal();
}
});
// Logout button - opens confirmation modal instead of directly logging out
document.getElementById('logoutButton').addEventListener('click', openLogoutModal);
// Confirm logout button
document.getElementById('confirmLogoutBtn').addEventListener('click', handleLogout);
});
</script>
</body>

View File

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

View File

@ -34,16 +34,6 @@
<i class="fas fa-file-invoice"></i>Projects
</a>
</li>
<!-- <li class="nav-item">
<a href="/client/timesheets" class="nav-button">
<i class="fas fa-clock"></i>Timesheets
</a>
</li>
<li class="nav-item">
<a href="/client/referrals" class="nav-button">
<i class="fas fa-share-alt"></i>Referrals
</a>
</li> -->
</ul>
<button class="mobile-menu-button">
@ -54,11 +44,14 @@
</nav>
<div class="dashboard-container">
<aside class="dashboard-sidebar">
<!-- <div class="sidebar-header">
<h2 class="sidebar-title">DocVA Client Portal</h2>
</div> -->
<nav class="sidebar-nav">
<ul class="sidebar-list">
<li class="sidebar-item">
<a href="/client/profile" class="sidebar-link">
<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>
@ -77,18 +70,6 @@
<span>Projects</span>
</a>
</li>
<!-- <li class="sidebar-item">
<a href="/client/invoices" class="sidebar-link">
<i class="fas fa-file-invoice sidebar-icon"></i>
<span>Invoices</span>
</a>
</li>
<li class="sidebar-item">
<a href="/client/referrals" class="sidebar-link">
<i class="fas fa-share-alt sidebar-icon"></i>
<span>Referrals</span>
</a>
</li> -->
<li class="sidebar-item">
<a href="javascript:void(0);" class="sidebar-link" id="logoutButton">
<i class="fas fa-sign-out-alt sidebar-icon"></i>
@ -142,7 +123,6 @@
<div class="content-section">
<div class="section-header">
<h2 class="section-title">Virtual Assistants <span class="total-count" id="vaListCount">0 VAs</span></h2>
</div>
<div class="table-container">
<div class="table-wrapper">
@ -151,9 +131,9 @@
<tr>
<th>Status</th>
<th>Virtual Assistant</th>
<th>Projects Assigned</th>
<th>Active Projects</th>
<th>Worked Time</th>
<th>Active Project</th>
<th>Total Projects</th>
<th>Today Logged Hours</th>
<th>Action</th>
</tr>
</thead>
@ -174,6 +154,77 @@
</div>
</main>
</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>
<script>
// Theme management
@ -187,7 +238,7 @@
}
// Initialize theme
const savedTheme = localStorage.getItem('theme') || getSystemTheme();
const savedTheme = getSystemTheme();
setTheme(savedTheme);
// API base URL
@ -242,7 +293,7 @@
// Format time function
function formatTime(seconds) {
if (!seconds) return 'Not working';
if (!seconds) return 'No Record Found';
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
@ -256,20 +307,88 @@
}
}
// Create VA card HTML
const vaModal = document.getElementById('vaModal');
function openVAModal(va) {
// Set basic VA info
document.getElementById('modalVAName').textContent = va.name;
// Set projects
const projects = va.projects || [];
document.getElementById('projectsCountBadge').textContent = projects.length;
const projectsTableBody = document.getElementById('modalProjectsTableBody');
const noProjectsMessage = document.getElementById('modalNoProjects');
if (projects.length > 0) {
projectsTableBody.innerHTML = projects.map(project => createProjectTableRow(project)).join('');
projectsTableBody.style.display = '';
noProjectsMessage.style.display = 'none';
} else {
projectsTableBody.innerHTML = '';
projectsTableBody.style.display = 'none';
noProjectsMessage.style.display = 'block';
}
// 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) {
const statusClass = va.isCurrentlyActive ? 'active' : 'inactive';
const statusText = va.isCurrentlyActive ? 'Active' : 'Inactive';
const activeProject = va?.lastProject?.name ? va.lastProject.name : '-';
const workedTime = formatTime(va.currentSessionDuration);
const projectsAssigned = va.projectsAssigned || 0;
const viewLink = va.isCurrentlyActive ? `<a href="/live/${va.lastProjectId}/${va.id}" class="view-link" data-va-id="${va.id}" target="_blank">
<i class="fas fa-eye"></i>
View
</a>` : '';
const viewLink = va.isCurrentlyActive ? `<a href="/live/${va.lastProjectId}/${va.id}" class="view-link" data-va-id="${va.id}" target="_blank">Live Tracking</a>` : '';
return `
<tr>
<tr class="va-row" data-va-id="${va.id}">
<td>
<div class="status-indicator">
<span class="status-text ${statusClass}">${statusText}</span>
@ -303,7 +422,6 @@
const response = await fetchWithAuth(`${API_BASE}/dashboard`);
if (!response) return;
const result = await response.json();
// console.log(data)
if (result.success) {
const data = result.data;
@ -320,12 +438,26 @@
// Update VA table
const vaTableBody = document.getElementById('vaTableBody');
const vaEmptyState = document.getElementById('vaEmptyState');
const allVAs = data.allVAs?.total || [];
const allVAs = data.allVAs || [];
if (allVAs.length > 0) {
vaTableBody.innerHTML = allVAs.map(va => createVATableRow(va)).join('');
vaTableBody.style.display = '';
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 {
vaTableBody.innerHTML = '';
vaTableBody.style.display = 'none';
@ -359,9 +491,36 @@
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
setInterval(loadDashboardData, 30000);
// Logout Modal Functions
function openLogoutModal() {
const modal = document.getElementById('logoutModal');
modal.style.display = 'flex';
document.body.style.overflow = 'hidden';
}
function closeLogoutModal() {
const modal = document.getElementById('logoutModal');
modal.style.display = 'none';
document.body.style.overflow = 'auto';
}
document.addEventListener('DOMContentLoaded', function() {
// Load dashboard data
loadDashboardData();
@ -373,11 +532,25 @@
sidebar.classList.toggle('active');
});
// Logout button
const logoutButton = document.getElementById('logoutButton');
logoutButton.addEventListener('click', handleLogout);
// Close modal with escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeLogoutModal();
}
});
document.getElementById('logoutModal').addEventListener('click', function(e) {
if (e.target === this) {
closeLogoutModal();
}
});
// Logout button - opens confirmation modal instead of directly logging out
document.getElementById('logoutButton').addEventListener('click', openLogoutModal);
// Confirm logout button
document.getElementById('confirmLogoutBtn').addEventListener('click', handleLogout);
});
</script>
</body>

View File

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

View File

@ -11,87 +11,6 @@
select option:disabled {
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>
</head>
@ -266,17 +185,28 @@
<option value="" disabled selected></option>
</select>
<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 class="form-group">
<div class="input-container">
<select id="state" class="select-field" required onchange="handleChangeSelect(this)">
<!-- <option value=""></option> -->
</select>
<input type="text" id="state" class="input-field" placeholder=" " required>
<!-- <select id="state" class="select-field" required onchange="handleChangeSelect(this)">
<option value=""></option>
</select> -->
<label for="state" class="input-label">State / Region *</label>
<div class="location-loading" id="state-loading">Loading states...</div>
<!-- <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>
<label for="status" class="input-label">Status *</label>
</div>
</div>
@ -298,19 +228,19 @@
<label for="phone" class="input-label">Phone Number *</label>
<div class="input-container">
<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 class="invalid-feedback" id="phone-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
Please enter a valid phone number (10-15 digits)
</div>
</div>
<div class="form-group">
<!-- <div class="form-group">
<div class="input-container">
<input type="text" id="skype" class="input-field" placeholder=" " oninput="validateSkypeId(this)">
<label for="skype" class="input-label">Skype ID</label>
</div>
</div>
</div> -->
<div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="prevStep(2)">Back</button>
@ -327,7 +257,7 @@
</div>
<form id="payment-form">
<div class="form-group">
<div class="payment-option" onclick="selectPaymentMethod('payoneer')">
<!-- <div class="payment-option" onclick="selectPaymentMethod('payoneer')">
<h4>Payoneer</h4>
<p>Receive payments via Payoneer</p>
</div>
@ -341,7 +271,7 @@
Account number must be 5-20 characters
</div>
</div>
</div>
</div> -->
<div class="payment-option" onclick="selectPaymentMethod('veem')">
<h4>Veem</h4>
@ -358,6 +288,22 @@
</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 class="form-actions">
@ -380,5 +326,19 @@
</div>
</div>
<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>
</html>