Added delete route form project, va, and client - Updated leaves and time adjustment api
This commit is contained in:
parent
39010ee61c
commit
16a62670e7
|
|
@ -18,56 +18,104 @@
|
||||||
{ code: 'FR', name: 'France' },
|
{ code: 'FR', name: 'France' },
|
||||||
{ code: 'BR', name: 'Brazil' },
|
{ code: 'BR', name: 'Brazil' },
|
||||||
{ code: 'JP', name: 'Japan' },
|
{ code: 'JP', name: 'Japan' },
|
||||||
{ code: 'CN', name: 'China' }
|
{ code: 'CN', name: 'China' },
|
||||||
|
{ code: 'MX', name: 'Mexico' },
|
||||||
|
{ code: 'ES', name: 'Spain' },
|
||||||
|
{ code: 'IT', name: 'Italy' },
|
||||||
|
{ code: 'NL', name: 'Netherlands' },
|
||||||
|
{ code: 'SE', name: 'Sweden' },
|
||||||
|
{ code: 'NO', name: 'Norway' },
|
||||||
|
{ code: 'DK', name: 'Denmark' },
|
||||||
|
{ code: 'FI', name: 'Finland' },
|
||||||
|
{ code: 'BE', name: 'Belgium' },
|
||||||
|
{ code: 'CH', name: 'Switzerland' },
|
||||||
|
{ code: 'AT', name: 'Austria' },
|
||||||
|
{ code: 'PT', name: 'Portugal' },
|
||||||
|
{ code: 'IE', name: 'Ireland' },
|
||||||
|
{ code: 'PL', name: 'Poland' },
|
||||||
|
{ code: 'CZ', name: 'Czech Republic' },
|
||||||
|
{ code: 'HU', name: 'Hungary' },
|
||||||
|
{ code: 'RO', name: 'Romania' },
|
||||||
|
{ code: 'GR', name: 'Greece' },
|
||||||
|
{ code: 'TR', name: 'Turkey' },
|
||||||
|
{ code: 'SA', name: 'Saudi Arabia' },
|
||||||
|
{ code: 'AE', name: 'United Arab Emirates' },
|
||||||
|
{ code: 'ZA', name: 'South Africa' },
|
||||||
|
{ code: 'NG', name: 'Nigeria' },
|
||||||
|
{ code: 'EG', name: 'Egypt' },
|
||||||
|
{ code: 'KE', name: 'Kenya' },
|
||||||
|
{ code: 'AR', name: 'Argentina' },
|
||||||
|
{ code: 'CO', name: 'Colombia' },
|
||||||
|
{ code: 'PE', name: 'Peru' },
|
||||||
|
{ code: 'CL', name: 'Chile' },
|
||||||
|
{ code: 'VE', name: 'Venezuela' },
|
||||||
|
{ code: 'KR', name: 'South Korea' },
|
||||||
|
{ code: 'SG', name: 'Singapore' },
|
||||||
|
{ code: 'MY', name: 'Malaysia' },
|
||||||
|
{ code: 'TH', name: 'Thailand' },
|
||||||
|
{ code: 'ID', name: 'Indonesia' },
|
||||||
|
{ code: 'PH', name: 'Philippines' },
|
||||||
|
{ code: 'VN', name: 'Vietnam' },
|
||||||
|
{ code: 'PK', name: 'Pakistan' },
|
||||||
|
{ code: 'BD', name: 'Bangladesh' },
|
||||||
|
{ code: 'LK', name: 'Sri Lanka' },
|
||||||
|
{ code: 'NP', name: 'Nepal' },
|
||||||
|
{ code: 'RU', name: 'Russia' },
|
||||||
|
{ code: 'UA', name: 'Ukraine' },
|
||||||
|
{ code: 'IL', name: 'Israel' },
|
||||||
|
{ code: 'NZ', name: 'New Zealand' },
|
||||||
|
{ code: 'HK', name: 'Hong Kong' },
|
||||||
|
{ code: 'TW', name: 'Taiwan' }
|
||||||
];
|
];
|
||||||
|
|
||||||
const states = {
|
// const states = {
|
||||||
'US': [
|
// 'US': [
|
||||||
{ code: 'CA', name: 'California' }, { code: 'TX', name: 'Texas' }, { code: 'FL', name: 'Florida' },
|
// { code: 'CA', name: 'California' }, { code: 'TX', name: 'Texas' }, { code: 'FL', name: 'Florida' },
|
||||||
{ code: 'NY', name: 'New York' }, { code: 'IL', name: 'Illinois' }, { code: 'PA', name: 'Pennsylvania' },
|
// { code: 'NY', name: 'New York' }, { code: 'IL', name: 'Illinois' }, { code: 'PA', name: 'Pennsylvania' },
|
||||||
{ code: 'OH', name: 'Ohio' }, { code: 'GA', name: 'Georgia' }, { code: 'NC', name: 'North Carolina' },
|
// { code: 'OH', name: 'Ohio' }, { code: 'GA', name: 'Georgia' }, { code: 'NC', name: 'North Carolina' },
|
||||||
{ code: 'MI', name: 'Michigan' }
|
// { code: 'MI', name: 'Michigan' }
|
||||||
],
|
// ],
|
||||||
'CA': [
|
// 'CA': [
|
||||||
{ code: 'ON', name: 'Ontario' }, { code: 'QC', name: 'Quebec' }, { code: 'BC', name: 'British Columbia' },
|
// { code: 'ON', name: 'Ontario' }, { code: 'QC', name: 'Quebec' }, { code: 'BC', name: 'British Columbia' },
|
||||||
{ code: 'AB', name: 'Alberta' }, { code: 'MB', name: 'Manitoba' }, { code: 'SK', name: 'Saskatchewan' }
|
// { code: 'AB', name: 'Alberta' }, { code: 'MB', name: 'Manitoba' }, { code: 'SK', name: 'Saskatchewan' }
|
||||||
],
|
// ],
|
||||||
'GB': [
|
// 'GB': [
|
||||||
{ code: 'ENG', name: 'England' }, { code: 'SCT', name: 'Scotland' }, { code: 'WLS', name: 'Wales' },
|
// { code: 'ENG', name: 'England' }, { code: 'SCT', name: 'Scotland' }, { code: 'WLS', name: 'Wales' },
|
||||||
{ code: 'NIR', name: 'Northern Ireland' }
|
// { code: 'NIR', name: 'Northern Ireland' }
|
||||||
],
|
// ],
|
||||||
'AU': [
|
// 'AU': [
|
||||||
{ code: 'NSW', name: 'New South Wales' }, { code: 'VIC', name: 'Victoria' }, { code: 'QLD', name: 'Queensland' },
|
// { code: 'NSW', name: 'New South Wales' }, { code: 'VIC', name: 'Victoria' }, { code: 'QLD', name: 'Queensland' },
|
||||||
{ code: 'WA', name: 'Western Australia' }, { code: 'SA', name: 'South Australia' }, { code: 'TAS', name: 'Tasmania' }
|
// { code: 'WA', name: 'Western Australia' }, { code: 'SA', name: 'South Australia' }, { code: 'TAS', name: 'Tasmania' }
|
||||||
],
|
// ],
|
||||||
'IN': [
|
// 'IN': [
|
||||||
{ code: 'MH', name: 'Maharashtra' }, { code: 'TN', name: 'Tamil Nadu' }, { code: 'UP', name: 'Uttar Pradesh' },
|
// { code: 'MH', name: 'Maharashtra' }, { code: 'TN', name: 'Tamil Nadu' }, { code: 'UP', name: 'Uttar Pradesh' },
|
||||||
{ code: 'KA', name: 'Karnataka' }, { code: 'GJ', name: 'Gujarat' }, { code: 'RJ', name: 'Rajasthan' }
|
// { code: 'KA', name: 'Karnataka' }, { code: 'GJ', name: 'Gujarat' }, { code: 'RJ', name: 'Rajasthan' }
|
||||||
],
|
// ],
|
||||||
'DE': [
|
// 'DE': [
|
||||||
{ code: 'BW', name: 'Baden-Württemberg' }, { code: 'BY', name: 'Bavaria' }, { code: 'BE', name: 'Berlin' },
|
// { code: 'BW', name: 'Baden-Württemberg' }, { code: 'BY', name: 'Bavaria' }, { code: 'BE', name: 'Berlin' },
|
||||||
{ code: 'HB', name: 'Bremen' }, { code: 'HH', name: 'Hamburg' }, { code: 'HE', name: 'Hesse' }
|
// { code: 'HB', name: 'Bremen' }, { code: 'HH', name: 'Hamburg' }, { code: 'HE', name: 'Hesse' }
|
||||||
],
|
// ],
|
||||||
'FR': [
|
// 'FR': [
|
||||||
{ code: 'ARA', name: 'Auvergne-Rhône-Alpes' }, { code: 'BFC', name: 'Bourgogne-Franche-Comté' },
|
// { code: 'ARA', name: 'Auvergne-Rhône-Alpes' }, { code: 'BFC', name: 'Bourgogne-Franche-Comté' },
|
||||||
{ code: 'BRE', name: 'Brittany' }, { code: 'CVL', name: 'Centre-Val de Loire' },
|
// { code: 'BRE', name: 'Brittany' }, { code: 'CVL', name: 'Centre-Val de Loire' },
|
||||||
{ code: 'COR', name: 'Corsica' }, { code: 'GES', name: 'Grand Est' }
|
// { code: 'COR', name: 'Corsica' }, { code: 'GES', name: 'Grand Est' }
|
||||||
],
|
// ],
|
||||||
'BR': [
|
// 'BR': [
|
||||||
{ code: 'SP', name: 'São Paulo' }, { code: 'RJ', name: 'Rio de Janeiro' }, { code: 'MG', name: 'Minas Gerais' },
|
// { code: 'SP', name: 'São Paulo' }, { code: 'RJ', name: 'Rio de Janeiro' }, { code: 'MG', name: 'Minas Gerais' },
|
||||||
{ code: 'BA', name: 'Bahia' }, { code: 'RS', name: 'Rio Grande do Sul' }, { code: 'PR', name: 'Paraná' }
|
// { code: 'BA', name: 'Bahia' }, { code: 'RS', name: 'Rio Grande do Sul' }, { code: 'PR', name: 'Paraná' }
|
||||||
],
|
// ],
|
||||||
'JP': [
|
// 'JP': [
|
||||||
{ code: 'TK', name: 'Tokyo' }, { code: 'OS', name: 'Osaka' }, { code: 'KY', name: 'Kyoto' },
|
// { code: 'TK', name: 'Tokyo' }, { code: 'OS', name: 'Osaka' }, { code: 'KY', name: 'Kyoto' },
|
||||||
{ code: 'HK', name: 'Hokkaido' }, { code: 'OK', name: 'Okinawa' }, { code: 'KN', name: 'Kanagawa' }
|
// { code: 'HK', name: 'Hokkaido' }, { code: 'OK', name: 'Okinawa' }, { code: 'KN', name: 'Kanagawa' }
|
||||||
],
|
// ],
|
||||||
'CN': [
|
// 'CN': [
|
||||||
{ code: 'BJ', name: 'Beijing' }, { code: 'SH', name: 'Shanghai' }, { code: 'TJ', name: 'Tianjin' },
|
// { code: 'BJ', name: 'Beijing' }, { code: 'SH', name: 'Shanghai' }, { code: 'TJ', name: 'Tianjin' },
|
||||||
{ code: 'CQ', name: 'Chongqing' }, { code: 'GD', name: 'Guangdong' }, { code: 'JS', name: 'Jiangsu' }
|
// { code: 'CQ', name: 'Chongqing' }, { code: 'GD', name: 'Guangdong' }, { code: 'JS', name: 'Jiangsu' }
|
||||||
]
|
// ]
|
||||||
};
|
// };
|
||||||
|
|
||||||
// Initialize the form
|
// Initialize the form
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Populate country dropdown
|
// Populate country dropdown
|
||||||
const countrySelect = document.getElementById('country');
|
const countrySelect = document.getElementById('country');
|
||||||
|
|
@ -80,32 +128,34 @@
|
||||||
countrySelect.appendChild(option);
|
countrySelect.appendChild(option);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
autoDetectCountry();
|
||||||
|
|
||||||
// Add event listener for country change
|
// Add event listener for country change
|
||||||
countrySelect.addEventListener('change', function() {
|
// countrySelect.addEventListener('change', function() {
|
||||||
const countryCode = this.value;
|
// const countryCode = this.value;
|
||||||
const stateSelect = document.getElementById('state');
|
// const stateSelect = document.getElementById('state');
|
||||||
|
|
||||||
// Show loading
|
// // Show loading
|
||||||
document.getElementById('state-loading').style.display = 'block';
|
// document.getElementById('state-loading').style.display = 'block';
|
||||||
|
|
||||||
// Clear current states
|
// // Clear current states
|
||||||
stateSelect.innerHTML = '<option value="" disabled selected></option>';
|
// stateSelect.innerHTML = '<option value="" disabled selected></option>';
|
||||||
|
|
||||||
// Simulate API delay
|
// // Simulate API delay
|
||||||
setTimeout(() => {
|
// setTimeout(() => {
|
||||||
if (countryCode && states[countryCode]) {
|
// if (countryCode && states[countryCode]) {
|
||||||
states[countryCode].forEach(state => {
|
// states[countryCode].forEach(state => {
|
||||||
const option = document.createElement('option');
|
// const option = document.createElement('option');
|
||||||
option.value = state.code;
|
// option.value = state.code;
|
||||||
option.textContent = state.name;
|
// option.textContent = state.name;
|
||||||
stateSelect.appendChild(option);
|
// stateSelect.appendChild(option);
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Hide loading
|
// // Hide loading
|
||||||
document.getElementById('state-loading').style.display = 'none';
|
// document.getElementById('state-loading').style.display = 'none';
|
||||||
}, 500);
|
// }, 500);
|
||||||
});
|
// });
|
||||||
|
|
||||||
|
|
||||||
// Add input event listeners to handle label animation
|
// Add input event listeners to handle label animation
|
||||||
|
|
@ -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 to navigate to any step (for tab navigation)
|
||||||
function goToStep(step) {
|
function goToStep(step) {
|
||||||
// Only allow navigation to completed steps or the next step
|
// Only allow navigation to completed steps or the next step
|
||||||
|
|
@ -471,17 +617,26 @@
|
||||||
} else {
|
} else {
|
||||||
const paymentMethod = selectedPayment.getAttribute('onclick').match(/'([^']+)'/)[1];
|
const paymentMethod = selectedPayment.getAttribute('onclick').match(/'([^']+)'/)[1];
|
||||||
if (paymentMethod === 'payoneer') {
|
if (paymentMethod === 'payoneer') {
|
||||||
const payoneerAccount = document.getElementById('payoneerAccount').value;
|
// const payoneerAccount = document.getElementById('payoneerAccount').value;
|
||||||
if (!payoneerAccount) {
|
// if (!payoneerAccount) {
|
||||||
showError('Please enter your Payoneer account number', 'payoneerAccount', step);
|
// showError('Please enter your Payoneer account number', 'payoneerAccount', step);
|
||||||
isValid = false;
|
// isValid = false;
|
||||||
}
|
// }
|
||||||
} else if (paymentMethod === 'veem') {
|
} else if (paymentMethod === 'veem') {
|
||||||
const veemAccount = document.getElementById('veemAccount').value;
|
const veemAccount = document.getElementById('veemAccount').value;
|
||||||
if (!veemAccount) {
|
if (!veemAccount) {
|
||||||
showError('Please enter your Veem account number', 'veemAccount', step);
|
showError('Please enter your Veem account number', 'veemAccount', step);
|
||||||
isValid = false;
|
isValid = false;
|
||||||
}
|
}
|
||||||
|
} else if (paymentMethod === 'wise') {
|
||||||
|
const wiseEmail = document.getElementById('wiseEmail').value;
|
||||||
|
if (!wiseEmail) {
|
||||||
|
showError('Please enter your Wise email address', 'wiseEmail', step);
|
||||||
|
isValid = false;
|
||||||
|
} else if (!validateEmail(wiseEmail)) {
|
||||||
|
showError('Please enter a valid email address for Wise', 'wiseEmail', step);
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -547,12 +702,15 @@
|
||||||
const paymentMethod = selectedPayment.getAttribute('onclick').match(/'([^']+)'/)[1];
|
const paymentMethod = selectedPayment.getAttribute('onclick').match(/'([^']+)'/)[1];
|
||||||
formData.userDetails.paymentMethod = paymentMethod;
|
formData.userDetails.paymentMethod = paymentMethod;
|
||||||
|
|
||||||
if (document.getElementById('payoneerAccount').value) {
|
// if (document.getElementById('payoneerAccount').value) {
|
||||||
formData.userDetails.payoneerAccount = document.getElementById('payoneerAccount').value;
|
// formData.userDetails.payoneerAccount = document.getElementById('payoneerAccount').value;
|
||||||
}
|
// }
|
||||||
if (document.getElementById('veemAccount').value) {
|
if (document.getElementById('veemAccount').value) {
|
||||||
formData.userDetails.veemAccount = document.getElementById('veemAccount').value;
|
formData.userDetails.veemAccount = document.getElementById('veemAccount').value;
|
||||||
}
|
}
|
||||||
|
if (paymentMethod === 'wise') {
|
||||||
|
formData.userDetails.wiseEmail = document.getElementById('wiseEmail').value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -575,12 +733,35 @@
|
||||||
|
|
||||||
// Show appropriate payment details
|
// Show appropriate payment details
|
||||||
if (method === 'payoneer') {
|
if (method === 'payoneer') {
|
||||||
document.getElementById('payoneer-details').style.display = 'block';
|
// document.getElementById('payoneer-details').style.display = 'block';
|
||||||
} else if (method === 'veem') {
|
} else if (method === 'veem') {
|
||||||
document.getElementById('veem-details').style.display = 'block';
|
document.getElementById('veem-details').style.display = 'block';
|
||||||
|
} else if (method === 'wise') {
|
||||||
|
document.getElementById('wise-details').style.display = 'block';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validateWiseEmail(input) {
|
||||||
|
const wiseError = document.getElementById('wise-email-error');
|
||||||
|
const value = input.value.trim();
|
||||||
|
|
||||||
|
// Remove any validation styling first
|
||||||
|
input.style.border = '';
|
||||||
|
wiseError.style.display = 'none';
|
||||||
|
|
||||||
|
if (value.length > 0) {
|
||||||
|
// Check if it's a valid email
|
||||||
|
if (!validateEmail(value)) {
|
||||||
|
wiseError.textContent = 'Please enter a valid email address for Wise';
|
||||||
|
wiseError.style.display = 'block';
|
||||||
|
input.style.border = '1px solid #ef4444';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Function to generate a strong password
|
// Function to generate a strong password
|
||||||
function generatePassword() {
|
function generatePassword() {
|
||||||
const length = 12;
|
const length = 12;
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,112 @@ 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"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
va_register_client,
|
va_register_client,
|
||||||
get_client_by_email
|
get_client_by_email,
|
||||||
|
delete_client,
|
||||||
|
update_client
|
||||||
};
|
};
|
||||||
|
|
@ -81,36 +81,8 @@ async function project_add_sf(req, res) {
|
||||||
console.warn('Project created but relationship update failed:', relationshipUpdate.message);
|
console.warn('Project created but relationship update failed:', relationshipUpdate.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send data to Salesforce API after successful registration
|
res.status(201).json({ success: true, data: created, relationships: relationshipUpdate });
|
||||||
try {
|
|
||||||
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/`;
|
|
||||||
|
|
||||||
const response = await fetch(salesforceUrl, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(created)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorText = await response.text();
|
|
||||||
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
console.log("✅ Successfully synced VA data to Salesforce:", result);
|
|
||||||
|
|
||||||
res.status(201).json({ success: true, data: created, SyncSalesforce: result });
|
|
||||||
// res.status(201).json({ success: true, data: created, relationships: relationshipUpdate });
|
|
||||||
} catch (error) {
|
|
||||||
console.error("❌ Failed to sync with Salesforce:", error.message);
|
|
||||||
|
|
||||||
// Create a custom error to distinguish Salesforce errors
|
|
||||||
const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
|
|
||||||
salesforceError.isSalesforceError = true;
|
|
||||||
throw salesforceError;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("project_add_sf error:", error);
|
console.error("project_add_sf error:", error);
|
||||||
return res.status(500).json({ success: false, message: "Internal Server Error" });
|
return res.status(500).json({ success: false, message: "Internal Server Error" });
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,7 @@ async function va_leave_apply_function(req, res) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const processedComment = processHTMLComment(comment);
|
||||||
// Create leave request
|
// Create leave request
|
||||||
const leaveRequest = new LeavesModel({
|
const leaveRequest = new LeavesModel({
|
||||||
vaId,
|
vaId,
|
||||||
|
|
@ -138,62 +139,62 @@ async function va_leave_apply_function(req, res) {
|
||||||
dateTo: endDate,
|
dateTo: endDate,
|
||||||
days: totalDays.toString(),
|
days: totalDays.toString(),
|
||||||
leaveType,
|
leaveType,
|
||||||
comment,
|
comment: processedComment.processedHTML,
|
||||||
|
attachments: processedComment.extractedImages,
|
||||||
status: "PENDING"
|
status: "PENDING"
|
||||||
});
|
});
|
||||||
|
|
||||||
await leaveRequest.save();
|
await leaveRequest.save();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/timesheet-adjustment`;
|
// const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/timesheet-adjustment`;
|
||||||
const sfData = {
|
// const sfData = {
|
||||||
"id": adjustmentRequest._id,
|
// "id": leaveRequest._id,
|
||||||
"va": {
|
// "va": {
|
||||||
"id": vaId
|
// "id": vaId
|
||||||
},
|
// },
|
||||||
"project": {
|
// "dateFrom": startDate.toISOString().split('T')[0],
|
||||||
"id": projectId
|
// "dateTo": endDate.toISOString().split('T')[0],
|
||||||
},
|
// "totalDays": totalDays,
|
||||||
"date": adjustmentDate.toISOString().split('T')[0],
|
// "leaveType": leaveType,
|
||||||
"loginTime": loginDateTime.toISOString(),
|
// "comment": processedComment.plainText, // Send plain text to Salesforce
|
||||||
"logoutTime": logoutDateTime.toISOString(),
|
// "attachments": processedComment.extractedImages, // Send extracted images info
|
||||||
"comment": comment,
|
// "status": "PENDING",
|
||||||
"status": "PENDING",
|
// "rejectionReason": ""
|
||||||
"rejectionReason": "",
|
// }
|
||||||
"adjustedHours": formatTime(adjustedSeconds)
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(salesforceUrl, {
|
// const response = await fetch(salesforceUrl, {
|
||||||
method: 'POST',
|
// method: 'POST',
|
||||||
headers: {
|
// headers: {
|
||||||
'Content-Type': 'application/json',
|
// 'Content-Type': 'application/json',
|
||||||
// Add any required headers for Salesforce authentication
|
// // Add any required headers for Salesforce authentication
|
||||||
// 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
|
// // 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
|
||||||
},
|
// },
|
||||||
body: JSON.stringify({"data": sfData})
|
// body: JSON.stringify({"data": sfData})
|
||||||
});
|
// });
|
||||||
|
|
||||||
if (!response.ok) {
|
// if (!response.ok) {
|
||||||
const errorText = await response.text();
|
// const errorText = await response.text();
|
||||||
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
|
// throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
|
||||||
}
|
// }
|
||||||
|
|
||||||
const result = await response.json();
|
// const result = await response.json();
|
||||||
// console.log("✅ Successfully synced VA data to Salesforce:", result);
|
// // console.log("✅ Successfully synced VA data to Salesforce:", result);
|
||||||
|
|
||||||
return res.status(201).json({
|
return res.status(201).json({
|
||||||
success: true,
|
success: true,
|
||||||
message: "Adjustment request submitted successfully",
|
message: "Leave request submitted successfully",
|
||||||
data: {
|
data: {
|
||||||
adjustmentId: adjustmentRequest._id,
|
leaveId: leaveRequest._id,
|
||||||
date: adjustmentDate.toISOString().split('T')[0],
|
dateFrom: startDate.toISOString().split('T')[0],
|
||||||
loginTime: loginDateTime.toISOString(),
|
dateTo: endDate.toISOString().split('T')[0],
|
||||||
logoutTime: logoutDateTime.toISOString(),
|
totalDays: totalDays,
|
||||||
adjustedHours: formatTime(adjustedSeconds),
|
leaveType,
|
||||||
comment: comment,
|
comment: processedComment.processedHTML,
|
||||||
|
attachments: processedComment.extractedImages,
|
||||||
status: "PENDING"
|
status: "PENDING"
|
||||||
},
|
},
|
||||||
SyncSalesforce: result
|
// SyncSalesforce: result
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to sync with Salesforce:", error.message);
|
console.error("Failed to sync with Salesforce:", error.message);
|
||||||
|
|
@ -238,4 +239,116 @@ async function va_leave_apply_function(req, res) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function processHTMLComment(htmlComment) {
|
||||||
|
try {
|
||||||
|
let processedHTML = htmlComment;
|
||||||
|
const extractedImages = [];
|
||||||
|
let plainText = '';
|
||||||
|
|
||||||
|
// Extract plain text from HTML for Salesforce
|
||||||
|
plainText = extractPlainTextFromHTML(htmlComment);
|
||||||
|
|
||||||
|
// Process base64 images in HTML
|
||||||
|
const imageProcessingResult = extractAndProcessBase64Images(htmlComment);
|
||||||
|
processedHTML = imageProcessingResult.processedHTML;
|
||||||
|
extractedImages.push(...imageProcessingResult.extractedImages);
|
||||||
|
|
||||||
|
return {
|
||||||
|
processedHTML,
|
||||||
|
plainText,
|
||||||
|
extractedImages
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error processing HTML comment:", error);
|
||||||
|
// Return original content if processing fails
|
||||||
|
return {
|
||||||
|
processedHTML: htmlComment,
|
||||||
|
plainText: extractPlainTextFromHTML(htmlComment),
|
||||||
|
extractedImages: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to extract plain text from HTML
|
||||||
|
function extractPlainTextFromHTML(html) {
|
||||||
|
try {
|
||||||
|
// Remove HTML tags and decode HTML entities
|
||||||
|
let plainText = html
|
||||||
|
.replace(/<[^>]*>/g, ' ') // Replace tags with spaces
|
||||||
|
.replace(/\s+/g, ' ') // Collapse multiple spaces
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
// Decode common HTML entities
|
||||||
|
plainText = plainText
|
||||||
|
.replace(/ /g, ' ')
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, "'")
|
||||||
|
.replace(/'/g, "'")
|
||||||
|
.replace(///g, '/');
|
||||||
|
|
||||||
|
return plainText;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error extracting plain text from HTML:", error);
|
||||||
|
return html.replace(/<[^>]*>/g, ' ').trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to extract and process base64 images from HTML
|
||||||
|
function extractAndProcessBase64Images(html) {
|
||||||
|
const extractedImages = [];
|
||||||
|
let processedHTML = html;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Regex to find base64 images in HTML (various formats)
|
||||||
|
const base64Regex = /<img[^>]+src="data:image\/([a-zA-Z0-9+]+);base64,([^"]+)"[^>]*>/gi;
|
||||||
|
|
||||||
|
let match;
|
||||||
|
let imageIndex = 0;
|
||||||
|
|
||||||
|
while ((match = base64Regex.exec(html)) !== null) {
|
||||||
|
const fullMatch = match[0];
|
||||||
|
const imageType = match[1]; // png, jpeg, gif, etc.
|
||||||
|
const base64Data = match[2];
|
||||||
|
|
||||||
|
// Create a unique filename
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const filename = `leave_image_${timestamp}_${imageIndex}.${imageType}`;
|
||||||
|
|
||||||
|
// Calculate approximate file size
|
||||||
|
const size = Math.floor((base64Data.length * 3) / 4); // Base64 size calculation
|
||||||
|
|
||||||
|
// Store image information
|
||||||
|
extractedImages.push({
|
||||||
|
filename: filename,
|
||||||
|
mimeType: `image/${imageType}`,
|
||||||
|
base64Data: base64Data,
|
||||||
|
size: size,
|
||||||
|
originalHTML: fullMatch, // Store original img tag for reference
|
||||||
|
index: imageIndex
|
||||||
|
});
|
||||||
|
|
||||||
|
// Replace base64 with a reference in HTML
|
||||||
|
const replacementHTML = `<img src="attachment://${filename}" alt="Leave evidence ${imageIndex + 1}" data-filename="${filename}" data-image-index="${imageIndex}" class="leave-image">`;
|
||||||
|
|
||||||
|
processedHTML = processedHTML.replace(fullMatch, replacementHTML);
|
||||||
|
|
||||||
|
imageIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
processedHTML,
|
||||||
|
extractedImages
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error processing base64 images:", error);
|
||||||
|
return {
|
||||||
|
processedHTML: html,
|
||||||
|
extractedImages: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export { va_leave_apply_function };
|
export { va_leave_apply_function };
|
||||||
|
|
@ -5,28 +5,61 @@ import { VAModel } from "../../../models/vaModel.js";
|
||||||
// Register a new VA
|
// Register a new VA
|
||||||
async function va_profile_update_callback(req, res) {
|
async function va_profile_update_callback(req, res) {
|
||||||
try {
|
try {
|
||||||
|
const { sf_token } = req.headers.sf_token;
|
||||||
const accessToken = req.headers.token;
|
const accessToken = req.headers.token;
|
||||||
|
|
||||||
|
let vaUser;
|
||||||
|
let authSource = 'regular';
|
||||||
|
|
||||||
|
if(sf_token) {
|
||||||
|
try {
|
||||||
|
const sfDecoded = jwt.verify(sf_token, process.env.JWT_SECRET_SF_TOKEN);
|
||||||
|
|
||||||
|
if (!sfDecoded?.email && !sfDecoded?.vaId) {
|
||||||
|
return res.status(400).json({ success: false, message: "Invalid Salesforce token payload" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find user by email or VA ID from Salesforce token
|
||||||
|
if (sfDecoded.email) {
|
||||||
|
vaUser = await VAModel.findOne({ "userDetails.email": sfDecoded.email });
|
||||||
|
} else if (sfDecoded.vaId) {
|
||||||
|
vaUser = await VAModel.findById(sfDecoded.vaId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!vaUser) {
|
||||||
|
return res.status(404).json({ success: false, message: "VA not found with Salesforce token" });
|
||||||
|
}
|
||||||
|
|
||||||
|
authSource = 'salesforce';
|
||||||
|
|
||||||
|
} catch (sfError) {
|
||||||
|
return res.status(401).json({ success: false, message: "Invalid or expired Salesforce token" });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
return res.status(401).json({ success: false, message: "Missing access token" });
|
return res.status(401).json({ success: false, message: "Missing access token" });
|
||||||
}
|
}
|
||||||
|
|
||||||
let decoded;
|
const decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN);
|
||||||
try {
|
|
||||||
decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN);
|
|
||||||
} catch (err) {
|
|
||||||
return res.status(401).json({ success: false, message: "Invalid or expired token" });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!decoded?.email) {
|
if (!decoded?.email) {
|
||||||
return res.status(400).json({ success: false, message: "Invalid token payload" });
|
return res.status(400).json({ success: false, message: "Invalid token payload" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const vaUser = await VAModel.findOne({ "userDetails.email": decoded.email });
|
vaUser = await VAModel.findOne({ "userDetails.email": decoded.email });
|
||||||
if (!vaUser) {
|
if (!vaUser) {
|
||||||
return res.status(404).json({ success: false, message: "VA not found" });
|
return res.status(404).json({ success: false, message: "VA not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
authSource = 'regular';
|
||||||
|
|
||||||
|
} catch (tokenError) {
|
||||||
|
return res.status(401).json({ success: false, message: "Invalid or expired access token" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const payload = req.body || {};
|
const payload = req.body || {};
|
||||||
const { userDetails } = payload;
|
const { userDetails } = payload;
|
||||||
|
|
||||||
|
|
@ -34,23 +67,36 @@ async function va_profile_update_callback(req, res) {
|
||||||
return res.status(400).json({ success: false, message: "Missing userDetails" });
|
return res.status(400).json({ success: false, message: "Missing userDetails" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allowed fields to update
|
const allowedFields = [
|
||||||
// const allowedFields = [
|
"firstName", "lastName", "dob", "age", "address",
|
||||||
// "firstName", "lastName", "dob", "age", "address",
|
"city", "state", "country", "phone", "skype",
|
||||||
// "bankName", "accountHolderName", "accountNumber", "routingNumber"
|
"paymentMethod", "payoneerAccount", "veemAccount"
|
||||||
// ];
|
];
|
||||||
|
|
||||||
// // Update allowed fields only
|
let hasUpdates = false;
|
||||||
// allowedFields.forEach(field => {
|
const updatedFields = [];
|
||||||
// if (userDetails[field] !== undefined) {
|
|
||||||
// vaUser.userDetails[field] = userDetails[field];
|
// 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 ---------------------- */
|
/* ---------------------- PASSWORD UPDATE ---------------------- */
|
||||||
|
let passwordUpdated = false;
|
||||||
if (userDetails.password || userDetails.cPassword || userDetails.oldPassword) {
|
if (userDetails.password || userDetails.cPassword || userDetails.oldPassword) {
|
||||||
const { oldPassword, password, cPassword } = userDetails;
|
const { oldPassword, password, cPassword } = userDetails;
|
||||||
|
|
||||||
|
if (authSource === 'salesforce') {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
message: "Password changes require regular authentication",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!oldPassword || !password || !cPassword) {
|
if (!oldPassword || !password || !cPassword) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
|
|
@ -78,10 +124,15 @@ async function va_profile_update_callback(req, res) {
|
||||||
const saltRounds = 10;
|
const saltRounds = 10;
|
||||||
const hashedPassword = await bcrypt.hash(password, saltRounds);
|
const hashedPassword = await bcrypt.hash(password, saltRounds);
|
||||||
vaUser.userDetails.password = hashedPassword;
|
vaUser.userDetails.password = hashedPassword;
|
||||||
} else {
|
hasUpdates = true;
|
||||||
|
passwordUpdated = true;
|
||||||
|
updatedFields.push('password');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasUpdates) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "Data not found",
|
message: "No valid fields to update",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -95,6 +146,7 @@ async function va_profile_update_callback(req, res) {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Profile updated successfully",
|
message: "Profile updated successfully",
|
||||||
data: updatedUser,
|
data: updatedUser,
|
||||||
|
updatedFields: updatedFields,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return res.status(500).json({ success: false, message: "Internal Server Error" });
|
return res.status(500).json({ success: false, message: "Internal Server Error" });
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ async function va_register(req, res) {
|
||||||
paymentMethod: userDetails.paymentMethod || null,
|
paymentMethod: userDetails.paymentMethod || null,
|
||||||
payoneerAccount: userDetails.payoneerAccount || null,
|
payoneerAccount: userDetails.payoneerAccount || null,
|
||||||
veemAccount: userDetails.veemAccount || null,
|
veemAccount: userDetails.veemAccount || null,
|
||||||
|
wiseEmail: userDetails.wiseEmail || null,
|
||||||
phone: userDetails.phone || null,
|
phone: userDetails.phone || null,
|
||||||
skype: userDetails.skype || null,
|
skype: userDetails.skype || null,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,18 @@ function formatHMS(ms) {
|
||||||
|
|
||||||
async function va_projects_report_function(req, res) {
|
async function va_projects_report_function(req, res) {
|
||||||
try {
|
try {
|
||||||
|
const sf_token = req.headers.sf_token;
|
||||||
|
let vaId;
|
||||||
|
let vaUser;
|
||||||
|
let vaEmail;
|
||||||
|
if(sf_token) {
|
||||||
|
vaId = req.body.vaId;
|
||||||
|
vaUser = await VAModel.findById(vaId);
|
||||||
|
if (!vaUser) {
|
||||||
|
return res.status(404).json({ success: false, message: "VA not found" });
|
||||||
|
}
|
||||||
|
vaEmail = vaUser.userDetails.email
|
||||||
|
} else {
|
||||||
const accessToken = req.headers.token;
|
const accessToken = req.headers.token;
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
return res.status(401).json({ success: false, message: "Missing token" });
|
return res.status(401).json({ success: false, message: "Missing token" });
|
||||||
|
|
@ -21,13 +33,14 @@ async function va_projects_report_function(req, res) {
|
||||||
return res.status(401).json({ success: false, message: "Invalid token" });
|
return res.status(401).json({ success: false, message: "Invalid token" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const vaEmail = decoded.email;
|
vaEmail = decoded.email;
|
||||||
const vaUser = await VAModel.findOne({ "userDetails.email": vaEmail });
|
vaUser = await VAModel.findOne({ "userDetails.email": vaEmail });
|
||||||
if (!vaUser) {
|
if (!vaUser) {
|
||||||
return res.status(404).json({ success: false, message: "VA not found" });
|
return res.status(404).json({ success: false, message: "VA not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const vaId = vaUser._id;
|
vaId = vaUser._id;
|
||||||
|
}
|
||||||
const { projectId, from, to } = req.params;
|
const { projectId, from, to } = req.params;
|
||||||
|
|
||||||
if (!projectId) {
|
if (!projectId) {
|
||||||
|
|
|
||||||
|
|
@ -59,14 +59,6 @@ async function va_timesheet_adjustment_function(req, res) {
|
||||||
|
|
||||||
const combineDateTime = (dateObj, timeStr) => {
|
const combineDateTime = (dateObj, timeStr) => {
|
||||||
const [hours, minutes, seconds] = timeStr.split(':').map(Number);
|
const [hours, minutes, seconds] = timeStr.split(':').map(Number);
|
||||||
|
|
||||||
// // Extract date components from the Date object
|
|
||||||
// const year = dateObj.getFullYear();
|
|
||||||
// const month = dateObj.getMonth();
|
|
||||||
// const day = dateObj.getDate();
|
|
||||||
|
|
||||||
// // Create UTC date directly
|
|
||||||
// return new Date(year, month, day, hours, minutes, seconds);
|
|
||||||
const combined = new Date(dateObj);
|
const combined = new Date(dateObj);
|
||||||
combined.setHours(hours, minutes, seconds, 0);
|
combined.setHours(hours, minutes, seconds, 0);
|
||||||
|
|
||||||
|
|
@ -120,13 +112,16 @@ async function va_timesheet_adjustment_function(req, res) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const processedComment = processHTMLComment(comment);
|
||||||
|
|
||||||
const adjustmentRequest = new AdjustedHoursModel({
|
const adjustmentRequest = new AdjustedHoursModel({
|
||||||
vaId,
|
vaId,
|
||||||
projectId,
|
projectId,
|
||||||
date: startOfDay,
|
date: startOfDay,
|
||||||
loginTime: loginDateTime,
|
loginTime: loginDateTime,
|
||||||
logoutTime: logoutDateTime,
|
logoutTime: logoutDateTime,
|
||||||
comment,
|
comment: processedComment.processedHTML,
|
||||||
|
attachments: processedComment.extractedImages,
|
||||||
status: "PENDING"
|
status: "PENDING"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -153,7 +148,8 @@ async function va_timesheet_adjustment_function(req, res) {
|
||||||
"date": adjustmentDate.toISOString().split('T')[0],
|
"date": adjustmentDate.toISOString().split('T')[0],
|
||||||
"loginTime": loginDateTime.toISOString(),
|
"loginTime": loginDateTime.toISOString(),
|
||||||
"logoutTime": logoutDateTime.toISOString(),
|
"logoutTime": logoutDateTime.toISOString(),
|
||||||
"comment": comment,
|
"comment": processedComment.plainText, // Send plain text to Salesforce
|
||||||
|
"attachments": processedComment.extractedImages, // Send extracted images info
|
||||||
"status": "PENDING",
|
"status": "PENDING",
|
||||||
"rejectionReason": "",
|
"rejectionReason": "",
|
||||||
"adjustedHours": formatTime(adjustedSeconds)
|
"adjustedHours": formatTime(adjustedSeconds)
|
||||||
|
|
@ -186,7 +182,8 @@ async function va_timesheet_adjustment_function(req, res) {
|
||||||
loginTime: loginDateTime.toISOString(),
|
loginTime: loginDateTime.toISOString(),
|
||||||
logoutTime: logoutDateTime.toISOString(),
|
logoutTime: logoutDateTime.toISOString(),
|
||||||
adjustedHours: formatTime(adjustedSeconds),
|
adjustedHours: formatTime(adjustedSeconds),
|
||||||
comment: comment,
|
comment: processedComment.processedHTML,
|
||||||
|
attachments: processedComment.extractedImages,
|
||||||
status: "PENDING"
|
status: "PENDING"
|
||||||
},
|
},
|
||||||
SyncSalesforce: result
|
SyncSalesforce: result
|
||||||
|
|
@ -221,4 +218,116 @@ async function va_timesheet_adjustment_function(req, res) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function processHTMLComment(htmlComment) {
|
||||||
|
try {
|
||||||
|
let processedHTML = htmlComment;
|
||||||
|
const extractedImages = [];
|
||||||
|
let plainText = '';
|
||||||
|
|
||||||
|
// Extract plain text from HTML for Salesforce
|
||||||
|
plainText = extractPlainTextFromHTML(htmlComment);
|
||||||
|
|
||||||
|
// Process base64 images in HTML
|
||||||
|
const imageProcessingResult = extractAndProcessBase64Images(htmlComment);
|
||||||
|
processedHTML = imageProcessingResult.processedHTML;
|
||||||
|
extractedImages.push(...imageProcessingResult.extractedImages);
|
||||||
|
|
||||||
|
return {
|
||||||
|
processedHTML,
|
||||||
|
plainText,
|
||||||
|
extractedImages
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error processing HTML comment:", error);
|
||||||
|
// Return original content if processing fails
|
||||||
|
return {
|
||||||
|
processedHTML: htmlComment,
|
||||||
|
plainText: extractPlainTextFromHTML(htmlComment),
|
||||||
|
extractedImages: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to extract plain text from HTML
|
||||||
|
function extractPlainTextFromHTML(html) {
|
||||||
|
try {
|
||||||
|
// Remove HTML tags and decode HTML entities
|
||||||
|
let plainText = html
|
||||||
|
.replace(/<[^>]*>/g, ' ') // Replace tags with spaces
|
||||||
|
.replace(/\s+/g, ' ') // Collapse multiple spaces
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
// Decode common HTML entities
|
||||||
|
plainText = plainText
|
||||||
|
.replace(/ /g, ' ')
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, "'")
|
||||||
|
.replace(/'/g, "'")
|
||||||
|
.replace(///g, '/');
|
||||||
|
|
||||||
|
return plainText;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error extracting plain text from HTML:", error);
|
||||||
|
return html.replace(/<[^>]*>/g, ' ').trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to extract and process base64 images from HTML
|
||||||
|
function extractAndProcessBase64Images(html) {
|
||||||
|
const extractedImages = [];
|
||||||
|
let processedHTML = html;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Regex to find base64 images in HTML (various formats)
|
||||||
|
const base64Regex = /<img[^>]+src="data:image\/([a-zA-Z0-9+]+);base64,([^"]+)"[^>]*>/gi;
|
||||||
|
|
||||||
|
let match;
|
||||||
|
let imageIndex = 0;
|
||||||
|
|
||||||
|
while ((match = base64Regex.exec(html)) !== null) {
|
||||||
|
const fullMatch = match[0];
|
||||||
|
const imageType = match[1]; // png, jpeg, gif, etc.
|
||||||
|
const base64Data = match[2];
|
||||||
|
|
||||||
|
// Create a unique filename
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const filename = `adjustment_image_${timestamp}_${imageIndex}.${imageType}`;
|
||||||
|
|
||||||
|
// Calculate approximate file size
|
||||||
|
const size = Math.floor((base64Data.length * 3) / 4); // Base64 size calculation
|
||||||
|
|
||||||
|
// Store image information
|
||||||
|
extractedImages.push({
|
||||||
|
filename: filename,
|
||||||
|
mimeType: `image/${imageType}`,
|
||||||
|
base64Data: base64Data,
|
||||||
|
size: size,
|
||||||
|
originalHTML: fullMatch, // Store original img tag for reference
|
||||||
|
index: imageIndex
|
||||||
|
});
|
||||||
|
|
||||||
|
// Replace base64 with a reference in HTML
|
||||||
|
const replacementHTML = `<img src="attachment://${filename}" alt="Adjustment evidence ${imageIndex + 1}" data-filename="${filename}" data-image-index="${imageIndex}" class="adjustment-image">`;
|
||||||
|
|
||||||
|
processedHTML = processedHTML.replace(fullMatch, replacementHTML);
|
||||||
|
|
||||||
|
imageIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
processedHTML,
|
||||||
|
extractedImages
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error processing base64 images:", error);
|
||||||
|
return {
|
||||||
|
processedHTML: html,
|
||||||
|
extractedImages: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export { va_timesheet_adjustment_function };
|
export { va_timesheet_adjustment_function };
|
||||||
|
|
@ -3,6 +3,18 @@ import { VAModel, ProjectModel, LoggedModel } from "../../../models/vaModel.js";
|
||||||
|
|
||||||
async function va_timesheet_summary_function(req, res) {
|
async function va_timesheet_summary_function(req, res) {
|
||||||
try {
|
try {
|
||||||
|
const sf_token = req.headers.sf_token;
|
||||||
|
let vaId;
|
||||||
|
let vaUser;
|
||||||
|
let vaEmail;
|
||||||
|
if(sf_token) {
|
||||||
|
vaId = req.body.vaId;
|
||||||
|
vaUser = await VAModel.findById(vaId);
|
||||||
|
if (!vaUser) {
|
||||||
|
return res.status(404).json({ success: false, message: "VA not found" });
|
||||||
|
}
|
||||||
|
vaEmail = vaUser.userDetails.email
|
||||||
|
} else {
|
||||||
const accessToken = req.headers.token;
|
const accessToken = req.headers.token;
|
||||||
|
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
|
|
@ -14,13 +26,14 @@ async function va_timesheet_summary_function(req, res) {
|
||||||
return res.status(401).json({ success: false, message: "Invalid token" });
|
return res.status(401).json({ success: false, message: "Invalid token" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const vaEmail = decoded.email;
|
vaEmail = decoded.email;
|
||||||
const vaUser = await VAModel.findOne({ "userDetails.email": vaEmail });
|
vaUser = await VAModel.findOne({ "userDetails.email": vaEmail });
|
||||||
if (!vaUser) {
|
if (!vaUser) {
|
||||||
return res.status(404).json({ success: false, message: "VA not found" });
|
return res.status(404).json({ success: false, message: "VA not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const vaId = vaUser._id;
|
vaId = vaUser._id;
|
||||||
|
}
|
||||||
const { projectId, date } = req.params;
|
const { projectId, date } = req.params;
|
||||||
|
|
||||||
if (!projectId || !date) {
|
if (!projectId || !date) {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ async function authValidator(req, res, next) {
|
||||||
try {
|
try {
|
||||||
const sf_token = req.headers.sf_token;
|
const sf_token = req.headers.sf_token;
|
||||||
if (sf_token) {
|
if (sf_token) {
|
||||||
|
const { vaId } = req.params;
|
||||||
|
if(vaId) req.body.vaId = vaId;
|
||||||
next();
|
next();
|
||||||
} else {
|
} else {
|
||||||
const accessToken = req.headers.token;
|
const accessToken = req.headers.token;
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,9 @@ const userDetailsSchema = new Schema({
|
||||||
veemAccount: {
|
veemAccount: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
|
wiseEmail: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
phone: {
|
phone: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
|
|
@ -404,6 +407,14 @@ const adjustedHoursSchema = new Schema({
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
attachments: [{
|
||||||
|
filename: String,
|
||||||
|
mimeType: String,
|
||||||
|
base64Data: String,
|
||||||
|
size: Number,
|
||||||
|
originalHTML: String,
|
||||||
|
index: Number
|
||||||
|
}],
|
||||||
status: {
|
status: {
|
||||||
type: String,
|
type: String,
|
||||||
enum: ["PENDING", "APPROVED", "REJECTED"],
|
enum: ["PENDING", "APPROVED", "REJECTED"],
|
||||||
|
|
@ -453,6 +464,14 @@ const leavesSchema = new Schema({
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
attachments: [{
|
||||||
|
filename: String,
|
||||||
|
mimeType: String,
|
||||||
|
base64Data: String,
|
||||||
|
size: Number,
|
||||||
|
originalHTML: String,
|
||||||
|
index: Number
|
||||||
|
}],
|
||||||
leaveType: {
|
leaveType: {
|
||||||
type: String,
|
type: String,
|
||||||
enum: ["SICK", "PTO"],
|
enum: ["SICK", "PTO"],
|
||||||
|
|
|
||||||
|
|
@ -31,10 +31,27 @@ class VaClientRepository {
|
||||||
|
|
||||||
async update(id, data) {
|
async update(id, data) {
|
||||||
try {
|
try {
|
||||||
|
// Convert nested userDetails object to dot notation
|
||||||
|
const updateObj = {};
|
||||||
|
|
||||||
|
// Handle userDetails fields with dot notation
|
||||||
|
if (data.userDetails) {
|
||||||
|
Object.keys(data.userDetails).forEach(key => {
|
||||||
|
updateObj[`userDetails.${key}`] = data.userDetails[key];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle other top-level fields if any
|
||||||
|
Object.keys(data).forEach(key => {
|
||||||
|
if (key !== 'userDetails') {
|
||||||
|
updateObj[key] = data[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const response = await ClientModel.findByIdAndUpdate(
|
const response = await ClientModel.findByIdAndUpdate(
|
||||||
id,
|
id,
|
||||||
{ $set: { ...data } },
|
{ $set: updateObj },
|
||||||
{ new: true }
|
{ new: true, runValidators: true }
|
||||||
);
|
);
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -42,9 +59,9 @@ class VaClientRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(id) {
|
async delete(id, options = {}) {
|
||||||
try {
|
try {
|
||||||
const response = await ClientModel.findByIdAndDelete(id);
|
const response = await ClientModel.findByIdAndDelete(id, options);
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import express from "express";
|
import express from "express";
|
||||||
const clientRoutes = express.Router();
|
const clientRoutes = express.Router();
|
||||||
|
|
||||||
import { va_register_client } from "../../controllers/client/vaClient.controller.js";
|
import { va_register_client, delete_client, update_client } from "../../controllers/client/vaClient.controller.js";
|
||||||
// import { va_clients_callback } from "../../controllers/va/vaClients/vaClients.controller.js";
|
// import { va_clients_callback } from "../../controllers/va/vaClients/vaClients.controller.js";
|
||||||
import { va_clients_projects_callback } from "../../controllers/va/vaClients/vaClientProject.controller.js";
|
import { va_clients_projects_callback } from "../../controllers/va/vaClients/vaClientProject.controller.js";
|
||||||
import { va_login_client } from "../../controllers/client/vaClientLogin.controller.js";
|
import { va_login_client } from "../../controllers/client/vaClientLogin.controller.js";
|
||||||
|
|
@ -21,6 +21,8 @@ clientRoutes.get("/refreshToken", client_refresh_token_callback); // Client Refr
|
||||||
clientRoutes.post("/forgot-password", client_request_password_reset);
|
clientRoutes.post("/forgot-password", client_request_password_reset);
|
||||||
clientRoutes.post("/verify-client-email", client_verify_email);
|
clientRoutes.post("/verify-client-email", client_verify_email);
|
||||||
clientRoutes.post("/logout", clientAuthValidator, client_logout_callback);
|
clientRoutes.post("/logout", clientAuthValidator, client_logout_callback);
|
||||||
|
clientRoutes.patch("/:clientId", update_client);
|
||||||
|
clientRoutes.delete("/:clientId", delete_client);
|
||||||
|
|
||||||
// clientRoutes.get("/:clientID?", va_clients_callback); // VA Clients Callback for getting all VA's or specific client by ID (NC)
|
// clientRoutes.get("/:clientID?", va_clients_callback); // VA Clients Callback for getting all VA's or specific client by ID (NC)
|
||||||
clientRoutes.get("/:clientId/projects/:projectId", va_clients_projects_callback); // VA Clients Projects Callback (NC)
|
clientRoutes.get("/:clientId/projects/:projectId", va_clients_projects_callback); // VA Clients Projects Callback (NC)
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ vaRoutes.get("/:userName", va_username_callback); // VA Username Callback
|
||||||
vaRoutes.delete("/:id", va_remove_function); // VA Username Callback
|
vaRoutes.delete("/:id", va_remove_function); // VA Username Callback
|
||||||
|
|
||||||
// timesheet adjustment routes
|
// timesheet adjustment routes
|
||||||
vaRoutes.get("/timesheet/summary/:projectId/:date", authValidator, va_timesheet_summary_function); // Get Timesheet Adjustment for VA on specific project and date
|
vaRoutes.get("/timesheet/summary/:projectId/:date/: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.post("/timesheet/adjustment/request", authValidator, va_timesheet_adjustment_function); // Timesheet Adjustment for VA
|
||||||
// vaRoutes.get("/timesheet/adjustments/list/:status?/:projectId?/:vaId?", get_adjustment_requests_list); // show all adjustment list to approve and reject
|
// vaRoutes.get("/timesheet/adjustments/list/:status?/:projectId?/:vaId?", get_adjustment_requests_list); // show all adjustment list to approve and reject
|
||||||
vaRoutes.patch("/timesheet/adjustments/:adjustmentId", va_timesheet_manage_adjustment_request);
|
vaRoutes.patch("/timesheet/adjustments/:adjustmentId", va_timesheet_manage_adjustment_request);
|
||||||
|
|
@ -67,7 +67,7 @@ vaRoutes.get("/search", va_filter_search);
|
||||||
|
|
||||||
|
|
||||||
// GET Windows Tab Logged Data for VA IN Between Login and LOGOUT
|
// GET Windows Tab Logged Data for VA IN Between Login and LOGOUT
|
||||||
vaRoutes.get("/report/project/:projectId/:from?/:to?", 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
|
// Getting Project List assigned to VA should be ACTIVE and add total logged hours added by VA
|
||||||
vaRoutes.get("/list/projects/:projectId?/:from?/:to?", authValidator, va_projects_log_list_function);
|
vaRoutes.get("/list/projects/:projectId?/:from?/:to?", authValidator, va_projects_log_list_function);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import logger from "../config/logger.js";
|
import logger from "../config/logger.js";
|
||||||
import SalesforceService from "./salesforce.service.js";
|
import SalesforceService from "./salesforce.service.js";
|
||||||
|
import { VAModel, ProjectModel, LoggedModel, WorkTrackModel } from "../models/vaModel.js";
|
||||||
|
|
||||||
class VaClientService {
|
class VaClientService {
|
||||||
constructor(vaClientRepository) {
|
constructor(vaClientRepository) {
|
||||||
|
|
@ -54,19 +55,142 @@ class VaClientService {
|
||||||
|
|
||||||
async updateClient(id, data) {
|
async updateClient(id, data) {
|
||||||
try {
|
try {
|
||||||
const response = await this.clientRepository.update(id, data);
|
// Check if client exists
|
||||||
if (!response) throw new Error("Client not found");
|
const existingClient = await this.clientRepository.findById(id);
|
||||||
return response;
|
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) {
|
} catch (error) {
|
||||||
logger.error("Client Service - updateClient error: " + error.message);
|
logger.error("Client Service - updateClient error: " + error.message);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteClient(id) {
|
async deleteClient(clientId) {
|
||||||
try {
|
try {
|
||||||
const response = await this.clientRepository.delete(id);
|
// Check if client exists
|
||||||
return response;
|
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) {
|
} catch (error) {
|
||||||
logger.error("Client Service - deleteClient error: " + error.message);
|
logger.error("Client Service - deleteClient error: " + error.message);
|
||||||
throw error;
|
throw error;
|
||||||
|
|
|
||||||
|
|
@ -266,17 +266,19 @@
|
||||||
<option value="" disabled selected></option>
|
<option value="" disabled selected></option>
|
||||||
</select>
|
</select>
|
||||||
<label for="country" class="input-label">Country *</label>
|
<label for="country" class="input-label">Country *</label>
|
||||||
|
<!-- <div class="location-loading" id="country-loading" style="display: none;">Detecting your location...</div> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="input-container">
|
<div class="input-container">
|
||||||
<select id="state" class="select-field" required onchange="handleChangeSelect(this)">
|
<input type="text" id="state" class="input-field" placeholder=" " required>
|
||||||
<!-- <option value=""></option> -->
|
<!-- <select id="state" class="select-field" required onchange="handleChangeSelect(this)">
|
||||||
</select>
|
<option value=""></option>
|
||||||
|
</select> -->
|
||||||
<label for="state" class="input-label">State / Region *</label>
|
<label for="state" class="input-label">State / Region *</label>
|
||||||
<div class="location-loading" id="state-loading">Loading states...</div>
|
<!-- <div class="location-loading" id="state-loading">Loading states...</div> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -327,7 +329,7 @@
|
||||||
</div>
|
</div>
|
||||||
<form id="payment-form">
|
<form id="payment-form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="payment-option" onclick="selectPaymentMethod('payoneer')">
|
<!-- <div class="payment-option" onclick="selectPaymentMethod('payoneer')">
|
||||||
<h4>Payoneer</h4>
|
<h4>Payoneer</h4>
|
||||||
<p>Receive payments via Payoneer</p>
|
<p>Receive payments via Payoneer</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -341,7 +343,7 @@
|
||||||
Account number must be 5-20 characters
|
Account number must be 5-20 characters
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> -->
|
||||||
|
|
||||||
<div class="payment-option" onclick="selectPaymentMethod('veem')">
|
<div class="payment-option" onclick="selectPaymentMethod('veem')">
|
||||||
<h4>Veem</h4>
|
<h4>Veem</h4>
|
||||||
|
|
@ -358,6 +360,22 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="payment-option" onclick="selectPaymentMethod('wise')">
|
||||||
|
<h4>Wise</h4>
|
||||||
|
<p>Receive payments via Wise</p>
|
||||||
|
</div>
|
||||||
|
<div id="wise-details" class="payment-details">
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-container">
|
||||||
|
<input type="email" id="wiseEmail" class="input-field" placeholder=" " oninput="validateWiseEmail(this)" required>
|
||||||
|
<label for="wiseEmail" class="input-label">Wise Email Address *</label>
|
||||||
|
</div>
|
||||||
|
<div class="invalid-feedback" id="wise-email-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
|
||||||
|
Please enter a valid email address for Wise
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue