From aecb4af2ac01ccedbc4ffe4e6ae65b7c3c6ea973 Mon Sep 17 00:00:00 2001 From: Aakash Date: Thu, 20 Nov 2025 20:15:16 +0530 Subject: [PATCH 1/6] Fixed issues and bugs --- public/css/va-register.css | 41 +- public/js/va-client-register.js | 1112 ++++++++++++++++- public/js/va-register.js | 327 ++++- .../client/vaClientEmailVerify.controller.js | 32 + .../va/vaAuth/vaVerifyEmail.controller.js | 32 + src/routes/v1/client.routes.js | 4 +- src/routes/v1/va.routes.js | 2 + views/va_client_register.ejs | 83 +- views/va_register.ejs | 42 +- 9 files changed, 1562 insertions(+), 113 deletions(-) create mode 100644 src/controllers/client/vaClientEmailVerify.controller.js create mode 100644 src/controllers/va/vaAuth/vaVerifyEmail.controller.js diff --git a/public/css/va-register.css b/public/css/va-register.css index 659500e..3acf759 100644 --- a/public/css/va-register.css +++ b/public/css/va-register.css @@ -475,4 +475,43 @@ body { font-size: 40px; margin-bottom: 40px; } - } \ No newline at end of file + } + + .step-error-container { + background-color: #fef2f2; + border: 1px solid #fecaca; + border-radius: 8px; + padding: 12px 16px; + margin-bottom: 20px; + animation: slideDown 0.3s ease-out; +} + +.error-message { + color: #dc2626; + font-size: 14px; + font-weight: 500; + display: flex; + align-items: center; + gap: 8px; +} + +.error-message::before { + content: "⚠"; + font-size: 16px; +} + +.field-error { + border: 1px solid #dc2626 !important; + background-color: #fef2f2; +} + +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} \ No newline at end of file diff --git a/public/js/va-client-register.js b/public/js/va-client-register.js index c44cba4..44debf4 100644 --- a/public/js/va-client-register.js +++ b/public/js/va-client-register.js @@ -1,6 +1,6 @@ // Email validation function function validateEmail(email) { - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + const emailRegex = /^[a-zA-Z0-9](?:[a-zA-Z0-9]*[._-]?[a-zA-Z0-9]+)*@[a-zA-Z0-9](?:[a-zA-Z0-9]*[-]?[a-zA-Z0-9]+)*\.[a-zA-Z]{2,}$/; return emailRegex.test(email); } @@ -35,12 +35,27 @@ function validateCardNumber(cardNumber) { return false; } + // Check for invalid patterns + if (isInvalidCardPattern(cleanNumber)) { + return false; + } + + // Check for invalid BIN ranges + if (hasInvalidBIN(cleanNumber)) { + return false; + } + // Luhn algorithm + return isValidLuhn(cleanNumber); +} + +// Enhanced Luhn algorithm +function isValidLuhn(cardNumber) { let sum = 0; let isEven = false; - for (let i = cleanNumber.length - 1; i >= 0; i--) { - let digit = parseInt(cleanNumber.charAt(i)); + for (let i = cardNumber.length - 1; i >= 0; i--) { + let digit = parseInt(cardNumber.charAt(i)); if (isEven) { digit *= 2; @@ -56,6 +71,66 @@ function validateCardNumber(cardNumber) { return sum % 10 === 0; } +// Function to check for invalid card patterns +function isInvalidCardPattern(cardNumber) { + const invalidPatterns = [ + /^0+$/, // All zeros + /^1+$/, // All ones + /^2+$/, // All twos + /^3+$/, // All threes + /^4+$/, // All fours + /^5+$/, // All fives + /^6+$/, // All sixes + /^7+$/, // All sevens + /^8+$/, // All eights + /^9+$/, // All nines + /^0123/, // Sequential + /^1234/, // Sequential + ]; + + return invalidPatterns.some(pattern => pattern.test(cardNumber)); +} + +// Function to check for invalid BIN ranges +function hasInvalidBIN(cardNumber) { + const firstSix = cardNumber.substring(0, 6); + const firstFour = cardNumber.substring(0, 4); + const firstTwo = cardNumber.substring(0, 2); + + // Invalid BIN ranges + const invalidBINs = [ + '000000', '000001', '000002', '000003', // Test cards should be handled separately + '111111', '222222', '333333', '444444', + '555555', '666666', '777777', '888888', + '999999', '123456', '654321' + ]; + + // Check specific invalid ranges + if (invalidBINs.includes(firstSix)) { + return true; + } + + // Check for known invalid starting digits + if (['0', '3', '6', '7', '8'].includes(firstTwo)) { + // 0 - Reserved + // 3 - Should only be 34, 37 (AMEX) + // 6 - Should follow Discover patterns + // 7, 8 - Not currently assigned to major networks + if (!isValidCardPrefix(cardNumber)) { + return true; + } + } + + return false; +} + +// Validate card prefix +function isValidCardPrefix(cardNumber) { + const type = detectCardType(cardNumber); + return type !== 'unknown'; +} + + // Expiry date validation function validateExpiryDate(expiryDate) { const expiryRegex = /^(0[1-9]|1[0-2])\/([0-9]{2})$/; @@ -108,11 +183,41 @@ function validateRoutingNumber(routingNumber) { return false; } - // Simple checksum validation - const digits = cleanRouting.split('').map(Number); + // Check for invalid patterns + if (isInvalidRoutingNumberPattern(cleanRouting)) { + return false; + } + + // ABA routing number checksum validation + return isValidABAChecksum(cleanRouting); +} + +function isInvalidRoutingNumberPattern(routingNumber) { + const invalidPatterns = [ + /^0+$/, // All zeros + /^1+$/, // All ones + /^2+$/, // All twos + /^3+$/, // All threes + /^4+$/, // All fours + /^5+$/, // All fives + /^6+$/, // All sixes + /^7+$/, // All sevens + /^8+$/, // All eights + /^9+$/, // All nines + /^012345678/, // Sequential + /^876543210/, // Reverse sequential + ]; + + return invalidPatterns.some(pattern => pattern.test(routingNumber)); +} + +function isValidABAChecksum(routingNumber) { + const digits = routingNumber.split('').map(Number); + + // ABA routing number checksum formula const sum = (3 * (digits[0] + digits[3] + digits[6])) + (7 * (digits[1] + digits[4] + digits[7])) + - (digits[2] + digits[5] + digits[8]); + (1 * (digits[2] + digits[5] + digits[8])); return sum % 10 === 0; } @@ -235,10 +340,14 @@ function getMaxCompletedStep() { return maxStep; } +async function handleNextStep(step) { + await nextStep(step); +} + // Function to navigate to next step -function nextStep(step) { +async function nextStep(step) { // Validate current step before proceeding - if (!validateStep(currentStep)) { + if (!(await validateStep(currentStep))) { return; } @@ -288,7 +397,7 @@ function prevStep(step) { } // Function to validate current step -function validateStep(step) { +async function validateStep(step) { let isValid = true; const form = document.getElementById(getFormId(step)); @@ -297,7 +406,7 @@ function validateStep(step) { const selectedPayment = document.querySelector('.payment-option.selected'); if (!selectedPayment) { isValid = false; - alert('Please select a payment method'); + showError('Please select a payment method', null, step); return isValid; } @@ -311,10 +420,9 @@ function validateStep(step) { const cardNumber = document.getElementById('card-number').value; if (!validateCardNumber(cardNumber)) { isValid = false; - document.getElementById('card-number').style.border = '1px solid #ef4444'; - alert('Please enter a valid card number'); + showError('Please enter a valid card number', 'card-number', step); + return isValid; } else { - document.getElementById('card-number').style.border = ''; // Show card type const cardType = detectCardType(cardNumber); if (cardType !== 'unknown') { @@ -327,10 +435,8 @@ function validateStep(step) { const expiryDate = document.getElementById('expiry-date').value; if (!validateExpiryDate(expiryDate)) { isValid = false; - document.getElementById('expiry-date').style.border = '1px solid #ef4444'; - alert('Please enter a valid expiry date (MM/YY)'); - } else { - document.getElementById('expiry-date').style.border = ''; + showError('Please enter a valid expiry date (MM/YY)', 'expiry-date', step); + return isValid; } // CVV validation @@ -338,20 +444,16 @@ function validateStep(step) { const cardType = detectCardType(cardNumber); if (!validateCVV(cvv, cardType)) { isValid = false; - document.getElementById('security-code').style.border = '1px solid #ef4444'; - alert(`Please enter a valid ${cardType === 'amex' ? '4-digit' : '3-digit'} CVV`); - } else { - document.getElementById('security-code').style.border = ''; + showError(`Please enter a valid ${cardType === 'amex' ? '4-digit' : '3-digit'} CVV`, 'security-code', step); + return isValid; } // ZIP code validation const zipCode = document.getElementById('card-zip').value; if (!validateZipCode(zipCode)) { isValid = false; - document.getElementById('card-zip').style.border = '1px solid #ef4444'; - alert('Please enter a valid ZIP code'); - } else { - document.getElementById('card-zip').style.border = ''; + showError('Please enter a valid ZIP code', 'card-zip', step); + return isValid; } } @@ -361,41 +463,54 @@ function validateStep(step) { const accountNumber = document.getElementById('account-number').value; if (!validateBankAccount(accountNumber)) { isValid = false; - document.getElementById('account-number').style.border = '1px solid #ef4444'; - alert('Please enter a valid account number (8-17 digits)'); - } else { - document.getElementById('account-number').style.border = ''; + showError('Please enter a valid account number (8-17 digits)', 'account-number', step); + return isValid; } // Routing number validation const routingNumber = document.getElementById('routing-number').value; if (!validateRoutingNumber(routingNumber)) { isValid = false; - document.getElementById('routing-number').style.border = '1px solid #ef4444'; - alert('Please enter a valid routing number'); - } else { - document.getElementById('routing-number').style.border = ''; + showError('Please enter a valid routing numbers', 'routing-number', step); + return isValid; } // Basic validation for other bank fields requiredFields.forEach(field => { if (!field.value.trim()) { isValid = false; - field.style.border = '1px solid #ef4444'; + field.classList.add('field-error'); + if (isValid === false) { + showError(`Please fill in ${field.labels[0]?.textContent || 'this field'}`, field.id, step); + } } else { - field.style.border = ''; + field.classList.remove('field-error'); } }); } } } else if (step === 1) { const email = document.getElementById('email').value; + const companyName = document.getElementById('company-name').value; + if (!validateEmail(email) && !companyName) { + isValid = false; + showError('Both Fields are Required', null, step); + return isValid; + } if (!validateEmail(email)) { isValid = false; - document.getElementById('email').style.border = '1px solid #ef4444'; - alert('Please enter a valid email address'); + showError('Please enter a valid email address', 'email', step); } else { - document.getElementById('email').style.border = ''; + const isEmailVerified = await verifyEmail(email); + if (!isEmailVerified) { + showError('Email already registered. Please use a different email address.', 'email', step); + isValid = false; + } + } + if (!companyName) { + isValid = false; + showError('Company / Client Name is Required', null, step); + return isValid; } // Validate other required fields @@ -403,19 +518,16 @@ function validateStep(step) { requiredFields.forEach(field => { if (field.id !== 'email' && !field.value.trim()) { isValid = false; - field.style.border = '1px solid #ef4444'; + field.classList.add('field-error'); } else if (field.id !== 'email') { - field.style.border = ''; + field.classList.remove('field-error'); } }); } else if (step === 3) { const email = document.getElementById('billing-email').value; if (!validateEmail(email)) { isValid = false; - document.getElementById('billing-email').style.border = '1px solid #ef4444'; - alert('Please enter a valid email address'); - } else { - document.getElementById('billing-email').style.border = ''; + showError('Please enter a valid email address', 'billing-email', step); } // Validate other required fields @@ -423,19 +535,27 @@ function validateStep(step) { requiredFields.forEach(field => { if (field.id !== 'email' && !field.value.trim()) { isValid = false; - field.style.border = '1px solid #ef4444'; + field.classList.add('field-error'); } else if (field.id !== 'email') { - field.style.border = ''; + field.classList.remove('field-error'); } }); } else if (step === 4) { const email = document.getElementById('signatory-email').value; if (!validateEmail(email)) { isValid = false; - document.getElementById('signatory-email').style.border = '1px solid #ef4444'; - alert('Please enter a valid email address'); - } else { - document.getElementById('signatory-email').style.border = ''; + showError('Please enter a valid email address', 'signatory-email', step); + } + const signatoryName = document.getElementById('signatory-name').value; + if (!signatoryName) { + isValid = false; + showError('Please enter a valid name', 'signatory-name', step); + } + + if (!signatoryName && !validateEmail(email)) { + isValid = false; + showError('Name and Email are Required', 'signatory-name', step); + return isValid; } // Validate other required fields @@ -443,9 +563,9 @@ function validateStep(step) { requiredFields.forEach(field => { if (field.id !== 'email' && !field.value.trim()) { isValid = false; - field.style.border = '1px solid #ef4444'; + field.classList.add('field-error'); } else if (field.id !== 'email') { - field.style.border = ''; + field.classList.remove('field-error'); } }); } else { @@ -454,9 +574,9 @@ function validateStep(step) { requiredFields.forEach(field => { if (!field.value.trim()) { isValid = false; - field.style.border = '1px solid #ef4444'; + field.classList.add('field-error'); } else { - field.style.border = ''; + field.classList.remove('field-error'); } }); } @@ -553,7 +673,7 @@ function selectPaymentOption(type) { // Function to submit the form async function submitForm() { // Validate current step - if (!validateStep(currentStep)) { + if (!(await validateStep(currentStep))) { return; } @@ -610,15 +730,15 @@ async function submitForm() { } else { // Handle server errors if (response.status === 409) { - alert('A user with this email already exists. Please use a different email.'); + showError('A user with this email already exists. Please use a different email.', null, 5); } else { - alert('There was an error submitting your registration. Please try again.'); + showError('There was an error submitting your registration. Please try again.', null, 5); } } } catch (error) { // Handle network errors console.error('Error:', error); - alert('There was a network error. Please check your connection and try again.'); + showError('There was a network error. Please check your connection and try again.', null, 5); } finally { // Restore button state submitBtn.textContent = originalText; @@ -682,14 +802,14 @@ async function detectLocation() { ? data.country_code : 'US'; const region = data.region || ''; - const city = data.city || ''; - const zip = data.postal || ''; + // const city = data.city || ''; + // const zip = data.postal || ''; // Populate city and state based on detected location - const cityInput = document.getElementById('city'); - const zipInput = document.getElementById('zip'); - if (cityInput) cityInput.value = city; - if (zipInput) zipInput.value = zip; + // const cityInput = document.getElementById('city'); + // const zipInput = document.getElementById('zip'); + // if (cityInput) cityInput.value = city; + // if (zipInput) zipInput.value = zip; // Populate states dropdown for the detected country await populateStates(countryCode, 'state'); @@ -863,6 +983,20 @@ document.addEventListener('DOMContentLoaded', function() { input.dispatchEvent(new Event('input')); }); + // Trigger change event for select + document.getElementById('billing-state').dispatchEvent(new Event('change')); + } else { + document.getElementById('billing-name').value = ''; + document.getElementById('billing-address').value = ''; + document.getElementById('billing-city').value = ''; + document.getElementById('billing-state').value = ''; + document.getElementById('billing-zip').value = ''; + + // Trigger input events to update labels + document.querySelectorAll('#billing-form .input-field').forEach(input => { + input.dispatchEvent(new Event('input')); + }); + // Trigger change event for select document.getElementById('billing-state').dispatchEvent(new Event('change')); } @@ -873,8 +1007,856 @@ document.addEventListener('DOMContentLoaded', function() { if (this.checked) { document.getElementById('billing-email').value = formData.userDetails.email || ''; document.getElementById('billing-email').dispatchEvent(new Event('input')); + } else { + document.getElementById('billing-email').value = ''; + document.getElementById('billing-email').dispatchEvent(new Event('input')); } }); // Detect user location when the page loads detectLocation(); -}); \ No newline at end of file +}); + +async function verifyEmail(email) { + + try { + const response = await fetch('/api/v1/client/verify-client-email', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({"email": email}) + }); + + if (response.ok) { + return false; + } else { + return true; + } + } catch (error) { + // Handle network errors + console.error('Error:', error); + showError('There was a network error. Please check your connection and try again.', null, 1); + return false; + } +} + +function showError(message, fieldId = null, step = null) { + // Determine the current step if not provided + const currentStep = step || getCurrentStep(); + const errorContainer = document.getElementById(`step-${currentStep}-error`); + const errorMessage = document.getElementById(`step-${currentStep}-error-message`); + + if (errorContainer && errorMessage) { + errorMessage.textContent = message; + errorContainer.style.display = 'block'; + + // Scroll to error container + errorContainer.scrollIntoView({ behavior: 'smooth', block: 'center' }); + + // Highlight specific field if provided + if (fieldId) { + const field = document.getElementById(fieldId); + if (field) { + field.classList.add('field-error'); + // Remove error after 5 seconds + setTimeout(() => { + field.classList.remove('field-error'); + }, 5000); + } + } + + // Auto-hide error after 8 seconds + setTimeout(() => { + hideError(currentStep); + }, 8000); + } +} + +function hideError(step = null) { + if (step) { + // Hide specific step error + const errorContainer = document.getElementById(`step-${step}-error`); + if (errorContainer) { + errorContainer.style.display = 'none'; + } + } else { + // Hide all step errors + for (let i = 1; i <= 5; i++) { + const errorContainer = document.getElementById(`step-${i}-error`); + if (errorContainer) { + errorContainer.style.display = 'none'; + } + } + } +} + +function clearFieldErrors() { + document.querySelectorAll('.field-error').forEach(field => { + field.classList.remove('field-error'); + }); +} + +function getCurrentStep() { + return currentStep; +} + +// Function to validate signatory name (no numbers allowed) +function validateSignatoryName(input) { + const errorElement = document.getElementById('signatory-name-error'); + const value = input.value; + + // Remove any numbers from the input + const cleanValue = value.replace(/[0-9]/g, ''); + + // If numbers were removed, update the input value + if (value !== cleanValue) { + input.value = cleanValue; + } + + // Show error if original value contained numbers + if (value !== cleanValue && value.length > 0) { + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else { + errorElement.style.display = 'none'; + input.classList.remove('field-error'); + return true; + } +} + +// Function to prevent number input in real-time +function preventNumbers(event) { + const charCode = event.which ? event.which : event.keyCode; + + // Allow letters, spaces, hyphens, apostrophes, and backspace + if ((charCode >= 65 && charCode <= 90) || // A-Z + (charCode >= 97 && charCode <= 122) || // a-z + charCode === 32 || // space + charCode === 45 || // hyphen + charCode === 39 || // apostrophe + charCode === 8 || // backspace + charCode === 46) { // delete + return true; + } + + // Prevent numbers and other characters + event.preventDefault(); + return false; +} + +// Function to validate company/client name +function validateCompanyName(input) { + const errorElement = document.getElementById('company-name-error'); + const value = input.value; + + // Check various invalid patterns + const validationResult = validateCompanyNameContent(value); + + if (!validationResult.isValid && value.trim().length > 0) { + errorElement.textContent = validationResult.message; + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else { + errorElement.style.display = 'none'; + input.classList.remove('field-error'); + return true; + } +} + +function validateCompanyNameContent(companyName) { + const trimmed = companyName.trim(); + + if (trimmed.length === 0) { + return { isValid: false, message: 'Company name is required' }; + } + + // Check minimum length + if (trimmed.length < 2) { + return { isValid: false, message: 'Company name must be at least 2 characters' }; + } + + // Remove whitespace for content analysis + const content = trimmed.replace(/\s/g, ''); + + // Check if contains only special characters + if (/^[^a-zA-Z0-9]+$/.test(content)) { + return { isValid: false, message: 'Company name cannot be only special characters' }; + } + + // Check if contains at least some valid content (letters or numbers) + if (!/[a-zA-Z0-9]/.test(content)) { + return { isValid: false, message: 'Company name must contain letters or numbers' }; + } + + // Optional: Check for excessive special characters (more than 50%) + const specialCharCount = (content.match(/[^a-zA-Z0-9]/g) || []).length; + if (specialCharCount > content.length / 2) { + return { isValid: false, message: 'Company name contains too many special characters' }; + } + + return { isValid: true, message: '' }; +} + +// Function to prevent invalid character input in real-time +function preventInvalidCompanyChars(event) { + const charCode = event.which ? event.which : event.keyCode; + const key = event.key; + + // Allow: letters, numbers, spaces, hyphens, apostrophes, ampersands, periods, commas + // Also allow common editing keys + if ((charCode >= 65 && charCode <= 90) || // A-Z + (charCode >= 97 && charCode <= 122) || // a-z + (charCode >= 48 && charCode <= 57) || // 0-9 + charCode === 32 || // space + charCode === 45 || // hyphen + charCode === 39 || // apostrophe + charCode === 38 || // ampersand (&) + charCode === 46 || // period (.) + charCode === 44 || // comma (,) + charCode === 8 || // backspace + charCode === 9 || // tab + charCode === 37 || // left arrow + charCode === 39 || // right arrow + charCode === 16 || // shift + charCode === 17 || // ctrl + charCode === 18 || // alt + charCode === 91 || // cmd (Mac) + /^[a-zA-Z0-9\s\-'&.,]$/.test(key)) { // allowed characters + return true; + } + + // Prevent invalid characters + event.preventDefault(); + return false; +} + +// Function to validate ZIP code in real-time +function validateZipCode(input) { + let errorElement; + + // Determine which error element to use based on input ID + if (input.id === 'billing-zip') { + errorElement = document.getElementById('billing-zip-error'); + } else if (input.id === 'card-zip') { + errorElement = document.getElementById('card-zip-error'); + } else { + errorElement = document.getElementById('zip-error'); + } + + const value = input.value; + + // Remove any non-numeric characters except hyphens + const cleanValue = value.replace(/[^0-9\-]/g, ''); + + // If invalid characters were removed, update the input value + if (value !== cleanValue) { + input.value = cleanValue; + } + + // Format ZIP code as user types (add hyphen after 5 digits) + if (cleanValue.length === 5 && !cleanValue.includes('-')) { + input.value = cleanValue; + } else if (cleanValue.length > 5 && !cleanValue.includes('-')) { + input.value = cleanValue.substring(0, 5) + '' + cleanValue.substring(5, 9); + } + + + if (input.value.length < 4) { + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else { + errorElement.style.display = 'none'; + input.classList.remove('field-error'); + return true; + } +} + +// Function to prevent non-numeric input in ZIP fields +function preventNonNumericZip(event) { + const charCode = event.which ? event.which : event.keyCode; + const key = event.key; + const currentValue = event.target.value; + + // Allow: numbers, hyphens (only in specific positions), and editing keys + if ((charCode >= 48 && charCode <= 57) || // 0-9 + (charCode === 45 && currentValue.length === 5 && !currentValue.includes('-')) || // hyphen only after 5 digits + charCode === 8 || // backspace + charCode === 9 || // tab + charCode === 37 || // left arrow + charCode === 39 || // right arrow + charCode === 46 || // delete + charCode === 16 || // shift + charCode === 17 || // ctrl + charCode === 18 || // alt + charCode === 91) { // cmd (Mac) + return true; + } + + // Prevent all other characters including letters and special characters + event.preventDefault(); + return false; +} + +// Function to validate city name in real-time +function validateCity(input) { + let errorElement; + + // Determine which error element to use based on input ID + if (input.id === 'billing-city') { + errorElement = document.getElementById('billing-city-error'); + } else { + errorElement = document.getElementById('city-error'); + } + + const value = input.value; + + // Remove invalid special characters (keep only letters, spaces, hyphens, apostrophes) + const cleanValue = value.replace(/[^a-zA-Z\s\-']/g, ''); + + // If invalid characters were removed, update the input value + if (value !== cleanValue) { + input.value = cleanValue; + } + + // Check if the value contains only numbers + const numbersOnlyCheck = value.replace(/[\s\-']/g, ''); + const hasOnlyNumbers = /^[0-9]+$/.test(numbersOnlyCheck) && numbersOnlyCheck.length > 0; + + // Check if the value contains only special characters + const specialCharsOnlyCheck = value.replace(/[\s]/g, ''); + const hasOnlySpecialChars = /^[^a-zA-Z0-9]+$/.test(specialCharsOnlyCheck) && specialCharsOnlyCheck.length > 0; + + // Check if we have at least one letter + const hasLetters = /[a-zA-Z]/.test(value); + + // Show appropriate error messages + if (value !== cleanValue && value.length > 0) { + errorElement.textContent = 'City name can only contain letters, spaces, hyphens, and apostrophes'; + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else if (hasOnlyNumbers) { + errorElement.textContent = 'City name cannot be only numbers. Please include letters.'; + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else if (hasOnlySpecialChars) { + errorElement.textContent = 'City name cannot be only special characters. Please include letters.'; + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else if (!hasLetters && value.trim().length > 0) { + errorElement.textContent = 'City name must include at least one letter'; + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else { + errorElement.style.display = 'none'; + input.classList.remove('field-error'); + return true; + } +} + +// Function to prevent invalid character input in city fields +function preventInvalidCityChars(event) { + const charCode = event.which ? event.which : event.keyCode; + const key = event.key; + + // Allow: letters, spaces, hyphens, apostrophes, and editing keys + if ((charCode >= 65 && charCode <= 90) || // A-Z + (charCode >= 97 && charCode <= 122) || // a-z + charCode === 32 || // space + charCode === 45 || // hyphen + charCode === 39 || // apostrophe + charCode === 8 || // backspace + charCode === 9 || // tab + charCode === 37 || // left arrow + charCode === 39 || // right arrow + charCode === 46 || // delete + charCode === 16 || // shift + charCode === 17 || // ctrl + charCode === 18 || // alt + charCode === 91 || // cmd (Mac) + /^[a-zA-Z\s\-']$/.test(key)) { // allowed characters + return true; + } + + // Prevent numbers and other special characters + event.preventDefault(); + return false; +} + +function validatePosition(input) { + const errorElement = document.getElementById('position-error'); + const value = input.value; + + // Remove numbers and invalid special characters + // Allow: letters, spaces, hyphens, apostrophes, periods, commas, ampersands, slashes + const cleanValue = value.replace(/[^a-zA-Z\s\-'.,&\/]/g, ''); + + // If invalid characters were removed, update the input value + if (value !== cleanValue) { + input.value = cleanValue; + } + + // Check if the value contains only numbers + const numbersOnlyCheck = value.replace(/[\s\-'.,&\/]/g, ''); + const hasOnlyNumbers = /^[0-9]+$/.test(numbersOnlyCheck) && numbersOnlyCheck.length > 0; + + // Check if the value contains only special characters + const specialCharsOnlyCheck = value.replace(/[\s]/g, ''); + const hasOnlySpecialChars = /^[^a-zA-Z0-9]+$/.test(specialCharsOnlyCheck) && specialCharsOnlyCheck.length > 0; + + // Check if we have at least one letter + const hasLetters = /[a-zA-Z]/.test(value); + + // Show appropriate error messages + if (value !== cleanValue && value.length > 0) { + errorElement.textContent = 'Position can only contain letters, spaces, hyphens, apostrophes, periods, commas, ampersands, and slashes'; + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else if (hasOnlyNumbers) { + errorElement.textContent = 'Position cannot be only numbers. Please include letters.'; + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else if (hasOnlySpecialChars) { + errorElement.textContent = 'Position cannot be only special characters. Please include letters.'; + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else if (!hasLetters && value.trim().length > 0) { + errorElement.textContent = 'Position must include at least one letter'; + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else { + errorElement.style.display = 'none'; + input.classList.remove('field-error'); + return true; + } +} + +// Function to prevent invalid character input in position field +function preventInvalidPositionChars(event) { + const charCode = event.which ? event.which : event.keyCode; + const key = event.key; + + // Allow: letters, spaces, hyphens, apostrophes, periods, commas, ampersands, slashes + // Also allow common editing keys + if ((charCode >= 65 && charCode <= 90) || // A-Z + (charCode >= 97 && charCode <= 122) || // a-z + charCode === 32 || // space + charCode === 45 || // hyphen + charCode === 39 || // apostrophe + charCode === 46 || // period + charCode === 44 || // comma + charCode === 38 || // ampersand (&) + charCode === 47 || // slash (/) + charCode === 8 || // backspace + charCode === 9 || // tab + charCode === 37 || // left arrow + charCode === 39 || // right arrow + charCode === 46 || // delete + charCode === 16 || // shift + charCode === 17 || // ctrl + charCode === 18 || // alt + charCode === 91 || // cmd (Mac) + /^[a-zA-Z\s\-'.,&\/]$/.test(key)) { // allowed characters + return true; + } + + // Prevent numbers and other special characters + event.preventDefault(); + return false; +} + +// Function to validate CVV in real-time +function validateCVVVV(input) { + const errorElement = document.getElementById('security-code-error'); + const value = input.value; + + // Remove any non-digit characters + const cleanValue = value.replace(/\D/g, ''); + + // If non-digits were removed, update the input value + if (value !== cleanValue) { + input.value = cleanValue; + } + + // Get card type to determine expected CVV length + const cardNumber = document.getElementById('card-number').value; + const cardType = detectCardType(cardNumber); + + // Validate CVV format and content + const isValid = validateCVVStrict(cleanValue, cardType); + + if (!isValid && cleanValue.length > 0) { + errorElement.textContent = getCVVErrorMessage(cleanValue, cardType); + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else { + errorElement.style.display = 'none'; + input.classList.remove('field-error'); + return true; + } +} + +// Function to prevent invalid character input in CVV field +function preventInvalidCVVChars(event) { + const charCode = event.which ? event.which : event.keyCode; + const key = event.key; + + // Allow only digits and common editing keys + if ((charCode >= 48 && charCode <= 57) || // 0-9 + charCode === 8 || // backspace + charCode === 9 || // tab + charCode === 37 || // left arrow + charCode === 39 || // right arrow + charCode === 46 || // delete + charCode === 16 || // shift + charCode === 17 || // ctrl + charCode === 18 || // alt + charCode === 91) { // cmd (Mac) + return true; + } + + // Prevent all non-digit characters + event.preventDefault(); + return false; +} + +// Enhanced CVV validation function +function validateCVVStrict(cvv, cardType) { + if (!cvv || !/^\d+$/.test(cvv)) { + return false; + } + + // Check for invalid patterns + if (isInvalidCVVPattern(cvv)) { + return false; + } + + const cvvLengths = { + visa: 3, + mastercard: 3, + amex: 4, + discover: 3, + diners: 3, + jcb: 3 + }; + + const expectedLength = cvvLengths[cardType] || 3; + + // Check length + if (cvv.length !== expectedLength) { + return false; + } + + return true; +} + +// Function to check for invalid CVV patterns +function isInvalidCVVPattern(cvv) { + const invalidPatterns = [ + /^0+$/, // All zeros: 000, 0000 + /^1+$/, // All ones: 111, 1111 + /^2+$/, // All twos: 222, 2222 + /^3+$/, // All threes: 333, 3333 + /^4+$/, // All fours: 444, 4444 + /^5+$/, // All fives: 555, 5555 + /^6+$/, // All sixes: 666, 6666 + /^7+$/, // All sevens: 777, 7777 + /^8+$/, // All eights: 888, 8888 + /^9+$/, // All nines: 999, 9999 + ]; + + return invalidPatterns.some(pattern => pattern.test(cvv)); +} + +// Function to get appropriate CVV error message +function getCVVErrorMessage(cvv, cardType) { + if (!cvv) { + return 'Security code is required'; + } + + if (!/^\d+$/.test(cvv)) { + return 'Security code must contain only numbers'; + } + + if (isInvalidCVVPattern(cvv)) { + return 'Please enter a valid security code'; + } + + const cvvLengths = { + visa: 3, + mastercard: 3, + amex: 4, + discover: 3, + diners: 3, + jcb: 3 + }; + + const expectedLength = cvvLengths[cardType] || 3; + + if (cvv.length !== expectedLength) { + if (cardType === 'amex') { + return 'American Express cards require a 4-digit security code'; + } else { + return `Security code must be ${expectedLength} digits`; + } + } + + return 'Please enter a valid security code'; +} + +// Bank Name validation +function validateBankName(input) { + const errorElement = document.getElementById('bank-name-error'); + const value = input.value; + + // Remove numbers and invalid special characters + const cleanValue = value.replace(/[^a-zA-Z\s\-'&.,()]/g, ''); + + if (value !== cleanValue) { + input.value = cleanValue; + } + + // Check content + const contentCheck = value.replace(/[\s\-'&.,()]/g, ''); + const hasOnlyNumbers = /^[0-9]+$/.test(contentCheck) && contentCheck.length > 0; + const hasOnlySpecialChars = /^[^a-zA-Z0-9]+$/.test(contentCheck) && contentCheck.length > 0; + const hasLetters = /[a-zA-Z]/.test(value); + + if (value !== cleanValue && value.length > 0) { + errorElement.textContent = 'Bank name can only contain letters, spaces, and common punctuation'; + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else if (hasOnlyNumbers) { + errorElement.textContent = 'Bank name cannot be only numbers'; + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else if (hasOnlySpecialChars) { + errorElement.textContent = 'Bank name cannot be only special characters'; + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else if (!hasLetters && value.trim().length > 0) { + errorElement.textContent = 'Bank name must include letters'; + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else { + errorElement.style.display = 'none'; + input.classList.remove('field-error'); + return true; + } +} + +function preventInvalidBankNameChars(event) { + const charCode = event.which ? event.which : event.keyCode; + const key = event.key; + + if ((charCode >= 65 && charCode <= 90) || + (charCode >= 97 && charCode <= 122) || + charCode === 32 || charCode === 45 || charCode === 39 || + charCode === 38 || charCode === 46 || charCode === 44 || + charCode === 40 || charCode === 41 || + charCode === 8 || charCode === 9 || charCode === 37 || + charCode === 39 || charCode === 46 || charCode === 16 || + charCode === 17 || charCode === 18 || charCode === 91 || + /^[a-zA-Z\s\-'&.,()]$/.test(key)) { + return true; + } + + event.preventDefault(); + return false; +} + +// Enhanced Account Number validation +function validateAccountNumber(input) { + const errorElement = document.getElementById('account-number-error'); + const value = input.value.replace(/\s/g, ''); + + // Remove any non-digit characters + const cleanValue = value.replace(/\D/g, ''); + + if (value !== cleanValue) { + input.value = cleanValue; + } + + // Validate account number + const isValid = validateAccountNumberStrict(cleanValue); + + if (!isValid && cleanValue.length > 0) { + errorElement.textContent = getAccountNumberErrorMessage(cleanValue); + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else { + errorElement.style.display = 'none'; + input.classList.remove('field-error'); + return true; + } +} + +function preventInvalidAccountNumberChars(event) { + const charCode = event.which ? event.which : event.keyCode; + + if ((charCode >= 48 && charCode <= 57) || + charCode === 8 || charCode === 9 || charCode === 37 || + charCode === 39 || charCode === 46 || charCode === 16 || + charCode === 17 || charCode === 18 || charCode === 91) { + return true; + } + + event.preventDefault(); + return false; +} + +function validateAccountNumberStrict(accountNumber) { + if (!accountNumber || !/^\d+$/.test(accountNumber)) { + return false; + } + + // Check length + if (accountNumber.length < 8 || accountNumber.length > 17) { + return false; + } + + // Check for invalid patterns + if (isInvalidAccountNumberPattern(accountNumber)) { + return false; + } + + // Simple checksum validation (basic example) + if (!isValidAccountNumberChecksum(accountNumber)) { + return false; + } + + return true; +} + +function isInvalidAccountNumberPattern(accountNumber) { + const invalidPatterns = [ + /^0+$/, // All zeros + /^1+$/, // All ones + /^2+$/, // All twos + /^3+$/, // All threes + /^4+$/, // All fours + /^5+$/, // All fives + /^6+$/, // All sixes + /^7+$/, // All sevens + /^8+$/, // All eights + /^9+$/, // All nines + /^12345678/, // Sequential + /^87654321/, // Reverse sequential + ]; + + return invalidPatterns.some(pattern => pattern.test(accountNumber)); +} + +function isValidAccountNumberChecksum(accountNumber) { + // Basic checksum validation - you might want to implement + // a more specific algorithm based on bank requirements + let sum = 0; + for (let i = 0; i < accountNumber.length; i++) { + sum += parseInt(accountNumber.charAt(i)); + } + + // Simple check: sum shouldn't be too low (e.g., all zeros) + return sum > 0; +} + +function getAccountNumberErrorMessage(accountNumber) { + if (!accountNumber) { + return 'Account number is required'; + } + + if (!/^\d+$/.test(accountNumber)) { + return 'Account number must contain only numbers'; + } + + if (accountNumber.length < 8) { + return 'Account number must be at least 8 digits'; + } + + if (accountNumber.length > 17) { + return 'Account number cannot exceed 17 digits'; + } + + if (isInvalidAccountNumberPattern(accountNumber)) { + return 'Please enter a valid account number'; + } + + return 'Please enter a valid account number'; +} + +// Account Holder Name validation +function validateAccountHolderName(input) { + const errorElement = document.getElementById('account-holder-name-error'); + const value = input.value; + + // Remove numbers and special characters (keep only letters and spaces) + const cleanValue = value.replace(/[^a-zA-Z\s]/g, ''); + + if (value !== cleanValue) { + input.value = cleanValue; + } + + // Check content + const hasOnlyNumbers = /^[0-9]+$/.test(value.replace(/\s/g, '')); + const hasOnlySpecialChars = /^[^a-zA-Z0-9]+$/.test(value.replace(/\s/g, '')); + const hasLetters = /[a-zA-Z]/.test(value); + + if (value !== cleanValue && value.length > 0) { + errorElement.textContent = 'Account holder name can only contain letters and spaces'; + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else if (hasOnlyNumbers) { + errorElement.textContent = 'Account holder name cannot contain only numbers'; + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else if (hasOnlySpecialChars) { + errorElement.textContent = 'Account holder name cannot contain only special characters'; + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else if (!hasLetters && value.trim().length > 0) { + errorElement.textContent = 'Account holder name must include letters'; + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else { + errorElement.style.display = 'none'; + input.classList.remove('field-error'); + return true; + } +} + +function preventInvalidAccountHolderChars(event) { + const charCode = event.which ? event.which : event.keyCode; + const key = event.key; + + if ((charCode >= 65 && charCode <= 90) || + (charCode >= 97 && charCode <= 122) || + charCode === 32 || + charCode === 8 || charCode === 9 || charCode === 37 || + charCode === 39 || charCode === 46 || charCode === 16 || + charCode === 17 || charCode === 18 || charCode === 91 || + /^[a-zA-Z\s]$/.test(key)) { + return true; + } + + event.preventDefault(); + return false; +} \ No newline at end of file diff --git a/public/js/va-register.js b/public/js/va-register.js index 2c65a26..8b2731c 100644 --- a/public/js/va-register.js +++ b/public/js/va-register.js @@ -109,7 +109,22 @@ // Add input event listeners to handle label animation - document.querySelectorAll('.input-field').forEach(input => { + document.querySelectorAll('.input-field:not(.dob)').forEach(input => { + setTimeout(() => { + if (input.value) { + input.nextElementSibling.style.top = '4px'; + input.nextElementSibling.style.fontSize = '12px'; + input.nextElementSibling.style.transform = 'none'; + } + }, 100); + + input.addEventListener('animationstart', function() { + if (this.value) { + this.nextElementSibling.style.top = '4px'; + this.nextElementSibling.style.fontSize = '12px'; + this.nextElementSibling.style.transform = 'none'; + } + }); input.addEventListener('input', function() { if (this.value) { this.nextElementSibling.style.top = '4px'; @@ -139,6 +154,13 @@ // Handle select field label animation document.querySelectorAll('.select-field').forEach(select => { + setTimeout(() => { + if (select.value) { + select.nextElementSibling.style.top = '4px'; + select.nextElementSibling.style.fontSize = '12px'; + select.nextElementSibling.style.transform = 'none'; + } + }, 100); select.addEventListener('change', function() { if (this.value) { this.nextElementSibling.style.top = '4px'; @@ -228,10 +250,14 @@ return maxStep; } + async function handleNextStep(step) { + await nextStep(step); + } + // Function to navigate to next step - function nextStep(step) { + async function nextStep(step) { // Validate current step before proceeding - if (!validateStep(currentStep)) { + if (!(await validateStep(currentStep))) { return; } @@ -306,9 +332,13 @@ } // Function to validate current step - function validateStep(step) { + async function validateStep(step) { let isValid = true; const form = document.getElementById(getFormId(step)); + + // Hide any previous errors for this step + hideError(step); + clearFieldErrors(); if (form) { // For account setup step @@ -319,21 +349,31 @@ // Email validation if (!validateEmail(email)) { - alert('Please enter a valid email address'); + showError('Please enter a valid email address', 'email', step); isValid = false; + } else { + // Email verification - wait for the result + const isEmailVerified = await verifyEmail(email); + if (!isEmailVerified) { + showError('Email already registered. Please use a different email address.', 'email', step); + isValid = false; + } } - + // Password validation if (password.length < 8) { - alert('Password must be at least 8 characters long'); + showError('Password must be at least 8 characters long', 'password', step); isValid = false; } if (password !== confirmPassword) { document.getElementById('password-match-error').style.display = 'block'; + document.getElementById('confirmPassword').classList.add('field-error'); + showError('Passwords do not match', 'confirmPassword', step); isValid = false; } else { document.getElementById('password-match-error').style.display = 'none'; + document.getElementById('confirmPassword').classList.remove('field-error'); } } @@ -342,15 +382,18 @@ const firstName = document.getElementById('firstName').value; if (!firstName) { document.getElementById('profile-incomplete').style.display = 'block'; + document.getElementById('firstName').classList.add('field-error'); + showError('First name is required', 'firstName', step); isValid = false; } else { document.getElementById('profile-incomplete').style.display = 'none'; + document.getElementById('firstName').classList.remove('field-error'); } // Age validation const age = document.getElementById('age').value; if (age && (age < 18 || age > 100)) { - alert('You must be 18 years or older to register'); + showError('You must be 18 years or older to register', 'dob', step); isValid = false; } @@ -359,12 +402,12 @@ const state = document.getElementById('state').value; if (!country) { - alert('Please select your country'); + showError('Please select your country', 'country', step); isValid = false; } if (!state) { - alert('Please select your state/region'); + showError('Please select your state/region', 'state', step); isValid = false; } @@ -373,9 +416,40 @@ requiredFields.forEach(field => { if (!field.value.trim()) { isValid = false; - field.style.border = '1px solid #ef4444'; + field.classList.add('field-error'); + if (isValid === false) { + showError(`Please fill in ${field.labels[0]?.textContent || 'this field'}`, field.id, step); + } } else { - field.style.border = ''; + field.classList.remove('field-error'); + } + }); + } + + if (step === 3) { + // Phone number validation + const phone = document.getElementById('phone').value; + if (!phone) { + showError('Please enter your phone number', 'phone', step); + isValid = false; + } else if (!validatePhoneNumberStrict(phone)) { + document.getElementById('phone-error').style.display = 'block'; + document.getElementById('phone').classList.add('field-error'); + showError('Please enter a valid phone number (10-15 digits)', 'phone', step); + isValid = false; + } else { + document.getElementById('phone-error').style.display = 'none'; + document.getElementById('phone').classList.remove('field-error'); + } + + // Validate other required fields in step 3 + const requiredFields = form.querySelectorAll('[required]'); + requiredFields.forEach(field => { + if (!field.value.trim()) { + isValid = false; + field.classList.add('field-error'); + } else { + field.classList.remove('field-error'); } }); } @@ -384,20 +458,20 @@ if (step === 4) { const selectedPayment = document.querySelector('.payment-option.selected'); if (!selectedPayment) { - alert('Please select a payment method'); + 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) { - alert('Please enter your Payoneer account number'); + showError('Please enter your Payoneer account number', 'payoneerAccount', step); isValid = false; } } else if (paymentMethod === 'veem') { const veemAccount = document.getElementById('veemAccount').value; if (!veemAccount) { - alert('Please enter your Veem account number'); + showError('Please enter your Veem account number', 'veemAccount', step); isValid = false; } } @@ -410,9 +484,9 @@ requiredFields.forEach(field => { if (!field.value.trim()) { isValid = false; - field.style.border = '1px solid #ef4444'; + field.classList.add('field-error'); } else { - field.style.border = ''; + field.classList.remove('field-error'); } }); } @@ -598,12 +672,12 @@ }, 1000); } else { // Handle server errors - alert('There was an error submitting your registration. Please try again.'); + showError('There was an error submitting your registration. Please try again.', null, 4); } } catch (error) { // Handle network errors console.error('Error:', error); - alert('There was a network error. Please check your connection and try again.'); + showError('There was a network error. Please check your connection and try again.', null, 4); } finally { // Restore button state submitBtn.textContent = originalText; @@ -611,3 +685,218 @@ } } + async function verifyEmail(email) { + + try { + const response = await fetch('/api/v1/va/verify-va-email', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({"email": email}) + }); + + if (response.ok) { + return false; + } else { + return true; + } + } catch (error) { + // Handle network errors + console.error('Error:', error); + showError('There was a network error. Please check your connection and try again.', null, 1); + return false; + } + } + + // Function to calculate age from date of birth + function calculateAge(dob) { + if (!dob) return ''; + + const birthDate = new Date(dob); + const today = new Date(); + + let age = today.getFullYear() - birthDate.getFullYear(); + const monthDiff = today.getMonth() - birthDate.getMonth(); + + // Adjust age if birthday hasn't occurred this year yet + if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) { + age--; + } + + return age; + } + + // Function to handle date of birth change and update age + function handleDOBChange() { + const dob = document.getElementById('dob').value; + const ageInput = document.getElementById('age'); + + if (dob) { + const age = calculateAge(dob); + ageInput.value = age; + + // Trigger input event to update the label position + ageInput.dispatchEvent(new Event('input')); + + // Validate age + if (age < 18) { + alert('You must be 18 years or older to register'); + ageInput.style.border = '1px solid #ef4444'; + } else if (age > 100) { + alert('Please enter a valid age'); + ageInput.style.border = '1px solid #ef4444'; + } else { + ageInput.style.border = ''; + } + } else { + // Clear age if DOB is cleared + ageInput.value = ''; + ageInput.dispatchEvent(new Event('input')); + } + } + + // Function to handle age manual input and update DOB (optional) + function handleAgeChange() { + const age = document.getElementById('age').value; + const dobInput = document.getElementById('dob'); + + if (age && age >= 18 && age <= 100) { + // Calculate approximate DOB based on age + const today = new Date(); + const birthYear = today.getFullYear() - age; + const approximateDOB = `${birthYear}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`; + + // You can set this as the DOB or just leave it for reference + // dobInput.value = approximateDOB; + // dobInput.dispatchEvent(new Event('change')); + } + } + + // Function to validate phone number in real-time + function validatePhoneNumber(input) { + const phoneError = document.getElementById('phone-error'); + const value = input.value; + + // Remove any non-digit characters + const digitsOnly = value.replace(/\D/g, ''); + + // Update the input value with digits only + if (value !== digitsOnly) { + input.value = digitsOnly; + } + + // Validate length + if (digitsOnly.length > 0 && (digitsOnly.length < 10 || digitsOnly.length > 15)) { + phoneError.style.display = 'block'; + input.style.border = '1px solid #ef4444'; + return false; + } else { + phoneError.style.display = 'none'; + input.style.border = ''; + return true; + } + } + + // Function to validate phone number on form submission + function validatePhoneNumberStrict(phone) { + const phoneRegex = /^[0-9]{10,15}$/; + return phoneRegex.test(phone); + } + + // Function to validate Payoneer account number + function validatePayoneerAccount(input) { + const payoneerError = document.getElementById('payoneer-error'); + const value = input.value.trim(); + + // Payoneer typically uses email format or alphanumeric IDs + // Adjust validation rules based on Payoneer's actual format requirements + if (value.length > 0 && (value.length < 5 || value.length > 20)) { + payoneerError.textContent = 'Payoneer account must be 5-20 characters'; + payoneerError.style.display = 'block'; + input.style.border = '1px solid #ef4445'; + return false; + } else { + payoneerError.style.display = 'none'; + input.style.border = ''; + return true; + } + } + + // Function to validate Veem account number + function validateVeemAccount(input) { + const veemError = document.getElementById('veem-error'); + const value = input.value.trim(); + + // Veem account numbers are typically longer alphanumeric strings + if (value.length > 0 && (value.length < 8 || value.length > 25)) { + veemError.textContent = 'Veem account must be 8-25 characters'; + veemError.style.display = 'block'; + input.style.border = '1px solid #ef4445'; + return false; + } else { + veemError.style.display = 'none'; + input.style.border = ''; + return true; + } + } + + function showError(message, fieldId = null, step = null) { + // Determine the current step if not provided + const currentStep = step || getCurrentStep(); + const errorContainer = document.getElementById(`step-${currentStep}-error`); + const errorMessage = document.getElementById(`step-${currentStep}-error-message`); + + if (errorContainer && errorMessage) { + errorMessage.textContent = message; + errorContainer.style.display = 'block'; + + // Scroll to error container + errorContainer.scrollIntoView({ behavior: 'smooth', block: 'center' }); + + // Highlight specific field if provided + if (fieldId) { + const field = document.getElementById(fieldId); + if (field) { + field.classList.add('field-error'); + // Remove error after 5 seconds + setTimeout(() => { + field.classList.remove('field-error'); + }, 5000); + } + } + + // Auto-hide error after 8 seconds + setTimeout(() => { + hideError(currentStep); + }, 8000); + } + } + + function hideError(step = null) { + if (step) { + // Hide specific step error + const errorContainer = document.getElementById(`step-${step}-error`); + if (errorContainer) { + errorContainer.style.display = 'none'; + } + } else { + // Hide all step errors + for (let i = 1; i <= 5; i++) { + const errorContainer = document.getElementById(`step-${i}-error`); + if (errorContainer) { + errorContainer.style.display = 'none'; + } + } + } + } + + function clearFieldErrors() { + document.querySelectorAll('.field-error').forEach(field => { + field.classList.remove('field-error'); + }); + } + + function getCurrentStep() { + return currentStep; + } \ No newline at end of file diff --git a/src/controllers/client/vaClientEmailVerify.controller.js b/src/controllers/client/vaClientEmailVerify.controller.js new file mode 100644 index 0000000..5813e41 --- /dev/null +++ b/src/controllers/client/vaClientEmailVerify.controller.js @@ -0,0 +1,32 @@ +import { ClientModel } from "../../models/vaModel.js"; + +// Verify VA Email +async function client_verify_email(req, res) { + try { + // const payload = req.body ? JSON.parse(req.body) : {}; + const payload = req.body || {}; + const { email } = payload; + + if (!email) { + return res.status(400).json({ success: false, message: "Please provide email" }); + } + + // Find user by email + const user = await ClientModel.findOne({ "userDetails.email": email }); + if (!user) { + return res.status(404).json({ success: false, message: "Email not found" }); + } + + // Return accessToken and user info + return res.status(200).json({ + success: true, + email: user.userDetails?.email, + message: "Email ID is already registerd" + }); + } catch (error) { + console.error("va_register error:", error); + return res.status(500).json({ success: false, message: "Internal Server Error" }); + } +} + +export { client_verify_email }; \ No newline at end of file diff --git a/src/controllers/va/vaAuth/vaVerifyEmail.controller.js b/src/controllers/va/vaAuth/vaVerifyEmail.controller.js new file mode 100644 index 0000000..4aa278e --- /dev/null +++ b/src/controllers/va/vaAuth/vaVerifyEmail.controller.js @@ -0,0 +1,32 @@ +import { VAModel } from "../../../models/vaModel.js"; + +// Verify VA Email +async function va_verify_email(req, res) { + try { + // const payload = req.body ? JSON.parse(req.body) : {}; + const payload = req.body || {}; + const { email } = payload; + + if (!email) { + return res.status(400).json({ success: false, message: "Please provide email" }); + } + + // Find user by email + const user = await VAModel.findOne({ "userDetails.email": email }); + if (!user) { + return res.status(404).json({ success: false, message: "Invalid Email ID" }); + } + + // Return accessToken and user info + return res.status(200).json({ + success: true, + email: user.userDetails?.email, + message: "Email verified successfully" + }); + } catch (error) { + console.error("va_register error:", error); + return res.status(500).json({ success: false, message: "Internal Server Error" }); + } +} + +export { va_verify_email }; \ No newline at end of file diff --git a/src/routes/v1/client.routes.js b/src/routes/v1/client.routes.js index 941632f..79bc669 100644 --- a/src/routes/v1/client.routes.js +++ b/src/routes/v1/client.routes.js @@ -9,13 +9,15 @@ import { client_refresh_token_callback } from "../../controllers/client/clientRe import { client_request_password_reset } from "../../controllers/passwordReset.controllers.js"; import { clientAuthValidator } from "../../middlewares/auth.middleware.js"; import { client_dashboard } from "./../../controllers/client/dashboard/clientDashboard.controller.js"; -import { client_dashboard_va_log } from "../../controllers/client/dashboard/clientDashboardVaLog.controller.js" +import { client_dashboard_va_log } from "../../controllers/client/dashboard/clientDashboardVaLog.controller.js"; +import { client_verify_email } from "../../controllers/client/vaClientEmailVerify.controller.js"; // clients routes clientRoutes.post("/register", va_register_client); // Register a new VA clientRoutes.post("/login", va_login_client); // Login Client clientRoutes.get("/refreshToken", client_refresh_token_callback); // Client Refresh Token clientRoutes.post("/forgot-password", client_request_password_reset); +clientRoutes.post("/verify-client-email", client_verify_email); // 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 7beaf43..0686e9d 100644 --- a/src/routes/v1/va.routes.js +++ b/src/routes/v1/va.routes.js @@ -22,6 +22,7 @@ import { va_timesheet_summary_function } from "../../controllers/va/vaTimesheetA import { va_timesheet_adjustment_function } from "../../controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustment.controller.js"; import { get_adjustment_requests_list } from "../../controllers/va/vaTimesheetAdjustment/vaAllTimesheetAdjustmentReuqest.controller.js" import { va_timesheet_manage_adjustment_request } from "../../controllers/va/vaTimesheetAdjustment/vaManageTimesheetAdjustment.controller.js" +import { va_verify_email } from "../../controllers/va/vaAuth/vaVerifyEmail.controller.js"; vaRoutes.get("/", va_callback); // VA Callback for getting active VA @@ -30,6 +31,7 @@ vaRoutes.get("/logout", authValidator, va_logout_callback); // VA Callback for l vaRoutes.post("/refreshToken", va_refresh_token_callback); // VA Callback for refresh token vaRoutes.post("/register", va_register); // Register a new VA vaRoutes.post("/forgot-password", va_request_password_reset); +vaRoutes.post("/verify-va-email", va_verify_email); vaRoutes.get("/profile", authValidator, va_profile_callback); // Get VA Profile vaRoutes.patch("/profile", authValidator, va_profile_update_callback); // Update VA Profile diff --git a/views/va_client_register.ejs b/views/va_client_register.ejs index b9c7d30..3dc00f9 100644 --- a/views/va_client_register.ejs +++ b/views/va_client_register.ejs @@ -92,12 +92,18 @@

Create Your Account

+
- +
+
@@ -107,7 +113,7 @@
- +
@@ -115,6 +121,9 @@

Business Information

+
@@ -124,9 +133,12 @@
- +
+
@@ -141,13 +153,16 @@
- +
+
- +
@@ -155,6 +170,9 @@

Billing Address

+
@@ -174,9 +192,12 @@
- +
+
@@ -190,9 +211,12 @@
- +
+
@@ -206,7 +230,7 @@
- +
@@ -214,21 +238,30 @@

Authorized Signatory

+

We will use the details below in the agreement that we will send to your prior to entering the project.

- +
+
- +
+
@@ -238,7 +271,7 @@
- +
@@ -246,6 +279,9 @@

Payment Details

+

Credit or Debit Card

@@ -267,16 +303,22 @@
- +
+
- +
+
@@ -286,15 +328,21 @@
- +
+
- +
+
@@ -314,9 +362,12 @@
- +
+
diff --git a/views/va_register.ejs b/views/va_register.ejs index 498db86..e45d784 100644 --- a/views/va_register.ejs +++ b/views/va_register.ejs @@ -165,17 +165,20 @@

Account Setup

+
- +
- +
@@ -186,7 +189,7 @@
- +
@@ -194,7 +197,7 @@
- +
@@ -202,7 +205,9 @@

Personal Information

- + @@ -232,7 +237,7 @@
- +
@@ -274,7 +279,7 @@
- +
@@ -282,13 +287,19 @@

Contact Information

+
- +
+
@@ -300,7 +311,7 @@
- +
@@ -308,6 +319,9 @@

Payment Details

+
@@ -317,9 +331,12 @@
- +
+
@@ -330,9 +347,12 @@
- +
+
From c1cf57e0f41645f4e5bd29fc8857878b734dfb9b Mon Sep 17 00:00:00 2001 From: Aakash Date: Fri, 21 Nov 2025 12:56:14 +0530 Subject: [PATCH 2/6] added leave route --- .../va/vaLeaves/vaLeavesApply.controller.js | 42 ++++++++++++++++ src/models/vaModel.js | 49 ++++++++++++++++++- 2 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 src/controllers/va/vaLeaves/vaLeavesApply.controller.js diff --git a/src/controllers/va/vaLeaves/vaLeavesApply.controller.js b/src/controllers/va/vaLeaves/vaLeavesApply.controller.js new file mode 100644 index 0000000..6f4d28f --- /dev/null +++ b/src/controllers/va/vaLeaves/vaLeavesApply.controller.js @@ -0,0 +1,42 @@ +import jwt from "jsonwebtoken"; +import { VAModel, LeavesModel } from "../../../models/vaModel.js"; + +// Adjust missing hours by creating new records +async function va_leave_apply_function(req, res) { + try { + 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 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 { dateFrom, dateTo, days, leaveType, comment } = req.body; + + if (!dateFrom || !dateTo || !days || !leaveType || !comment) { + return res.status(400).json({ + success: false, + message: "dateFrom, dateTo, days, leaveType, and comment are required" + }); + } + + + + } catch (error) { + console.error("Leave apply error:", error); + return res.status(500).json({ success: false, message: "Internal Server Error" }); + } +} + +export { va_leave_apply_function }; \ No newline at end of file diff --git a/src/models/vaModel.js b/src/models/vaModel.js index 15dd444..820be1f 100644 --- a/src/models/vaModel.js +++ b/src/models/vaModel.js @@ -423,6 +423,52 @@ const adjustedHoursSchema = new Schema({ }] }, { timestamps: true }); +const leavesSchema = new Schema({ + vaId: { + type: Types.ObjectId, + ref: "VA", + required: true + }, + clientId: { + type: Types.ObjectId, + ref: "Client", + required: true + }, + dateFrom: { + type: Date, + required: true + }, + dateTo: { + type: Date, + required: true + }, + days: { + type: String, + required: true + }, + comment: { + type: String, + required: true + }, + leaveType: { + type: String, + enum: ["SICK", "PTO"], + default: "SICK", + required: true + }, + status: { + type: String, + enum: ["PENDING", "APPROVED", "REJECTED"], + default: "PENDING" + }, + reviewedAt: { + type: Date + }, + rejectionReason: { + type: String // If rejected, store the reason + } +}, { timestamps: true }); + /* ---------------------- Model Exports ---------------------- */ const VAModel = model("VA", vaSchema); const ClientModel = model("Client", clientSchema); @@ -431,5 +477,6 @@ const LoggedModel = model("TrackLog", loggedSchema); const WorkTrackModel = model("TrackWork", vaWorkTrackSchema); const PasswordResetModel = model("PasswordReset", passwordResetSchema); const AdjustedHoursModel = model("AdjustedHours", adjustedHoursSchema); +const LeavesModel = model("Leaves", leavesSchema); -export { VAModel, ClientModel, ProjectModel, LoggedModel, WorkTrackModel, PasswordResetModel, AdjustedHoursModel }; \ No newline at end of file +export { VAModel, ClientModel, ProjectModel, LoggedModel, WorkTrackModel, PasswordResetModel, AdjustedHoursModel, LeavesModel }; \ No newline at end of file From 5eef945f50fa4ca2b5eed4d636aad2a665edf359 Mon Sep 17 00:00:00 2001 From: Aakash Date: Fri, 21 Nov 2025 15:40:11 +0530 Subject: [PATCH 3/6] Added leave manage route --- .../va/vaAuth/vaLogin.controller.js | 46 +++- .../va/vaLeaves/vaLeaveList.controller.js | 101 +++++++++ .../va/vaLeaves/vaLeaveManage.controller.js | 114 ++++++++++ .../va/vaLeaves/vaLeavesApply.controller.js | 209 +++++++++++++++++- .../va/vaLog/vaLogTrackWork.controller.js | 51 ++++- .../va/vaProjects/vaProjectLog.controller.js | 116 ++++++---- .../vaTimesheetAdjustment.controller.js | 85 ++++++- src/models/vaModel.js | 3 +- src/routes/v1/va.routes.js | 13 +- 9 files changed, 669 insertions(+), 69 deletions(-) create mode 100644 src/controllers/va/vaLeaves/vaLeaveList.controller.js create mode 100644 src/controllers/va/vaLeaves/vaLeaveManage.controller.js diff --git a/src/controllers/va/vaAuth/vaLogin.controller.js b/src/controllers/va/vaAuth/vaLogin.controller.js index 08a173b..2bd329f 100644 --- a/src/controllers/va/vaAuth/vaLogin.controller.js +++ b/src/controllers/va/vaAuth/vaLogin.controller.js @@ -42,11 +42,49 @@ async function closePreviousSession(vaId, lastLog) { }); // console.log(`Created logout log: ${logoutLog._id}`); + try { + const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`; + const sfDataSync = { + "_id": logoutLog._id, + "loggedType": "LOGOUT", + "vaId": vaId, + "vaEmail": lastLog.vaEmail, + "projectId": lastLog.projectId, + "time": logoutLog.time, + } + + 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(sfDataSync) + }); + + 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); - return { - closedWorkTracks: openWorkTracks.length, - logoutLogId: logoutLog._id - }; + return { + closedWorkTracks: openWorkTracks.length, + logoutLogId: logoutLog._id, + SyncSalesforce: result + }; + } catch (error) { + console.error("Failed to sync with Salesforce:", error.message); + console.log(error) + + // Create a custom error to distinguish Salesforce errors + const salesforceError = new Error(`Salesforce sync failed: ${error.message}`); + salesforceError.isSalesforceError = true; + throw salesforceError; + } } catch (error) { console.error("Error closing previous session:", error); diff --git a/src/controllers/va/vaLeaves/vaLeaveList.controller.js b/src/controllers/va/vaLeaves/vaLeaveList.controller.js new file mode 100644 index 0000000..d79a9e1 --- /dev/null +++ b/src/controllers/va/vaLeaves/vaLeaveList.controller.js @@ -0,0 +1,101 @@ +import jwt from "jsonwebtoken"; +import { VAModel, LeavesModel } from "../../../models/vaModel.js"; + +// Get all leave lists applied by VA +async function va_leave_list_function(req, res) { + try { + 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 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; + + // Get parameters from route params (they are optional) + const { status, type } = req.params; + + // Get sorting from query params (optional) + const { + sortBy = 'createdAt', + sortOrder = 'desc' + } = req.query; + + // Build filter object + const filter = { vaId }; + + // Handle status filter from params + if (status && status !== 'ALL') { + filter.status = status.toUpperCase(); + } + + // Handle type filter from params (using 'type' instead of 'leaveType') + if (type && type !== 'ALL') { + filter.leaveType = type.toUpperCase(); + } + + // Validate sort parameters + const validSortFields = ['createdAt', 'dateFrom', 'dateTo', 'status', 'leaveType']; + const sortField = validSortFields.includes(sortBy) ? sortBy : 'createdAt'; + const sortDirection = sortOrder.toLowerCase() === 'asc' ? 1 : -1; + + // Get all leaves without pagination + const leaves = await LeavesModel.find(filter) + .sort({ [sortField]: sortDirection }) + .select('-__v') + .lean(); + + // Format the response data + const formattedLeaves = leaves.map(leave => ({ + leaveId: leave._id, + dateFrom: leave.dateFrom.toISOString().split('T')[0], + dateTo: leave.dateTo.toISOString().split('T')[0], + totalDays: parseInt(leave.days), + leaveType: leave.leaveType, + comment: leave.comment, + status: leave.status, + appliedOn: leave.createdAt, + reviewedAt: leave.reviewedAt || null, + rejectionReason: leave.rejectionReason || null + })); + + return res.status(200).json({ + success: true, + message: "Leave list retrieved successfully", + data: { + leaves: formattedLeaves, + totalCount: formattedLeaves.length + } + }); + + } catch (error) { + console.error("Leave list error:", error); + + if (error.name === 'JsonWebTokenError') { + return res.status(401).json({ success: false, message: "Invalid token" }); + } + + if (error.name === 'TokenExpiredError') { + return res.status(401).json({ success: false, message: "Token expired" }); + } + + return res.status(500).json({ + success: false, + message: "Internal Server Error", + error: error.message + }); + } +} + +export { va_leave_list_function }; \ No newline at end of file diff --git a/src/controllers/va/vaLeaves/vaLeaveManage.controller.js b/src/controllers/va/vaLeaves/vaLeaveManage.controller.js new file mode 100644 index 0000000..09bd226 --- /dev/null +++ b/src/controllers/va/vaLeaves/vaLeaveManage.controller.js @@ -0,0 +1,114 @@ +import jwt from "jsonwebtoken"; +import { VAModel, LeavesModel } from "../../../models/vaModel.js"; + +// Approve or reject leave +async function va_leave_manage_function(req, res) { + try { + + const { leaveId } = req.params; + const { action, rejectionReason } = req.body; + + if (!leaveId) { + return res.status(400).json({ + success: false, + message: "Leave ID is required" + }); + } + + if (!action || !['APPROVE', 'REJECT'].includes(action.toUpperCase())) { + return res.status(400).json({ + success: false, + message: "Action is required and must be either 'APPROVE' or 'REJECT'" + }); + } + + // If rejecting, rejectionReason is required + if (action.toUpperCase() === 'REJECT' && !rejectionReason) { + return res.status(400).json({ + success: false, + message: "Rejection reason is required when rejecting a leave" + }); + } + + // Find the leave request + const leaveRequest = await LeavesModel.findById(leaveId); + if (!leaveRequest) { + return res.status(404).json({ + success: false, + message: "Leave request not found" + }); + } + + // Check if leave is already processed + if (leaveRequest.status !== 'PENDING') { + return res.status(400).json({ + success: false, + message: `Leave request is already ${leaveRequest.status.toLowerCase()}` + }); + } + + // Prepare update data + const updateData = { + status: action.toUpperCase() === 'APPROVE' ? 'APPROVED' : 'REJECTED', + reviewedAt: new Date() + }; + + // Add rejection reason if rejecting + if (action.toUpperCase() === 'REJECT') { + updateData.rejectionReason = rejectionReason; + } + + // Update the leave request + const updatedLeave = await LeavesModel.findByIdAndUpdate( + leaveId, + updateData, + { new: true } // Return updated document + ).select('-__v').lean(); + + // Format the response + const formattedLeave = { + leaveId: updatedLeave._id, + dateFrom: updatedLeave.dateFrom.toISOString().split('T')[0], + dateTo: updatedLeave.dateTo.toISOString().split('T')[0], + totalDays: parseInt(updatedLeave.days), + leaveType: updatedLeave.leaveType, + comment: updatedLeave.comment, + status: updatedLeave.status, + appliedOn: updatedLeave.createdAt, + reviewedAt: updatedLeave.reviewedAt, + rejectionReason: updatedLeave.rejectionReason || null + }; + + return res.status(200).json({ + success: true, + message: `Leave request ${action.toLowerCase()}d successfully`, + data: formattedLeave + }); + + } catch (error) { + console.error("Leave approve/reject error:", error); + + if (error.name === 'JsonWebTokenError') { + return res.status(401).json({ success: false, message: "Invalid token" }); + } + + if (error.name === 'TokenExpiredError') { + return res.status(401).json({ success: false, message: "Token expired" }); + } + + if (error.name === 'CastError') { + return res.status(400).json({ + success: false, + message: "Invalid leave ID format" + }); + } + + return res.status(500).json({ + success: false, + message: "Internal Server Error", + error: error.message + }); + } +} + +export { va_leave_manage_function }; \ No newline at end of file diff --git a/src/controllers/va/vaLeaves/vaLeavesApply.controller.js b/src/controllers/va/vaLeaves/vaLeavesApply.controller.js index 6f4d28f..489a6e3 100644 --- a/src/controllers/va/vaLeaves/vaLeavesApply.controller.js +++ b/src/controllers/va/vaLeaves/vaLeavesApply.controller.js @@ -1,7 +1,7 @@ import jwt from "jsonwebtoken"; import { VAModel, LeavesModel } from "../../../models/vaModel.js"; -// Adjust missing hours by creating new records +// Apply for leave async function va_leave_apply_function(req, res) { try { const accessToken = req.headers.token; @@ -22,20 +22,219 @@ async function va_leave_apply_function(req, res) { } const vaId = vaUser._id; - const { dateFrom, dateTo, days, leaveType, comment } = req.body; + const { dateFrom, dateTo, leaveType, comment } = req.body; - if (!dateFrom || !dateTo || !days || !leaveType || !comment) { + if (!dateFrom || !dateTo || !leaveType || !comment) { return res.status(400).json({ success: false, - message: "dateFrom, dateTo, days, leaveType, and comment are required" + message: "dateFrom, dateTo, leaveType, and comment are required" }); } + // Validate date formats + let startDate, endDate; + try { + startDate = new Date(dateFrom); + endDate = new Date(dateTo); + + // Set to start of day for accurate calculation + startDate.setHours(0, 0, 0, 0); + endDate.setHours(0, 0, 0, 0); + + if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) { + return res.status(400).json({ + success: false, + message: "Invalid date format. Use valid date format (YYYY-MM-DD)" + }); + } + } catch (error) { + return res.status(400).json({ + success: false, + message: "Invalid date format" + }); + } + + // Ensure start date is before or equal to end date + if (startDate > endDate) { + return res.status(400).json({ + success: false, + message: "Start date must be before or equal to end date" + }); + } + + // Ensure dates are not in the past + const today = new Date(); + today.setHours(0, 0, 0, 0); + if (startDate < today) { + return res.status(400).json({ + success: false, + message: "Cannot apply for leave for past dates" + }); + } + + // Validate leave type + const validLeaveTypes = ["SICK", "PTO"]; + if (!validLeaveTypes.includes(leaveType)) { + return res.status(400).json({ + success: false, + message: "Invalid leave type. Must be SICK or PTO" + }); + } + + // Calculate total days (inclusive of both start and end dates) + const timeDifference = endDate.getTime() - startDate.getTime(); + const totalDays = Math.floor(timeDifference / (1000 * 60 * 60 * 24)) + 1; // +1 to include both start and end dates + + if (totalDays <= 0) { + return res.status(400).json({ + success: false, + message: "Invalid date range" + }); + } + + // Check for overlapping leave requests + const existingLeave = await LeavesModel.findOne({ + vaId, + status: "PENDING", + $or: [ + { + dateFrom: { $lte: endDate }, + dateTo: { $gte: startDate } + } + ] + }); + + if (existingLeave) { + return res.status(400).json({ + success: false, + message: "You already have a pending leave request that overlaps with these dates" + }); + } + + // Check for overlapping approved leaves + const existingApprovedLeave = await LeavesModel.findOne({ + vaId, + status: "APPROVED", + $or: [ + { + dateFrom: { $lte: endDate }, + dateTo: { $gte: startDate } + } + ] + }); + + if (existingApprovedLeave) { + return res.status(400).json({ + success: false, + message: "You already have an approved leave that overlaps with these dates" + }); + } + + // Create leave request + const leaveRequest = new LeavesModel({ + vaId, + dateFrom: startDate, + dateTo: endDate, + days: totalDays.toString(), + leaveType, + comment, + 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 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}`); + // } + + // 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", + // data: { + // adjustmentId: adjustmentRequest._id, + // date: adjustmentDate.toISOString().split('T')[0], + // loginTime: loginDateTime.toISOString(), + // logoutTime: logoutDateTime.toISOString(), + // adjustedHours: formatTime(adjustedSeconds), + // comment: comment, + // status: "PENDING" + // }, + // SyncSalesforce: result + // }); + // } catch (error) { + // console.error("Failed to sync with Salesforce:", error.message); + // console.log(error) + + // // Create a custom error to distinguish Salesforce errors + // const salesforceError = new Error(`Salesforce sync failed: ${error.message}`); + // salesforceError.isSalesforceError = true; + // throw salesforceError; + // } + + return res.status(201).json({ + success: true, + message: "Leave request submitted successfully", + data: { + leaveId: leaveRequest._id, + dateFrom: startDate.toISOString().split('T')[0], + dateTo: endDate.toISOString().split('T')[0], + totalDays: totalDays, + leaveType, + comment, + status: "PENDING" + } + }); } catch (error) { console.error("Leave apply error:", error); - return res.status(500).json({ success: false, message: "Internal Server Error" }); + + if (error.name === 'JsonWebTokenError') { + return res.status(401).json({ success: false, message: "Invalid token" }); + } + + if (error.name === 'TokenExpiredError') { + return res.status(401).json({ success: false, message: "Token expired" }); + } + + return res.status(500).json({ + success: false, + message: "Internal Server Error", + error: error.message + }); } } diff --git a/src/controllers/va/vaLog/vaLogTrackWork.controller.js b/src/controllers/va/vaLog/vaLogTrackWork.controller.js index 89366f6..ddf5d22 100644 --- a/src/controllers/va/vaLog/vaLogTrackWork.controller.js +++ b/src/controllers/va/vaLog/vaLogTrackWork.controller.js @@ -65,13 +65,52 @@ async function checkDailyHoursAndLogout(vaId, vaEmail, projectId, project, curre vaEmail, projectId }); + + try { + const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`; + const sfDataSync = { + "_id": logoutLog._id, + "loggedType": "LOGOUT", + "vaId": vaId, + "vaEmail": vaEmail, + "projectId": projectId, + "time": logoutLog.time, + } + + 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(sfDataSync) + }); + + 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); - return { - shouldLogout: true, - totalTodayHours: totalTodayHours, - dedicatedHours: project.dedicatedHours, - logoutLogId: logoutLog._id - }; + return { + shouldLogout: true, + totalTodayHours: totalTodayHours, + dedicatedHours: project.dedicatedHours, + logoutLogId: logoutLog._id, + SyncSalesforce: result + }; + } catch (error) { + console.error("Failed to sync with Salesforce:", error.message); + console.log(error) + + // Create a custom error to distinguish Salesforce errors + const salesforceError = new Error(`Salesforce sync failed: ${error.message}`); + salesforceError.isSalesforceError = true; + throw salesforceError; + } } return { diff --git a/src/controllers/va/vaProjects/vaProjectLog.controller.js b/src/controllers/va/vaProjects/vaProjectLog.controller.js index a06daaa..1fcc9c6 100644 --- a/src/controllers/va/vaProjects/vaProjectLog.controller.js +++ b/src/controllers/va/vaProjects/vaProjectLog.controller.js @@ -66,11 +66,49 @@ async function closePreviousSession(vaId, lastLog) { }); // console.log(`Created logout log: ${logoutLog._id}`); + try { + const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`; + const sfDataSync = { + "_id": logoutLog._id, + "loggedType": "LOGOUT", + "vaId": vaId, + "vaEmail": lastLog.vaEmail, + "projectId": lastLog.projectId, + "time": logoutLog.time, + } + + 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(sfDataSync) + }); + + 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); - return { - closedWorkTracks: openWorkTracks.length, - logoutLogId: logoutLog._id - }; + return { + closedWorkTracks: openWorkTracks.length, + logoutLogId: logoutLog._id, + SyncSalesforce: result + }; + } catch (error) { + console.error("Failed to sync with Salesforce:", error.message); + console.log(error) + + // Create a custom error to distinguish Salesforce errors + const salesforceError = new Error(`Salesforce sync failed: ${error.message}`); + salesforceError.isSalesforceError = true; + throw salesforceError; + } } catch (error) { console.error("Error closing previous session:", error); @@ -151,47 +189,47 @@ async function va_project_log_callback(req, res) { const createdLog = await LoggedModel.create({ loggedType: type, vaId, vaEmail, projectId }) // console.log(createdLog) - try { - const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`; - const sfData = { - "_id": createdLog._id, - "loggedType": type, - "vaId": vaId, - "vaEmail": vaEmail, - "projectId": projectId, - "time": createdLog.time, - } + // try { + // const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`; + // const sfData = { + // "_id": createdLog._id, + // "loggedType": type, + // "vaId": vaId, + // "vaEmail": vaEmail, + // "projectId": projectId, + // "time": createdLog.time, + // } - 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(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(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); - res.status(201).json({ success: true, message: `${type} Successfully`, logId: createdLog._id, SyncSalesforce: result }); - } catch (error) { - console.error("Failed to sync with Salesforce:", error.message); - console.log(error) + // res.status(201).json({ success: true, message: `${type} Successfully`, logId: createdLog._id, SyncSalesforce: result }); + // } catch (error) { + // console.error("Failed to sync with Salesforce:", error.message); + // console.log(error) - // Create a custom error to distinguish Salesforce errors - const salesforceError = new Error(`Salesforce sync failed: ${error.message}`); - salesforceError.isSalesforceError = true; - throw salesforceError; - } + // // Create a custom error to distinguish Salesforce errors + // const salesforceError = new Error(`Salesforce sync failed: ${error.message}`); + // salesforceError.isSalesforceError = true; + // throw salesforceError; + // } - // return res.status(201).send({ success: true, message: `${type} Successfully`, logId: createdLog._id }); + return res.status(201).send({ success: true, message: `${type} Successfully`, logId: createdLog._id }); } catch (error) { return res.status(500).send("Internal Server Error"); diff --git a/src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustment.controller.js b/src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustment.controller.js index d659168..6931442 100644 --- a/src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustment.controller.js +++ b/src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustment.controller.js @@ -140,19 +140,80 @@ async function va_timesheet_adjustment_function(req, res) { return `${h}h ${m}m ${s}s`; }; - return res.status(201).json({ - success: true, - message: "Adjustment request submitted successfully", - data: { - adjustmentId: adjustmentRequest._id, - date: adjustmentDate.toISOString().split('T')[0], - loginTime: loginDateTime.toISOString(), - logoutTime: logoutDateTime.toISOString(), - adjustedHours: formatTime(adjustedSeconds), - comment: comment, - status: "PENDING" + 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 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}`); + } + + 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", + data: { + adjustmentId: adjustmentRequest._id, + date: adjustmentDate.toISOString().split('T')[0], + loginTime: loginDateTime.toISOString(), + logoutTime: logoutDateTime.toISOString(), + adjustedHours: formatTime(adjustedSeconds), + comment: comment, + status: "PENDING" + }, + SyncSalesforce: result + }); + } catch (error) { + console.error("Failed to sync with Salesforce:", error.message); + console.log(error) + + // Create a custom error to distinguish Salesforce errors + const salesforceError = new Error(`Salesforce sync failed: ${error.message}`); + salesforceError.isSalesforceError = true; + throw salesforceError; + } + + // return res.status(201).json({ + // success: true, + // message: "Adjustment request submitted successfully", + // data: { + // adjustmentId: adjustmentRequest._id, + // date: adjustmentDate.toISOString().split('T')[0], + // loginTime: loginDateTime.toISOString(), + // logoutTime: logoutDateTime.toISOString(), + // adjustedHours: formatTime(adjustedSeconds), + // comment: comment, + // status: "PENDING" + // } + // }); } catch (error) { console.error("Timesheet adjustment error:", error); diff --git a/src/models/vaModel.js b/src/models/vaModel.js index dc2f6e9..b33241f 100644 --- a/src/models/vaModel.js +++ b/src/models/vaModel.js @@ -432,8 +432,7 @@ const leavesSchema = new Schema({ }, clientId: { type: Types.ObjectId, - ref: "Client", - required: true + ref: "Client" }, dateFrom: { type: Date, diff --git a/src/routes/v1/va.routes.js b/src/routes/v1/va.routes.js index 0686e9d..087e684 100644 --- a/src/routes/v1/va.routes.js +++ b/src/routes/v1/va.routes.js @@ -23,6 +23,9 @@ import { va_timesheet_adjustment_function } from "../../controllers/va/vaTimeshe import { get_adjustment_requests_list } from "../../controllers/va/vaTimesheetAdjustment/vaAllTimesheetAdjustmentReuqest.controller.js" import { va_timesheet_manage_adjustment_request } from "../../controllers/va/vaTimesheetAdjustment/vaManageTimesheetAdjustment.controller.js" import { va_verify_email } from "../../controllers/va/vaAuth/vaVerifyEmail.controller.js"; +import { va_leave_apply_function } from "../../controllers/va/vaLeaves/vaLeavesApply.controller.js"; +import { va_leave_list_function } from "../../controllers/va/vaLeaves/vaLeaveList.controller.js" +import { va_leave_manage_function } from "../../controllers/va/vaLeaves/vaLeaveManage.controller.js"; vaRoutes.get("/", va_callback); // VA Callback for getting active VA @@ -43,9 +46,17 @@ vaRoutes.get("/:userName", va_username_callback); // 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.post("/timesheet/adjustment/request", authValidator, va_timesheet_adjustment_function); // Timesheet Adjustment for VA -vaRoutes.get("/timesheet/adjustments/list/:status?/:projectId?/:vaId?", get_adjustment_requests_list); // show all adjustment list to approve and reject +// vaRoutes.get("/timesheet/adjustments/list/:status?/:projectId?/:vaId?", get_adjustment_requests_list); // show all adjustment list to approve and reject vaRoutes.patch("/timesheet/adjustments/:adjustmentId", va_timesheet_manage_adjustment_request); +// Leaves routes +vaRoutes.get("/leaves/all", authValidator, va_leave_list_function); // Get all leaves +vaRoutes.get("/leaves/status/:status", authValidator, va_leave_list_function); // Leaves Filter by status +vaRoutes.get("/leaves/type/:leaveType", authValidator, va_leave_list_function); // Filter by leave type +vaRoutes.get("/leaves/status/:status/type/:leaveType", authValidator, va_leave_list_function); // leaves Filter +vaRoutes.post("/leave/request", authValidator, va_leave_apply_function); // Timesheet Adjustment for VA +vaRoutes.patch("/leave/:leaveId", va_leave_manage_function); + // timesheet adjustment routes // Example: GET /va/search?name=john&email=@example.com&from=2025-01-01&to=2025-01-31 (NC) From dc085787a341d1d36ef81909b46867442f454186 Mon Sep 17 00:00:00 2001 From: Aakash Date: Fri, 21 Nov 2025 16:38:31 +0530 Subject: [PATCH 4/6] Fixed issues and bugs --- public/css/dashboard.css | 20 ++++- .../va/vaAuth/vaLogin.controller.js | 77 ++++++++++--------- src/helper/updateEntityRelationships.js | 73 ++++++++++++++---- views/client/dashboard-dashboard.ejs | 11 ++- 4 files changed, 128 insertions(+), 53 deletions(-) diff --git a/public/css/dashboard.css b/public/css/dashboard.css index 4bfd24f..c56f08c 100644 --- a/public/css/dashboard.css +++ b/public/css/dashboard.css @@ -80,7 +80,6 @@ body { padding: 0 1.5rem; gap: 1rem; width: 100%; - max-width: 1024px; flex-direction: row; align-items: center; justify-content: space-between; @@ -188,7 +187,10 @@ body { width: 16rem; background-color: var(--bg-card); border-right: 1px solid var(--border-color); - display: none; + /* display: none; */ +} +.dashboard-sidebar.active { + display: block; } @media (min-width: 768px) { @@ -196,6 +198,20 @@ body { display: block; } } +@media (max-width: 768px) { + .dashboard-sidebar { + position: absolute; + left: 0; + bottom: 0; + z-index: 99; + top: 0; + transform: translateX(-100%); + transition: .3s ease; + } + .dashboard-sidebar.active { + transform: translateX(0); + } +} .sidebar-header { padding: 1.5rem; diff --git a/src/controllers/va/vaAuth/vaLogin.controller.js b/src/controllers/va/vaAuth/vaLogin.controller.js index 2bd329f..7e00699 100644 --- a/src/controllers/va/vaAuth/vaLogin.controller.js +++ b/src/controllers/va/vaAuth/vaLogin.controller.js @@ -42,49 +42,54 @@ async function closePreviousSession(vaId, lastLog) { }); // console.log(`Created logout log: ${logoutLog._id}`); - try { - const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`; - const sfDataSync = { - "_id": logoutLog._id, - "loggedType": "LOGOUT", - "vaId": vaId, - "vaEmail": lastLog.vaEmail, - "projectId": lastLog.projectId, - "time": logoutLog.time, - } + // try { + // const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`; + // const sfDataSync = { + // "_id": logoutLog._id, + // "loggedType": "LOGOUT", + // "vaId": vaId, + // "vaEmail": lastLog.vaEmail, + // "projectId": lastLog.projectId, + // "time": logoutLog.time, + // } - 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(sfDataSync) - }); + // 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(sfDataSync) + // }); - 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 { + // return { + // closedWorkTracks: openWorkTracks.length, + // logoutLogId: logoutLog._id, + // SyncSalesforce: result + // }; + // } catch (error) { + // console.error("Failed to sync with Salesforce:", error.message); + // console.log(error) + + // // Create a custom error to distinguish Salesforce errors + // const salesforceError = new Error(`Salesforce sync failed: ${error.message}`); + // salesforceError.isSalesforceError = true; + // throw salesforceError; + // } + + return { closedWorkTracks: openWorkTracks.length, logoutLogId: logoutLog._id, - SyncSalesforce: result }; - } catch (error) { - console.error("Failed to sync with Salesforce:", error.message); - console.log(error) - - // Create a custom error to distinguish Salesforce errors - const salesforceError = new Error(`Salesforce sync failed: ${error.message}`); - salesforceError.isSalesforceError = true; - throw salesforceError; - } } catch (error) { console.error("Error closing previous session:", error); diff --git a/src/helper/updateEntityRelationships.js b/src/helper/updateEntityRelationships.js index a689f87..2de29d3 100644 --- a/src/helper/updateEntityRelationships.js +++ b/src/helper/updateEntityRelationships.js @@ -46,17 +46,39 @@ async function updateEntityRelationships(data) { results.vaUpdate = `Updated ${vaUpdateResult.modifiedCount} VAs`; } - // Update Project with clientId and vaId + // // Update Project with clientId and vaId + // const projectUpdateData = {}; + // if (clientId) { + // projectUpdateData.clientId = clientId; + // } + + // // 🐞🐞🐞 Here Duplicate Entry of Va is Created if SF pass same va multiple times in same req + // if (vaId && Array.isArray(vaId)) { + // projectUpdateData.vaAssigned = vaId; + // } + + // if (Object.keys(projectUpdateData).length > 0) { + // const projectUpdate = await ProjectModel.findByIdAndUpdate( + // projectId, + // projectUpdateData, + // { new: true } + // ); + // results.projectUpdate = projectUpdate ? 'Project updated successfully' : 'Project not found'; + // } + + if (vaId && Array.isArray(vaId)) { + await ProjectModel.findByIdAndUpdate( + projectId, + { $addToSet: { vaAssigned: { $each: vaId } } } + ); + } + + // Update other fields normally const projectUpdateData = {}; if (clientId) { projectUpdateData.clientId = clientId; } - // 🐞🐞🐞 Here Duplicate Entry of Va is Created if SF pass same va multiple times in same req - if (vaId && Array.isArray(vaId)) { - projectUpdateData.vaAssigned = vaId; - } - if (Object.keys(projectUpdateData).length > 0) { const projectUpdate = await ProjectModel.findByIdAndUpdate( projectId, @@ -215,14 +237,37 @@ async function removeEntityRelationships(data) { // Remove projectId from VAs if (vaId && Array.isArray(vaId)) { - await VAModel.updateMany( - { _id: { $in: vaId } }, - { $pull: { - projectIds: projectId, - // 🐞🐞🐞 Here before removing we need to check if VA is still assigned to other projects of the same client - clientIds: clientId - }} - ); + // await VAModel.updateMany( + // { _id: { $in: vaId } }, + // { $pull: { + // projectIds: projectId, + // // 🐞🐞🐞 Here before removing we need to check if VA is still assigned to other projects of the same client + // clientIds: clientId + // }} + // ); + for (const va of vaId) { + // Check if VA is still assigned to other projects of the same client + const otherProjectsWithSameClient = await ProjectModel.countDocuments({ + _id: { $ne: projectId }, + clientId: clientId, + vaAssigned: va + }); + + // Prepare update operation + const updateOperation = { + $pull: { projectIds: projectId } + }; + + // Only remove clientId if VA is not assigned to any other projects from this client + if (!otherProjectsWithSameClient) { + updateOperation.$pull = { + ...updateOperation.$pull, + clientIds: clientId + }; + } + + await VAModel.findByIdAndUpdate(va, updateOperation); + } } results.projectRemoval = 'Project relationships removed successfully'; diff --git a/views/client/dashboard-dashboard.ejs b/views/client/dashboard-dashboard.ejs index 091dc6b..640438c 100644 --- a/views/client/dashboard-dashboard.ejs +++ b/views/client/dashboard-dashboard.ejs @@ -256,7 +256,7 @@
-
${va.firstName} ${va.lastName}
+
${va.name}
${va.email || 'N/A'}
@@ -350,6 +350,15 @@ // Optional: Auto-refresh data every 30 seconds setInterval(loadDashboardData, 30000); + + + document.addEventListener('DOMContentLoaded', () => { + var menuToggle = document.querySelector('.mobile-menu-button'); + var sidebar = document.querySelector('.dashboard-sidebar'); + menuToggle.addEventListener('click', () => { + sidebar.classList.toggle('active') + }) + }) From dea2a2985e21e20cc4410d300b14902e6de789a0 Mon Sep 17 00:00:00 2001 From: Aakash Date: Fri, 21 Nov 2025 18:13:22 +0530 Subject: [PATCH 5/6] Fixed issues and bugs --- public/js/va-client-register.js | 62 +++++++++++++++++++++++++++ public/js/va-register.js | 8 ++-- src/services/passwordReset.service.js | 4 +- views/forgot_password.ejs | 18 ++++++-- 4 files changed, 83 insertions(+), 9 deletions(-) diff --git a/public/js/va-client-register.js b/public/js/va-client-register.js index 44debf4..3fa242e 100644 --- a/public/js/va-client-register.js +++ b/public/js/va-client-register.js @@ -495,6 +495,15 @@ async function validateStep(step) { if (!validateEmail(email) && !companyName) { isValid = false; showError('Both Fields are Required', null, step); + let requiredFields1 = form.querySelectorAll('[required]'); + requiredFields1.forEach(field => { + if (!field.value.trim()) { + isValid = false; + field.classList.add('field-error'); + } else if (field.id !== 'email') { + field.classList.remove('field-error'); + } + }); return isValid; } if (!validateEmail(email)) { @@ -856,6 +865,8 @@ document.addEventListener('DOMContentLoaded', function() { const cardNumberInput = document.getElementById('card-number'); if (cardNumberInput) { cardNumberInput.addEventListener('input', function(e) { + const cursorPosition = e.target.selectionStart; + const originalLength = e.target.value.length; // Format card number with spaces let value = e.target.value.replace(/\s/g, ''); let formattedValue = ''; @@ -868,6 +879,39 @@ document.addEventListener('DOMContentLoaded', function() { } e.target.value = formattedValue; + + let newCursorPosition = cursorPosition; + + if (formattedValue.length > originalLength) { + const addedChars = formattedValue.length - originalLength; + + // Count how many spaces were added before the cursor position + let spacesAddedBeforeCursor = 0; + for (let i = 0; i < cursorPosition && i < formattedValue.length; i++) { + if (formattedValue[i] === ' ' && (i < cursorPosition || + (i === cursorPosition && formattedValue[i] === ' '))) { + spacesAddedBeforeCursor++; + } + } + + newCursorPosition = cursorPosition + spacesAddedBeforeCursor; + } else if (formattedValue.length < originalLength) { + const removedChars = originalLength - formattedValue.length; + + // Count how many spaces were removed before the cursor position + let spacesRemovedBeforeCursor = 0; + const oldValue = e.target.getAttribute('data-old-value') || ''; + for (let i = 0; i < cursorPosition && i < oldValue.length; i++) { + if (oldValue[i] === ' ' && i < cursorPosition) { + spacesRemovedBeforeCursor++; + } + } + + newCursorPosition = Math.max(0, cursorPosition - spacesRemovedBeforeCursor); + } + + e.target.setAttribute('data-old-value', formattedValue); + e.target.setSelectionRange(newCursorPosition, newCursorPosition); // Real-time card type detection const cardType = detectCardType(value); @@ -876,6 +920,24 @@ document.addEventListener('DOMContentLoaded', function() { console.log('Card type:', cardType); } }); + + cardNumberInput.addEventListener('keydown', function(e) { + // Allow: backspace, delete, tab, escape, enter, arrows, home, end + if ([8, 9, 13, 27, 37, 38, 39, 40, 46].includes(e.keyCode) || + // Allow: Ctrl+A, Ctrl+C, Ctrl+V, Ctrl+X + (e.keyCode === 65 && e.ctrlKey === true) || + (e.keyCode === 67 && e.ctrlKey === true) || + (e.keyCode === 86 && e.ctrlKey === true) || + (e.keyCode === 88 && e.ctrlKey === true) || + // Allow: numbers and numpad numbers + (e.keyCode >= 48 && e.keyCode <= 57) || + (e.keyCode >= 96 && e.keyCode <= 105)) { + return; + } + + // Prevent all other keys + e.preventDefault(); + }); } // Real-time expiry date formatting diff --git a/public/js/va-register.js b/public/js/va-register.js index 8b2731c..32928e0 100644 --- a/public/js/va-register.js +++ b/public/js/va-register.js @@ -393,7 +393,7 @@ // Age validation const age = document.getElementById('age').value; if (age && (age < 18 || age > 100)) { - showError('You must be 18 years or older to register', 'dob', step); + showError('You age must be 18 years or older to register', 'dob', step); isValid = false; } @@ -741,10 +741,12 @@ // Validate age if (age < 18) { - alert('You must be 18 years or older to register'); + // alert('You must be 18 years or older to register'); + showError('You Age must be 18 years or older to register', 'dob',); ageInput.style.border = '1px solid #ef4444'; } else if (age > 100) { - alert('Please enter a valid age'); + // alert('Please enter a valid age'); + showError('Please enter a valid age', 'dob'); ageInput.style.border = '1px solid #ef4444'; } else { ageInput.style.border = ''; diff --git a/src/services/passwordReset.service.js b/src/services/passwordReset.service.js index 8f0b6fa..f435c4f 100644 --- a/src/services/passwordReset.service.js +++ b/src/services/passwordReset.service.js @@ -19,8 +19,8 @@ class PasswordResetService { if (!user) { logger.info(`Password reset requested for non-existent ${userType}: ${email}`); return { - success: true, - message: "If the email exists, a reset link has been sent" + success: false, + message: "Email not recognized. Please sign up to continue." }; } diff --git a/views/forgot_password.ejs b/views/forgot_password.ejs index e769fbd..5c39933 100644 --- a/views/forgot_password.ejs +++ b/views/forgot_password.ejs @@ -127,6 +127,11 @@
+ + \ No newline at end of file diff --git a/views/client/dashboard-virtual-assistants.ejs b/views/client/dashboard-virtual-assistants.ejs index 2d6610a..cd5fbf6 100644 --- a/views/client/dashboard-virtual-assistants.ejs +++ b/views/client/dashboard-virtual-assistants.ejs @@ -29,12 +29,12 @@ Virtual Assistants -