diff --git a/public/css/dashboard.css b/public/css/dashboard.css
index d73ce47..fc52d06 100644
--- a/public/css/dashboard.css
+++ b/public/css/dashboard.css
@@ -826,6 +826,7 @@ tr[data-vaid]:hover {
cursor: pointer;
z-index: 10;
position: relative;
+ text-decoration: none;
}
/* Project Details Styles in Popup */
@@ -1299,4 +1300,337 @@ tr[data-vaid]:hover {
[data-theme="dark"] .status-badge.inactive {
background: #4b5563;
color: #d1d5db;
+}
+
+.modal-actions {
+ text-align: center;
+ margin-top: 20px;
+}
+
+.modal-actions a {
+ text-decoration: none;
+}
+
+.profile-container {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 20px;
+}
+
+.profile-header {
+ margin-bottom: 30px;
+ padding-bottom: 20px;
+ border-bottom: 1px solid #e1e5e9;
+}
+
+.profile-title {
+ font-size: 2rem;
+ font-weight: 600;
+ color: #2d3748;
+ margin: 0;
+}
+
+.profile-sections {
+ display: grid;
+ gap: 30px;
+}
+
+.profile-section {
+ background: white;
+ border-radius: 12px;
+ padding: 30px;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+ border: 1px solid #e1e5e9;
+}
+
+.section-header {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ margin-bottom: 25px;
+ padding-bottom: 15px;
+ border-bottom: 1px solid #e1e5e9;
+}
+
+.section-icon {
+ font-size: 1.5rem;
+ color: #4299e1;
+}
+
+.section-title {
+ font-size: 1.5rem;
+ font-weight: 600;
+ color: #2d3748;
+ margin: 0;
+}
+
+.form-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: 20px;
+}
+
+.form-group {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.form-label {
+ font-weight: 500;
+ color: #4a5568;
+ font-size: 0.875rem;
+}
+
+.form-input {
+ padding: 12px;
+ border: 1px solid #e1e5e9;
+ border-radius: 8px;
+ font-size: 1rem;
+ background: #f7fafc;
+ color: #4a5568;
+}
+
+.empty-field {
+ color: #a0aec0;
+ font-style: italic;
+}
+
+.password-section {
+ text-align: center;
+ padding: 40px 20px;
+}
+
+.change-password-btn {
+ background: #4299e1;
+ color: white;
+ border: none;
+ padding: 12px 24px;
+ border-radius: 8px;
+ cursor: pointer;
+ font-weight: 500;
+ font-size: 1rem;
+ transition: background-color 0.2s;
+}
+
+.change-password-btn:hover {
+ background: #3182ce;
+}
+
+/* Modal Styles */
+.modal-overlay {
+ display: none;
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.5);
+ z-index: 1000;
+ align-items: center;
+ justify-content: center;
+ padding: 20px;
+}
+
+
+.modal-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 24px;
+ border-bottom: 1px solid #e1e5e9;
+}
+
+.modal-title {
+ font-size: 1.5rem;
+ font-weight: 600;
+ color: #2d3748;
+ margin: 0;
+}
+
+.modal-close {
+ background: none;
+ border: none;
+ font-size: 1.25rem;
+ cursor: pointer;
+ color: #718096;
+ padding: 5px;
+ border-radius: 4px;
+}
+
+.modal-close:hover {
+ background: #f7fafc;
+ color: #2d3748;
+}
+
+.modal-body {
+ padding: 24px;
+}
+
+.modal-form {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.modal-form .form-group {
+ gap: 8px;
+}
+
+.modal-form .form-label {
+ font-weight: 500;
+ color: #4a5568;
+}
+
+.modal-form .form-input {
+ padding: 12px;
+ border: 1px solid #cbd5e0;
+ border-radius: 8px;
+ font-size: 1rem;
+ background: white;
+ transition: border-color 0.2s;
+}
+
+.modal-form .form-input:focus {
+ outline: none;
+ border-color: #4299e1;
+ box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.1);
+}
+
+.modal-form .form-input.error {
+ border-color: #e53e3e;
+}
+
+.error-message {
+ color: #e53e3e;
+ font-size: 0.875rem;
+ margin-top: 4px;
+}
+
+.success-message {
+ color: #38a169;
+ font-size: 0.875rem;
+ margin-top: 4px;
+}
+
+.modal-actions {
+ display: flex;
+ gap: 15px;
+ justify-content: flex-end;
+ margin-top: 30px;
+ padding-top: 20px;
+ border-top: 1px solid #e1e5e9;
+}
+
+.save-btn {
+ background: #48bb78;
+ color: white;
+ border: none;
+ padding: 12px 24px;
+ border-radius: 8px;
+ cursor: pointer;
+ font-weight: 500;
+ transition: background-color 0.2s;
+}
+
+.save-btn:hover {
+ background: #38a169;
+}
+
+.save-btn:disabled {
+ background: #a0aec0;
+ cursor: not-allowed;
+}
+
+.cancel-btn {
+ background: #e53e3e;
+ color: white;
+ border: none;
+ padding: 12px 24px;
+ border-radius: 8px;
+ cursor: pointer;
+ font-weight: 500;
+ transition: background-color 0.2s;
+}
+
+.cancel-btn:hover {
+ background: #c53030;
+}
+
+/* Logout Confirmation Modal Styles */
+.logout-modal-body {
+ text-align: center;
+ padding: 30px 20px;
+}
+
+.logout-icon {
+ font-size: 3rem;
+ color: #e53e3e;
+ margin-bottom: 20px;
+}
+
+.logout-title {
+ font-size: 1.5rem;
+ font-weight: 600;
+ color: #2d3748;
+ margin-bottom: 10px;
+}
+
+.logout-message {
+ color: #718096;
+ font-size: 1rem;
+ line-height: 1.5;
+ margin-bottom: 30px;
+}
+
+.logout-actions {
+ display: flex;
+ gap: 15px;
+ justify-content: center;
+}
+
+.logout-confirm-btn {
+ background: #e53e3e;
+ color: white;
+ border: none;
+ padding: 12px 24px;
+ border-radius: 8px;
+ cursor: pointer;
+ font-weight: 500;
+ transition: background-color 0.2s;
+ min-width: 120px;
+}
+
+.logout-confirm-btn:hover {
+ background: #c53030;
+}
+
+.logout-cancel-btn {
+ background: #718096;
+ color: white;
+ border: none;
+ padding: 12px 24px;
+ border-radius: 8px;
+ cursor: pointer;
+ font-weight: 500;
+ transition: background-color 0.2s;
+ min-width: 120px;
+}
+
+.logout-cancel-btn:hover {
+ background: #4a5568;
+}
+
+@media (max-width: 768px) {
+ .form-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .modal-actions {
+ flex-direction: column;
+ }
+
+ .logout-actions {
+ flex-direction: column;
+ }
}
\ No newline at end of file
diff --git a/public/js/va-client-register.js b/public/js/va-client-register.js
index a9d6788..7c330b2 100644
--- a/public/js/va-client-register.js
+++ b/public/js/va-client-register.js
@@ -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;
diff --git a/public/js/va-register.js b/public/js/va-register.js
index fea6594..7ce3f38 100644
--- a/public/js/va-register.js
+++ b/public/js/va-register.js
@@ -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 = '';
+ // // Clear current states
+ // stateSelect.innerHTML = '';
- // 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 = '';
diff --git a/src/controllers/client/dashboard/clientDashboard.controller.js b/src/controllers/client/dashboard/clientDashboard.controller.js
index cfd80bf..b13f92d 100644
--- a/src/controllers/client/dashboard/clientDashboard.controller.js
+++ b/src/controllers/client/dashboard/clientDashboard.controller.js
@@ -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);
diff --git a/src/controllers/client/vaClient.controller.js b/src/controllers/client/vaClient.controller.js
index 700b238..1547e84 100644
--- a/src/controllers/client/vaClient.controller.js
+++ b/src/controllers/client/vaClient.controller.js
@@ -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
};
\ No newline at end of file
diff --git a/src/controllers/project/deleteProjectSf.controller.js b/src/controllers/project/deleteProjectSf.controller.js
new file mode 100644
index 0000000..c3c6e39
--- /dev/null
+++ b/src/controllers/project/deleteProjectSf.controller.js
@@ -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 };
\ No newline at end of file
diff --git a/src/controllers/project/projectAddSf.controller.js b/src/controllers/project/projectAddSf.controller.js
index 39bb9b5..ccad5d0 100644
--- a/src/controllers/project/projectAddSf.controller.js
+++ b/src/controllers/project/projectAddSf.controller.js
@@ -5,7 +5,7 @@ import { updateEntityRelationships } from "../../helper/updateEntityRelationship
async function project_add_sf(req, res) {
try {
const payload = req.body|| {};
- const { name, hourlyRate, clientId, vaAssigned, dedicatedHours } = payload;
+ const { name, hourlyRate, clientId, vaAssigned, dedicatedHours, vaWorkingScheduleStart, vaWorkingScheduleEnd, isFlexibleAllowedPt, vaWorkingDelayAllowedStart, vaWorkingDelayAllowedEnd } = payload;
// Validate required fields
if (!name) {
@@ -65,6 +65,11 @@ async function project_add_sf(req, res) {
clientId: clientObjectId,
vaAssigned: vaAssignedArray,
dedicatedHours: dedicatedHours ? parseFloat(dedicatedHours) : 0,
+ vaWorkingScheduleStart: vaWorkingScheduleStart || null,
+ vaWorkingScheduleEnd: vaWorkingScheduleEnd || null,
+ isFlexibleAllowedPt: isFlexibleAllowedPt || true,
+ vaWorkingDelayAllowedStart: vaWorkingDelayAllowedStart || null,
+ vaWorkingDelayAllowedEnd: vaWorkingDelayAllowedEnd || null
};
const created = await ProjectModel.create(toCreate);
@@ -81,36 +86,8 @@ async function project_add_sf(req, res) {
console.warn('Project created but relationship update failed:', relationshipUpdate.message);
}
- // Send data to Salesforce API after successful registration
- try {
- // const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/`;
-
- // const response = await fetch(salesforceUrl, {
- // method: 'POST',
- // headers: {
- // 'Content-Type': 'application/json',
- // },
- // body: JSON.stringify(created)
- // });
-
- // if (!response.ok) {
- // const errorText = await response.text();
- // throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
- // }
-
- // const result = await response.json();
- // console.log("✅ Successfully synced VA data to Salesforce:", result);
-
- // res.status(201).json({ success: true, data: created, SyncSalesforce: result });
- res.status(201).json({ success: true, data: created, relationships: relationshipUpdate });
- } catch (error) {
- console.error("❌ Failed to sync with Salesforce:", error.message);
-
- // Create a custom error to distinguish Salesforce errors
- const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
- salesforceError.isSalesforceError = true;
- throw salesforceError;
- }
+ res.status(201).json({ success: true, data: created, relationships: relationshipUpdate });
+
} catch (error) {
console.error("project_add_sf error:", error);
return res.status(500).json({ success: false, message: "Internal Server Error" });
diff --git a/src/controllers/project/projectUpdateSf.controller.js b/src/controllers/project/projectUpdateSf.controller.js
index 5035f29..62e3047 100644
--- a/src/controllers/project/projectUpdateSf.controller.js
+++ b/src/controllers/project/projectUpdateSf.controller.js
@@ -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 = [];
diff --git a/src/controllers/va/vaLeaves/vaLeavesApply.controller.js b/src/controllers/va/vaLeaves/vaLeavesApply.controller.js
index b7e3fde..35ba56d 100644
--- a/src/controllers/va/vaLeaves/vaLeavesApply.controller.js
+++ b/src/controllers/va/vaLeaves/vaLeavesApply.controller.js
@@ -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 = /]+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 = `
`;
+
+ 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 };
\ No newline at end of file
diff --git a/src/controllers/va/vaLog/vaLogTrackWork.controller.js b/src/controllers/va/vaLog/vaLogTrackWork.controller.js
index ddf5d22..8333dfd 100644
--- a/src/controllers/va/vaLog/vaLogTrackWork.controller.js
+++ b/src/controllers/va/vaLog/vaLogTrackWork.controller.js
@@ -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(),
});
diff --git a/src/controllers/va/vaPayroll/vaPaySlip.controller.js b/src/controllers/va/vaPayroll/vaPaySlip.controller.js
new file mode 100644
index 0000000..640a9c0
--- /dev/null
+++ b/src/controllers/va/vaPayroll/vaPaySlip.controller.js
@@ -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 };
\ No newline at end of file
diff --git a/src/controllers/va/vaPayroll/vaPayrollSchedule.controller.js b/src/controllers/va/vaPayroll/vaPayrollSchedule.controller.js
new file mode 100644
index 0000000..bbe879f
--- /dev/null
+++ b/src/controllers/va/vaPayroll/vaPayrollSchedule.controller.js
@@ -0,0 +1,37 @@
+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();
+ // 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_payroll_schedule_function };
\ No newline at end of file
diff --git a/src/controllers/va/vaProfile/vaDelete.controller.js b/src/controllers/va/vaProfile/vaDelete.controller.js
new file mode 100644
index 0000000..8b71c12
--- /dev/null
+++ b/src/controllers/va/vaProfile/vaDelete.controller.js
@@ -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 };
\ No newline at end of file
diff --git a/src/controllers/va/vaProfile/vaProfileUpdate.controller.js b/src/controllers/va/vaProfile/vaProfileUpdate.controller.js
index 66a78b3..f1d6d20 100644
--- a/src/controllers/va/vaProfile/vaProfileUpdate.controller.js
+++ b/src/controllers/va/vaProfile/vaProfileUpdate.controller.js
@@ -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 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" });
+ 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" });
+ }
}
+
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" });
diff --git a/src/controllers/va/vaProfile/vaRegister.controller.js b/src/controllers/va/vaProfile/vaRegister.controller.js
index e88cecf..5746119 100644
--- a/src/controllers/va/vaProfile/vaRegister.controller.js
+++ b/src/controllers/va/vaProfile/vaRegister.controller.js
@@ -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,
},
diff --git a/src/controllers/va/vaProjects/vaProjectLog.controller.js b/src/controllers/va/vaProjects/vaProjectLog.controller.js
index 1f62636..a4ab951 100644
--- a/src/controllers/va/vaProjects/vaProjectLog.controller.js
+++ b/src/controllers/va/vaProjects/vaProjectLog.controller.js
@@ -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");
}
}
diff --git a/src/controllers/va/vaProjects/vaProjectReport.controller.js b/src/controllers/va/vaProjects/vaProjectReport.controller.js
index 2b2bbc6..13c7a34 100644
--- a/src/controllers/va/vaProjects/vaProjectReport.controller.js
+++ b/src/controllers/va/vaProjects/vaProjectReport.controller.js
@@ -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) {
diff --git a/src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustment.controller.js b/src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustment.controller.js
index 6931442..a252386 100644
--- a/src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustment.controller.js
+++ b/src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustment.controller.js
@@ -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 = /
]+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 = `
`;
+
+ 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 };
\ No newline at end of file
diff --git a/src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustmentSummary.controller.js b/src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustmentSummary.controller.js
index e0d2096..94020f2 100644
--- a/src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustmentSummary.controller.js
+++ b/src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustmentSummary.controller.js
@@ -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;
-
- 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" });
+ }
+
+ 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) {
diff --git a/src/middlewares/auth.middleware.js b/src/middlewares/auth.middleware.js
index ee688f6..49cf229 100644
--- a/src/middlewares/auth.middleware.js
+++ b/src/middlewares/auth.middleware.js
@@ -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;
diff --git a/src/models/vaModel.js b/src/models/vaModel.js
index b33241f..e6794e1 100644
--- a/src/models/vaModel.js
+++ b/src/models/vaModel.js
@@ -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"],
diff --git a/src/repository/vaClient.repository.js b/src/repository/vaClient.repository.js
index d087d4c..15a170f 100644
--- a/src/repository/vaClient.repository.js
+++ b/src/repository/vaClient.repository.js
@@ -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;
diff --git a/src/routes/v1/client.routes.js b/src/routes/v1/client.routes.js
index 066ccc3..9e2c6e6 100644
--- a/src/routes/v1/client.routes.js
+++ b/src/routes/v1/client.routes.js
@@ -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);
diff --git a/src/routes/v1/index.js b/src/routes/v1/index.js
index a426f60..4cd658a 100644
--- a/src/routes/v1/index.js
+++ b/src/routes/v1/index.js
@@ -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);
diff --git a/src/routes/v1/va.routes.js b/src/routes/v1/va.routes.js
index 087e684..b9c77c9 100644
--- a/src/routes/v1/va.routes.js
+++ b/src/routes/v1/va.routes.js
@@ -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;
\ No newline at end of file
diff --git a/src/server.js b/src/server.js
index be52a17..7069ed5 100644
--- a/src/server.js
+++ b/src/server.js
@@ -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' });
}
});
diff --git a/src/services/vaClient.service.js b/src/services/vaClient.service.js
index b6c971b..ea5e297 100644
--- a/src/services/vaClient.service.js
+++ b/src/services/vaClient.service.js
@@ -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;
diff --git a/views/client/dashboard-dashboard.ejs b/views/client/dashboard-dashboard.ejs
index 0e55e21..e637a04 100644
--- a/views/client/dashboard-dashboard.ejs
+++ b/views/client/dashboard-dashboard.ejs
@@ -34,16 +34,6 @@
Projects
-
diff --git a/views/client/dashboard-profile.ejs b/views/client/dashboard-profile.ejs new file mode 100644 index 0000000..4355b94 --- /dev/null +++ b/views/client/dashboard-profile.ejs @@ -0,0 +1,478 @@ + + +
+ + +
+ + + + +
+