Added delete route form project, va, and client - Updated leaves and time adjustment api

This commit is contained in:
Aakash Mittal 2025-11-26 19:38:08 +05:30
parent 39010ee61c
commit 16a62670e7
16 changed files with 1002 additions and 261 deletions

View File

@ -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;

View File

@ -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
}; };

View File

@ -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" });

View File

@ -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(/&nbsp;/g, ' ')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.replace(/&#x27;/g, "'")
.replace(/&#x2F;/g, '/');
return plainText;
} catch (error) {
console.error("Error extracting plain text from HTML:", error);
return html.replace(/<[^>]*>/g, ' ').trim();
}
}
// Helper function to extract and process base64 images from HTML
function extractAndProcessBase64Images(html) {
const extractedImages = [];
let processedHTML = html;
try {
// Regex to find base64 images in HTML (various formats)
const base64Regex = /<img[^>]+src="data:image\/([a-zA-Z0-9+]+);base64,([^"]+)"[^>]*>/gi;
let match;
let imageIndex = 0;
while ((match = base64Regex.exec(html)) !== null) {
const fullMatch = match[0];
const imageType = match[1]; // png, jpeg, gif, etc.
const base64Data = match[2];
// Create a unique filename
const timestamp = Date.now();
const filename = `leave_image_${timestamp}_${imageIndex}.${imageType}`;
// Calculate approximate file size
const size = Math.floor((base64Data.length * 3) / 4); // Base64 size calculation
// Store image information
extractedImages.push({
filename: filename,
mimeType: `image/${imageType}`,
base64Data: base64Data,
size: size,
originalHTML: fullMatch, // Store original img tag for reference
index: imageIndex
});
// Replace base64 with a reference in HTML
const replacementHTML = `<img src="attachment://${filename}" alt="Leave evidence ${imageIndex + 1}" data-filename="${filename}" data-image-index="${imageIndex}" class="leave-image">`;
processedHTML = processedHTML.replace(fullMatch, replacementHTML);
imageIndex++;
}
return {
processedHTML,
extractedImages
};
} catch (error) {
console.error("Error processing base64 images:", error);
return {
processedHTML: html,
extractedImages: []
};
}
}
export { va_leave_apply_function }; export { va_leave_apply_function };

View File

@ -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" });

View File

@ -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,
}, },

View File

@ -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) {

View File

@ -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(/&nbsp;/g, ' ')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.replace(/&#x27;/g, "'")
.replace(/&#x2F;/g, '/');
return plainText;
} catch (error) {
console.error("Error extracting plain text from HTML:", error);
return html.replace(/<[^>]*>/g, ' ').trim();
}
}
// Helper function to extract and process base64 images from HTML
function extractAndProcessBase64Images(html) {
const extractedImages = [];
let processedHTML = html;
try {
// Regex to find base64 images in HTML (various formats)
const base64Regex = /<img[^>]+src="data:image\/([a-zA-Z0-9+]+);base64,([^"]+)"[^>]*>/gi;
let match;
let imageIndex = 0;
while ((match = base64Regex.exec(html)) !== null) {
const fullMatch = match[0];
const imageType = match[1]; // png, jpeg, gif, etc.
const base64Data = match[2];
// Create a unique filename
const timestamp = Date.now();
const filename = `adjustment_image_${timestamp}_${imageIndex}.${imageType}`;
// Calculate approximate file size
const size = Math.floor((base64Data.length * 3) / 4); // Base64 size calculation
// Store image information
extractedImages.push({
filename: filename,
mimeType: `image/${imageType}`,
base64Data: base64Data,
size: size,
originalHTML: fullMatch, // Store original img tag for reference
index: imageIndex
});
// Replace base64 with a reference in HTML
const replacementHTML = `<img src="attachment://${filename}" alt="Adjustment evidence ${imageIndex + 1}" data-filename="${filename}" data-image-index="${imageIndex}" class="adjustment-image">`;
processedHTML = processedHTML.replace(fullMatch, replacementHTML);
imageIndex++;
}
return {
processedHTML,
extractedImages
};
} catch (error) {
console.error("Error processing base64 images:", error);
return {
processedHTML: html,
extractedImages: []
};
}
}
export { va_timesheet_adjustment_function }; export { va_timesheet_adjustment_function };

View File

@ -3,6 +3,18 @@ import { VAModel, ProjectModel, LoggedModel } from "../../../models/vaModel.js";
async function va_timesheet_summary_function(req, res) { 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) {

View File

@ -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;

View File

@ -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"],

View File

@ -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;

View File

@ -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)

View File

@ -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);

View File

@ -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;

View File

@ -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">