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 = `Leave evidence ${imageIndex + 1}`; + + 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 = `Adjustment evidence ${imageIndex + 1}`; + + 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 @@ +
- + + -
Loading states...
+
@@ -327,7 +329,7 @@
-
+

Veem

@@ -358,6 +360,22 @@
+ +
+

Wise

+

Receive payments via Wise

+
+
+
+
+ + +
+ +
+