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