Added delete route form project, va, and client - Updated leaves and time adjustment api
This commit is contained in:
parent
39010ee61c
commit
16a62670e7
|
|
@ -9,65 +9,113 @@
|
|||
|
||||
// Countries and states data
|
||||
const countries = [
|
||||
{ code: 'US', name: 'United States' },
|
||||
{ code: 'CA', name: 'Canada' },
|
||||
{ code: 'GB', name: 'United Kingdom' },
|
||||
{ code: 'AU', name: 'Australia' },
|
||||
{ code: 'IN', name: 'India' },
|
||||
{ code: 'DE', name: 'Germany' },
|
||||
{ code: 'FR', name: 'France' },
|
||||
{ code: 'BR', name: 'Brazil' },
|
||||
{ code: 'JP', name: 'Japan' },
|
||||
{ code: 'CN', name: 'China' }
|
||||
{ code: 'US', name: 'United States' },
|
||||
{ code: 'CA', name: 'Canada' },
|
||||
{ code: 'GB', name: 'United Kingdom' },
|
||||
{ code: 'AU', name: 'Australia' },
|
||||
{ code: 'IN', name: 'India' },
|
||||
{ code: 'DE', name: 'Germany' },
|
||||
{ code: 'FR', name: 'France' },
|
||||
{ code: 'BR', name: 'Brazil' },
|
||||
{ code: 'JP', name: 'Japan' },
|
||||
{ code: 'CN', name: 'China' },
|
||||
{ code: 'MX', name: 'Mexico' },
|
||||
{ code: 'ES', name: 'Spain' },
|
||||
{ code: 'IT', name: 'Italy' },
|
||||
{ code: 'NL', name: 'Netherlands' },
|
||||
{ code: 'SE', name: 'Sweden' },
|
||||
{ code: 'NO', name: 'Norway' },
|
||||
{ code: 'DK', name: 'Denmark' },
|
||||
{ code: 'FI', name: 'Finland' },
|
||||
{ code: 'BE', name: 'Belgium' },
|
||||
{ code: 'CH', name: 'Switzerland' },
|
||||
{ code: 'AT', name: 'Austria' },
|
||||
{ code: 'PT', name: 'Portugal' },
|
||||
{ code: 'IE', name: 'Ireland' },
|
||||
{ code: 'PL', name: 'Poland' },
|
||||
{ code: 'CZ', name: 'Czech Republic' },
|
||||
{ code: 'HU', name: 'Hungary' },
|
||||
{ code: 'RO', name: 'Romania' },
|
||||
{ code: 'GR', name: 'Greece' },
|
||||
{ code: 'TR', name: 'Turkey' },
|
||||
{ code: 'SA', name: 'Saudi Arabia' },
|
||||
{ code: 'AE', name: 'United Arab Emirates' },
|
||||
{ code: 'ZA', name: 'South Africa' },
|
||||
{ code: 'NG', name: 'Nigeria' },
|
||||
{ code: 'EG', name: 'Egypt' },
|
||||
{ code: 'KE', name: 'Kenya' },
|
||||
{ code: 'AR', name: 'Argentina' },
|
||||
{ code: 'CO', name: 'Colombia' },
|
||||
{ code: 'PE', name: 'Peru' },
|
||||
{ code: 'CL', name: 'Chile' },
|
||||
{ code: 'VE', name: 'Venezuela' },
|
||||
{ code: 'KR', name: 'South Korea' },
|
||||
{ code: 'SG', name: 'Singapore' },
|
||||
{ code: 'MY', name: 'Malaysia' },
|
||||
{ code: 'TH', name: 'Thailand' },
|
||||
{ code: 'ID', name: 'Indonesia' },
|
||||
{ code: 'PH', name: 'Philippines' },
|
||||
{ code: 'VN', name: 'Vietnam' },
|
||||
{ code: 'PK', name: 'Pakistan' },
|
||||
{ code: 'BD', name: 'Bangladesh' },
|
||||
{ code: 'LK', name: 'Sri Lanka' },
|
||||
{ code: 'NP', name: 'Nepal' },
|
||||
{ code: 'RU', name: 'Russia' },
|
||||
{ code: 'UA', name: 'Ukraine' },
|
||||
{ code: 'IL', name: 'Israel' },
|
||||
{ code: 'NZ', name: 'New Zealand' },
|
||||
{ code: 'HK', name: 'Hong Kong' },
|
||||
{ code: 'TW', name: 'Taiwan' }
|
||||
];
|
||||
|
||||
const states = {
|
||||
'US': [
|
||||
{ code: 'CA', name: 'California' }, { code: 'TX', name: 'Texas' }, { code: 'FL', name: 'Florida' },
|
||||
{ code: 'NY', name: 'New York' }, { code: 'IL', name: 'Illinois' }, { code: 'PA', name: 'Pennsylvania' },
|
||||
{ code: 'OH', name: 'Ohio' }, { code: 'GA', name: 'Georgia' }, { code: 'NC', name: 'North Carolina' },
|
||||
{ code: 'MI', name: 'Michigan' }
|
||||
],
|
||||
'CA': [
|
||||
{ code: 'ON', name: 'Ontario' }, { code: 'QC', name: 'Quebec' }, { code: 'BC', name: 'British Columbia' },
|
||||
{ code: 'AB', name: 'Alberta' }, { code: 'MB', name: 'Manitoba' }, { code: 'SK', name: 'Saskatchewan' }
|
||||
],
|
||||
'GB': [
|
||||
{ code: 'ENG', name: 'England' }, { code: 'SCT', name: 'Scotland' }, { code: 'WLS', name: 'Wales' },
|
||||
{ code: 'NIR', name: 'Northern Ireland' }
|
||||
],
|
||||
'AU': [
|
||||
{ code: 'NSW', name: 'New South Wales' }, { code: 'VIC', name: 'Victoria' }, { code: 'QLD', name: 'Queensland' },
|
||||
{ code: 'WA', name: 'Western Australia' }, { code: 'SA', name: 'South Australia' }, { code: 'TAS', name: 'Tasmania' }
|
||||
],
|
||||
'IN': [
|
||||
{ code: 'MH', name: 'Maharashtra' }, { code: 'TN', name: 'Tamil Nadu' }, { code: 'UP', name: 'Uttar Pradesh' },
|
||||
{ code: 'KA', name: 'Karnataka' }, { code: 'GJ', name: 'Gujarat' }, { code: 'RJ', name: 'Rajasthan' }
|
||||
],
|
||||
'DE': [
|
||||
{ code: 'BW', name: 'Baden-Württemberg' }, { code: 'BY', name: 'Bavaria' }, { code: 'BE', name: 'Berlin' },
|
||||
{ code: 'HB', name: 'Bremen' }, { code: 'HH', name: 'Hamburg' }, { code: 'HE', name: 'Hesse' }
|
||||
],
|
||||
'FR': [
|
||||
{ code: 'ARA', name: 'Auvergne-Rhône-Alpes' }, { code: 'BFC', name: 'Bourgogne-Franche-Comté' },
|
||||
{ code: 'BRE', name: 'Brittany' }, { code: 'CVL', name: 'Centre-Val de Loire' },
|
||||
{ code: 'COR', name: 'Corsica' }, { code: 'GES', name: 'Grand Est' }
|
||||
],
|
||||
'BR': [
|
||||
{ code: 'SP', name: 'São Paulo' }, { code: 'RJ', name: 'Rio de Janeiro' }, { code: 'MG', name: 'Minas Gerais' },
|
||||
{ code: 'BA', name: 'Bahia' }, { code: 'RS', name: 'Rio Grande do Sul' }, { code: 'PR', name: 'Paraná' }
|
||||
],
|
||||
'JP': [
|
||||
{ code: 'TK', name: 'Tokyo' }, { code: 'OS', name: 'Osaka' }, { code: 'KY', name: 'Kyoto' },
|
||||
{ code: 'HK', name: 'Hokkaido' }, { code: 'OK', name: 'Okinawa' }, { code: 'KN', name: 'Kanagawa' }
|
||||
],
|
||||
'CN': [
|
||||
{ code: 'BJ', name: 'Beijing' }, { code: 'SH', name: 'Shanghai' }, { code: 'TJ', name: 'Tianjin' },
|
||||
{ code: 'CQ', name: 'Chongqing' }, { code: 'GD', name: 'Guangdong' }, { code: 'JS', name: 'Jiangsu' }
|
||||
]
|
||||
};
|
||||
// const states = {
|
||||
// 'US': [
|
||||
// { code: 'CA', name: 'California' }, { code: 'TX', name: 'Texas' }, { code: 'FL', name: 'Florida' },
|
||||
// { code: 'NY', name: 'New York' }, { code: 'IL', name: 'Illinois' }, { code: 'PA', name: 'Pennsylvania' },
|
||||
// { code: 'OH', name: 'Ohio' }, { code: 'GA', name: 'Georgia' }, { code: 'NC', name: 'North Carolina' },
|
||||
// { code: 'MI', name: 'Michigan' }
|
||||
// ],
|
||||
// 'CA': [
|
||||
// { code: 'ON', name: 'Ontario' }, { code: 'QC', name: 'Quebec' }, { code: 'BC', name: 'British Columbia' },
|
||||
// { code: 'AB', name: 'Alberta' }, { code: 'MB', name: 'Manitoba' }, { code: 'SK', name: 'Saskatchewan' }
|
||||
// ],
|
||||
// 'GB': [
|
||||
// { code: 'ENG', name: 'England' }, { code: 'SCT', name: 'Scotland' }, { code: 'WLS', name: 'Wales' },
|
||||
// { code: 'NIR', name: 'Northern Ireland' }
|
||||
// ],
|
||||
// 'AU': [
|
||||
// { code: 'NSW', name: 'New South Wales' }, { code: 'VIC', name: 'Victoria' }, { code: 'QLD', name: 'Queensland' },
|
||||
// { code: 'WA', name: 'Western Australia' }, { code: 'SA', name: 'South Australia' }, { code: 'TAS', name: 'Tasmania' }
|
||||
// ],
|
||||
// 'IN': [
|
||||
// { code: 'MH', name: 'Maharashtra' }, { code: 'TN', name: 'Tamil Nadu' }, { code: 'UP', name: 'Uttar Pradesh' },
|
||||
// { code: 'KA', name: 'Karnataka' }, { code: 'GJ', name: 'Gujarat' }, { code: 'RJ', name: 'Rajasthan' }
|
||||
// ],
|
||||
// 'DE': [
|
||||
// { code: 'BW', name: 'Baden-Württemberg' }, { code: 'BY', name: 'Bavaria' }, { code: 'BE', name: 'Berlin' },
|
||||
// { code: 'HB', name: 'Bremen' }, { code: 'HH', name: 'Hamburg' }, { code: 'HE', name: 'Hesse' }
|
||||
// ],
|
||||
// 'FR': [
|
||||
// { code: 'ARA', name: 'Auvergne-Rhône-Alpes' }, { code: 'BFC', name: 'Bourgogne-Franche-Comté' },
|
||||
// { code: 'BRE', name: 'Brittany' }, { code: 'CVL', name: 'Centre-Val de Loire' },
|
||||
// { code: 'COR', name: 'Corsica' }, { code: 'GES', name: 'Grand Est' }
|
||||
// ],
|
||||
// 'BR': [
|
||||
// { code: 'SP', name: 'São Paulo' }, { code: 'RJ', name: 'Rio de Janeiro' }, { code: 'MG', name: 'Minas Gerais' },
|
||||
// { code: 'BA', name: 'Bahia' }, { code: 'RS', name: 'Rio Grande do Sul' }, { code: 'PR', name: 'Paraná' }
|
||||
// ],
|
||||
// 'JP': [
|
||||
// { code: 'TK', name: 'Tokyo' }, { code: 'OS', name: 'Osaka' }, { code: 'KY', name: 'Kyoto' },
|
||||
// { code: 'HK', name: 'Hokkaido' }, { code: 'OK', name: 'Okinawa' }, { code: 'KN', name: 'Kanagawa' }
|
||||
// ],
|
||||
// 'CN': [
|
||||
// { code: 'BJ', name: 'Beijing' }, { code: 'SH', name: 'Shanghai' }, { code: 'TJ', name: 'Tianjin' },
|
||||
// { code: 'CQ', name: 'Chongqing' }, { code: 'GD', name: 'Guangdong' }, { code: 'JS', name: 'Jiangsu' }
|
||||
// ]
|
||||
// };
|
||||
|
||||
// Initialize the form
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Populate country dropdown
|
||||
const countrySelect = document.getElementById('country');
|
||||
|
|
@ -80,32 +128,34 @@
|
|||
countrySelect.appendChild(option);
|
||||
});
|
||||
|
||||
autoDetectCountry();
|
||||
|
||||
// Add event listener for country change
|
||||
countrySelect.addEventListener('change', function() {
|
||||
const countryCode = this.value;
|
||||
const stateSelect = document.getElementById('state');
|
||||
// countrySelect.addEventListener('change', function() {
|
||||
// const countryCode = this.value;
|
||||
// const stateSelect = document.getElementById('state');
|
||||
|
||||
// Show loading
|
||||
document.getElementById('state-loading').style.display = 'block';
|
||||
// // Show loading
|
||||
// document.getElementById('state-loading').style.display = 'block';
|
||||
|
||||
// Clear current states
|
||||
stateSelect.innerHTML = '<option value="" disabled selected></option>';
|
||||
// // Clear current states
|
||||
// stateSelect.innerHTML = '<option value="" disabled selected></option>';
|
||||
|
||||
// Simulate API delay
|
||||
setTimeout(() => {
|
||||
if (countryCode && states[countryCode]) {
|
||||
states[countryCode].forEach(state => {
|
||||
const option = document.createElement('option');
|
||||
option.value = state.code;
|
||||
option.textContent = state.name;
|
||||
stateSelect.appendChild(option);
|
||||
});
|
||||
}
|
||||
// // Simulate API delay
|
||||
// setTimeout(() => {
|
||||
// if (countryCode && states[countryCode]) {
|
||||
// states[countryCode].forEach(state => {
|
||||
// const option = document.createElement('option');
|
||||
// option.value = state.code;
|
||||
// option.textContent = state.name;
|
||||
// stateSelect.appendChild(option);
|
||||
// });
|
||||
// }
|
||||
|
||||
// Hide loading
|
||||
document.getElementById('state-loading').style.display = 'none';
|
||||
}, 500);
|
||||
});
|
||||
// // Hide loading
|
||||
// document.getElementById('state-loading').style.display = 'none';
|
||||
// }, 500);
|
||||
// });
|
||||
|
||||
|
||||
// Add input event listeners to handle label animation
|
||||
|
|
@ -209,6 +259,102 @@
|
|||
});
|
||||
});
|
||||
|
||||
async function autoDetectCountry() {
|
||||
const countrySelect = document.getElementById('country');
|
||||
// const loadingElement = document.getElementById('country-loading');
|
||||
|
||||
try {
|
||||
// loadingElement.style.display = 'block';
|
||||
|
||||
// Using ipapi.co API (free tier available)
|
||||
const response = await fetch('https://ipapi.co/json/');
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Geolocation service unavailable');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data && data.country_code) {
|
||||
const detectedCountryCode = data.country_code;
|
||||
const detectedCountryName = data.country_name;
|
||||
|
||||
// Find if the detected country is in our list
|
||||
const countryExists = countries.find(country =>
|
||||
country.code === detectedCountryCode
|
||||
);
|
||||
|
||||
if (countryExists) {
|
||||
// Set the country in dropdown
|
||||
countrySelect.value = detectedCountryCode;
|
||||
|
||||
// Trigger the change event to update UI
|
||||
countrySelect.dispatchEvent(new Event('change'));
|
||||
|
||||
// Show success message
|
||||
console.log(`Auto-detected country: ${detectedCountryName}`);
|
||||
} else {
|
||||
console.log('Detected country not in supported list');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Could not auto-detect country:', error.message);
|
||||
// Fallback: Use browser's geolocation API as backup
|
||||
fallbackGeolocation();
|
||||
} finally {
|
||||
// loadingElement.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback geolocation using browser's navigator.geolocation
|
||||
function fallbackGeolocation() {
|
||||
if (!navigator.geolocation) {
|
||||
console.log('Geolocation is not supported by this browser');
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
async function(position) {
|
||||
try {
|
||||
const { latitude, longitude } = position.coords;
|
||||
|
||||
// Use OpenStreetMap Nominatim API for reverse geocoding
|
||||
const response = await fetch(
|
||||
`https://nominatim.openstreetmap.org/reverse?format=json&lat=${latitude}&lon=${longitude}`
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data && data.address && data.address.country_code) {
|
||||
const countryCode = data.address.country_code.toUpperCase();
|
||||
const countrySelect = document.getElementById('country');
|
||||
|
||||
const countryExists = countries.find(country =>
|
||||
country.code === countryCode
|
||||
);
|
||||
|
||||
if (countryExists) {
|
||||
countrySelect.value = countryCode;
|
||||
countrySelect.dispatchEvent(new Event('change'));
|
||||
console.log(`Fallback geolocation detected: ${data.address.country}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Fallback geolocation failed:', error);
|
||||
}
|
||||
},
|
||||
function(error) {
|
||||
console.log('Browser geolocation error:', error.message);
|
||||
},
|
||||
{
|
||||
enableHighAccuracy: false,
|
||||
timeout: 10000,
|
||||
maximumAge: 600000 // 10 minutes
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Function to navigate to any step (for tab navigation)
|
||||
function goToStep(step) {
|
||||
// Only allow navigation to completed steps or the next step
|
||||
|
|
@ -469,20 +615,29 @@
|
|||
showError('Please select a payment method', null, step);
|
||||
isValid = false;
|
||||
} else {
|
||||
const paymentMethod = selectedPayment.getAttribute('onclick').match(/'([^']+)'/)[1];
|
||||
if (paymentMethod === 'payoneer') {
|
||||
const payoneerAccount = document.getElementById('payoneerAccount').value;
|
||||
if (!payoneerAccount) {
|
||||
showError('Please enter your Payoneer account number', 'payoneerAccount', step);
|
||||
isValid = false;
|
||||
}
|
||||
} else if (paymentMethod === 'veem') {
|
||||
const veemAccount = document.getElementById('veemAccount').value;
|
||||
if (!veemAccount) {
|
||||
showError('Please enter your Veem account number', 'veemAccount', step);
|
||||
isValid = false;
|
||||
}
|
||||
}
|
||||
const paymentMethod = selectedPayment.getAttribute('onclick').match(/'([^']+)'/)[1];
|
||||
if (paymentMethod === 'payoneer') {
|
||||
// const payoneerAccount = document.getElementById('payoneerAccount').value;
|
||||
// if (!payoneerAccount) {
|
||||
// showError('Please enter your Payoneer account number', 'payoneerAccount', step);
|
||||
// isValid = false;
|
||||
// }
|
||||
} else if (paymentMethod === 'veem') {
|
||||
const veemAccount = document.getElementById('veemAccount').value;
|
||||
if (!veemAccount) {
|
||||
showError('Please enter your Veem account number', 'veemAccount', step);
|
||||
isValid = false;
|
||||
}
|
||||
} else if (paymentMethod === 'wise') {
|
||||
const wiseEmail = document.getElementById('wiseEmail').value;
|
||||
if (!wiseEmail) {
|
||||
showError('Please enter your Wise email address', 'wiseEmail', step);
|
||||
isValid = false;
|
||||
} else if (!validateEmail(wiseEmail)) {
|
||||
showError('Please enter a valid email address for Wise', 'wiseEmail', step);
|
||||
isValid = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -547,12 +702,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 +733,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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
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);
|
||||
}
|
||||
|
||||
// Send data to Salesforce API after successful registration
|
||||
try {
|
||||
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/`;
|
||||
res.status(201).json({ success: true, data: created, relationships: relationshipUpdate });
|
||||
|
||||
const response = await fetch(salesforceUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(created)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log("✅ Successfully synced VA data to Salesforce:", result);
|
||||
|
||||
res.status(201).json({ success: true, data: created, SyncSalesforce: result });
|
||||
// res.status(201).json({ success: true, data: created, relationships: relationshipUpdate });
|
||||
} catch (error) {
|
||||
console.error("❌ Failed to sync with Salesforce:", error.message);
|
||||
|
||||
// Create a custom error to distinguish Salesforce errors
|
||||
const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
|
||||
salesforceError.isSalesforceError = true;
|
||||
throw salesforceError;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("project_add_sf error:", error);
|
||||
return res.status(500).json({ success: false, message: "Internal Server Error" });
|
||||
|
|
|
|||
|
|
@ -131,6 +131,7 @@ async function va_leave_apply_function(req, res) {
|
|||
});
|
||||
}
|
||||
|
||||
const processedComment = processHTMLComment(comment);
|
||||
// Create leave request
|
||||
const leaveRequest = new LeavesModel({
|
||||
vaId,
|
||||
|
|
@ -138,62 +139,62 @@ async function va_leave_apply_function(req, res) {
|
|||
dateTo: endDate,
|
||||
days: totalDays.toString(),
|
||||
leaveType,
|
||||
comment,
|
||||
comment: processedComment.processedHTML,
|
||||
attachments: processedComment.extractedImages,
|
||||
status: "PENDING"
|
||||
});
|
||||
|
||||
await leaveRequest.save();
|
||||
|
||||
try {
|
||||
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/timesheet-adjustment`;
|
||||
const sfData = {
|
||||
"id": adjustmentRequest._id,
|
||||
"va": {
|
||||
"id": vaId
|
||||
},
|
||||
"project": {
|
||||
"id": projectId
|
||||
},
|
||||
"date": adjustmentDate.toISOString().split('T')[0],
|
||||
"loginTime": loginDateTime.toISOString(),
|
||||
"logoutTime": logoutDateTime.toISOString(),
|
||||
"comment": comment,
|
||||
"status": "PENDING",
|
||||
"rejectionReason": "",
|
||||
"adjustedHours": formatTime(adjustedSeconds)
|
||||
}
|
||||
// const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/timesheet-adjustment`;
|
||||
// const sfData = {
|
||||
// "id": leaveRequest._id,
|
||||
// "va": {
|
||||
// "id": vaId
|
||||
// },
|
||||
// "dateFrom": startDate.toISOString().split('T')[0],
|
||||
// "dateTo": endDate.toISOString().split('T')[0],
|
||||
// "totalDays": totalDays,
|
||||
// "leaveType": leaveType,
|
||||
// "comment": processedComment.plainText, // Send plain text to Salesforce
|
||||
// "attachments": processedComment.extractedImages, // Send extracted images info
|
||||
// "status": "PENDING",
|
||||
// "rejectionReason": ""
|
||||
// }
|
||||
|
||||
const response = await fetch(salesforceUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
// Add any required headers for Salesforce authentication
|
||||
// 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
|
||||
},
|
||||
body: JSON.stringify({"data": sfData})
|
||||
});
|
||||
// const response = await fetch(salesforceUrl, {
|
||||
// method: 'POST',
|
||||
// headers: {
|
||||
// 'Content-Type': 'application/json',
|
||||
// // Add any required headers for Salesforce authentication
|
||||
// // 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
|
||||
// },
|
||||
// body: JSON.stringify({"data": sfData})
|
||||
// });
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
|
||||
}
|
||||
// if (!response.ok) {
|
||||
// const errorText = await response.text();
|
||||
// throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
|
||||
// }
|
||||
|
||||
const result = await response.json();
|
||||
// console.log("✅ Successfully synced VA data to Salesforce:", result);
|
||||
// const result = await response.json();
|
||||
// // console.log("✅ Successfully synced VA data to Salesforce:", result);
|
||||
|
||||
return res.status(201).json({
|
||||
success: true,
|
||||
message: "Adjustment request submitted successfully",
|
||||
message: "Leave request submitted successfully",
|
||||
data: {
|
||||
adjustmentId: adjustmentRequest._id,
|
||||
date: adjustmentDate.toISOString().split('T')[0],
|
||||
loginTime: loginDateTime.toISOString(),
|
||||
logoutTime: logoutDateTime.toISOString(),
|
||||
adjustedHours: formatTime(adjustedSeconds),
|
||||
comment: comment,
|
||||
leaveId: leaveRequest._id,
|
||||
dateFrom: startDate.toISOString().split('T')[0],
|
||||
dateTo: endDate.toISOString().split('T')[0],
|
||||
totalDays: totalDays,
|
||||
leaveType,
|
||||
comment: processedComment.processedHTML,
|
||||
attachments: processedComment.extractedImages,
|
||||
status: "PENDING"
|
||||
},
|
||||
SyncSalesforce: result
|
||||
// SyncSalesforce: result
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to sync with Salesforce:", error.message);
|
||||
|
|
@ -238,4 +239,116 @@ async function va_leave_apply_function(req, res) {
|
|||
}
|
||||
}
|
||||
|
||||
function processHTMLComment(htmlComment) {
|
||||
try {
|
||||
let processedHTML = htmlComment;
|
||||
const extractedImages = [];
|
||||
let plainText = '';
|
||||
|
||||
// Extract plain text from HTML for Salesforce
|
||||
plainText = extractPlainTextFromHTML(htmlComment);
|
||||
|
||||
// Process base64 images in HTML
|
||||
const imageProcessingResult = extractAndProcessBase64Images(htmlComment);
|
||||
processedHTML = imageProcessingResult.processedHTML;
|
||||
extractedImages.push(...imageProcessingResult.extractedImages);
|
||||
|
||||
return {
|
||||
processedHTML,
|
||||
plainText,
|
||||
extractedImages
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error processing HTML comment:", error);
|
||||
// Return original content if processing fails
|
||||
return {
|
||||
processedHTML: htmlComment,
|
||||
plainText: extractPlainTextFromHTML(htmlComment),
|
||||
extractedImages: []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to extract plain text from HTML
|
||||
function extractPlainTextFromHTML(html) {
|
||||
try {
|
||||
// Remove HTML tags and decode HTML entities
|
||||
let plainText = html
|
||||
.replace(/<[^>]*>/g, ' ') // Replace tags with spaces
|
||||
.replace(/\s+/g, ' ') // Collapse multiple spaces
|
||||
.trim();
|
||||
|
||||
// Decode common HTML entities
|
||||
plainText = plainText
|
||||
.replace(/ /g, ' ')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/'/g, "'")
|
||||
.replace(///g, '/');
|
||||
|
||||
return plainText;
|
||||
} catch (error) {
|
||||
console.error("Error extracting plain text from HTML:", error);
|
||||
return html.replace(/<[^>]*>/g, ' ').trim();
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to extract and process base64 images from HTML
|
||||
function extractAndProcessBase64Images(html) {
|
||||
const extractedImages = [];
|
||||
let processedHTML = html;
|
||||
|
||||
try {
|
||||
// Regex to find base64 images in HTML (various formats)
|
||||
const base64Regex = /<img[^>]+src="data:image\/([a-zA-Z0-9+]+);base64,([^"]+)"[^>]*>/gi;
|
||||
|
||||
let match;
|
||||
let imageIndex = 0;
|
||||
|
||||
while ((match = base64Regex.exec(html)) !== null) {
|
||||
const fullMatch = match[0];
|
||||
const imageType = match[1]; // png, jpeg, gif, etc.
|
||||
const base64Data = match[2];
|
||||
|
||||
// Create a unique filename
|
||||
const timestamp = Date.now();
|
||||
const filename = `leave_image_${timestamp}_${imageIndex}.${imageType}`;
|
||||
|
||||
// Calculate approximate file size
|
||||
const size = Math.floor((base64Data.length * 3) / 4); // Base64 size calculation
|
||||
|
||||
// Store image information
|
||||
extractedImages.push({
|
||||
filename: filename,
|
||||
mimeType: `image/${imageType}`,
|
||||
base64Data: base64Data,
|
||||
size: size,
|
||||
originalHTML: fullMatch, // Store original img tag for reference
|
||||
index: imageIndex
|
||||
});
|
||||
|
||||
// Replace base64 with a reference in HTML
|
||||
const replacementHTML = `<img src="attachment://${filename}" alt="Leave evidence ${imageIndex + 1}" data-filename="${filename}" data-image-index="${imageIndex}" class="leave-image">`;
|
||||
|
||||
processedHTML = processedHTML.replace(fullMatch, replacementHTML);
|
||||
|
||||
imageIndex++;
|
||||
}
|
||||
|
||||
return {
|
||||
processedHTML,
|
||||
extractedImages
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error processing base64 images:", error);
|
||||
return {
|
||||
processedHTML: html,
|
||||
extractedImages: []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export { va_leave_apply_function };
|
||||
|
|
@ -5,27 +5,60 @@ import { VAModel } from "../../../models/vaModel.js";
|
|||
// Register a new VA
|
||||
async function va_profile_update_callback(req, res) {
|
||||
try {
|
||||
const { sf_token } = req.headers.sf_token;
|
||||
const accessToken = req.headers.token;
|
||||
|
||||
if (!accessToken) {
|
||||
return res.status(401).json({ success: false, message: "Missing access token" });
|
||||
let vaUser;
|
||||
let authSource = 'regular';
|
||||
|
||||
if(sf_token) {
|
||||
try {
|
||||
const sfDecoded = jwt.verify(sf_token, process.env.JWT_SECRET_SF_TOKEN);
|
||||
|
||||
if (!sfDecoded?.email && !sfDecoded?.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) {
|
||||
return res.status(401).json({ success: false, message: "Missing access token" });
|
||||
}
|
||||
|
||||
const decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN);
|
||||
|
||||
if (!decoded?.email) {
|
||||
return res.status(400).json({ success: false, message: "Invalid token payload" });
|
||||
}
|
||||
|
||||
vaUser = await VAModel.findOne({ "userDetails.email": decoded.email });
|
||||
if (!vaUser) {
|
||||
return res.status(404).json({ success: false, message: "VA not found" });
|
||||
}
|
||||
|
||||
authSource = 'regular';
|
||||
|
||||
} catch (tokenError) {
|
||||
return res.status(401).json({ success: false, message: "Invalid or expired access token" });
|
||||
}
|
||||
}
|
||||
|
||||
let decoded;
|
||||
try {
|
||||
decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN);
|
||||
} catch (err) {
|
||||
return res.status(401).json({ success: false, message: "Invalid or expired token" });
|
||||
}
|
||||
|
||||
if (!decoded?.email) {
|
||||
return res.status(400).json({ success: false, message: "Invalid token payload" });
|
||||
}
|
||||
|
||||
const vaUser = await VAModel.findOne({ "userDetails.email": decoded.email });
|
||||
if (!vaUser) {
|
||||
return res.status(404).json({ success: false, message: "VA not found" });
|
||||
}
|
||||
|
||||
const payload = req.body || {};
|
||||
const { userDetails } = payload;
|
||||
|
|
@ -34,23 +67,36 @@ async function va_profile_update_callback(req, res) {
|
|||
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 +124,16 @@ 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",
|
||||
});
|
||||
}
|
||||
|
||||
await vaUser.save();
|
||||
|
|
@ -95,6 +146,7 @@ async function va_profile_update_callback(req, res) {
|
|||
success: true,
|
||||
message: "Profile updated successfully",
|
||||
data: updatedUser,
|
||||
updatedFields: updatedFields,
|
||||
});
|
||||
} catch (error) {
|
||||
return res.status(500).json({ success: false, message: "Internal Server Error" });
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ async function va_register(req, res) {
|
|||
paymentMethod: userDetails.paymentMethod || null,
|
||||
payoneerAccount: userDetails.payoneerAccount || null,
|
||||
veemAccount: userDetails.veemAccount || null,
|
||||
wiseEmail: userDetails.wiseEmail || null,
|
||||
phone: userDetails.phone || null,
|
||||
skype: userDetails.skype || null,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -11,23 +11,36 @@ function formatHMS(ms) {
|
|||
|
||||
async function va_projects_report_function(req, res) {
|
||||
try {
|
||||
const accessToken = req.headers.token;
|
||||
if (!accessToken) {
|
||||
return res.status(401).json({ success: false, message: "Missing token" });
|
||||
}
|
||||
const sf_token = req.headers.sf_token;
|
||||
let vaId;
|
||||
let vaUser;
|
||||
let vaEmail;
|
||||
if(sf_token) {
|
||||
vaId = req.body.vaId;
|
||||
vaUser = await VAModel.findById(vaId);
|
||||
if (!vaUser) {
|
||||
return res.status(404).json({ success: false, message: "VA not found" });
|
||||
}
|
||||
vaEmail = vaUser.userDetails.email
|
||||
} else {
|
||||
const accessToken = req.headers.token;
|
||||
if (!accessToken) {
|
||||
return res.status(401).json({ success: false, message: "Missing token" });
|
||||
}
|
||||
|
||||
const decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN);
|
||||
if (!decoded || !decoded.email) {
|
||||
return res.status(401).json({ success: false, message: "Invalid token" });
|
||||
}
|
||||
const decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN);
|
||||
if (!decoded || !decoded.email) {
|
||||
return res.status(401).json({ success: false, message: "Invalid token" });
|
||||
}
|
||||
|
||||
const vaEmail = decoded.email;
|
||||
const vaUser = await VAModel.findOne({ "userDetails.email": vaEmail });
|
||||
if (!vaUser) {
|
||||
return res.status(404).json({ success: false, message: "VA not found" });
|
||||
}
|
||||
vaEmail = decoded.email;
|
||||
vaUser = await VAModel.findOne({ "userDetails.email": vaEmail });
|
||||
if (!vaUser) {
|
||||
return res.status(404).json({ success: false, message: "VA not found" });
|
||||
}
|
||||
|
||||
const vaId = vaUser._id;
|
||||
vaId = vaUser._id;
|
||||
}
|
||||
const { projectId, from, to } = req.params;
|
||||
|
||||
if (!projectId) {
|
||||
|
|
|
|||
|
|
@ -59,14 +59,6 @@ async function va_timesheet_adjustment_function(req, res) {
|
|||
|
||||
const combineDateTime = (dateObj, timeStr) => {
|
||||
const [hours, minutes, seconds] = timeStr.split(':').map(Number);
|
||||
|
||||
// // Extract date components from the Date object
|
||||
// const year = dateObj.getFullYear();
|
||||
// const month = dateObj.getMonth();
|
||||
// const day = dateObj.getDate();
|
||||
|
||||
// // Create UTC date directly
|
||||
// return new Date(year, month, day, hours, minutes, seconds);
|
||||
const combined = new Date(dateObj);
|
||||
combined.setHours(hours, minutes, seconds, 0);
|
||||
|
||||
|
|
@ -120,13 +112,16 @@ async function va_timesheet_adjustment_function(req, res) {
|
|||
});
|
||||
}
|
||||
|
||||
const processedComment = processHTMLComment(comment);
|
||||
|
||||
const adjustmentRequest = new AdjustedHoursModel({
|
||||
vaId,
|
||||
projectId,
|
||||
date: startOfDay,
|
||||
loginTime: loginDateTime,
|
||||
logoutTime: logoutDateTime,
|
||||
comment,
|
||||
comment: processedComment.processedHTML,
|
||||
attachments: processedComment.extractedImages,
|
||||
status: "PENDING"
|
||||
});
|
||||
|
||||
|
|
@ -153,7 +148,8 @@ async function va_timesheet_adjustment_function(req, res) {
|
|||
"date": adjustmentDate.toISOString().split('T')[0],
|
||||
"loginTime": loginDateTime.toISOString(),
|
||||
"logoutTime": logoutDateTime.toISOString(),
|
||||
"comment": comment,
|
||||
"comment": processedComment.plainText, // Send plain text to Salesforce
|
||||
"attachments": processedComment.extractedImages, // Send extracted images info
|
||||
"status": "PENDING",
|
||||
"rejectionReason": "",
|
||||
"adjustedHours": formatTime(adjustedSeconds)
|
||||
|
|
@ -186,7 +182,8 @@ async function va_timesheet_adjustment_function(req, res) {
|
|||
loginTime: loginDateTime.toISOString(),
|
||||
logoutTime: logoutDateTime.toISOString(),
|
||||
adjustedHours: formatTime(adjustedSeconds),
|
||||
comment: comment,
|
||||
comment: processedComment.processedHTML,
|
||||
attachments: processedComment.extractedImages,
|
||||
status: "PENDING"
|
||||
},
|
||||
SyncSalesforce: result
|
||||
|
|
@ -221,4 +218,116 @@ async function va_timesheet_adjustment_function(req, res) {
|
|||
}
|
||||
}
|
||||
|
||||
function processHTMLComment(htmlComment) {
|
||||
try {
|
||||
let processedHTML = htmlComment;
|
||||
const extractedImages = [];
|
||||
let plainText = '';
|
||||
|
||||
// Extract plain text from HTML for Salesforce
|
||||
plainText = extractPlainTextFromHTML(htmlComment);
|
||||
|
||||
// Process base64 images in HTML
|
||||
const imageProcessingResult = extractAndProcessBase64Images(htmlComment);
|
||||
processedHTML = imageProcessingResult.processedHTML;
|
||||
extractedImages.push(...imageProcessingResult.extractedImages);
|
||||
|
||||
return {
|
||||
processedHTML,
|
||||
plainText,
|
||||
extractedImages
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error processing HTML comment:", error);
|
||||
// Return original content if processing fails
|
||||
return {
|
||||
processedHTML: htmlComment,
|
||||
plainText: extractPlainTextFromHTML(htmlComment),
|
||||
extractedImages: []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to extract plain text from HTML
|
||||
function extractPlainTextFromHTML(html) {
|
||||
try {
|
||||
// Remove HTML tags and decode HTML entities
|
||||
let plainText = html
|
||||
.replace(/<[^>]*>/g, ' ') // Replace tags with spaces
|
||||
.replace(/\s+/g, ' ') // Collapse multiple spaces
|
||||
.trim();
|
||||
|
||||
// Decode common HTML entities
|
||||
plainText = plainText
|
||||
.replace(/ /g, ' ')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/'/g, "'")
|
||||
.replace(///g, '/');
|
||||
|
||||
return plainText;
|
||||
} catch (error) {
|
||||
console.error("Error extracting plain text from HTML:", error);
|
||||
return html.replace(/<[^>]*>/g, ' ').trim();
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to extract and process base64 images from HTML
|
||||
function extractAndProcessBase64Images(html) {
|
||||
const extractedImages = [];
|
||||
let processedHTML = html;
|
||||
|
||||
try {
|
||||
// Regex to find base64 images in HTML (various formats)
|
||||
const base64Regex = /<img[^>]+src="data:image\/([a-zA-Z0-9+]+);base64,([^"]+)"[^>]*>/gi;
|
||||
|
||||
let match;
|
||||
let imageIndex = 0;
|
||||
|
||||
while ((match = base64Regex.exec(html)) !== null) {
|
||||
const fullMatch = match[0];
|
||||
const imageType = match[1]; // png, jpeg, gif, etc.
|
||||
const base64Data = match[2];
|
||||
|
||||
// Create a unique filename
|
||||
const timestamp = Date.now();
|
||||
const filename = `adjustment_image_${timestamp}_${imageIndex}.${imageType}`;
|
||||
|
||||
// Calculate approximate file size
|
||||
const size = Math.floor((base64Data.length * 3) / 4); // Base64 size calculation
|
||||
|
||||
// Store image information
|
||||
extractedImages.push({
|
||||
filename: filename,
|
||||
mimeType: `image/${imageType}`,
|
||||
base64Data: base64Data,
|
||||
size: size,
|
||||
originalHTML: fullMatch, // Store original img tag for reference
|
||||
index: imageIndex
|
||||
});
|
||||
|
||||
// Replace base64 with a reference in HTML
|
||||
const replacementHTML = `<img src="attachment://${filename}" alt="Adjustment evidence ${imageIndex + 1}" data-filename="${filename}" data-image-index="${imageIndex}" class="adjustment-image">`;
|
||||
|
||||
processedHTML = processedHTML.replace(fullMatch, replacementHTML);
|
||||
|
||||
imageIndex++;
|
||||
}
|
||||
|
||||
return {
|
||||
processedHTML,
|
||||
extractedImages
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error processing base64 images:", error);
|
||||
return {
|
||||
processedHTML: html,
|
||||
extractedImages: []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export { va_timesheet_adjustment_function };
|
||||
|
|
@ -3,24 +3,37 @@ import { VAModel, ProjectModel, LoggedModel } from "../../../models/vaModel.js";
|
|||
|
||||
async function va_timesheet_summary_function(req, res) {
|
||||
try {
|
||||
const accessToken = req.headers.token;
|
||||
const sf_token = req.headers.sf_token;
|
||||
let vaId;
|
||||
let vaUser;
|
||||
let vaEmail;
|
||||
if(sf_token) {
|
||||
vaId = req.body.vaId;
|
||||
vaUser = await VAModel.findById(vaId);
|
||||
if (!vaUser) {
|
||||
return res.status(404).json({ success: false, message: "VA not found" });
|
||||
}
|
||||
vaEmail = vaUser.userDetails.email
|
||||
} else {
|
||||
const accessToken = req.headers.token;
|
||||
|
||||
if (!accessToken) {
|
||||
return res.status(401).json({ success: false, message: "Missing token" });
|
||||
if (!accessToken) {
|
||||
return res.status(401).json({ success: false, message: "Missing token" });
|
||||
}
|
||||
|
||||
const decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN);
|
||||
if (!decoded || !decoded.email) {
|
||||
return res.status(401).json({ success: false, message: "Invalid token" });
|
||||
}
|
||||
|
||||
vaEmail = decoded.email;
|
||||
vaUser = await VAModel.findOne({ "userDetails.email": vaEmail });
|
||||
if (!vaUser) {
|
||||
return res.status(404).json({ success: false, message: "VA not found" });
|
||||
}
|
||||
|
||||
vaId = vaUser._id;
|
||||
}
|
||||
|
||||
const decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN);
|
||||
if (!decoded || !decoded.email) {
|
||||
return res.status(401).json({ success: false, message: "Invalid token" });
|
||||
}
|
||||
|
||||
const vaEmail = decoded.email;
|
||||
const vaUser = await VAModel.findOne({ "userDetails.email": vaEmail });
|
||||
if (!vaUser) {
|
||||
return res.status(404).json({ success: false, message: "VA not found" });
|
||||
}
|
||||
|
||||
const vaId = vaUser._id;
|
||||
const { projectId, date } = req.params;
|
||||
|
||||
if (!projectId || !date) {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ async function authValidator(req, res, next) {
|
|||
try {
|
||||
const sf_token = req.headers.sf_token;
|
||||
if (sf_token) {
|
||||
const { vaId } = req.params;
|
||||
if(vaId) req.body.vaId = vaId;
|
||||
next();
|
||||
} else {
|
||||
const accessToken = req.headers.token;
|
||||
|
|
|
|||
|
|
@ -46,6 +46,9 @@ const userDetailsSchema = new Schema({
|
|||
veemAccount: {
|
||||
type: String,
|
||||
},
|
||||
wiseEmail: {
|
||||
type: String,
|
||||
},
|
||||
phone: {
|
||||
type: String,
|
||||
},
|
||||
|
|
@ -404,6 +407,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"],
|
||||
|
|
@ -453,6 +464,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"],
|
||||
|
|
|
|||
|
|
@ -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,9 @@ class VaClientRepository {
|
|||
}
|
||||
}
|
||||
|
||||
async delete(id) {
|
||||
async delete(id, options = {}) {
|
||||
try {
|
||||
const response = await ClientModel.findByIdAndDelete(id);
|
||||
const response = await ClientModel.findByIdAndDelete(id, options);
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import express from "express";
|
||||
const clientRoutes = express.Router();
|
||||
|
||||
import { va_register_client } from "../../controllers/client/vaClient.controller.js";
|
||||
import { va_register_client, delete_client, update_client } 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";
|
||||
|
|
@ -21,6 +21,8 @@ clientRoutes.get("/refreshToken", client_refresh_token_callback); // Client Refr
|
|||
clientRoutes.post("/forgot-password", client_request_password_reset);
|
||||
clientRoutes.post("/verify-client-email", client_verify_email);
|
||||
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/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
|
||||
|
||||
// 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);
|
||||
|
|
@ -67,7 +67,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);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import logger from "../config/logger.js";
|
||||
import SalesforceService from "./salesforce.service.js";
|
||||
import { VAModel, ProjectModel, LoggedModel, WorkTrackModel } from "../models/vaModel.js";
|
||||
|
||||
class VaClientService {
|
||||
constructor(vaClientRepository) {
|
||||
|
|
@ -53,20 +54,143 @@ 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;
|
||||
|
|
|
|||
|
|
@ -266,17 +266,19 @@
|
|||
<option value="" disabled selected></option>
|
||||
</select>
|
||||
<label for="country" class="input-label">Country *</label>
|
||||
<!-- <div class="location-loading" id="country-loading" style="display: none;">Detecting your location...</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="input-container">
|
||||
<select id="state" class="select-field" required onchange="handleChangeSelect(this)">
|
||||
<!-- <option value=""></option> -->
|
||||
</select>
|
||||
<input type="text" id="state" class="input-field" placeholder=" " required>
|
||||
<!-- <select id="state" class="select-field" required onchange="handleChangeSelect(this)">
|
||||
<option value=""></option>
|
||||
</select> -->
|
||||
<label for="state" class="input-label">State / Region *</label>
|
||||
<div class="location-loading" id="state-loading">Loading states...</div>
|
||||
<!-- <div class="location-loading" id="state-loading">Loading states...</div> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -327,7 +329,7 @@
|
|||
</div>
|
||||
<form id="payment-form">
|
||||
<div class="form-group">
|
||||
<div class="payment-option" onclick="selectPaymentMethod('payoneer')">
|
||||
<!-- <div class="payment-option" onclick="selectPaymentMethod('payoneer')">
|
||||
<h4>Payoneer</h4>
|
||||
<p>Receive payments via Payoneer</p>
|
||||
</div>
|
||||
|
|
@ -341,7 +343,7 @@
|
|||
Account number must be 5-20 characters
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="payment-option" onclick="selectPaymentMethod('veem')">
|
||||
<h4>Veem</h4>
|
||||
|
|
@ -358,6 +360,22 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="payment-option" onclick="selectPaymentMethod('wise')">
|
||||
<h4>Wise</h4>
|
||||
<p>Receive payments via Wise</p>
|
||||
</div>
|
||||
<div id="wise-details" class="payment-details">
|
||||
<div class="form-group">
|
||||
<div class="input-container">
|
||||
<input type="email" id="wiseEmail" class="input-field" placeholder=" " oninput="validateWiseEmail(this)" required>
|
||||
<label for="wiseEmail" class="input-label">Wise Email Address *</label>
|
||||
</div>
|
||||
<div class="invalid-feedback" id="wise-email-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
|
||||
Please enter a valid email address for Wise
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
|
|
|
|||
Loading…
Reference in New Issue