Compare commits
11 Commits
142f4f77cd
...
d0bb922da1
| Author | SHA1 | Date |
|---|---|---|
|
|
d0bb922da1 | |
|
|
1baa6da4c7 | |
|
|
68e3cf14c9 | |
|
|
3154a7e0d3 | |
|
|
84871c7236 | |
|
|
9de7384020 | |
|
|
870d81e2ca | |
|
|
4d757daf97 | |
|
|
16a62670e7 | |
|
|
39010ee61c | |
|
|
9c736dea02 |
File diff suppressed because it is too large
Load Diff
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -9,65 +9,113 @@
|
|||
|
||||
// Countries and states data
|
||||
const countries = [
|
||||
{ code: 'US', name: 'United States' },
|
||||
{ code: 'CA', name: 'Canada' },
|
||||
{ code: 'GB', name: 'United Kingdom' },
|
||||
{ code: 'AU', name: 'Australia' },
|
||||
{ code: 'IN', name: 'India' },
|
||||
{ code: 'DE', name: 'Germany' },
|
||||
{ code: 'FR', name: 'France' },
|
||||
{ code: 'BR', name: 'Brazil' },
|
||||
{ code: 'JP', name: 'Japan' },
|
||||
{ code: 'CN', name: 'China' }
|
||||
{ code: 'US', name: 'United States' },
|
||||
{ code: 'CA', name: 'Canada' },
|
||||
{ code: 'GB', name: 'United Kingdom' },
|
||||
{ code: 'AU', name: 'Australia' },
|
||||
{ code: 'IN', name: 'India' },
|
||||
{ code: 'DE', name: 'Germany' },
|
||||
{ code: 'FR', name: 'France' },
|
||||
{ code: 'BR', name: 'Brazil' },
|
||||
{ code: 'JP', name: 'Japan' },
|
||||
{ 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
|
||||
|
|
@ -469,20 +615,29 @@
|
|||
showError('Please select a payment method', null, step);
|
||||
isValid = false;
|
||||
} 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;
|
||||
}
|
||||
} else if (paymentMethod === 'veem') {
|
||||
const veemAccount = document.getElementById('veemAccount').value;
|
||||
if (!veemAccount) {
|
||||
showError('Please enter your Veem account number', 'veemAccount', step);
|
||||
isValid = false;
|
||||
}
|
||||
}
|
||||
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;
|
||||
// }
|
||||
} 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 = '';
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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/`;
|
||||
res.status(201).json({ success: true, data: created, relationships: relationshipUpdate });
|
||||
|
||||
// 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" });
|
||||
|
|
|
|||
|
|
@ -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 = [];
|
||||
|
|
|
|||
|
|
@ -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(/ /g, ' ')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/'/g, "'")
|
||||
.replace(///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 };
|
||||
|
|
@ -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(),
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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;
|
||||
|
||||
if (!accessToken) {
|
||||
return res.status(401).json({ success: false, message: "Missing access 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" });
|
||||
}
|
||||
|
||||
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" });
|
||||
}
|
||||
|
||||
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" });
|
||||
}
|
||||
}
|
||||
|
||||
let decoded;
|
||||
try {
|
||||
decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN);
|
||||
} catch (err) {
|
||||
return res.status(401).json({ success: false, message: "Invalid or expired token" });
|
||||
}
|
||||
|
||||
if (!decoded?.email) {
|
||||
return res.status(400).json({ success: false, message: "Invalid token payload" });
|
||||
}
|
||||
|
||||
const vaUser = await VAModel.findOne({ "userDetails.email": decoded.email });
|
||||
if (!vaUser) {
|
||||
return res.status(404).json({ success: false, message: "VA not found" });
|
||||
}
|
||||
|
||||
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,11 +122,21 @@ 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",
|
||||
});
|
||||
success: false,
|
||||
message: "No valid fields to update",
|
||||
});
|
||||
}
|
||||
|
||||
if (status) {
|
||||
vaUser.status = status;
|
||||
updatedFields.push('status');
|
||||
}
|
||||
|
||||
await vaUser.save();
|
||||
|
|
@ -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" });
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
success: true,
|
||||
message: `Login Successful (previous session closed)`,
|
||||
logId: createdLog._id,
|
||||
previousSession: {
|
||||
closedWorkTracks: closeResult.closedWorkTracks,
|
||||
logoutLogId: closeResult.logoutLogId
|
||||
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: `${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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,23 +11,36 @@ function formatHMS(ms) {
|
|||
|
||||
async function va_projects_report_function(req, res) {
|
||||
try {
|
||||
const accessToken = req.headers.token;
|
||||
if (!accessToken) {
|
||||
return res.status(401).json({ success: false, message: "Missing token" });
|
||||
}
|
||||
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" });
|
||||
}
|
||||
|
||||
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 decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN);
|
||||
if (!decoded || !decoded.email) {
|
||||
return res.status(401).json({ success: false, message: "Invalid token" });
|
||||
}
|
||||
|
||||
const vaEmail = decoded.email;
|
||||
const vaUser = await VAModel.findOne({ "userDetails.email": vaEmail });
|
||||
if (!vaUser) {
|
||||
return res.status(404).json({ success: false, message: "VA not found" });
|
||||
}
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -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(/ /g, ' ')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/'/g, "'")
|
||||
.replace(///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 };
|
||||
|
|
@ -3,24 +3,37 @@ import { VAModel, ProjectModel, LoggedModel } from "../../../models/vaModel.js";
|
|||
|
||||
async function va_timesheet_summary_function(req, res) {
|
||||
try {
|
||||
const accessToken = req.headers.token;
|
||||
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" });
|
||||
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" });
|
||||
}
|
||||
|
||||
vaEmail = decoded.email;
|
||||
vaUser = await VAModel.findOne({ "userDetails.email": vaEmail });
|
||||
if (!vaUser) {
|
||||
return res.status(404).json({ success: false, message: "VA not found" });
|
||||
}
|
||||
|
||||
vaId = vaUser._id;
|
||||
}
|
||||
|
||||
const decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN);
|
||||
if (!decoded || !decoded.email) {
|
||||
return res.status(401).json({ success: false, message: "Invalid token" });
|
||||
}
|
||||
|
||||
const vaEmail = decoded.email;
|
||||
const vaUser = await VAModel.findOne({ "userDetails.email": vaEmail });
|
||||
if (!vaUser) {
|
||||
return res.status(404).json({ success: false, message: "VA not found" });
|
||||
}
|
||||
|
||||
const vaId = vaUser._id;
|
||||
const { projectId, date } = req.params;
|
||||
|
||||
if (!projectId || !date) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
@ -53,26 +55,213 @@ 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;
|
||||
try {
|
||||
// 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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -224,7 +275,7 @@
|
|||
return response;
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
async function handleLogout() {
|
||||
try {
|
||||
// Call logout API
|
||||
await fetchWithAuth(`${API_BASE}/logout`, {
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -224,7 +275,7 @@
|
|||
return response;
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
async function handleLogout() {
|
||||
try {
|
||||
// Call logout API
|
||||
await fetchWithAuth(`${API_BASE}/logout`, {
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue